EMS 知识库
首页 / knowledge/代码解析/data/resume_data/flash

flash_data_store.c

源文件路径:data/resume_data/flash/flash_data_store.c 所属模块:data(数据管理) / resume_data / flash —— 运行数据断电续传存储与导出 代码行数:2995

1. 文件职责

本文件实现 EMS 的"运行数据历史落盘 + 云端断点续传补报"完整闭环。它每 60 秒把电网、逆变器、电池、系统状态、能量统计、调度等一百多个实时量打包成定长 400 字节的 ParaTab 记录,按环形地址连续写入 SPI Flash 的主运行数据区(0x209000~0xF62FFF),同时对 32 MB/64 MB 器件并行写入一条 398 字节的扩展记录 ParaTab2。云端通过 "Resume" 特殊指令下发"起始时间 + 时长(分钟)",本文件就用二分查找在 Flash 中定位起止记录,把区间内的记录转成 CSV 文本写入 SDRAM 缓冲 resume_buff_sdram,由网络任务分包上报。数据流位置是"采集完成 → 本地持久化 → 历史补报"的末端环节,由 data/data.c 中的 uCOS-II 任务 task_system_data 周期驱动(data.c:258 store_data_init()、data.c:296 resume_data_handle()),网络侧由 net/ems.c 与 net/server.c 协同完成握手。写指针本身也做三扇区冗余备份,上电时三取二恢复,保证掉电后写入位置不丢。

2. 关键数据结构与宏

2.1 本文件定义/拥有的数据对象

名称类型说明
FlashDataAddrInfostatic FlashDataAddr唯一的写指针状态:RunDataStoreCurAddr 指向下一条待写入记录的首地址
s_t_resume_datastatic ParaTab(400 B)主记录打包缓冲,store_run_table1_data() 逐字段填充后整体写 Flash
s_t_resume_data2static ParaTab2(398 B)扩展记录打包缓冲,仅 Flash_ReadDensity() > 16 时使用
s_p_search_data / s_p_search_data2static ParaTab* / ParaTab2*读回缓冲的解析指针,分别指向 s_u8_data_buf / s_u8_data_buf2 内部
s_u8_data_buf / s_u8_data_buf2static u8[FLASH_SECTOR_SIZE](各 4096 B)Flash 扇区级读写缓冲,通过 __attribute__((at(SDRAM_FLASH_DATA_STOE_BUF_ADDR))) 绝对定位在 SDRAM(flash_data_store.c:40-41)
resume_buff_sdramu8[SDRAM_5M](5 MB,非 static)上报报文缓冲,绝对定位 SDRAM_RESUME_START_ADDR,实际每包只用前约 5 KB
g_u32_resume_data_lenu32(非 static)上报报文长度游标;起始置 7(=协议头长度),每拼一行 CSV 递增
g_resume_infoRESUME_BUFFER_T[10](非 static)断点续传指令槽,由 net/server.c 解析 "Resume" 指令后写入,本文件消费并清空
s_s8_csv_title_bufstatic char[1800]CSV 表头行(一次构建,全生命周期复用)
s_s8_csv_data_bufstatic char[1024]单条记录的 CSV 数据行
s_s8_csv_data_buf_tempstatic char[1024]上一包"剩余半行"暂存,用于跨包续拼
s_u8_csv_buffer_flagstatic u8有半行数据被暂存到 temp 时置 TRUE
s_u8_read_search_data_okstatic u8读回 + CRC 校验成功标志(本模块实际的"成功"判据,而不是 read_search_data 的返回值)
s_u32_resume_lengthstatic u32双重语义:先是被查区间的记录条数(由分钟数换算),打包过程中被逐包递减计数
s_u32_read_addrstatic u32跨包续读的 Flash 地址游标
udwSendCsvDataAddrStart / ...Endu32(非 static,未在头文件导出)二分查找到的起止记录地址;0 表示未找到
search_time_start / search_time_endstruct RTC_DateTime(非 static,未导出)补报区间起点与终点(end = start + length 分钟,见 get_end_time)
s_store_resume_tickstatic time_t上次落盘的时间戳,用于 60 s 节流
s_as8ResumeCmdIdstatic s8[32]断点续传指令 ID 字符串,最多写 16 字节

2.2 使用到的外部宏/常量

