51单片机(8051内核)是嵌入式개발中最经典的微제어기之一,广泛적용于工业控制、스마트 홈、데이터采集等领域。在51单片机上实现Modbus RTU슬레이브通讯,可以让单片机设备接入표준的산업 자동화네트워크,与PLC、구성(SCADA)软件、上位机进行데이터交互。本文提供完整的51单片机Modbus RTU슬레이브实现代码,包括시리얼 포트初始化、CRC16체크섬、프로토콜분석、레지스터 매핑和主循环处理。
一、硬件환경与자료分配
1.1 推荐硬件설정
| 项目 | 사양 | 설명 |
|---|---|---|
| 单片机 | STC89C52RC / STC12C5A60S2 | 增强型51内核,速度更快 |
| 晶振 | 11.0592MHz | 시리얼 포트통신 속도精准,推荐使用 |
| 시리얼 포트 | 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주소 | 유형 | 변수名 | 설명 |
|---|---|---|---|
| 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 // 슬레이브 주소
#define MODBUS_BAUDRATE 9600 // 통신 속도
#define MODBUS_BUF_SIZE 64 // 수신/보내기缓冲区大小
/* 기능 코드定义 */
#define FC_READ_COILS 0x01 // 读코일
#define FC_READ_INPUTS 0x02 // 读이산 입력
#define FC_READ_HOLDING 0x03 // 读유지 레지스터
#define FC_READ_ANALOG 0x04 // 读입력 레지스터
#define FC_WRITE_COIL 0x05 // 写单个코일
#define FC_WRITE_REG 0x06 // 写单个레지스터
#define FC_WRITE_COILS 0x0F // 写多个코일
#define FC_WRITE_REGS 0x10 // 写多个레지스터
/* 异常码 */
#define EX_ILLEGAL_FUNC 0x01 // 非法기능 코드
#define EX_ILLEGAL_ADDR 0x02 // 非法주소
#define EX_ILLEGAL_VALUE 0x03 // 非法데이터值
#define EX_SLAVE_FAILURE 0x04 // 슬레이브设备故障
/* 레지스터 수량 */
#define COIL_COUNT 16
#define INPUT_COUNT 16
#define HOLDING_COUNT 32
#define ANALOG_COUNT 16
/* 全局변수 */
extern uint8 coil_regs[COIL_COUNT]; // 코일 상태
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=보내기,0=수신
/* ============================================================
* 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; // CRC高바이트
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:读이산 입력
* ============================================================ */
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:读유지 레지스터
* ============================================================ */
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:读입력 레지스터(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:写单个코일
* ============================================================ */
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:写单个레지스터
* ============================================================ */
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:写多个코일
* ============================================================ */
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;
}
// 响应:주소+数量
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:写多个레지스터
* ============================================================ */
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];
}
// 响应
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 검증
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值,实际项目中替换为真实ADC읽기
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(); // 处理Modbus请求
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下,一个문자(含시작位、데이터 비트、정지 비트)约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
- 시리얼 포트 매개변수:9600,8데이터 비트,패리티 없음,1정지 비트
- 슬레이브 주소:1
- 读유지 레지스터:주소0,数量10,应返回[100, 200, 9600, 0, 0...]
- 写单个코일:주소0写ON,对应RELAY1应吸合
- 读입력 레지스터:주소0,数量4,应返回[512, 1024, 2048, 4095]
4.2 常见문제排查
- 无响应:检查RS485배선(A/B是否接反)、슬레이브 주소、통신 속도是否一致
- CRC错误:确认晶振为11.0592MHz,检查통신 속도계산是否正确
- 偶尔丢包:추가帧间隔时间,检查RS485总线是否加120Ω终端电阻
- 只能读不能写:检查写操作的주소范围,确认레지스터是否可写
- 데이터错乱:检查大端小端형식,Modbus표준为高바이트在前
五、扩展建议
- 추가看门狗:防止程序跑飞,提高工业现场可靠性
- 매개변수掉电저장:使用STC单片机的EEPROM저장유지 레지스터值
- 多시리얼 포트支持:STC12시리즈有2个시리얼 포트,可同时支持Modbus和디버깅출력
- 추가广播功能:支持슬레이브 주소0的广播写操作
- 自定义기능 코드:实现厂商特定功能,如固件업그레이드、校准等
- Modbus ASCII:如需ASCII모드,将CRC替换为LRC체크섬,帧형식改为콜론开头
本代码实现了完整的Modbus RTU슬레이브功能,支持所有표준기능 코드,可直接用于51单片机项目개발。建议先在STC89C52RC개발板上验证通讯正常,再移植到实际产品中。代码结构清晰,레지스터 매핑表可根据实际需求修改。
답글 작성