以“设备当前波特率为 9600,主站把保持寄存器 40002 写成 0x13,切换到 19200”为例,完整流程如下。

主站以旧波特率发送写命令
        ↓
FreeModbus 解析请求
        ↓
写入 holding_registers[波特率代码]
        ↓
设置 holding_dirty
        ↓
先以旧波特率发送成功响应
        ↓
响应发送完成
        ↓
setBaudrateCode() 更新 APP 配置
        ↓
停止旧 Modbus 栈
        ↓
按新波特率重新初始化
        ↓
延迟写入 Flash

1. 主站发送写请求

波特率代码位于:

MODBUS_HOLDING_BAUDRATE_CODE = 1

也就是:

  • 显示地址:40002
  • Modbus PDU 地址:1
  • 允许值:
代码波特率
0x114800
0x129600
0x1319200
0x14115200

假设当前设备使用 9600,主站通过功能码 06 写入:

从站地址:1
功能码:06
寄存器地址:0001
寄存器值:0013

这条命令必须使用当前的旧波特率 9600 发送。

2. FreeModbus 收到完整帧

USART 中断逐字节接收数据,RTU 的 T3.5 定时器判断一帧结束,然后产生:

EV_FRAME_RECEIVED

主循环持续执行:

while(1) {
    (void)eMBPoll();
    app_measurement_task();
    modbus_app_task();
    alarm_task();
}

位置:main.c

eMBPoll() 收到完整帧后,检查从站地址,然后产生 EV_EXECUTE 事件。

3. 执行功能码 06

下一次 eMBPoll() 处理 EV_EXECUTE,找到功能码处理函数:

eMBFuncWriteHoldingRegister()

FreeModbus 从请求帧中取出地址和值:

usRegAddress = ...;
usRegAddress++;

这里 PDU 地址 1 加一后,以地址 2 传给应用回调:

eMBRegHoldingCB(buffer, 2, 1, MB_REG_WRITE);

4. 应用回调解析数据

进入 modbus_app.c

eMBErrorCode eMBRegHoldingCB(UCHAR *buffer,
                             USHORT address,
                             USHORT count,
                             eMBRegisterMode mode)

首先转换成数组下标:

start = address - 1U;

因此:

address = 2
start   = 1

正好对应:

holding_registers[MODBUS_HOLDING_BAUDRATE_CODE]

然后把 Modbus 大端字节转换成 uint16_t

values[i] = ((uint16_t)buffer[0] << 8) | buffer[1];

得到:

values[0] = 0x0013;

最后调用:

modbus_map_write_holding(1, 1, values);

5. 校验并更新保持寄存器映像

进入 modbus_map_write_holding()

它先检查地址范围,然后创建候选数组:

memcpy(candidate, holding_registers, sizeof(candidate));
memcpy(&candidate[start], values,
       quantity * sizeof(uint16_t));

这样做是为了先验证,不立即破坏原数组。

波特率代码校验逻辑是:

if(address == MODBUS_HOLDING_BAUDRATE_CODE) {
    return value >= 0x11U && value <= 0x14U;
}

0x13 合法,所以正式写入:

holding_registers[1] = 0x13;

并设置脏标志:

holding_dirty |= DIRTY_BIT(1);

相当于:

holding_dirty |= 1U << 1;

此时要特别注意:

holding_registers[] 已经是 0x13
APP config 仍然是旧的 0x12
USART 仍然运行在旧波特率 9600

这里只是更新了 Modbus 映像,还没有真正修改串口。

6. 先以旧波特率返回响应

回调成功后,FreeModbus 生成功能码 06 的响应。功能码 06 的正常响应基本是原请求回显:

从站地址:1
功能码:06
寄存器地址:0001
寄存器值:0013
CRC

此时协议栈和 USART 仍然使用旧波特率 9600,所以响应也以 9600 发送。

开始发送后:

tx_active = TRUE;

随后主循环虽然调用了 modbus_app_task(),但会被这里拦住:

if(!xMBPortSerialIsIdle()) {
    return;
}

因此响应没有发完之前,不会修改波特率。