名称来源说明
RESUME_DATA_HEAD1/HEAD2(0x55/0xAA)、RESUME_END_CODE(0xB400)flash_data_store.h记录起始码与版本尾码
RUN_DATA_CUR_ADDR_BACKUP1/2/3、FLASH_DTATA_RUN_START/END、FLASH_DTATA2_*、MAX_SEARCH_CYCLEflash_data_store.hFlash 分区地址与二分查找上限
FLASH_SECTOR_SIZE(4096)、FLASH_ADDR_TO_SECTOR(n)、FLASH_OK(=1)、FLASH_ERR(=0)hardware/flash/flash.h:27、35、107-109扇区粒度与 Flash 驱动返回码
EMS(0x01)、REQUEST_COMM(0x01)、CMD_RESUME_INTERFACE(0x0C)net/ems.h:75、88、104上报报文头三字节
RESUME_STS_NONE/Idle/Response/Sending/Overnet/server.h:206-213断点续传状态机枚举
POSITIVE_VALUE_OFFSET(32000)、FORCE_SOC_MULT(4)logic/dispatch.h:16、18CSV 导出时的功率/SOC 反算系数
TIME_BASE_YEAR(1900)、NOW_BASE_YEAR(2000)midware/auxiliary/system_aux.h:13-14tm 年份与 RTC_DateTime.bYears 的换算基准
SYSTEM_BASE_PARA_PAGEdata/system_config/system_config.h保存任务页面号,用于掉电保存 u8_flash_renewal_flag

3. 函数清单

函数名作用域说明
packet_head全局(未在 .h 声明)从指定地址读 400 B,搜索 AA 55 起始码,输出其在缓冲内的偏移 *position
packet_end全局(未在 .h 声明)读 400 B,检查偏移 396/397 是否为 00 B4 且 CRC16 正确,判定该记录是否完整
verify_end_codestatic判断上电位置处是否存在"有头无尾"的残包,返回 TRUE 表示需要擦除整片运行数据
store_data_addr_initstatic读三份写指针备份扇区、三取二表决并做区间合法性检查,恢复 FlashDataAddrInfo
store_search_title_pack_csvstatic用 GET_VARIABLE_NAME 批量拼接 CSV 表头行到 s_s8_csv_title_buf
store_data_init全局模块初始化:重试 store_data_addr_init 最多 3 次、构建表头、初始化落盘节流时间戳
store_data_addrstatic把当前写指针追加写入三个备份扇区,返回写成功扇区数是否 ≥2
store_run_table1_datastatic采集百余项实时数据填入 ParaTab,计算 CRC 后按环形地址写入主区
store_run_table2_datastatic填充 ParaTab2(仅 ErrBackupBox 与时间),写入扩展区同一逻辑地址 + 固定偏移处
store_run_datastatic落盘总调度:table1 → (density>16 时)table2 → 写指针备份
show_search_data全局把 s_p_search_data 指向的 ParaTab 全部字段以十六进制 printf(调试接口)
judge_next_sector_already_writestatic判断 addr 所在扇区的下一个扇区是否已有数据,用于识别"写指针是否已绕圈"
judge_search_data_timestatic读一条记录的时间戳并与目标时间比较,返回 0(小于)/1(大于)/2(落在 ±180 s 窗口内)
store_search_sector_first_head_addrstatic在一个 4 KB 扇区内找第一条 AA 55 记录的首地址
store_bin_search_data_addrstatic在 Flash 运行区按定长步长做二分查找,定位与目标时间匹配的记录地址
read_search_datastatic读一条 ParaTab(含跨区拆读),输出起始码偏移并做 CRC 校验,设置 s_u8_read_search_data_ok
read_search_data2static读一条 ParaTab2(含跨区拆读)并做 CRC 校验,设置 s_p_search_data2
ParaTabToArray全局(未在 .h 声明、无任何调用者)只拷贝了 u16_head、Time 和第一个功率字段就结束,是残缺的遗留辅助函数
store_search_data_pack_csvstatic把 s_p_search_data(+s_p_search_data2)格式化成一行 CSV 写入 s_s8_csv_data_buf
Pack_resume_data_buf_head_endstatic为 resume_buff_sdram 补上 7 字节协议头和 2 字节 CRC16,并修正总长度
Pack_search_data_to_csvstatic循环读记录、转 CSV、往 resume_buff_sdram 拼包,满 5 KB 或读完即置 RESUME_STS_Sending 返回
get_end_timestatic由起点 + s_u32_resume_length 分钟算出补报区间终点 search_time_end
get_start_time_addrstatic以起点时间为基准最多 5 次(每次 +60 s)二分查找起始记录地址
get_end_time_addrstatic以终点时间为基准最多 5 次(每次 −60 s)二分查找结束记录地址
send_search_datastatic断点续传发送主状态机:首次查地址换算出总条数,之后逐包打包并推进状态
Get_Resume_Status_Current全局读断点续传状态(转发 g_stEMSInfo.u8ResumeStatusCurr)
Set_Resume_Status_Current全局写断点续传状态
resume_data_handle全局模块主循环入口:升级互斥、首启版本校验擦除、60 s 落盘、断点指令轮询与派发
get_resume_cmd_id_code全局返回 s_as8ResumeCmdId 指针
set_resume_cmd_id_code全局拷贝云端下发的指令 ID(len > 16 或指针为空则丢弃)
erase_run_data全局擦除备份扇区到运行区末尾、必要时擦除扩展区,并重写初始写指针
get_resume_index_length全局返回 10 个断点指令槽中是否还有非空槽(TRUE/FALSE)

