IAR下STM8S003模拟串口实战:从环境搭建到稳定通信

发布时间:2026/11/5 7:05:02
IAR下STM8S003模拟串口实战:从环境搭建到稳定通信

简介:这份资源面向在 IAR Embedded Workbench 下开发 STM8S003 的嵌入式工程师与学习者,针对该芯片原生物理串口资源不足的问题,给出基于 PC3、PC4 引脚配合定时器实现软件模拟串口(Software UART)的完整工程。内容涵盖 GPIO 推挽/开漏配置、定时器周期与波特率匹配、发送波形生成、查询与中断两种接收模式的处理逻辑,以及中断服务程序中的数据收发流程,适合已具备一定单片机基础、希望掌握资源受限场景下串口扩展思路的开发者参考。压缩包共 53 个文件,约 418KB,以 IAR 工程与调试配置文件为主,辅以 C 源码、头文件、编译中间产物及 HEX 输出,工程结构完整,导入后即可编译调试。目前已有 425 人学习下载。读者可从中获得可直接运行的模拟串口示例代码、工程配置模板与收发实现细节,并在此基础上扩展多路模拟串口或适配不同波特率。

1. 从一次串口不够用的翻车说起:IAR 下给 STM8S003 补一个模拟串口

手上这块 STM8S003 的小板子,引脚本来就紧,真串口被调试口占了,业务上又非要再挂一路串口去对接模块。第一次遇到这情况时我图省事,直接把 TX/RX 往两个普通 IO 上一接,代码写完烧进去,上位机收到的全是乱码,示波器一量波形,位宽飘得离谱——这就是典型的“以为软件延时能凑合,结果被中断打断得面目全非”。STM8S003 这类 8 位小资源芯片,做模拟串口(软件串口)本身不难,难的是在 IAR 环境里把时钟、延时、中断优先级这几件事配顺,配不好就是收发全废、偶发丢字节。这篇记录的就是我在 IAR 里给 STM8S003 搭一套能跑通的模拟串口环境的完整过程:装什么版本、工程怎么建、延时怎么标定、坏了怎么救。适合刚上手 STM8、被串口资源卡住,或者想把这套环境复现一遍的开发者。

2. 环境与版本先列清:IAR、器件支持包和 STM8S003 的选型

动手之前先把环境钉死,这一步偷懒,后面编译报错、下载失败、波形不对,全都会变成玄学。STM8S003 是 STM8S 系列里的入门型号,Flash 8KB、RAM 1KB,资源小得可怜,所以工具链版本和优化等级都得提前想好,不能等写完代码再回头调。

2.1 IAR 版本与 STM8 器件支持包怎么选

我一般用 IAR Embedded Workbench for STM8,版本上 3.x 系列就够用,太老的 2.x 对新型号支持不全,太新的又可能和手头的仿真器驱动对不上。安装时注意两点:一是安装路径别带中文和空格,IAR 的某些脚本对路径很敏感;二是器件支持包(device support)要覆盖 STM8S 系列,装完后在Options → General Options → Target里能选到STM8S003F3才算装对。

# 安装完成后确认器件支持文件是否存在(路径按实际安装目录调整) ls "C:/IAR Systems/Embedded Workbench */stm8/config/devices" # 正常应能看到 STM8S 系列相关的 .ddf / .men 文件

这段只是核对,不是必须执行的命令。逻辑是:IAR 的器件描述文件决定了编译器认不认识 STM8S003 的寄存器映射和存储布局,缺了它,工程能建但链接会报存储区错误。参数上,安装目录里的stm8子目录是关键,config/devices下按系列分文件,找不到 STM8S 就说明器件包没装全,重跑安装程序勾上对应组件即可。

2.2 新建工程时的关键选项

工程类型选Empty project,然后在Options里逐项确认。下面这张表是我每次建工程都会过一遍的配置项,照着对一遍能省掉大半莫名其妙的报错。

