在 STC15W201S 上实现 MODBUS RTU 协议

STC15W201S 是一款 8pin增强型8051单片机,片内资源有限但足够跑一个轻量级的 Modbus RTU 从站。


一、STC15W201S 资源盘点

资源 规格 对 Modbus 的影响
内核 1T 8051 执行速度比传统12T快12倍
Flash 1KB / 2KB / 4KB(看具体型号) 代码量需控制在2KB以内
RAM 128/256字节 缓冲区要小而精
UART 1个串口(P3.0 RXD / P3.1 TXD) 够用
定时器 5个 用于波特率和帧超时计时

关键点 :STC15W201S 只有 8个引脚 ,资源非常紧张。但好消息是有现成方案------已有开发者实现了 Keil UV2 编译后程序小于2KB 的 Modbus RTU 从站代码。


二、硬件连接方案

复制代码
┌─────────────┐        MAX485         ┌─────────────┐
│  Modbus主机  │◄──RS485总线──►│ MAX485  │◄──UART──►│ STC15W201S │
│ (PLC/HMI)   │                     │           │       P3.0/RXD  │
│             │                     │  DE/RE#   │◄──GPIO──►│ P3.2(控制)│
└─────────────┘                     └───────────┘       P3.1/TXD  │
                                                      └─────────────┘

关键硬件点

  • 使用 MAX485/SP3485 等RS485收发器芯片
  • STC15W201S 的 P3.2 外接一个GPIO控制 MAX485 的 DE/RE# 引脚(发送时拉高,接收时拉低)
  • 波特率常用 9600 或 115200,数据位8位,无校验位,1位停止位

三、串口初始化(UART + 定时器2)

STC15W201S 串口1 默认工作在 方式1 (8位UART,可变波特率),推荐使用 定时器2 作为波特率发生器。

c 复制代码
#include "config.h"   // STC官方头文件

/* ---------------------------
 * 串口1初始化 @11.0592MHz
 * 波特率9600,8N1
 *--------------------------- */
void UART1_Init(void)
{
    SCON  = 0x50;     // 8位数据,允许接收
    AUXR |= 0x01;     // 串口1选择定时器2为波特率发生器
    AUXR |= 0x04;     // 定时器2时钟 = Fosc (1T模式)
    T2L   = 0xE0;     // 9600@11.0592MHz 定时初值
    T2H   = 0xFE;
    AUXR |= 0x10;     // 启动定时器2
    ES    = 1;        // 允许串口中断
    EA    = 1;        // 开总中断
}

波特率速查(11.0592MHz晶振,1T模式):

  • 9600:T2L=0xE0, T2H=0xFE
  • 115200:T2L=0xF7, T2H=0xFF

四、Modbus RTU 核心代码实现

1. CRC16 校验(查表法,高效)

c 复制代码
/* CRC16查表法 */
static const uint16_t crc_table[] = {
    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,0x9300,0x93C1,0x9281,0x5240,
    0x9600,0x96C1,0x9781,0x5740,0x9501,0x55C0,0x5480,0x9441,
    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
};

uint16_t CRC16(uint8_t *buf, uint16_t len)
{
    uint16_t crc = 0xFFFF;
    while (len--) {
        crc = (crc >> 8) ^ crc_table[(crc ^ *buf++) & 0xFF];
    }
    return crc;
}

2. Modbus 数据帧结构

复制代码
┌──────────┬──────────┬──────────┬──────────┬──────────┐
│ 从站地址 │ 功能码   │ 数据域   │ CRC低字节 │ CRC高字节 │
│ 1字节    │ 1字节    │ N字节    │ 1字节    │ 1字节    │
└──────────┴──────────┴──────────┴──────────┴──────────┘

帧间隔判定 :RTU模式下,帧与帧之间必须有至少 3.5个字符时间 的静默间隔。

3. 接收状态机 + 帧超时判断