4. 核心逻辑解析

4.1 记录格式与 Ring Buffer 布局

ParaTab 在 #pragma pack(1) 下恰好 400 字节,字段偏移与 packet_end() 里硬编码的 396/397 完全吻合:

偏移字段长度
0u16_head(=0x55AA,小端存储为 AA 55)2
2time_t Time(RTC 秒计数)4
6电网数据 6 项(s32)24
30逆变器数据 33 项(u16/s16/s32/u8/s8)70
100电池数据 51 项(基础 15 项 + 18 路电流 + 18 路 SOC)89
189系统状态 7 项(状态字、EMS 故障码、u64 故障/告警位图 ×4、电表故障码)41
230能量统计 19 项(u32)76
306调度 4 项 + energy_gc_charge + 动力线温度 2 项19
325u8_reserve[71]71
396u16_endcode = RESUME_END_CODE(字节 00 B4)2
398u16_crc_code(CRC16/Modbus,覆盖 0~397)2

ParaTab2 为 398 字节:u16_head(0) + Time(2) + u32_bb_error_code(6) + u8_reserve[386](10) + u16_crc_code(396)。

写入地址按"只增不减"推进:

  1. FlashDataAddrInfo.RunDataStoreCurAddr 从 FLASH_DTATA_RUN_START(0x209000) 开始,每次 += 400;
  2. 当 RunDataStoreCurAddr + 400 > FLASH_DTATA_RUN_END 时拆包——先写 secremain = FLASH_SECTOR_SIZE - addr % FLASH_SECTOR_SIZE 字节到尾部,剩余部分写到 FLASH_DTATA_RUN_START(flash_data_store.c:1073-1084),随后 RunDataStoreCurAddr = FLASH_DTATA_RUN_START + 余量,即完成一次绕圈;
  3. 扩展区使用同一逻辑地址 u32_StoreAddr + FLASH_DTATA2_RUN_OFFSET(flash_data_store.c:1142),保证两张表在时间维度严格对齐。

区间容量:(0xF62FFF - 0x209000 + 1)/400 ≈ 34990 条,按 1 条/分钟约可保存 24 天,之后开始覆盖最旧数据。

4.2 写指针三副本与上电恢复

store_data_addr()(flash_data_store.c:525)依次处理 BACKUP1/2/3 三个扇区,每个扇区的处理方式一致:

store_data_addr_init()(flash_data_store.c:256)是对称的反过程:读三份末槽,解码字节序 buf[i-4+3..i-4+0] 为大端地址,然后三取二表决(flash_data_store.c:376-383),三者互不相同则回退 FLASH_DTATA_RUN_START,最后再做一次 [FLASH_DTATA_RUN_START, FLASH_DTATA_RUN_END] 闭区间检查(flash_data_store.c:386-389)。

4.3 上电版本/残包校验与整片擦除

verify_end_code()(flash_data_store.c:215)结合 judge_next_sector_already_write() 判断"当前写指针是否已经绕过圈",分出两条路径:

返回值语义:TRUE = 残包/版本不匹配 → 需要擦除;FALSE = 数据完好。resume_data_handle() 在升级完成后的首次运行中(g_t_system_base_info.u8_flash_renewal_flag == FALSE)调用它,若为 TRUE 则调 resume_flash_reset(),该函数只是把 data/data.c 里的 s_u8_ems_erase_run_data_flag 置 TRUE,由 erase_flash_handle() 延迟 10 s 后真正执行 erase_run_data()(data.c:171-177),避免在采集任务运行中长时间独占 Flash。

packet_end() 的校验逻辑是:偏移 396=0x00、397=0xB4(RESUME_END_CODE 的小端表示),再 memcpy 成 ParaTab 做 CRC16 比对(flash_data_store.c:198-205)。RESUME_END_CODE 起到"数据布局版本号"的作用——字段改版(如新增/删除字段导致 CRC 覆盖区变化)后旧记录无法通过校验,自然触发整片擦除重建。

4.4 CSV 表头构建

store_search_title_pack_csv()(flash_data_store.c:404)用 GET_VARIABLE_NAME 字符串化宏一次性拼出 1800 字节内的表头:

4.5 60 秒周期落盘

resume_data_handle() 中的节流(flash_data_store.c:2825-2833):

if((get_rtc_tick() - s_store_resume_tick) >= 60) { store_run_data(); s_store_resume_tick = get_rtc_tick(); }
else if(get_rtc_tick() < s_store_resume_tick)   { s_store_resume_tick = get_rtc_tick(); }   // RTC 回拨保护

store_run_data() = store_run_table1_data() →(density>16)store_run_table2_data() → store_data_addr()。三者任一失败即整体判失败,写指针不推进(table1 失败直接 return FALSE,不回滚已写指针,靠 CRC 保护脏记录)。

