恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM8S003 GPIO模拟串口:IAR实现软UART发送与接收
首页
资讯中心
/
STM8S003 GPIO模拟串口:IAR实现软UART发送与接收
STM8S003 GPIO模拟串口:IAR实现软UART发送与接收
发布时间:2026/10/14 18:34:21
简介这份资源面向在IAR Embedded Workbench下开发STM8S003的嵌入式工程师与学习者针对该芯片原生物理串口资源不足的问题给出基于PC3、PC4引脚配合定时器实现软件模拟UART的完整工程。压缩包共53个文件约418KB以C源码、头文件与IAR工程配置为主同时包含编译输出、调试配置及日志等辅助文件可直接导入IAR打开编译。工程内提供发送与接收函数示例覆盖GPIO推挽/开漏配置、定时器波特率设置、查询接收与中断接收两种模式并附有README说明导入与运行注意事项。目前已有423人学习下载适合希望掌握资源受限MCU串口扩展思路、理解UART时序软件模拟原理的开发者参考也可在此基础上扩展多路模拟串口或适配不同波特率。1. 当硬件串口不够用用 IAR 在 STM8S003 上把 GPIO 变成串口手上这块 STM8S003F3P6 只有 8KB Flash、1KB RAM价格便宜到可以随便往板子上怼但它的原生 UART 只有一组。当你同时要接一个无线模块、又要留一路调试口打印日志的时候硬件串口立刻就不够分了。更难受的是STM8S003 的引脚复用非常有限想再找一个带 UART 功能的备用引脚基本没戏。这时候模拟串口Software UART / Bit-Banging UART就成了唯一出路——用两个普通 GPIO一个做 TX 一个做 RX靠定时器精确控制每一位的时序在软件层面把串口协议“拼”出来。IAR for STM8 是这套方案里最顺手的工具链编译效率高、寄存器查看方便配合 ST-Link 调试能直接把时序问题定位到微秒级。这篇笔记面向的是已经能用 IAR 点亮 LED、但对模拟串口还没下过手的嵌入式从业者我会把发送和接收两条路都拆开讲清楚包括定时器选型、中断优先级、波特率误差计算以及那些只有真正烧录跑过才会遇到的坑。2. 模拟串口的核心原理与 IAR 工程配置2.1 异步串口的位时序到底怎么拆异步串口在没有时钟线的情况下靠的是双方约定好的波特率来对齐每一位。一个完整的字节帧由起始位、8 位数据位、停止位组成空闲时线路保持高电平。接收方检测到下降沿后等待 1.5 个位周期再采样第一位数据之后每隔 1 个位周期采样一次连续采 8 次最后等 1 个位周期确认停止位。这个“等 1.5 位再采”的策略是为了让采样点落在每一位的中间位置留出最大的容错窗口。模拟串口要做的就是用定时器产生这个位周期。以 9600bps 为例一个位周期是 104.17 微秒。STM8S003 跑 16MHz 主频时一个机器周期是 62.5 纳秒定时器计数 1667 次刚好接近 104.17 微秒。但 1667 不是整数实际会产生误差。这个误差累积到第 10 位停止位时如果超过半个位周期接收就会出错。所以波特率越高对定时器精度的要求越苛刻。常见做法是优先选 9600 或 4800 这种低位速给误差留足余量。发送端相对简单因为它自己控制时序不需要采样对齐。只要按位周期依次把数据位推出去就行。接收端才是真正的难点因为外部信号的到来时刻是随机的必须用中断捕获起始沿然后靠定时器中断在精确的时刻采样。2.2 定时器选型TIM1 还是 TIM4STM8S003 内部有三个定时器TIM1 是 16 位高级定时器TIM2 是 16 位通用定时器TIM4 是 8 位基本定时器。模拟串口一般用 TIM4 做位周期定时原因是它结构简单、中断向量独立不会和其他外设抢资源。TIM4 是 8 位向上计数器配合 16 位预分频器可以灵活地产生各种位周期。配置 TIM4 的关键参数是预分频值PSCR和自动重载值ARR。计算公式是位周期 (PSCR 1) × (ARR 1) / 主频以 16MHz 主频、9600bps 为例位周期约 104.17 微秒。取 PSCR 15则计数频率为 1MHz每个计数 1 微秒。ARR 取 104实际位周期为 105 微秒误差约 0.8%。这个误差在 10 位帧内累积约 8 微秒远小于半个位周期52 微秒完全可用。如果主频是 8MHzPSCR 取 7 得到 1MHz 计数频率ARR 同样取 104。如果主频是 16MHz 但想要 115200bps位周期只有 8.68 微秒ARR 只能取 8 或 9误差会超过 3%这时候就需要评估接收方能不能容忍了。2.3 IAR 工程的关键选项设置在 IAR 里新建 STM8 工程后有几个选项直接影响模拟串口能不能跑起来。首先是芯片型号要选对STM8S003F3 的 Flash 是 8KBRAM 是 1KB选错了链接器会报空间不足。其次是优化等级模拟串口的中断服务函数对时序敏感建议用 Medium 或 Low 优化High 优化可能会把关键延时循环优化掉。时钟配置方面STM8S003 默认用内部 16MHz RC 振荡器精度约 ±1%对 9600bps 来说够用。如果板上焊了外部晶振可以在CLK_CKDIVR寄存器里切换到外部时钟源精度会更好。中断向量表要确认 TIM4 的中断入口地址正确IAR 的stm8s_it.c文件里默认有TIM4_UPD_OVF_IRQHandler的弱定义需要在自己的代码里重写。还有一个容易忽略的点IAR 的printf默认走硬件 UART如果要用模拟串口打印调试信息需要重定向putchar函数把数据交给模拟串口的发送函数。这个后面会具体讲。3. 发送端实现从 GPIO 翻转到底层驱动封装3.1 发送一个字节的完整代码发送端的逻辑很直接拉低 TX 引脚产生起始位然后按位周期依次输出 8 位数据低位先发最后拉高产生停止位。下面是一个基于 TIM4 查询方式的发送函数不占用中断资源#include stm8s.h #define TX_PIN GPIO_PIN_5 #define TX_PORT GPIOD // 微秒级延时基于 TIM4 查询 static void delay_us(uint16_t us) { TIM4-CNTR 0; TIM4-ARR 15; // 16MHz / 16 1MHz16 个计数约 1us TIM4-PSCR 0x0F; // 预分频 16 TIM4-CR1 | TIM4_CR1_CEN; while (us--) { TIM4-CNTR 0; while ((TIM4-SR1 TIM4_SR1_UIF) 0); TIM4-SR1 ~TIM4_SR1_UIF; } TIM4-CR1 ~TIM4_CR1_CEN; } // 发送一个字节波特率 9600 void soft_uart_send_byte(uint8_t data) { uint8_t i; // 起始位 GPIO_WriteLow(TX_PORT, TX_PIN); delay_us(104); // 8 位数据低位先发 for (i 0; i 8; i) { if (data 0x01) GPIO_WriteHigh(TX_PORT, TX_PIN); else GPIO_WriteLow(TX_PORT, TX_PIN); data 1; delay_us(104); } // 停止位 GPIO_WriteHigh(TX_PORT, TX_PIN); delay_us(104); }这段代码里delay_us用 TIM4 做单次定时每次重新装载计数器并等待更新标志。soft_uart_send_byte按起始位、8 位数据、停止位的顺序翻转 GPIO。注意数据是低位先发这和标准 UART 一致。delay_us(104)对应 9600bps 的位周期如果改成 4800bps 就改成 208。这个实现有个明显问题发送期间 CPU 被完全占用无法响应其他中断。如果系统里还有别的实时任务就需要改成中断驱动的方式——把每一位的发送放到 TIM4 中断里主循环只管把数据塞进发送缓冲区。3.2 中断驱动的发送缓冲队列中断驱动发送的核心思路是维护一个环形缓冲区主循环调用发送函数时只负责把数据放进缓冲区并启动定时器后续每一位的翻转都在 TIM4 中断里完成。这样 CPU 占用率大幅降低。#define TX_BUF_SIZE 64 static volatile uint8_t tx_buf[TX_BUF_SIZE]; static volatile uint8_t tx_head 0; static volatile uint8_t tx_tail 0; static volatile uint8_t tx_bit_cnt 0; static volatile uint8_t tx_data 0; static volatile uint8_t tx_busy 0; // 把数据放入发送缓冲区 void soft_uart_write(uint8_t data) { uint8_t next (tx_head 1) % TX_BUF_SIZE; while (next tx_tail); // 缓冲区满则等待 tx_buf[tx_head] data; tx_head next; if (!tx_busy) { tx_busy 1; // 启动第一个字节的发送 tx_data tx_buf[tx_tail]; tx_tail (tx_tail 1) % TX_BUF_SIZE; tx_bit_cnt 0; GPIO_WriteLow(TX_PORT, TX_PIN); // 起始位 TIM4-CNTR 0; TIM4-CR1 | TIM4_CR1_CEN; } } // TIM4 中断服务函数 INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) { TIM4-SR1 ~TIM4_SR1_UIF; if (tx_bit_cnt 8) { if (tx_data 0x01) GPIO_WriteHigh(TX_PORT, TX_PIN); else GPIO_WriteLow(TX_PORT, TX_PIN); tx_data 1; tx_bit_cnt; } else if (tx_bit_cnt 8) { GPIO_WriteHigh(TX_PORT, TX_PIN); // 停止位 tx_bit_cnt; } else { // 当前字节发完检查缓冲区 if (tx_tail ! tx_head) { tx_data tx_buf[tx_tail]; tx_tail (tx_tail 1) % TX_BUF_SIZE; tx_bit_cnt 0; GPIO_WriteLow(TX_PORT, TX_PIN); // 下一个起始位 } else { tx_busy 0; TIM4-CR1 ~TIM4_CR1_CEN; // 关闭定时器 } } }环形缓冲区的大小TX_BUF_SIZE取 64 是经验值太小容易在突发发送时丢数据太大浪费 RAM。STM8S003 只有 1KB RAM缓冲区不建议超过 128 字节。tx_busy标志用来判断当前是否正在发送避免重复启动定时器。中断里先清更新标志再根据tx_bit_cnt的状态决定输出哪一位。停止位发完后如果缓冲区还有数据直接加载下一个字节并重新拉低起始位实现连续发送。3.3 重定向 printf 到模拟串口调试阶段最方便的还是用printf打印变量。IAR 里printf最终会调用putchar所以只要重写putchar就能把输出重定向到模拟串口#include stdio.h int putchar(int c) { soft_uart_write((uint8_t)c); return c; }这样调用printf(adc%d\r\n, val)时数据就会从模拟串口的 TX 引脚发出去。注意 IAR 的printf默认使用完整格式化库会占用较多 Flash 空间。如果空间紧张可以在工程选项里把printf formatter改成small或tiny代价是不支持浮点数格式化。4. 接收端实现外部中断加定时器采样的组合拳4.1 起始沿捕获与采样点对齐接收端的难点在于数据什么时候来是未知的必须用外部中断捕获起始位的下降沿然后启动定时器在每一位的中间时刻采样。STM8S003 的 GPIO 支持外部中断但只有 Port B 和 Port D 的部分引脚有外部中断功能。选 RX 引脚时要查数据手册确认。起始沿捕获后第一次采样要等 1.5 个位周期之后每隔 1 个位周期采样一次。用 TIM4 定时的话第一次定时值设为 1.5 倍位周期后续改为 1 倍位周期。下面是一个接收状态机的实现#define RX_PIN GPIO_PIN_4 #define RX_PORT GPIOD static volatile uint8_t rx_data 0; static volatile uint8_t rx_bit_cnt 0; static volatile uint8_t rx_complete 0; static volatile uint8_t rx_byte 0; // 外部中断捕获起始位下降沿 INTERRUPT_HANDLER(EXTI_PORTD_IRQHandler, 6) { if (GPIO_ReadInputPin(RX_PORT, RX_PIN) RESET) { // 确认是起始位启动定时器第一次等 1.5 位周期 TIM4-CNTR 0; TIM4-ARR 156; // 1.5 × 104 156 TIM4-CR1 | TIM4_CR1_CEN; rx_bit_cnt 0; rx_data 0; // 关闭外部中断避免数据位期间误触发 EXTI-CR1 ~EXTI_CR1_PDIS; } } // TIM4 中断采样数据位 INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) { TIM4-SR1 ~TIM4_SR1_UIF; if (rx_bit_cnt 8) { // 采样当前位 if (GPIO_ReadInputPin(RX_PORT, RX_PIN)) rx_data | (1 rx_bit_cnt); rx_bit_cnt; TIM4-ARR 104; // 后续改为 1 倍位周期 TIM4-CNTR 0; } else { // 停止位接收完成 rx_byte rx_data; rx_complete 1; TIM4-CR1 ~TIM4_CR1_CEN; // 重新使能外部中断 EXTI-CR1 | EXTI_CR1_PDIS; } }外部中断里先判断引脚电平确实是低电平避免噪声误触发。然后设置 TIM4 的 ARR 为 1561.5 倍位周期启动定时器。第一次 TIM4 中断到来时正好落在第一位数据的中间之后每次中断都把 ARR 改回 104保证后续采样间隔是 1 个位周期。采样完 8 位后第 9 次中断对应停止位此时把数据存入rx_byte并置位rx_complete。4.2 接收超时与帧错误处理实际环境中接收引脚可能因为干扰产生虚假起始沿或者发送方中途断开导致帧不完整。如果不做超时处理接收状态机会永远卡在等待停止位的状态。常见做法是在 TIM4 中断里加一个超时计数如果超过 2 个位周期还没有完成接收就复位状态机。static volatile uint8_t rx_timeout 0; // 在 TIM4 中断的停止位分支里 } else { if (GPIO_ReadInputPin(RX_PORT, RX_PIN) SET) { // 停止位正确接收有效 rx_byte rx_data; rx_complete 1; } else { // 停止位错误丢弃该帧 rx_complete 0; } TIM4-CR1 ~TIM4_CR1_CEN; EXTI-CR1 | EXTI_CR1_PDIS; }停止位采样时顺便判断电平如果是低电平说明帧格式错误直接丢弃。这个判断不增加额外开销但能过滤掉大部分噪声引起的误帧。4.3 主循环里的数据读取与解析接收完成后的数据放在rx_byte里主循环轮询rx_complete标志来读取void main(void) { CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1); // 16MHz GPIO_Init(TX_PORT, TX_PIN, GPIO_MODE_OUT_PP_HIGH_FAST); GPIO_Init(RX_PORT, RX_PIN, GPIO_MODE_IN_PU_NO_IT); // 配置外部中断 EXTI_SetExtIntSensitivity(EXTI_PORT_GPIOG, EXTI_SENSITIVITY_FALL_ONLY); enableInterrupts(); while (1) { if (rx_complete) { rx_complete 0; // 处理接收到的字节 if (rx_byte 0x0D) { // 收到回车执行命令 } } } }主循环里只做标志判断和业务处理采样工作全部在中断里完成。这种架构下即使主循环里有耗时操作只要中断响应及时接收就不会丢数据。5. 避坑指南模拟串口调试中最容易翻车的五个点5.1 波特率误差累积导致接收乱码现象发送端用 9600bps 发数据接收端用 9600bps 收但收到的字节偶尔出错出错位置不固定。原因STM8S003 内部 RC 振荡器精度约 ±1%加上定时器分频的取整误差实际波特率可能偏离标称值 2% 以上。10 位帧累积下来采样点可能偏移到数据位的边缘。解决优先用外部晶振做时钟源。如果没有外部晶振把波特率降到 4800 或 2400误差容忍度会大很多。另外可以在代码里微调 ARR 值用示波器测实际位宽把误差压到 1% 以内。5.2 中断优先级冲突导致接收丢帧现象系统里同时开了多个中断模拟串口接收偶尔丢字节尤其是其他中断频繁触发的时候。原因STM8 的中断优先级是固定的TIM4 中断的优先级可能低于其他外设。如果高优先级中断服务函数执行时间超过一个位周期TIM4 中断就会被延迟采样点偏移导致误码。解决把 TIM4 中断的优先级设到最高在ITC-ISPR寄存器里配置。同时检查其他中断服务函数的执行时间确保没有超过半个位周期9600bps 下约 52 微秒。5.3 GPIO 模式配置错误导致电平异常现象TX 引脚用示波器看一直是高电平没有波形输出。原因GPIO 初始化时模式选错了。TX 引脚应该配成推挽输出GPIO_MODE_OUT_PP_HIGH_FAST如果配成开漏输出又没有外部上拉引脚就一直是高阻态。RX 引脚应该配成带中断的输入模式如果配成输出模式外部信号根本进不来。解决对照数据手册确认每个引脚的复用功能TX 用推挽输出RX 用浮空输入或上拉输入。初始化代码里显式设置不要依赖复位默认值。5.4 缓冲区溢出导致数据覆盖现象连续发送大量数据时后面的字节覆盖了前面的字节接收端收到的数据顺序错乱。原因环形缓冲区的头尾指针没有正确处理满和空的条件。当tx_head tx_tail时既可能是空也可能是满需要额外用一个计数变量或者留一个空位来区分。解决缓冲区大小设为TX_BUF_SIZE实际可用容量是TX_BUF_SIZE - 1。写入前判断(tx_head 1) % TX_BUF_SIZE tx_tail如果成立说明缓冲区满等待或丢弃。读取时判断tx_head ! tx_tail才取数据。5.5 IAR 优化等级过高导致延时循环被优化现象用for循环做微秒延时的代码在 Debug 模式下正常切换到 Release 模式后时序完全乱掉。原因IAR 的 High 优化会把没有副作用的空循环直接删掉或者把循环变量优化到寄存器里导致循环次数变化。解决延时函数里的循环变量加volatile修饰或者在 IAR 工程选项里把优化等级降到 Medium。更稳妥的做法是用定时器做延时不依赖软件循环。6. 进阶技巧用逻辑分析仪验证时序与动态调参模拟串口调通之后真正让我踏实的是用逻辑分析仪抓了一次波形。把 TX 引脚接到分析仪的通道 0设置采样率 1MHz触发方式设为下降沿然后让程序发一个 0x55。0x55 的二进制是 01010101低位先发所以波形是起始位低电平、然后高高低低交替、最后停止位高电平。分析仪上量出来的位宽如果是 104 微秒左右说明波特率准确如果偏差超过 5 微秒接收端在 10 位帧的末尾就可能采错。我一般会在代码里留一个动态调参的接口通过修改一个全局变量来微调 ARR 值不用重新编译就能用调试器改变量观察效果。具体做法是把 ARR 的计算封装成函数static uint16_t bit_period_arr 104; // 默认 9600bps void soft_uart_set_baud(uint16_t arr) { bit_period_arr arr; } // 在发送和接收的定时器配置里统一用 bit_period_arr调试时在 IAR 的 Watch 窗口里直接改bit_period_arr的值比如从 104 改成 103 或 105观察接收端是否恢复正常。这个方法比反复烧录快得多尤其适合在没有高精度晶振的板子上找最佳值。还有一个验证接收容错度的技巧让发送端故意用偏快或偏慢的波特率发数据看接收端在多大偏差下还能正确解码。我实测 STM8S003 在 16MHz 内部 RC 下9600bps 能容忍约 ±3% 的偏差超过这个范围就开始出现误码。这个数据可以作为选型参考——如果通信双方都用内部 RC波特率最好不要超过 4800。从那以后我每次做模拟串口都会先用逻辑分析仪把起始位的下降沿和第一位数据的采样点对齐情况抓出来看一眼确认采样点落在位周期正中间才继续往下调。这个习惯帮我省掉了大量“玄学乱码”的排查时间。希望帮到你。本文还有配套的精品资源点击获取