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 本文件定义/拥有的数据对象
| 名称 | 类型 | 说明 |
|---|---|---|
FlashDataAddrInfo | static FlashDataAddr | 唯一的写指针状态:RunDataStoreCurAddr 指向下一条待写入记录的首地址 |
s_t_resume_data | static ParaTab(400 B) | 主记录打包缓冲,store_run_table1_data() 逐字段填充后整体写 Flash |
s_t_resume_data2 | static ParaTab2(398 B) | 扩展记录打包缓冲,仅 Flash_ReadDensity() > 16 时使用 |
s_p_search_data / s_p_search_data2 | static ParaTab* / ParaTab2* | 读回缓冲的解析指针,分别指向 s_u8_data_buf / s_u8_data_buf2 内部 |
s_u8_data_buf / s_u8_data_buf2 | static u8[FLASH_SECTOR_SIZE](各 4096 B) | Flash 扇区级读写缓冲,通过 __attribute__((at(SDRAM_FLASH_DATA_STOE_BUF_ADDR))) 绝对定位在 SDRAM(flash_data_store.c:40-41) |
resume_buff_sdram | u8[SDRAM_5M](5 MB,非 static) | 上报报文缓冲,绝对定位 SDRAM_RESUME_START_ADDR,实际每包只用前约 5 KB |
g_u32_resume_data_len | u32(非 static) | 上报报文长度游标;起始置 7(=协议头长度),每拼一行 CSV 递增 |
g_resume_info | RESUME_BUFFER_T[10](非 static) | 断点续传指令槽,由 net/server.c 解析 "Resume" 指令后写入,本文件消费并清空 |
s_s8_csv_title_buf | static char[1800] | CSV 表头行(一次构建,全生命周期复用) |
s_s8_csv_data_buf | static char[1024] | 单条记录的 CSV 数据行 |
s_s8_csv_data_buf_temp | static char[1024] | 上一包"剩余半行"暂存,用于跨包续拼 |
s_u8_csv_buffer_flag | static u8 | 有半行数据被暂存到 temp 时置 TRUE |
s_u8_read_search_data_ok | static u8 | 读回 + CRC 校验成功标志(本模块实际的"成功"判据,而不是 read_search_data 的返回值) |
s_u32_resume_length | static u32 | 双重语义:先是被查区间的记录条数(由分钟数换算),打包过程中被逐包递减计数 |
s_u32_read_addr | static u32 | 跨包续读的 Flash 地址游标 |
udwSendCsvDataAddrStart / ...End | u32(非 static,未在头文件导出) | 二分查找到的起止记录地址;0 表示未找到 |
search_time_start / search_time_end | struct RTC_DateTime(非 static,未导出) | 补报区间起点与终点(end = start + length 分钟,见 get_end_time) |
s_store_resume_tick | static time_t | 上次落盘的时间戳,用于 60 s 节流 |
s_as8ResumeCmdId | static 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_CYCLE | flash_data_store.h | Flash 分区地址与二分查找上限 |
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/Over | net/server.h:206-213 | 断点续传状态机枚举 |
POSITIVE_VALUE_OFFSET(32000)、FORCE_SOC_MULT(4) | logic/dispatch.h:16、18 | CSV 导出时的功率/SOC 反算系数 |
TIME_BASE_YEAR(1900)、NOW_BASE_YEAR(2000) | midware/auxiliary/system_aux.h:13-14 | tm 年份与 RTC_DateTime.bYears 的换算基准 |
SYSTEM_BASE_PARA_PAGE | data/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_code | static | 判断上电位置处是否存在"有头无尾"的残包,返回 TRUE 表示需要擦除整片运行数据 |
store_data_addr_init | static | 读三份写指针备份扇区、三取二表决并做区间合法性检查,恢复 FlashDataAddrInfo |
store_search_title_pack_csv | static | 用 GET_VARIABLE_NAME 批量拼接 CSV 表头行到 s_s8_csv_title_buf |
store_data_init | 全局 | 模块初始化:重试 store_data_addr_init 最多 3 次、构建表头、初始化落盘节流时间戳 |
store_data_addr | static | 把当前写指针追加写入三个备份扇区,返回写成功扇区数是否 ≥2 |
store_run_table1_data | static | 采集百余项实时数据填入 ParaTab,计算 CRC 后按环形地址写入主区 |
store_run_table2_data | static | 填充 ParaTab2(仅 ErrBackupBox 与时间),写入扩展区同一逻辑地址 + 固定偏移处 |
store_run_data | static | 落盘总调度:table1 → (density>16 时)table2 → 写指针备份 |
show_search_data | 全局 | 把 s_p_search_data 指向的 ParaTab 全部字段以十六进制 printf(调试接口) |
judge_next_sector_already_write | static | 判断 addr 所在扇区的下一个扇区是否已有数据,用于识别"写指针是否已绕圈" |
judge_search_data_time | static | 读一条记录的时间戳并与目标时间比较,返回 0(小于)/1(大于)/2(落在 ±180 s 窗口内) |
store_search_sector_first_head_addr | static | 在一个 4 KB 扇区内找第一条 AA 55 记录的首地址 |
store_bin_search_data_addr | static | 在 Flash 运行区按定长步长做二分查找,定位与目标时间匹配的记录地址 |
read_search_data | static | 读一条 ParaTab(含跨区拆读),输出起始码偏移并做 CRC 校验,设置 s_u8_read_search_data_ok |
read_search_data2 | static | 读一条 ParaTab2(含跨区拆读)并做 CRC 校验,设置 s_p_search_data2 |
ParaTabToArray | 全局(未在 .h 声明、无任何调用者) | 只拷贝了 u16_head、Time 和第一个功率字段就结束,是残缺的遗留辅助函数 |
store_search_data_pack_csv | static | 把 s_p_search_data(+s_p_search_data2)格式化成一行 CSV 写入 s_s8_csv_data_buf |
Pack_resume_data_buf_head_end | static | 为 resume_buff_sdram 补上 7 字节协议头和 2 字节 CRC16,并修正总长度 |
Pack_search_data_to_csv | static | 循环读记录、转 CSV、往 resume_buff_sdram 拼包,满 5 KB 或读完即置 RESUME_STS_Sending 返回 |
get_end_time | static | 由起点 + s_u32_resume_length 分钟算出补报区间终点 search_time_end |
get_start_time_addr | static | 以起点时间为基准最多 5 次(每次 +60 s)二分查找起始记录地址 |
get_end_time_addr | static | 以终点时间为基准最多 5 次(每次 −60 s)二分查找结束记录地址 |
send_search_data | static | 断点续传发送主状态机:首次查地址换算出总条数,之后逐包打包并推进状态 |
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 完全吻合:
| 偏移 | 字段 | 长度 |
|---|---|---|
| 0 | u16_head(=0x55AA,小端存储为 AA 55) | 2 |
| 2 | time_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 |
| 325 | u8_reserve[71] | 71 |
| 396 | u16_endcode = RESUME_END_CODE(字节 00 B4) | 2 |
| 398 | u16_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)。
写入地址按"只增不减"推进:
FlashDataAddrInfo.RunDataStoreCurAddr从FLASH_DTATA_RUN_START(0x209000) 开始,每次 += 400;- 当
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 + 余量,即完成一次绕圈; - 扩展区使用同一逻辑地址
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 三个扇区,每个扇区的处理方式一致:
- 读整个 4 KB 扇区到
s_u8_data_buf; - 从偏移 0 开始找第一个"连续 4 字节全 FF"的位置
i(该位置即下一个空闲槽); - 若
i + 4 > FLASH_SECTOR_SIZE(扇区写满)则置temp = TRUE,改写回偏移 0(即扇区绕回起点); - 否则写到
BACKUPn + i; - 统计成功扇区数
l_u8_store_addr_ok_count,返回count >= 2(两副本写成功即认为可靠)。
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() 判断"当前写指针是否已经绕过圈",分出两条路径:
- 已绕圈:从当前写指针位置向后找
AA 55,用packet_end()校验该记录是否完整; - 未绕圈:从
FLASH_DTATA_RUN_START开始找。
返回值语义: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 字节内的表头:
- 第一部分硬编码顺序:
Date, Time, SN,+ 电网 6 项 + 逆变器 33 项 + 电池(BatV/Pbat/BatC/SOC/FlagBms/.../BatC1-12/SOC1-12)+ 系统状态 7 项 + 能量统计 19 项 + 调度 4 项 + 动力线温度 2 项; - 第二部分在
Flash_ReadDensity() > 16时追加ErrBackupBox; - 末尾把最后一个逗号
memset成'\n'(flash_data_store.c:488)。
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)主要计算逻辑:
u16_head = (HEAD1<<8)|HEAD2,Time = get_rtc_tick();PacL1 = parallel_read_dc_total_set_inv_power()、PacL2 = -parallel_read_dc_total_set_bat_power()、PacL3 = 0(原三相场量改用并机总量,PacL3恒 0);Tinv/Fac/Ua/Ub/Uc依inverter_read_run_mode() == cUPS分支取值:UPS 模式取旁路(backup)电压/频率,其他模式取电网电压/频率(flash_data_store.c:712-739);- SOC 统一右移 2 位存储(
bms_read_total_soc()/4、bms_read_single_soc(n)/4,flash_data_store.c:797-851),导出时再*4还原(flash_data_store.c:2115、2171),即内部 SOC 以 0.25 % 为分辨率; u8_ems_status三级判定:任一(EMS 故障 / 逆变器故障 / 逆变器扩展故障1、2 / BMS 故障)→ 1=Fault;否则(BMS 保护 / BMS 扩展保护2)→ 2=Protect;否则 3=Normal(flash_data_store.c:948-953);EpvTotal依g_t_logic_info.t_config_data.u8_system_mode分三种:DC 模式 = 储能侧 PV 输入 ×10;Hybrid/AC 模式 = 储能侧 PV ×10 + PV 逆变器输出;其他 = 0(flash_data_store.c:975-980);- 调度 4 项直接取
g_t_dispatch_t.t_stsystemcmdinfo的u8Dispatch/u16ActivePower/u8ForceSOC/u8ControlMode(flash_data_store.c:1041-1050); - 最后写
u16_endcode = RESUME_END_CODE、u16_crc_code = GetCrcCode_Modbus(&s_t_resume_data, sizeof(ParaTab)-2)。
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)
关键点:
- 单次只处理 10 个槽中的 1 个。若当前槽
u32_length == 0,则s_u8_resume_index自增(到 10 回绕 0)后直接 return(flash_data_store.c:2880-2887),即空槽时以 500 ms 步进轮询下一个槽; - 槽有值时,必须状态为
RESUME_STS_NONE才把槽内容搬到search_time_start与s_u32_resume_length,然后置RESUME_STS_Idle(flash_data_store.c:2855-2878); - 发送完毕(
send_search_data内s_u32_resume_length == 0)会置g_resume_info[index].u8_finished = TRUE并回到RESUME_STS_NONE,resume_data_handle随后把该槽 6 个时间字段、长度、finished 全清零(flash_data_store.c:2893-2906)。
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(小端),然后:
flag == 0x00(找起点):time <= t <= time+180→ 返回 2(命中);t < time→ 0;否则 1;flag == 0x01(找终点):time-180 <= t <= time→ 返回 2;其余同上。
即匹配窗口是 ±180 秒(±3 分钟),因为记录粒度是 1 分钟且可能存在丢包/断电缺记录。
store_bin_search_data_addr(flag, time)(flash_data_store.c:1513)的搜索空间分两种:
judge_next_sector_already_write(CurAddr) == 1(已绕圈):先在[下一个扇区首地址, FLASH_DTATA_RUN_END]段搜索,未命中再回退到[FLASH_DTATA_RUN_START, CurAddr]段;== 0(未绕圈):只在[FLASH_DTATA_RUN_START, CurAddr]段搜索。
两段搜索的边界均为 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):
| 偏移 | 内容 |
|---|---|
| 0 | EMS = 0x01 |
| 1 | REQUEST_COMM = 0x01 |
| 2 | CMD_RESUME_INTERFACE = 0x0C |
| 3~6 | 数据域长度,大端 32 位 = g_u32_resume_data_len - 7 |
| 7 ~ 7+L-1 | CSV 文本 |
| 7+L、8+L | CRC16/Modbus,高字节在前([0]=>>8, [1]=&0xFF) |
Pack_search_data_to_csv(&read_addr, len)(flash_data_store.c:2341)的一次调用流程:
- 首包(
i == 0)时把g_u32_resume_data_len重置为 7、memset整个 5 MB 缓冲、先拷入 CSV 表头;若s_u8_csv_buffer_flag == TRUE,说明上一包尾部留了半行,把s_s8_csv_data_buf_temp续拼到表头之后; - 对每条记录最多尝试 3 次
read_search_data()(含 CRC 重读);3 次都失败则Drop_packet_cnt++并跳过该记录继续前进(最多容忍len-1条坏记录,达到len时立即封包发送并返回); - 通过校验后(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); - 若
g_u32_resume_data_len + strlen(csv_data_buf) + 2 >= 5*1024(剩余空间不足一行),把整行暂存到s_s8_csv_data_buf_temp并置s_u8_csv_buffer_flag = TRUE,封包发送、返回; - 读到连续 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)):
| 字段 | 处理 |
|---|---|
| Date | snprintf("20%02u/%02u/%02u,"),bYears 已是两位数年份 |
| 电压 Vpv/Ugrid/Ubackup/Ubus | %u 原值(0.1 V) |
| 功率 Pmeter/Pbat/Pac/Preal/Pbackup | %d 原值 |
| 频率 FGrid/Fac/Fbackup | fmt_fixed(x, 100) → 两位小数 Hz |
| BatV / BatC / BatC1-12 | fmt_fixed(x, 100) → 两位小数 |
| SOC / SOC1-12 | fmt_fixed(soc * 4, 10) → 把 0.25 % 分辨率还原为一位小数百分比 |
| EmsStatus | 1→Fault、2→Protection、其余→Normal |
| EpvTotal/Einput 系列 | fmt_fixed(x, 100);Einvinput/Echarge 系列用 fmt_fixed(x, 10) |
| EVEnergyTotal | fmt_fixed(x, 1000) |
| Pdispatch | u32_dispatch_power - POSITIVE_VALUE_OFFSET 还原带符号功率 |
| DispatchSoc | u8_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.RunDataStoreCurAddr | store_data_addr_init(上电)、store_run_table1_data(每次落盘)、erase_run_data | verify_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 置 FALSE | resume_data_handle | 首次上电或 EMS 固件升级完成后 |
s_u8_read_search_data_ok | read_search_data(CRC 结果) | Pack_search_data_to_csv | 每条记录读回 |
5. 对外接口
5.1 本文件导出(供外部调用)
- 函数(头文件声明):
store_data_init、show_search_data、Get_Resume_Status_Current、Set_Resume_Status_Current、resume_data_handle、get_resume_cmd_id_code、set_resume_cmd_id_code、erase_run_data - 函数(仅在
.c中全局可见,头文件未声明):packet_head(flash_data_store.c:106)、packet_end(:160)、ParaTabToArray(:1937) - 全局变量:
resume_buff_sdram(:60)、g_u32_resume_data_len(:61)、g_resume_info(:63);另有udwSendCsvDataAddrStart/End(:50)、search_time_start/search_time_end(:51) 为全局但无外部引用
具体调用者:
| 调用者 | 用到的符号 |
|---|---|
data/data.c | store_data_init()(data.c:258)、resume_data_handle()(data.c:296)、erase_run_data()(data.c:82、175) |
net/ems.c | resume_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.c | g_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.c | Get_Resume_Status_Current()(wireless_driver.c:243、497、621) |
5.2 本文件调用的外部函数/变量
| 类别 | 来源模块 | 代表符号 |
|---|---|---|
| Flash 驱动 | hardware/flash | Flash_Read、Flash_Write、Flash_Erase_Spanned_Sector、Flash_ReadDensity |
| CRC | common/crc | GetCrcCode_Modbus |
| 时间/RTC | midware/auxiliary/system_aux、hardware/pcf8563 | get_rtc_tick、mktime、localtime、strftime、struct RTC_DateTime |
| 数据类型与断言 | data/system_config | g_t_system_base_info.u8_flash_renewal_flag、save_job_post、SYSTEM_BASE_PARA_PAGE |
| BMS | midware/bms | bms_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/meter | meter_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/inverter | inverter_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 |
| 并机逻辑 | logic | parallel_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/dispatch | g_t_dispatch_t.t_stsystemcmdinfo、POSITIVE_VALUE_OFFSET、FORCE_SOC_MULT |
| 充电桩 | midware/evc、evc_ii | get_evc_enable_flag、get_evc_power_total、get_evc_ii_power_total、get_evc_energy_total、get_evc_ii_energy_total |
| 油机 | logic/generator | get_offgrid_mode_gc_power |
| 系统辅助 | data/system_config | get_system_sn、get_offgrid_mode 系列 |
| 数值格式 | logic/math_tools | fmt_fixed |
| 升级判定 | net/ems_download | IsDownloadEMS |
| 擦除请求 | data/data.c | resume_flash_reset(data.c:446) |
| EMS 帧头/状态 | net/ems、net/server | EMS、REQUEST_COMM、CMD_RESUME_INTERFACE、RESUME_STS_*、g_stEMSInfo |
| 看门狗 | hardware/iwdg | IWDG_Feed(在本文件不直接调用,由调用方 erase_flash_handle 负责) |
| 类型定义 | hardware/sdram | SDRAM_RESUME_START_ADDR、SDRAM_FLASH_DATA_STOE_BUF_ADDR/2、SDRAM_5M |
6. 依赖关系
- 依赖(#include,flash_data_store.c:13-32):
flash_data_store.h、<time.h>、ems.h、dispatch.h、system_aux.h、bms.h、meter.h、inverter.h、data.h、logic.h、sdram.h、system_config.h、iwdg.h、simple_encryption.h、crc.h、ems_download.h、evc.h、evc_ii.h、generator.h、math_tools.h- 注:
simple_encryption.h在本文件中未见被使用的符号,属于历史遗留 include。
- 注:
- 被依赖:
data/data.c(task_system_data启动与周期调用、erase_flash_handle执行擦除、resume_flash_reset提供擦除请求)net/ems.c(上报调度与报文发送)net/server.c(断点指令解析、应答处理、指令 ID 设置)midware/ethernt/wireless/wireless_driver.c(借状态位判断是否处于断点续传,用于抑制升级/电表丢失告警)- 头文件被上列 4 个
.c直接 include
7. 备注与风险点
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 才没出事。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 个%llu64 位故障码、SN 与 24 个fmt_fixed小数),余量很小;SN 变长或故障码取值很大时即可触发。s_s8_csv_data_buf与s_s8_csv_data_buf_temp相邻定义(:58-59),被踩的正是暂存缓冲。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)。s_u32_resume_length语义复用易错:同一变量先表示"云端下发的补报分钟数"(get_end_time用它算终点,flash_data_store.c:2548),随后被覆盖为"待处理记录条数"(flash_data_store.c:2705/2709),再在打包中被逐包递减。任一路径漏改都可能导致报文按错误条数循环。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时提前收尾,不至于无限循环,但会显著拖慢响应。Flash_Write的隐式整扇区擦除带来写放大与磨损:hardware/flash/flash.c:802-840在写入前会读回整个扇区,若目标区域任一字节日非0xFF,就先擦除 4 KB 整扇区再回写。- 对三个写指针备份扇区(各 1024 个 4 字节槽):填满后每 60 s 都要擦一次整扇区,即 约 1440 次/天。以典型 10 万次擦写寿命计,约 70 天后该扇区就可能失效。
- 对主运行区绕圈回写:绕回
FLASH_DTATA_RUN_START时同样要擦除该扇区,属环形覆盖的正常代价。
- 跨区拆包假设写指针必在最后一个扇区: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 无意义数据并把写指针推错。这是一条"依赖宏取值巧合"的脆弱逻辑。 ParaTabToArray(flash_data_store.c:1937)是废弃代码:全工程无任何调用者,且只拷贝了u16_head、Time和一个功率字段就返回,明显是早期版本残留;它是全局符号却未在头文件声明,容易被误认为是有效接口。show_search_data有明显复制粘贴错误:PbackupL2/PbackupL3两行都打印s16_inv_backup_power_a(flash_data_store.c:1250-1251),从不打印_b/_c。作为调试函数会给出误导性输出。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 通过但头未找到时可能推进错误偏移。- 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 出现重复行。 udwSendCsvDataAddrStart != NULL用NULL比较u32地址:flash_data_store.c:2609、2657、2696、2698。语义上等价于!= 0,但类型不严谨;store_bin_search_data_addr也确实用return NULL表示"未找到"。- 无锁共享缓冲:
resume_buff_sdram、g_u32_resume_data_len、s_u8_read_search_data_ok、s_p_search_data等均为跨任务共享(task_system_data与网络任务),代码中没有信号量或临界区保护。当前依赖状态机保证"Sending 期间不重写",但这属于隐式约定,一旦有人增加一个异步清理点(如 server.c 在任意时刻清零缓冲)即会竞态。 memset(resume_buff_sdram, 0, sizeof(resume_buff_sdram))清零 5 MB:在 Flash 打包路径上首次进入时执行(flash_data_store.c:2445),而实际只使用前 ~5 KB,是明显的性能浪费(也解释了为何 5 MB 缓冲却限制在 5 KB 包大小)。Flash_ReadDensity() > 16为魔法条件:该比较的语义是"Flash 容量 > 16 MB"(Flash_ReadDensity()返回 MB 数,hardware/flash/flash.c:126-129),在 flash_data_store.c 中出现 5 次(:477、1199、2368、2966 等),无命名宏,可读性差且容易在移植时误改。u32_energy_gc_charge(EGCbat)只写不报:flash_data_store.c:1034 采集并存入 Flash,但 CSV 表头与store_search_data_pack_csv都没有输出它,云端拿不到该字段。judge_search_data_time在Flash_Read失败时返回 0:if(!res) return res;(flash_data_store.c:1389)等价于返回 FLASH_ERR(0),而 0 的语义是"时间小于目标",二分查找会朝后收缩start_point。读失败被静默当成时间比较结果,不报错也不重试。s_u8_resume_index单调轮询导致响应延迟:空槽时每次调用只前进一格,10 个槽全部为空时最坏要 10 个调用周期(约 5 s)才能发现新指令;且else分支直接return,跳过了同一轮中"发送中的包"的推进判断(send_search_data未被调用),在多发场景下会额外引入延迟。- 状态机跨文件分散,
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。 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/时间校验,实际危害有限,但属于逻辑不严密。