store_run_table1_data()(flash_data_store.c:640-1110)主要计算逻辑:

4.6 断点续传发送状态机

状态由 net/server.h 的 E_Resume_Sts 表示,存放在 g_stEMSInfo.u8ResumeStatusCurr,本文件通过 Get/Set_Resume_Status_Current() 读写。

                    server.c:2719-2772  (收到 "Resume" 指令, 填 g_resume_info 空槽)
                                    |
                                    v
   RESUME_STS_NONE --resume_data_handle 取槽--> RESUME_STS_Idle
                                    |
                    send_search_data(index) 每 500 ms 被调一次
                                    |
   首轮(first==TRUE): get_end_time / get_start_time_addr / get_end_time_addr
                      -> 换算出 s_u32_resume_length(条数), s_u32_read_addr
                                    |
   后续轮: Pack_search_data_to_csv() 拼满约 5 KB 或读完
           -> Pack_resume_data_buf_head_end() -> RESUME_STS_Sending
                                    |
   net/ems.c:4875  轮询到 Sending -> handle_message_queue(STATUS_SEND_RESUME_TO_SERVER,
                                                     resume_buff_sdram, g_u32_resume_data_len)
                                    |
   server.c:6722 收到含 "Status":"Success" 的 0x0C 应答
           -> memset(resume_buff_sdram) / g_u32_resume_data_len = 0
           -> RESUME_STS_Idle  (允许拼下一包)
   或 ems.c:4881 处于 RESUME_STS_Over 时超时重发(5 s 一次, 失败 >10 次后回 Idle)

关键点:

4.7 时间窗口查找与二分搜索

get_end_time()(flash_data_store.c:2536)把 search_time_start 用 mktime 转秒,加 s_u32_resume_length * 60(即云端下发的分钟数)得到终点,再 localtime 拆回 search_time_end。年份换算:tm_year = bYears + NOW_BASE_YEAR - TIME_BASE_YEAR = bYears + 100。

judge_search_data_time(flag, addr, time)(flash_data_store.c:1375)读取记录 2~5 字节拼出 time_t(小端),然后:

即匹配窗口是 ±180 秒(±3 分钟),因为记录粒度是 1 分钟且可能存在丢包/断电缺记录。

store_bin_search_data_addr(flag, time)(flash_data_store.c:1513)的搜索空间分两种:

两段搜索的边界均为 end_point = (右界 - start_addr + 1)/sizeof(ParaTab),mid_addr = start_addr + mid_point * sizeof(ParaTab)——完全依赖记录定长 400 B。search_cnt > MAX_SEARCH_CYCLE(30) 时返回 NULL 防止死循环(34990 条记录二分最多约 16 次,30 次余量充足)。

get_start_time_addr() / get_end_time_addr()(flash_data_store.c:2575、2623)各做最多 5 次尝试:起点每次 +60 s,终点每次 −60 s,一旦 store_bin_search_data_addr 返回非 0 就 break。因此实际可容忍的"找不到精确点"偏移约为 5 分钟,与 judge_search_data_time 的 ±3 分钟窗口存在重叠区间。

store_search_sector_first_head_addr(addr)(flash_data_store.c:1460)负责把一个扇区内的模糊首地址收敛到第一条 AA 55 记录,因为绕圈后扇区起始处可能是上一条记录的尾巴。

4.8 上报报文组帧与分包

报文格式(Pack_resume_data_buf_head_end,flash_data_store.c:2307):

偏移内容
0EMS = 0x01
1REQUEST_COMM = 0x01
2CMD_RESUME_INTERFACE = 0x0C
3~6数据域长度,大端 32 位 = g_u32_resume_data_len - 7
7 ~ 7+L-1CSV 文本
7+L、8+LCRC16/Modbus,高字节在前([0]=>>8, [1]=&0xFF)

Pack_search_data_to_csv(&read_addr, len)(flash_data_store.c:2341)的一次调用流程:

  1. 首包(i == 0)时把 g_u32_resume_data_len 重置为 7、memset 整个 5 MB 缓冲、先拷入 CSV 表头;若 s_u8_csv_buffer_flag == TRUE,说明上一包尾部留了半行,把 s_s8_csv_data_buf_temp 续拼到表头之后;
  2. 对每条记录最多尝试 3 次 read_search_data()(含 CRC 重读);3 次都失败则 Drop_packet_cnt++ 并跳过该记录继续前进(最多容忍 len-1 条坏记录,达到 len 时立即封包发送并返回);
  3. 通过校验后(density>16 时再 read_search_data2())调 store_search_data_pack_csv() 生成一行 CSV,追加到 SDRAM;地址前进 sizeof(ParaTab) + offset,跨 FLASH_DTATA_RUN_END 时按 START + (addr+400+off-END-1) 折算回绕(flash_data_store.c:2377-2384);
  4. 若 g_u32_resume_data_len + strlen(csv_data_buf) + 2 >= 5*1024(剩余空间不足一行),把整行暂存到 s_s8_csv_data_buf_temp 并置 s_u8_csv_buffer_flag = TRUE,封包发送、返回;
  5. 读到连续 10 个 0xFF(到达未写区)或处理完 len 条,同样封包并置 RESUME_STS_Sending。

