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