配置项位置建议值说明
Target deviceGeneral Options → TargetSTM8S003F3选错型号会导致寄存器地址和 Flash 大小不符
Code modelGeneral Options → CodeDefault小资源芯片别乱开大模型
OptimizationC/C++ Compiler → OptimizationsLow 或 None(调试期)高优化会把软件延时循环优化掉
Data modelGeneral Options → DataDefault与库函数匹配
Output formatLinker → Output默认 hex方便用 STVP 或仿真器烧录

优化等级这一项是血泪经验:模拟串口靠的是软件延时循环,编译器一旦把空循环优化没了,位宽直接崩。调试阶段我固定用 Low 甚至 None,等功能验证完再考虑往上调,调完必须重新用示波器或逻辑分析仪量一遍波形。

2.3 时钟与引脚的前置确认

STM8S003 默认走内部 16MHz RC 时钟,分频后给 CPU。模拟串口的波特率完全依赖这个时钟的准确性,所以第一步是在代码里把时钟源和分频明确写死,别依赖复位默认值。

// 时钟初始化:内部 16MHz RC,1 分频给 CPU CLK_ICKR |= 0x01; // 使能内部高速 RC while(!(CLK_ICKR & 0x02)); // 等待 HSIRDY 就绪 CLK_CKDIVR = 0x00; // HSIDIV=0, CPUDIV=0,CPU 跑 16MHz

逻辑说明:CLK_ICKR的 bit0 是 HSION,置位后内部 RC 起振,bit1 是就绪标志,必须等它置位再继续,否则后面算出来的延时全是错的。CLK_CKDIVR低 3 位是 HSIDIV、高 3 位是 CPUDIV,全清零就是 16MHz 不分频。参数上,如果你的板子外挂了晶振,这里要改成切换外部时钟并等待稳定,但 STM8S003 大多数小应用用内部 RC 就够,前提是接受它几个百分点的频率偏差——模拟串口对偏差敏感,后面标定延时就是为这个服务的。

引脚选择上,TX 用一个普通推挽输出 IO,RX 用一个带外部中断能力的输入 IO。选之前翻一遍数据手册的引脚复用表,避开已经被调试口或其它外设占用的脚,这一步选错,后面代码再对也收不到数据。

3. 模拟串口的配置原理与代码落地:延时、位时序和收发框架

环境搭好,接下来是这套方案的核心:用软件把 UART 的位时序“手工”打出来。原理不复杂——起始位拉低、8 个数据位从低位到高位依次输出、停止位拉高,每一位的持续时间由波特率决定。难的是在 IAR 的编译结果下,这个持续时间要准。

3.1 波特率与位延时的换算

以 9600bps 为例,一位的时间是 1/9600 ≈ 104.17 微秒。CPU 跑 16MHz,一个指令周期因指令而异,所以不能直接拿“104 微秒 × 16”当循环次数,必须实测标定。我一般的做法是先写一个粗略的延时函数,用逻辑分析仪量实际位宽,再按比例修正循环次数。

// 粗略的微秒级延时,需按实测标定 NOP 数量 void delay_us(uint16_t us) { while(us--) { // 16MHz 下约 1us 的循环体,具体指令数需实测调整 __no_operation(); __no_operation(); __no_operation(); __no_operation(); } }

逻辑说明:__no_operation()是 IAR 提供的内联空操作,编译后是一条单周期指令,用它来凑时间是可控的。参数上,循环体里放几条 NOP 取决于编译优化等级和实际主频,所以这段代码的注释里我特意写了“需实测调整”——这不是偷懒,是这类软件延时唯一靠谱的定标方式。标定时把 TX 引脚在起始位拉低后翻转几次,用逻辑分析仪抓位宽,偏差超过 3% 就加减 NOP 数量,直到 9600bps 下每位接近 104 微秒。

3.2 发送一个字节的时序实现