read_search_data()(flash_data_store.c:1691)读满 sizeof(ParaTab) 后:先在缓冲内找 AA 55 把偏移写入 *offset;再 GetCrcCode_Modbus(buf, sizeof(ParaTab)-2) 与缓冲末 2 字节(小端 [399]<<8 | [398])比对,命中则 s_p_search_data = (ParaTab*)s_u8_data_buf 且 s_u8_read_search_data_ok = TRUE。函数本身恒返回 FALSE(flash_data_store.c:1809),调用者必须看 s_u8_read_search_data_ok。

store_search_data_pack_csv()(flash_data_store.c:1959)的格式化系数(配合 logic/math_tools.c 的 fmt_fixed(raw, scale)):

字段处理
Datesnprintf("20%02u/%02u/%02u,"),bYears 已是两位数年份
电压 Vpv/Ugrid/Ubackup/Ubus%u 原值(0.1 V)
功率 Pmeter/Pbat/Pac/Preal/Pbackup%d 原值
频率 FGrid/Fac/Fbackupfmt_fixed(x, 100) → 两位小数 Hz
BatV / BatC / BatC1-12fmt_fixed(x, 100) → 两位小数
SOC / SOC1-12fmt_fixed(soc * 4, 10) → 把 0.25 % 分辨率还原为一位小数百分比
EmsStatus1→Fault、2→Protection、其余→Normal
EpvTotal/Einput 系列fmt_fixed(x, 100);Einvinput/Echarge 系列用 fmt_fixed(x, 10)
EVEnergyTotalfmt_fixed(x, 1000)
Pdispatchu32_dispatch_power - POSITIVE_VALUE_OFFSET 还原带符号功率
DispatchSocu8_dispatch_soc * FORCE_SOC_MULT / 10
末列若 s_p_search_data2 有效且时间匹配则追加 ErrBackupBox;随后把最后一个逗号回退 l_u16CSVDataLen -= 1 换成 \n

read_search_data2()(flash_data_store.c:1819)与 read_search_data 结构相同,但地址先 += FLASH_DTATA2_RUN_OFFSET,CRC 用 sizeof(ParaTab2)-2 = 396,成功时置 s_p_search_data2,失败时显式置 s_p_search_data2 = NULL(flash_data_store.c:1924-1925)。

4.9 与 uCOS-II 任务/信号量的配合

交互点说明
任务task_system_data(data/data.c:250)——save_job_init() 后启动时调 store_data_init(),主循环里调 resume_data_handle()
调度周期循环首句 OSSemPend(g_t_power_down_sem, 500, &err)(data.c:263)。OS_TICKS_PER_SEC = 1000(UCOSII/uCOS-CONFIG/os_cfg.h:51),故超时 500 ms;掉电中断 exti.c:95 会 OSSemPost 该信号量抢占执行掉电保存路径
信号量只用 g_t_power_down_sem(user/main.c:120 创建),本文件不创建/挂起任何信号量,只依赖"任务被周期唤醒"这一时序
看门狗erase_run_data() 由 erase_flash_handle() 调用,前后各有 IWDG_Feed()(data.c:174-176),因为整片擦除耗时较长
持久化挂起版本校验后调 save_job_post(SYSTEM_BASE_PARA_PAGE)(flash_data_store.c:2819)把 u8_flash_renewal_flag = TRUE 落盘,避免每次上电都重新扫描 Flash
升级互斥if(!IsDownloadEMS()) 包住整个处理体(flash_data_store.c:2806),EMS 固件下载期间不做落盘也不做补报
与网络任务的缓冲所有权resume_buff_sdram / g_u32_resume_data_len 由本文件写、net/ems.c 读并发消息(ems.c:5569 handle_message_queue)、net/server.c 在收到成功应答后清零(server.c:6728-6731)。三者之间没有互斥,靠状态机时序(Sending 期间本文件不写缓冲)保证

4.10 关键全局变量的读写触发点

