前言
前两章我们搞定了Modbus基础原理、RTU完整帧结构、CRC16算法、正常/异常报文解析。很多同学学完理论还是不会写代码,核心问题只有一个:只懂协议,不懂工程落地逻辑。
本章我们从零手写纯裸机Modbus RTU主机,不依赖任何第三方库、不移植源码、全程逐行编写、逐行讲解。
读完本章,你将掌握:
-
Modbus主机核心调度逻辑(轮询机制、一问一答)
-
03/06/16三大核心功能码完整实现
-
工业级超时判断、丢包重发、异常帧过滤
-
CRC校验、帧合法性校验全套逻辑
-
STM32裸机主机工程模板
1. Modbus主机核心逻辑(工程底层核心)
1.1 主机唯一规则:绝对被动轮询
再次强调Modbus最核心的底层规则,所有代码逻辑都围绕这一点展开:
总线永远只有一台主机,所有通信必须主机先发指令,从机被动应答,从机绝不主动发包。
主机工作流程固定死,没有任何例外:
-
组装Modbus请求帧(地址+功能码+寄存器地址+数量+CRC)
-
串口发送完整报文
-
开启超时计时,等待从机应答
-
收到数据后,校验帧合法性、CRC、异常码
-
合法数据解析存储,异常/超时直接丢弃、标记故障
-
间隔固定时间,发起下一轮轮询
1.2 新手最大误区(90%人踩坑)
-
误区1:连续快速发指令,不等待应答 → 总线拥堵、数据错乱
-
误区2:没有超时机制 → 从机掉线后程序永久卡死
-
误区3:不校验CRC、不判断异常帧 → 干扰环境下数据乱跳
-
误区4:帧长度写死 → 多寄存器读取解析失败
本章代码将彻底规避以上所有问题,完全对标工业设备标准。
2. 工程底层配置(统一标准)
2.1 硬件基础
-
MCU:STM32F103/F407 通用
-
通信方式:USART串口 + MAX485收发模块
-
波特率:9600/115200(工程最常用)
-
数据位8、停止位1、无校验(Modbus RTU标准)
2.2 全局宏定义(统一参数,方便改配)
单独封装配置宏,后续适配不同设备只需改这几行:
// Modbus主机全局配置
#define MODBUS_BAUDRATE 115200 // 波特率
#define MODBUS_TIMEOUT 100 // 超时时间ms
#define MODBUS_MAX_TX_LEN 20 // 最大发送帧长
#define MODBUS_MAX_RX_LEN 30 // 最大接收帧长
#define MODBUS_SLAVE_ADDR 0x01 // 默认从机地址
2.3 全局变量定义
// 收发缓冲区
uint8_t modbus_tx_buf[MODBUS_MAX_TX_LEN];
uint8_t modbus_rx_buf[MODBUS_MAX_RX_LEN];
// 帧长度、超时标记、接收计数
uint8_t tx_len = 0;
uint8_t rx_len = 0;
uint8_t modbus_timeout_flag = 0;
uint32_t modbus_tick = 0;
// 解析出的寄存器数据(可存储16组寄存器)
uint16_t modbus_reg_data[16] = {0};
3. 前置基础函数(CRC+串口底层)
3.1 标准Modbus CRC16校验函数(复用前章)
固定多项式0xA001、初始值0xFFFF,完全适配RTU协议:
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length)
{
uint16_t crc = 0xFFFF;
uint16_t i, j;
for(i = 0; i < length; i++)
{
crc ^= data[i];
for(j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
3.2 CRC追加函数(自动补全帧尾校验码)
自动计算CRC并按低字节在前、高字节在后的Modbus标准追加:
void Modbus_Append_CRC(uint8_t *buf, uint8_t *len)
{
uint16_t crc = Modbus_CRC16(buf, *len);
buf[*len] = crc & 0xFF; // CRC低字节
buf[*len + 1] = (crc >> 8) & 0xFF;// CRC高字节
*len += 2;
}
3.3 串口基础收发函数
基于STM32标准串口,极简封装,适配所有型号:
// 串口发送指定长度数据
void USART_Send_Data(uint8_t *buf, uint8_t len)
{
uint8_t i = 0;
for(i = 0; i < len; i++)
{
USART_SendData(USART1, buf[i]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 等待发送完成
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
// 串口中断接收(清空接收缓冲区、重置超时)
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_FLAG_RXNE) != RESET)
{
modbus_rx_buf[rx_len++] = USART_ReceiveData(USART1);
modbus_tick = HAL_GetTick(); // 刷新计时,防止超时
USART_ClearITPendingBit(USART1, USART_FLAG_RXNE);
}
}
4. 三大核心功能码 主机发送函数(可直接商用)
我们实现工程最常用的三个功能码:03读寄存器、06写单寄存器、16写多寄存器,参数封装统一,调用极简。
4.1 03功能码:读取保持寄存器
参数:从机地址、起始寄存器地址、读取寄存器数量
void Modbus_Master_Read_Reg(uint8_t slave_addr, uint16_t start_reg, uint16_t reg_num)
{
tx_len = 0;
// 1. 组装请求帧
modbus_tx_buf[tx_len++] = slave_addr; // 从机地址
modbus_tx_buf[tx_len++] = 0x03; // 03功能码
modbus_tx_buf[tx_len++] = (start_reg >> 8) & 0xFF; // 起始地址高字节
modbus_tx_buf[tx_len++] = start_reg & 0xFF; // 起始地址低字节
modbus_tx_buf[tx_len++] = (reg_num >> 8) & 0xFF; // 数量高字节
modbus_tx_buf[tx_len++] = reg_num & 0xFF; // 数量低字节
// 2. 追加CRC校验
Modbus_Append_CRC(modbus_tx_buf, &tx_len);
// 3. 清空接收缓冲区、重置超时
rx_len = 0;
modbus_timeout_flag = 0;
modbus_tick = HAL_GetTick();
// 4. 发送报文
USART_Send_Data(modbus_tx_buf, tx_len);
}
4.2 06功能码:写入单个保持寄存器
void Modbus_Master_Write_Single_Reg(uint8_t slave_addr, uint16_t reg_addr, uint16_t value)
{
tx_len = 0;
// 1. 组装请求帧
modbus_tx_buf[tx_len++] = slave_addr;
modbus_tx_buf[tx_len++] = 0x06; // 06功能码
modbus_tx_buf[tx_len++] = (reg_addr >> 8) & 0xFF;
modbus_tx_buf[tx_len++] = reg_addr & 0xFF;
modbus_tx_buf[tx_len++] = (value >> 8) & 0xFF; // 写入数据高字节
modbus_tx_buf[tx_len++] = value & 0xFF; // 写入数据低字节
// 2. 追加CRC
Modbus_Append_CRC(modbus_tx_buf, &tx_len);
// 3. 重置接收状态
rx_len = 0;
modbus_timeout_flag = 0;
modbus_tick = HAL_GetTick();
// 4. 发送
USART_Send_Data(modbus_tx_buf, tx_len);
}
4.3 16功能码:批量写入多个寄存器
void Modbus_Master_Write_Multi_Reg(uint8_t slave_addr, uint16_t start_reg, uint16_t reg_num, uint16_t *data_buf)
{
tx_len = 0;
// 基础帧头
modbus_tx_buf[tx_len++] = slave_addr;
modbus_tx_buf[tx_len++] = 0x10; // 16功能码
modbus_tx_buf[tx_len++] = (start_reg >> 8) & 0xFF;
modbus_tx_buf[tx_len++] = start_reg & 0xFF;
modbus_tx_buf[tx_len++] = (reg_num >> 8) & 0xFF;
modbus_tx_buf[tx_len++] = reg_num & 0xFF;
modbus_tx_buf[tx_len++] = reg_num * 2; // 数据总字节数
// 填充多组写入数据
for(uint16_t i = 0; i < reg_num; i++)
{
modbus_tx_buf[tx_len++] = (data_buf[i] >> 8) & 0xFF;
modbus_tx_buf[tx_len++] = data_buf[i] & 0xFF;
}
// 追加CRC、发送
Modbus_Append_CRC(modbus_tx_buf, &tx_len);
rx_len = 0;
modbus_timeout_flag = 0;
modbus_tick = HAL_GetTick();
USART_Send_Data(modbus_tx_buf, tx_len);
}
5. 工业级帧解析+容错处理(核心精华)
这是区分玩具代码 和量产代码的关键!包含:超时判断、CRC校验、异常帧过滤、地址校验、数据解析全套逻辑。
// 返回值:0=失败 1=成功 2=从机异常
uint8_t Modbus_Master_Parse_Frame(uint16_t reg_num)
{
uint16_t crc_calc, crc_rx;
uint16_t i;
// 1. 超时判断(无应答直接返回失败)
if(HAL_GetTick() - modbus_tick > MODBUS_TIMEOUT)
{
modbus_timeout_flag = 1;
rx_len = 0;
return 0;
}
// 2. 帧长度合法性校验(最小帧长5字节)
if(rx_len < 5) return 0;
// 3. 从机地址校验
if(modbus_rx_buf[0] != MODBUS_SLAVE_ADDR) return 0;
// 4. 判断是否为异常响应帧
if(modbus_rx_buf[1] & 0x80)
{
rx_len = 0;
return 2; // 返回从机异常码
}
// 5. 仅处理03功能码应答
if(modbus_rx_buf[1] != 0x03) return 0;
// 6. 数据字节数校验
if(modbus_rx_buf[2] != reg_num * 2) return 0;
// 7. CRC校验(核心防干扰)
crc_calc = Modbus_CRC16(modbus_rx_buf, rx_len - 2);
crc_rx = (uint16_t)modbus_rx_buf[rx_len - 2] | ((uint16_t)modbus_rx_buf[rx_len - 1] << 8);
if(crc_calc != crc_rx) return 0;
// 8. 解析寄存器数据(大端模式转换)
for(i = 0; i < reg_num; i++)
{
modbus_reg_data[i] = ((uint16_t)modbus_rx_buf[3 + i*2] << 8) | modbus_rx_buf[4 + i*2];
}
// 清空接收缓存,准备下一轮轮询
rx_len = 0;
return 1;
}
6. 主循环轮询调度(完整运行逻辑)
采用100ms定时轮询,不阻塞程序、稳定不卡死,适配所有工控场景:
int main(void)
{
// 底层初始化
HAL_Init();
SystemClock_Config();
USART1_Init(); // 串口初始化
LED_Init(); // 状态灯初始化
uint32_t poll_tick = 0;
uint8_t res = 0;
while(1)
{
// 100ms轮询一次Modbus
if(HAL_GetTick() - poll_tick >= 100)
{
poll_tick = HAL_GetTick();
// 读取从机0、1号寄存器(温度、电压)
Modbus_Master_Read_Reg(MODBUS_SLAVE_ADDR, 0, 2);
// 解析应答数据
res = Modbus_Master_Parse_Frame(2);
if(res == 1)
{
// 解析成功,数据有效
printf("温度:%.2f ℃\r\n", modbus_reg_data[0] / 10.0f);
printf("电压:%.2f V\r\n", modbus_reg_data[1] / 10.0f);
LED_ON();
}
else if(res == 2)
{
printf("从机设备异常!\r\n");
LED_BLINK();
}
else
{
printf("通信超时/数据错误!\r\n");
LED_OFF();
}
}
}
}
7. 工程级避坑指南(专治所有通信故障)
7.1 通信抖动、偶尔丢包
原因:轮询间隔过短、485总线干扰、无超时保护 解决方案:固定100ms轮询、开启超时过滤、严格CRC校验
7.2 从机返回异常码02
原因:寄存器地址越界、起始地址+读取数量超出设备寄存器范围 解决方案:对照设备手册核对寄存器地址(注意手册1起始/代码0起始偏移)
7.3 CRC校验一直失败
99%概率:CRC高低字节顺序写反!必须严格遵循低字节在前、高字节在后
7.4 程序偶尔卡死
原因:无超时机制,从机掉线后主机无限等待 解决方案:本章100ms超时判断,彻底杜绝卡死
8. 本章核心总结
本章我们从零手写了一套工业级Modbus RTU主机,彻底摆脱开源库依赖:
-
掌握了主机核心轮询、一问一答调度逻辑
-
实现03/06/16三大刚需功能码
-
补齐超时、CRC、异常帧、地址校验全套容错