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

유료 ¥4.99유료 ¥4.99

이 콘텐츠는 회원/유료 시스템과 연동되어 있으며, 잠금 해제 후 전체 자료를 보거나 첨부파일을 다운로드할 수 있습니다.

보기解锁방식
51 MCU 와 485 장치 통신을 위한 Modbus 프로토콜 소스 코드 구현

51单片机是一种经典的8位嵌入式微제어기,因其价格低廉、개발简单而被广泛적용于工业控制、스마트 홈、仪器仪表等领域。RS485总线是工业现场最常用的串行通讯표준之一,而Modbus RTU프로토콜则是RS485总线上最主流的적용层프로토콜。本文提供一套完整的51单片机实现Modbus RTU슬레이브通讯的C언어源码,支持코일读写、레지스터读写等표준기능 코드。

一、系统架构

本方案以STC89C52RC(或兼容的AT89S52)为主控芯片,通过MAX485芯片实现RS485电平转换。51单片机作为Modbus슬레이브,响应마스터(PLC、触摸屏、上位机等)的读写请求。

1.1 硬件연결

51单片机引脚MAX485引脚설명
P3.0 (RXD)RO (1脚)수신데이터
P3.1 (TXD)DI (4脚)보내기데이터
P2.0 (或其他IO)DE/RE (2,3脚)收发控制:高=보내기,低=수신
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帧由以下필드组成:

필드길이설명
슬레이브 주소1바이트1-247,0为广播주소
기능 코드1바이트01/02/03/05/06/15/16等
데이터N바이트주소、数量、데이터值等
CRC 검증2바이트低바이트在前,高바이트在后

三、完整C언어源码

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       // 슬레이브 주소
#define MODBUS_BAUD     9600    // 통신 속도
#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    // 读이산 입력
#define FUNC_READ_REG       0x03    // 读유지 레지스터
#define FUNC_READ_INPUT_REG 0x04    // 读입력 레지스터
#define FUNC_WRITE_COIL     0x05    // 写单코일
#define FUNC_WRITE_REG      0x06    // 写单레지스터
#define FUNC_WRITE_COILS    0x0F    // 写多코일
#define FUNC_WRITE_REGS     0x10    // 写多레지스터

// 异常码
#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;

// 코일 상태(位寻址区,最多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 검증
    crc = modbus_crc16(tx_buf, len);
    tx_buf[len++] = crc & 0xFF;       // 低바이트
    tx_buf[len++] = (crc >> 8) & 0xFF; // 高바이트
    
    tx_len = len;
    tx_done = 0;
    
    RS485_DIR = 1;  // 전환到보내기모드
    
    // 보내기第一个바이트触发中断
    SBUF = tx_buf[0];
}

// 异常响应
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 검증
    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:  // 读유지 레지스터 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;
    }
}

四、使用설명

4.1 编译환경

  • 推荐使用Keil uVision4或uVision5进行编译
  • 晶振频率:11.0592MHz(必须使用此频率才能保证9600bps准确)
  • 单片机型号:STC89C52RC / AT89S52 / STC12C5A60S2等兼容型号
  • 代码空间:约4KB,RAM占用约150바이트

4.2 测试方法

  • 使用USB转RS485컨버터연결电脑和模块
  • 열기Modbus Poll或Modbus Slave디버깅软件
  • 设置:슬레이브 주소1、통신 속도9600、데이터 비트8、정지 비트1、패리티 없음
  • 기능 코드03유지 레지스터 읽기 (FC03),주소0开始,읽기10个
  • 기능 코드06写单레지스터,修改hold_reg的值
  • 기능 코드01读코일,보기coil_buf的状态
  • 기능 코드05写单코일,控制릴레이吸合/断开

4.3 自定义修改

  • 修改슬레이브 주소:改变MODBUS_ADDR宏定义
  • 修改통신 속도:修改TH1/TL1初值(9600=0xFD,4800=0xFA,19200=0xFD+SMOD)
  • 추가코일 수량:修改coil_buf数组大小
  • 추가레지스터 수량:修改hold_reg数组大小
  • 추가적용逻辑:在main函数的while循环中추가用户代码

五、常见문제

  • 通讯无响应:检查A/B线是否接反,슬레이브 주소是否一致,통신 속도是否일치
  • CRC 검증错误:确认晶振为11.0592MHz,检查시리얼 포트 설정
  • 데이터乱码:检查통신 속도设置,确认SMOD位설정正确
  • 只能수신不能보내기:检查RS485_DIR引脚控制是否正确,DE/RE是否接在一起
  • 多슬레이브冲突:确保每个슬레이브 주소唯一,总线两端接120Ω终端电阻
실제 프로젝트에 이 자료를 활용하시겠습니까?

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

엔지니어 회원

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

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

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

답글 작성

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