c 复制代码
/* ---------- 全局变量 ---------- */
#define MODBUS_ADDR     0x01    // 本机地址
#define RX_BUF_SIZE     64      // 接收缓冲区
#define TX_BUF_SIZE     64      // 发送缓冲区

volatile uint8_t rxBuf[RX_BUF_SIZE];
volatile uint8_t rxLen = 0;
volatile uint8_t rxFlag = 0;    // 1=接收到完整帧
volatile uint8_t rxTimer = 0;   // 帧间隔计时器(1ms步进)

/* ---------- 串口中断接收 ---------- */
void UART1_ISR(void) interrupt 4
{
    if (RI) {
        RI = 0;
        rxTimer = 0;            // 收到数据,重置超时计时
        
        if (!rxFlag && rxLen < RX_BUF_SIZE) {
            rxBuf[rxLen++] = SBUF;
        }
    }
    if (TI) {
        TI = 0;
        busy = 0;               // 发送完成标志
    }
}

/* ---------- 1ms定时器中执行帧超时判断 ---------- */
void Modbus_RXD_Timeout(void)
{
    if (rxLen > 0) {
        rxTimer++;
        
        /* 超时阈值 = 3.5个字符时间 ≈ 4ms@9600bps */
        if (rxTimer > 4) {
            rxFlag = 1;         // 标记一帧接收完成
        }
    }
}

超时时间计算 :9600bps 下,每字节约 1ms ,3.5个字符 ≈ 4ms 。115200bps 下约 0.4ms

4. 功能码03(读保持寄存器)实现

c 复制代码
/* 保持寄存器(4xxxx区),示例6个寄存器 */
uint16_t holdReg[6] = {0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1111, 0x2222};

/* ---------- Modbus 请求解析 ---------- */
void Modbus_Process(void)
{
    uint16_t crc_rx, crc_cal;
    uint16_t startAddr, regCount;
    uint8_t i, txLen = 0;
    uint8_t txBuf[TX_BUF_SIZE];

    if (!rxFlag) return;
    rxFlag = 0;

    /* 1. 地址不匹配则丢弃 */
    if (rxBuf[0] != MODBUS_ADDR) {
        rxLen = 0;
        return;
    }

    /* 2. CRC校验 */
    crc_rx = ((uint16_t)rxBuf[rxLen - 1] << 8) | rxBuf[rxLen - 2];
    crc_cal = CRC16((uint8_t*)rxBuf, rxLen - 2);
    if (crc_rx != crc_cal) {
        rxLen = 0;
        return;
    }

    /* 3. 按功能码分发 */
    switch (rxBuf[1]) {
        case 0x03:  /* 读保持寄存器 */
            startAddr = ((uint16_t)rxBuf[2] << 8) | rxBuf[3];
            regCount  = ((uint16_t)rxBuf[4] << 8) | rxBuf[5];
            
            txBuf[txLen++] = MODBUS_ADDR;    // 地址
            txBuf[txLen++] = 0x03;           // 功能码
            txBuf[txLen++] = regCount * 2;   // 字节数
            
            for (i = 0; i < regCount; i++) {
                txBuf[txLen++] = holdReg[startAddr + i] >> 8;
                txBuf[txLen++] = holdReg[startAddr + i] & 0xFF;
            }
            break;

        case 0x06:  /* 写单个保持寄存器 */
            startAddr = ((uint16_t)rxBuf[2] << 8) | rxBuf[3];
            holdReg[startAddr] = ((uint16_t)rxBuf[4] << 8) | rxBuf[5];
            
            /* 写单个寄存器,回显原请求 */
            for (i = 0; i < 6; i++) {
                txBuf[txLen++] = rxBuf[i];
            }
            break;

        default:
            /* 非法功能码异常响应 */
            txBuf[txLen++] = MODBUS_ADDR;
            txBuf[txLen++] = 0x80 | rxBuf[1];  // 功能码最高位置1
            txBuf[txLen++] = 0x01;              // 异常码:非法功能
            break;
    }

    /* 4. 附加CRC并发送 */
    crc_cal = CRC16(txBuf, txLen);
    txBuf[txLen++] = crc_cal & 0xFF;
    txBuf[txLen++] = crc_cal >> 8;

    RS485_Send_Enable();    // 拉高DE,切换为发送模式
    for (i = 0; i < txLen; i++) {
        SBUF = txBuf[i];
        while (!TI);
        TI = 0;
    }
    RS485_Recv_Enable();    // 拉低RE#,切回接收模式

    rxLen = 0;
}

