串口唤醒省电模式——高波特率下可靠接收数据说明
STC32G 系列单片机支持通过 普通 I/O 口中断 从掉电模式(STOP)唤醒 MCU。将 RXD(P3.0)配置为下降沿中断 + 唤醒使能后,串口数据到来时 MCU 即可被唤醒并接收数据。但这个流程在较高波特率时会出问题:唤醒后直接开串口接收,前几个 bit 的采样点会偏移到相邻 bit 上,导致数据错位。
为什么高速率下会错位
UART 接收器内部靠一个 16 倍波特率的过采样时钟来定位每个 bit 的中心采样点。这个采样时钟由波特率定时器的溢出脉冲分频产生。正常工作时,起始位的下降沿会使接收器内部的计数器复位,保证 8 个数据 bit 的采样点依次对准各自中心。
问题出在省电模式唤醒这个场景。MCU 进入掉电模式后主时钟停振,用于产生波特率的定时器也随之停止。唤醒时 IRCDB 寄存器会控制等待一段时间,让内部高速 IRC 频率恢复标称值后再让 CPU 开始运行。此时频率已经正确,但波特率定时器重新启动后的第一个溢出脉冲和输入数据的 bit 边界之间的相位是随机的——定时器停在了什么位置、重新启动后第一个溢出何时到来,与 RXD 线上此刻在传哪个 bit 没有任何同步关系。
这个随机相位偏移一旦产生,会固定地影响后续所有采样点。偏移量本身是一个固定的绝对时间(取决于定时器启动瞬间的计数值),这个绝对时间在不同波特率下占一个 bit 的比例不同:
| 波特率 |
每个 bit 内定时器溢出次数 |
相位偏移占 bit 宽度的比例 |
实际效果 |
| 2400 |
5000 次 |
极小 |
采样点仍在 bit 中心附近,数据正确 |
| 9600 |
1250 次 |
较小 |
部分 bit 的采样点可能落入边界判决区 |
| 115200 |
104 次 |
显著 |
采样点可能越过 bit 边界读到相邻 bit |
| 230400 |
52 次 |
显著 |
以 0xFF 为唤醒字节时可正常工作 |
| 460800 |
26 次 |
严重 |
唤醒后首有效字节丢失 |
实测结果与此一致:2400bps 下首字节完全正常,9600bps 开始出现错位。
实测数据(STC32G144K246 @48MHz,下降沿 I/O 中断唤醒):
| 波特率 |
发送数据 |
接收数据 |
首字节正确? |
| 2400 |
00 50 A5 23 32 |
00 50 A5 23 32 |
✓ 是 |
| 9600 |
00 50 A5 23 32 |
前 2 字节内容错误 |
✗ 否 |
| 115200 |
00 50 A5 23 32 |
7D E9 23 32 |
✗ 否 |
| 230400 |
00 50 A5 23 32 |
0xFF唤醒正常 |
✗ 否(0xFF唤醒可) |
| 460800 |
00 50 A5 23 32 |
7D E9 23 32 |
✗ 否 |
注:9600bps 下一个 bit 约 104μs,每个 bit 内定时器溢出 1250 次。若相位偏差几十个计数周期,占比不到 5%,但已足以让某些 bit 的采样点落入边界附近,导致误判。
处理方法:放弃第一个字节,用它换同步时间
解决问题的思路很直接——既然唤醒后定时器重启的相位不可控,那就不给它机会出错:让出第一个字节的时间不做接收,等这个字节完整传完、RXD 回到空闲高电平后,再启动波特率时钟和接收器。此时接收器从空闲状态开始监视 RXD,下一个字节的起始位下降沿能触发完整且正确的位同步。
具体流程:
- 进入休眠前:关掉波特率时钟(
T2R = 0)。注意仅关 REN 是不够的——REN=0 只是不让收到的数据写入 SBUF、不产生 RI 中断,UART 内部该检测起始位还是检测、该移位接收还是移位,状态机一直在跑。必须停掉波特率时钟才能让接收器彻底停下来。
- 下降沿唤醒:RXD 上第一个字节的起始位(下降沿)触发 P3 中断,MCU 从 STOP 模式唤醒。
- 等待停止位:ISR 中执行
while(P30==0),持续检测 RXD 电平。这个循环会一直等到 P3.0 变高——也就是第一个字节的停止位到达。无论唤醒字节是 0x00(连续 9 bit 低电平)还是 0xFF(仅起始位为低),等待时间都足够让 HIRC 和定时器充分稳定。
- 停止位到达:P3.0 恢复高电平,RXD 处于空闲状态。此时 ISR 退出等待。
- 开启接收:依次写入定时器重载值、启动波特率时钟、加若干 NOP 让时钟跑几个周期、清除 RI、置位 REN。由于 RXD 当前为空闲高电平,UART 接收器能正确地检测到下一个字节的起始位,完成位同步。
- 唤醒字节被丢弃:第一个字节已完整经过,不进入接收缓冲区。
时序关系描述:
唤醒字节在 RXD 线上依次呈现为:起始位(低)→ 8 个数据位(LSB 在前)→ 停止位(高)。起始位的下降沿触发 P3 中断,MCU 开始唤醒。ISR 中的 while(P30==0) 循环持续检测 RXD,直到停止位的高电平出现——此时第一个字节已完整结束,RXD 回到空闲高电平。ISR 退出等待,装入定时器重载值、启动波特率时钟、加 NOP 延时、清 RI、置 REN。UART 接收器从空闲高电平开始监视 RXD,下一个字节的起始位下降沿将触发完整的位同步过程,该字节及其后的所有字节均能正确接收。
波特率选型与功耗考虑
本方案需要额外丢弃一个唤醒字节,这引发了一个直观的疑问:用低速波特率(如 9600)直接接收不就不用丢字节了吗?
实际情况是:高波特率下虽然多丢一个字节,但整体通讯时间大幅缩短,MCU 能更快返回休眠,平均功耗反而更低。
以接收 32 字节有效数据为例:
| 方案 |
需传输总量 |
总耗时 |
相对耗时 |
| 9600 直收(不丢字节) |
32 字节 |
32×10×104μs =33.3ms |
100%(基准) |
| 115200(丢 1 唤醒字节) |
33 字节 |
33×10×8.68μs =2.87ms |
8.6% |
| 230400(丢 1 唤醒字节) |
33 字节 |
33×10×4.34μs =1.43ms |
4.3% |
结论:115200 方案的总耗时仅为 9600 的 8.6%。MCU 在唤醒状态下的工作电流(mA 级)持续时间越短,平均电流就越低。在间歇工作场景下,高波特率+丢弃唤醒字节方案的综合功耗显著优于低速直收方案。
定长接收说明
STC32G12K128 系列没有串口超时中断功能,因此需要兼容该型号时必须使用定长接收(收满指定字节数后设置完成标志),不能依赖超时中断判断帧结束。
参考代码(定长接收·115200bps)
以下代码基于 STC32G 数据手册 §9.7.7(使用 RxD 管脚中断唤醒省电模式)的官方例程框架,添加了 I/O 下降沿唤醒 + 主动丢弃唤醒字节的处理。已验证通过 STC32G144K246 @48MHz。
/*******************************
* STC32G 省电模式串口唤醒参考程序
* IO 中断唤醒 + 丢弃首字节
* 115200bps, 4字节定长接收, 纯寄存器操作
*******************************/
#include "stc32g144k246.h"
#include "intrins.h"
#undef main
#define FOSC 48000000UL /* 主频 48MHz */
#define BRT 115200UL /* 波特率 */
#define T2RL (unsigned int)(65536UL - (FOSC / BRT + 2UL) / 4UL)
volatile bit fRdy; /* 收到完整帧标志 */
volatile unsigned char cnt; /* 接收字节计数器 */
unsigned char xdata buf[4]; /* 接收缓冲区 */
volatile bit fBusy; /* 发送忙标志 */
/* 发送一个字节 */
void SendByte(unsigned char d)
{
while (fBusy);
fBusy = 1; SBUF = d;
}
/* 发送字符串 */
void SendStr(char *s)
{
while (*s) SendByte((unsigned char)(*s++));
}
/* 简单延时 */
void Delay(unsigned int ms)
{
unsigned int i;
do { i = FOSC / 6000; while (--i); } while (--ms);
}
void main(void)
{
unsigned char i;
/* === 系统初始化 === */
P_SW2 |= 0x80; /* 允许访问 XFR 寄存器 */
CKCON = 0; WTST = 0; /* 最高速度模式: 程序存储器无等待 */
/* 所有 IO 口设为准双向模式 */
P0M0 = 0; P0M1 = 0; P1M0 = 0; P1M1 = 0;
P2M0 = 0; P2M1 = 0; P3M0 = 0; P3M1 = 0;
IRCDB = 0x10; /* 内部 IRC 稳定等待时间 */
/* P3.0 配置为下降沿中断模式(暂不使能中断与唤醒,避免上电误触发)*/
P3NCS &= ~0xFF; /* 施密特触发输入 */
P3IM0 = 0; /* 下降沿中断模式 */
/* === UART1 初始化:定时器2做波特率发生器 === */
SCON = 0x50; /* 模式1: 8位UART, 允许接收 */
AUXR = (AUXR & ~0x0F) | 0x05; /* T2x12=1(1T模式), S1BRT=1(选T2做波特率) */
TM2PS = 0; /* 预分频=0 */
T2H = (unsigned char)(T2RL >> 8); /* 装载重载值高8位 */
T2L = (unsigned char)(T2RL); /* 装载重载值低8位 */
T2R = 1; /* 启动定时器2,产生波特率时钟 */
REN = 1; /* 允许接收 */
ES = 1; /* 使能串口中断 */
TI = 0; RI = 0; /* 清除中断标志 */
fBusy = 0; fRdy = 0; cnt = 0;
EA = 1; /* 开总中断 */
Delay(10); /* 等待串口稳定 */
SendStr("STC32G LP Wakeup OK\r\n");
while (1)
{
if (fRdy)
{
fRdy = 0;
if (buf[0] == 0x55)
{
/* 收到休眠指令 0x55:进入掉电模式 */
SendStr(">sleep\r\n");
Delay(5);
/* 关闭串口,准备休眠 */
T2R = 0; REN = 0;
/* 使能 P3.0 下降沿中断 + 掉电唤醒功能 */
P3INTF = 0; /* 清除残留中断标志 */
P3WKUE = 0x01; /* 使能 P3.0 掉电唤醒 */
P3INTE = 0x01; /* 使能 P3.0 中断 */
EA = 1;
/* 进入掉电模式 STOP */
PCON |= 0x02;
NOP(4);
/* 从掉电模式醒来后,P3 中断已被 ISR 清除使能 */
fRdy = 0; cnt = 0; ES = 1;
}
else
{
/* 普通数据:回显收到的4个字节 */
for (i = 0; i < 4; i++) SendByte(buf[i]);
}
}
}
}
/* 串口1中断服务函数 (中断号 4) */
void UART1_ISR(void) interrupt 4
{
if (TI) { TI = 0; fBusy = 0; }
if (RI)
{
RI = 0;
buf[cnt++] = SBUF;
if (cnt >= 4) { cnt = 0; fRdy = 1; }
}
}
/* P3 口中断服务函数 (中断号 40) */
void PORT3_ISR(void) interrupt 40
{
unsigned char f = P3INTF;
P3INTF = 0;
if (f & 0x01)
{
/* 等待唤醒字节的停止位结束 */
while (P30 == 0);
/* 关闭 P3 中断,重新启动串口 */
P3INTE &= ~0x01;
P3INTF = 0;
T2H = (unsigned char)(T2RL >> 8);
T2L = (unsigned char)(T2RL);
T2R = 1; RI = 0; REN = 1;
}
}
使用说明:
- 休眠指令:发送
55 xx xx xx(4 字节,首字节 0x55),MCU 收到后进入掉电模式
- 唤醒方式:发送任意 5 字节(
FF + 4字节数据),首字节 0xFF 用于唤醒并自动丢弃,后续 4 字节被正常接收并回显
- 波特率切换:修改
BRT 宏定义和对应的 T2RL 值即可
实测结果:
| 波特率 |
发送 |
接收 |
结论 |
| 115200 |
FF A5 5A 00 FF |
A5 5A 00 FF |
✅ 唤醒+回显正确 |
| 230400 |
FF A5 5A 00 FF |
A5 5A 00 FF |
✅ 唤醒+回显正确 |