51 MCU 와 485 장치 통신을 위한 Modbus 프로토콜 소스 코드 구현

Paid ¥4.99Paid ¥4.99

该内容已接入现有会员/付费系统,解锁后可查看完整资料或下载附件。

查看解锁方式
51 MCU 와 485 장치 통신을 위한 Modbus 프로토콜 소스 코드 구현

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脚)电源
GNDGND (5脚)
-A (6脚)RS485 A线
-B (7脚)RS485 B线

1.2 资源分配

  • 시리얼 포트:使用UART,模式1(8位数据,可变波特率)
  • 定时器:T1作为波特率发生器,9600bps @ 11.0592MHz晶振
  • 中断:串口中断接收数据,定时器中断处理超时
  • 内存:线圈状态使用位寻址区,寄存器使用内部RAM

二、Modbus RTU协议帧格式

Modbus RTU帧由以下字段组成:

fieldLength说明
slave 주소1字节1-247,0为广播地址
기능 코드1字节01/02/03/05/06/15/16等
데이터Nbyte주소、Quantity、数据值等
CRC check2字节低字节在前,高字节在后

三、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Ω终端电阻
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 *.