5. 主循环

c 复制代码
void main(void)
{
    UART1_Init();
    
    /* 初始化定时器0产生1ms中断(用于帧超时计时) */
    TMOD |= 0x01;        // 定时器0模式1
    TH0 = 0xFC;          // 1ms@11.0592MHz
    TL0 = 0x66;
    ET0 = 1;             // 允许T0中断
    TR0 = 1;             // 启动T0
    
    EA = 1;
    
    RS485_Recv_Enable();  // 默认为接收模式

    while (1) {
        Modbus_Process();   // 处理Modbus请求
        /* 其他业务逻辑... */
    }
}

/* 定时器0中断 --- 1ms周期 */
void Timer0_ISR(void) interrupt 1
{
    TH0 = 0xFC;
    TL0 = 0x66;
    Modbus_RXD_Timeout();   // 帧超时判断
}

参考代码 在STC15W201S上实现MODBUS RTU协议 www.youwenfan.com/contentcst/182684.html

五、代码体积对比与优化

方案 功能码 编译后大小 备注
极简版 03 + 06 < 2KB 可访问4xxxx寄存器,适合8pin型号
标准版 01/02/03/04/05/06/15/16 ~4-6KB 适合STC15W4K系列
完整版 全部功能码 + 异常处理 ~8KB+ 适合大容量型号

现成的 极简版方案已在STC15W204S(同系列8pin)上验证通过,Keil UV2 编译后小于2KB。如果你用的是 STC15W201S(1KB Flash),可能需要进一步裁剪,只保留 03/06 两个功能码。


六、调试建议

  1. 先用串口调试助手测试:跳过RS485,直接用USB转TTL连接STC15W201S的P3.0/P3.1,用 Modbus Poll 软件验证通信
  2. CRC校验是重灾区:发送前务必核对CRC字节顺序(低字节在前,高字节在后)
  3. 地址不匹配不响应:这是Modbus从站的基本要求,避免总线冲突
  4. 帧超时参数要跟波特率联动:高速波特率下3.5字符时间很短,超时阈值需相应减小
相关推荐
xzl042 小时前
瑞萨 FSP 和 STM32 HAL 库的启动流程核心差异
stm32·单片机·嵌入式硬件·rt-thread
芯希望2 小时前
XBLW芯伯乐XBL1507B系列3A 150kHz 40V DC-DC转换器,高效率宽输入电源解决方案
单片机·嵌入式硬件·dc-dc·工业控制·国产替代·电源管理·xblw芯伯乐
不做无法实现的梦~2 小时前
STM32 蜗轮蜗杆电机控制系统设计
stm32·单片机·嵌入式硬件
foundbug9993 小时前
STM32 上实现 Modbus-RTU
stm32·单片机·嵌入式硬件
飞睿科技3 小时前
飞睿智能5.8G毫米波雷达智能猫砂盆检测方案
嵌入式硬件·物联网·雷达·智能猫砂盆·宠物用品
小叮当⇔3 小时前
TI电源管理芯片——TPS65251RHAR手册
单片机·嵌入式硬件
leo__5204 小时前
51单片机实现读写U盘
嵌入式硬件·mongodb·51单片机
项目題供诗4 小时前
STM32-GPIO输入(四)
stm32·单片机·嵌入式硬件
我在人间贩卖青春4 小时前
ADC采集
stm32·adc