diesel_generator.c
GENSET_RUN_TABLE_T 字段(diesel_generator.h:50)3. 函数清单4. 核心逻辑解析4.1 调度与状态机(genset_comm_handle(),:296)4.2 帧格式(Modbus RTU,四个查询函数结构相同)4.3 页轮询(send_query_genset_command(),:228)4.4 数据清零(genset_run_data_clear(),:267)4.5 与 uCOS-II 的配合4.6 关键全局变量的读写5. 对外接口导出(供外部调用)调用(依赖的外部函数/变量)6. 依赖关系7. 备注与风险点源文件路径:
midware/meter/diesel_generator.c所属模块:midware/meter(电表/油机中间件) 代码行数:337
1. 文件职责
本文件实现油机(柴油发电机组)控制器的 Modbus RTU 数据轮询。油机控制器(Genset Controller,从站地址 99)挂在与电表同一条 RS485 总线上(由 usart1 承载),EMS 通过标准 Modbus 功能码 0x03(读保持寄存器)周期性读取四段寄存器:Page 4 基础仪表、Page 5 扩展仪表、Page 7 累积仪表、Page 154 告警状态。
数据流向:genset_comm_handle() 被 midware/meter/meter.c:5876 的 task_meter() 循环每轮调用一次,函数内部按 500ms 节拍轮流发起一次查询,把响应解析进全局表 g_t_diesel_gen;net/ems.c 的 STATUS_SEND_GENSET_DATA 报文(net/ems.c:1585-1617)再把这些字段上报云端。
本文件只有采集和"通讯丢失清零"两类职责,不参与任何能量调度决策——全工程检索表明 g_t_diesel_gen 只被 net/ems.c 读取用于上报,logic/ 层不引用其数据。油机的启停/带载由硬件或人工控制,EMS 侧只做监视。
2. 关键数据结构与宏
| 名称 | 类型 | 说明 |
|---|---|---|
GENSET_DEBUG_INFO | 宏(diesel_generator.h:20) | 0,关闭 printf 调试打印 |
GENSET_ADDR | 宏 | 99,油机控制器的 Modbus 从站地址(GSM/GC 控制器常用的固定地址 0x63) |
DG_PAGE_4_A0_33_REG_ADDR | 宏 | 4*256 = 1024(0x0400),Page 4 起始寄存器号;命名含义为"页 4 的 A0 号寄存器",即用寄存器号高字节编码页号 |
DG_PAGE_5_A10_11_REG_ADDR | 宏 | 5*256+10 = 1290(0x050A),Page 5 的 A10 号寄存器 |
DG_PAGE_7_A6_9_REG_ADDR | 宏 | 7*256+6 = 1798(0x0706),Page 7 的 A6 号寄存器 |
DG_PAGE_154_ALARM_REG_ADDR | 宏 | 154*256 = 39424(0x9A00),Page 154 告警页起始寄存器 |
DG_PAGE_4_A0_33_REG_NUM | 宏 | 34,Page 4 一次读 34 个寄存器(A0–A33) |
DG_PAGE_5_A10_11_REG_NUM | 宏 | 2,Page 5 一次读 2 个寄存器(A10–A11) |
DG_PAGE_7_A6_9_REG_NUM | 宏 | 4,Page 7 一次读 4 个寄存器(A6–A9) |
DG_PAGE_154_ALARM_REG_NUM | 宏 | 9,Page 154 一次读 9 个寄存器 |
DG_QUERY_SPEED_VALUE | 宏 | 500,查询节拍 500ms(两个不同页之间至少间隔 500ms) |
DG_COMM_LOST_VALUE | 宏 | 1000*15 = 15000ms,15 秒内无任何有效响应即判定通讯丢失 |
Query_DG_E | 枚举 | 查询页顺序:PAGE_4_BASIC_E=1、PAGE_5_EXTEND_E=2、PAGE_7_ACCUMULATED_E=3、PAGE_154_ALARM_E=4、PAGE_N_MAX_E=5(同时也是"回到起点"的哨兵值) |
GENSET_RUN_TABLE_T | 结构体 | 油机运行数据表,见下节 |
GENSET_CONTROL_CMD_TABLE_T | 结构体 | 控制/节拍表:u32_get_data_last_time(最近一次有效响应时刻)、u32_dg_query_tick(上一次发起查询的时刻) |
DIESEL_GENERATOR_TABLE_T | 结构体 | 汇总表:t_dg_run_data + t_dg_ctrl_cmd,全局实例 g_t_diesel_gen |
METER_READ_FUN_CODE | 宏(meter.h:47) | 0x03,Modbus 读保持寄存器功能码 |
GENSET_RUN_TABLE_T 字段(diesel_generator.h:50)
| 字段 | 类型 | 单位/来源页 | 说明 |
|---|---|---|---|
u16_dg_fuel_level | u16 | 1%(Page 4) | 燃油液位 |
u16_dg_engine_bat_volt | u16 | 0.1V(Page 4) | 发动机启动电池电压 |
u32_dg_fuel_consumption | u32 | 0.01 L/h(Page 5 A10–A11) | 燃油消耗率(32bit 跨 2 寄存器) |
u32_dg_volt_a/b/c | u32 | 0.1V(Page 4) | 三相电压,每相跨 2 个寄存器 |
u32_dg_cur_a/b/c | u32 | 0.1A(Page 4) | 三相电流 |
s32_dg_actpower_a/b/c | s32 | 1W(Page 4) | 三相有功功率,有符号(发电为正、反相为负) |
u32_dg_positive_energy | u32 | 0.1kWh(Page 7 A6–A9) | 正向累积发电量 |
u16_dg_freq | u16 | 0.1Hz(Page 4) | 发电机频率 |
u32_dg_engine_run_time | u32 | 1s(Page 7) | 发动机累计运行时间 |
u16_alarm[8] | u16[8] | 位域(Page 154) | 8 个 16bit 告警字,展开成 128 个告警位上报云端 |
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
ems_query_genset_page4_command | static | 组帧并发送 Page 4(A0–A33,34 寄存器)读请求,解析 73 字节响应中的电压/电流/功率/频率/油位/电池电压 |
ems_query_genset_page5_command | static | 组帧并发送 Page 5(A10–A11)读请求,解析燃油消耗率 |
ems_query_genset_page7_command | static | 组帧并发送 Page 7(A6–A9)读请求,解析累计运行时间与正向发电量 |
ems_query_genset_page154_command | static | 组帧并发送 Page 154(9 寄存器)读请求,解析 8 个告警字 |
send_query_genset_command | static | 页轮询调度器:用静态游标依次切换 4 个页并调用对应查询函数 |
genset_run_data_clear | static | 把 t_dg_run_data 全部清零,告警字填 4369(0x1111) 表示无效 |
genset_comm_handle | 全局 | 对外入口:判断使能、按 500ms 节拍发起轮询、15s 无响应时清数据 |
本文件另在 :25-30 有 6 行前置声明(4 个页查询 + send_query_genset_command + genset_run_data_clear)。
4. 核心逻辑解析
4.1 调度与状态机(genset_comm_handle(),:296)
每轮 task_meter() 调用一次
│
├─ genset_enable_flag == FALSE ──► u32_get_data_last_time = now
│ (若此前在通讯中,调 genset_run_data_clear())
│
└─ TRUE
├─ (now - u32_dg_query_tick) >= 500ms ──► tick = now; send_query_genset_command(); s_genset_comm = TRUE
│ (若 tick > now,说明 u32 节拍回绕 → tick = now)
│
└─ (now - u32_get_data_last_time) >= 15000ms ──► last_time = now
若 s_genset_comm == TRUE → 清数据并置 FALSE
要点:
- 轮询节拍:
u32_dg_query_tick记录上次"决定查询"的时刻,abs(OSTimeGet() - tick) >= 500才发起下一次查询。由于 4 个页依次轮询,一个完整循环(4 个页)周期约 2 秒(500ms × 4,再加各页阻塞收发耗时)。因此油机数据的最坏刷新延迟约 2 秒。 - 节拍回绕保护:
if (u32_dg_query_tick > OSTimeGet()) → tick = now。OSTimeGet()是 uCOS-II 的 32 位 ms 节拍,约 49.7 天回绕一次;这个分支专门处理回绕,避免长时间停止轮询。 - 通讯丢失判定:
u32_get_data_last_time只在任一页查询收到长度与字节数都匹配的响应时被刷新(四个页查询函数里都有g_t_diesel_gen.t_dg_ctrl_cmd.u32_get_data_last_time = OSTimeGet();)。15 秒内没有任何一页成功,且本机确实发起过查询(s_u8_genset_comm == TRUE),才调用genset_run_data_clear()把数据清零。s_u8_genset_comm的作用是避免"从未通讯过"时就清零(清一次即可,不用反复清)。 - 失能处理:
u8_genset_enable_flag == FALSE时每轮都把u32_get_data_last_time刷成当前时刻(防止重新使能瞬间触发丢失判定),若之前处于通讯中则清一次数据。 - 无重试机制:单页查询失败不做重发,只是该页数据保持旧值;下一轮调度会跳到下一页,等 4 帧后(约 2 秒)才再次轮到它。
4.2 帧格式(Modbus RTU,四个查询函数结构相同)
请求帧(8 字节)
| 偏移 | 内容 |
|---|---|
| 0 | GENSET_ADDR = 0x63 (99) |
| 1 | METER_READ_FUN_CODE = 0x03 |
| 2–3 | 起始寄存器号,大端 |
| 4–5 | 寄存器数量,大端 |
| 6–7 | CRC-16/Modbus,低字节在前(:54-55 明确注释"高位在后面") |
组帧实现:先在 6 字节的 query_genset_cmd[6] 里拼载荷,memcpy 到 l_u8_tx_buf[8],再 GetCrcCode_Modbus(l_u8_tx_buf, 6) 追加 CRC。
响应帧(标准 Modbus 0x03 响应)
| 偏移 | 内容 |
|---|---|
| 0 | 从站地址 |
| 1 | 功能码 0x03 |
| 2 | 字节数 = 寄存器数 × 2 |
| 3.. | 寄存器数据,每寄存器 大端 2 字节 |
| 末尾 2 字节 | CRC |
响应有效性判定(四个函数一致,以 Page 4 为例,:59):
if ((l_u16_recv_num > 0) && (g_u8_recv_buf[2] == DG_PAGE_4_A0_33_REG_NUM*2))
即仅校验"收到了数据"且"字节数字段等于期望值",不校验从站地址、功能码,也不校验帧总长。CRC 校验在 meter_recv_data()(meter.c:66)内部完成。
四个页的请求参数与解析偏移:
| 页 | 起始寄存器 | 数量 | 期望字节数 | 响应总长 | 解析字段(缓冲区下标,数据区自 buf[3] 起) |
|---|---|---|---|---|---|
| Page 4 | 0x0400 | 34 | 68 | 73 | 油位 [9..10](寄存器 A3)、电池电压 [13..14](A5)、频率 [17..18](A7)、Ua [19..22](A8-9)、Ub [23..26](A10-11)、Uc [27..30](A12-13)、Ia [43..46](A20-21)、Ib [47..50](A22-23)、Ic [51..54](A24-25)、Pa [59..62](A28-29)、Pb [63..66](A30-31)、Pc [67..70](A32-33) |
| Page 5 | 0x050A | 2 | 4 | 9 | 燃油消耗率 [3..6](A10–A11 拼成 32bit) |
| Page 7 | 0x0706 | 4 | 8 | 13 | 累计运行时间 [3..6](A6-7)、正向发电量 [7..10](A8-9) |
| Page 154 | 0x9A00 | 9 | 18 | 23 | 告警字 alarm[0..7] 取 [5..20],即跳过第一个寄存器(A0),使用 A1–A8 |
所有 32bit 量的拼接方式统一为 (b0<<24)|(b1<<16)|(b2<<8)|b3(大端),存进 u32;有功功率额外做 (s32) 强转保留符号。
4.3 页轮询(send_query_genset_command(),:228)
static u8 g_u8_query_dg_cmd = PAGE_N_MAX_E; // 静态游标,初值 5
if (g_u8_query_dg_cmd >= PAGE_N_MAX_E) g_u8_query_dg_cmd = PAGE_4_BASIC_E; // 回到第 1 页
else g_u8_query_dg_cmd++;
switch (g_u8_query_dg_cmd) { case 1..4: 调对应页查询; default: 游标复位; }
- 游标用"先自增/复位、再 switch"的方式实现环形轮询,顺序固定为 Page 4 → 5 → 7 → 154 → 4…。
default分支把游标置回PAGE_N_MAX_E(5),下一轮即从 Page 4 重新开始,属于防御性兜底。- 该函数是阻塞的:每个页查询内部完成一次完整的 Modbus 事务(写 + 读),因此在
task_meter()的单次调用里最多消耗一次总线事务时间。
4.4 数据清零(genset_run_data_clear(),:267)
逐字段赋值 0,最后把 8 个告警字都置为 4369:
for (u8 i = 0; i < 8; i++) g_t_diesel_gen.t_dg_run_data.u16_alarm[i] = 4369;
4369 = 0x1111,是"告警数据无效"的魔数标记(云端借此区分"确实无告警(0)"与"通讯丢失(0x1111)")。函数不清 t_dg_ctrl_cmd(节拍字段由调用方维护)。
4.5 与 uCOS-II 的配合
- 本文件不使用信号量、互斥量或消息邮箱,所有时序靠
OSTimeGet()读取的 32 位毫秒节拍做非阻塞比较。 - 唯一的阻塞点是底层
usart1_send_data()(超时 1000ms,见meter.c:57)与meter_recv_data()。meter_recv_data()是非阻塞轮询:只有usart1_is_recv_async_data()为真时才读取并把*len写成实际长度,否则立刻把*len = 0(meter.c:110)。因此油机查询不会长时间挂在接收等待上,这依赖上一轮的响应已经完整落到 USART1 接收缓冲——也就是说油机查询实际上依赖"500ms 轮询周期 > Modbus 事务往返时间"这一隐含时序假设。 - 任务上下文:唯一的调用方是
task_meter()(由user/main.c静态创建的电表任务),其循环体尾部有OSTimeDlyHMSM(0,0,0,300)延时(meter.c:5881)与IWDG_Feed()。
4.6 关键全局变量的读写
| 变量 | 写方 | 读方 | 触发条件 |
|---|---|---|---|
g_t_diesel_gen.t_dg_run_data.* | 本文件四个页查询函数、genset_run_data_clear() | net/ems.c:1585-1617(组上报 JSON) | 收到对应页有效响应时写;15s 无响应或油机失能时清零 |
g_t_diesel_gen.t_dg_ctrl_cmd.u32_get_data_last_time | 页查询函数、genset_comm_handle() | 本文件丢失判定 | 每次有效响应刷新;丢失/失能时也刷新 |
g_t_diesel_gen.t_dg_ctrl_cmd.u32_dg_query_tick | genset_comm_handle() | 本文件节拍判定 | 每次发起查询时刷新 |
g_t_meter.t_config_data.u8_genset_enable_flag | HMI hmi/key.c:8255、Modbus 从站 midware/interface/modbus/modbus.c:8068 | 本文件 :300 | 用户在 LCD 或上位机写寄存器开启/关闭油机监视 |
g_u8_recv_buf(meter.c:40,256 字节,extern) | meter_recv_data() | 本文件四个页查询函数 | 共享的总线接收缓冲,与电表查询复用同一块内存 |
5. 对外接口
导出(供外部调用)
| 名称 | 声明位置 | 调用方 |
|---|---|---|
void genset_comm_handle(void) | diesel_generator.h:86 | midware/meter/meter.c:5876(task_meter() 主循环) |
DIESEL_GENERATOR_TABLE_T g_t_diesel_gen | diesel_generator.h:83(extern) | 定义于本文件 :20;被 net/ems.c 读取上报 |
调用(依赖的外部函数/变量)
| 名称 | 来源 |
|---|---|
meter_write_data() / meter_recv_data() | midware/meter/meter.h:457-458(实现于 meter.c:51/66,走 usart1) |
g_u8_recv_buf[] | midware/meter/meter.h:451(大小 METER_RECV_BUFFER_SIZE = 256) |
g_t_meter.t_config_data.u8_genset_enable_flag | midware/meter/meter.h:410 |
METER_READ_FUN_CODE | midware/meter/meter.h:47 |
GetCrcCode_Modbus() | common/crc.h:17 |
OSTimeGet() | uCOS-II(经 includes.h) |
memcpy / abs | 标准 C 库(abs 经 includes.h / stdlib.h) |
TRUE / FALSE | includes.h |
6. 依赖关系
- 依赖:
diesel_generator.h、includes.h、midware/meter/meter.h(电表总线读写与配置结构)、common/crc.h(Modbus CRC16)。 - 被依赖:
midware/meter/meter.c:5876(唯一调用genset_comm_handle()的地方)。net/ems.c:1585-1617(读g_t_diesel_gen组CMD_SEND_GENSET_DATA报文)、net/ems.c:5021(用u8_genset_enable_flag决定上报时机,间隔 300 个 RTC 节拍)。net/server.c、hmi/key.c、midware/interface/modbus/modbus.c通过u8_genset_enable_flag间接参与(不含g_t_diesel_gen数据)。
7. 备注与风险点
- 响应长度未校验,存在越界读风险——四个查询函数只检查
g_u8_recv_buf[2]等于期望字节数,然后直接索引到g_u8_recv_buf[70](Page 4)。虽然g_u8_recv_buf有 256 字节(meter.h:92)不会越界读内存,但会把上一帧的残留数据当成本帧数据解析:meter_recv_data()只在 CRC 正确时把*len设为实际长度,而查询函数传入的l_u16_recv_num被置为非 0 后就完全不使用它做长度比对。正确做法是同时校验l_u16_recv_num >= 3 + 期望字节数 + 2。 - 不校验从站地址与功能码——
:59的条件里没有g_u8_recv_buf[0] == GENSET_ADDR和g_u8_recv_buf[1] == 0x03。异常响应帧(addr + 0x83 + 异常码 + CRC)的buf[2]是异常码,恰好等于 68/4/8/18 的概率极低,所以实际风险有限,但这是协议实现上的薄弱点。 - 拷贝全局接收缓冲导致耦合——四个页查询都直接使用
g_u8_recv_buf(电表模块的全局缓冲)而不做深拷贝。任何在油机查询之后、解析之前插入的其他总线事务都会污染数据(当前调用序列中不存在这种情况,属潜在脆弱点)。 p4页首寄存器被跳过——Page 154(9 个寄存器)的 8 个告警字取自buf[5..20],即跳过第 0 个寄存器(A0)。从命名与实现看 A0 可能是"运行状态字"而非告警位,但代码中没有任何注释说明,属寄存器映射未文档化的隐患。u8_genset_enable_flag从 HMI/Modbus 写入时无互斥——该字段由其他任务写入(hmi/key.c、modbus.c),本文件在task_meter()中读取;理论上存在读到中间态的可能(u8单字节读写在本平台是原子的,实际安全)。- 失能不清
u32_dg_query_tick——重新使能后,若u32_dg_query_tick停留在很久以前,第一次调用会立即发起查询(这是期望行为),但如果停留在"未来"(节拍回绕后残留),会走tick > now分支重设,行为正确。 - 响应延迟假设——
meter_recv_data()是非阻塞的,因此油机查询在"响应尚未到达"时会静默失败并把该页数据保留旧值。500ms 的DG_QUERY_SPEED_VALUE是唯一保证,代码里没有任何显式的"等待响应"或"失败重发"逻辑。若总线负载加重(电表查询与油机查询共用usart1),这一假设可能不成立。 - 魔数——
:286的告警填值4369(0x1111) 无宏定义、无注释;:154/:175的寄存器地址用page*256+n的算术写法,隐含"页号高字节编码"这一非标准 Modbus 约定,需对照油机控制器手册才能确认。 - 代码重复严重——四个页查询函数结构几乎完全相同(组帧 → 发送 → 校验 → 逐字段挑字节),只有寄存器地址/个数/解析偏移不同;单帧接收解析逻辑可用表驱动,现实现使得新增一页必须复制约 50 行代码。
l_u8_tx_buf[8]+query_genset_cmd[6]冗余——先填 6 字节数组再memcpy到 8 字节数组,多一次拷贝,没有实际必要。printf调试分支保留但关闭——GENSET_DEBUG_INFO为 0,printf会被预处理器剔除,不影响运行;但四个函数各有 12 行的 printf 块,占源码篇幅较大。