一、系统架构与硬件连接
1.1 硬件连接表
| STM32F103C8T6 | RS485模块 | 说明 |
|---|---|---|
| PA9 (TX1) | DI | 发送数据 |
| PA10 (RX1) | RO | 接收数据 |
| PA8 | DE/RE | 收发控制(高电平发送,低电平接收) |
| 3.3V | VCC | 电源 |
| GND | GND | 共地 |
1.2 MODBUS RTU协议帧格式
[从机地址][功能码][数据起始地址][数据数量/数据值][CRC16校验]
- 地址:1字节(1-247)
- 功能码:1字节(03=读保持寄存器,06=写单个寄存器,16=写多个寄存器)
- 数据:N字节
- CRC16:2字节(低位在前)
二、代码实现
2.1 头文件(modbus_rtu.h)
c
#ifndef __MODBUS_RTU_H
#define __MODBUS_RTU_H
#include "stm32f10x.h"
// MODBUS配置
#define MODBUS_SLAVE_ADDR 0x01 // 从机地址
#define MODBUS_BAUDRATE 9600 // 波特率
#define MODBUS_REG_NUM 10 // 寄存器数量
// 功能码定义
#define FUNC_READ_REG 0x03 // 读保持寄存器
#define FUNC_WRITE_SINGLE 0x06 // 写单个寄存器
#define FUNC_WRITE_MULTI 0x10 // 写多个寄存器
// 错误码定义
#define ERR_NONE 0x00 // 无错误
#define ERR_FUNC_CODE 0x01 // 非法功能码
#define ERR_REG_ADDR 0x02 // 非法数据地址
#define ERR_REG_VALUE 0x03 // 非法数据值
// 全局变量
extern uint16_t modbus_regs[MODBUS_REG_NUM]; // MODBUS寄存器数组
extern uint8_t modbus_rx_buf[256]; // 接收缓冲区
extern uint8_t modbus_tx_buf[256]; // 发送缓冲区
extern uint8_t modbus_rx_len; // 接收长度
extern uint8_t modbus_frame_flag; // 帧接收完成标志
// 函数声明
void MODBUS_Init(void);
void MODBUS_ProcessFrame(void);
void MODBUS_SendResponse(uint8_t *data, uint8_t len);
uint16_t CRC16_Calculate(uint8_t *data, uint16_t len);
void MODBUS_UpdateDisplay(void);
#endif /* __MODBUS_RTU_H */
2.2 主程序(main.c)
c
#include "stm32f10x.h"
#include "modbus_rtu.h"
#include "usart.h"
#include "delay.h"
#include "lcd.h"
// MODBUS寄存器数据
uint16_t modbus_regs[MODBUS_REG_NUM] = {
0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1111,
0x2222, 0x3333, 0x4444, 0x5555, 0x6666
};
// 通信缓冲区
uint8_t modbus_rx_buf[256];
uint8_t modbus_tx_buf[256];
uint8_t modbus_rx_len = 0;
uint8_t modbus_frame_flag = 0;
// 系统状态显示
typedef struct {
uint32_t rx_count; // 接收帧计数
uint32_t tx_count; // 发送帧计数
uint16_t last_reg_addr; // 最后操作的寄存器地址
uint16_t last_reg_value; // 最后操作的寄存器值
uint8_t last_func_code; // 最后功能码
} SystemStatus;
SystemStatus sys_status = {0};
int main(void)
{
// 系统初始化
SystemInit();
Delay_Init();
USART1_Init(9600); // MODBUS通信串口
USART2_Init(115200); // 调试输出串口
LCD_Init(); // LCD显示屏
printf("STM32 MODBUS RTU Slave Starting...\r\n");
LCD_ShowString(0, 0, "MODBUS RTU Slave");
LCD_ShowString(0, 20, "Addr: 0x01 Baud: 9600");
// MODBUS初始化
MODBUS_Init();
uint32_t last_display_update = 0;
while(1)
{
// 处理MODBUS帧
if(modbus_frame_flag)
{
MODBUS_ProcessFrame();
modbus_frame_flag = 0;
modbus_rx_len = 0;
}
// 每秒更新显示
if(millis() - last_display_update > 1000)
{
MODBUS_UpdateDisplay();
last_display_update = millis();
}
Delay_ms(10);
}
}
2.3 MODBUS核心实现(modbus_rtu.c)
c
#include "modbus_rtu.h"
#include "string.h"
// GPIO控制RS485收发
#define RS485_TX_EN() GPIO_SetBits(GPIOA, GPIO_Pin_8) // PA8高电平,发送模式
#define RS485_RX_EN() GPIO_ResetBits(GPIOA, GPIO_Pin_8) // PA8低电平,接收模式
/**
* @brief MODBUS初始化
*/
void MODBUS_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
// 2. 配置RS485收发控制引脚(PA8)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
RS485_RX_EN(); // 默认接收模式
// 3. 配置USART1引脚(PA9-TX, PA10-RX)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4. USART参数配置
USART_InitStructure.USART_BaudRate = MODBUS_BAUDRATE;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
// 5. 使能接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 6. NVIC配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 7. 使能USART
USART_Cmd(USART1, ENABLE);
printf("MODBUS RTU Initialized: Addr=0x%02X, Baud=%d\r\n",
MODBUS_SLAVE_ADDR, MODBUS_BAUDRATE);
}
/**
* @brief CRC16计算
* @param data: 数据指针
* @param len: 数据长度
* @retval CRC16校验值
*/
uint16_t CRC16_Calculate(uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
uint16_t i, j;
for(i = 0; i < len; i++)
{
crc ^= data[i];
for(j = 0; j < 8; j++)
{
if(crc & 0x0001)
{
crc >>= 1;
crc ^= 0xA001; // MODBUS CRC多项式
}
else
{
crc >>= 1;
}
}
}
return crc;
}
/**
* @brief 发送MODBUS响应
* @param data: 发送数据指针
* @param len: 数据长度
*/
void MODBUS_SendResponse(uint8_t *data, uint8_t len)
{
uint16_t crc;
// 切换到发送模式
RS485_TX_EN();
Delay_us(10);
// 发送数据
for(uint8_t i = 0; i < len; i++)
{
USART_SendData(USART1, data[i]);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
// 等待发送完成
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
// 切换回接收模式
Delay_us(10);
RS485_RX_EN();
sys_status.tx_count++;
}
/**
* @brief 处理MODBUS帧
*/
void MODBUS_ProcessFrame(void)
{
uint8_t slave_addr = modbus_rx_buf[0];
uint8_t func_code = modbus_rx_buf[1];
uint16_t crc_received, crc_calculated;
// 检查从机地址
if(slave_addr != MODBUS_SLAVE_ADDR && slave_addr != 0x00)
{
return; // 不是发给本机的帧
}
// 检查CRC
crc_received = (modbus_rx_buf[modbus_rx_len-1] << 8) | modbus_rx_buf[modbus_rx_len-2];
crc_calculated = CRC16_Calculate(modbus_rx_buf, modbus_rx_len-2);
if(crc_received != crc_calculated)
{
printf("CRC Error: Received=0x%04X, Calculated=0x%04X\r\n",
crc_received, crc_calculated);
return;
}
sys_status.last_func_code = func_code;
sys_status.rx_count++;
printf("RX Frame: ");
for(uint8_t i = 0; i < modbus_rx_len; i++)
{
printf("%02X ", modbus_rx_buf[i]);
}
printf("\r\n");
// 处理不同功能码
switch(func_code)
{
case FUNC_READ_REG: // 读保持寄存器
Handle_ReadRegisters();
break;
case FUNC_WRITE_SINGLE: // 写单个寄存器
Handle_WriteSingleRegister();
break;
case FUNC_WRITE_MULTI: // 写多个寄存器
Handle_WriteMultipleRegisters();
break;
default: // 非法功能码
Send_ExceptionResponse(ERR_FUNC_CODE);
break;
}
}
/**
* @brief 处理读寄存器请求
*/
void Handle_ReadRegisters(void)
{
uint16_t start_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t reg_count = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
uint8_t resp_len = 0;
uint16_t crc;
// 检查地址合法性
if(start_addr >= MODBUS_REG_NUM ||
start_addr + reg_count > MODBUS_REG_NUM)
{
Send_ExceptionResponse(ERR_REG_ADDR);
return;
}
// 构建响应帧
modbus_tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
modbus_tx_buf[resp_len++] = FUNC_READ_REG;
modbus_tx_buf[resp_len++] = reg_count * 2; // 字节数
// 添加寄存器数据
for(uint16_t i = 0; i < reg_count; i++)
{
modbus_tx_buf[resp_len++] = (modbus_regs[start_addr + i] >> 8) & 0xFF;
modbus_tx_buf[resp_len++] = modbus_regs[start_addr + i] & 0xFF;
}
// 计算CRC
crc = CRC16_Calculate(modbus_tx_buf, resp_len);
modbus_tx_buf[resp_len++] = crc & 0xFF;
modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
// 发送响应
MODBUS_SendResponse(modbus_tx_buf, resp_len);
sys_status.last_reg_addr = start_addr;
sys_status.last_reg_value = modbus_regs[start_addr];
printf("Read Registers: Addr=%d, Count=%d\r\n", start_addr, reg_count);
}
/**
* @brief 处理写单个寄存器请求
*/
void Handle_WriteSingleRegister(void)
{
uint16_t reg_addr = (modbus_rx_buf[2] << 8) | modbus_rx_buf[3];
uint16_t reg_value = (modbus_rx_buf[4] << 8) | modbus_rx_buf[5];
uint8_t resp_len = 0;
uint16_t crc;
// 检查地址合法性
if(reg_addr >= MODBUS_REG_NUM)
{
Send_ExceptionResponse(ERR_REG_ADDR);
return;
}
// 写入寄存器
modbus_regs[reg_addr] = reg_value;
// 回显相同的数据作为响应
memcpy(modbus_tx_buf, modbus_rx_buf, 6);
resp_len = 6;
// 计算CRC
crc = CRC16_Calculate(modbus_tx_buf, resp_len);
modbus_tx_buf[resp_len++] = crc & 0xFF;
modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
// 发送响应
MODBUS_SendResponse(modbus_tx_buf, resp_len);
sys_status.last_reg_addr = reg_addr;
sys_status.last_reg_value = reg_value;
printf("Write Register: Addr=%d, Value=0x%04X\r\n", reg_addr, reg_value);
}
/**
* @brief 发送异常响应
*/
void Send_ExceptionResponse(uint8_t error_code)
{
uint8_t resp_len = 0;
uint16_t crc;
modbus_tx_buf[resp_len++] = MODBUS_SLAVE_ADDR;
modbus_tx_buf[resp_len++] = 0x80 | modbus_rx_buf[1]; // 错误响应功能码
modbus_tx_buf[resp_len++] = error_code;
crc = CRC16_Calculate(modbus_tx_buf, resp_len);
modbus_tx_buf[resp_len++] = crc & 0xFF;
modbus_tx_buf[resp_len++] = (crc >> 8) & 0xFF;
MODBUS_SendResponse(modbus_tx_buf, resp_len);
printf("Exception Response: Error Code=%d\r\n", error_code);
}
/**
* @brief 更新显示
*/
void MODBUS_UpdateDisplay(void)
{
char display_buf[32];
LCD_ShowString(0, 40, "MODBUS Status:");
sprintf(display_buf, "RX: %lu TX: %lu", sys_status.rx_count, sys_status.tx_count);
LCD_ShowString(0, 60, display_buf);
sprintf(display_buf, "Func: 0x%02X", sys_status.last_func_code);
LCD_ShowString(0, 80, display_buf);
sprintf(display_buf, "Reg[%d]: 0x%04X",
sys_status.last_reg_addr, sys_status.last_reg_value);
LCD_ShowString(0, 100, display_buf);
// 显示部分寄存器值
sprintf(display_buf, "R0:%04X R1:%04X", modbus_regs[0], modbus_regs[1]);
LCD_ShowString(0, 120, display_buf);
}
2.4 串口中断处理(stm32f10x_it.c)
c
#include "stm32f10x_it.h"
#include "modbus_rtu.h"
extern uint8_t modbus_rx_buf[];
extern uint8_t modbus_rx_len;
extern uint8_t modbus_frame_flag;
// 帧超时检测(3.5个字符时间)
#define FRAME_TIMEOUT (3500000 / MODBUS_BAUDRATE) // 微秒
void USART1_IRQHandler(void)
{
static uint32_t last_rx_time = 0;
uint8_t data;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
data = USART_ReceiveData(USART1);
// 检查缓冲区是否溢出
if(modbus_rx_len < sizeof(modbus_rx_buf))
{
modbus_rx_buf[modbus_rx_len++] = data;
}
// 更新最后接收时间
last_rx_time = micros();
// 清除中断标志
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
// 在主循环中检查帧超时
if(modbus_rx_len > 0 && (micros() - last_rx_time) > FRAME_TIMEOUT)
{
modbus_frame_flag = 1;
}
}
2.5 调试串口输出(usart.c)
c
#include "usart.h"
#include <stdarg.h>
#include <stdio.h>
// 重定向printf到USART2
int fputc(int ch, FILE *f)
{
USART_SendData(USART2, (uint8_t)ch);
while(USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET);
return ch;
}
void USART2_Init(uint32_t baudrate)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
// PA2-TX, PA3-RX
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = baudrate;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &USART_InitStructure);
USART_Cmd(USART2, ENABLE);
}
三、测试工具与验证
3.1 PC端测试工具
-
Modbus Poll(Windows)
- 连接:选择COM口,波特率9600,RTU模式
- 功能码03:读取寄存器
- 功能码06:写入单个寄存器
-
QModMaster(跨平台)
- 开源MODBUS主站工具
- 支持RTU/TCP
3.2 测试步骤
-
连接硬件:STM32通过RS485模块连接到PC
-
启动从机:STM32运行MODBUS从机程序
-
发送测试帧:
读寄存器:01 03 00 00 00 01 84 0A (从机地址01,功能码03,起始地址0000,读取1个寄存器) -
观察响应:
响应帧:01 03 02 12 34 B5 33 (从机地址01,功能码03,字节数2,数据1234,CRC校验)
3.3 LCD显示效果
MODBUS RTU Slave
Addr: 0x01 Baud: 9600
MODBUS Status:
RX: 125 TX: 124
Func: 0x03
Reg[0]: 0x1234
R0:1234 R1:5678
四、常见问题解决
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 无响应 | 地址不匹配 | 检查从机地址设置 |
| CRC错误 | 波特率不一致 | 确认双方波特率相同 |
| 数据错乱 | 收发切换延迟不足 | 增加RS485收发切换延时 |
| 丢帧 | 中断处理不及时 | 优化中断服务程序 |
| 显示乱码 | LCD初始化问题 | 检查LCD引脚连接 |
参考代码 STM32+MODEBUS+485代码,串口发送,实时显示 www.youwenfan.com/contentcsu/56209.html
五、扩展功能建议
5.1 增加更多功能码
c
// 增加04功能码(读输入寄存器)
case 0x04:
Handle_ReadInputRegisters();
break;
// 增加05功能码(写单个线圈)
case 0x05:
Handle_WriteSingleCoil();
break;
5.2 数据记录与报警
c
// 监控寄存器变化
if(modbus_regs[0] > 0xFF00)
{
// 触发报警
GPIO_SetBits(GPIOB, GPIO_Pin_0); // 蜂鸣器报警
printf("Alarm Triggered! Value=0x%04X\r\n", modbus_regs[0]);
}
5.3 多从机支持
c
// 支持广播地址0x00
if(slave_addr == MODBUS_SLAVE_ADDR || slave_addr == 0x00)
{
// 处理广播命令
Process_BroadcastCommand();
}
六、总结
这套STM32+MODBUS+RS485方案具有以下特点:
完整协议实现 :支持03/06/16功能码
实时数据显示 :LCD实时显示通信状态
稳定可靠 :CRC校验、超时检测、错误处理
易于扩展:模块化设计,方便添加新功能
应用场景:
- 工业自动化控制系统
- 智能仪表数据采集
- 楼宇自动化系统
- 远程监控系统
通过LCD实时显示MODBUS通信状态,可以直观地监控系统运行情况,便于调试和维护。