变量写方读方触发条件
FlashDataAddrInfo.RunDataStoreCurAddrstore_data_addr_init(上电)、store_run_table1_data(每次落盘)、erase_run_dataverify_end_code、store_bin_search_data_addr、judge_next_sector_already_write每 60 s +600(或绕圈折算)
g_resume_info[i]net/server.c:2766-2772(云端下发改指令)、resume_data_handle(清零完成槽)resume_data_handle、net/ems.c:4989(get_resume_index_length)收到 "Resume" 特殊指令
g_u32_resume_data_len本文件(打包)、server.c:6729 与 ems.c:4890(清零)net/ems.c:5561-5569每次封包 / 每次收到 0x0C 成功应答
g_stEMSInfo.u8ResumeStatusCurr本文件 Set_Resume_Status_Current、net/server.c:6731、net/ems.c:4892/4896本文件、wireless_driver.c:243/497/621、server.c:6731置 Sending(封包)/ 置 Idle(应答或超时恢复)
g_t_system_base_info.u8_flash_renewal_flag本文件置 TRUE + 落盘(flash_data_store.c:2818-2819)、server.c:1263 置 FALSEresume_data_handle首次上电或 EMS 固件升级完成后
s_u8_read_search_data_okread_search_data(CRC 结果)Pack_search_data_to_csv每条记录读回

5. 对外接口

5.1 本文件导出(供外部调用)

具体调用者:

调用者用到的符号
data/data.cstore_data_init()(data.c:258)、resume_data_handle()(data.c:296)、erase_run_data()(data.c:82、175)
net/ems.cresume_buff_sdram、g_u32_resume_data_len(ems.c:4889-4890、5561-5569)、Get/Set_Resume_Status_Current(ems.c:4875-4896)、get_resume_cmd_id_code()(ems.c:2831)、get_resume_index_length()(ems.c:4989)、Set_Resume_Status_Current(ems.c:4970-5006)
net/server.cg_resume_info(server.c:2742-2772)、set_resume_cmd_id_code()(server.c:2736)、Set_Resume_Status_Current()(server.c:6731)、resume_buff_sdram/g_u32_resume_data_len(server.c:6728-6729)、resume_flash_reset() 联动(server.c:1259)
midware/ethernt/wireless/wireless_driver.cGet_Resume_Status_Current()(wireless_driver.c:243、497、621)

5.2 本文件调用的外部函数/变量

类别来源模块代表符号
Flash 驱动hardware/flashFlash_Read、Flash_Write、Flash_Erase_Spanned_Sector、Flash_ReadDensity
CRCcommon/crcGetCrcCode_Modbus
时间/RTCmidware/auxiliary/system_aux、hardware/pcf8563get_rtc_tick、mktime、localtime、strftime、struct RTC_DateTime
数据类型与断言data/system_configg_t_system_base_info.u8_flash_renewal_flag、save_job_post、SYSTEM_BASE_PARA_PAGE
BMSmidware/bmsbms_read_total_volt、bms_read_total_soc、bms_read_single_soc、bms_read_single_cur、bms_read_status_flag、bms_read_work_status、bms_read_max_charge_power、bms_read_max_dischg_power、bms_read_relay_status、bms_read_shut_down、bms_read_num、bms_read_cell_min/max_volt_info、bms_read_low/high_temper_info、bms_read_total_power、bms_read_total_cur、bms_read_fault_code、bms_read_protection_code、bms_read_protection_extend2_code、bms_read_pwrline_low/high_temper_info(bms.h:1206-1207)
电表midware/metermeter_read_actpower、get_average_power_of_gridmeter、energy_read_grid_consumption、energy_read_grid_feedin、energy_read_storage_*、energy_read_bat_chgrge、energy_read_bat_dischg、energy_read_gc_output、energy_read_gc_charge、energy_read_grid_chgrge、energy_read_bat_dischg_feedin_grid、energy_read_pv_inverter_output
逆变器midware/inverterinverter_read_pv_volt/power、inverter_read_grid_volt/freq、inverter_read_backup_volt/freq/actpower、inverter_read_inv_actpower、inverter_read_bat_volt、inverter_read_bus_volt、inverter_read_run_mode、inverter_read_machine_temp、inverter_read_fault_code、inverter_read_fault_code_extend/extend2、inverter_read_warning_code
并机逻辑logicparallel_read_dc_total_set_inv_power、parallel_read_dc_total_set_bat_power、g_t_logic_info.t_config_data.u8_system_mode、DC_SYSTEM_MODE/HYBIRD_SYSTEM_MODE/AC_SYSTEM_MODE、cUPS
调度logic/dispatchg_t_dispatch_t.t_stsystemcmdinfo、POSITIVE_VALUE_OFFSET、FORCE_SOC_MULT
充电桩midware/evc、evc_iiget_evc_enable_flag、get_evc_power_total、get_evc_ii_power_total、get_evc_energy_total、get_evc_ii_energy_total
油机logic/generatorget_offgrid_mode_gc_power
系统辅助data/system_configget_system_sn、get_offgrid_mode 系列
数值格式logic/math_toolsfmt_fixed
升级判定net/ems_downloadIsDownloadEMS
擦除请求data/data.cresume_flash_reset(data.c:446)
EMS 帧头/状态net/ems、net/serverEMS、REQUEST_COMM、CMD_RESUME_INTERFACE、RESUME_STS_*、g_stEMSInfo
看门狗hardware/iwdgIWDG_Feed(在本文件不直接调用,由调用方 erase_flash_handle 负责)
类型定义hardware/sdramSDRAM_RESUME_START_ADDR、SDRAM_FLASH_DATA_STOE_BUF_ADDR/2、SDRAM_5M

