你的判断是对的:比赛现场最值得熟悉的是“寄存器映射层 + 业务绑定层”,FreeModbus 协议栈本身一般不要临场修改。

整体调用链可以记成:

eMBPoll() → 四类寄存器回调 → modbus_map_*()Get/Set业务函数 → ADC、RTC、DAC、Flash 等硬件功能

第一优先级:寄存器表定义

重点看 modbus_map.h

这里的四个枚举直接决定四类寄存器的数量和顺序:

  • modbus_input_address_t:输入寄存器,功能码 04
  • modbus_holding_address_t:保持寄存器,功能码 03/06/10
  • modbus_coil_address_t:线圈,功能码 01/05/0F
  • modbus_discrete_address_t:离散输入,功能码 02

每个枚举末尾的:

  • MODBUS_INPUT_COUNT
  • MODBUS_HOLDING_COUNT
  • MODBUS_COIL_COUNT
  • MODBUS_DISCRETE_COUNT

就是对应区域的总数量。

现场增减、调整顺序,首先改这里。由于枚举地址连续递增,把一个项目插入中间,后面所有地址都会自动后移。

特别注意:

  • floatuint32_t 占两个连续寄存器。
  • 必须保持 XXX_HIXXX_LO 相邻,且 HI 在前。
  • 上位机 PDU 地址从 0 开始;显示地址通常从 30001/40001/00001/10001 开始。

第二优先级:映射内容与业务功能

核心文件是 modbus_map.c,这是比赛时最可能改动的文件。

输入寄存器、离散输入

update_measurement_snapshot()

  • GetTime() → 时间输入寄存器
  • GetCH0()/GetCH1()/GetCH2Temp() → 测量值输入寄存器
  • IsCH0Alarm()/IsCH1Alarm() → 离散输入和报警位

新增只读测量数据时,一般需要:

  1. modbus_map.h 增加地址枚举。
  2. update_measurement_snapshot() 中把业务值写入对应寄存器。
  3. 如果是 32 位值,用 registers_set_u32()
  4. 如果是浮点值,用 registers_set_float()

保持寄存器

需要同时理清三个阶段。

初始化值在 modbus_map_init()

  • 决定设备启动时保持寄存器显示什么。
  • 从业务层的 GetID()GetRatio0() 等函数获取当前配置。

写入合法性在:

  • holding_scalar_is_valid():单寄存器范围,例如 ID、波特率、DAC。
  • holding_candidate_is_valid():浮点值、组合字段的合法性检查。

写入后真正执行功能在 apply_holding_changes()

  • 寄存器地址 → setID()
  • 寄存器地址 → setBaudrateCode()
  • 寄存器地址 → setRatio0()
  • 寄存器地址 → setThreshold0()
  • 寄存器地址 → setDAC()

因此,新增一个可写保持寄存器时,通常至少要修改:

  1. 地址枚举。
  2. modbus_map_init() 初始值。
  3. 合法性检查。
  4. apply_holding_changes() 中的业务函数调用。

线圈

看:

  • modbus_map_write_coils():线圈地址对应什么命令。
  • modbus_map_task():命令在主循环中何时真正执行。

当前三个线圈分别设置:

  • reboot_pending
  • sleep_pending
  • apply_time_pending

当前设计是“命令线圈”:读回永远是 0,写 1 触发一次动作。新增动作时需要增加:

  1. 线圈枚举。
  2. pending 标志。
  3. modbus_map_write_coils()switch 分支。
  4. modbus_map_task() 中的实际处理。

第三优先级:四类寄存器回调

modbus_app.c

  • eMBRegInputCB()
  • eMBRegHoldingCB()
  • eMBRegCoilsCB()
  • eMBRegDiscreteCB()

这些函数负责:

  • 将 FreeModbus 的地址转换成映射表地址。
  • 处理寄存器高低字节。
  • 打包、拆包线圈位。
  • 调用 modbus_map_read_*()modbus_map_write_*()
  • 将映射层错误转换成 Modbus 异常码。

只改变数量、顺序和功能时,这四个函数通常不需要修改,因为数组大小都引用了 MODBUS_*_COUNT

但一定要理解这一句:

start = address - 1U;

FreeModbus 调用回调时传入的是从 1 开始的内部地址,映射层使用从 0 开始的 PDU 地址。不要因为上位机写的是地址 0,就把这里的减 1 删除。

还要理解数据顺序:

32位数据:高16位寄存器 → 低16位寄存器
单个寄存器:高字节 → 低字节

第四优先级:业务 Getter/Setter

APP.hAPP.c

这里决定寄存器最后对应哪个真实功能,例如:

  • GetCH0():读取测量结果
  • setTime():设置 RTC
  • setDAC():设置 DAC
  • setThreshold0():修改报警阈值
  • reboot()/sleep():执行一次性动作

如果现场只要求“更换某个寄存器对应的功能”,通常就是修改 modbus_map.c 中的映射调用,再确认这里已有对应 Getter/Setter。

如果没有,就在 APP.h/.c 增加,尽量不要让 modbus_map.c 直接操作底层 ADC、RTC、GPIO。

持久化配置还要关注

如果新增的保持寄存器要求掉电保存,还要修改:

  • app_config.h 中的 Config_t
  • app_config.c 中的默认值和加载合法性检查
  • APP.c 中相应的 Getter/Setter,并在 Setter 中调用 config_mark_dirty()

否则寄存器虽然可以写、功能也能执行,但重启后会恢复。

两个容易踩的限制

第一,当前保持寄存器脏标志是:

static uint32_t holding_dirty;
#define DIRTY_BIT(address) (1U << address)

而且有 MODBUS_HOLDING_COUNT <= 32 的编译期限制。因此保持寄存器超过 32 个时,必须把脏标志改成数组、64 位整数或其他结构,不能只改 MODBUS_HOLDING_COUNT

第二,增加持久化字段会改变 Config_t 的 Flash 数据布局。旧配置页可能被按照新结构错误解释。正式比赛前最好同步修改 CONFIG_MAGIC,让旧配置失效并重新加载默认值。

一般不需要临场修改

  • Modbus/freemodbus/:协议状态机、CRC、标准功能码。
  • Modbus/port/:串口、RS-485 DE、T3.5 定时器。
  • mbconfig.h:只有需要启用/关闭功能码、RTU/ASCII/TCP 时才改。
  • start_stack():只有从站地址、波特率、校验方式发生规则变化时才重点看。
  • main.c:只要保持循环调用 eMBPoll()、测量任务和 modbus_app_task() 即可。

最实用的复习顺序是:

  1. 背熟 modbus_map.h 四张地址表。
  2. 理清 update_measurement_snapshot()
  3. 理清 modbus_map_init()
  4. 理清 holding_candidate_is_valid()apply_holding_changes()
  5. 理清 modbus_map_write_coils()modbus_map_task()
  6. 最后理解四个 eMBReg*CB() 的地址转换与字节序。

仓库里的 APP/README.md 已经列出了当前完整寄存器表,可以把它作为比赛现场的第一份对照表。