ginlong_comm_can.c
ginlong_comm_can.c:301-302)4.3 帧格式(CAN 标准帧,11 位 ID)4.4 0x191 DC 功率控制的下发限幅逻辑(ginlong_comm_can.c:102-180)4.5 0x198 三相不平衡控制的下发逻辑(ginlong_comm_can.c:189-287)4.6 接收解析与超时保护(ginlong_comm_can.c:348-372)4.7 发送节拍与互斥4.8 节拍回绕保护5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
midware/inverter/ginlong/ginlong_comm_can.c所属模块:midware / inverter / ginlong(锦浪逆变器通信) 代码行数:374
1. 文件职责
本文件实现 EMS 与锦浪(Ginlong/Solis)储能逆变器之间的 CAN 实时控制通道。它负责三件事:上电后通过 0x181 握手报文与逆变器建立联系(对方回 0x201 后才认为通道可用);按固定节拍向逆变器下发功率设定值(单相 DC 控制用 0x191,三相不平衡控制用 0x198);并从逆变器上报的 0x269 运行信息包中回读 T 相有功功率、电网频率与电池功率,写入全局运行数据结构 g_t_inv_info.t_run_info。
该通道是"下发为主、回读为辅"的实时控制链路,与同目录的 ginlong_comm_usart.c(Modbus-RTU 参数配置主通道)互补:串口通道负责大量寄存器读写与参数配置,CAN 通道负责毫秒级功率跟踪。它由 midware/inverter/inverter.c 的逆变器轮询任务周期调用(inverter.c:1239),自身不含任务与阻塞等待(除固定的发送间隔延时)。
2. 关键数据结构与宏
本文件自身不定义结构体,使用的宏全部来自 ginlong_comm_can.h,关键结构来自头文件。
| 名称 | 类型 | 说明 |
|---|---|---|
GINLONG_EMS_INV_IDCODE_PACK | 宏 | 0x181,EMS 握手报文 ID,数据域为 ASCII AlphaESS |
GINLONG_EMS_CTRL_INV_DC_POWER1 | 宏 | 0x191,DC 功率控制报文 ID |
GINLONG_EMS_CTRL_THREE_UNBALANCE_POWER | 宏 | 0x198,三相不平衡功率控制报文 ID |
GINLONG_INV_EMS_IDCODE_PACK | 宏 | 0x201,逆变器握手应答 ID |
GINLONG_INV_EMS_RETURN_RUN_INFO_PACK5 | 宏 | 0x269,逆变器运行信息包 5 |
GINLONG_INV_CAN_DEBUG | 宏 | 调试打印开关,默认 0 |
GINLONG_COMM_CAN_T | 结构体 | CAN 通道节拍状态表(ginlong_comm_can.h:31),含握手标志与 3 个发送节拍 |
g_t_ginlong_comm_can_info | 全局变量 | GINLONG_COMM_CAN_T 的唯一实例,本文件第 26 行定义 |
CAN_MSG_TYPEDEF_T | 结构体 | CAN 帧描述结构(hardware/can/can0.h:21,#pragma pack(1)):frame_id / frame_type / frame_len / frame_data[8] / reading_flag |
CAN_FT_DATA | 宏 | 数据帧类型标志,传给 frame_type |
GINLONG_TABLE_T | 结构体 | 锦浪全量数据表,经 g_t_ginlong_info 访问其 t_ginlong_comm_info.u8_ems_inv_comm_flag |
INVERTER_TABLE_T | 结构体 | 逆变器全量表,g_t_inv_info 提供 t_config_data(额定功率)与 t_run_info(运行数据) |
cNormalMode | 宏 | 3(midware/inverter/inverter.h:23),逆变器正常运行模式 |
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
CAN_Send_Msg | static | CAN 发送封装:保证相邻两帧间隔 ≥15 ms(不足则 OSTimeDlyHMSM 补齐),再调 can1_send_standard_pack |
send_ems_inv_idcode_cmd | static | 组帧并发送 0x181 握手报文,数据域填 ASCII AlphaESS(8 字节) |
ginlong_comm_init_can | 全局 | 初始化 CAN1:若分频系数非 8 则调用 can1_mode_init(8),并把握手标志置 TRUE |
send_dc_ctrl_inv_power_1 | static | 组织并发送 0x191 单相 DC 功率控制帧:直流设定功率、有功设定、T 相电表有功、无功设定 |
send_three_unbalance_power_ctrl | static | 组织并发送 0x198 三相不平衡控制帧:直流设定功率 + A/B/C 三相独立有功设定 |
ginlong_inverter_comm_can | 全局 | CAN 通道周期主函数:固件升级期间挂起;未握手时 1 s 发一次握手;已握手时按 600 ms / 2000 ms 分频下发控制帧,并循环收包解析 |
4. 核心逻辑解析
4.1 握手状态机
握手用两个标志表示,状态迁移如下(ginlong_comm_can.c:304-346):
[上电] ginlong_comm_init_can() → u8_ems_inv_idcode_flag = TRUE
│
├─ flag == TRUE(未握手)
│ └─ (OSTimeGet() - u32_ems_inv_idcode_tick) > 1000ms
│ → send_ems_inv_idcode_cmd() (0x181, "AlphaESS")
│ → u32_ems_inv_idcode_tick = OSTimeGet()
│
└─ 收到 frame_id == 0x201(GINLONG_INV_EMS_IDCODE_PACK)
→ u8_ems_inv_idcode_flag = FALSE ← 进入控制态
- 未握手时只发握手帧,不发任何控制帧,避免逆变器未就绪时收到无效功率指令。
- 握手上限 1 s 一次,即最坏 1 s 后才可能重新尝试。
- 控制态下不再重发握手(没有回转路径),因此一旦逆变器复位导致链路重建,需等待上层重新调用
ginlong_comm_init_can()才会重新握手。
4.2 通道使能条件(ginlong_comm_can.c:301-302)
if(!(is_inverter_firm_download_finished() || Get_Status_Is_Updated_From_Server(UPDT_INV3))
&& TRUE == g_t_ginlong_info.t_ginlong_comm_info.u8_ems_inv_comm_flag)
两个条件都必须满足才进入通信体:
- 未在固件升级流程中:
is_inverter_firm_download_finished()(固件已下载完、正在串口烧写)为假,且服务器下发的UPDT_INV3(ARM 升级任务)状态为假。升级期间 CAN 通道必须静默,防止干扰串口烧写。 - 串口主通道已建立:
u8_ems_inv_comm_flag == TRUE(该标志由ginlong_comm_usart.c维护)。
也就是说,CAN 通道从属于串口通道:串口没通,CAN 不发。
4.3 帧格式(CAN 标准帧,11 位 ID)
所有帧均为 frame_type = CAN_FT_DATA、frame_len = 8,数据域大端(高字节在前):
| 报文 | ID | 方向 | 周期 | Byte0-1 | Byte2-3 | Byte4-5 | Byte6-7 |
|---|---|---|---|---|---|---|---|
| 握手 | 0x181 | EMS→INV | 1000 ms | AlphaESS ASCII(8 字节固定) | |||
| DC 控制 | 0x191 | EMS→INV | 600 ms | s16 DC 设定功率 | s16 AC 有功设定 | s16 T 相电表有功 | s16 AC 无功设定 |
| 三相不平衡 | 0x198 | EMS→INV | 2000 ms | s16 DC 设定功率 | s16 A 相有功 | s16 B 相有功 | s16 C 相有功 |
| 握手应答 | 0x201 | INV→EMS | — | 无数据解析,仅改标志 | |||
| 运行信息 5 | 0x269 | INV→EMS | 逆变器自定 | s16 T 相有功 | u16 电网频率 | Byte4 丢弃;Byte5-6 s16/u16 电池功率 | Byte7 丢弃 |
组帧写法统一为 (u8)(val >> 8 & 0x00ff) 与 (u8)(val >> 0 & 0x00ff)(ginlong_comm_can.c:170-177、277-284),即大端序。CAN 层不做校验和,校验依赖 CAN 控制器自身的 CRC 字段(硬件实现)。
4.4 0x191 DC 功率控制的下发限幅逻辑(ginlong_comm_can.c:102-180)
数据来源:parallel_read_dc_total_set_bat_power()(并机模块算出的直流设定总电池功率)与 parallel_read_dc_total_set_inv_power()(逆变侧设定有功)。
- 直流功率限幅:正数(放电)不得超过
g_t_inv_info.t_config_data.u32_dc_rate_discharge_power;负数(充电)绝对值不得超过u32_dc_rate_charge_power(:121-128)。 - 防掉零延时:若本次设定功率非 0,刷新
s_u32_dc_set_power_tick;随后若「电池禁充(pvcharger_read_bat_max_charge_power() == 0)且可放电(pvcharger_read_bat_max_dischg_power() != 0)且设定值在[0,100)且距上次非零设定不足 60 s」,则强制把设定功率抬到100(:137-141)。目的是避免电池禁充期间功率被压到 0 导致逆变器停机,强制留一个小功率维持运行。 - 有功设定特殊处理(
:143-150):超过u32_ac_rate_discharge_power则钳位;<= 90则无条件改写为 40。这是锦浪逆变器的经验值处理,属于隐式约定。 - Byte4-5 填入 T 相电表有功:
meter_read_actpower(GRID_METER, PHASE_T)(:174-175),供逆变器做并网点功率闭环参考。 - 无功设定
l_s16_ac_set_reactpower恒为 0(:113),随后的限幅代码(:152-159)对其不产生实际效果。
4.5 0x198 三相不平衡控制的下发逻辑(ginlong_comm_can.c:189-287)
- 数据来源:
parallel_read_dc_three_phase_set_bat_power()与parallel_read_dc_three_phase_set_inv_power(PHASE_L1/L2/L3)。 - 单相额定功率
l_u16_ac_single_rate_power = u32_ac_rate_charge_power / 3(:205),每相设定值以此为上限钳位。 - 随机化抖动:任一相设定值
< 20时改为rand() % 20(:230、:239、:248)。代码注释明确说明原因——"固定值改随机值是因为锦浪接收到各相设置功率一致时会控不住"(:223)。 - 非正常模式强制 5000 W:若
inverter_read_run_mode() != cNormalMode,三相设定全部强制为5000,并记s_u32_delay_tick = OSTimeGet();切回cNormalMode后仍需保持 5000 输出 5 s(:251-264)。这是逆变器离开并网模式(如故障/待机)时的保持逻辑。
4.6 接收解析与超时保护(ginlong_comm_can.c:348-372)
l_u32_inv_recv_tick = OSTimeGet();
while (get_can1_standard_pack(&l_t_inveter_info) == TRUE) {
switch (frame_id) { ... }
if (abs(l_u32_inv_recv_tick - OSTimeGet()) > 50) break; // 50 系统节拍总时长上限
}
- 用
while把 CAN 接收 FIFO 一次性排空,但总耗时被 50 拍保护,防止在高负载下长时间独占任务。 0x201:清握手标志。0x269:frame_data[0..1]→t_run_info.s32_inv_actpower_t;frame_data[2..3]→t_run_info.u16_grid_freq;frame_data[5..6]→t_run_info.s16_bat_power。注意s16_bat_power被赋成(u16)(...),赋值时再做符号转换。0x191/0x198没有发送应答校验:这两条控制帧发出去后不检查逆变器是否回执,只靠周期性重发(600 ms / 2000 ms)保证最终一致性,属于开环控制。
4.7 发送节拍与互斥
CAN_Send_Msg()(:38-50)通过静态变量 s_u32_send_tick 实现全模块级的最小 15 ms 帧间隔:
if (abs(OSTimeGet() - s_u32_send_tick) < 15) OSTimeDlyHMSM(0,0,0,15);
s_u32_send_tick = OSTimeGet();
这是唯一会阻塞的地方(最长 15 ms),目的是避免 CAN 总线突发拥塞。注意:延时用的是固定 15 ms 而非差额,因此实际帧间隔会略大于 15 ms。由于该函数被 ginlong_inverter_comm_can() 主线调用,而主线本身可能同时发握手帧和控制帧(理论上不会,因为握手与控制在 if/else 两个分支),实际不会造成叠加。
4.8 节拍回绕保护
三处节拍比较都附带 else if (tick > OSTimeGet()) tick = OSTimeGet();(:311、:327、:341)。这是 OSTimeGet() 32 位毫秒计数器回绕(约 49.7 天)时的兜底:一旦发现记录值超前于当前值,直接同步回当前值,避免后续 OSTimeGet() - tick 出现巨大正数而永久跳过发送。
5. 对外接口
本文件导出(供外部调用):
| 符号 | 类型 | 调用方 |
|---|---|---|
ginlong_comm_init_can | 函数 | inverter.c:83(inverter_state_init()) |
ginlong_inverter_comm_can | 函数 | inverter.c:1239(逆变器轮询任务,仅在 get_inverter_type() == GINLONG_TYPE 且非透传模式时) |
g_t_ginlong_comm_can_info | 全局变量 | 本文件内部使用;因头文件未 extern,外部实际不可见 |
本文件调用的外部函数/变量:
| 名称 | 来源模块 |
|---|---|
can1_send_standard_pack、get_can1_standard_pack、can1_mode_init、get_can1_prescaler、CAN_MSG_TYPEDEF_T、CAN_FT_DATA | hardware/can/can1.h、hardware/can/can0.h |
OSTimeGet、OSTimeDlyHMSM | uCOS-II(UCOSII/) |
parallel_read_dc_total_set_bat_power、parallel_read_dc_total_set_inv_power、parallel_read_dc_three_phase_set_bat_power、parallel_read_dc_three_phase_set_inv_power | midware/inverter/parallel.h(并机功率分配) |
pvcharger_read_bat_max_charge_power、pvcharger_read_bat_max_dischg_power | midware/inverter/pvcharger.h |
meter_read_actpower、GRID_METER、PHASE_T | midware/meter/meter.h |
inverter_read_run_mode、inverter_get_three_unbalance_mode、cNormalMode、g_t_inv_info | midware/inverter/inverter.h |
is_inverter_firm_download_finished | net/inv_download.h |
Get_Status_Is_Updated_From_Server、UPDT_INV3、g_u8_fw_sdram1/2 | net/upd_download.h、net/server.h |
g_t_ginlong_info | midware/inverter/ginlong/ginlong_comm_usart.h |
abs、rand | C 标准库 |
6. 依赖关系
依赖(include): ginlong_comm_can.h、ginlong_comm_usart.h、inverter.h、meter.h、bms.h、logic.h、upd_download.h、inv_download.h、can1.h(ginlong_comm_can.c:13-21)。其中 bms.h 与 logic.h 在文件中未见直接引用,属于冗余 include。
被依赖:
midware/inverter/inverter.c:inverter_state_init()调用ginlong_comm_init_can()(inverter.c:83);逆变器轮询调用ginlong_inverter_comm_can()(inverter.c:1239)。- 与
midware/inverter/ginlong/ginlong_upgrade.c间接互斥:ginlong_upgrade.c通过usart2独占升级链路,ginlong_comm_can.c通过Get_Status_Is_Updated_From_Server(UPDT_INV3)判断是否让出 CAN 通道。
7. 备注与风险点
- 固定 15 ms 延时可能拉长控制周期:
CAN_Send_Msg()(:42-47)在间隔不足时无条件OSTimeDlyHMSM(0,0,0,15),若一次轮询内连续发两帧会阻塞 15 ms × N。当前控制周期 600 ms / 2000 ms 余量充足,但若将来缩短周期需重新评估。 abs()用于u32运算:abs(OSTimeGet() - s_u32_send_tick)、abs(l_u32_inv_recv_tick - OSTimeGet())的实参是u32相减,先隐式转int再取绝对值。当差值超过INT_MAX(约 24.8 天)时结果未定义,长期运行(>24 天)理论上存在隐患。rand() % 20依赖全局随机数种子::230/239/248处使用rand()但本文件未srand(),若工程其他位置也未播种,抖动序列每次上电相同。这是锦浪控不住问题的"规避手段",并无协议依据,属于经验性 workaround。<= 90 → 40硬编码(:147-150):AC 有功设定值在(0, 90]区间被无条件改写为 40,既无注释也无配置项,容易被误认为 bug,实为逆变器特性适配。0x269数据解析跨字节错位嫌疑::361用frame_data[5]/frame_data[6]组合电池功率,跳过了frame_data[4];:359-360只用了[0..3]。若逆变器实际协议布局不同,s16_bat_power会取错字节。建议对照锦浪 CAN 协议文档复核。s16_bat_power赋值类型不匹配::361右侧为(u16)强制转换,左侧为s16,虽结果等价于位重解释,但写法上易误导。- 握手无重试回转:一旦
u8_ems_inv_idcode_flag被清成FALSE,本文件不再重发0x181(:304)。若逆变器中途重启,只能依赖上层重新调用ginlong_comm_init_can()。 - 接收循环的 50 拍限制:
:368的abs(...) > 50若因回绕产生大正数会立即break,属于安全方向的失效(少收而非死循环),可接受。 - 冗余 include:
bms.h、logic.h未在本文件直接使用;函数注释多处沿用kelong模块的模板(:53、:78、:95、:182、:289),说明由其他厂商模块复制演进而来。