51 MCU 를 기반으로 한 Modbus 통신 코드 예제

무료무료 기술 자료

이 글은 바로 읽을 수 있으며 기초 학습과 검색 유입에 적합합니다.

51 MCU 를 기반으로 한 Modbus 통신 코드 예제

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 / SP3485TTL转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개발板上验证通讯正常,再移植到实际产品中。代码结构清晰,레지스터 매핑表可根据实际需求修改。

실제 프로젝트에 이 자료를 활용하시겠습니까?

도구 센터에서 프레임 분석, CRC 검증, 장비 디버깅을 진행하거나 요청을 제출하여 선정 및 연동 제안을 받아보세요.

엔지니어 회원

이 글을 실행 가능한 디버깅 자료로 활용하세요

가입하면 고급 프레임 분석, 자료 패키지 다운로드, 코드 예제, 엔지니어링 사례, 우선 기술 지원을 이용할 수 있어 실제 프로젝트 납품에 적합합니다.

고급 도구 무제한 이용
자료 패키지와 코드 패키지
완전한 엔지니어링 사례 라이브러리
우선 기술 지원 채널

답글 작성

이메일 주소는 공개되지 않습니다. 必填项已用 * 标注