1. RS485 物理层基础

RS485 采用差分信号传输,A、B 两线之间的电压差表示逻辑状态(>200mV 为 1,<-200mV 为 0),因此具有很强的抗共模干扰能力。其关键特性包括:

  • 多节点能力:标准驱动芯片(如 MAX485)支持 32 个节点,若使用低负载芯片(如 ISL3170)可达 256 个节点。
  • 传输距离:在 100kbps 速率下可达 1200 米,速率越高,距离越短(如 10Mbps 时约 15 米)。
  • 半双工模式:同一时刻只能发送或接收,需要控制 DE/RE 引脚切换方向。

收发切换的时序陷阱

  • 发送完成后,必须等待最后一个字节完全移出移位寄存器(通常需要 2-3 个字符时间),才能关闭发送模式。
  • 若切换过快,总线上的最后一位可能被截断,导致接收方帧错误。
  • 推荐使用 __HAL_UART_GET_FLAG(&huart, UART_FLAG_TC) 等待发送完成,再拉低 DE。

2. Modbus RTU 协议帧格式

Modbus RTU 以字节流形式传输,帧结构如下:

[地址域 1字节] [功能码 1字节] [数据域 N字节] [CRC16 低字节] [CRC16 高字节]
  • 地址域:从机地址 1-247,0 为广播地址。
  • 功能码:常用 0x03(读保持寄存器)、0x06(写单个寄存器)、0x10(写多个寄存器)。
  • 数据域:寄存器地址、数量、数据等,大端序(高字节在前)。
  • CRC16:对地址到数据域末尾的所有字节计算,多项式 0xA001,初始值 0xFFFF,低字节先发送。

帧间隔要求:RTU 模式下,帧与帧之间至少需要 3.5 个字符时间的静默(如 9600bps 下约 4ms),帧内字节间隔不能超过 1.5 个字符时间,否则视为帧断裂。

3. 从机实现核心设计

3.1 状态机解析

推荐使用状态机处理字节流,避免阻塞式接收:

typedef enum {
    IDLE,       // 空闲,等待起始字节
    RECEIVING,  // 接收中
    COMPLETE,   // 帧接收完成
} FrameState;

3.2 定时器管理帧超时

使用一个 1ms 或 0.1ms 的定时器中断,在每次收到字节时重置计数值,若超过 3.5 字符时间未收到新字节,则判定帧结束。

4. 完整代码示例(STM32 HAL 库)

以下代码基于 STM32F1xx 和 MAX485 芯片,使用 USART2 + 定时器 TIM6。

4.1 初始化配置

// USART2 初始化:9600, 8N1
void MX_USART2_UART_Init(void) {
    huart2.Instance = USART2;
    huart2.Init.BaudRate = 9600;
    huart2.Init.WordLength = UART_WORDLENGTH_8B;
    huart2.Init.StopBits = UART_STOPBITS_1;
    huart2.Init.Parity = UART_PARITY_NONE;
    huart2.Init.Mode = UART_MODE_TX_RX;
    huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    HAL_UART_Init(&huart2);
    // 使能接收中断
    HAL_UART_Receive_IT(&huart2, &rx_byte, 1);
}

// 控制 DE/RE 引脚(PC1)
#define RS485_DE_HIGH() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_SET)
#define RS485_DE_LOW()  HAL_GPIO_WritePin(GPIOC, GPIO_PIN_1, GPIO_PIN_RESET)

4.2 定时器配置(1ms 中断)

void MX_TIM6_Init(void) {
    TIM6->PSC = 72-1;  // 72MHz/72 = 1MHz
    TIM6->ARR = 1000-1; // 1ms 中断
    HAL_TIM_Base_Start_IT(&htim6);
}

volatile uint16_t frame_timer = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM6) {
        if (frame_timer < 0xFFFF) frame_timer++;
        if (rx_state == RECEIVING && frame_timer > 4) { // 4ms > 3.5字符@9600
            rx_state = COMPLETE;
        }
    }
}

4.3 接收中断处理

volatile uint8_t rx_byte;
volatile uint8_t rx_buf[256];
volatile uint16_t rx_len = 0;
volatile FrameState rx_state = IDLE;

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART2) {
        if (rx_state == IDLE) {
            rx_len = 0;
            rx_state = RECEIVING;
        }
        if (rx_len < 256) {
            rx_buf[rx_len++] = rx_byte;
            frame_timer = 0; // 重置超时
        }
        HAL_UART_Receive_IT(&huart2, &rx_byte, 1); // 继续接收
    }
}

