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

freeFree Technical Resource

This content is free to read, suitable for basic learning and search traffic.

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

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 / SP3485TTL转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开发板上验证通讯正常,再移植到实际产品中。代码结构清晰,寄存器映射表可根据实际需求修改。

Put this resource to use in a real project?

Go to the Tool Center for message parsing, CRC 검증 and device debugging, or submit your requirements for selection and integration advice.

Engineer Membership

Turn this article into actionable debugging resources

After activation, you can use advanced message parsing, resource pack downloads, code examples, engineering cases and priority technical support, suitable for real project delivery.

Unlimited Advanced Tools
Resource & Code Packs
Complete Engineering Case Library
Priority Technical Support

Leave a Reply

Your email 주소 will not be published. Required fields are marked *.