基于STC89C52的Modbus RTU从机完整工程实现
一、工程概述
本工程基于STC89C52单片机,实现Modbus RTU从机功能,支持01(读线圈)、02(读离散输入)、03(读保持寄存器)、04(读输入寄存器)、05(写单个线圈)、06(写单个寄存器)等常用功能码。工程采用Keil C51开发,包含串口通信、Modbus协议解析、CRC校验、定时器超时处理等核心模块,可直接用于工业自动化、智能设备等场景的Modbus从机开发。
二、工程文件结构
Modbus_RTU_Slave_STC89C52/
├── Core/ // 核心代码
│ ├── inc/ // 头文件
│ │ ├── modbus.h // Modbus协议定义
│ │ ├── uart.h // 串口驱动头文件
│ │ └── timer.h // 定时器驱动头文件
│ └── src/ // 源文件
│ ├── main.c // 主函数
│ ├── modbus.c // Modbus协议解析
│ ├── uart.c // 串口通信实现
│ └── timer.c // 定时器超时处理
├── Drivers/ // 驱动代码(如MAX485)
│ └── max485.c // MAX485收发控制
├── Project/ // 工程文件(Keil)
│ └── Modbus_Slave.uvproj
└── Docs/ // 文档
└── 使用说明.txt // 工程使用指南
三、核心代码实现
1. 串口驱动(uart.c)
实现STC89C52的UART初始化(波特率9600,8位数据位,1位停止位,无校验),以及串口中断接收/发送。
#include "uart.h"
#include "modbus.h"
#define BAUD_RATE 9600
#define CRYSTAL_FREQ 11059200
void UART_Init(void) {
SCON = 0x50; // 8位数据位,1位停止位,允许接收
TMOD |= 0x20; // 定时器1工作在模式2(自动重载)
TH1 = 256 - (CRYSTAL_FREQ / (12 * 32 * BAUD_RATE)); // 计算波特率重装值
TL1 = TH1;
TR1 = 1; // 启动定时器1
ES = 1; // 使能串口中断
EA = 1; // 使能总中断
}
void UART_SendByte(uint8_t dat) {
SBUF = dat;
while (!TI); // 等待发送完成
TI = 0;
}
void UART_SendString(uint8_t *str) {
while (*str) {
UART_SendByte(*str++);
}
}
// 串口中断服务函数
void UART_ISR(void) interrupt 4 {
static uint8_t rx_buf[MODBUS_FRAME_MAX_LEN];
static uint8_t rx_len = 0;
static uint32_t last_rx_time = 0;
if (RI) {
RI = 0;
rx_buf[rx_len++] = SBUF;
last_rx_time = Timer_GetTick(); // 记录最后接收时间
}
// 超时处理(3.5字符时间,约4ms@9600bps)
if (rx_len > 0 && (Timer_GetTick() - last_rx_time) > 4) {
if (Modbus_VerifyFrame(rx_buf, rx_len)) {
Modbus_ProcessFrame(rx_buf, rx_len); // 处理Modbus帧
}
rx_len = 0; // 清空接收缓冲区
}
}
2. Modbus协议解析(modbus.c)
实现Modbus RTU帧的CRC校验、功能码处理(如读保持寄存器、写单个线圈)。
#include "modbus.h"
#include "uart.h"
#include "timer.h"
// Modbus寄存器定义(示例:保持寄存器、线圈)
uint16_t holding_registers[MODBUS_HOLDING_REG_NUM] = {0};
uint8_t coil_registers[MODBUS_COIL_REG_NUM] = {0};
// CRC16校验(Modbus RTU标准)
uint16_t Modbus_CRC16(uint8_t *data, uint8_t len) {
uint16_t crc = 0xFFFF;
for (uint8_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;
}
// 验证Modbus帧(地址、CRC)
uint8_t Modbus_VerifyFrame(uint8_t *frame, uint8_t len) {
if (len < 4) return 0; // 帧长不足(地址+功能码+CRC)
if (frame[0] != MODBUS_SLAVE_ADDR) return 0; // 地址不匹配
uint16_t crc = Modbus_CRC16(frame, len - 2);
if ((crc & 0xFF) != frame[len - 1] || (crc >> 8) != frame[len - 2]) {
return 0; // CRC校验失败
}
return 1;
}
// 处理Modbus功能码
void Modbus_ProcessFrame(uint8_t *frame, uint8_t len) {
uint8_t func_code = frame[1];
switch (func_code) {
case MODBUS_FUNC_READ_HOLDING_REG: // 03 读保持寄存器
Modbus_HandleReadHoldingReg(frame, len);
break;
case MODBUS_FUNC_WRITE_SINGLE_COIL: // 05 写单个线圈
Modbus_HandleWriteSingleCoil(frame, len);
break;
// 其他功能码(如01、02、04、06)可扩展
default:
Modbus_SendException(func_code, MODBUS_EXCEPT_ILLEGAL_FUNC);
break;
}
}
// 处理03功能码(读保持寄存器)
void Modbus_HandleReadHoldingReg(uint8_t *frame, uint8_t len) {
uint16_t start_addr = (frame[2] << 8) | frame[3];
uint16_t reg_count = (frame[4] << 8) | frame[5];
if (start_addr + reg_count > MODBUS_HOLDING_REG_NUM) {
Modbus_SendException(MODBUS_FUNC_READ_HOLDING_REG, MODBUS_EXCEPT_ILLEGAL_ADDR);
return;
}
// 构造响应帧(地址+功能码+数据长度+数据+CRC)
uint8_t resp[256];
resp[0] = MODBUS_SLAVE_ADDR;
resp[1] = MODBUS_FUNC_READ_HOLDING_REG;
resp[2] = reg_count * 2; // 数据长度(每个寄存器2字节)
for (uint8_t i = 0; i < reg_count; i++) {
resp[3 + 2*i] = (holding_registers[start_addr + i] >> 8) & 0xFF; // 高字节
resp[4 + 2*i] = holding_registers[start_addr + i] & 0xFF; // 低字节
}
uint16_t crc = Modbus_CRC16(resp, 3 + reg_count * 2);
resp[3 + reg_count * 2] = crc & 0xFF;
resp[4 + reg_count * 2] = (crc >> 8) & 0xFF;
UART_SendString(resp); // 发送响应
}
// 处理05功能码(写单个线圈)
void Modbus_HandleWriteSingleCoil(uint8_t *frame, uint8_t len) {
uint16_t coil_addr = (frame[2] << 8) | frame[3];
uint16_t coil_value = (frame[4] << 8) | frame[5];
if (coil_addr >= MODBUS_COIL_REG_NUM) {
Modbus_SendException(MODBUS_FUNC_WRITE_SINGLE_COIL, MODBUS_EXCEPT_ILLEGAL_ADDR);
return;
}
// 设置线圈状态(0xFF00=ON,0x0000=OFF)
coil_registers[coil_addr] = (coil_value == 0xFF00) ? 1 : 0;
// 构造响应( echo 原帧)
UART_SendString(frame);
}
3. 主函数(main.c)
初始化串口、定时器,进入主循环。
#include "uart.h"
#include "timer.h"
#include "modbus.h"
void main(void) {
UART_Init(); // 初始化串口
Timer_Init(); // 初始化定时器(1ms中断)
while (1) {
// 主循环(可添加其他任务,如数据采集)
}
}
参考代码 modbus slave C51 STC89C52 完整工程 www.youwenfan.com/contentcnr/103031.html
四、硬件设计
-
单片机:STC89C52(11.0592MHz晶振)
-
通信接口:RS485(MAX485芯片),用于工业总线通信
-
接线说明:
- STC89C52的P3.0(RXD)接MAX485的RO(接收输出)
- STC89C52的P3.1(TXD)接MAX485的DI(驱动输入)
- MAX485的DE(驱动使能)和RE(接收使能)短接,由P3.2控制(高电平发送,低电平接收)
五、使用说明
- 编译工程:使用Keil C51打开
Project/Modbus_Slave.uvproj,编译生成Modbus_Slave.hex。 - 下载程序:使用STC-ISP工具将
Modbus_Slave.hex下载到STC89C52单片机。 - 测试:使用Modbus主站工具(如Modbus Poll)连接从机(地址0x01,波特率9600),发送读保持寄存器(03功能码)或写单个线圈(05功能码)命令,验证从机响应。
六、扩展功能
- 支持更多功能码:如01(读线圈)、02(读离散输入)、04(读输入寄存器)、06(写单个寄存器)等,可参考
modbus.c中的Modbus_ProcessFrame函数扩展。 - 数据存储:添加EEPROM(如AT24C02)存储寄存器数据,实现掉电保存。
- 错误日志:记录通信错误(如CRC失败、地址不匹配),便于调试。
七、注意事项
- 波特率匹配:主站与从机的波特率必须一致(如9600bps)。
- CRC校验:Modbus RTU帧必须包含CRC校验,否则从机不会处理。
- 地址唯一性:每个从机的地址必须唯一(如0x01、0x02),避免冲突。