
用了 STM8 也有七八年了从最开始跟着别人项目抄寄存器到后来老老实实用官方标准外设库写逻辑中间踩过的坑能绕芯片两圈。最近在整理一套 STM8 库函数开发手册前面把时钟、GPIO、定时器这些基础外设梳理完这篇专门讲三个日常项目里绕不开的模块UART1 串口、内部数据 EEPROM、FLASH 操作库以及硬件 IIC 和软件 IIC 两条实现路线。这几个模块没什么花哨的但凡是做数据采集、小仪表、传感器节点这类产品基本天天都在跟它们打交道。这篇我不打算堆手册原文而是按实际项目里“怎么用、为什么这么用、出问题了怎么查”的思路来写争取让刚接触 STM8 库函数的人能直接照着搭也让已经在用的人看了能少踩几个我之前踩过的坑。先给还没上手的人交代一个背景STM8 虽然是个 8 位机频率不高、资源紧张但胜在便宜、稳定、开发简单。尤其是 STM8S103、STM8S105 这两颗料内部集成了 1KB 左右的数据 EEPROM 和几 KB 到几十 KB 的 Flash很多场景不需要外挂存储芯片就能把参数保存下来。而 UART1 是调试时最顺手的输出通道IIC 则是跟各种外设传感器、存储芯片通信的万能钥匙。把这几个模块吃透等于掌握了这颗芯片一半以上的实际生产力。下面直接进入正题。1. 开发前的几个基础认知1.1 为什么还在用 STM8它到底强在哪有些人觉得 STM8 是老古董了Cortex-M0 的芯片也不贵何必守着 8 位机不放。但实际做产品的时候成本、功耗、供货稳定性、开发周期这些都是硬指标STM8 在简单控制类项目里依然很有竞争力。它的外设设计很“直接”没有太多复杂的总线矩阵和时钟树库函数的封装也比较薄基本上看一眼数据手册就能猜到某个函数内部在操作哪个寄存器。另外STM8 的片上数据 EEPROM 是个很大的亮点。很多 M0 芯片虽然也有 Flash但真正能像 EEPROM 那样按字节擦写、并且擦写次数扛得住的数据区反而不多见。STM8 把数据 EEPROM 独立划分出来1KB 空间虽然不大但存校准参数、设备编号、运行状态这些绰绰有余。配合 FLASH 操作库还能在应用层实现简单的 Bootloader 或者 OTA 功能这在很多小家电、电动工具、传感器采集模块里非常实用。1.2 库函数和寄存器新手该走哪条路我一直的建议是新手直接学库函数不要一上来就啃寄存器。原因很简单STM8 的库函数封装得比较直白函数名基本就是操作意图比如 UART1_SendData8、FLASH_ProgramByte、I2C_GenerateSTART看名字就知道干什么。而且库函数内部处理了很多细节比如等待忙标志、检查解锁状态、处理中断标志位这些如果全靠自己写寄存器遗漏一个标志位就可能导致莫名其妙的问题。不过我也不是让大家完全不懂寄存器遇到问题的时候还是得能看懂库函数源码知道它操作了哪些寄存器、在等哪个标志位。比如 UART1_Init 初始化之后如果你想动态改波特率就得知道 UART1 的波特率寄存器 BRR2 和 BRR1 怎么配。所以我的建议是先会用库函数再回头补寄存器两条腿走路才稳。这篇里我尽量以库函数为主但涉及原理的地方会点到底层寄存器的行为方便你排查问题。1.3 开发环境与工程骨架让库函数跑起来STM8 的官方标准外设库目前网上很容易找到解压之后里面按芯片系列分了目录比如 STM8S_StdPeriph_Lib 下的 Libraries 文件夹里就是标准外设库的源码和头文件。新建工程的时候我通常是复制一份整个库的源码目录然后只把当前芯片需要的源文件加入工程像 stm8s_gpio.c、stm8s_uart1.c、stm8s_flash.c、stm8s_i2c.c 这些。头文件里把 STM8S103 或者 STM8S105 的宏定义打开再把 stm8s_conf.h 里需要的外设头文件取消注释编译环境就搭好了。这里有几个容易忽略的点。第一库函数源码里很多地方依赖 assert 参数检查如果开了 USE_FULL_ASSERT参数越界会直接进入死循环调试的时候这个功能挺有用但正式发布前建议关掉不然误触发会很痛苦。第二STM8 默认上电后主时钟是 HSI 内部的 16MHz 再经过 8 分频也就是 2MHz如果你不把时钟提频就配 115200 波特率误差会非常大。所以我一般在 main 函数最开始就调用 CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV1)把主频提到 16MHz再初始化各个外设。这个细节非常重要后面所有外设的时序配置都依赖它。2. UART1 串口调试和通信的第一选择2.1 UART1 初始化时钟、引脚、波特率一个都不能错UART1 在 STM8S103 上默认映射到 PD5(RX) 和 PD6(TX)注意别接反了我第一次画板子的时候就是把 TX 和 RX 画反查了半天还以为是代码问题。初始化的时候要分三步走先配置 GPIO 为复用推挽输出和浮空输入再调用 UART1_Init 设置波特率、数据位、停止位、校验位最后调用 UART1_Cmd 使能串口。波特率这一块强烈建议自己算一遍别总是复制网上现成的配置。STM8 的 UART1 波特率计算公式是波特率 主频 / (16 × UART_DIV)这里的 UART_DIV 是一个 16 位的分频值会被拆到 BRR2 和 BRR1 两个寄存器里。以 16MHz 主频算 9600 波特率16000000 / (16 × 9600) 104.17取整为 104实际波特率是 9615误差只有 0.16%非常稳。但如果从 2MHz 默认主频去算 1152001600000 / (16 × 115200) ≈ 0.87取整后误差能到 30%直接乱码这就是很多人上电串口打印全是乱码的根本原因。库函数封装了 UART1_Init你只要传波特率进去就行但传递进去的波特率能不能分频分得准完全取决于主频。所以初始化串口之前一定要先确认 CLK 已经提到 16MHz。另外库函数初始化完之后记得调用 UART1_Cmd(ENABLE) 使能串口漏掉这一步的话寄存器配置都写了但串口完全没反应这个跟 STM32 的写法不太一样容易踩坑。2.2 发送与接收轮询、中断、printf 重定向发送数据最简单的方式是轮询等待发送数据寄存器空然后写数据。库函数里对应的判断是 UART1_GetFlagStatus(UART1_FLAG_TXE)这个标志表示发送数据寄存器已经空了可以写入下一个字节。如果你想把一包数据完整发出去还要额外等待 UART1_FLAG_TC也就是发送完成标志否则在低波特率下连续发送时最后一两个字节可能没发完就进入休眠或者被关闭了。接收数据我一般不太建议在主循环里一直轮询 UART1_FLAG_RXNE除非你的产品逻辑非常简单。更好的方案是开启接收中断在中断服务函数里把收到的字节塞进一个环形缓冲区主循环需要的时候再从缓冲区取。STM8 的库函数里UART1 接收中断的入口是 UART1_RX_IRQHandler在 stm8s_it.c 里实现即可。中断里处理要快别做复杂的解析工作否则一旦数据量上来很容易丢字节。调试串口最爽的用法是重定向 printf。在 C 源文件里自己实现一个 fputc 函数内部调用 UART1_SendData8同时等待 TC 标志这样就能像在 PC 上写程序一样用 printf 打印调试信息。需要注意一点printf 默认可能会占用比较大的代码空间STM8 的 Flash 又不富裕所以 Release 版本里最好用条件编译把调试打印关掉或者自己写一个简单的 my_printf只支持 %d、%x、%s 几个常用格式能省下不少空间。2.3 实战经验串口乱码和丢数据的排查串口乱码的原因基本逃不出三件事波特率误差大、主频没配对、外部晶振和内部 RC 差异。如果主频用的是外部晶振还要看一下库里面的时钟源配置是不是真的切到了外部晶振而不仅仅是调了个分频系数。另一个容易被忽视的点是有些 STM8 型号的 UART1 引脚可以重映射比如映射到 PC 和 PD 的切换芯片默认引脚和你的板子实际接线不一致也会导致完全收不到数据。丢数据的问题更常见于中断处理和主循环抢占资源。尤其是你在中断里收发数据、同时主循环又在做比较耗时的运算或者延时超过一个字节的接收时间RXNE 标志被覆盖数据就丢了。解决办法是接收用环形缓冲区并且在中断里关掉不必要的其他中断保证接收 ISR 的响应时间是确定性的。还有一个小技巧如果对数据完整性要求高可以在协议层加帧头、帧尾和校验字节不要裸着发数据这能帮你省掉大量排查时间。3. 内部数据 EEPROM掉电保存数据的正确姿势3.1 数据 EEPROM 和程序 FLASH到底有什么区别很多初学者把 STM8 内部的 EEPROM 和 Flash 当成同一种东西其实它们在硬件设计上定位完全不同。数据 EEPROM 区域地址从 0x4000 开始是专门给用户存数据的可以按字节擦写擦写次数通常标称 10 万次适合频繁更新参数。程序 Flash 区域从 0x8000 开始是放代码的虽然也能通过 FLASH 操作库写入但一般建议按块擦除再编程频繁小数据更新并不适合。还有一个特别重要的点STM8 在主程序区执行代码的时候如果同时去擦写数据 EEPROMCPU 是会等待 Flash 控制器操作完成的也就是所谓“总线暂停”。数据 EEPROM 的每次字节编程典型时间在 6ms 左右具体跟主频有关也就是说写一个字节期间你的主循环可能被“卡”住几毫秒。如果你的系统对实时性要求很高比如正在驱动电机 PWM 或者在做通信协议时序就得特别小心最好避开关键时序窗口再去写 EEPROM。3.2 字节编程流程解锁、写数、等待、上锁STM8 对数据 EEPROM 是有写保护机制的不是直接往地址写数据就行。库函数封装好的流程很简单但我建议你还是理解一下底层过程不然出问题的时候根本不知道卡在哪。标准流程是先等待上一次编程结束BSY 标志为 0然后往 FLASH_DUKR 寄存器依次写入 0xAE 和 0x56 两个解锁密钥接着检查解锁成功的标志位确认之后才能往目标地址写数据写完再等待 BSY 清零最后把密钥寄存器写个无效值重新上锁。如果你直接调库函数其实就是 FLASH_Unlock(FLASH_MEMTYPE_DATA)、FLASH_ProgramByte(addr, data)、FLASH_Lock(FLASH_MEMTYPE_DATA) 这三个调用。这里我要特别提一句解锁密钥的顺序千万别写反主程序区解锁是先写 0x56 再写 0xAE而数据 EEPROM 解锁是先写 0xAE 再写 0x56两者的密钥顺序正好相反。我见过不止一个同事把这两组顺序搞混结果就是数据区一直写不进去查了半天还以为是芯片坏了。写代码的时候我习惯封装一个带状态返回的写函数比如uint8_t EEPROM_WriteByte(uint16_t offset, uint8_t data) { uint32_t addr 0x004000UL offset; if (offset 1023) return 1; FLASH_Unlock(FLASH_MEMTYPE_DATA); FLASH_ProgramByte(addr, data); while (FLASH_GetFlagStatus(FLASH_FLAG_BSY) SET); FLASH_Lock(FLASH_MEMTYPE_DATA); return 0; }读数据就简单了直接把这个地址当作普通内存读即可不需要任何解锁操作。这也是 STM8 用起来比外部串行 EEPROM 方便的地方省掉了 IIC 通信的时序读写像操作数组一样直接。3.3 提高 EEPROM 寿命磨损均衡与双区备份数据 EEPROM 虽然标称 10 万次擦写但如果你在产品里高频写入同一地址比如每秒钟存一次运行状态那个地址很快就会报废。实际项目中我总结了一套简单的提高寿命方案把存储区划分成多个槽位加入户参数只占 4 字节那就准备 8 个甚至 16 个槽位轮着写每个槽位写之前先读一下魔数magic number判断当前有效槽位下一次写就写到下一个槽位。这样总擦写次数等于单槽位寿命乘以槽位数量翻好几倍。另一个必须注意的问题是掉电保护。如果写完魔数写到一半突然断电下次上电读取的数据可能是残缺的。我常用的做法是双区备份参数区 A 和参数区 B 各存一份完整的数据块数据块里带校验字节上电时先读 A校验不过就读 B两者都无效就恢复默认值。写入时也先写 A 再写 B保证任何一个时刻至少有一份完整数据可恢复。这个方法实现成本很低但能避免很多“设备参数莫名其妙变成默认值”的售后问题非常值得做进产品里。4. FLASH 操作库把程序区当成存储区用4.1 FLASH 编程的原理与流程STM8 程序区 Flash 的写入原理跟数据 EEPROM 完全不同。程序区的最小擦除单位是块STM8S103 每块 64 字节STM8S105 每块 128 字节写之前必须先擦除整个块不能像 EEPROM 那样单独改一个字节。所以在设计“把程序区当存储区”的方案时一定要把数据按块大小对齐组织否则就会出现“想改 4 字节却要把 128 字节全部重新写一遍”的尴尬场景。FLASH 编程的解锁流程和数据区类似但要特别注意密钥顺序前面说过主程序区是先写 0x56再写 0xAE。库函数里调用 FLASH_Unlock(FLASH_MEMTYPE_PROG) 即可内部已经处理好顺序。擦除块用 FLASH_EraseBlock传入块编号和存储区类型编程用 FLASH_ProgramBlock 之类的函数把缓冲区数据写进去。操作完成后记得把 FLASH_PUKR 重新上锁防止程序跑飞时意外修改 Flash 代码。4.2 块擦除和标准块编程的库函数用法实际使用中用库函数做 FLASH 操作比直接操纵寄存器省心得多但有几个参数要理解清楚。以 STM8S105 为例Flash 从 0x8000 开始每块 128 字节如果你要擦除从 0x8000 开始的第一块库函数里对应的块编号通常是 0 或者 1具体要看你在初始化配置里怎么定义 Flash 起始块。为了避免记错我习惯直接算物理地址先用一个宏定义好应用区的起始地址比如 0x9000给 Bootloader 留出空间然后用 (addr - 0x8000) / 128 算出块号传给库函数这样即使换芯片型号只要改宏定义和块大小就能复用。编程数据时库函数的 FLASH_ProgramBlock 要求传入待写入的内存缓冲区指针、目标块地址和编程模式。编程模式一般选标准编程即可不需要涉及那种“快速编程”模式后者对电压和时序要求更苛刻适合生产烧录不适合应用代码里调用。还有一个细节写入缓冲区大小必须和块大小严格一致多写或者少写都会报错别想着写半个块试试Flash 控制器会直接给你错误标志。4.3 四个必须注意的 FLASH 操作红线第一个红线写 Flash 期间务必关闭中断。因为 Flash 编程时 CPU 会暂停取指如果这时发生中断中断向量所在地址可能正在被擦写或者处于不可读状态轻则中断丢失重则程序跑飞。稳妥的做法是 FLASH 操作前用 _asm(sim) 关总中断操作完成后 _asm(rim) 开总中断。第二个红线不要擦写正在执行的代码所在块。这是很多人容易犯的错如果你在地址 0x8000 开始的块里运行代码又想去擦掉这个块来更新程序那就是“坐在树枝上锯树枝”必死无疑。做 Bootloader 的时候跳板代码要么放在不会被擦除的保留块里要么放到 RAM 里执行具体方案比较麻烦这里先提醒一句。第三个红线Flash 的写入次数比数据 EEPROM 低很多通常只有 1 万次左右。用它存频繁变化的运行数据非常不明智如果只是存版本号、校准表这种“烧录后基本不变”的内容才是它的正确打开方式。第四个红线写 Flash 之前一定要确认电源稳定最怕写入过程中电压跌落轻则写坏一个块重则损坏整片 Flash 的可靠性。产品里如果有大功率负载建议在写入 Flash 的任务里做电压检测低于阈值直接放弃本次写入。5. IIC硬件外设和软件模拟怎么选5.1 IIC 协议基础时序、地址、上拉电阻IIC也叫 I2C是一种两线制总线一条时钟线 SCL一条数据线 SDA所有设备都挂在这两条线上靠设备地址区分。通信时序的核心就三件事起始条件SCL 高电平期间 SDA 从高拉低、停止条件SCL 高电平期间 SDA 从低拉高、以及数据位的传输SCL 高电平期间 SDA 必须保持稳定SCL 低电平期间 SDA 才能变化。只要把这几个基本时序写对挂什么 EEPROM、传感器、屏幕都是同样的套路。上拉电阻是 IIC 电路里最容易忽略又最关键的参数。IIC 的 SDA 和 SCL 都是开漏输出必须靠外部上拉电阻把线路拉高。电阻太小灌电流太大推不动低电平电阻太大上升沿太慢高速模式下时序跟不上。常用的安全值是 4.7kΩ适合 100kHz 标准模式如果总线速率要跑到 400kHz 快速模式或者总线上挂的设备比较多总线电容增大建议改用 2.2kΩ 甚至 1kΩ。你可以简单估算上升时间约等于 0.8473 × 上拉电阻 × 总线电容400kHz 要求上升时间小于 300ns如果总线电容按 100pF 算电阻最大只能到 3.5kΩ 左右所以 4.7kΩ 在高速模式下确实偏大。5.2 软件模拟 IIC代码简单、调试方便软件模拟 IIC 就是用普通 GPIO 按照时序一点一点拉电平好处是引脚可以随便选不依赖芯片的 I2C 外设而且代码逻辑完全可控想加调试打印就能加。很多老工程师在低速 IIC 场景下只信软件模拟因为出问题了能拿逻辑分析仪一级一级看时序硬件 I2C 一旦卡死在状态机里反而不好查。下面这段是我用 STM8 库函数写的软件 IIC 核心代码起始和停止是最基本的两个时序#define IIC_PORT GPIOC #define IIC_SCL GPIO_PIN_3 #define IIC_SDA GPIO_PIN_4 void IIC_Start(void) { GPIO_WriteHigh(IIC_PORT, IIC_SDA); GPIO_WriteHigh(IIC_PORT, IIC_SCL); delay_us(5); GPIO_WriteLow(IIC_PORT, IIC_SDA); delay_us(5); GPIO_WriteLow(IIC_PORT, IIC_SCL); } void IIC_Stop(void) { GPIO_WriteLow(IIC_PORT, IIC_SDA); GPIO_WriteHigh(IIC_PORT, IIC_SCL); delay_us(5); GPIO_WriteHigh(IIC_PORT, IIC_SDA); delay_us(5); }发送一个字节的时候从最高位开始依次把 SDA 置成对应电平然后拉高 SCL、延时、再拉低 SCL循环 8 次最后释放 SDA 读取从机的 ACK 应答。接收字节正好相反主机在 SCL 高电平期间读取 SDA 的电平状态逐位拼成一个字节。延时长度的选取取决于你用的挂载设备常用几微秒即可整个软件 IIC 在一个 16MHz 的 STM8 上跑 100kHz 是绰绰有余的。软件 IIC 的缺点也很明显比较占 CPU 时间而且如果你在主循环里频繁轮询读传感器其他任务的实时性会被拖累。这就要求你控制好调用频率不要在每次主循环里做大量重复的 IIC 读取最好做成定时触发读到的数据缓存起来供业务逻辑使用。5.3 硬件 I2C 外设速度快但容易踩坑STM8 内部自带硬件 I2C 外设库函数在 stm8s_i2c.c 里初始化之后主控可以自动产生起始条件、发送地址、收发数据CPU 只需要在事件中断里处理各个状态即可。相比软件模拟硬件 I2C 最大的优势是不占 CPU在连续传输大量数据时效率高很多比如驱动 OLED 屏幕全屏刷新软件模拟可能会卡顿硬件 I2C 就轻松很多。但硬件 I2C 有几个典型的坑我几乎每一次用都会被坑一下。第一个坑是总线忙标志不能自动清除如果通信过程中从机异常或者 SDA 被拉死硬件 I2C 可能一直报 BUSY库函数里的发送流程就一直卡在等待事件。我的处理办法是初始化之前先检查总线是否空闲检测到异常就切换成 GPIO 模式把 SCL 翻转几下强制恢复总线状态再重新初始化外设。第二个坑是事件标志的判断顺序有些库版本要求在发出起始条件后先等待 EV5 再发地址发完地址等 EV6顺序不能乱一旦跳步状态机就乱了后续数据根本发不出去。如果你在项目中发现硬件 I2C 总是偶发卡死我建议先在逻辑分析仪上抓一下波形确认起始条件和地址字节是否正确。如果是地址应答丢失导致的可以检查一下从机地址是不是 7 位地址没有左移一位。IIC 协议里发送的地址字节是“7 位地址左移 1 位最低位表示读写方向”比如 AT24C02 的 7 位地址是 0x50实际发送写地址就是 0xA0读地址就是 0xA1很多人地址写错就是这个左移和读写位的问题。5.4 实战读写 AT24C02 的完整流程AT24C02 是最经典的 IIC EEPROM2Kbit 也就是 256 字节容量用来存配置参数非常合适。用软件 IIC 操作它分三步写字节、随机读字节、页写入。写字节的流程是发送起始条件发送设备写地址 0xA0等待 ACK发送要写的内部字节地址等待 ACK发送数据字节等待 ACK发送停止条件。读字节稍微绕一点需要先“伪写”一次也就是发送设备写地址和内部字节地址然后再发一次起始条件发送设备读地址 0xA1这时候主机才能读到数据读完发非应答再停止。这里我给出一个随机读的代码骨架方便你直接抄uint8_t AT24C02_ReadByte(uint8_t addr) { uint8_t data 0; IIC_Start(); IIC_SendByte(0xA0); IIC_WaitAck(); IIC_SendByte(addr); IIC_WaitAck(); IIC_Start(); IIC_SendByte(0xA1); IIC_WaitAck(); data IIC_RecvByte(); IIC_SendNotAck(); IIC_Stop(); return data; }页写入是一个高效但容易出错的操作用法。AT24C02 的页大小是 8 字节连续写入一页时地址的自增是在芯片内部完成的写超过 8 个字节地址会回绕到本页开头把前面的数据覆盖掉这个特性经常被忽略。要跨页写连续数据时必须在软件层把数据按页切分一个页写完后重新发送起始条件和地址再写下一页。另外AT24C02 写完一页之后内部还需要几毫秒的写入时间这时候芯片不响应任何命令必须等够时间或者轮询 ACK 才能开始下一个操作不然新数据写不进去。这也是很多人“写入偶尔失败、时序一改又好了”的常见原因。6. 综合问题速查与避坑记录6.1 下载失败类错误排查STM8 开发中经常遇到下载程序报错比如常见的 “Flash download failed” 或者 STVP 提示连接失败。这类问题第一反应往往不是程序问题而是硬件连接问题。先用最简连接排查SWIM 线尽量短最好不要超过 10cm接线顺序 VDD、GND、SWIM、NRST 都接上重启目标板后立刻点击下载成功率会高很多。如果仍然失败大概率是芯片开启了读保护ROP这时候需要在 STVP 的 Option Bytes 界面把读保护解除而不是反复点击下载。还有一个很隐蔽的情况目标板的复位电路电容太大导致复位时间过长下载器在上电瞬间抓不到芯片。我见过有人把复位引脚接了 10uF 电容结果 99% 概率下载失败。排查时可以直接把复位电容临时摘掉试试如果恢复正常就是复位电路的问题。最后提醒一句如果用的是 ST-Link注意固件版本和 IDE 驱动是否匹配老版本 ST-Link 在 Windows 10 以上系统偶尔会有驱动兼容问题更新一下驱动或者换个 USB 口往往就解决了。6.2 EEPROM/FLASH 操作异常排查如果代码看起来逻辑没问题数据就是写不进 EEPROM优先级最高的怀疑对象就是解锁顺序和上锁时机。前面强调过数据区解锁是先 0xAE 再 0x56程序区解锁是先 0x56 再 0xAE这两个顺序在某些库函数封装下虽然不用手动写但如果你是自己操作寄存器一个字节错位就会导致解锁无效。另一个常见问题是写 EEPROM 的代码里没有等待 BSY 标志就立刻进行下一次写入高速连续写数据场景下就会丢字节或者写错地址一定要在每次写入之间确认 Flash 控制器已经空闲。FLASH 操作异常比 EEPROM 更严重一旦出现 PRGERR编程错误标志意味着写入的电压、地址对齐或者块擦除状态有问题。遇到 PRGERR 先检查块有没有先擦除再检查目标地址是不是超出了当前芯片型号的 Flash 末尾地址。还有一个容易忽略的点如果你在 RAM 里跑的程序往 Flash 写数据库函数的延迟参数可能要根据实际主频调整主频不对导致内部延时时间不足也会偶发写失败这时把主频提回 16MHz 再试问题往往会消失。6.3 IIC 通信异常排查IIC 不出波形的时候先用万用表量 SCL 和 SDA 的静态电平正常情况下两条线都应该是高电平如果有一条被拉低说明有设备在占用总线或者发生了总线死锁。总线死锁最常见的场景是主机发送了起始条件之后从机还没来得及响应主机突然复位SDA 上的起始条件不完整总线一直处于“半开”状态。解决办法是在初始化时让 SCL 先翻转几次把挂载设备的内部状态机复位然后再进入正常通信。数据错误则要优先检查地址字节和设备地址。7 位地址左移一位加读写位这个细节几乎所有 IIC 通信错误里都出现过。另一个高频问题是 ACK 检测失败如果你用的是硬件 I2C检查一下超时处理是否完善建议在所有等待事件的地方加一个超时计数器避免外设在极端情况下永久卡死主循环。用软件模拟 IIC 时延时一定要给足尤其是从机是慢速设备时ACK 位后要适当加一点延时再发下一个字节不然连续读写容易在第二个字节开始就出现数据错位。6.4 我总结的几个工程习惯写了这么多年 STM8我觉得比单个外设用法更重要的是整体工程习惯。先说代码组织每个外设驱动我习惯单独建一个 .c 和 .h 文件接口尽量封装成“初始化 读 写 状态查询”四件套这样换芯片平台的时候只要改底层实现上层逻辑基本不用动。再说测试验证每次写完 EEPROM 或 FLASH 的驱动我会先写一个简单的自测函数存一串已知数据重启读出来比对正确之后再接业务逻辑。这个习惯帮我省掉了大量联调时间。还有一个小细节是关于中断优先级的。STM8 的中断优先级和 STM32 不一样它是通过中断控制器统一管理的如果 UART1 接收中断和定时器中断同时到来响应顺序和嵌套规则跟你的预期可能不同。所以我在做通信协议时尽量只在 UART1 接收中断里做最简单的字节收容把协议解析全部放到主循环里做避免中断嵌套带来的不可控时序。最后再分享一个小技巧STM8 的数据 EEPROM 既然可以直接像普通内存一样读我经常在代码里用 const 数组定义一套默认参数然后把这套 const 数组放到一个特定地址段上电时如果检测到 EEPROM 里的参数无效就从 Flash 的 const 区把默认参数拷过去恢复。这样做既省了外部存储芯片的成本又让产品恢复出厂设置变得非常简单只需要一个标志位就能触发参数重新初始化。这个思路在很多量产项目里都非常实用。