1. 项目概述:基于STM32的MODBUS-RTU协议实现
在工业控制领域,MODBUS-RTU协议因其简单可靠的特点被广泛应用。最近我在一个温控系统中实现了基于STM32F103VET6的MODBUS-RTU主从机通信,将DS18B20温度传感器的数据通过MODBUS协议传输,并用4位数码管显示。这个方案特别适合需要远程监控温度的小型工业场景。
整个系统包含两个核心部分:作为从机的温度采集节点和作为主机的数据显示终端。从机负责读取DS18B20的温度数据并响应主机的查询请求,主机则定时轮询从机获取最新温度值,通过74HC595驱动的数码管模块显示。这种架构在实际应用中非常灵活,可以根据需要扩展多个温度采集节点。
2. 硬件设计与选型
2.1 核心控制器选择
我选择了STM32F103VET6作为主控芯片,主要基于以下考虑:
- 72MHz主频足够处理MODBUS协议和传感器数据
- 丰富的USART接口方便实现多设备通信
- 充足的GPIO可驱动数码管等外设
- 成本适中,开发资源丰富
2.2 温度传感器选型
DS18B20数字温度传感器具有以下优势:
- 单总线接口节省IO资源
- ±0.5℃的测量精度满足大多数应用
- 独特的64位序列号支持多设备并联
- -55℃~+125℃的宽温度范围
注意:DS18B20对时序要求严格,建议使用硬件定时器实现精确延时,避免软件延时带来的误差。
2.3 显示模块设计
采用4位共阳数码管+74HC595移位寄存器的方案:
- 仅需3个GPIO即可控制4位数码管
- 595的串行输入简化了布线
- 动态扫描方式降低功耗
- 内置锁存功能避免显示闪烁
3. MODBUS-RTU协议实现详解
3.1 从机实现核心逻辑
从机代码的核心是解析MODBUS请求并返回相应数据。以下是关键处理流程:
c复制void USART1_IRQHandler(void)
{
static uint8_t usRxBuffer[8];
static uint8_t ucRxCounter = 0;
// 接收数据到缓冲区
usRxBuffer[ucRxCounter++] = USART1->DR;
// 帧超时检测
if(ucRxCounter > 0) {
TIM4->CNT = 0;
TIM4->CR1 |= TIM_CR1_CEN;
}
// 完整帧处理
if(ucRxCounter >= 8) {
Process_Modbus_Frame(usRxBuffer);
ucRxCounter = 0;
}
}
3.2 功能码03处理(读取保持寄存器)
这是最常用的功能码,用于读取温度数据:
c复制if(usRxBuffer[1] == 0x03) {
// 校验寄存器地址和数量
if(memcmp(&usRxBuffer[2], "\x20\x00\x00\x01", 4) == 0) {
int16_t temp = DS18B20_GetTemp() * 10; // 放大10倍保留小数
usSndBuffer[0] = DeviceAddr;
usSndBuffer[1] = 0x03;
usSndBuffer[2] = 0x02; // 返回字节数
usSndBuffer[3] = temp >> 8;
usSndBuffer[4] = temp & 0xFF;
uint16_t crc = CRC16(usSndBuffer, 5);
usSndBuffer[5] = crc & 0xFF;
usSndBuffer[6] = crc >> 8;
USART_Send(usSndBuffer, 7);
}
}
3.3 功能码06处理(写单个寄存器)
用于修改从机地址和波特率:
c复制case 0x06:
// 检查写入值是否为特定触发值
if((usRxBuffer[4] == 0x00) && (usRxBuffer[5] == 0xC0)) {
DeviceAddr = usRxBuffer[2]; // 更新设备地址
UART1_BaudRate = 38400; // 设置新波特率
USART_Init(USART1, UART1_BaudRate); // 重新初始化串口
// 返回确认响应
memcpy(usSndBuffer, usRxBuffer, 6);
CRC16(usSndBuffer, 6);
Send_Data(usSndBuffer, 8);
}
break;
4. 主机系统实现
4.1 主机查询机制
主机需要定时轮询从机获取温度数据:
c复制void MODBUS_Query(void)
{
static uint8_t query[8] = {0x01,0x03,0x20,0x00,0x00,0x01,0x00,0x00};
query[0] = TargetAddr; // 目标从机地址
uint16_t crc = CRC16(query, 6);
query[6] = crc & 0xFF;
query[7] = crc >> 8;
USART_Send(USART1, query, 8);
HAL_Delay(200); // 等待从机响应
}
4.2 数码管显示实现
采用74HC595驱动数码管的代码:
c复制void HC595_Send(uint8_t *data, uint8_t len)
{
HAL_GPIO_WritePin(HC595_LATCH_GPIO_Port, HC595_LATCH_Pin, GPIO_PIN_RESET);
for(int i=len-1; i>=0; i--) {
for(int j=0; j<8; j++) {
HAL_GPIO_WritePin(HC595_DATA_GPIO_Port, HC595_DATA_Pin,
(data[i] & (1<<j)) ? GPIO_PIN_SET : GPIO_PIN_RESET);
HAL_GPIO_WritePin(HC595_CLK_GPIO_Port, HC595_CLK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(HC595_CLK_GPIO_Port, HC595_CLK_Pin, GPIO_PIN_RESET);
}
}
HAL_GPIO_WritePin(HC595_LATCH_GPIO_Port, HC595_LATCH_Pin, GPIO_PIN_SET);
}
5. 关键问题与解决方案
5.1 CRC校验失败问题排查
在实际调试中,CRC校验失败是最常见的问题。主要原因包括:
- 字节间隔超时设置不当
- 波特率不匹配
- 线路干扰导致数据错误
解决方案:
- 调整定时器超时时间(通常3.5个字符时间)
- 确保主从机波特率一致
- 在RS485总线上加120Ω终端电阻
5.2 数码管显示闪烁问题
当系统负载较重时,数码管可能出现闪烁。解决方法:
- 提高扫描频率(建议>100Hz)
- 使用DMA传输显示数据
- 在定时器中断中处理显示刷新
5.3 多从机通信管理
当系统需要管理多个温度采集节点时:
- 为每个从机分配唯一地址
- 增加查询间隔避免总线冲突
- 实现超时重试机制
6. 系统优化建议
6.1 协议栈优化
可以考虑以下优化措施:
- 使用DMA进行串口数据传输
- 实现MODBUS协议的状态机处理
- 添加异常响应处理(非法功能码等)
6.2 功能扩展思路
基于现有框架可以轻松扩展:
- 添加更多功能码(如05写线圈)
- 支持浮点数传输
- 实现MODBUS TCP网关功能
6.3 抗干扰设计
工业环境中的抗干扰措施:
- 在RS485接口添加TVS二极管
- 使用屏蔽双绞线
- 做好电源滤波
在实际项目中,我发现MODBUS协议虽然简单,但要实现稳定可靠的通信还是需要注意很多细节。特别是时序控制和错误处理方面,需要充分考虑各种异常情况。这个项目中最有价值的经验是:协议层的稳定性往往比功能实现更重要,良好的错误处理机制可以大幅提高系统可靠性。