## 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 状态机解析 推荐使用状态机处理字节流,避免阻塞式接收: ```c typedef enum { IDLE, // 空闲,等待起始字节 RECEIVING, // 接收中 COMPLETE, // 帧接收完成 } FrameState; ``` ### 3.2 定时器管理帧超时 使用一个 1ms 或 0.1ms 的定时器中断,在每次收到字节时重置计数值,若超过 3.5 字符时间未收到新字节,则判定帧结束。 ## 4. 完整代码示例(STM32 HAL 库) 以下代码基于 STM32F1xx 和 MAX485 芯片,使用 USART2 + 定时器 TIM6。 ### 4.1 初始化配置 ```c // 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 中断) ```c 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 接收中断处理 ```c 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 主循环处理帧 ```c 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 计算函数 ```c 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 功能码处理示例(读保持寄存器) ```c 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 设备。