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 两个功能码。
六、调试建议
- 先用串口调试助手测试:跳过RS485,直接用USB转TTL连接STC15W201S的P3.0/P3.1,用 Modbus Poll 软件验证通信
- CRC校验是重灾区:发送前务必核对CRC字节顺序(低字节在前,高字节在后)
- 地址不匹配不响应:这是Modbus从站的基本要求,避免总线冲突
- 帧超时参数要跟波特率联动:高速波特率下3.5字符时间很短,超时阈值需相应减小