资讯详情

STM8S003 GPIO模拟UART实战:IAR环境下的定时器配置与状态机实现

📅 2026/11/5 4:18:13 | 华诺云谱 👁 阅读
STM8S003 GPIO模拟UART实战:IAR环境下的定时器配置与状态机实现
简介这份资源面向在 IAR Embedded Workbench 下开发 STM8S003 的嵌入式工程师与学习者针对该芯片原生物理串口资源不足的问题给出基于 PC3、PC4 引脚配合定时器实现软件模拟串口Software UART的完整工程。内容涵盖 GPIO 推挽/开漏配置、波特率定时器设定、发送波形生成、查询与中断两种接收模式的处理逻辑以及中断服务程序中的数据收发流程适合已具备一定单片机基础、希望掌握资源受限场景下串口扩展思路的开发者参考。压缩包共 53 个文件约 418KB以 C 源码与头文件为核心辅以 IAR 工程与调试配置文件、编译中间产物及日志导入后即可编译调试。目前已有 425 人学习下载。通过研读示例代码与工程配置读者可理解模拟串口的时序实现与中断接收机制并在此基础上扩展多路模拟串口或适配不同波特率。1. STM8S003 没有独立串口时用 IAR 把 GPIO 模拟成 UART 的完整落地路径手上有一块 STM8S003 的小板子引脚排下来发现真正能用的 UART 只有一组结果被传感器和调试口同时盯上硬件串口根本不够分。这种场景在低成本项目里太常见了芯片便宜、外设少但功能需求一点没减。模拟串口就是在这种夹缝里长出来的方案——用两个普通 GPIO一个当 TX 一个当 RX靠定时器掐时间位把电平一位一位搬出去。它不占硬件 UART不挑引脚代价是吃 CPU 时序和中断响应。这篇笔记围绕 IAR 环境下 STM8S003 的模拟串口实现展开从波特率怎么算、定时器怎么配到收发状态机怎么写、示波器上波形为什么会歪全部落到能直接抄的代码和参数上。适合正在用 STM8S003 做小体积产品、被串口数量卡住、又不想换芯片的嵌入式从业者。2. 模拟串口的时间基准波特率、定时器与采样点怎么定模拟串口的本质是把「异步串行」这件事用软件重新实现一遍。硬件 UART 内部有一个波特率发生器发送时按位宽移位接收时在位的中间采样软件模拟要做的是同一件事只不过时间基准换成了 MCU 的定时器移位换成了 GPIO 电平翻转。理解这一点后面所有参数才有依据而不是抄一组数就完事。2.1 一位到底多宽从波特率到定时器计数值以常见的 9600 波特率为例一位的时间是 1/9600 ≈ 104.17 微秒。STM8S003 在 16MHz 主频下一个机器周期是 62.5 纳秒定时器 1 的计数时钟如果取 1 分频计数值就是 104.17µs / 62.5ns ≈ 1667。这个数字不是随便取的它直接决定波形每一位的宽度误差。实际工程里更常见的做法是把定时器时钟再分频让计数值落在一个整数附近减少累计误差。参数取值说明系统时钟16 MHzSTM8S003 内部 HSI 默认值也可外部晶振定时器时钟分频1TIM1 计数频率 16 MHz目标波特率9600一位约 104.17 µs理论计数值1667104.17µs × 16MHz实际写入值1667误差约 0.02%可接受采样点位中点接收时在起始位下降沿后 1.5 位处首次采样需要提醒的是模拟串口的误差是累计的。发送 10 位1 起始 8 数据 1 停止如果每位偏差 1 个计数累计就是 10 个计数约 0.6µs对 104µs 的位宽来说仍在容忍范围内但如果分频选错导致每位偏差 5%第 8 位就可能采到相邻位上去。所以定时器初值一定要按实际主频重算不能照搬别人的 1667。2.2 定时器选型TIM1 还是 TIM4STM8S003 上有 TIM116 位高级定时器和 TIM48 位基本定时器。模拟串口对定时器的要求是计数范围要覆盖一位的计数值中断要足够快且不能被其他长中断拖住。9600 波特率下 1667 的计数值8 位的 TIM4 最大只能到 255必须靠预分频把计数频率降下来比如 64 分频后计数值约 26精度就差了。所以常见做法是用 TIM1 做 1 分频精确定时TIM4 留给系统滴答或其他慢速任务。/* TIM1 初始化1 分频向上计数用于模拟串口位定时 */ void SimUart_TimerInit(void) { TIM1_DeInit(); TIM1_TimeBaseInit(0, TIM1_COUNTERMODE_UP, 1667, 0); /* 16MHz/1, 计数值1667 */ TIM1_ITConfig(TIM1_IT_UPDATE, ENABLE); /* 使能更新中断 */ TIM1_Cmd(ENABLE); } /* 中断服务每个位周期进入一次驱动收发状态机 */ #pragma vector TIM1_OVR_UIF_vector __interrupt void TIM1_UPD_ISR(void) { TIM1_ClearITPendingBit(TIM1_IT_UPDATE); SimUart_Tick(); /* 状态机推进一步 */ }这段代码的关键在TIM1_TimeBaseInit的第三个参数它就是一位的计数值。改波特率时只改这一个数比如 19200 就改成 833115200 改成 139。中断里不做具体收发只调用一个 tick 函数是为了让状态机逻辑集中、便于调试。TIM1_ClearITPendingBit必须放在最前面否则中断会反复进入这是 STM8 上很典型的一个翻车点。2.3 收发采样点为什么接收比发送难发送相对简单定时器每到一个位周期就把数据字节的对应位推到 TX 引脚上10 个周期发完一帧。接收难在同步——发送方和接收方各自跑自己的时钟没有共享时钟线接收方只能靠起始位的下降沿对齐然后在每位的中点采样。中点采样的原因是位边缘附近电平正在跳变最容易读错中点最稳。实现上接收状态机在空闲时等待 RX 引脚下降沿可以用外部中断也可以定时器轮询检测到下降沿后启动定时器第一次中断发生在 1.5 个位周期后跳过起始位并进入第 0 位中点之后每个位周期采一次采满 8 位后检查停止位是否为高。这个 1.5 位的偏移是模拟串口接收的核心参数写错就整帧错位。提示如果 RX 引脚同时接了其他外设下降沿检测可能被干扰触发建议在起始位确认时连续采样两次两次都为低才认为有效起始位。3. IAR 工程里的模拟串口实现GPIO 配置、状态机与收发函数这一章落到 IAR 工程的实际组织方式。STM8S003 的 GPIO 配置和 STM32 不一样方向寄存器 DDR、输出模式 CR1、上拉 CR2 要分开设漏一个就会出现「引脚没反应」的玄学问题。下面按发送、接收、主循环调用三块拆开写。3.1 GPIO 初始化TX 推挽输出、RX 上拉输入TX 引脚要能主动拉高拉低配成推挽输出RX 引脚要能被外部拉低配成输入并开上拉避免悬空时读到随机电平。STM8S003 的 PD5、PD6 是常用组合具体引脚按板子走线改。/* 模拟串口 GPIO 初始化TXPD5 推挽输出RXPD6 上拉输入 */ void SimUart_GpioInit(void) { /* TX: PD5 输出推挽低速 */ PD_DDR | (1 5); /* 方向输出 */ PD_CR1 | (1 5); /* 推挽模式 */ PD_CR2 ~(1 5); /* 低速 */ /* RX: PD6 输入上拉 */ PD_DDR ~(1 6); /* 方向输入 */ PD_CR1 | (1 6); /* 上拉输入 */ PD_CR2 ~(1 6); PD_ODR | (1 5); /* TX 空闲态为高 */ }PD_DDR决定方向PD_CR1在输出时选推挽/开漏、在输入时选上拉/浮空PD_CR2管速度或中断。三个寄存器各管一摊这是 STM8 和 STM32 最大的手感差异。TX 空闲态必须置高因为 UART 协议里空闲是高电平起始位才是低电平如果初始化后 TX 是低接收方会误以为一直有起始位。2.2 发送状态机一个字节怎么拆成 10 个位周期发送用一个结构体维护当前字节、位计数和忙标志。调用发送函数时把字节装进去、拉低 TX 产生起始位、启动定时器之后每个 tick 推一位。typedef struct { uint8_t data; /* 待发送字节 */ uint8_t bitIdx; /* 当前位索引 0~9 */ uint8_t busy; /* 忙标志 */ } SimUartTx_t; static SimUartTx_t tx; /* 发送一个字节阻塞式启动非阻塞式完成 */ void SimUart_SendByte(uint8_t byte) { while (tx.busy); /* 等待上一字节发完 */ tx.data byte; tx.bitIdx 0; tx.busy 1; PD_ODR ~(1 5); /* 起始位拉低 */ TIM1_SetCounter(0); /* 重置计数对齐位宽 */ TIM1_Cmd(ENABLE); } /* 定时器 tick 中调用每位推一次电平 */ void SimUart_TxTick(void) { if (!tx.busy) return; tx.bitIdx; if (tx.bitIdx 8) { /* 数据位LSB 先发 */ if (tx.data 0x01) PD_ODR | (1 5); else PD_ODR ~(1 5); tx.data 1; } else if (tx.bitIdx 9) { PD_ODR | (1 5); /* 停止位拉高 */ } else { tx.busy 0; /* 一帧结束 */ TIM1_Cmd(DISABLE); } }位索引用 0 到 9 共 10 个周期第 0 周期是起始位在 SendByte 里已经拉低第 1 到 8 周期推 8 个数据位第 9 周期推停止位。数据位 LSB 先发这是 UART 标准写反了收到的就是位翻转的乱码。TIM1_SetCounter(0)在启动时重置计数保证起始位宽度和其他位一致否则第一位的宽度会受上一次计数残留影响。3.3 接收状态机下降沿对齐与中点采样接收用外部中断捕获起始位然后交给定时器逐位采样。STM8S003 的 PD 口支持外部中断配置时注意 CR2 寄存器要开中断位。typedef struct { uint8_t data; uint8_t bitIdx; uint8_t active; } SimUartRx_t; static SimUartRx_t rx; /* PD 口外部中断检测起始位下降沿 */ #pragma vector EXTI_PORTD_vector __interrupt void EXTI_PD_ISR(void) { if (!rx.active !(PD_IDR (1 6))) { rx.active 1; rx.bitIdx 0; rx.data 0; TIM1_SetCounter(0); TIM1_Cmd(ENABLE); /* 启动位定时 */ } } /* 定时器 tick 中调用1.5 位后首次采样之后每位一次 */ void SimUart_RxTick(void) { static uint8_t half 0; if (!rx.active) return; if (!half) { /* 第一次中断1.5 位处跳过半个位 */ half 1; return; } if (rx.bitIdx 8) { if (PD_IDR (1 6)) rx.data | (1 rx.bitIdx); rx.bitIdx; } else { /* 停止位检查 */ if (PD_IDR (1 6)) { SimUart_RxCallback(rx.data); /* 交给上层处理 */ } rx.active 0; half 0; TIM1_Cmd(DISABLE); } }这里用了一个half标志实现 1.5 位偏移外部中断在下降沿触发定时器第一次溢出是 1 个位周期后此时还在起始位内再等一个周期才到第 0 位中点所以第一次 tick 直接返回第二次才开始采样。这个细节如果漏掉采到的就是起始位而不是数据位整帧右移一位表现为收到的字节总是原值的两倍或带固定偏移。注意接收期间如果系统里有其他中断耗时超过半个位宽9600 下约 52µs采样点会漂移。常见做法是在接收窗口内关掉非必要中断或者把模拟串口接收放到最高优先级。4. 避坑与排查模拟串口在 STM8S003 上最容易翻车的五件事模拟串口没有硬件兜底所有时序问题都会直接体现在波形和收到的数据上。下面五条是实际调试中反复出现的按「现象 → 原因 → 解决」整理。4.1 收到的全是 0xFF 或 0x00现象上位机收到的每个字节都是 0xFF 或 0x00偶尔夹杂正确值。原因通常是 TX 引脚方向或模式配错推挽没开导致引脚拉不动或者空闲态没置高让接收方一直判为帧错误。解决用万用表量 TX 空闲电压应为 3.3V 左右检查PD_DDR和PD_CR1是否都置位确认PD_ODR初始化时置高。4.2 波特率偏差导致后几位错位现象短帧1 到 2 字节正常长帧从第 5、6 位开始出错。原因是定时器计数值按整数取整后存在固定偏差帧越长累计越大。解决按实际主频重算计数值优先选能让计数值接近整数的分频组合必要时在发送第 5 位后微调一次计数把累计误差拉回。4.3 中断里调用发送函数导致死等现象程序卡死看门狗复位。原因是在定时器中断里调用了阻塞式SimUart_SendByte而它内部while(tx.busy)等待的正是当前中断推进的状态形成自锁。解决中断里只做 tick发送函数只在主循环调用或者把发送改成纯状态机调用即返回。4.4 接收被其他中断打断丢位现象接收偶尔丢一个字节或某位翻转。原因是高优先级中断比如 ADC 完成耗时超过半个位宽采样点被推迟。解决接收窗口内临时关闭非必要中断或把 TIM1 中断优先级提到最高实在避不开就把波特率降到 4800位宽翻倍容忍度更高。4.5 IAR 优化等级改变后时序变了现象Debug 版正常Release 版波形变窄或变宽。原因是高优化等级下 GPIO 操作被重排或合并位翻转之间的指令周期数变了。解决对时序敏感的 GPIO 操作加volatile关键段用__disable_interrupt()保护或者把优化等级固定在 Low 并实测波形确认。5. 用示波器和回环测试验证模拟串口一套可复用的验收流程代码写完不等于能用模拟串口尤其如此因为它的正确性完全依赖时序。我一般会走两步验证先做 TX 单端波形检查再做 TX-RX 回环压力测试。第一步用示波器抓 TX 引脚发一个 0x55二进制 01010101这个值高低交替最容易看出位宽是否均匀。触发设在下降沿时基设 100µs/格正常应该看到起始位低、8 位交替、停止位高每位宽度一致。如果某一位明显偏宽或偏窄回去查定时器计数值和中断响应时间。第二步把 TX 和 RX 用杜邦线短接做自发自收。写一个测试循环连续发 0x00 到 0xFF 共 256 个字节接收端比对统计错误数。这个测试能暴露累计误差和中断冲突比单发几个字节可靠得多。/* 回环自测TX-RX 短接遍历 0x00~0xFF 并统计错误 */ void SimUart_LoopbackTest(void) { uint8_t i, err 0; for (i 0; i 255; i) { SimUart_SendByte(i); while (tx.busy); /* 等发完 */ if (SimUart_GetRxByte() ! i) /* 取接收结果比对 */ err; } /* err 应为 0非 0 说明时序或采样点有问题 */ }跑这个测试时有个细节发送和接收共用同一个定时器如果发送还没结束就启动接收两者会抢 tick。常见做法是发送完成后延时一个位周期再开接收或者干脆用两个定时器分别管收发。STM8S003 资源紧张时单定时器分时复用也能跑但要在状态机里明确当前是发送态还是接收态不能同时置位。还有一个容易被忽略的点回环测试通过不代表对接真实设备没问题。真实设备的波特率误差、电平幅度、线缆容抗都会影响结果。我习惯在回环通过后再拿一个 USB 转串口模块对发至少 1000 帧确认长时间运行不丢字节才算验收完成。从那以后我每次做模拟串口都强制先跑 0x55 波形再看回环错误数两步不过不往下写业务逻辑。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