一、Modbus RTU 协议核心
1、帧格式
[地址] [功能码] [数据] [CRC16] [3.5字符静默]
2、常用功能码
| 功能码 | 名称 | 操作 |
|---|---|---|
| 0x03 | 读保持寄存器 | 读取1-125个寄存器 |
| 0x06 | 写单个寄存器 | 写1个寄存器 |
| 0x10 | 写多个寄存器 | 写1-123个寄存器 |
二、STM32 硬件配置
1、串口配置
c
// CubeMX 配置:
// USART1: 波特率9600/19200/38400, 8N1
// DMA: RX/TX 循环模式
// NVIC: 串口空闲中断
2、引脚定义
PA9 → USART1_TX
PA10 → USART1_RX
三、Modbus 数据结构
c
// modbus.h
#ifndef __MODBUS_H
#define __MODBUS_H
#include "stm32f1xx_hal.h"
// Modbus 寄存器定义
#define REG_INPUT_START 0x0000
#define REG_INPUT_NREGS 100
#define REG_HOLDING_START 0x1000
#define REG_HOLDING_NREGS 100
// 设备地址
#define MODBUS_SLAVE_ADDR 0x01
// 功能码
#define MODBUS_FC_READ_COILS 0x01
#define MODBUS_FC_READ_DISCRETE 0x02
#define MODBUS_FC_READ_HOLDING 0x03
#define MODBUS_FC_READ_INPUT 0x04
#define MODBUS_FC_WRITE_SINGLE 0x06
#define MODBUS_FC_WRITE_MULTIPLE 0x10
// 异常码
#define MODBUS_EX_ILLEGAL_FUNCTION 0x01
#define MODBUS_EX_ILLEGAL_ADDRESS 0x02
#define MODBUS_EX_ILLEGAL_VALUE 0x03
#define MODBUS_EX_SLAVE_FAILURE 0x04
// 缓冲区大小
#define MODBUS_RX_BUFFER_SIZE 256
#define MODBUS_TX_BUFFER_SIZE 256
// Modbus 从站结构体
typedef struct {
// 寄存器数组
uint16_t holding_regs[REG_HOLDING_NREGS];
uint16_t input_regs[REG_INPUT_NREGS];
// 缓冲区
uint8_t rx_buf[MODBUS_RX_BUFFER_SIZE];
uint8_t tx_buf[MODBUS_TX_BUFFER_SIZE];
// 状态
uint8_t slave_addr;
uint16_t rx_length;
uint16_t tx_length;
uint8_t is_busy;
} Modbus_Slave_t;
// 函数声明
void Modbus_Init(Modbus_Slave_t *slave, uint8_t addr);
void Modbus_Process(Modbus_Slave_t *slave);
void Modbus_UART_Idle_Callback(UART_HandleTypeDef *huart);
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length);
#endif
四、CRC16 校验(核心)
c
// modbus_crc.c
#include "modbus.h"
// CRC16 表(查表法,速度快)
static const uint16_t crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
// ... 完整表格共256个值
};
uint16_t Modbus_CRC16(uint8_t *data, uint16_t length)
{
uint8_t tmp;
uint16_t crc = 0xFFFF;
while (length--) {
tmp = *data++ ^ (uint8_t)crc;
crc >>= 8;
crc ^= crc16_table[tmp];
}
return crc;
}
五、Modbus 从站处理(核心)
c
// modbus_slave.c
#include "modbus.h"
// 初始化
void Modbus_Init(Modbus_Slave_t *slave, uint8_t addr)
{
slave->slave_addr = addr;
slave->rx_length = 0;
slave->tx_length = 0;
slave->is_busy = 0;
// 初始化寄存器
for (int i = 0; i < REG_HOLDING_NREGS; i++) {
slave->holding_regs[i] = 0;
}
for (int i = 0; i < REG_INPUT_NREGS; i++) {
slave->input_regs[i] = 0;
}
}
// 处理请求
void Modbus_Process(Modbus_Slave_t *slave)
{
uint8_t *rx = slave->rx_buf;
uint8_t *tx = slave->tx_buf;
uint16_t crc, crc_recv;
uint8_t exception = 0;
// 1. 检查地址
if (rx[0] != slave->slave_addr && rx[0] != 0xFF) {
return; // 地址不匹配
}
// 2. CRC 校验
crc_recv = rx[slave->rx_length - 2] | (rx[slave->rx_length - 1] << 8);
crc = Modbus_CRC16(rx, slave->rx_length - 2);
if (crc != crc_recv) {
return; // CRC错误
}
// 3. 解析功能码
uint8_t func = rx[1];
uint16_t addr = (rx[2] << 8) | rx[3];
uint16_t quantity = (rx[4] << 8) | rx[5];
// 4. 根据功能码处理
switch (func) {
case MODBUS_FC_READ_HOLDING: // 0x03
if (quantity >= 1 && quantity <= 125) {
tx[0] = rx[0]; // 地址
tx[1] = func; // 功能码
tx[2] = quantity * 2; // 字节数
for (int i = 0; i < quantity; i++) {
if (addr + i < REG_HOLDING_NREGS) {
tx[3 + i*2] = slave->holding_regs[addr + i] >> 8;
tx[4 + i*2] = slave->holding_regs[addr + i] & 0xFF;
} else {
exception = MODBUS_EX_ILLEGAL_ADDRESS;
break;
}
}
if (exception == 0) {
slave->tx_length = 3 + quantity * 2;
}
} else {
exception = MODBUS_EX_ILLEGAL_VALUE;
}
break;
case MODBUS_FC_WRITE_SINGLE: // 0x06
if (addr < REG_HOLDING_NREGS) {
uint16_t value = (rx[4] << 8) | rx[5];
slave->holding_regs[addr] = value;
// 响应(回显)
memcpy(tx, rx, 6);
slave->tx_length = 6;
} else {
exception = MODBUS_EX_ILLEGAL_ADDRESS;
}
break;
case MODBUS_FC_WRITE_MULTIPLE: // 0x10
{
uint8_t byte_count = rx[6];
if (quantity >= 1 && quantity <= 123 &&
byte_count == quantity * 2) {
for (int i = 0; i < quantity; i++) {
if (addr + i < REG_HOLDING_NREGS) {
uint16_t value = (rx[7 + i*2] << 8) | rx[8 + i*2];
slave->holding_regs[addr + i] = value;
} else {
exception = MODBUS_EX_ILLEGAL_ADDRESS;
break;
}
}
if (exception == 0) {
// 响应:地址 + 寄存器数量
memcpy(tx, rx, 6);
slave->tx_length = 6;
}
} else {
exception = MODBUS_EX_ILLEGAL_VALUE;
}
break;
}
default:
exception = MODBUS_EX_ILLEGAL_FUNCTION;
break;
}
// 5. 异常响应
if (exception != 0) {
tx[0] = rx[0];
tx[1] = func | 0x80; // 功能码最高位置1
tx[2] = exception;
slave->tx_length = 3;
}
// 6. 添加CRC
if (slave->tx_length > 0) {
crc = Modbus_CRC16(tx, slave->tx_length);
tx[slave->tx_length] = crc & 0xFF;
tx[slave->tx_length + 1] = crc >> 8;
slave->tx_length += 2;
}
}
六、串口空闲中断处理
c
// stm32f1xx_it.c
#include "modbus.h"
extern Modbus_Slave_t mb_slave;
extern UART_HandleTypeDef huart1;
// 空闲中断回调
void Modbus_UART_Idle_Callback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
uint32_t temp;
// 清除空闲中断标志
__HAL_UART_CLEAR_IDLEFLAG(huart);
// 获取接收数据长度
temp = huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
mb_slave.rx_length = temp;
// 停止DMA,防止数据被覆盖
HAL_UART_DMAStop(huart);
// 处理Modbus数据
if (mb_slave.rx_length >= 4) { // 最小帧长度
Modbus_Process(&mb_slave);
// 发送响应
if (mb_slave.tx_length > 0) {
// 等待3.5个字符时间 (Modbus要求)
uint32_t delay = 3500000 / huart->Init.BaudRate;
HAL_Delay(delay);
// 发送响应
HAL_UART_Transmit_DMA(huart, mb_slave.tx_buf, mb_slave.tx_length);
}
}
// 重新启动DMA接收
HAL_UART_Receive_DMA(huart, mb_slave.rx_buf, MODBUS_RX_BUFFER_SIZE);
}
}
七、Modbus 主站(主机)实现
c
// modbus_master.c
#include "modbus.h"
typedef struct {
uint8_t tx_buf[MODBUS_TX_BUFFER_SIZE];
uint8_t rx_buf[MODBUS_RX_BUFFER_SIZE];
uint16_t rx_length;
uint8_t slave_addr;
uint32_t timeout;
} Modbus_Master_t;
// 读取保持寄存器
uint8_t Modbus_Read_Holding(Modbus_Master_t *master, uint8_t slave_addr,
uint16_t reg_addr, uint16_t reg_num,
uint16_t *data, uint32_t timeout)
{
// 构造请求
master->tx_buf[0] = slave_addr; // 地址
master->tx_buf[1] = 0x03; // 功能码
master->tx_buf[2] = reg_addr >> 8; // 起始地址高字节
master->tx_buf[3] = reg_addr & 0xFF; // 起始地址低字节
master->tx_buf[4] = reg_num >> 8; // 寄存器数量高字节
master->tx_buf[5] = reg_num & 0xFF; // 寄存器数量低字节
// CRC
uint16_t crc = Modbus_CRC16(master->tx_buf, 6);
master->tx_buf[6] = crc & 0xFF;
master->tx_buf[7] = crc >> 8;
// 发送请求
HAL_UART_Transmit_DMA(&huart1, master->tx_buf, 8);
// 等待响应
uint32_t start = HAL_GetTick();
while (HAL_GetTick() - start < timeout) {
if (master->rx_length >= 5 + reg_num * 2) {
// 校验响应
if (master->rx_buf[0] == slave_addr &&
master->rx_buf[1] == 0x03) {
// 提取数据
for (int i = 0; i < reg_num; i++) {
data[i] = (master->rx_buf[3 + i*2] << 8) |
master->rx_buf[4 + i*2];
}
return 1; // 成功
}
}
}
return 0; // 超时
}
八、主函数示例
c
// main.c
#include "main.h"
#include "modbus.h"
Modbus_Slave_t mb_slave;
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;
int main(void)
{
HAL_Init();
SystemClock_Config();
// 初始化外设
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
// 初始化Modbus从站
Modbus_Init(&mb_slave, 0x01);
// 启动DMA接收
HAL_UART_Receive_DMA(&huart1, mb_slave.rx_buf, MODBUS_RX_BUFFER_SIZE);
// 启用串口空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
while (1) {
// 主循环(空闲中断处理Modbus)
// 可以添加LED闪烁等指示
HAL_Delay(1000);
}
}
九、CubeMX 关键配置
1、USART 配置
c
// 波特率: 9600/19200/38400
// Word Length: 8 bits
// Parity: None
// Stop Bits: 1
// Over Sampling: 16 Samples
2、DMA 配置
USART1_RX:
Mode: Circular
Increment Address: Enable
Data Width: Byte
USART1_TX:
Mode: Normal
Increment Address: Enable
Data Width: Byte
3、NVIC 配置
USART1_IRQn: Enable
DMA1_Channel4_5_IRQn: Enable
参考代码 Modbus串口通讯程序 www.youwenfan.com/contentcsu/69985.html
十、测试与调试
1、Modbus Poll 测试
请求帧: 01 03 00 00 00 02 C4 0B
- 地址: 0x01
- 功能码: 0x03
- 起始地址: 0x0000
- 寄存器数量: 0x0002
响应帧: 01 03 04 00 00 00 00 FA 33
- 字节数: 0x04
- 数据: 0x0000 0x0000
2、调试函数
c
void Modbus_Debug_Print(uint8_t *data, uint16_t len)
{
printf("Modbus Frame (%d bytes): ", len);
for (int i = 0; i < len; i++) {
printf("%02X ", data[i]);
}
printf("\r\n");
}