math_tools.c
fmt_fixed(s64 raw, u32 scale)4.2 parse_fixed(const char *s, u32 scale)4.3 parse_dispatch_para(const char *s, u32 *out, int max_fields)4.4 通信协议帧格式4.5 定时 / uCOS-II 配合4.6 关键全局变量读写5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
logic/math_tools.c所属模块:logic(能量调度核心,但实际是纯数据处理工具,不参与调度决策) 代码行数:137
1. 文件职责
本文件提供 EMS 固件里通用的「定点整数 ↔ 浮点字符串」互转工具,属于纯数据处理层,不参与任何调度或控制决策。fmt_fixed() 把以 0.01/0.1/0.001 等比例存储的定点整数量(电压、电流、功率、能量、频率)格式化成带小数点的字符串,用于拼装上报云端的 JSON 报文与存储 CSV 历史数据;parse_fixed() 反向把云端下发的浮点字符串(如 "3.3")解析为定点整数,用于安全参数配置和固件升级包参数;parse_dispatch_para() 专门解析调度指令 JSON 中 Parameter2":" 后面那串逗号分隔的功率参数。它处在数据流的「格式化/解析」旁路环节:上游是 net/ems.c(打包上报)、net/server.c(解析下行配置)、data/resume_data/flash/flash_data_store.c(写 CSV);下游是 TCP/UDP 报文缓冲区和 Flash。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
DISPATCH_PARAMETER2 | 宏(定义于本文件第 93 行) | 字面量 "Parameter2\":\"",即 JSON 中 Parameter2":" 的片段,用于定位调度参数字符串起点 |
s_au8FmtFixedBuf[16][16] | 函数内 static char 二维数组 | fmt_fixed() 的环形输出缓冲池,16 个槽 × 16 字节;每调用一次换一个槽,保证同一条 printf/snprintf 里多次调用结果不被覆盖 |
s_u8FmtFixedIdx | 函数内 static u8 | 环形索引,每次调用后 (idx + 1) & 15,16 槽轮转 |
s64 / u32 / u8 / s32 | 平台基础类型(chip.h / includes.h) | 见 system/sys/chip.h;本文件未定义任何结构体、枚举或联合体 |
本文件没有自定义结构体、枚举、位域;唯一宏 DISPATCH_PARAMETER2 定义在文件中部(第 93 行,parse_dispatch_para 之前),没有 #undef。
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
fmt_fixed | 全局 | 把定点整数按 scale 格式化为 "整数.小数" 字符串,返回指向 16 槽环形静态缓冲区的指针 |
parse_fixed | 全局 | 把浮点字符串按 scale 解析为定点值,返回 u32(有符号语义在内部计算) |
parse_dispatch_para | 全局 | 从 Parameter2":" 片段起解析逗号分隔的数字列表到 out[],返回成功解析的字段个数 |
共 3 个函数,全部为全局作用域,无 static 函数、无回调、无函数指针。
4. 核心逻辑解析
4.1 fmt_fixed(s64 raw, u32 scale)
流程:
- 取环形缓冲当前槽
l_p_buf = s_au8FmtFixedBuf[s_u8FmtFixedIdx],随后s_u8FmtFixedIdx = (s_u8FmtFixedIdx + 1) & 15; - 用除 10 循环求
scale的十进制位数:while (l_u32_scale > 1) { l_u8_digits++; l_u32_scale /= 10; }(scale=1→0 位,10→1 位,100→2 位,1000→3 位); - 取绝对值:
l_u32_abs = (raw < 0) ? (u32)(-raw) : (u32)raw; - 拆分整数/小数部分:
l_u32_ip = l_u32_abs / scale,l_u32_fp = l_u32_abs % scale; - 格式化:
snprintf(l_p_buf, 16, "%s%u.%0*u", (raw < 0) ? "-" : "", l_u32_ip, l_u8_digits, l_u32_fp); - 返回缓冲指针。
要点:
- 缓冲区固定 16 字节,
snprintf传的sizeof(s_au8FmtFixedBuf[0])就是 16,天然限长,不会溢出;但s32最小值(如-2147483648)配scale=1时输出"-2147483648"恰好 12 字节,仍在范围内。 - 输出示例:
raw=5000, scale=100→"50.00";raw=-5, scale=100→"-0.05";raw=0, scale=10→"0.0"。 - 同一条语句最多调用 16 次:这是文件头注释明确警告的限制,第 17 次会回卷覆盖第 1 个槽,导致前面已经拼进报文的值被后续调用改写(典型现象是上报报文里出现重复/错位数值)。
raw形参类型是s64,但函数头注释写的是s32;内部(u32)raw强转表明实际有效范围只有 32 位有符号,超过即截断。
4.2 parse_fixed(const char *s, u32 scale)
流程:
- 处理符号:
'-'置l_s32_sign = -1并前移,'+'仅前移; - 整数部分:
while (*s >= '0' && *s <= '9') l_s32_ip = l_s32_ip * 10 + (*s - '0');(无位数限制,直接累加到s32); - 若遇
.:先s++,再循环小数位。循环里维护l_u32_rem_scale = scale:只要rem_scale >= 10就rem_scale /= 10并累加一位小数(l_s32_fp = l_s32_fp * 10 + digit);当rem_scale降到 1 后,剩余小数位只s++不取值(截断)。循环结束后l_s32_fp *= rem_scale补齐位数; - 返回
l_s32_sign * (l_s32_ip * scale + l_s32_fp)(表达式按s32计算,函数返回类型为u32)。
解析示例(scale=100):"1.234" → 整数 1,小数位取两位 23,rem_scale 最终为 1,结果 1*100 + 23 = 123(截断到 1.23);"1.2" → 小数 2,rem_scale 降到 10,结果 100 + 2*10 = 120(即 1.20);"-0.5" → -(0 + 50) = -50。
要点与隐患:
- 小数位解析是「按 scale 的十进制位数截断/补零」,因此
scale必须是 10 的幂;若传入scale=8,rem_scale从 8 起不满足>= 10,小数部分永远为 0,结果退化成整数。 - 返回类型为无符号
u32,负结果按二进制补码回绕(-50→4294967246)。调用方net/server.c中既有l_u32_res = parse_fixed(...)(28 处 scale=1、25 处 scale=10、14 处 scale=100),也有l_s32_res = parse_fixed(...)(少数几处),只有后者的语义是严格正确的;用u32接收有符号量时,后续inverter_set_*若按u32直接使用会得到极大值。 - 整数部分累加与
l_s32_ip * scale都没有溢出保护:scale=100 时整数部分超过约 2147 万就会溢出s32。 - 不校验字符串合法性:
s为NULL、空串、或".5"(无整数位)都能「成功」返回,无错误码。四个调用方(server.c:5874/6009/6082 等)只以json_get_value()是否返回为条件,不做二次校验。
4.3 parse_dispatch_para(const char *s, u32 *out, int max_fields)
流程:
- 前缀校验:
strncmp(s, DISPATCH_PARAMETER2, sizeof(DISPATCH_PARAMETER2) - 1) != 0则直接返回 0(sizeof - 1正好是字面量长度 13,不含结尾\0); - 跳过前缀,进入字段循环,最多
max_fields次:- 跳过空格 → 处理正负号 → 累加整数位(
l_u8_got_digit标记是否至少有一位数字); - 若无任何数字位 →
break(结束解析); - 若遇
.,只s++并跳过所有小数位(小数被完全丢弃,直接截断取整); - 写入
out[l_i_count] = l_s32_sign * l_s32_val并l_i_count++; - 若当前字符是
,则s++并继续下一个字段,否则break;
- 跳过空格 → 处理正负号 → 累加整数位(
- 返回解析成功的字段个数。
要点:
- 该函数只做「个数」判定。调用方
net/server.c:806-816依次尝试max_fields = 8 / 7 / 4,用返回值是否等于该值来区分三种不同长度的调度指令格式(8 参数 / 7 参数 / 4 参数),解析结果写入DISPATCH_CMD_T.f_para[8](logic/dispatch.h:89定义为u32 f_para[8])。 f_para是无符号数组,而函数内部按有符号解析后赋值,负参数(如放电功率)会以大u32形式存放,消费方必须自行做(s32)转换。max_fields由调用方给出,函数内部不校验max_fields与out实际容量。当前调用方最多传 8、f_para也是 8,巧合安全;若将来传入大于数组长度的值就会越界写。
4.4 通信协议帧格式
本文件不涉及收发,无起始码/长度/命令字/校验;它只处理已经收到的报文中的数据字段。不过 DISPATCH_PARAMETER2 这个宏实际上固化了云端下发 JSON 的一个字段名约定:字段必须以 Parameter2 命名,且其值必须是 Parameter2":"<数字>[,<数字>...]" 的形式(无空格)。这是一处隐式协议契约,协议改名会直接导致解析返回 0,而调用方只会把它当作「该字段不存在」处理。
4.5 定时 / uCOS-II 配合
无。三个函数都是纯计算、无阻塞、不可重入风险点集中在 fmt_fixed 的静态缓冲区上(见备注 2)。本文件不调用任何 uCOS-II API,不使用信号量、事件标志或消息邮箱。
4.6 关键全局变量读写
无。本文件不读写 g_t_logic_info / g_t_ems_info 等全局业务结构,所有状态都是 fmt_fixed() 内的函数级 static 变量。
5. 对外接口
导出(供外部调用)——声明于 logic/math_tools.h:
| 名称 | 主要调用者 |
|---|---|
fmt_fixed(s64, u32) | net/ems.c(约 136 处,拼装上报 JSON)、data/resume_data/flash/flash_data_store.c(27 处,写 CSV 历史数据) |
parse_fixed(const char *, u32) | net/server.c(67 处,解析下行安全参数 / 配置,scale 取 1、10、100) |
parse_dispatch_para(const char *, u32 *, int) | net/server.c:806 / 810 / 814(解析 Parameter2 调度参数) |
本文件不导出任何全局变量。
被调用(外部依赖):
| 名称 | 来源 |
|---|---|
snprintf | C 标准库 stdio.h(本文件显式 #include <stdio.h>) |
strncmp | C 标准库 string.h(本文件显式 #include <string.h>) |
FALSE | 公共宏(经 chip.h → includes.h 链引入) |
s64 / u32 / u8 / s32 | system/sys/chip.h |
6. 依赖关系
依赖:
system/sys/chip.h(#include "chip.h")——基础类型与FALSE;- C 标准库
<string.h>、<stdio.h>; logic/math_tools.h(#include "math_tools.h")——自身原型声明(保证定义与声明一致)。
被依赖(include 点):
net/ems.c:59、net/server.c:60、data/resume_data/flash/flash_data_store.c:32、logic/math_tools.c:16。
7. 备注与风险点
- 函数头注释的参数类型已过期:第 20 行写
const char *fmt_fixed(s32 raw, u32 scale),实际定义(26 行、头文件第 13 行)是s64 raw;math_tools.h用的是s64。注释里的「raw 不能超过 32 位」才是真正有效的约束。 fmt_fixed的 16 槽静态缓冲区:整个函数不可重入、不线程安全,且在同一个表达式里调用超过 16 次会静默数据错乱(文件头注释用「数据会被踩掉!!!」强调过)。当前net/ems.c中单条snprintf里最多的调用次数不到 16(例如 8 个单体电流 + 8 个单体 SOC 分属两条语句),属于「刚好没越界」的状态;后续在此类语句中新增字段极易踩线,且失效表现为报文数值错乱而非崩溃,极难定位。建议改为由调用方传缓冲区,或至少增加「超过 16 次」的编译期/运行期保护。parse_fixed返回类型与语义不符:返回值是u32,但内部按有符号计算,负值回绕为接近UINT32_MAX的大数。net/server.c中 67 处调用绝大多数用l_u32_res接收,只有少数用l_s32_res。对功率/电流这类可正可负的配置项而言,用u32接收是明确的类型错误,只是恰好被后续inverter_set_*的形参类型「兜住」,一旦某个 setter 改成u32形参就会变成天文数字。parse_fixed无溢出与合法性校验:整数位累加、l_s32_ip * scale均可溢出;s为NULL会直接解引用崩溃(函数内没有判空);非法输入(空串、.、纯符号)返回 0 且无错误码,调用方无法区分「真的解析到 0」和「解析失败」。parse_fixed/parse_dispatch_para的小数一律截断且scale必须是 10 的幂:传入非 10 的幂(如 8、60)时小数部分被静默丢弃。所有调用点目前只用 1/10/100,属约定俗成而非强制。DISPATCH_PARAMETER2宏定义在函数之间且与协议字段名重复:第 93 行的宏写死"Parameter2\":\"",而协议字段名在net/server.h:21已定义为FIELD_NAME_SPECIAL_CMD_PARA2 "Parameter2"。同一协议字面量在两处各自硬编码(且本文件还额外带上了":"的 JSON 片段格式假设),协议改名时必须同时改两处,且本文件没有#undef,宏在文件内全局可见,有与它处同名宏冲突的隐患。parse_dispatch_para不校验out容量:max_fields完全由调用方决定,函数直接写out[l_i_count]。当前调用方传 8 而f_para[8],刚好安全;新增调用点若传入 >8 会立即越界写DISPATCH_CMD_T相邻字段。parse_dispatch_para的调用契约很脆弱:它要求传入的指针必须正好指向Parameter2":"的首字符,所以server.c不得不用strstr()先搜一遍再传进来;若 JSON 里Parameter2出现在Parameter20之类的相似字段之前,strstr会命中错误位置(截断后的前缀比较仍然是真),从而解析出错误参数。同时字段值里带空格("1, 2, 3")可被容忍(循环开头会跳空格),但带引号/方括号(数组形式)会立刻停止解析。parse_dispatch_para的返回值被当成格式判别码:== 8 / == 7 / == 4三个分支决定调度指令是哪种格式。若云端下发的参数个数恰好是 5 或 6(不在这三个值里),三个分支全部失败,l_u8_recv_flag[1]保持 0,整条指令被静默丢弃且无任何日志(server.c里该处没有 else 分支的记录动作)。fmt_fixed中(u32)(-raw)对raw = INT64_MIN是未定义行为,且任何绝对值超过0xFFFFFFFF的输入都会被静默截断。当前调用方传入的都是u16/s16/u32级别的量,实际不会触发,但类型契约上留有缺口,建议在函数入口做范围断言。(raw < 0)判定用于取符号:raw为-0不可能出现(整型无负零),但scale为 0 会导致l_u32_ip除零崩溃(l_u32_abs / scale)。函数内没有scale == 0保护,也没有对scale非 10 的幂的告警。