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

freeFree Technical Resource

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

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

51单片机(8051内核)是エンベデッド·イン开发中最经典的微コントローラーは之一,广泛应用于工业控制、Smart home、データの収集等领域。在51单片机上実装Modbus RTUスレーブ通讯,可以让单片机デバイス接入標準的工业自動化网络,与PLC、组态ソフトウェア、トップマシン进行データ交互。本記事提供完全的51单片机Modbus RTUスレーブ実装代码,包括シリアルポート初期化、CRC16チェック、协议解析、レジスタマップ和メイン·サイクル処理。

一、硬件环境与リソース分配

1.1 おすすめハードウェアの構成

项目规格説明
单片机STC89C52RC / STC12C5A60S2增强型51内核,速度更快
晶振11.0592MHzシリアルポートボーレート正確,Recommended for use
serial portUART0(P3.0/P3.1)用于Modbus RTU通讯
RS485芯片MAX485 / SP3485TTL转RS485,DE/RE接P3.2
RAM需求≥256バイト標準51内部128バイト可能不够
Flash需求≥8KB代码约4-6KB

1.2 レジスタマッピングテーブル

ModbusaddressType变量名説明
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 address
#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 data 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);
}

/* ============================================================
 * function code01:リードコイルを読む
 * ============================================================ */
void FC01_ReadCoils(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 byte_count, i, bit_pos;
    
    // チェックアドレス範囲
    if (start_addr + quantity > COIL_COUNT) {
        Modbus_Exception(FC_READ_COILS, EX_ILLEGAL_ADDR);
        return;
    }
    
    byte_count = (quantity + 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 < quantity; i++) {
        bit_pos = start_addr + i;
        if (coil_regs[bit_pos]) {
            tx_buf[3 + i/8] |= (1 << (i % 8));
        }
    }
    
    Modbus_Send(3 + byte_count);
}

/* ============================================================
 * function code02:read discrete inputs
 * ============================================================ */
void FC02_ReadInputs(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 byte_count, i, bit_pos;
    
    if (start_addr + quantity > INPUT_COUNT) {
        Modbus_Exception(FC_READ_INPUTS, EX_ILLEGAL_ADDR);
        return;
    }
    
    byte_count = (quantity + 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 < quantity; i++) {
        bit_pos = start_addr + i;
        if (input_regs[bit_pos]) {
            tx_buf[3 + i/8] |= (1 << (i % 8));
        }
    }
    
    Modbus_Send(3 + byte_count);
}

/* ============================================================
 * function code03:read holding registers
 * ============================================================ */
void FC03_ReadHolding(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 i;
    
    if (start_addr + quantity > 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] = quantity * 2;
    
    for (i = 0; i < quantity; i++) {
        tx_buf[3 + i*2]     = (holding_regs[start_addr + i] >> 8) & 0xFF;
        tx_buf[3 + i*2 + 1] = holding_regs[start_addr + i] & 0xFF;
    }
    
    Modbus_Send(3 + quantity * 2);
}

/* ============================================================
 * function code04:read input registers(ADC值)
 * ============================================================ */
void FC04_ReadAnalog(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 i;
    
    if (start_addr + quantity > 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] = quantity * 2;
    
    for (i = 0; i < quantity; i++) {
        tx_buf[3 + i*2]     = (analog_regs[start_addr + i] >> 8) & 0xFF;
        tx_buf[3 + i*2 + 1] = analog_regs[start_addr + i] & 0xFF;
    }
    
    Modbus_Send(3 + quantity * 2);
}

/* ============================================================
 * function code05: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);
}

/* ============================================================
 * function code06: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);
}

/* ============================================================
 * function code0F:write multiple coils
 * ============================================================ */
void FC0F_WriteCoils(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 byte_count = req[6];
    uint8 i;
    
    if (start_addr + quantity > COIL_COUNT) {
        Modbus_Exception(FC_WRITE_COILS, EX_ILLEGAL_ADDR);
        return;
    }
    
    // 解析コイルの状態
    for (i = 0; i < quantity; i++) {
        coil_regs[start_addr + i] = (req[7 + i/8] >> (i % 8)) & 0x01;
    }
    
    // Response:address+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);
}

/* ============================================================
 * function code10:write multiple registers
 * ============================================================ */
void FC10_WriteRegs(uint8 *req)
{
    uint16 start_addr = (req[2] << 8) | req[3];
    uint16 quantity   = (req[4] << 8) | req[5];
    uint8 i;
    
    if (start_addr + quantity > HOLDING_COUNT) {
        Modbus_Exception(FC_WRITE_REGS, EX_ILLEGAL_ADDR);
        return;
    }
    
    // 解析レジスタの値(前の高さ。)
    for (i = 0; i < quantity; i++) {
        holding_regs[start_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バイト:address+function code+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;  // CRCError,丢弃
    }
    
    // 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;   // parameter1
    holding_regs[1] = 200;   // parameter2
    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下,一个文字(含スタート地点。、data 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 コイルの状態パッキング

リードコイルを読む和離散入力の読み取り的レスポンス中,多个ステータス按位パッキング到バイト中,最低位对应第一1つのコイル。例如読み取り81つのコイル,ステータス为[1,0,1,0,0,0,0,0],パッキング为0x05。

四、测试与验证

4.1 使用Modbus Poll测试

  • 连接方式:USB转RS485変換器です。,A接A,B接B
  • Serial port parameters:9600,8データ·ビット,検証なし。,1ストップ·ビット
  • slave address:1
  • read holding registers:address0,Quantity10,应戻る[100, 200, 9600, 0, 0...]
  • write single coil:address0写ON,对应RELAY1应吸合
  • read input registers:address0,Quantity4,应戻る[512, 1024, 2048, 4095]

4.2 よくある質問トラブルシューティング

  • 応答なし。:チェックRS485配線(A/B是否接反)、slave address、ボーレート是否一致
  • CRCError:確認晶振为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 verification 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 address will not be published. Required fields are marked *.