这是流程中最关键的一点。否则设备可能发送到一半突然从 9600 切到 19200,主站就会收到乱码或半帧。

7. 响应发送完成

USART 发送中断把响应的所有字节发送完毕,随后:

vMBPortSerialEnable(TRUE, FALSE);

这个函数会:

  1. 关闭发送空中断。
  2. 等待最后一个字节真正移出 USART。
  3. 将 RS485 切换回接收方向。
  4. 设置:
tx_active = FALSE;

此时主站已经完整收到旧波特率下的成功响应。

8. modbus_map_task() 应用新配置

下一轮主循环再次进入:

modbus_app_task();

现在串口已经空闲,所以继续执行:

actions = modbus_map_task();

modbus_map_task() 调用:

actions = apply_holding_changes();

apply_holding_changes() 发现波特率对应的 dirty 位:

if(dirty & DIRTY_BIT(MODBUS_HOLDING_BAUDRATE_CODE)) {
    setBaudrateCode(
        (uint8_t)holding_registers[
            MODBUS_HOLDING_BAUDRATE_CODE
        ]
    );

    actions |= MODBUS_MAP_ACTION_RECONFIGURE;
}

于是执行:

setBaudrateCode(0x13);

9. 修改 APP 配置

setBaudrateCode()APP.c 中:

void setBaudrateCode(uint8_t code)
{
    if(code >= 0x11U &&
       code <= 0x14U &&
       config.baudrate_code != code) {

        config.baudrate_code = code;
        config_mark_dirty();
    }
}

执行后:

config.baudrate_code = 0x13
config_dirty         = 1

现在 APP 配置已经变成新波特率代码,但 USART 尚未重新初始化。

同时返回动作:

MODBUS_MAP_ACTION_RECONFIGURE

10. 关闭旧协议栈

回到 modbus_app_task()

if(actions & MODBUS_MAP_ACTION_RECONFIGURE) {
    (void)eMBDisable();
    (void)start_stack();
}

首先:

eMBDisable();

它会停止旧 Modbus RTU 栈:

vMBPortSerialEnable(FALSE, FALSE);
vMBPortTimersDisable();

也就是:

  • 关闭 USART 接收中断
  • 关闭 USART 发送中断
  • 关闭 RTU 帧间隔定时器
  • 将协议栈状态改成 STATE_DISABLED

11. 用新波特率启动协议栈

随后执行:

start_stack();

其中:

status = eMBInit(
    MB_RTU,
    GetID(),
    0U,
    GetBaudrate(),
    MB_PAR_NONE
);

GetBaudrate() 会读取刚更新的:

config.baudrate_code == 0x13

然后通过:

code2baudrate(0x13)

得到:

19200

最终重新初始化串口:

eMBInit(..., 19200, ...);
eMBEnable();

从这一刻开始,设备只使用新波特率 19200 接收后续 Modbus 请求,主站也需要切换到 19200。

12. 延迟保存到 Flash

setBaudrateCode() 只是修改了 RAM 中的:

static Config_t config;

并设置:

config_dirty = 1;

在后面的循环里,app_config_task() 判断配置已经保持稳定至少 500 ms 后:

if(config_dirty &&
   g_system_ticks - config_dirty_tick >= 500U) {

    save_config(&config);
}

这样会把新波特率保存到 Flash,保证下次重新上电仍然使用 19200。

所以存在一个短暂阶段:

新波特率已经生效
但尚未保存到 Flash

如果这 500 ms 内突然断电,下次启动可能仍使用旧波特率。

非法值时的流程

如果主站写入:

0x15

校验会失败:

value >= 0x11 && value <= 0x14

不成立,因此:

  • 不更新 holding_registers[]
  • 不设置 holding_dirty
  • 不调用 setBaudrateCode()
  • 不重启协议栈
  • 不修改 Flash
  • 主站收到 Modbus 异常码 03:非法数据值

异常响应仍然使用旧波特率发送。

最终,修改波特率采用的是一种很合理的“两阶段提交”:

第一阶段:校验并更新 Modbus 映像,使用旧波特率回复主站
第二阶段:回复完成后,把配置应用到 APP,再以新波特率重启协议栈