尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STC32G双串口中断收发实战:从寄存器配置到环形缓冲

STC32G双串口中断收发实战:从寄存器配置到环形缓冲 简介面向STC32G单片机开发者这份C语言工程示例来自STC32G-DEMO-CODE参考代码聚焦串口1与串口2的中断模式收发解决双串口同时与电脑通信测试中常见的中断配置与数据交互问题。压缩包共10个文件主要包含C源码、汇编源文件、uvproj/uvopt工程文件、烧录hex及M51列表文件整体仅25KB体积小巧、结构清晰适合初学者直接导入Keil学习也方便工程师按需裁剪参考。目前已有553人学习下载适用于STC32G入门、课堂实验及项目前期验证。通过示例可掌握双串口独立初始化、中断使能与收发完成回调等关键流程配合编译生成的hex文件可直接烧录至开发板与电脑串口助手进行收发测试边跑边对照源码理解时序逻辑与调试排错思路。包含两个独立工程示例可对照学习不同代码组织方式结合官方手册阅读能更深入理解寄存器配置细节从而节省从零搭建工程的时间。1. 先在 STC32G 上把串口1和串口2 的中断收发跑通再谈别的拿到一块 STC32G 最小系统板多数人的第一件事不是点灯而是让串口先和电脑说上话。等真正把串口1、串口2 都接上才会发现 C语言 里用查询方式做串口收发在 115200 波特率下有多浪费 CPU一个 while 等 RI 标志位的循环就能把主循环堵死更别说双串口同时有数据。标题里的 STC32G-DEMO-CODE-220311kw 是 STC 官方例程包的版本标识编号 10 这一份对应的正是串口1、串口2 中断模式与电脑收发测试。下面按顺序把整条路走一遍中断模式为什么是标准答案、两个串口的寄存器和中断入口怎么配、电脑端用什么工具验证、以及最容易翻车的几个点。适合刚用 Keil C251 建工程的新手也适合想确认双串口共用定时器2 时坑在哪里的老手。2. STC32G 双串口中断模式引脚映射、波特率来源与中断入口串口中断模式本质上是把“收到一个字节”和“发送完一个字节”变成两个硬件事件由 RI/TI 标志触发中断服务函数里只管搬数据业务逻辑留在主循环。落地之前要理清三件事引脚在哪儿、波特率谁产生、中断进哪个入口。2.1 串口1 与串口2 的默认引脚和切换位STC32G 的串口1 默认在 P3.0RXD1/P3.1TXD1串口2 默认在 P4.6RXD2/P4.7TXD2。这两个默认组合同时也是最容易接错的点不少板子把串口2 引到了 P1.0/P1.1如果初始化时没注意引脚切换位插上线自然收不到任何数据。串口1 的引脚组由 P_SW1 的 S1_S 位控制可以把 RXD1/TXD1 挪到 P3.6/P3.7 或 P1.6/P1.7串口2 由 P_SW2 的 bit0 控制置 1 后切到 P1.0/P1.1。例程里一般默认不动这些位但改工程时很容易踩到。还有两个隐藏前提。一是 P_SW2 的高位 EAXFR 控制扩展 SFR 的访问如果你在初始化里还要配置 PxM0/PxM1 这类扩展寄存器需要先把 EAXFR 位置 1否则写进去的值不生效。二是 P3.0/P3.1 同时也是 ISP 串口下载用的引脚千万别把它设成推挽输出去驱动外部负载否则“串口烧写失败”会来得莫名其妙。引脚模式方面串口收发保持默认的准双向模式就能跑没必要改成推挽。2.2 波特率三种来源定时器1、定时器2、BRG 怎么选STC32G 的波特率源比传统 51 灵活得多。串口1 可以选定时器1、定时器2 或独立的 BRG串口2 一般用定时器2 或 BRG。常见做法是串口1 挂定时器1串口2 挂定时器2两个定时器各管一个串口互不牵连这是官方例程里最稳的组合。三种来源的计算公式主频 SYSclkSMOD0定时器18 位自动重装T1x121 时波特率 SYSclk / (32 × (256 − TH1))定时器2波特率发生器模式T2x121 时波特率 SYSclk / (4 × (65536 − RCAP2))BRG 内部波特率发生器结构和定时器2 相同好处是把两个定时器都省出来以 11.0592MHz 主频、115200 波特率为例TH1 取 0xFDRCAP2 取 0xFFE8误差为 0。要注意主频不是 11.0592MHz 时必须按公式重算比如 24MHz 内部 IRC 下用定时器1 跑 115200误差会到 8.5%直接乱码。一个关键约束定时器2 可以同时给两个串口做波特率源但代价是两个串口的波特率必须完全一样。想让串口1 跑 115200、串口2 跑 9600就必须拆开——一个用定时器1一个用定时器2或者其中一个改用 BRG。这是例程里看不到、但改需求时立刻会撞上的边界条件。对比项串口1 (UART1)串口2 (UART2)默认引脚P3.0(RXD) / P3.1(TXD)P4.6(RXD2) / P4.7(TXD2)数据寄存器SBUFS2BUF控制寄存器SCONS2CONKeil C251 中断号48中断使能位IE 寄存器的 ESIE2 寄存器的 ES2常用波特率源定时器1 / 定时器2 / BRG定时器2 / BRG收发标志RI / TI软件清零S2RI / S2TI软件清零2.3 中断入口与标志位RI 和 TI 都要手动清UART1 对应 Keil C251 的 interrupt 4UART2 是 interrupt 8入口号和使能位不同服务函数里做的事完全一样收到数据先读 SBUF/S2BUF 再清 RI发送完成清 TI 并继续发下一字节。这里有个容易翻车几十次的细节RI 和 TI 共用同一个中断入口服务函数里必须先判断是哪个标志置位再分别处理只清一个会导致另一个标志永远挂着中断退不出去。S2RI/S2TI 同样必须软件清零别指望硬件自动恢复。另外不同版本头文件里 IE2 的 ES2 位位置可能不同常见是 0x02 或 0x04编译报错就去 stc32g.h 里对一下位定义不要硬背地址。3. 用 Keil C251 写最小双串口中断收发 Demo原理清楚了直接落代码。下面这套是能编译、能烧录、能回显的最小实现串口1 收到的字节原样回显并转发到串口2串口2 收到的字节回显到串口2方便电脑端分开验证两路。3.1 Keil C251 工程与 STC32G 头文件的三个检查点STC32G 是 251 内核必须用 Keil C251拿 C51 工程硬改必然编译不过。建工程时检查三处第一Device 里要能看到 STC32G12K128看不到就用 STC-ISP 软件里的“添加型号和头文件到 Keil”功能补一次器件库第二Options for Target 的 Output 页勾选 Create HEX File否则没东西可烧第三头文件统一用 STC32G.H网上流传的 C51 版 STC8G.H 位定义对不上寄存器写错位是最隐蔽的翻车点。内存模型按例程默认走一般是 Large XSmall别手滑改成 Small否则变量默认塞进 data 区稍微用点缓冲就溢出。220311kw 这个版本年代较早如果 Keil 版本太新报器件不识别优先检查器件库而不是换编译器版本。3.2 初始化代码串口1 挂定时器1串口2 挂定时器2#include STC32G.H void UART1_Init(void) { P_SW1 ~0xC0; // S1_S[1:0]00串口1 留在 P3.0/P3.1 SCON 0x50; // 方式18位可变波特率REN1 允许接收 TMOD (TMOD 0x0F) | 0x20; // 定时器1 设为 8 位自动重装 AUXR | 0x40; // T1x121定时器1 按 1T 计数 TH1 0xFD; TL1 0xFD; // 115200 11.0592MHzSMOD0 TR1 1; // 启动定时器1 ES 1; // 打开串口1 中断 } void UART2_Init(void) { P_SW2 ~0x01; // S2_S0串口2 留在 P4.6/P4.7 S2CON 0x50; // 方式1S2REN1 允许接收 T2L 0xE8; T2H 0xFF; // RCAP20xFFE8115200 11.0592MHz AUXR | 0x14; // T2x1211TT2R1启动定时器2 IE2 | 0x02; // ES21打开串口2 中断 }SCON 0x50 拆开看是 SM00、SM11、REN1对应 8 位可变波特率方式加接收使能S2CON 的位定义和 SCON 完全同构。AUXR 里 T1x12 和 T2x12 决定定时器是按 1T 还是 12T 计数这个位不置 1上面算好的重载值全部无效实际波特率会掉到 1/12。改成其他波特率时按第 2 章公式算重载值目标波特率TH1T1x121RCAP2H:RCAP2LT2x12196000xDC0xFE:0xE0192000xEE0xFF:0x70576000xFA0xFF:0xD01152000xFD0xFF:0xE8表的前提是主频 11.0592MHz、SMOD0。如果板子实际跑的是 24MHz 内部 IRC推荐在 STC-ISP 烧录时把“输入用户程序运行时的 IRC 频率”改成 11.0592MHz再烧一遍比改代码里的重载值省事。3.3 中断服务函数与环形缓冲接收进队列发送靠 TI 接力中断里直接做业务是最常见的坏习惯。正确结构是中断只负责把字节放进环形缓冲主循环再取出来处理发送则反过来主循环把字节放进发送队列空闲时直接启动第一字节剩下的由 TI 中断接力发送。#define RBUF_SIZE 64 // 环形缓冲容量两个串口各一套 volatile u8 u1_rxbuf[RBUF_SIZE], u1_txbuf[RBUF_SIZE]; volatile u8 u1_rx_head, u1_rx_tail; volatile u8 u1_tx_head, u1_tx_tail; volatile bit u1_tx_busy; void UART1_Isr() interrupt 4 { if (RI) // 收到一个字节 { RI 0; u8 n (u8)((u1_rx_head 1) % RBUF_SIZE); if (n ! u1_rx_tail) // 缓冲不满才收满了丢弃 { u1_rxbuf[u1_rx_head] SBUF; u1_rx_head n; } } if (TI) // 发送完成 { TI 0; if (u1_tx_head ! u1_tx_tail) // 队列里还有字节 { SBUF u1_txbuf[u1_tx_tail]; u1_tx_tail (u1_tx_tail 1) % RBUF_SIZE; } else { u1_tx_busy 0; // 队列空了发送状态机回到空闲 } } } void UART1_SendByte(u8 dat) { if (!u1_tx_busy) // 空闲时直接启动第一字节 { u1_tx_busy 1; SBUF dat; return; } u8 n (u8)((u1_tx_head 1) % RBUF_SIZE); if (n ! u1_tx_tail) // 队满则丢避免中断里死循环 { u1_txbuf[u1_tx_head] dat; u1_tx_head n; } }环形缓冲用 head 指向下一个写入位置、tail 指向下一个读出位置空和满都靠 head tail 判断所以必须浪费一个存储单元来区分。发送函数“空闲直接发、忙时入队”的设计保证了第一个字节不需要等中断后续字节靠 TI 中断自动接力主循环里连续调用 UART1_SendByte 不会阻塞。UART2 的代码完全同构把寄存器换成 S2CON/S2BUF、中断号换成 8 即可。主循环里的消费逻辑长这样void main(void) { EA 1; // 开总中断 UART1_Init(); UART2_Init(); while (1) { while (u1_rx_head ! u1_rx_tail) // 串口1 回显并转发串口2 { u8 ch u1_rxbuf[u1_rx_tail]; u1_rx_tail (u1_rx_tail 1) % RBUF_SIZE; UART1_SendByte(ch); UART2_SendByte(ch); } // 串口2 的消费循环同构收到就回显到串口2 } }主循环里读 u1_rxbuf 时关不关中断都行因为 head 只有中断写、tail 只有主循环写单生产者单消费者的环形缓冲天然安全。这也是这个结构最省心的地方。4. 串口调试助手与电脑收发测试接线、参数和失败排查代码烧进去之后真正的验证在电脑端。串口调试助手选哪个都行关键是接线、参数和判定方法要对否则很容易把板子问题误判成代码问题。4.1 USB 转 TTL 接线与 CH340 驱动检查电脑没有原生串口一般用 CH340 或 FTDI 芯片的 USB 转 TTL 小板系统里会多出一个虚拟串口 COMx。接线必须交叉并共地CH340 端STC32G 板端TXDP3.0 (RXD1)RXDP3.1 (TXD1)GNDGNDTXD 接 RXD、RXD 接 TXD两头一样接必收不到。GND 不共串口通信会时好时坏。电平要和板子供电一致3.3V 供电的板子别硬怼 5V TTL 电平。插上之后如果设备管理器里“端口(COM和LPT)”没有出现新 COM 口优先怀疑 CH340 驱动没装好其次是线的问题——很多 USB 线只充电不传数据换线比折腾驱动更快。4.2 串口助手参数与 HEX 显示打开 XCOM、SSCOM 或同类工具选择正确的 COM 口参数设为 115200、8 位数据、无校验、1 位停止位、无流控。串口号不用猜设备管理器里看是 COM 几就是几。显示区勾选 HEX 显示方便对着字节核对发送区如果勾了“加回车换行”发一个 A 实际会发出 0x41 0x0D 0x0A 三个字节回显也应该一模一样。4.3 三个测试用例单字节回显、0x00~0xFF 压力、双串口转发第一个用例是单字节回显。助手发一个 A板子回一个 A说明中断接收和发送链路通了。第二个用例是连续 0x00~0xFF 共 256 字节的循环压力测试用脚本比用鼠标点更可靠import serial ser serial.Serial(COM7, 115200, timeout2) payload bytes(range(256)) ok err 0 for i in range(100): ser.reset_input_buffer() # 清掉上次残留保证对账干净 ser.write(payload) back ser.read(256) # 等 256 字节超时 2 秒返回 if len(back) 256 and back payload: ok 1 continue err 1 pos next((j for j in range(256) if j len(back) or back[j] ! payload[j]), -1) print(fround {i}: rcvd {len(back)} bytes, first mismatch at {pos}) print(fok{ok}, err{err}) ser.close()timeout2 是防止死等的兜底不是期待它真的等 2 秒reset_input_buffer 保证每一轮从空缓冲开始对账。如果 err 不为 0先把板子上环形缓冲的容量调大或者提高主循环的消费频率排除溢出后再怀疑波特率误差。第三个用例验证双串口独立工作串口1 发数据板子回显到串口1 的同时转发到串口2用第二根 USB 转 TTL 接 P4.6/P4.7 就能看到串口2 也有数据。如果两个串口波特率相同更省事的做法是把板上 P3.1TXD1直接短接到 P4.6RXD2串口1 一发数据串口2 的中断就会收到不需要第二根线。4.4 串口烧写失败与回显异常的排查表现象优先检查顺序设备管理器没有 COM 口装 CH340 驱动换 USB 数据线换电脑 USB 口串口烧写失败 / 超时先点“下载/编程”再给目标板上电下载波特率降到 57600 或 38400确认型号选的是 STC32G 系列回显乱码波特率不匹配主频不是 11.0592MHz 但按 11.0592 算重载值HEX 和文本显示混淆只能收不能发 / 发一次卡死TI 中断里没清标志或发送队列溢出后 u1_tx_busy 没复位串口2 完全没反应P4.6/P4.7 是否被复用P_SW2 是否被改成 P1.0/P1.1S2REN 位没置 1收到数据偶尔丢字节环形缓冲太小主循环消费太慢下载时 IRC 频率与代码里的 FOSC 不一致烧写失败里最常见的是“点了下载但板子没重新上电”。STC32G 的 ISP 流程是电脑端先进入等待状态目标板断电再上电才进入烧录模式很多新手卡在这一步。另外烧录用的是 P3.0/P3.1如果这块板子同时接了 CH340 和别的外设下载时先把外设断开。5. 中断收发框架升级溢出计数、空闲判帧与自环验证回显跑通只是起点距离“能上线”还差三件事发送背压的可见性、接收按帧切分、以及用统计手段替代目测。5.1 发送队列的空闲启动机制与溢出计数第 3 章的发送结构已经具备可上线基础但要加一个溢出计数器否则压力测试丢数据时你只能靠猜。在 ISR 的接收分支里缓冲满丢弃字节时把 u1_rx_overflow 自增主循环定期通过串口上报这个值。同时把缓冲容量改成 2 的幂用位与替代取模中断里的开销更小#define RBUF_SIZE 64 // 必须是 2 的幂 #define RBUF_MASK (RBUF_SIZE - 1) // 入队时 u1_rxbuf[u1_rx_head] SBUF; u1_rx_head (u1_rx_head 1) RBUF_MASK; // 原本的 % RBUF_SIZE 全部换成 RBUF_MASK语义等价溢出计数器 位与运算配合压力测试时能精确知道“丢了几字节、丢在哪个环节”而不是看到 err 计数后无从下手。5.2 接收按帧处理用空闲窗口判帧字节流进来之后业务层需要知道“一帧到没到”。固定长度帧很好处理长度不定时常用空闲窗口法超过 3 字节时间没有新数据就认为上一帧结束。用一个 1ms 的时基计数实现volatile u8 g_tick_1ms; // 由定时器0 或系统 tick 递增 u8 frame_buf[64]; u8 frame_len; u8 Frame_Feed(u8 ch) { static u16 last_tick; u16 now g_tick_1ms; if ((u16)(now - last_tick) 3) // 空闲超过 3ms开新帧 frame_len 0; last_tick now; if (frame_len sizeof(frame_buf)) frame_buf[frame_len] ch; return (frame_len 2) ? 1 : 0; // 这里按实际帧长条件改 }115200 波特率下 1 字节约 87µs3ms 空闲窗口足够容纳正常数据流中的停顿又不会把两帧数据错误拼成一帧。窗口大小和波特率强相关9600 波特率下建议放宽到 10ms 以上。5.3 用数据统计验证误码而不是看着像没问题最后一步是把第 4 章的 Python 脚本改成长时间双工模式跑 30 分钟以上记录 err 计数同时板子周期性上报 u1_rx_overflow。两个计数都保持 0才说明收发链路真正干净。虚拟串口在实际环境里的表现和真实串口差异很大USB 转接芯片内部缓冲和操作系统驱动调度会把偶发溢出放大短时间测试根本看不出来。如果只想快速验证板级硬件把 P3.1TXD1短接到 P4.6RXD2做板上自环串口2 能完整收到串口1 发出的 256 字节循环序列基本就能排除焊接和引脚映射问题剩下的都是软件层的事。本文还有配套的精品资源点击获取
返回列表