51单片机(8051内核)是エンベデッド·イン开发中最经典的微コントローラーは之一,广泛应用于工业控制、Smart home、データの収集等领域。在51单片机上実装Modbus RTUスレーブ通讯,可以让单片机デバイス接入標準的工业自動化网络,与PLC、组态ソフトウェア、トップマシン进行データ交互。本記事提供完全的51单片机Modbus RTUスレーブ実装代码,包括シリアルポート初期化、CRC16チェック、协议解析、レジスタマップ和メイン·サイクル処理。
一、硬件环境与リソース分配
1.1 おすすめハードウェアの構成
| 项目 | 规格 | 説明 |
|---|---|---|
| 单片机 | STC89C52RC / STC12C5A60S2 | 增强型51内核,速度更快 |
| 晶振 | 11.0592MHz | シリアルポートボーレート正確,Recommended for use |
| serial port | UART0(P3.0/P3.1) | 用于Modbus RTU通讯 |
| RS485芯片 | MAX485 / SP3485 | TTL转RS485,DE/RE接P3.2 |
| RAM需求 | ≥256バイト | 標準51内部128バイト可能不够 |
| Flash需求 | ≥8KB | 代码约4-6KB |
1.2 レジスタマッピングテーブル
| Modbusaddress | 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 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开发板上验证通信は正常,再移植到实际产品中。代码構造清晰,レジスタマッピングテーブル可根据实际需求修改。
Leave a Reply