有了延时,发送就顺了:拉低起始位、延时一位、按低位到高位输出 8 个数据位、每位延时、最后拉高停止位。

#define TX_PIN GPIO_PIN_5 // 假设 TX 接在 PD5,按实际接线改 #define TX_PORT GPIOD void soft_uart_send_byte(uint8_t data) { uint8_t i; TX_PORT->ODR &= ~(1 << TX_PIN); // 起始位:拉低 delay_us(104); for(i = 0; i < 8; i++) { if(data & 0x01) TX_PORT->ODR |= (1 << TX_PIN); // 数据位 1 else TX_PORT->ODR &= ~(1 << TX_PIN); // 数据位 0 delay_us(104); data >>= 1; // 低位先发 } TX_PORT->ODR |= (1 << TX_PIN); // 停止位:拉高 delay_us(104); }

逻辑说明:ODR是 STM8 的输出数据寄存器,直接按位操作比调库函数快,减少时序抖动。data >>= 1配合data & 0x01实现低位先发,这是标准 UART 的位序,写反了上位机收到的就是位翻转的乱码。参数上,delay_us(104)对应 9600bps,换波特率就改这个数,比如 115200 就是约 8.7 微秒,但这么短的位宽对软件延时精度要求极高,STM8S003 上我一般不超过 38400。发送期间最好关掉全局中断,否则一个中断进来,位宽就被拉长了。

3.3 接收端:外部中断加采样

接收比发送麻烦,因为要在起始位下降沿触发后,按位在中间时刻采样。我的做法是把 RX 脚配成带外部中断的输入,下降沿触发,进中断后先延时半个位周期避开起始位边沿,再逐位采样。

// RX 脚外部中断服务函数(示意,中断向量名按 IAR 的 stm8s_it.c 填) #pragma vector = EXTI_PORTD_VECTOR __interrupt void exti_portd_isr(void) { uint8_t i, data = 0; EXTI_CR1 &= ~0x03; // 先关掉本口中断,避免位内误触发 delay_us(52); // 半个位周期,对准起始位中心 for(i = 0; i < 8; i++) { delay_us(104); // 移到下一位中心 data >>= 1; if(RX_PORT->IDR & (1 << RX_PIN)) data |= 0x80; // 高位补 1,配合右移实现低位先收 } delay_us(104); // 跳过停止位 // data 即收到的字节,放入缓冲区 EXTI_CR1 |= 0x03; // 恢复中断 }

逻辑说明:进中断先关本口中断,是因为采样期间引脚上的电平跳变会再次触发中断,把时序彻底打乱。delay_us(52)是半个位周期,目的是让后续采样点落在每一位的中间而不是边沿,这是抗抖动最有效的一招。data >>= 1再data |= 0x80的组合,等价于把新采到的位放到最高位再整体右移,最终得到低位在前的正确字节。参数上,采样点偏移量(52 微秒)和位周期(104 微秒)必须和发送端一致,两端都用同一套标定过的延时函数,否则收发对不上。

3.4 中断优先级与临界区处理

STM8S003 的中断资源有限,模拟串口最怕的就是收发过程中被高优先级中断打断。配置上,把外部中断的优先级设到较高,同时在发送的关键时序段用disableInterrupts()/enableInterrupts()包起来。

#include "intrinsics.h" void soft_uart_send_string(uint8_t *str) { __disable_interrupt(); // 进入临界区,保证位时序连续 while(*str) { soft_uart_send_byte(*str++); } __enable_interrupt(); // 退出临界区 }

逻辑说明:__disable_interrupt和__enable_interrupt是 IAR 的内建函数,对应 STM8 的 SIM 寄存器操作。整串发送期间关中断,代价是这段时间内其它中断响应被推迟,所以字符串不能太长,或者改成逐字节关开。参数取舍上,如果系统里有定时器中断必须及时响应,就别整串关,改成每个字节发送前关、发完开,牺牲一点字节间间隔换取中断实时性。

