51单片机(8051内核)是嵌入式开发中最经典的微控制器之一,广泛应用于工业控制、스마트 홈、数据采集等领域。在51单片机上实现Modbus RTU从站通讯,可以让单片机设备接入标准的工业自动化网络,与PLC、组态软件、上位机进行数据交互。本文提供完整的51单片机Modbus RTU从站实现代码,包括串口初始化、CRC16校验、协议解析、寄存器映射和主循环处理。
一、硬件环境与资源分配
1.1 推荐硬件配置
| 项目 | 规格 | 说明 |
|---|---|---|
| 单片机 | STC89C52RC / STC12C5A60S2 | 增强型51内核,速度更快 |
| 晶振 | 11.0592MHz | 串口波特率精准,Recommended for use |
| 시리얼 포트 | UART0(P3.0/P3.1) | 用于Modbus RTU通讯 |
| RS485芯片 | MAX485 / SP3485 | TTL转RS485,DE/RE接P3.2 |
| RAM需求 | ≥256字节 | 标准51内部128字节可能不够 |
| Flash需求 | ≥8KB | 代码约4-6KB |
1.2 寄存器映射表
| Modbus주소 | Type | 变量名 | 说明 |
|---|---|---|---|
| 0x0000-0x000F | 线圈(Coil) | coil_regs[16] | 16路继电器输出 |
| 0x0000-0x000F | 离散输入 | input_regs[16] | 16路开关量输入 |
| 0x0000-0x001F | 保持寄存器 | holding_regs[32] | 32个可读写参数 |
| 0x0000-0x000F | 输入寄存器 | analog_regs[16] | 16路ADC采集值 |
二、完整代码实现
2.1 头文件定义(modbus.h)
#ifndef __MODBUS_H
#define __MODBUS_H
#include <reg52.h>
/* 类型定义 */
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned long uint32;
/* Modbus配置 */
#define MODBUS_SLAVE_ADDR 0x01 // slave 주소
#define MODBUS_BAUDRATE 9600 // Baud rate
#define MODBUS_BUF_SIZE 64 // receive/发送缓冲区大小
/* 功能码定义 */
#define FC_READ_COILS 0x01 // 读线圈
#define FC_READ_INPUTS 0x02 // read discrete inputs
#define FC_READ_HOLDING 0x03 // read holding registers
#define FC_READ_ANALOG 0x04 // read input registers
#define FC_WRITE_COIL 0x05 // write single coil
#define FC_WRITE_REG 0x06 // write single register
#define FC_WRITE_COILS 0x0F // write multiple coils
#define FC_WRITE_REGS 0x10 // write multiple registers
/* Exception Code */
#define EX_ILLEGAL_FUNC 0x01 // 非法功能码
#define EX_ILLEGAL_ADDR 0x02 // 非法地址
#define EX_ILLEGAL_VALUE 0x03 // Illegal 데이터 value
#define EX_SLAVE_FAILURE 0x04 // Substation equipment malfunction
/* number of registers */
#define COIL_COUNT 16
#define INPUT_COUNT 16
#define HOLDING_COUNT 32
#define ANALOG_COUNT 16
/* 全局变量 */
extern uint8 coil_regs[COIL_COUNT]; // Coil status
extern uint8 input_regs[INPUT_COUNT]; // 离散输入
extern uint16 holding_regs[HOLDING_COUNT]; // 保持寄存器
extern uint16 analog_regs[ANALOG_COUNT]; // 输入寄存器(ADC)
/* 函数声明 */
void Modbus_Init(void);
void Modbus_Process(void);
uint16 CRC16(uint8 *buf, uint8 len);
#endif
2.2 Modbus主实现文件(modbus.c)
#include "modbus.h"
/* 全局变量定义 */
uint8 coil_regs[COIL_COUNT] = {0};
uint8 input_regs[INPUT_COUNT] = {0};
uint16 holding_regs[HOLDING_COUNT] = {0};
uint16 analog_regs[ANALOG_COUNT] = {0};
/* 接收缓冲区 */
static uint8 rx_buf[MODBUS_BUF_SIZE];
static uint8 rx_len = 0;
static uint8 rx_flag = 0; // 接收完成标志
/* 发送缓冲区 */
static uint8 tx_buf[MODBUS_BUF_SIZE];
/* RS485方向控制引脚 */
sbit RS485_DE = P3^2; // DE/RE控制,1=send,0=receive
/* ============================================================
* CRC16校验(Modbus标准多项式0xA001)
* ============================================================ */
uint16 CRC16(uint8 *buf, uint8 len)
{
uint16 crc = 0xFFFF;
uint8 i, j;
for (i = 0; i < len; i++) {
crc ^= buf[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}
/* ============================================================
* 串口初始化(9600bps,8N1,11.0592MHz晶振)
* ============================================================ */
void UART_Init(void)
{
TMOD &= 0x0F; // 清除定时器1模式位
TMOD |= 0x20; // 定时器1,模式2(8位自动重装)
TH1 = 0xFD; // 9600bps @ 11.0592MHz
TL1 = 0xFD;
TR1 = 1; // 启动定时器1
SCON = 0x50; // 串口模式1,8位UART,允许接收
ES = 1; // 使能串口中断
EA = 1; // 开总中断
RS485_DE = 0; // 默认接收模式
}
/* ============================================================
* 串口中断服务函数
* ============================================================ */
void UART_ISR(void) interrupt 4
{
static uint8 timer_cnt = 0;
if (RI) { // 接收中断
RI = 0;
if (rx_len < MODBUS_BUF_SIZE) {
rx_buf[rx_len++] = SBUF;
timer_cnt = 0; // 重置超时计数
}
}
if (TI) { // 发送中断
TI = 0;
}
}
/* ============================================================
* 定时器0中断:用于Modbus帧间隔检测(3.5字符时间)
* 9600bps时,1字符≈1ms,3.5字符≈3.5ms
* 定时器0每1ms中断一次
* ============================================================ */
void Timer0_Init(void)
{
TMOD &= 0xF0; // 清除定时器0模式
TMOD |= 0x01; // 定时器0,模式1(16位)
TH0 = 0xFC; // 1ms @ 11.0592MHz (65536-921=0xFC67)
TL0 = 0x67;
ET0 = 1; // 使能定时器0中断
TR0 = 1; // 启动定时器0
}
void Timer0_ISR(void) interrupt 1
{
static uint8 cnt = 0;
TH0 = 0xFC; // 重装初值
TL0 = 0x67;
if (rx_len > 0) {
cnt++;
if (cnt >= 4) { // 约4ms无新数据,认为帧结束
rx_flag = 1;
cnt = 0;
}
} else {
cnt = 0;
}
}
/* ============================================================
* 发送一帧数据(添加CRC)
* ============================================================ */
void Modbus_Send(uint8 len)
{
uint16 crc;
uint8 i;
crc = CRC16(tx_buf, len);
tx_buf[len++] = crc & 0xFF; // CRC低字节
tx_buf[len++] = (crc >> 8) & 0xFF; // CRChigh byte
RS485_DE = 1; // 切换到发送模式
for (i = 0; i < len; i++) {
SBUF = tx_buf[i];
while (!TI); // 等待发送完成
TI = 0;
}
// 等待最后一个字节发送完成
while (!TI);
TI = 0;
RS485_DE = 0; // 切换回接收模式
}
/* ============================================================
* 发送异常响应
* ============================================================ */
void Modbus_Exception(uint8 func, uint8 code)
{
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = func | 0x80; // 功能码最高位置1
tx_buf[2] = code;
Modbus_Send(3);
}
/* ============================================================
* 기능 코드01:读线圈
* ============================================================ */
void FC01_ReadCoils(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 byte_count, i, bit_pos;
// 检查地址范围
if (시작_addr + 수량 > COIL_COUNT) {
Modbus_Exception(FC_READ_COILS, EX_ILLEGAL_ADDR);
return;
}
byte_count = (수량 + 7) / 8;
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_READ_COILS;
tx_buf[2] = byte_count;
// 打包线圈状态(低位在前)
for (i = 0; i < byte_count; i++) {
tx_buf[3 + i] = 0;
}
for (i = 0; i < 수량; i++) {
bit_pos = 시작_addr + i;
if (coil_regs[bit_pos]) {
tx_buf[3 + i/8] |= (1 << (i % 8));
}
}
Modbus_Send(3 + byte_count);
}
/* ============================================================
* 기능 코드02:read discrete inputs
* ============================================================ */
void FC02_ReadInputs(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 byte_count, i, bit_pos;
if (시작_addr + 수량 > INPUT_COUNT) {
Modbus_Exception(FC_READ_INPUTS, EX_ILLEGAL_ADDR);
return;
}
byte_count = (수량 + 7) / 8;
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_READ_INPUTS;
tx_buf[2] = byte_count;
for (i = 0; i < byte_count; i++) {
tx_buf[3 + i] = 0;
}
for (i = 0; i < 수량; i++) {
bit_pos = 시작_addr + i;
if (input_regs[bit_pos]) {
tx_buf[3 + i/8] |= (1 << (i % 8));
}
}
Modbus_Send(3 + byte_count);
}
/* ============================================================
* 기능 코드03:read holding registers
* ============================================================ */
void FC03_ReadHolding(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 i;
if (시작_addr + 수량 > HOLDING_COUNT) {
Modbus_Exception(FC_READ_HOLDING, EX_ILLEGAL_ADDR);
return;
}
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_READ_HOLDING;
tx_buf[2] = 수량 * 2;
for (i = 0; i < 수량; i++) {
tx_buf[3 + i*2] = (holding_regs[시작_addr + i] >> 8) & 0xFF;
tx_buf[3 + i*2 + 1] = holding_regs[시작_addr + i] & 0xFF;
}
Modbus_Send(3 + 수량 * 2);
}
/* ============================================================
* 기능 코드04:read input registers(ADC值)
* ============================================================ */
void FC04_ReadAnalog(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 i;
if (시작_addr + 수량 > ANALOG_COUNT) {
Modbus_Exception(FC_READ_ANALOG, EX_ILLEGAL_ADDR);
return;
}
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_READ_ANALOG;
tx_buf[2] = 수량 * 2;
for (i = 0; i < 수량; i++) {
tx_buf[3 + i*2] = (analog_regs[시작_addr + i] >> 8) & 0xFF;
tx_buf[3 + i*2 + 1] = analog_regs[시작_addr + i] & 0xFF;
}
Modbus_Send(3 + 수량 * 2);
}
/* ============================================================
* 기능 코드05:write single coil
* ============================================================ */
void FC05_WriteCoil(uint8 *req)
{
uint16 addr = (req[2] << 8) | req[3];
uint16 value = (req[4] << 8) | req[5];
if (addr >= COIL_COUNT) {
Modbus_Exception(FC_WRITE_COIL, EX_ILLEGAL_ADDR);
return;
}
// 0xFF00=吸合,0x0000=断开
if (value == 0xFF00) {
coil_regs[addr] = 1;
} else if (value == 0x0000) {
coil_regs[addr] = 0;
} else {
Modbus_Exception(FC_WRITE_COIL, EX_ILLEGAL_VALUE);
return;
}
// 回显请求(原样返回)
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_WRITE_COIL;
tx_buf[2] = req[2];
tx_buf[3] = req[3];
tx_buf[4] = req[4];
tx_buf[5] = req[5];
Modbus_Send(6);
}
/* ============================================================
* 기능 코드06:write single register
* ============================================================ */
void FC06_WriteReg(uint8 *req)
{
uint16 addr = (req[2] << 8) | req[3];
uint16 value = (req[4] << 8) | req[5];
if (addr >= HOLDING_COUNT) {
Modbus_Exception(FC_WRITE_REG, EX_ILLEGAL_ADDR);
return;
}
holding_regs[addr] = value;
// 回显请求
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_WRITE_REG;
tx_buf[2] = req[2];
tx_buf[3] = req[3];
tx_buf[4] = req[4];
tx_buf[5] = req[5];
Modbus_Send(6);
}
/* ============================================================
* 기능 코드0F:write multiple coils
* ============================================================ */
void FC0F_WriteCoils(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 byte_count = req[6];
uint8 i;
if (시작_addr + 수량 > COIL_COUNT) {
Modbus_Exception(FC_WRITE_COILS, EX_ILLEGAL_ADDR);
return;
}
// 解析线圈状态
for (i = 0; i < 수량; i++) {
coil_regs[시작_addr + i] = (req[7 + i/8] >> (i % 8)) & 0x01;
}
// Response:주소+Quantity
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_WRITE_COILS;
tx_buf[2] = req[2];
tx_buf[3] = req[3];
tx_buf[4] = req[4];
tx_buf[5] = req[5];
Modbus_Send(6);
}
/* ============================================================
* 기능 코드10:write multiple registers
* ============================================================ */
void FC10_WriteRegs(uint8 *req)
{
uint16 시작_addr = (req[2] << 8) | req[3];
uint16 수량 = (req[4] << 8) | req[5];
uint8 i;
if (시작_addr + 수량 > HOLDING_COUNT) {
Modbus_Exception(FC_WRITE_REGS, EX_ILLEGAL_ADDR);
return;
}
// 解析寄存器值(高字节在前)
for (i = 0; i < 수량; i++) {
holding_regs[시작_addr + i] = (req[7 + i*2] << 8) | req[7 + i*2 + 1];
}
// Response
tx_buf[0] = MODBUS_SLAVE_ADDR;
tx_buf[1] = FC_WRITE_REGS;
tx_buf[2] = req[2];
tx_buf[3] = req[3];
tx_buf[4] = req[4];
tx_buf[5] = req[5];
Modbus_Send(6);
}
/* ============================================================
* Modbus初始化
* ============================================================ */
void Modbus_Init(void)
{
UART_Init();
Timer0_Init();
rx_len = 0;
rx_flag = 0;
}
/* ============================================================
* Modbus主处理函数(在主循环中调用)
* ============================================================ */
void Modbus_Process(void)
{
uint16 crc_recv, crc_calc;
if (!rx_flag) return;
// 禁用中断,防止处理过程中接收新数据
ES = 0;
// 1. 检查帧长度(最小4字节:주소+기능 코드+CRC*2)
if (rx_len < 4) {
goto reset;
}
// 2. 检查从站地址
if (rx_buf[0] != MODBUS_SLAVE_ADDR) {
goto reset; // 不是发给我的,忽略
}
// 3. CRC check
crc_recv = (rx_buf[rx_len-1] << 8) | rx_buf[rx_len-2];
crc_calc = CRC16(rx_buf, rx_len - 2);
if (crc_recv != crc_calc) {
goto reset; // CRC오답,丢弃
}
// 4. 根据功能码分发处理
switch (rx_buf[1]) {
case FC_READ_COILS:
FC01_ReadCoils(rx_buf);
break;
case FC_READ_INPUTS:
FC02_ReadInputs(rx_buf);
break;
case FC_READ_HOLDING:
FC03_ReadHolding(rx_buf);
break;
case FC_READ_ANALOG:
FC04_ReadAnalog(rx_buf);
break;
case FC_WRITE_COIL:
FC05_WriteCoil(rx_buf);
break;
case FC_WRITE_REG:
FC06_WriteReg(rx_buf);
break;
case FC_WRITE_COILS:
FC0F_WriteCoils(rx_buf);
break;
case FC_WRITE_REGS:
FC10_WriteRegs(rx_buf);
break;
default:
Modbus_Exception(rx_buf[1], EX_ILLEGAL_FUNC);
break;
}
reset:
rx_len = 0;
rx_flag = 0;
ES = 1; // 重新使能串口中断
}
2.3 主程序(main.c)
#include "modbus.h"
/* 继电器输出引脚映射 */
sbit RELAY1 = P1^0;
sbit RELAY2 = P1^1;
sbit RELAY3 = P1^2;
sbit RELAY4 = P1^3;
/* 开关量输入引脚映射 */
sbit INPUT1 = P2^0;
sbit INPUT2 = P2^1;
sbit INPUT3 = P2^2;
sbit INPUT4 = P2^3;
/* 延时函数 */
void Delay_ms(uint16 ms)
{
uint16 i, j;
for (i = 0; i < ms; i++)
for (j = 0; j < 114; j++);
}
/* ADC采集(需根据实际ADC芯片实现,此处为模拟) */
void ADC_Scan(void)
{
// 模拟ADC值,实际项目中替换为真实ADCread
analog_regs[0] = 512; // 通道0
analog_regs[1] = 1024; // 通道1
analog_regs[2] = 2048; // 通道2
analog_regs[3] = 4095; // 通道3
}
/* 更新输入状态 */
void Input_Scan(void)
{
input_regs[0] = INPUT1 ? 1 : 0;
input_regs[1] = INPUT2 ? 1 : 0;
input_regs[2] = INPUT3 ? 1 : 0;
input_regs[3] = INPUT4 ? 1 : 0;
}
/* 更新继电器输出 */
void Output_Update(void)
{
RELAY1 = coil_regs[0] ? 1 : 0;
RELAY2 = coil_regs[1] ? 1 : 0;
RELAY3 = coil_regs[2] ? 1 : 0;
RELAY4 = coil_regs[3] ? 1 : 0;
}
void main(void)
{
// 初始化
Modbus_Init();
// 设置保持寄存器初始值
holding_regs[0] = 100; // 파라미터1
holding_regs[1] = 200; // 파라미터2
holding_regs[2] = 9600; // 波特率参数
while (1) {
Modbus_Process(); // 处理ModbusRequest
Input_Scan(); // 扫描输入
Output_Update(); // 更新输出
// 每100ms采集一次ADC
static uint16 tick = 0;
if (++tick >= 100) {
tick = 0;
ADC_Scan();
}
Delay_ms(1);
}
}
三、关键技术说明
3.1 帧间隔检测
Modbus RTU协议规定,两帧之间必须有至少3.5个字符时间的静默间隔。在9600bps下,一个字符(含起始位、데이터 bit、stop bit)约1ms,因此3.5字符约3.5ms。本实现使用定时器0每1ms中断一次,连续4ms无新数据则认为一帧接收完成。
3.2 CRC16校验
Modbus RTU使用CRC16校验,多项式为0xA001(即0x8005的反转)。计算时初始值为0xFFFF,每字节异或后右移8次,最低位为1时异或0xA001。发送时CRC低字节在前,高字节在后。
3.3 RS485方向控制
MAX485芯片的DE和RE引脚短接,由单片机IO控制:高电平为发送模式,低电平为接收模式。发送完成后必须及时切换回接收模式,否则无法接收下一帧数据。
3.4 线圈状态打包
读线圈和读离散输入的响应中,多个状态按位打包到字节中,最低位对应第一个线圈。例如读取8个线圈,状态为[1,0,1,0,0,0,0,0],打包为0x05。
四、测试与验证
4.1 使用Modbus Poll测试
- 连接方式:USB转RS485转换器,A接A,B接B
- Serial port 파라미터s:9600,8数据位,无校验,1停止位
- slave 주소:1
- read holding registers:주소0,Quantity10,应返回[100, 200, 9600, 0, 0...]
- write single coil:주소0写ON,对应RELAY1应吸合
- read input registers:주소0,Quantity4,应返回[512, 1024, 2048, 4095]
4.2 常见问题排查
- 无响应:检查RS485接线(A/B是否接反)、slave 주소、波特率是否一致
- CRC오답:确认晶振为11.0592MHz,检查波特率计算是否正确
- 偶尔丢包:增加帧间隔时间,检查RS485总线是否加120Ω终端电阻
- 只能读不能写:检查写操作的地址范围,确认寄存器是否可写
- 数据错乱:检查大端小端格式,Modbus标准为高字节在前
五、扩展建议
- 增加看门狗:防止程序跑飞,提高工业现场可靠性
- 参数掉电保存:使用STC单片机的EEPROM保存保持寄存器值
- 多串口支持:STC12시리즈有2个串口,可同时支持Modbus和调试输出
- 增加广播功能:支持从站地址0的广播写操作
- 自定义功能码:实现厂商特定功能,如固件升级、校准等
- Modbus ASCII:如需ASCII模式,将CRC替换为LRC verification,帧格式改为冒号开头
本代码实现了完整的Modbus RTU从站功能,支持所有标准功能码,可直接用于51单片机项目开发。建议先在STC89C52RC开发板上验证通讯正常,再移植到实际产品中。代码结构清晰,寄存器映射表可根据实际需求修改。
Leave a Reply