4.4 主循环处理帧

void Modbus_Poll(void) {
    if (rx_state == COMPLETE) {
        // 1. 校验 CRC
        uint16_t crc = Modbus_CRC16(rx_buf, rx_len - 2);
        if (crc != (rx_buf[rx_len-1] << 8 | rx_buf[rx_len-2])) {
            rx_state = IDLE; // CRC 错误,丢弃
            return;
        }
        // 2. 检查地址
        if (rx_buf[0] != 0x01 && rx_buf[0] != 0x00) { // 本机地址 1
            rx_state = IDLE;
            return;
        }
        // 3. 处理功能码
        uint8_t func = rx_buf[1];
        uint8_t tx_len = 0;
        switch (func) {
            case 0x03: // 读保持寄存器
                tx_len = Handle_ReadHolding(rx_buf, tx_buf);
                break;
            case 0x06: // 写单个寄存器
                tx_len = Handle_WriteSingle(rx_buf, tx_buf);
                break;
            default: // 异常响应
                tx_buf[0] = rx_buf[0];
                tx_buf[1] = func | 0x80;
                tx_buf[2] = 0x01; // 非法功能码
                tx_len = 3;
                break;
        }
        // 4. 发送响应
        if (tx_len > 0) {
            uint16_t crc_tx = Modbus_CRC16(tx_buf, tx_len);
            tx_buf[tx_len++] = crc_tx & 0xFF;
            tx_buf[tx_len++] = (crc_tx >> 8) & 0xFF;
            RS485_DE_HIGH();
            HAL_UART_Transmit(&huart2, tx_buf, tx_len, 100);
            while (__HAL_UART_GET_FLAG(&huart2, UART_FLAG_TC) == RESET);
            RS485_DE_LOW();
        }
        rx_state = IDLE;
    }
}

4.5 CRC16 计算函数

uint16_t Modbus_CRC16(uint8_t *data, uint16_t len) {
    uint16_t crc = 0xFFFF;
    for (uint16_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

4.6 功能码处理示例(读保持寄存器)

uint8_t Handle_ReadHolding(uint8_t *rx, uint8_t *tx) {
    uint16_t start_reg = (rx[2] << 8) | rx[3];
    uint16_t reg_cnt = (rx[4] << 8) | rx[5];
    if (reg_cnt > 125 || start_reg + reg_cnt > 10) { // 假设有10个寄存器
        tx[0] = rx[0]; tx[1] = 0x83; tx[2] = 0x02; // 非法数据地址
        return 3;
    }
    tx[0] = rx[0];
    tx[1] = 0x03;
    tx[2] = reg_cnt * 2;
    for (uint16_t i = 0; i < reg_cnt; i++) {
        uint16_t val = holding_regs[start_reg + i];
        tx[3 + i*2] = val >> 8;
        tx[4 + i*2] = val & 0xFF;
    }
    return 3 + reg_cnt * 2;
}

5. 注意事项与调试技巧

  • 总线终端电阻:在总线两端各接 120Ω 匹配电阻,防止信号反射。若节点数少且距离短,可省略。
  • 偏置电阻:在 A、B 间加 390Ω 上拉/下拉,确保空闲时总线电平确定(A 高 B 低)。
  • 隔离与地电位:长距离通信建议使用隔离收发器(如 ADM2483),避免地环路。
  • 调试工具:使用 USB-RS485 适配器配合串口助手,先用 PC 模拟主机测试从机响应。
  • 时序验证:用示波器观察 A-B 差分波形,确认发送切换时序是否正确。
  • 异常处理:从机应能处理非法功能码、非法地址、CRC 错误,并返回异常响应或静默丢弃。

6. 总结

实现 RS485 Modbus 从机的关键在于:物理层正确控制收发切换、协议层严格遵循帧时序和 CRC 校验。本文给出的状态机框架可轻松扩展至其他功能码,且不占用 CPU 阻塞时间。在实际项目中,建议进一步加入看门狗、错误计数和诊断寄存器,提升通信的鲁棒性。掌握这些基础,你就能快速构建出符合工业标准的 Modbus 设备。