6. 依赖关系

7. 备注与风险点

  1. store_search_sector_first_head_addr 存在 1 字节越界读:循环 for(i = 0; i < FLASH_SECTOR_SIZE; i++) 内访问 s_u8_data_buf[i+1](flash_data_store.c:1473),当 i == 4095 时访问下标 4096,超出 4096 字节数组边界。同文件 packet_head 用了 i < 400 - 1 做了正确保护,此处遗漏。read_search_data/read_search_data2 中的 for(i < sizeof(ParaTab)) + buf[i+1] 也越界 1 字节(flash_data_store.c:1728、1774),只是缓冲实际有 4096 B 才没出事。
  2. store_search_data_pack_csv 的 snprintf 累加长度有溢出风险:l_u16CSVDataLen += snprintf(...),而 snprintf 返回的是"理想长度"而非实际写入长度。一旦某行 CSV 超过 sizeof(s_s8_csv_data_buf)=1024,l_u16CSVDataLen 会大于 1024,后续 l_u16CSVDataBufLen - l_u16CSVDataLen 在 u16 下回绕成巨大值,snprintf 随即越界写全局缓冲。按字段估算当前一行约 950~1000 字节(含 4 个 %llu 64 位故障码、SN 与 24 个 fmt_fixed 小数),余量很小;SN 变长或故障码取值很大时即可触发。s_s8_csv_data_buf 与 s_s8_csv_data_buf_temp 相邻定义(:58-59),被踩的正是暂存缓冲。
  3. s_p_search_data2 的时间匹配用错了长度:memcmp(&s_p_search_data2->Time, &s_p_search_data->Time, sizeof(struct RTC_DateTime))(flash_data_store.c:2288)用 6 字节(RTC_DateTime 大小)比较 4 字节的 time_t,多读了其后 2 字节——ParaTab 后跟 s32_grid_meter_actpower_a 低 16 位,ParaTab2 后跟恒为 0 的 u32_bb_error_code。结果只有当 A 相电网功率低 16 位恰为 0 时才会追加 ErrBackupBox 列,表头有列而数据行缺列,CSV 列错位。正确写法应为 sizeof(time_t)。
  4. s_u32_resume_length 语义复用易错:同一变量先表示"云端下发的补报分钟数"(get_end_time 用它算终点,flash_data_store.c:2548),随后被覆盖为"待处理记录条数"(flash_data_store.c:2705/2709),再在打包中被逐包递减。任一路径漏改都可能导致报文按错误条数循环。
  5. send_search_data 中 udwSendCsvDataAddrEnd == NULL 的兜底不完整:s_u32_read_addr = udwSendCsvDataAddrStart; 写在 if(udwSendCsvDataAddrStart != NULL) 之内、if(udwSendCsvDataAddrEnd != NULL) 之外(flash_data_store.c:2696-2714)。当只有起点找到而终点没找到时,s_u32_resume_length 仍是"分钟数"而不是"条数",却已开始按条数循环打包,可能读过有效区间继续读无效区——好在 read_search_data 的 CRC 会失败并走丢包分支,最终在 Drop_packet_cnt >= len 时提前收尾,不至于无限循环,但会显著拖慢响应。
  6. Flash_Write 的隐式整扇区擦除带来写放大与磨损:hardware/flash/flash.c:802-840 在写入前会读回整个扇区,若目标区域任一字节日非 0xFF,就先擦除 4 KB 整扇区再回写。
    • 对三个写指针备份扇区(各 1024 个 4 字节槽):填满后每 60 s 都要擦一次整扇区,即 约 1440 次/天。以典型 10 万次擦写寿命计,约 70 天后该扇区就可能失效。
    • 对主运行区绕圈回写:绕回 FLASH_DTATA_RUN_START 时同样要擦除该扇区,属环形覆盖的正常代价。
  7. 跨区拆包假设写指针必在最后一个扇区:flash_data_store.c:1075 计算 secremain = FLASH_SECTOR_SIZE - addr % FLASH_SECTOR_SIZE,随后 l_ua6_pack_len -= secremain(u16)。只有当 addr 落在 0xF62000~0xF62FFF 时 secremain <= 400 才成立;若未来修改 FLASH_DTATA_RUN_END 使其不在扇区边界,该减法会下溢成约 60592,导致 Flash_Write 写入 60 KB 无意义数据并把写指针推错。这是一条"依赖宏取值巧合"的脆弱逻辑。
  8. ParaTabToArray(flash_data_store.c:1937)是废弃代码:全工程无任何调用者,且只拷贝了 u16_head、Time 和一个功率字段就返回,明显是早期版本残留;它是全局符号却未在头文件声明,容易被误认为是有效接口。
  9. show_search_data 有明显复制粘贴错误:PbackupL2/PbackupL3 两行都打印 s16_inv_backup_power_a(flash_data_store.c:1250-1251),从不打印 _b/_c。作为调试函数会给出误导性输出。
  10. read_search_data 恒返回 FALSE:函数体内所有分支最终都走到 return FALSE;(flash_data_store.c:1809),成功信息只通过 s_u8_read_search_data_ok 传出。调用方 Pack_search_data_to_csv 未使用返回值,当前无功能影响,但接口语义具有强误导性。同时 *offset 只在找到 AA 55 时被赋值,未找到时保留上一次的值,而调用方用它推进读地址——CRC 通过但头未找到时可能推进错误偏移。
  11. 10 次发送失败放弃时的半行残留:net/ems.c:4888-4893 在重试 10 次后 memset(resume_buff_sdram) 并把状态置回 RESUME_STS_Idle,但没有复位 s_u8_csv_buffer_flag 与 s_s8_csv_data_buf_temp。下一轮 Pack_search_data_to_csv 首包会把这条已被发送过的半行再次拼到新表头之后,形成一行重复数据(同时 s_u32_read_addr 已推进,该行对应的记录不会再从 Flash 重读)。属于数据重复而非内存破坏,但会让云端 CSV 出现重复行。
  12. udwSendCsvDataAddrStart != NULL 用 NULL 比较 u32 地址:flash_data_store.c:2609、2657、2696、2698。语义上等价于 != 0,但类型不严谨;store_bin_search_data_addr 也确实用 return NULL 表示"未找到"。
  13. 无锁共享缓冲:resume_buff_sdram、g_u32_resume_data_len、s_u8_read_search_data_ok、s_p_search_data 等均为跨任务共享(task_system_data 与网络任务),代码中没有信号量或临界区保护。当前依赖状态机保证"Sending 期间不重写",但这属于隐式约定,一旦有人增加一个异步清理点(如 server.c 在任意时刻清零缓冲)即会竞态。
  14. memset(resume_buff_sdram, 0, sizeof(resume_buff_sdram)) 清零 5 MB:在 Flash 打包路径上首次进入时执行(flash_data_store.c:2445),而实际只使用前 ~5 KB,是明显的性能浪费(也解释了为何 5 MB 缓冲却限制在 5 KB 包大小)。
  15. Flash_ReadDensity() > 16 为魔法条件:该比较的语义是"Flash 容量 > 16 MB"(Flash_ReadDensity() 返回 MB 数,hardware/flash/flash.c:126-129),在 flash_data_store.c 中出现 5 次(:477、1199、2368、2966 等),无命名宏,可读性差且容易在移植时误改。
  16. u32_energy_gc_charge(EGCbat)只写不报:flash_data_store.c:1034 采集并存入 Flash,但 CSV 表头与 store_search_data_pack_csv 都没有输出它,云端拿不到该字段。
  17. judge_search_data_time 在 Flash_Read 失败时返回 0:if(!res) return res;(flash_data_store.c:1389)等价于返回 FLASH_ERR(0),而 0 的语义是"时间小于目标",二分查找会朝后收缩 start_point。读失败被静默当成时间比较结果,不报错也不重试。
  18. s_u8_resume_index 单调轮询导致响应延迟:空槽时每次调用只前进一格,10 个槽全部为空时最坏要 10 个调用周期(约 5 s)才能发现新指令;且 else 分支直接 return,跳过了同一轮中"发送中的包"的推进判断(send_search_data 未被调用),在多发场景下会额外引入延迟。
  19. 状态机跨文件分散,RESUME_STS_Response 是死枚举:本文件只用 NONE/Idle/Sending;RESUME_STS_Over 由 net/ems.c:5571 在 handle_message_queue 发送后设置,net/ems.c:4881 消费;而 RESUME_STS_Response 在全工程从未被赋值(搜索仅命中枚举定义),是废弃状态。阅读本文件时看不到完整迁移图,修改状态流转必须同时核对 net/ems.c 与 net/server.c。
  20. store_data_addr_init 与 store_data_addr 的扇区填满判定不一致:前者判断 i + 4 >= FLASH_SECTOR_SIZE(i ≥ 4092,break 后取 i-4 位置的数据),后者判断 i + 4 > FLASH_SECTOR_SIZE(i ≥ 4093,置 temp 后改写回偏移 0)。在扇区最后 1~2 个槽位附近,两侧对"槽位在哪"的理解不同,存在读到"刚被绕回覆盖的那一格"的可能;由于有 2/3 表决和后续 CRC/时间校验,实际危害有限,但属于逻辑不严密。