4. 避坑与排查:模拟串口在 IAR 下最容易翻车的几处

这套东西跑通之前,我踩的坑基本都集中在这几处。下面按“现象 → 原因 → 解决”记下来,遇到问题先对着排查,比盲目改代码快得多。

4.1 现象:上位机全是乱码,示波器位宽忽长忽短

原因:编译器优化把延时循环优化掉了,或者发送期间被中断打断。解决:调试期把优化等级降到 None,确认波形正常后再逐级往上调;发送关键段加临界区保护。这一条是最常见的,十次乱码有六次是优化惹的祸。

4.2 现象:能发不能收,或者偶尔收到错字节

原因:RX 采样点没对准位中心,或者外部中断在采样期间被重复触发。解决:核对半个位周期的延时值,进中断先关本口中断,采样完再恢复;同时确认 RX 脚确实配置成了输入且上拉使能,悬空脚会收到一堆噪声。

4.3 现象:换一块板子或换个批次,通信就不稳

原因:内部 RC 时钟存在个体偏差,延时是按某一块板标定的,换板后位宽偏移累积。解决:延时标定值留出余量,或者在对精度要求高的场合改用外部晶振;接收端采样点尽量往位中心靠,容忍一定偏差。

4.4 现象:IAR 编译报存储区溢出或链接错误

原因:器件型号选错,或者优化等级和库不匹配导致代码体积暴涨。解决:回到Options → General Options → Target确认是 STM8S003F3,Flash 8KB 的边界很紧,去掉不必要的库和调试打印,必要时开中等优化。

4.5 现象:烧录后程序不跑,或跑一下就复位

原因:时钟初始化顺序不对,或者中断向量没对上。解决:确认CLK_ICKR等待就绪的循环没被优化掉,中断向量名和 IAR 的启动文件一致;用仿真器单步跟一下复位后的第一条指令,看是不是卡在时钟等待里。

5. 装完怎么验证跑通:从回环测试到长时间压测

环境装好、代码写完,最后一步是验证这套模拟串口到底能不能稳定跑。我的习惯是分三层验:先自发自收确认时序,再对接真实模块确认协议,最后长时间压测看丢包。

第一层,把 TX 和 RX 用杜邦线短接做回环,发一串已知字节,看接收缓冲区是否原样返回。这一步能同时验证发送时序和接收采样点是否匹配,是最快的自检手段。

// 回环自检:发 0x55(01010101)便于观察位翻转 uint8_t test = 0x55; soft_uart_send_byte(test); // 在接收缓冲区里比对是否收到 0x55

选0x55是因为它的二进制是交替的 0 和 1,示波器上能清楚看到方波,位宽偏差一眼就能看出来。如果回环收到的不是0x55,先别怀疑接收代码,回头量发送波形,多半是延时标定偏了。

第二层,接上真实模块,用逻辑分析仪同时抓 TX 和 RX,确认起始位、数据位、停止位的边界对齐,波特率误差控制在 3% 以内。这一步能暴露回环测试发现不了的问题,比如模块的电气特性导致边沿变缓。

第三层,连续发送几千字节,统计丢包和错包。模拟串口在中断频繁的系统里,长时间跑最容易暴露临界区没保护好的问题。我一般跑十分钟以上,丢包率超过千分之一就得回头查中断优先级和临界区。

验证层级方法通过标准
时序自检TX/RX 短接回环收发字节完全一致
协议对接接真实模块 + 逻辑分析仪位边界对齐,误差 < 3%
稳定性连续发送 10 分钟以上丢包率 < 0.1%

从那以后我每次给 STM8S003 配模拟串口,都强制先跑一遍回环再上真实模块,绝不跳过——因为跳过的那次,我在现场对着乱码查了两个小时,最后发现只是优化等级没降下来。希望这套流程能帮到你,少走那段弯路。

本文还有配套的精品资源,点击获取