51单片机是一种经典的8位エンベデッド·イン微コントローラーは,因其价格低廉、开发简单而被广泛应用于工业控制、Smart home、仪器仪表等领域。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脚) | 电源 |
| GND | GND (5脚) | 地 |
| - | A (6脚) | RS485 A线 |
| - | B (7脚) | RS485 B线 |
1.2 リソース分配
- serial port:使用UART,モード1(8ビット·データ,可变ボーレート)
- タイマータイマー。:T1作为ボーレート发生器,9600bps @ 11.0592MHz晶振
- 中断:シリアルポート中断受信データ,タイマータイマー。中断処理タイムアウト
- 内存:コイルの状態使用位寻址区,レジスタ使用内部RAM
二、Modbus RTU协议フレーム形式
Modbus RTUフレーム由以下字段组成:
| field | Length | 説明 |
|---|---|---|
| slave address | 1バイト | 1-247,0ブロードキャストアドレス |
| function code | 1バイト | 01/02/03/05/06/15/16等 |
| data | Nbyte | address、Quantity、データ值等 |
| CRC check | 2バイト | 低バイト数前に,ハイバイト在后 |
三、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 address
#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変換器です。连接电脑和モジュール
- openModbus Poll或Modbus Slaveデバッグ软件
- 設定:slave address1、Baud rate9600、data bit8、stop bit1、検証なし。
- function code03リードホールドレジスタ,address0开始,read10个
- function code06単一レジスタの書き込み,修改hold_reg的值
- function code01リードコイルを読む,Viewcoil_buf的ステータス
- function code05シングルコイルを書く。,控制继电器吸合/断开
4.3 自定义修改
- アドレスの変更:改变MODBUS_ADDR宏定义
- 修改ボーレート:修改TH1/TL1初值(9600=0xFD,4800=0xFA,19200=0xFD+SMOD)
- 增加コイルの数:修改coil_buf配列大小
- 增加レジスタの数:修改hold_reg配列大小
- 追加应用逻辑:在main函数的while循环中追加用户代码
五、common problems
- 通讯応答なし。:チェックA/B线是否接反,駅からの住所是否一致,ボーレート一致するかどうか
- CRC checkError:確認晶振为11.0592MHz,チェックシリアルポート設定
- データ乱码:チェックボーレート設定,ConfirmSMOD位設定正确
- 只能受信不能送信:チェックRS485_DIR引脚控制是否正确,DE/RE是否接在一起
- 多スレーブ冲突:确保每个駅からの住所唯一,バス两端接120Ω終端抵抗
Leave a Reply