51单片机是一种经典的8位嵌入式微控制器,因其价格低廉、开发简单而被广泛应用于工业控制、스마트 홈、仪器仪表等领域。RS485总线是工业现场最常用的串行通讯标准之一,而Modbus RTU协议则是RS485总线上最主流的应用层协议。本文提供一套完整的51单片机实现Modbus RTU从站通讯的C语言源码,支持线圈读写、寄存器读写等标准功能码。
一、系统架构
本方案以STC89C52RC(或兼容的AT89S52)为主控芯片,通过MAX485芯片实现RS485电平转换。51单片机作为Modbusslave,响应主站(PLC、触摸屏、上位机等)的读写请求。
1.1 硬件连接
| 51单片机引脚 | MAX485引脚 | 说明 |
|---|---|---|
| P3.0 (RXD) | RO (1脚) | 接收数据 |
| P3.1 (TXD) | DI (4脚) | 发送数据 |
| P2.0 (或其他IO) | DE/RE (2,3脚) | 收发控制:高=send,低=receive |
| VCC (5V) | VCC (8脚) | 电源 |
| GND | GND (5脚) | 地 |
| - | A (6脚) | RS485 A线 |
| - | B (7脚) | RS485 B线 |
1.2 资源分配
- 시리얼 포트:使用UART,模式1(8位数据,可变波特率)
- 定时器:T1作为波特率发生器,9600bps @ 11.0592MHz晶振
- 中断:串口中断接收数据,定时器中断处理超时
- 内存:线圈状态使用位寻址区,寄存器使用内部RAM
二、Modbus RTU协议帧格式
Modbus RTU帧由以下字段组成:
| field | Length | 说明 |
|---|---|---|
| slave 주소 | 1字节 | 1-247,0为广播地址 |
| 기능 코드 | 1字节 | 01/02/03/05/06/15/16等 |
| 데이터 | Nbyte | 주소、Quantity、数据值等 |
| CRC check | 2字节 | 低字节在前,高字节在后 |
三、completeC语言源码
3.1 头文件定义(modbus.h)
#ifndef __MODBUS_H
#define __MODBUS_H
#include <reg52.h>
// 基本类型定义
typedef unsigned char uint8;
typedef unsigned int uint16;
typedef unsigned long uint32;
// IO定义
sbit RS485_DIR = P2^0; // RS485收发控制
// Modbus参数配置
#define MODBUS_ADDR 1 // slave 주소
#define MODBUS_BAUD 9600 // Baud rate
#define RX_BUF_SIZE 64 // 接收缓冲区大小
#define TX_BUF_SIZE 64 // 发送缓冲区大小
#define TIMEOUT_MS 50 // 帧超时时间(ms)
// 功能码定义
#define FUNC_READ_COIL 0x01 // 读线圈
#define FUNC_READ_INPUT 0x02 // read discrete inputs
#define FUNC_READ_REG 0x03 // read holding registers
#define FUNC_READ_INPUT_REG 0x04 // read input registers
#define FUNC_WRITE_COIL 0x05 // 写单线圈
#define FUNC_WRITE_REG 0x06 // 写单寄存器
#define FUNC_WRITE_COILS 0x0F // 写多线圈
#define FUNC_WRITE_REGS 0x10 // 写多寄存器
// Exception Code
#define EXCEPT_ILLEGAL_FUNC 0x01
#define EXCEPT_ILLEGAL_ADDR 0x02
#define EXCEPT_ILLEGAL_VAL 0x03
#define EXCEPT_SLAVE_FAIL 0x04
// 全局变量
extern uint8 rx_buf[RX_BUF_SIZE];
extern uint8 tx_buf[TX_BUF_SIZE];
extern uint8 rx_len;
extern uint8 tx_len;
extern bit rx_done;
extern bit tx_done;
// 函数声明
void modbus_init(void);
void modbus_task(void);
uint16 modbus_crc16(uint8 *buf, uint8 len);
#endif
3.2 主程序(main.c)
#include "modbus.h"
// 全局变量定义
uint8 rx_buf[RX_BUF_SIZE];
uint8 tx_buf[TX_BUF_SIZE];
uint8 rx_len = 0;
uint8 tx_len = 0;
bit rx_done = 0;
bit tx_done = 1;
// Coil status(位寻址区,最多16路)
// 20H-2FH为位寻址区,共128位
// 线圈0-7对应20H.0-20H.7
// 线圈8-15对应21H.0-21H.7
uint8 coil_buf[2] = {0x00, 0x00};
// 离散输入状态
uint8 input_buf[2] = {0x00, 0x00};
// 保持寄存器(最多10个)
uint16 hold_reg[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// 输入寄存器
uint16 input_reg[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// 毫秒计数器
uint16 ms_counter = 0;
uint16 frame_timer = 0;
void main(void)
{
modbus_init();
while(1)
{
if(rx_done)
{
modbus_task();
rx_done = 0;
rx_len = 0;
}
// 在这里添加用户应用代码
// 例如:读取传感器数据更新到input_reg
// input_reg[0] = read_temperature();
}
}
// 串口初始化
void modbus_init(void)
{
TMOD |= 0x20; // T1模式2,8位自动重装
TH1 = 0xFD; // 9600bps @ 11.0592MHz
TL1 = 0xFD;
TR1 = 1; // 启动T1
SCON = 0x50; // 串口模式1,允许接收
PCON = 0x00; // SMOD=0
ES = 1; // 使能串口中断
EA = 1; // 开总中断
RS485_DIR = 0; // 默认接收模式
}
// 串口中断服务程序
void uart_isr(void) interrupt 4
{
if(RI) // 接收中断
{
RI = 0;
if(rx_len < RX_BUF_SIZE)
{
rx_buf[rx_len++] = SBUF;
frame_timer = 0; // 重置帧超时计时器
}
}
if(TI) // 发送中断
{
static uint8 tx_idx = 0;
TI = 0;
tx_idx++;
if(tx_idx < tx_len)
{
SBUF = tx_buf[tx_idx];
}
else
{
tx_idx = 0;
RS485_DIR = 0; // 切换回接收模式
tx_done = 1;
}
}
}
// 定时器0中断(1ms @ 11.0592MHz)
void timer0_isr(void) interrupt 1
{
TH0 = 0xFC;
TL0 = 0x67;
ms_counter++;
// 帧超时检测
if(rx_len > 0 && !rx_done)
{
frame_timer++;
if(frame_timer >= TIMEOUT_MS)
{
rx_done = 1; // 帧接收完成
}
}
}
3.3 Modbus协议处理(modbus.c)
#include "modbus.h"
// 外部变量声明
extern uint8 coil_buf[2];
extern uint8 input_buf[2];
extern uint16 hold_reg[10];
extern uint16 input_reg[10];
// CRC16校验表(Modbus多项式0xA001)
const uint16 code crc_table[256] = {
0x0000,0xC0C1,0xC181,0x0140,0xC301,0x03C0,0x0280,0xC241,
0xC601,0x06C0,0x0780,0xC741,0x0500,0xC5C1,0xC481,0x0440,
0xCC01,0x0CC0,0x0D80,0xCD41,0x0F00,0xCFC1,0xCE81,0x0E40,
0x0A00,0xCAC1,0xCB81,0x0B40,0xC901,0x09C0,0x0880,0xC841,
0xD801,0x18C0,0x1980,0xD941,0x1B00,0xDBC1,0xDA81,0x1A40,
0x1E00,0xDEC1,0xDF81,0x1F40,0xDD01,0x1DC0,0x1C80,0xDC41,
0x1400,0xD4C1,0xD581,0x1540,0xD701,0x17C0,0x1680,0xD641,
0xD201,0x12C0,0x1380,0xD341,0x1100,0xD1C1,0xD081,0x1040,
0xF001,0x30C0,0x3180,0xF141,0x3300,0xF3C1,0xF281,0x3240,
0x3600,0xF6C1,0xF781,0x3740,0xF501,0x35C0,0x3480,0xF441,
0x3C00,0xFCC1,0xFD81,0x3D40,0xFF01,0x3FC0,0x3E80,0xFE41,
0xFA01,0x3AC0,0x3B80,0xFB41,0x3900,0xF9C1,0xF881,0x3840,
0x2800,0xE8C1,0xE981,0x2940,0xEB01,0x2BC0,0x2A80,0xEA41,
0xEE01,0x2EC0,0x2F80,0xEF41,0x2D00,0xEDC1,0xEC81,0x2C40,
0xE401,0x24C0,0x2580,0xE541,0x2700,0xE7C1,0xE681,0x2640,
0x2200,0xE2C1,0xE381,0x2340,0xE101,0x21C0,0x2080,0xE041,
0xA001,0x60C0,0x6180,0xA141,0x6300,0xA3C1,0xA281,0x6240,
0x6600,0xA6C1,0xA781,0x6740,0xA501,0x65C0,0x6480,0xA441,
0x6C00,0xACC1,0xAD81,0x6D40,0xAF01,0x6FC0,0x6E80,0xAE41,
0xAA01,0x6AC0,0x6B80,0xAB41,0x6900,0xA9C1,0xA881,0x6840,
0x7800,0xB8C1,0xB981,0x7940,0xBB01,0x7BC0,0x7A80,0xBA41,
0xBE01,0x7EC0,0x7F80,0xBF41,0x7D00,0xBDC1,0xBC81,0x7C40,
0xB401,0x74C0,0x7580,0xB541,0x7700,0xB7C1,0xB681,0x7640,
0x7200,0xB2C1,0xB381,0x7340,0xB101,0x71C0,0x7080,0xB041,
0x5000,0x90C1,0x9181,0x5140,0x9301,0x53C0,0x5280,0x9241,
0x9601,0x56C0,0x5780,0x9741,0x5500,0x95C1,0x9481,0x5440,
0x9C01,0x5CC0,0x5D80,0x9D41,0x5F00,0x9FC1,0x9E81,0x5E40,
0x5A00,0x9AC1,0x9B81,0x5B40,0x9901,0x59C0,0x5880,0x9841,
0x8801,0x48C0,0x4980,0x8941,0x4B00,0x8BC1,0x8A81,0x4A40,
0x4E00,0x8EC1,0x8F81,0x4F40,0x8D01,0x4DC0,0x4C80,0x8C41,
0x4400,0x84C1,0x8581,0x4540,0x8701,0x47C0,0x4680,0x8641,
0x8201,0x42C0,0x4380,0x8341,0x4100,0x81C1,0x8081,0x4040
};
// CRC16计算
uint16 modbus_crc16(uint8 *buf, uint8 len)
{
uint16 crc = 0xFFFF;
uint8 i;
while(len--)
{
i = crc ^ *buf++;
crc >>= 8;
crc ^= crc_table[i];
}
return crc;
}
// 发送响应帧
void modbus_send(uint8 len)
{
uint16 crc;
// 添加CRC check
crc = modbus_crc16(tx_buf, len);
tx_buf[len++] = crc & 0xFF; // 低字节
tx_buf[len++] = (crc >> 8) & 0xFF; // high byte
tx_len = len;
tx_done = 0;
RS485_DIR = 1; // 切换到发送模式
// 发送第一个字节触发中断
SBUF = tx_buf[0];
}
// Exception Response
void modbus_exception(uint8 func, uint8 code)
{
tx_buf[0] = MODBUS_ADDR;
tx_buf[1] = func | 0x80;
tx_buf[2] = code;
modbus_send(3);
}
// Modbus主处理函数
void modbus_task(void)
{
uint8 addr, func;
uint16 reg_addr, reg_num;
uint16 crc_recv, crc_calc;
uint8 i, j, idx;
// 检查帧长度
if(rx_len < 4) return;
// 检查从站地址
addr = rx_buf[0];
if(addr != MODBUS_ADDR && addr != 0) return;
// CRC check
crc_recv = rx_buf[rx_len-2] | (rx_buf[rx_len-1] << 8);
crc_calc = modbus_crc16(rx_buf, rx_len-2);
if(crc_recv != crc_calc) return;
func = rx_buf[1];
switch(func)
{
case FUNC_READ_COIL: // 读线圈 0x01
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 16 || reg_addr + reg_num > 16)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
tx_buf[0] = MODBUS_ADDR;
tx_buf[1] = func;
tx_buf[2] = (reg_num + 7) / 8;
for(i = 0; i < tx_buf[2]; i++)
{
tx_buf[3+i] = 0;
for(j = 0; j < 8; j++)
{
idx = reg_addr + i*8 + j;
if(idx < reg_addr + reg_num)
{
if(coil_buf[idx/8] & (1 << (idx%8)))
tx_buf[3+i] |= (1 << j);
}
}
}
modbus_send(3 + tx_buf[2]);
break;
}
case FUNC_READ_REG: // read holding registers 0x03
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 10 || reg_addr + reg_num > 10)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
tx_buf[0] = MODBUS_ADDR;
tx_buf[1] = func;
tx_buf[2] = reg_num * 2;
for(i = 0; i < reg_num; i++)
{
tx_buf[3 + i*2] = (hold_reg[reg_addr+i] >> 8) & 0xFF;
tx_buf[3 + i*2 + 1] = hold_reg[reg_addr+i] & 0xFF;
}
modbus_send(3 + reg_num*2);
break;
}
case FUNC_WRITE_COIL: // 写单线圈 0x05
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 16)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
if(reg_num == 0xFF00)
coil_buf[reg_addr/8] |= (1 << (reg_addr%8));
else if(reg_num == 0x0000)
coil_buf[reg_addr/8] &= ~(1 << (reg_addr%8));
else
{
modbus_exception(func, EXCEPT_ILLEGAL_VAL);
return;
}
// 回显请求
for(i = 0; i < 6; i++) tx_buf[i] = rx_buf[i];
modbus_send(6);
break;
}
case FUNC_WRITE_REG: // 写单寄存器 0x06
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 10)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
hold_reg[reg_addr] = reg_num;
for(i = 0; i < 6; i++) tx_buf[i] = rx_buf[i];
modbus_send(6);
break;
}
case FUNC_WRITE_COILS: // 写多线圈 0x0F
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 16 || reg_addr + reg_num > 16)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
for(i = 0; i < reg_num; i++)
{
idx = reg_addr + i;
if(rx_buf[7 + i/8] & (1 << (i%8)))
coil_buf[idx/8] |= (1 << (idx%8));
else
coil_buf[idx/8] &= ~(1 << (idx%8));
}
tx_buf[0] = MODBUS_ADDR;
tx_buf[1] = func;
tx_buf[2] = rx_buf[2];
tx_buf[3] = rx_buf[3];
tx_buf[4] = rx_buf[4];
tx_buf[5] = rx_buf[5];
modbus_send(6);
break;
}
case FUNC_WRITE_REGS: // 写多寄存器 0x10
{
reg_addr = (rx_buf[2]<<8) | rx_buf[3];
reg_num = (rx_buf[4]<<8) | rx_buf[5];
if(reg_addr >= 10 || reg_addr + reg_num > 10)
{
modbus_exception(func, EXCEPT_ILLEGAL_ADDR);
return;
}
for(i = 0; i < reg_num; i++)
{
hold_reg[reg_addr+i] = (rx_buf[7+i*2]<<8) | rx_buf[7+i*2+1];
}
tx_buf[0] = MODBUS_ADDR;
tx_buf[1] = func;
tx_buf[2] = rx_buf[2];
tx_buf[3] = rx_buf[3];
tx_buf[4] = rx_buf[4];
tx_buf[5] = rx_buf[5];
modbus_send(6);
break;
}
default:
modbus_exception(func, EXCEPT_ILLEGAL_FUNC);
break;
}
}
四、Instructions for Use
4.1 编译环境
- Recommended for useKeil uVision4或uVision5进行编译
- 晶振频率:11.0592MHz(必须使用此频率才能保证9600bps准确)
- 单片机型号:STC89C52RC / AT89S52 / STC12C5A60S2等兼容型号
- 代码空间:约4KB,RAM占用约150字节
4.2 测试方法
- 使用USB转RS485转换器连接电脑和模块
- 열기Modbus Poll或Modbus Slave调试软件
- 设置:slave 주소1、Baud rate9600、데이터 bit8、stop bit1、无校验
- 기능 코드03读取保持寄存器,주소0开始,read10个
- 기능 코드06写单寄存器,修改hold_reg的值
- 기능 코드01读线圈,Viewcoil_buf的状态
- 기능 코드05写单线圈,控制继电器吸合/断开
4.3 自定义修改
- 修改从站地址:改变MODBUS_ADDR宏定义
- 修改波特率:修改TH1/TL1初值(9600=0xFD,4800=0xFA,19200=0xFD+SMOD)
- 增加线圈数量:修改coil_buf数组大小
- 增加寄存器数量:修改hold_reg数组大小
- 添加应用逻辑:在main函数的while循环中添加用户代码
五、common problems
- 通讯无响应:检查A/B线是否接反,从站地址是否一致,波特率是否匹配
- CRC check오답:确认晶振为11.0592MHz,检查串口配置
- 数据乱码:检查波特率设置,ConfirmSMOD位配置正确
- 只能接收不能发送:检查RS485_DIR引脚控制是否正确,DE/RE是否接在一起
- 多从站冲突:确保每个从站地址唯一,总线两端接120Ω终端电阻
Leave a Reply