ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM8实战:UART1、EEPROM、FLASH与IIC外设开发避坑指南

STM8实战:UART1、EEPROM、FLASH与IIC外设开发避坑指南 去年调一块STM8S105K4的板子项目本身不大IO控制加一组Modbus通信我却因为UART1和EEPROM的小问题折腾了整整三天。网上关于STM8库函数开发的资料不少但大多只给一段示例代码没人说清楚为什么要这样写、踩坑时往哪个方向查。所以这个系列我打算按“工程实战能用”的标准来写这篇是第3篇专门讲UART1、内部EEPROM、FLASH操作库、IIC硬件和软件这四块。它们都是开发里绕不开的外设也是我实际项目中踩坑最多的几个地方。文章默认你已经装了IAR for STM8或Cosmic并且会用标准外设库建工程。如果你还在犹豫用IAR还是Cosmic或者新建工程时总报一些莫名奇妙的头文件错误我先把这部分讲明白因为后面所有代码都是在这个基础上跑的。1. 入手STM8开发先把工具链和库函数版本理顺1.1 为什么我坚持用库函数而不是寄存器STM8的寄存器其实不多GPIO、UART、定时器加起来就那么几个直接操作寄存器完全可行。但我依然建议用ST官方标准外设库理由很现实库函数已经做过一轮验证尤其对时钟分频、中断向量这类容易写错的地方库的封装比你自己翻寄存器手册可靠得多。另外STM8库函数的命名很统一UART1_Init、I2C_Init、FLASH_ProgramByte看到名字就知道干什么。项目中途换人维护或者三个月后自己回来看代码读起来也轻松。寄存器写法的代码虽然省几行但每行都需要对着数据手册确认位含义调试成本反而更高。不过要注意标准外设库是“够用但不精致”的封装。比如UART1_Init这个函数内部帮你算了波特率分频值但如果你给它一个算不准的波特率它不会报错只会默默生成一个有误差的时钟。这就是为什么我后面要专门讲波特率误差。1.2 IAR for STM8与Cosmic的选择STM8主要就两个官方认可的编译器IAR for STM8和Cosmic C for STM8。我在项目里两套都用过说下实际感受。IAR for STM8的优点是集成调试环境做得完整工程配置直观代码补全和断点调试都顺手适合从STM32转过来的朋友。它的免费版和评估版有代码大小限制超过限额会编译失败需要换正式授权。Cosmic的老用户很多很多早期项目都是用它编译的如果你拿到的第三方库是Cosmic工程直接用IAR打开不一定顺利反而Cosmic自己更兼容。我个人的建议是新项目直接用IAR。理由不是Cosmic不行而是IAR的报错信息更友好工程配置对新手更友好。但如果你是接手老项目或者要编译别人给的Cosmic工程就别强行迁移Cosmic完全够用。无论用哪个编译器都有两个共通的坑要提前避开工程路径和源码路径不要有中文不要有空格。ST的库文件对中文路径支持很差一个全角字符就能让编译器报错报到你怀疑人生。芯片型号一定要选对。同样一段代码选STM8S105K4和选STM8S103F3编译出来的FLASH和RAM占用完全不同关键是外设基地址都变了选错型号连跑都跑不起来。1.3 新建工程时必须检查的三个位置新建完工程先别急着写代码花两分钟确认三个地方第一头文件路径是否包含标准外设库的inc和src目录。很多编译错误“file stm8s.h not found”就是这里没配好。IAR里在Project Options C/C Compiler Preprocessor里加路径Cosmic在CXSTM8的Include Path里加。第二预定义符号要写STM8S105或者你用的具体型号。库的头文件靠这个宏来决定选哪颗芯片的外设定义漏掉它你会看到一堆寄存器地址未定义的报错。第三中断向量文件stm8s_it.c不要删。很多人觉得这个文件没什么用就直接移除结果中断函数没法定义程序跑飞了都不知道。正确做法是在这个文件里添加你自己的中断处理函数或者用#pragma vector在别的C文件里重定义但后者要求你非常清楚自己在做什么。这三步做完后面写UART、EEPROM、IIC才会顺。2. UART1串口实操初始化、中断收发和一点调测经验2.1 初始化代码与波特率误差UART1在STM8S系列里是异步串口库函数初始化就那么几行void UART1_Config(void) { UART1_Init(9600, UART1_WORDLENGTH_8D, UART1_STOPBITS_1, UART1_PARITY_NO, UART1_SYNCMODE_CLOCK_DISABLE, UART1_MODE_TXRX_ENABLE); UART1_ITConfig(UART1_IT_RXNE, ENABLE); // 只开接收中断 }这段代码看着简单但有两个地方值得展开。一是UART1_MODE_TXRX_ENABLE这个参数同时使能发送和接收。有些初学朋友只用了UART1_MODE_TX_ENABLE结果收不到数据又查不出原因。二是UART1_Init内部会根据当前系统时钟和你要的波特率去算BRR1、BRR2两个分频寄存器如果算出来不是整数就会产生误差。以16MHz系统时钟为例常见波特率的实际误差我测过大概是这样目标波特率实际分频值实际波特率误差960016679598.080.02%1920083319207.680.04%115200139115107.910.08%25000064250000.000%误差在0.1%以内通常没问题但如果系统时钟是2MHz这种低频再配115200误差可能超过2%对端就会收到乱码。所以别只看库函数“支持”某个波特率要自己按下式核算一下分频值 系统时钟 / (目标波特率) 实际波特率 系统时钟 / 分频值 误差 (目标波特率 - 实际波特率) / 目标波特率误差超过1%就要考虑改系统时钟或者换一个波特率档位。2.2 发送数据TXE和TC两个标志怎么用UART1发送数据库函数是UART1_SendData8但很多人不知道它只把数据丢进发送数据寄存器并不代表数据已经从引脚发完了。真正判断发送完成的标志有两个UART1_FLAG_TXE和UART1_FLAG_TC。这两个标志的区别是这样的TXE表示发送数据寄存器空你可以往里面写下一个字节了TC表示整个移位寄存器都已经发送完毕线上已经空闲。如果你只是连续发一帧数据用TXE就够但如果你发送完最后一帧要立刻切换引脚方向、关闭发送模式或者置低RE引脚就必须等TC标志。我实际踩过一个坑用UART1_SendData8发完一帧后马上进入低功耗模式结果最后一个字节丢了一半。原因就是我只看了TXE没看TC数据还在移位寄存器里没发完就停了。这里也顺便提醒调用UART1_SendData8之前最好用while等待TXE为SET不然连续发送时会丢数据。void UART1_SendString(uint8_t *str, uint16_t len) { while (len--) { while (UART1_GetFlagStatus(UART1_FLAG_TXE) RESET); UART1_SendData8(*str); } while (UART1_GetFlagStatus(UART1_FLAG_TC) RESET); // 收尾必须等TC }2.3 接收中断与帧解析套路接收部分我强烈建议用中断别在主循环里轮询UART1_FLAG_RXNE。原因很简单串口数据什么时候来是随机的轮询方式对时序要求高的系统来说太容易丢数据。中断服务函数建议放在stm8s_it.c里#pragma vector UART1_RXNE_vector __interrupt void UART1_RXNE_IRQHandler(void) { uint8_t ch UART1_ReceiveData8(); /* 丢进环形缓冲或状态机处理 */ }注意UART1_ReceiveData8()这个函数读一次就会自动清除RXNE标志。如果你在中断里先判断了标志位又没及时读数据可能导致标志不消失、中断反复进入。这个在调试时很容易让人懵。帧解析我一般不用中断里处理一大段逻辑的做法而是用状态机。比如Modbus RTU的帧中断里只做字节缓存和超时计时主循环里再按状态机解析。这样既不会在中断里睡太久也不会漏掉关键帧。状态机写起来不复杂一个switch按帧头、长度、数据、校验几个状态切就行。还有个小经验调试串口时用示波器或逻辑分析仪看波形永远比瞎猜快。先确认TXD引脚有没有波形再确认电平对不对然后再怀疑波特率。TXD量到波形但上位机乱码就十有八九是波特率误差问题回到2.1的表格去核算。3. 内部EEPROM解锁、读写、掉电保护一个都不能少3.1 EEPROM解锁和IO操作顺序STM8的内部EEPROM在芯片手册里叫Data EEPROM它和真正的独立EEPROM芯片在功能上使用习惯类似掉电不丢、可读可写但写入前需要擦除、写入有时间和寿命限制。库函数读写EEPROM的典型流程是void EEPROM_WriteBytes(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; FLASH_Unlock(FLASH_MEMTYPE_DATA); // 解锁数据区 for (i 0; i len; i) { while (FLASH_GetFlagStatus(FLASH_FLAG_EOP) RESET); FLASH_ProgramByte(addr i, buf[i]); } FLASH_WaitForLastOperation(FLASH_MEMTYPE_DATA); // 等待EOP后再继续下一步操作 }这里有个关键点解锁EEPROM必须用FLASH_Unlock(FLASH_MEMTYPE_DATA)这个宏的参数区分数据区和程序区。很多人和程序区的FLASH_Unlock(FLASH_MEMTYPE_PROG)搞混结果要么写不进去要么直接改动了程序区内容非常危险。另外操作EEPROM之前要确保没有正在进行的其他FLASH/EEPROM操作。库函数里有FLASH_WaitForLastOperation标准流程是每次写入前先等上一次EOP标志尤其是连续写入多字节时不能偷懒。解锁本身是一个“软锁”机制上电默认是锁住的你要往FLASH_DUKR寄存器写两个密钥值0xAE和0x56才能解锁。库函数封装成了FLASH_Unlock但你要知道它内部做的事这会在排查问题时帮上大忙。3.2 EEPROM的地址规划、读写校验和掉电保护STM8S105的EEPROM容量一般是1KB起始地址0x4000不同型号容量不同最稳妥的做法是查你自己手头型号的数据手册别凭记忆用地址。我见过有人把STM8S103的128字节EEPROM当成1KB来用写入越界后数据被写到FLASH区直接把程序破坏了。EEPROM的一个重要特性是擦写寿命有限数据手册标称通常10万次左右。这个数字看起来不小但如果你在循环里频繁写入比如每秒存一次计数器大约28小时就会接近寿命上限。所以设计时要注意两点一是减少写入频率。状态变化才写不要每轮循环都写把多次小更新合并成一次大写入。二是用“损耗均衡”的思路。不要在固定地址上写而是准备一个小的环形缓冲每次写到下一个位置写完更新一个当前指针。这个思路和高级EEPROM芯片里的wear leveling原理一样能让整个EEPROM区域的寿命均摊。掉电保护也一样重要。实际场景里设备可能在EEPROM写一半的时候突然断电结果那个地址的数据变成未知值。我用的方案比较朴实但很有效每次写关键数据时同时写一份数据副本和校验码。比如地址A写真实数据地址B写备份地址C写异或校验。上电读取时先校验C和A如果对不上尝试用B恢复A再不行就用默认值。#define EEPROM_DATA_ADDR 0x4000 #define EEPROM_BAK_ADDR 0x4004 #define EEPROM_CHK_ADDR 0x4008 uint8_t EEPROM_ReadSafeValue(void) { uint8_t val FLASH_ReadByte(EEPROM_DATA_ADDR); uint8_t bak FLASH_ReadByte(EEPROM_BAK_ADDR); uint8_t chk FLASH_ReadByte(EEPROM_CHK_ADDR); if ((val ^ bak) chk) return val; if ((bak ^ val) chk) return bak; // 校验失败用备份 return 0xFF; // 都不可信返回默认值 }这个写法不复杂但能在绝大多数掉电场景下保住关键参数。比裸写在固定地址上要稳得多。还有一个很多新手不知道的事STM8的EEPROM是可以用字节寻址写入的但它本质上是FLASH介质所以擦除操作按块更高效。如果你只改一个字节库函数的FLASH_EraseByte也支持。注意写入前先擦除是FLASH介质的老规矩和数据手册保持一致。4. FLASH自编程与在线升级程序区擦写的边界和下载失败排查4.1 程序区、数据区、选项字节的区别STM8的FLASH不是一整块统一管理的内部按功能分成几个区域。简单理解程序区Program memory放你的代码地址从0x8000开始掉电不丢。数据区Data memory就是上一节讲的EEPROM地址从0x4000开始也是掉电不丢。选项字节区Option bytes配置芯片的读保护、看门狗、复位源等。在库函数里FLASH模块同时管理这几块。访问它们前都要解锁但解锁参数不一样程序区用FLASH_MEMTYPE_PROG数据区用FLASH_MEMTYPE_DATA。这里最需要注意的坑是程序区不能擦写正在运行的块。你不应该在运行App 0x8000处的代码时同时去擦写0x8000这个块这会让MCU找不到下一条指令直接死机。正确做法是让Bootloader和App分别放在不同的FLASH块里Bootloader负责擦写App所在的块。这个“不要擦自己正在跑的块”的原则是理解自编程的关键。4.2 BootloaderApp的结构与中断向量表问题在线升级IAP是目前STM8项目里很常见的需求。基本结构是Bootloader放低地址区App放高地址区Bootloader通过串口或IIC接收固件数据写到App区域然后跳转执行。举例STM8S105的FLASH如果从0x8000到0xFFFF那么Bootloader可以占0x8000-0x8FFFApp从0x9000开始。跳转的核心代码void JumpToApp(void) { void (*app_reset)(void); app_reset (void (*)(void))0x9000; // App的复位向量地址 app_reset(); }实际工程里还要关闭全局中断、恢复系统时钟默认状态再跳转。这个步骤顺序错了App端很容易死得不明不白。中断向量表是STM8自编程里最常见的坑。不同于STM32有VTOR寄存器可以重定位中断向量表STM8的中断向量表位置相对固定。如果你的App从0x9000开始但UART1中断向量还指向0x8004附近那就只能进入Bootloader的中断处理没法正确触发App自己的中断。想解决这个问题要么Bootloader里写一个“中断转发”的跳板Bootloader的每个中断处理函数都被链接到App对应中断函数的地址然后直接跳转要么让App仍然从0x8000开始运行把Bootloader放到高地址区占用的空间小一点。两种方案我都做过前者灵活但代码量感人后者简单但Bootloader升级能力受限。没有银弹看项目需要。4.3 Flash下载失败常见原因排查做STM8开发时你是不是也见过类似这样的报错连接调试器时报Flash下载失败或者下载到一半直接中断。我梳理过最常见的几类原因按出现频率排现象特征优先排查方向下载器连不上目标芯片SWIM接口接线、驱动安装、芯片供电连接正常但擦除失败芯片使能了读保护ROP需要先解除保护或全片擦除下载到一半失败电源电流不够、复位电路太弱、SWIM线太长下载成功但跑飞选项字节配置错误、时钟配置和实际晶体不一致SWIM是STM8的调试接口相当于STM32的SWD。排插这个报错的第一步是量芯片核心供电是否正常第二步是确认SWIM引脚有没有被别的外设占用很多板子把这个引脚复用成普通IO调试器自然连不上。如果你用STVP或IAR下载时提示保护位相关错误多半是芯片选项字节里的ROP被置位了。解除办法一般是对芯片执行“全片擦除”这个过程会清掉Flash里的所有代码和数据相当于恢复出厂状态。这是一个破坏性操作但对变砖的板子来说也是最后的救命稻草。这个章节我想再强调一次做过IAP的板子FLASH区域里既有Bootloader又有App如果Bootloader写坏了同时又把读保护打开了那就只能动用全片擦除。所以做在线升级项目时Bootloader区务必提前预留好足够的升级回退机制别把产品做成一次性的。5. IIC通信硬件I2C外设和GPIO模拟我的取舍5.1 硬件IIC库函数的调用顺序和卡死现场STM8内部有I2C外设库函数提供了一套完整的调用接口。主模式写一个字节的标准流程大概是这样void I2C_WriteByte(uint8_t devAddr, uint8_t regAddr, uint8_t data) { while (I2C_GetFlagStatus(I2C_FLAG_BUSY) SET); // 总线忙就等 I2C_GenerateSTART(ENABLE); while (I2C_CheckEvent(I2C_EVENT_MASTER_START_SENT) ERROR); I2C_Send7bitAddress(devAddr, I2C_DIRECTION_TX); while (I2C_CheckEvent(I2C_EVENT_MASTER_ADDRESS_ACKED) ERROR); I2C_SendData(regAddr); while (I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED) ERROR); I2C_SendData(data); while (I2C_CheckEvent(I2C_EVENT_MASTER_BYTE_TRANSMITTED) ERROR); I2C_GenerateSTOP(ENABLE); }这段代码能跑通但它有个隐患如果从设备不响应比如地址错了、设备没上电I2C外设会一直在等ACK程序卡死在while里。如果此时总线上出现干扰造成总线错误外设进入忙状态后不会自己退出你要么重启要么执行软件复位。这个状态在调试时遇到过好几次每次都要拔电重来。硬件IIC卡死后我恢复总线的操作是把SCL和SDA引脚手动切成普通GPIO开漏模式然后对SCL连续发出9个脉冲让处于异常状态的从设备释放总线再重新切回I2C外设模式并重新初始化。这招对很多从设备都有效比断电重启快多了。5.2 软件IIC代码框架与上拉电阻取值因为硬件IIC的卡死问题我后来在很多项目里改用了软件IIC也就是用GPIO模拟IIC时序。它的优势很直接时序完全由你的代码控制不存在内部状态机的迷之行为出了问题示波器一看就知道。软件IIC的核心是四段时序起始、停止、发送字节、接收字节。我贴一段精简框架#define I2C_SCL_PORT GPIOC #define I2C_SCL_PIN GPIO_PIN_5 #define I2C_SDA_PORT GPIOC #define I2C_SDA_PIN GPIO_PIN_6 #define SCL_H() GPIO_WriteHigh(I2C_SCL_PORT, I2C_SCL_PIN) #define SCL_L() GPIO_WriteLow(I2C_SCL_PORT, I2C_SCL_PIN) #define SDA_H() GPIO_WriteHigh(I2C_SDA_PORT, I2C_SDA_PIN) #define SDA_L() GPIO_WriteLow(I2C_SDA_PORT, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputPin(I2C_SDA_PORT, I2C_SDA_PIN) void I2C_Start(void) { SDA_H(); SCL_H(); delay_us(4); SDA_L(); delay_us(4); SCL_L(); } void I2C_Stop(void) { SDA_L(); SCL_H(); delay_us(4); SDA_H(); delay_us(4); } void I2C_WriteByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) SDA_H(); else SDA_L(); dat 1; delay_us(2); SCL_H(); delay_us(4); SCL_L(); delay_us(2); } SDA_H(); // 释放SDA准备接收从设备应答 delay_us(2); SCL_H(); delay_us(4); SCL_L(); delay_us(2); }这段代码里我用了delay_us(4)对应大约100kHz的IIC速率。如果要跑400kHz就把延时压到delay_us(1)左右但具体值要根据芯片主频实测不是越小越好延时太短会超过从设备的最小时序要求。软件IIC的硬件接线也有讲究。IIC是开漏结构SDA和SCL都必须接上拉电阻不能只靠GPIO内部上拉。上拉电阻取多大取决于总线速度和总线电容。简单的估算公式是上升时间约等于0.85倍的RC乘积。如果总线电容约100pF上拉电阻10k上升时间约0.85微秒100kHz时半周期5微秒勉强够用但400kHz时半周期1.25微秒就明显不够了需要换成2.2k到4.7k。工作模式常见上拉电阻范围100kHz 标准模式4.7k - 10k400kHz 快速模式2.2k - 4.7k多个从设备且线长按下限选或实测波形确认我自己的习惯是如果板子同时接了多个IIC设备或IIC线缆超过10厘米直接选4.7k然后在400kHz下实测波形。上升沿太缓就从4.7k降到2.2k再测上升沿陡起来就没问题。5.3 硬件IIC和软件IIC怎么选我给了两套方案你可能会问到底选哪个。我的选择经验是这样如果从设备是固定型号、固定地址比如板载的EEPROM和传感器用软件IIC更省心不用研究那些难懂的事件标志调试快。如果对速度有硬性要求比如要挂一颗高速传感器或者总线上有很多设备频繁通信那么硬件IIC更好毕竟它能把时序控制交给外设理论上更稳定。但无论选哪个都建议在早期验证阶段先用软件IIC跑通从设备的读写时序确认从设备地址和寄存器地址都没错再决定是否换硬件IIC。这样能把变量拆开别让“从设备没响应”和“外设配置不对”两个问题混在一起排查。我用这个办法规避过好几次莫名的“卡死”排查效率提升明显。另外软件IIC还有一个隐藏优点它能在任意GPIO上实现不受外设引脚映射限制。有些板子的I2C引脚被调试器或其他外设占用了软件IIC就能绕开冲突。缺点也很明显全程占用CPU通信频率高了之后主循环会被拖慢。所以软件IIC适合短报文、低频次的应用不适合高速大批量数据传输。6. 一些真实使用心得这篇讲的内容覆盖外设比较多最后聊几句我在实际项目里的体会。第一点调试STM8的IIC时别太迷信“硬件外设肯定更稳”。STM8的硬件I2C一旦挂起处理起来比想象中麻烦务必要有总线恢复的手段。软件IIC虽然代码多一点但调试体验真的舒服出问题能直接看到是哪一步异常。第二点EEPROM和FLASH的访问权限、解锁序列是STM8和STM32差异最大的地方。很多从STM32转过来的朋友会惯性带入“随便读写Flash”的思维在STM8上会栽跟头。每次操作前问自己一句这个区域解锁了吗我是不是在擦自己正在跑的程序区第三点库函数只是封装不是黑盒。碰到奇怪现象时打开ST的库源文件看一眼内部实现比在网上搜一圈更有用。比如UART1_Init的波特率分频计算比如FLASH_Unlock的密钥序列源码里写得明明白白。STM8这颗芯片在2025年看来确实不算年轻但它在低成本、高可靠场景下依然有大量应用。把这些外设的基础逻辑搞清楚后面就算换芯片平台这套经验也完全能迁移。最后分享一个从朋友那里学来、我也一直在用的小技巧调通一个外设后在代码注释里记录下调试过程中最诡异的一次问题哪怕只有一句话。三个月后你大概率会感谢当时的自己。
返回列表