dispatch.c
frequency_regulation_detection()(:235)4.3 调频指令执行:excute_frequency_regulation_command()(:529)4.4 频率读取与单位换算(:598、:620)4.5 云端调度超时与老化结果:dectect_api_dispatch_comm()(:748)4.6 老化 6 态循环:handle_aging_mode()(:877)4.7 系统调度总分派:excute_system_dispatch_command()(:1392)4.8 各模式执行体的功率限值算法4.9 馈网优先级:dispatch_api_maxfeedin_priority()(:210)4.10 上报表与影子副本4.11 定时与 uCOS-II 配合4.12 g_t_dispatch_t 的读写全景4.13 通信协议帧格式5. 对外接口6. 依赖关系7. 备注与风险点源文件路径:
logic/dispatch.c所属模块:logic(能量调度核心 —— 云端 EverGen 系统调度 / FCAS-FFR 调频调度 / 老化测试) 代码行数:1615
1. 文件职责
本文件是 EMS 的「调度」执行中枢,把网络层下发的两类指令翻译成对 g_t_logic_info.t_logic_control_parameter 的功率限值写入:一类是云端 EverGen 系统调度(按 u8ControlMode 分派到 14 个 handle_* 执行体,覆盖光伏优先充电、负载跟随、最大输出、最大消纳、经济模式、电池禁充、西澳 AC/DC 调度、SunSpec 有功调度、老化测试等),另一类是本地调频调度(FCAS/FFR 共 14 种模式,实现阶跃与下垂两种频率响应算法并直接驱动充放电)。它同时负责两类指令的超时退出:超时后清空调度状态、恢复 MPPT 到 100%,让控制权回到 logic.c 的自发自用逻辑。驱动方是 logic 任务:logic.c:2928-2929 每周期调用 frequency_regulation_detection()(调频检测)与 dectect_api_dispatch_comm()(云端通信检测);logic.c:2314/2318 在 u8_sequencecontrol_mode 为 DISPATCH_FREQ_MODE/DISPATCH_MODE 时分别调用 excute_frequency_regulation_command() 与 excute_system_dispatch_command() 真正落功率。文件还维护老化测试的 6 态循环状态机与上报影子副本,供 net/ems.c 与 net/server.c 读写。
2. 关键数据结构与宏
本文件不定义新类型,全部来自 logic/dispatch.h 与 logic/logic.h。
| 名称 | 类型 | 说明 |
|---|---|---|
DISPATCH_TABLE_T / g_t_dispatch_t | 结构体 / 全局变量 | 本文件 :30 定义实例,调度状态中枢,含系统调度表、调频表、老化表 |
STEverGenCmdTab | 结构体 | 系统调度表,字段 u8Dispatch/u8ControlMode/u8AgingMode/u8ForceSOC/u8_pv_on_off/u16ActivePower/u16ReactivePower/u32CmdGetTick/u32_runtime |
FREQ_DISPATCH_T | 结构体 | 调频表,含死区/饱和带、满充放功率、动作标志 u8_freq_dispatch_flag、输出 s32_ac_dispatch_power |
AGINGMODE_TABLE_T / s_t_agingmode_data_temp | 结构体 / static 变量 | 老化上报表及其影子副本(:31 定义,= {0}) |
DISPATCH_CMD_T | 结构体 | 云端指令载体:u8_enable + f_para[8] + u32_runtime |
FM_BAND_E | 枚举 | 调频阶跃锁存态:NORMAL_FREQ_E(0)/OVER_FREQ_E(1)/UNDER_FREQ_E(2) |
AGING_MODE_E | 枚举 | 老化子模式:AGING_END/PV_TO_GRID/PV_TO_BAT/BAT_TO_GRID/GRID_TO_BAT/BAT_TO_GRID_2/AGING_EXIT/AGING_START=0xff |
AGING_STATE_E | 枚举 | 老化结果:AGING_FAILURE/AGING_SUCCEED/AGING_ERROR/AGING_COMPLETE/AGING_ONGOING |
ENModeControl | 枚举 | 调度模式(FCAS_*/FFR_*/CHARGE_CONTROL_2/SUNSPECT_ACTIVE_MODE 等 30 个成员) |
s_u8_pv_off_flag | static u8(:29) | 本文件内部 MPPT 关断意图标志,TRUE 表示要求 u16_mppt_percentage = 0 |
POSITIVE_VALUE_OFFSET | 宏 | 32000,功率零偏置。u16ActivePower/u16ReactivePower 与 32000 的差即实际功率,符号决定充放 |
NEGATIVE_VALUE_OFFSET | 宏 | -32000,本文件无引用点 |
FORCE_SOC_MULT | 宏 | 4,u8ForceSOC 的放大倍数(SOC 判据 ×4 ± 4) |
DISPATCH_POWER_MIN_SET / DISPATCH_POWER_MAX_SET | 宏 | 0 / 64000,西澳 CHARGE_CONTROL_2 有效性开区间 |
EVERGEN_FUNC_START / EVERGEN_FUNC_STOP | 宏 | 1 / 0,u8Dispatch 取值 |
FCAS_STEP_MODE/FAST_FCAS_STEP_MODE/VERY_FAST_FCAS_STEP_MODE 等 | 枚举 | 调频模式判别用;阶跃三兄弟走同一分支,FFR 八兄弟走同一分支 |
DEBUG_EVERGEN / DEBUG / DEBUG_TCP_CLIENT_STATE | 宏(编译开关) | 调试打印门控,来自 includes.h 链;大量 printf 被其包裹 |
3. 函数清单
本文件共 40 个函数定义,其中 static 1 个,其余 39 个为全局作用域。logic/dispatch.h 只登记了其中 24 个。
| 函数名 | 作用域 | 说明 |
|---|---|---|
set_freq_dispatch_parameter | static(:44) | 判定是否为调频指令并按模式拆分 f_para[] 填入调频表;非调频模式返回 FALSE |
set_dispatch_parameter | 全局(:98) | 指令总入口:先试调频解析,失败则按系统调度填充 t_stsystemcmdinfo |
get_dispatch_running_mode | 全局(:128) | 返回当前调度模式(调频优先,否则 SFFR,否则 NO_DISPATCH_CONTROL) |
dispatch_regulation_flag | 全局(:160) | 调频使能时返回调频动作标志,否则返回并网状态 u8_grid_status |
dispatch_api_data_interval | 全局(:178) | 上报间隔 ms;调频固定 50,SFFR 为 1000,其余 50 |
dispatch_api_maxfeedin_priority | 全局(:210,未在头文件登记) | 按 get_maxfeedin_priority_flag() 决定是否恢复额定馈网功率,并可清电网侧参数 |
frequency_regulation_detection | 全局(:235) | 调频检测状态机:超时退出、优先级豁免、阶跃/下垂/FFR 算法、MPPT 与降载标志联动 |
excute_frequency_regulation_command | 全局(:529) | 把 s32_ac_dispatch_power 落成实际充放电限值并切换 MPPT |
get_freq_dispatch_enable | 全局(:576) | 读调频使能 |
frequency_regulation_flag | 全局(:587) | 读调频动作标志 |
get_dispatch_running_freq | 全局(:598) | 读电网频率(0.01 Hz,环境 4500-5500) |
get_fast_dispatch_running_freq | 全局(:620) | 读快速调频频率(0.001 Hz,环境 45000-55000) |
get_fcas_running_flag | 全局(:642) | 是否 FCAS 阶跃/下垂模式 |
get_fast_fcas_running_flag | 全局(:667) | 是否 FAST_FCAS 模式 |
get_very_fast_fcas_running_flag | 全局(:692) | 是否 VERY_FAST_FCAS 模式 |
get_ffr_running_flag | 全局(:716) | 是否任一 FFR 模式 |
dectect_api_dispatch_comm | 全局(:748) | 云端调度超时退出、老化结果判定与翻转状态维护 |
handle_aging_mode | 全局(:877,未登记) | 老化测试 6 态循环状态机 |
handle_bat_charge_from_pv | 全局(:984,未登记) | 光伏优先充电模式执行体 |
handle_charge_control | 全局(:1011,未登记) | 按目标 SOC 分三段的强制充放电(实际只被 SFFR_MODE 调用) |
handle_parallel_charge_control | 全局(:1072,未登记) | 并机充放电控制(实际被 CHARGE_CONTROL 调用) |
handle_load_following | 全局(:1112,未登记) | 负载跟随模式执行体 |
handle_max_output | 全局(:1135,未登记) | 最大输出模式执行体 |
handle_normal_mode | 全局(:1151,未登记) | 普通模式执行体 |
handle_optimise_consume | 全局(:1174,未登记) | 优化消纳模式执行体 |
handle_max_consume | 全局(:1192,未登记) | 最大消纳模式执行体 |
handle_eco_mode | 全局(:1210,未登记) | 经济模式执行体 |
handle_bat_no_charge | 全局(:1228,未登记) | 电池禁充模式执行体 |
handle_charge_control_2_dc | 全局(:1249,未登记) | 西澳调度 DC 侧参数处理 |
handle_charge_control_2_ac | 全局(:1285,未登记) | 西澳调度 AC 侧参数处理(带最小 800 ms 计算间隔限制) |
handle_charge_control_2 | 全局(:1345,未登记) | 西澳调度入口:DC 有效优先,其次 AC,都无效则自发自用 |
dispatch_Sunspec_ative_power_mode | 全局(:1374,未登记) | SunSpec 有功功率调度,仅 #ifdef INV_AC_CTRL 下有效 |
excute_system_dispatch_command | 全局(:1392) | 系统调度总分派 + MPPT 收尾 |
get_dispatch_enable | 全局(:1506) | 读 u8Dispatch |
set_dispatch_enable | 全局(:1517) | 写 u8Dispatch |
get_dispatch_mode | 全局(:1528) | 智能控制模式下返回 SMART_CONTROL,否则返回 u8ControlMode |
get_dispatch_set_power | 全局(:1544) | 返回设定功率(已减零偏置);西澳模式只回有效值,都无效返回 65535 |
aging_mode_send_data_flag | 全局(:1582) | memcmp 老化表与影子副本,判断是否有变化待上报 |
aging_mode_data_sync | 全局(:1593) | 把老化表拷进影子副本(上报成功回调) |
dispatch_set_mode | 全局(:1605) | 仅当 mode < DISPATCH_MAX_MODE 时写 u8ControlMode,否则返回 -1 |
4. 核心逻辑解析
4.1 指令入口与「一个字段、两套语义」的解析
set_dispatch_parameter()(:98)是唯一写入口(由 net/server.c:835 调用):
if (set_freq_dispatch_parameter(dispatch_cmd) == FALSE) { /* 不是调频指令 */
u32CmdGetTick = OSTimeGet();
u8Dispatch = u8_enable;
u16ActivePower = f_para[0];
u16ReactivePower = f_para[1];
u8ForceSOC = f_para[2];
u8ControlMode = f_para[3];
u8_pv_on_off = f_para[7];
u32_runtime = u32_runtime;
}
set_freq_dispatch_parameter()(:44)先取 l_s32_mode = f_para[3] 做模式判别,命中 14 种 FCAS/FFR 模式之一才返回 TRUE。因此 f_para[3] 在调频指令里是"调频模式号",在系统调度指令里是 u8ControlMode——同一个槽位承担两种含义,靠"是否属于调频枚举集合"来区分。这是本文件最容易误改的地方。
调频参数的拆分规则(:66-79)分两类:
| 子类 | 判定 | f_para[] 映射 |
|---|---|---|
阶跃类(FCAS_STEP_MODE/FAST_FCAS_STEP_MODE/VERY_FAST_FCAS_STEP_MODE) | 三个 STEP 模式 | u16_db = f_para[0]、u16_sb = f_para[1] |
下垂类 + FFR 全部(FCAS_DROOP_MODE…FFR_Live_Test_MODE) | 其余 11 种 | u16_charge_power = f_para[0]、u16_ldb = f_para[1]、u16_hdb = f_para[2]、u16_lsb = f_para[4]、u16_hsb = f_para[5]、u16_discharge_power = f_para[6] |
两个分支都会写 u8_dispatch_mode、u8_dispatch_enable、u32_dispatch_tick = OSTimeGet()、u32_dispatch_run_time = u32_runtime,返回 TRUE。未命中的模式返回 FALSE(:88),由调用方回落到系统调度分支。
4.2 调频检测状态机:frequency_regulation_detection()(:235)
函数内 5 个 static 状态量:s_u8_fcas_step_mode(阶跃锁存)、s_u32_freq_dispatch_tick(FFR 延时基准)、s_u8_inv_reduce_power_off(降载开关影子)、s_u16_charge_power/s_u16_discharge_power(上次满充放功率,用于检测变化并复位锁存)。执行顺序如下。
步骤 1 — 指令超时退出(:245-256)
if (mode != 0 || enable == TRUE) {
if ((OSTimeGet() - u32_dispatch_tick) > (u32_dispatch_run_time * 1000))
memset(&t_freq_dispatch_rundata, 0, sizeof(FREQ_DISPATCH_T));
else if (u32_dispatch_tick > OSTimeGet()) u32_dispatch_tick = OSTimeGet();
}
u32_dispatch_run_time 单位为秒,OSTimeGet() 单位为毫秒(OS_TICKS_PER_SEC = 1000)。
步骤 2 — 锁存复位与优先级豁免(:258-276)
- 若当前模式不属于三个阶跃模式,
s_u8_fcas_step_mode = NORMAL_FREQ_E(:258-263)。 - 若满足以下任一条件,调频被无条件让位:
u8_sequencecontrol_mode∈ {CMD_FORCE_CHARGE_MODE,SYS_FORCE_CHARGE_MODE,BAT_FORCE_CHARGE_MODE,USER_FORCE_CHARGE_MODE,DISPATCH_BAT_MODE},或u8_grid_status == FALSE(离网),或t_config_data.Generator == TRUE(油机运行)。此时flag = FALSE、s32_ac_dispatch_power = 0、锁存复位(:265-276)。
步骤 3 — 三种响应算法(:277-496)
(a) 阶跃模式(:281-372) 先把单死区/单饱和带展开成"四点"模型,并用逆变器额定输入/输出功率作为满充/满放功率:
u16_ldb = u16_hdb = u16_db; // 死区
u16_lsb = u16_hsb = u16_sb; // 饱和带
u16_charge_power = s32_inv_rated_input_power;
u16_discharge_power = s32_inv_rated_output_power;
若充/放功率发生变化则复位锁存(:291-297)。频率异常保护:freq <= 4500 || freq >= 5500(45 Hz / 55 Hz)时输出 0 并复位锁存(:299-305)。正常区间的锁存迁移:
| 当前锁存 | 条件 | 输出 s32_ac_dispatch_power | 目标锁存 |
|---|---|---|---|
NORMAL_FREQ_E | freq <= 5000 - u16_lsb | +u16_discharge_power(满放) | UNDER_FREQ_E |
NORMAL_FREQ_E | freq >= 5000 + u16_hsb | -u16_charge_power(满充) | OVER_FREQ_E |
NORMAL_FREQ_E | 其他 | 0 | 不变 |
OVER_FREQ_E | freq <= 5000 - u16_lsb | +u16_discharge_power | UNDER_FREQ_E |
OVER_FREQ_E | freq <= 5000 + u16_hdb | 0 | NORMAL_FREQ_E |
OVER_FREQ_E | 其他 | 保持(不再赋值) | 不变 |
UNDER_FREQ_E | freq >= 5000 - u16_ldb | 0 | NORMAL_FREQ_E |
UNDER_FREQ_E | freq >= 5000 + u16_hsb | -u16_charge_power | OVER_FREQ_E |
UNDER_FREQ_E | 其他 | 保持 | 不变 |
这是典型的带回差的施密特阶跃:进入动作按饱和带 sb,退出动作按死区 db,两者独立可调;由于阶跃模式 db≠sb 才产生回差。OVER_FREQ_E/UNDER_FREQ_E 的"其余分支只置 l_u8_freq_dispatch_flag_temp = TRUE 而不重写功率"体现了阶跃响应的锁存保持语义(l_u8_freq_dispatch_flag_temp 是本函数唯一输出,:504 再写回全局)。
(b) 下垂模式(:374-413) 线性插值公式,freq 与 5000(50.00 Hz)的偏差在 [db, sb] 区间内按比例出力:
频偏低于下限:
freq <= 5000 - lsb → +u16_discharge_power (满放)
5000-lsb > freq > 5000-ldb → +u16_discharge_power * ((5000-ldb) - freq) / (lsb - ldb)
频偏高于上限:
freq >= 5000 + hsb → -u16_charge_power (满充)
5000+hsb > freq > 5000+hdb → +u16_charge_power * ((5000+hdb) - freq) / (lsb - ldb)
死区内 → 0(自发自用)
注意高频侧的斜率分母写的是 (u16_lsb - u16_ldb)——低侧的带宽,而不是 (u16_hsb - u16_hdb)。见备注 2。
(c) FFR 模式(:415-489) 这段代码用 C 的 case 穿透(fall-through) 给 l_u32_freq_dispatch_delay 赋不同延时:
case FFR_PRIMARY_MODE: if (delay == 0) delay = 20*1000; /* 落到下一 case */
case FFR_SECONDARY_MODE: if (delay == 0) delay = 30*60*1000; /* 落到下一 case */
case FFR_HIGE_MODE: if (delay == 0) { delay = 1000; s_u32_freq_dispatch_tick = OSTimeGet(); }
case FFR_STEP_TEST1_MODE: ... /* 8 个测试/实机模式 */
case FFR_Live_Test_MODE: if (delay == 0) { delay = 1000; s_u32_freq_dispatch_tick = OSTimeGet(); }
因为 delay 是每次调用都重新初始化为 0 的普通局部变量(:239),穿透链的净效果是:FFR_PRIMARY_MODE → 20 s;FFR_SECONDARY_MODE → 30 min;FFR_HIGE_MODE 与 8 个测试模式 → 1 s。代价是只有 FFR_HIGE_MODE 与测试模式那一支会给 s_u32_freq_dispatch_tick 打时间戳(见备注 3)。
动作逻辑与下垂模式相同(线性插值,同样用 (u16_lsb - u16_ldb) 作高频分母),区别在死区内不清零而是保持输出,靠 (OSTimeGet() - s_u32_freq_dispatch_tick) > l_u32_freq_dispatch_delay 延时归零(:476-488):
else {
l_u8_freq_dispatch_flag_temp = TRUE; // 进入 Waiting,电池不充不放,等待下次频率触发
if ((OSTimeGet() - s_u32_freq_dispatch_tick) > l_u32_freq_dispatch_delay)
s32_ac_dispatch_power = 0;
else if (s_u32_freq_dispatch_tick > OSTimeGet())
s_u32_freq_dispatch_tick = OSTimeGet();
}
(d) default(:491-494) 不支持的调频模式直接 memset 整张调频表,立即退出调度。
步骤 4 — 动作标志变化时恢复 MPPT(:504-512)
if (l_u8_freq_dispatch_flag_temp != u8_freq_dispatch_flag) {
u8_freq_dispatch_flag = l_u8_freq_dispatch_flag_temp;
if (u8_freq_dispatch_flag == FALSE) {
s_u8_pv_off_flag = FALSE; // 退出调频恢复 MPPT 追踪
g_t_logic_info.u16_mppt_percentage = 10000;
}
}
步骤 5 — 调频使能时关闭电压/频率降载(:514-520)
s_u8_inv_reduce_power_off 实际存的是 u8_dispatch_enable 的影子,变化时调用:
inverter_set_p_v_mode_down_load_flag(u8_dispatch_enable == FALSE);
inverter_set_p_f_mode_down_load_flag(u8_dispatch_enable == FALSE);
即调频使能期间关闭逆变器自身的 P-V / P-F 降载,避免与 EMS 调度叠加(:516 注释"开启频率调度关闭电压降载、频率降载")。
4.3 调频指令执行:excute_frequency_regulation_command()(:529)
由 logic.c:2314 在 DISPATCH_FREQ_MODE 下调用。以 s32_ac_dispatch_power 的符号二分:
| 条件 | 动作 |
|---|---|
> 0(放电) | s_u8_pv_off_flag = FALSE;s32_pgrid_metert = 0;s32_pinvt = 0;s32_max_feedin_power = s32_ac_dispatch_power;bat_force_discharge() |
<= 0(充电) | s_u8_pv_off_flag = TRUE;若 s32_bat_charge_power_limit > abs(s32_ac_dispatch_power) 则钳到该值;s32_pgrid_metert = 0;s32_pinvt = 0;s32_max_feedin_power = 0;bat_force_charge() |
收尾统一按 s_u8_pv_off_flag 设置 g_t_logic_info.u16_mppt_percentage = 0 / 10000(:556-563)。注意充电分支只压不抬充电限值(若原限值更小则保持更小),放电分支则完全由 s32_max_feedin_power 承担限流。
4.4 频率读取与单位换算(:598、:620)
get_dispatch_running_freq(): // 单位 0.01 Hz
debug_enable → u16_debug_freq
ct_read_frequent(GRID_CT) > 4400 → ct_read_frequent(GRID_CT)
否则 → parallel_read_system_inv_grid_freq()
get_fast_dispatch_running_freq(): // 单位 0.001 Hz
debug_enable → u16_debug_freq * 10
ct_read_frequent(FAST_GRID_CT) > 44000 → ct_read_frequent(FAST_GRID_CT)
否则 → parallel_read_system_inv_grid_freq() * 10
阈值 4400(44 Hz)与 44000(44 Hz)用来判定 CT 读数是否有效——低于 44 Hz 视为 CT 无输出,回落到并机模块汇总的逆变器电网频率。两个函数的调试注入路径都带 t_fm_debug.u8_debug_enable 使能。
4.5 云端调度超时与老化结果:dectect_api_dispatch_comm()(:748)
由 logic.c:2929 每周期调用。三段逻辑:
(a) 超时退出(:753-787) 仅对白名单内的 u8ControlMode(BAT_CHARGE_FROM_PV、CHARGE_CONTROL、LOAD_FOLLOWING、MAX_OUTPUT、NORMAL_MODE、OPTIMISE_CONSUME、MAX_CONSUME、ECO_MODE、BAT_NO_CHARGE、AGING_MODE、SFFR_MODE、CHARGE_CONTROL_2、SUNSPECT_ACTIVE_MODE)生效。条件 (OSTimeGet() - u32CmdGetTick) > (u32_runtime * 1000):
- 若正在老化,先置
u8_aging_result = AGING_COMPLETE、u8_aging_status = AGING_EXIT; - 然后把
u16ActivePower/u16ReactivePower复位为POSITIVE_VALUE_OFFSET(= 0 W)、u8ForceSOC = 0、u8Dispatch = EVERGEN_FUNC_STOP、u8ControlMode = 0、u8AgingMode = AGING_START、u8_pv_on_off = 0。
(b) u8Dispatch == EVERGEN_FUNC_STOP 的二次复位(:795-821) 再清一遍字段并把 u32_runtime 硬编码为 90 秒;若老化未完成则置 AGING_FAILURE/AGING_END;若调频未在动作则恢复 MPPT(s_u8_pv_off_flag = FALSE、u16_mppt_percentage = 10000)。
(c) 老化结果判定(:823-862)
if (u8ControlMode == AGING_MODE) {
if (ems_read_fault_code() || bms_read_fault_code() || inverter_read_fault_code() ||
inverter_read_fault_code_extend() || inverter_read_fault_code_extend2())
u8_aging_result = AGING_ERROR; // 任一故障 → 错误
else if (s_t_agingmode_data_temp.u8_aging_status != u8AgingMode && // 老化步进已推进
u8AgingMode != AGING_END &&
u8_aging_result != AGING_FAILURE && u8_aging_result != AGING_COMPLETE)
{
u8_aging_result = AGING_SUCCEED; // 当前步成功
if (u8AgingMode != AGING_START) u8_aging_status = u8AgingMode;
}
else u8_aging_result = AGING_ONGOING;
if ((OSTimeGet() - s_u32_send_data_tick) >= 60 * 1000) // 每 60 s 翻转一次
u8_flip_status = (u8_flip_status == FALSE);
}
else s_u32_send_data_tick = OSTimeGet();
u8_aging_status 相当于"已确认完成的步",与指令下发的 u8AgingMode 比较即得"步进已推进"。每 60 秒翻转 u8_flip_status,是给上位机做存活/心跳判据用的。
4.6 老化 6 态循环:handle_aging_mode()(:877)
SOC 门限来自功率字段(:879-891):l_u16_max_chargr_soc = u16ActivePower * 10、l_u16_max_dischargr_soc = u16ReactivePower * 10,为 0 时取默认 800 / 500(即 80% / 50%)。
u8AgingMode | 动作 | 迁移条件 | 目标 |
|---|---|---|---|
AGING_START(0xff) | 先赋值 u8AgingMode = PV_TO_BAT,然后穿透执行 PV_TO_GRID 的代码体 | 穿透(无 break) | PV_TO_GRID 行为 |
PV_TO_GRID | PV 开;pgrid_metert = 0、pinvt = 0、max_feedin = 额定输出功率、充电限值 0、放电限值 0;bat_force_discharge() | 无(由外部指令切走) | — |
PV_TO_BAT | PV 开;SOC >= 充电门限 → 转 BAT_TO_GRID;否则 max_feedin = 0、bat_force_charge() | SOC 达充电门限 | BAT_TO_GRID |
BAT_TO_GRID | PV 关;SOC <= 放电门限 → 转 GRID_TO_BAT;否则 max_feedin = 额定、充电限值 0、bat_force_discharge() | SOC 跌破放电门限 | GRID_TO_BAT |
GRID_TO_BAT | PV 关;SOC >= 充电门限 → 转 BAT_TO_GRID_2;否则 max_feedin = 0、bat_force_charge() | SOC 达充电门限 | BAT_TO_GRID_2 |
BAT_TO_GRID_2 | PV 关;SOC <= 放电门限 → 转回 PV_TO_BAT(循环开始);否则 max_feedin = 额定、充电限值 0、bat_force_discharge() | SOC 跌破放电门限 | PV_TO_BAT |
AGING_END | 赋 u8AgingMode = AGING_END,穿透到 default | 穿透(无 break) | AGING_START(被 default 覆盖) |
default | u8AgingMode = AGING_START | — | AGING_START |
因此老化循环是 PV_TO_BAT → BAT_TO_GRID → GRID_TO_BAT → BAT_TO_GRID_2 → PV_TO_BAT 的 SOC 往复(PV 充电 / 上网放电 / 电网充电 / 上网放电),直到云端撤销指令。AGING_EXIT 只被 dectect_api_dispatch_comm() 写给 u8_aging_status,不参与这里的状态判断。
4.7 系统调度总分派:excute_system_dispatch_command()(:1392)
logic.c:2318 在 DISPATCH_MODE 下调用。switch (u8ControlMode) 的映射关系(:1410-1467):
u8ControlMode | 实际调用 | 备注 |
|---|---|---|
BAT_CHARGE_FROM_PV | handle_bat_charge_from_pv() | — |
CHARGE_CONTROL | handle_parallel_charge_control() | 名字与用途错位 |
SFFR_MODE | handle_charge_control() | 名字与用途错位 |
LOAD_FOLLOWING | handle_load_following() | — |
MAX_OUTPUT | handle_max_output() | — |
NORMAL_MODE | handle_normal_mode() | — |
OPTIMISE_CONSUME | handle_optimise_consume() | — |
MAX_CONSUME | handle_max_consume() | — |
ECO_MODE | handle_eco_mode() | — |
BAT_NO_CHARGE | handle_bat_no_charge() | — |
AGING_MODE | handle_aging_mode() | — |
CHARGE_CONTROL_2 | handle_charge_control_2() | 西澳调度 |
SUNSPECT_ACTIVE_MODE | dispatch_Sunspec_ative_power_mode() | 仅 #ifdef INV_AC_CTRL |
default | s_u8_pv_off_flag = FALSE; self_use_funtion(); | 未定义模式(含 21-25/31 等枚举空洞)回到自发自用 |
之后有两次收尾覆盖:
if (u8_pv_on_off == 1) s_u8_pv_off_flag = FALSE; // 云端强制开 PV
else if (u8_pv_on_off == 2) s_u8_pv_off_flag = TRUE; // 云端强制关 PV
if (frequency_regulation_flag() == FALSE) // 调频未动作时 MPPT 由本文件决定
u16_mppt_percentage = s_u8_pv_off_flag ? 0 : 10000;
MPPT 的赋值刻意加了 frequency_regulation_flag() == FALSE 门控(:1478),避免覆盖调频分支的 MPPT 决策——这是两个子系统之间唯一的显式互斥点。
4.8 各模式执行体的功率限值算法
所有执行体只写三个字段,且限值只压不抬:
s32_bat_charge_power_limit/s32_bat_dischg_power_limit:写入时一律先比较再取小(if (limit > X) limit = X;),因此上层的更小限值不会被放大。s32_pgrid_metert(电网侧电表偏置)、s32_max_feedin_power(馈网上限)、s32_pinvt(逆变器侧偏置)。
| 执行体 | 关键动作 |
|---|---|
handle_bat_charge_from_pv(:984) | s_u8_pv_off_flag = FALSE;dispatch_api_maxfeedin_priority(FALSE)(不管馈网优先级);充电限值钳到 abs(32000 - u16ActivePower);放电限值 = 0;self_use_funtion() |
handle_charge_control(:1011) | 取 parallel_read_system_bat_average_soc() 与目标 u8ForceSOC * 4 比较,形成三段:<= 目标-4 强制充(并钳充电限值到 abs(32000 - u16ActivePower))、目标±4 内充放限值都置 0、>= 目标+4 强制放(钳放电限值)。每段都调 bat_force_charge()/bat_force_discharge() |
handle_parallel_charge_control(:1072) | 按 32000 与 u16ActivePower 的大小关系二分:小于则钳充电限值并强制充、大于则钳放电限值并强制放、相等则两者归零 |
handle_load_following(:1112) | 充电限值与放电限值都钳到 abs(32000 - u16ActivePower),然后 self_use_funtion() |
handle_max_output(:1135) | dispatch_api_maxfeedin_priority(TRUE);直接 bat_force_discharge()(清充电限值那行被注释掉,:1141) |
handle_normal_mode(:1151) | 同 handle_load_following |
handle_optimise_consume(:1174) | s32_pgrid_metert = -s32_pinvt,并逐相 s32_pgrid_meter{a,b,c} = -s32_pinv{a,b,c};s32_max_feedin_power = 0;bat_force_charge() |
handle_max_consume(:1192) | 与 handle_optimise_consume 完全相同,唯一区别是 s_u8_pv_off_flag = TRUE(关 PV) |
handle_eco_mode(:1210) | 只钳充电限值,然后 self_use_funtion()(不钳放电限值) |
handle_bat_no_charge(:1228) | 充电限值 = 0;self_use_funtion();再把 s32_ac_total_set_inv_power 的下限钳到 0(:1237-1240,注释说明"电池禁充时 AC 控制值不能发负值,会导致 BUG 过压") |
handle_charge_control_2_dc(:1249) | 32000 - dc_set_power < 0 → 钳放电限值 + bat_force_discharge();> 0 → 放电限值 0 + 钳充电限值 + bat_force_charge();== 0 → no_bat_logic_control() |
handle_charge_control_2_ac(:1285) | 带节流:abs(OSTimeGet() - u32_calculation_total_inv_tick) < max(800, u32_calculation_interval) 时直接 return。放电时取 min(s32_max_feedin_power, abs(ac_set_power - 32000)) 作为电网侧偏置;充电时先 s32_pgrid_metert = s32_pgridt 再把 ac_set_power - 32000 叠加到 s32_pgrid_metert;最后 self_use_funtion() |
handle_charge_control_2(:1345) | 参数有效性判定:u16ActivePower ∈ (0, 64000) → 走 DC;否则 u16ReactivePower ∈ (0, 64000) → 走 AC;都无效 → self_use_funtion()。注释说明云端用 -1 表示无效(u16 解析成 65535) |
4.9 馈网优先级:dispatch_api_maxfeedin_priority()(:210)
s32_max_feedin_power = (get_maxfeedin_priority_flag() == TRUE) ? s32_max_feedin_power
: s32_inv_rated_output_power;
if (clear_grid_params && get_maxfeedin_priority_flag() != TRUE) {
s32_pgrid_metert = s32_pgrid_metera = s32_pgrid_meterb = s32_pgrid_meterc = 0;
s32_pinvt = 0;
}
即:若外部(CSIP/DNSP 等)声明了馈网优先级,则保留既有 s32_max_feedin_power;否则把它放宽到逆变器额定输出功率,并可顺带清空电网侧参数。
4.10 上报表与影子副本
aging_mode_send_data_flag()(:1582)用 memcmp 比较 g_t_dispatch_t.t_agingmode_info 与 s_t_agingmode_data_temp;aging_mode_data_sync()(:1593)在收到云端 CMD_SEND_BURNIN_DATA 确认(net/server.c:7021)后把前者拷入后者。这样 net/ems.c:5092 每 10 秒检查一次 aging_mode_send_data_flag() == TRUE 才发数据,避免重复上报。注意 memcmp 会包含 u8_flip_status 这个每 60 秒必然翻转的位,所以只要老化在跑,aging_mode_send_data_flag() 每 60 秒必然为 TRUE。
get_dispatch_set_power()(:1544)把零偏置去掉后返回;西澳模式(CHARGE_CONTROL_2)只回有效值,否则返回 65535 表示无效。
4.11 定时与 uCOS-II 配合
| 机制 | 说明 |
|---|---|
| 调用节拍 | logic 任务以 20 tick(20 ms,logic.c:2912 的 OSFlagPend(..., 20, ...))循环,本文件所有检测函数都按此节拍轮询,不使用阻塞延时 |
| 立即唤醒 | net/server.c:838-839 在 set_dispatch_parameter() 后 OSFlagPost(g_t_fast_dispatch_flag, POST_PEND_FAST_ENABLE, ...) + OSTimeDlyResume(TASK_LOGIC_CONTROL_PRIO),使下行指令不必等满 20 ms |
| 时间基准 | 全部用 OSTimeGet(),单位 ms(OS_TICKS_PER_SEC = 1000) |
| 时钟回绕 | 本文件有 4 处 else if (tick > OSTimeGet()) tick = OSTimeGet(); 保护(:252、:783、:854、:484),但未保护"tick 早于当前时刻但差值已超 2^31"的情形 |
| 信号量 / 邮箱 | 本文件不使用,全部通过共享全局变量与上面的单一事件标志组同步 |
4.12 g_t_dispatch_t 的读写全景
| 字段组 | 写者 | 读者 |
|---|---|---|
t_stsystemcmdinfo.* | set_dispatch_parameter()(net 任务)、dectect_api_dispatch_comm()、handle_aging_mode()、get/set_dispatch_enable、dispatch_set_mode | 本文件全部 handle_*、net/ems.c:773/1490/1557、midware/interface/modbus、midware/inverter/parallel.c |
t_freq_dispatch_rundata.* | set_freq_dispatch_parameter()(net 任务)、frequency_regulation_detection()、excute_frequency_regulation_command() | net/ems.c 上报、midware/interface/modbus/modbus.c:3604/3612、midware/inverter/parallel.c:788/959 |
t_agingmode_info.* | dectect_api_dispatch_comm()、aging_mode_data_sync() | net/ems.c:5092(经 aging_mode_send_data_flag) |
t_fm_debug 只被测试代码写(外部写入 t_freq_dispatch_rundata.t_fm_debug),本文件只读。
4.13 通信协议帧格式
本文件不直接实现帧收发,但它是 net/server.c 下行 JSON 的消费者,隐含如下协议约定:
- 参数来自下行报文的
Parameter2字段,用parse_dispatch_para()解析成 8 / 7 / 4 个十进制数字填入DISPATCH_CMD_T.f_para[8](net/server.c:805/809/813)。 Parameter3字段解析为u32_runtime,且被夹到最小 90 秒(net/server.c:821-824)。f_para[3]是模式选择位(调频模式号或u8ControlMode)。- 上报侧没有重传机制:数据攒够(调频 10 个点、系统调度 1000 ms / 间隔个点)后置
g_u8_send_dispatch_data_flag由网络任务发送(net/ems.c:6349-6545)。
5. 对外接口
导出(供外部使用):
| 名称 | 类型 | 调用方 |
|---|---|---|
g_t_dispatch_t | 全局变量 | net/ems.c、net/server.c、midware/interface/modbus、midware/interface/dry_node、midware/inverter/parallel.c、data/resume_data/flash/flash_data_store.c |
set_dispatch_parameter | 函数 | net/server.c:835 |
frequency_regulation_detection / dectect_api_dispatch_comm | 函数 | logic/logic.c:2928 / logic.c:2929 |
excute_frequency_regulation_command / excute_system_dispatch_command | 函数 | logic/logic.c:2314 / logic.c:2318 |
get_dispatch_running_mode / dispatch_api_data_interval / dispatch_regulation_flag | 函数 | net/ems.c:6492 / ems.c:6508 / ems.c:6537 |
frequency_regulation_flag | 函数 | logic/logic.c:2241/3501、midware/interface/modbus/modbus.c:3604/4513、net/ems.c:6355/6404/6453 |
get_freq_dispatch_enable | 函数 | midware/inverter/parallel.c:788/959 |
get_dispatch_running_freq / get_fast_dispatch_running_freq | 函数 | midware/interface/modbus/modbus.c:3612、net/ems.c:6349/6398/6447/6531/6589 |
get_fcas_running_flag / get_fast_fcas_running_flag / get_very_fast_fcas_running_flag / get_ffr_running_flag | 函数 | net/ems.c:6321/6370/6419/6573 |
get_dispatch_enable | 函数 | logic/logic.c:2245、midware/bms/bms.c:2225 |
get_dispatch_mode / get_dispatch_set_power | 函数 | net/ems.c:774/1491/1557 等上报点 |
aging_mode_send_data_flag / aging_mode_data_sync | 函数 | net/ems.c:5092 / net/server.c:7021 |
set_dispatch_enable / dispatch_set_mode | 函数 | midware/interface/modbus/sunspec_business.c:1113/1114 |
dispatch_api_maxfeedin_priority | 函数 | 无外部调用方(只有本文件 handle_* 内部使用),且未在头文件登记 |
被调用(本文件引用的外部符号):
| 符号 | 来源 |
|---|---|
OSTimeGet、OSFlagPost(间接) | UCOSII/uCOS-II |
bat_force_charge、bat_force_discharge、self_use_funtion、no_bat_logic_control、logic_read_sequencecontrol_mode | logic/logic.h:653-660(实现在 logic/logic.c) |
g_t_logic_info、u16_mppt_percentage、s32_ac_total_set_inv_power、s32_pgridt、u32_calculation_interval、u32_calculation_total_inv_tick、u8_dnsp_maxfeedin_priority_flag | logic/logic.h |
parallel_read_system_bat_average_soc、parallel_read_system_inv_grid_freq、parallel_read_system_self_consumption(注释中) | midware/inverter/parallel.h |
ct_read_frequent、GRID_CT、FAST_GRID_CT | midware/meter/ct.h |
meter_read_actpower、meter_read_reactpower、GRID_METER、PV_METER、PHASE_ALL、PHASE_T | midware/meter/meter.h |
inverter_read_inv_actpower、inverter_read_grid_actpower、inverter_read_backup_actpower、inverter_read_bat_power、inverter_read_pv_power、inverter_set_p_v_mode_down_load_flag、inverter_set_p_f_mode_down_load_flag、inverter_read_fault_code*、inverter_read_safety*、get_inverter_type(经 csip_aus) | midware/inverter/inverter.h |
bms_read_total_power、bms_read_fault_code | midware/bms/bms.h |
ems_read_fault_code | data/data.h:127 |
ac_control_Sunspec_ative_power、ac_control_* | logic/ac_control.h |
get_maxfeedin_priority_flag | logic/logic.h:708 |
memset、memcmp、abs、printf | C 运行库 / includes.h |
6. 依赖关系
依赖(dispatch.c:13-23):dispatch.h、inverter.h、bms.h、ct.h、data.h、modbus.h、logic.h、server.h、ac_control.h、sunspec_business.h、meter.h。
被依赖:
- 头文件
dispatch.h被logic/logic.c、logic/csip_aus.c、net/ems.c、net/server.c(及server.h)、midware/interface/modbus(及modbus.h)、midware/interface/dry_node/dry_node.c、midware/inverter/parallel.c、data/resume_data/flash/flash_data_store.c引入。 - 函数层面的直接调用方集中在
logic/logic.c、net/ems.c、net/server.c、midware/inverter/parallel.c、midware/bms/bms.c、midware/interface/modbus/*、midware/interface/modbus/sunspec_business.c。
7. 备注与风险点
handle_charge_control与handle_parallel_charge_control的映射与命名相反::1416-1422里case CHARGE_CONTROL:调handle_parallel_charge_control(),case SFFR_MODE:调handle_charge_control()。两个函数名都带charge_control,但一个按并机偏置充放、一个按目标 SOC 三段充放,极易在维护时改错分支。建议改名或在switch处加注释。- 下垂/FFR 高频侧的斜率分母用了低侧带宽:
:404-405与:472-473的公式u16_charge_power * ((5000+hdb) - freq) / (u16_lsb - u16_ldb)分母是低侧的(lsb - ldb),而低侧公式:392-393、:458-459用的也是(lsb - ldb)。若高侧带宽(hsb - hdb)与低侧不等(CSIP/FCAS 规范允许不对称),高侧的出力斜率会算错。从代码结构看这更可能是复制粘贴漏改。注意:set_freq_dispatch_parameter()里下垂/FFR 参数是按ldb=f_para[1]、hdb=f_para[2]、lsb=f_para[4]、hsb=f_para[5]拆的,低侧带宽 =lsb-ldb、高侧带宽 =hsb-hdb,两者可独立配置,因此这个分母确实是个 bug 而非巧合。 - FFR 的 fall-through 延时链导致
s_u32_freq_dispatch_tick未打戳::415-440依赖case穿透给l_u32_freq_dispatch_delay赋值。由于该变量每次调用都重置为 0,穿透链的if (delay == 0)只有第一次命中才执行,所以对FFR_PRIMARY_MODE与FFR_SECONDARY_MODE,s_u32_freq_dispatch_tick不会被打时间戳(只有FFR_HIGE_MODE与 8 个测试模式那一支会)。结果是这两个模式在死区内的"延时退出"分支(:480)用的是一个可能为 0 或过期很久的时间戳,(OSTimeGet() - tick) > delay立刻成立,输出被立即归零——即这两模式的延时保持实际不生效。另外l_u32_freq_dispatch_delay不必要地是普通局部变量(每次调用重建),若改成static语义会变,改动时须留意。 case AGING_END:穿透到default使AGING_END无法保持::969-974
case AGING_END:
g_t_dispatch_t.t_stsystemcmdinfo.u8AgingMode = AGING_END;
default:
g_t_dispatch_t.t_stsystemcmdinfo.u8AgingMode = AGING_START;
第一句赋的 AGING_END 立即被 default 覆盖成 AGING_START。因此 AGING_END 是一个永远观察不到的状态值,dectect_api_dispatch_comm() 里 u8AgingMode != AGING_END 的判断(:834)也就永远为真。case AGING_START: 穿透到 PV_TO_GRID(:895-907)同理:第一句把模式置为 PV_TO_BAT,但接着执行的是 PV_TO_GRID 的动作体,所以老化开始的第一周期实际是"上网放电"而非"PV 充电"。
- 限制值"只压不抬"的隐式约定没有注释:全部 16 个
handle_*都只在limit > X时把limit改小,从不放大。这意味着限值一旦被上一层(如logic.c的 SOC 限值、bms.c的get_dispatch_enable()分支)压低,本文件的模式切换无法把它恢复。加上多个handle_*(handle_max_output的:1141、handle_parallel_charge_control的:1094)把"清零某一侧限值"的语句注释掉了,行为差异很难从代码表面读出。 handle_optimise_consume与handle_max_consume仅差一个 MPPT 标志::1174-1202两函数体除s_u8_pv_off_flag外完全相同(前者FALSE、后者TRUE)。这是刻意的(优化消纳 vs 最大消纳的差别就是是否允许 PV 发电),但两份 30 行代码重复维护,改动时容易只改一处。s_u8_pv_off_flag是文件级static,与调频子系统双向耦合:四个地方读写它(:509、:533、:543、:817、:899…),且excute_system_dispatch_command()的 MPPT 赋值被frequency_regulation_flag() == FALSE门控(:1478)。若调频标志为TRUE但同时收到了系统调度指令(两个指令源并存),系统调度的 PV 开关意图会被静默丢弃。g_t_dispatch_t跨任务无锁:写者来自net任务(set_dispatch_parameter→net/server.c:835)与logic任务(所有handle_*、dectect_api_dispatch_comm),读者还有modbus、ems、parallel、bms任务。同步手段只有g_t_fast_dispatch_flag事件标志(保证logic被唤醒),不对结构体本身互斥。set_dispatch_parameter()分 8 条语句写t_stsystemcmdinfo,期间被logic任务读走会得到"新模式 + 旧功率"的组合。风险在老化/西澳调度这类功率字段与模式字段强耦合的场景下最明显。- 多处
u32/s32时钟比较未防溢出::248、:767、:849、:1292都用(OSTimeGet() - tick) > X的形式,tick有的是u32(u32CmdGetTick)有的是s32。s32承载OSTimeGet()返回值在 24.9 天后符号翻转,u32的减法在回绕时虽能给出正确差值,但else if (tick > OSTimeGet())这类保护只覆盖了"tick 在未来"的情况。 u8ForceSOC * FORCE_SOC_MULT ± 4的三处口径不一致:本文件:1018/1035/1041用u8ForceSOC * 4 ± 4作 SOC 判据,net/ems.c:774/1491上报时写u8ForceSOC * FORCE_SOC_MULT / 10,而handle_aging_mode()又把u16ActivePower * 10当 SOC 门限(:880)。同一批云端字段被三种单位解释,±4与/10这两个缩放没有任何注释。- 硬编码值散落:
:418的20*1000、:423的30*60*1000、:809的90、:849的60 * 1000、:1292的800、:299的4500/5500、:1420(SFFR_MODE)、:1565的65535。频率阈值4500/5500只有注释"频率异常"说明是 45/55 Hz 边界,没有宏;±4、800、90完全靠读代码推断含义。 - 调试打印的门控组合可疑:
:789与:864写的是#if (DEBUG == 1) & (DEBUG_EVERGEN == 1)——用的是按位与&而不是逻辑与&&。在#if预处理表达式里两者对整型常量结果等价,但写法本身是笔误;同样的模式在:564、:1400等 20 余处#if (DEBUG_EVERGEN == 1)里也存在。无功能影响,但会让人误以为条件更复杂。 excute_system_dispatch_command()里有引用未定义标识符的调试代码::1400-1403的printf使用了l_s32_inv_ac_rated_power、l_s32_allow_charge_power、l_s32_allow_dischg_power、l_u16_bat_total_soc、g_t_dispatch_t.t_stsystemcmdinfo.u8AgingModel——这些都不是本函数的局部变量,u8AgingModel字段名也不存在(应为u8AgingMode)。它被#if (DEBUG_EVERGEN == 1)包裹,只有开启该宏时才会编译失败。属版本遗留死代码,建议清理。get_dispatch_running_mode()对调频模式永远返回NO_DISPATCH_CONTROL::128-152的调频分支只有default: return NO_DISPATCH_CONTROL;,没有任何具体case。因此只要调频使能,该函数就返回"无调度",net/ems.c:6492的if (get_dispatch_running_mode() != NO_DISPATCH_CONTROL)也就不会为调频启动数据上报路径(调频上报走的是net/ems.c:6321的另一条路径)。看起来是有意为之,但这个"空 switch"很容易被误认为是漏写case。dispatch_api_maxfeedin_priority未在头文件声明:它被定义为全局而非static,却只在本文件内部使用,且不出现在dispatch.h。属于"既不是内部函数也没有对外契约"的中间态,建议加static或补声明。同类情况还有 15 个handle_*与dispatch_Sunspec_ative_power_mode。NEGATIVE_VALUE_OFFSET与本文件无引用:定义在dispatch.h,本文件从未使用,属遗留宏。