用STM8S105的GPIO模拟SPI驱动AFE4300实现人体成分测量
简介面向人体成分分析场景的STM8S105AFE4300阻抗测量工程代码围绕体脂秤、健康监测等设备需求重点解决MCU通过虚拟SPI驱动AFE4300实现生物阻抗采集的问题适合嵌入式软硬件开发者、医疗电子爱好者及需要快速上手AFE4300的工程师。压缩包共278个文件体积约5.84MB文件类型以IAR的pbi工程文件、c/h源码、dep与xcl配置以及o目标文件为主其中pbi为工程入口h/c包含驱动与主逻辑dep/xcl用于编译链接整体结构清晰可直接用IAR打开编译便于研读初始化、虚拟SPI时序、寄存器配置和阻抗计算逻辑。该工程已有382人浏览学习。项目通过虚拟SPI测试给出了STM8S105与AFE4300之间完整的通信链路并包含外设初始化、读写时序、数据解析等关键代码读者可在此基础上快速移植到相似硬件平台省去底层驱动调试环节加速人体阻抗测量功能落地。1. STM8S105 与 AFE4300 的人体成分测量先从 VirtualSPI 说起如果你在智能体脂秤或者人体成分分析仪的项目里见过 STM8S105 和 AFE4300 这两个型号那你大概率也遇到过同样的处境AFE4300 是一颗典型的模拟前端芯片官方推荐用 SPI 接口读取数据但实际项目里主控的硬件 SPI 引脚可能被 LCD 或者 Flash 占用甚至因为 PCB Layout 的原因不得不把 AFE4300 挂在不支持硬件 SPI 的 GPIO 上。这时候用 GPIO 模拟 SPI 时序也就是标题里所谓的 VirtualSPI就是最务实的方案。这个组合在医疗和健康类产品里非常常见。STM8S105 作为一颗低成本 8 位主控提供了足够的计算能力处理阻抗测量数据而 AFE4300 负责完成生物阻抗测量中最关键的模拟部分——激励信号生成、信号放大和 ADC 采样。整套方案的核心难点不是传感器本身而是怎么把 SPI 时序在 GPIO 上稳定复现以及如何理解阻抗测量链路中的数据流。本文会沿着 VirtualSPI 实现、AFE4300 配置、人体成分参数换算、STM8S105 工程落地这几个层次逐步展开适合正在做健康秤、体脂秤或人体成分分析仪的嵌入式工程师也适合想搞懂 AFE4300 到底怎么用的硬件开发者。2. 用 STM8S105 的 GPIO 模拟 AFE4300 所需 SPI 时序2.1 VirtualSPI 的时钟极性与数据采样点设计AFE4300 的 SPI 接口兼容标准 SPI Mode 0也就是时钟空闲为低电平数据在上升沿采样下降沿切换。这个前提不搞清楚后面调试会浪费大量时间。STM8S105 的 GPIO 翻转速度受限于系统时钟在 16MHz 主频下一条GPIO_WriteBit大概需要几十纳秒到一百多纳秒因此模拟出来的 SPI 时钟频率通常在几百 kHz 到 1MHz 之间。对于 AFE4300 的寄存器读写来说这个速度完全足够。常见做法是用四个 GPIO 分别映射 SCLK、MOSI、MISO、CS然后在代码里用一个状态机来翻转电平。核心是要保证 MOSI 数据在 SCLK 上升沿之前稳定MISO 采样点放在 SCLK 为高电平的中间位置。下面是标准的最小实现#define VSPI_PORT GPIOD #define VSPI_SCLK_PIN GPIO_PIN_3 #define VSPI_MOSI_PIN GPIO_PIN_2 #define VSPI_MISO_PIN GPIO_PIN_4 #define VSPI_CS_PIN GPIO_PIN_5 static void vspi_delay(void) { /* 空转几个 NOP 为 GPIO 翻转预留建立时间 */ __asm(NOP); __asm(NOP); __asm(NOP); __asm(NOP); } uint8_t vspi_xfer_byte(uint8_t data) { uint8_t rx 0; for (uint8_t i 0; i 8; i) { /* 先放 MOSI再拉高时钟 */ if (data 0x80) { GPIO_WriteHigh(VSPI_PORT, VSPI_MOSI_PIN); } else { GPIO_WriteLow(VSPI_PORT, VSPI_MOSI_PIN); } data 1; vspi_delay(); GPIO_WriteHigh(VSPI_PORT, VSPI_SCLK_PIN); vspi_delay(); /* 时钟高电平期间读取 MISO */ rx 1; if (GPIO_ReadInputPin(VSPI_PORT, VSPI_MISO_PIN)) { rx | 0x01; } GPIO_WriteLow(VSPI_PORT, VSPI_SCLK_PIN); vspi_delay(); } return rx; }这段代码里有几个关键设计。MOSI 先于 SCLK 拉高保证 AFE4300 在上升沿采样时数据已经稳定MISO 的读取放在 SCLK 高电平的中段避开边沿处的数据翻转窗口而vspi_delay的空操作则是在用时间换可靠——STM8S105 的 GPIO 输出本身没有可编程 slew rate 控制如果不加延时高速翻转时 MISO 采样容易踩到信号边沿。参数上需要关注的是vspi_delay的颗粒度。STM8 的每个 NOP 在 16MHz 下大约 62.5ns四个 NOP 加上 GPIO 操作本身的耗时单个时钟周期大约在 500ns 到 1μs 之间。如果主频改成 2MHz 低功耗模式延时逻辑要保持相同的物理时间应该调整 NOP 数量而不是直接改时钟。实际项目里我一般会把这个延时做成宏方便在不同时钟频率之间切换。2.2 片选时序与寄存器访问的命令结构AFE4300 的寄存器读写命令都是以字节为单位组织的。写入时先拉低 CS发送寄存器地址字节再发送两个数据字节高字节在前最后拉高 CS。读取时同样先发地址字节然后连续接收两个字节。整个过程可以用下面这段代码串起来void afe4300_write(uint8_t reg, uint16_t value) { GPIO_WriteLow(VSPI_PORT, VSPI_CS_PIN); vspi_delay(); vspi_xfer_byte(reg 0x7F); /* 写地址最高位为 0 */ vspi_xfer_byte((value 8) 0xFF); /* 高字节 */ vspi_xfer_byte(value 0xFF); /* 低字节 */ vspi_delay(); GPIO_WriteHigh(VSPI_PORT, VSPI_CS_PIN); vspi_delay(); } uint16_t afe4300_read(uint8_t reg) { uint16_t rx 0; GPIO_WriteLow(VSPI_PORT, VSPI_CS_PIN); vspi_delay(); vspi_xfer_byte((reg 0x7F) | 0x80); /* 读地址最高位置 1 */ rx (uint16_t)vspi_xfer_byte(0x00) 8; rx | vspi_xfer_byte(0x00); vspi_delay(); GPIO_WriteHigh(VSPI_PORT, VSPI_CS_PIN); vspi_delay(); return rx; }这段代码在 CS 拉低和拉高之间加入了延时是为了保证 AFE4300 的帧同步逻辑能够正确识别。AFE4300 的 SPI 从机在 CS 上升沿锁存命令如果 CS 释放得太快芯片可能把最后一字节当成无效数据丢弃。这个延时不需要太长但必须存在尤其是当 GPIO 翻转很快时。这里有个容易踩的坑AFE4300 读操作在发送地址之后并不需要发送哑字节而是直接读两个字节。与某些 SPI Flash 必须先写 0x00 或 0xFF 才能读出数据不同AFE4300 在发送读地址后随后的 MISO 线上就会出现高字节数据。如果继续用vspi_xfer_byte(0x00)只是为了让时钟继续走读回的字节才是有效的所以写法上虽然一样但理解上不能混淆。2.3 GPI/O 配置与复用冲突的检查清单STM8S105 的 GPIO 复用功能会导致 VirtualSPI 失败的一个隐藏因素。很多型号的 PD3、PD4 在默认状态下可能是 UART 或 SPI 的复用引脚直接用GPIO_Init配置成输出或输入之前需要确认选项字节里没有使能复用功能。另一个常见问题是 MISO 引脚没有配置为输入上拉或输入浮空导致读到的数据全为 0。下面是一个可靠的初始化片段void vspi_gpio_init(void) { GPIO_Init(VSPI_PORT, VSPI_SCLK_PIN | VSPI_MOSI_PIN | VSPI_CS_PIN, GPIO_MODE_OUT_PP_HIGH_FAST); /* 推挽输出 */ GPIO_Init(VSPI_PORT, VSPI_MISO_PIN, GPIO_MODE_IN_PU_NO_IT); /* 上拉输入 */ GPIO_WriteHigh(VSPI_PORT, VSPI_CS_PIN); /* CS 空闲为高 */ GPIO_WriteHigh(VSPI_PORT, VSPI_SCLK_PIN); /* 确保 SCLK 高电平注意模式 */ GPIO_WriteLow(VSPI_PORT, VSPI_SCLK_PIN); /* 空闲拉低 */ }细心的读者会发现这里先拉高又拉低 SCLK是故意的。AFE4300 在 CS 拉低之前如果 SCLK 处于不确定状态可能产生一个额外的边沿导致寄存器地址错位。初始化时把 SCLK 明确拉低可以避免首次通信时出现虚假时钟边沿。3. AFE4300 阻抗测量链路的寄存器配置与数据读取3.1 AFE4300 的人体成分测量模式选择AFE4300 内部有两套独立的测量链路一套用于体重计weight scale一套用于人体成分分析body composition。人体成分测量模式的核心是四电极阻抗测量AFE4300 内部包含波形发生器、电流激励 DAC、仪表放大器和 16 位 ADC。使用前需要通过寄存器选择激励电流源和测量通道。最关键的几个寄存器是控制寄存器、激励电流控制寄存器和通道选择寄存器。下面是一段典型的初始化序列void afe4300_human_body_config(void) { afe4300_write(0x00, 0x0000); /* 复位 */ vspi_delay(); afe4300_write(0x01, 0x0001); /* 进入人体成分测量模式 */ afe4300_write(0x05, 0x0000); /* 关闭内部偏置 */ afe4300_write(0x06, 0x0001); /* 使能内部 1.7V 参考 */ afe4300_write(0x10, 0x0001); /* 选择 50kHz 正弦波 */ }这段配置里寄存器 0x01 的 bit0 置 1 是将芯片从体重计模式切换到人体成分模式。寄存器 0x06 使能内部参考电压是必须的否则 ADC 的满量程范围不确定。0x10 寄存器的 bit0 位选择激励频率不同频率对应不同的人体组织穿透深度50kHz 是 BIA生物电阻抗分析里最常用的频率。激励电流的控制在另一个寄存器中AFE4300 可以产生 250μA 到 1mA 的正弦激励电流。选择多大的电流取决于电极面积和皮肤接触阻抗电流太小则信噪比差电流太大可能引起皮肤不适。下面这段代码把电流设置为 500μAafe4300_write(0x11, 0x0002); /* 500uA 激励电流 */寄存器 0x11 的低两位控制电流档位00 对应 250μA01 对应 500μA10 对应 1mA。这个参数要在实际测试中调整尤其是老年人皮肤较干、接触阻抗高的情况下过低的激励电流会导致测量结果跳动严重。3.2 电极通道与阻抗数据寄存器映射AFE4300 在人体成分模式下支持四个电极通道通常对应左手、右手、左脚、右脚。通过通道选择寄存器可以分别测量不同电极对之间的电压从而计算分段阻抗。通道选择寄存器和数据寄存器之间存在直接的映射关系读错通道会导致数据张冠李戴。void afe4300_measure_channel(uint8_t ch) { afe4300_write(0x12, ch); /* 通道选择 0~3 */ afe4300_write(0x13, 0x0001); /* 启动一次转换 */ /* 等待转换完成通常需要几毫秒 */ while ((afe4300_read(0x14) 0x0001) 0) { /* 轮询等待 */ } } uint16_t afe4300_get_impedance(void) { return afe4300_read(0x15); /* 读取阻抗原始值 */ }寄存器 0x14 的 bit0 是转换完成标志。这里如果使用 VirtualSPI 而不用硬件 SPI在轮询等待时需要留意每次afe4300_read(0x14)都会产生完整的 SPI 帧AFE4300 内部的转换状态机不受 SPI 读操作影响可以安全地持续轮询。AFE4300 的阻抗测量结果是 16 位有符号数还是无符号数要结合寄存器说明来看。在默认配置下输出的是无符号数代表 ADC 采样后的原始比值不是真正的欧姆值。需要结合校准电阻和 PGA 增益来换算。这就是为什么很多人拿到 AFE4300 数据后觉得读数不稳定或者量级不对其实是把原始 ADC 值直接当成了阻抗值。3.3 从 ADC 原始值到阻抗值的换算公式AFE4300 内部有可编程增益放大器PGA增益倍数可以通过寄存器配置。不同增益下同样一个阻抗值映射到的 ADC 数值不同所以换算公式必须带上增益参数。常见做法是先测量一个已知电阻作为参考推导出线性系数再应用到实际测量中。换算公式如下float afe4300_calc_impedance(uint16_t raw, uint8_t gain_idx) { /* 增益系数表实际值需要针对每一台设备标定 */ static const float gain_scale[4] { 0.5f, 1.0f, 2.0f, 4.0f }; /* 参考电阻 100 欧姆在 1 倍增益下标定得到的满量程值 */ const float vref 1.7f; float voltage (float)raw / 65535.0f * vref * 2.0f; float i_exc 0.0005f; /* 500uA */ float impedance voltage / i_exc / gain_scale[gain_idx]; return impedance; }这段计算里做了一次线性映射但没有考虑相位信息。AFE4300 实际上还能输出阻抗的实部和虚部但默认配置下只给出幅值。如果要做更精细的身体成分计算比如区分细胞外液和细胞内液需要用多个频率下的阻抗幅值和相位角联合计算。上述换算只是给工程调试提供一个快速参考真正用于医疗级产品时每个设备都必须用高精度电阻网络做逐台标定。4. STM8S105 与 AFE4300 的工程落地点时钟、中断与数据缓存4.1 为什么 STM8S105 用内部时钟也能稳定跑 AFE4300STM8S105 的时钟源可以选择内部 HSI 16MHz 或外部晶振。AFE4300 是模拟前端芯片对 SPI 时钟频率的绝对精度要求不高关键是激励信号的频率稳定性。激励正弦波是由 AFE4300 内部振荡器或者外部时钟基准产生的STM8 的时钟只影响 SPI 时序不影响测量频率。因此内部 RC 振荡器也能满足需求。但要注意的是STM8S105 内部 RC 在全温度范围内精度约为 ±2%这会导致 VirtualSPI 的时钟周期变化。只要vspi_delay的时序裕量足够这个误差不影响通信。真正受影响的是如果使用 STM8 的定时器来产生方波激励而不用 AFE4300 内部波形发生器则频率误差会直接进入测量结果。所以在人体成分项目中建议让 AFE4300 自己产生激励信号STM8 只负责吞吐数据。4.2 用中断还是轮询读取转换结果AFE4300 每次阻抗转换需要约 1ms 到 5ms取决于滤波器设置和稳定时间。STM8S105 在等待期间可以处理 UI 或者蓝牙通信。两种方案各有适用范围。如果整个系统只有 AFE4300 一个外部设备用死等轮询即可代码简单直接。如果系统同时要驱动 LCD、读取按键、处理蓝牙协议则用定时器轮询或者 GPIO 中断更合适。AFE4300 有一个中断输出引脚可以配置为转换完成后拉低用来触发 STM8 的外部中断。下面这个片段演示了在中断服务函数里读取数据INTERRUPT_HANDLER(EXTI_PORTD_IRQHandler, 6) { /* 确保是 AFE4300 的中断引脚触发的 */ if (GPIO_ReadInputPin(VSPI_PORT, VSPI_INT_PIN) RESET) { current_impedance afe4300_get_impedance(); measure_done 1; } EXTI_ClearITPendingBit(EXTI_Pin5); }注意中断服务函数里调用了afe4300_get_impedance()这是完整 SPI 通信操作耗时几十微秒。中断里做这种操作不是不可以但要保证 AFE4300 的转换结果在中断被响应时依然有效。如果中断在转换完成后很久才被响应AFE4300 的数据寄存器可能会被下一次转换覆盖。产品里通常会加一个标志位主循环检测到measure_done后再去读中断只负责置位。4.3 VirtualSPI 在实时性要求下的缓存策略当测量点数很多时比如扫频测量 5kHz 到 500kHz 多个频率点每次都启动转换、等待完成、读取数据总耗时会累加。一个常见优化策略是连续测量同一通道的次数取平均先把原始 ADC 值缓存到数组全部采集完成后再做滤波和阻抗换算。#define SAMPLE_COUNT 8 uint16_t raw_buf[SAMPLE_COUNT]; void afe4300_continuous_measure(uint8_t ch) { for (uint8_t i 0; i SAMPLE_COUNT; i) { afe4300_measure_channel(ch); raw_buf[i] afe4300_get_impedance(); } }这里SAMPLE_COUNT为 8意味着 8 次转换之间没有延时。如果相邻两次转换间隔太短滤波器还没稳定数据可能存在互相关。实际使用时需要在启动转换后稍微延时几个毫秒具体值取决于 AFE4300 内部低通滤波器的建立时间。5. 参数校准与阻抗测量的常见坑从寄存器到人体成分数据的闭环验证5.1 用标准电阻验证 VirtualSPI 通信和 AFE4300 测量链路拿到板子的第一步不是去测人体而是用标准电阻做链路验证。找一个 0.1% 精度的 100Ω 和 470Ω 电阻分别接到电极接口上读取阻抗值。如果两个电阻读出的数值比例与实际电阻比例一致说明 AFE4300 的线性度和 SPI 通信正常。下面是调试时常用的验证脚本片段// 包含两个已知电阻的测量结果换算成欧姆 // 100R 读数 raw_100470R 读数 raw_470 // 检查 (raw_470 - raw_100) / (470 - 100) 是否接近常数 float scale (float)(raw_470 - raw_100) / (470.0f - 100.0f);如果这个斜率在不同频率下不稳定优先怀疑 AFE4300 的激励电流设置是否正确。电流档位错误会导致输出电压摆幅不同而 ADC 是等比例采样的理论上斜率应该一致但实际电路中放大器可能存在非线性。5.2 电极接触阻抗对测量结果的影响及补偿方法人体成分测量最难以控制的变量是电极与皮肤的接触阻抗。干电极在皮肤干燥时接触阻抗可能高达几十千欧严重时激励电流根本无法有效注入人体。四电极测量能够消除接触阻抗的影响因为电压测量电极几乎没有电流通过。如果采用两电极或简化设计接触阻抗会直接叠加到被测阻抗上测量误差会随皮肤状态变化。这也是为什么正规的体脂秤产品一定要用四电极。在调试原型机时即使硬件上是四电极也建议用导电胶或者湿巾润湿电极以降低接触阻抗带来的噪声。如果发现测量值在短时间内漂移很大通常不是 AFE4300 或者 SPI 的问题而是电极与皮肤接触状态在变化。5.3 用 PC 端工具辅助调参从 STM8 串口导出测量数据做离线分析VirtualSPI 的优点是灵活缺点是你无法直接看到 SPI 波形。为了确认时序是否正确一个实用方式是 STM8S105 把每次 AFE4300 的寄存器读写记录通过串口发送到 PC在串口助手里比对返回数据。特别是在调vspi_delay时将读回的寄存器值与 datasheet 默认值对照可以迅速判断时序偏紧还是偏松。AFE4300 上电后某些寄存器有默认复位值例如设备 ID 寄存器。读回这个固定值做冒烟测试比直接去读 ADC 结果更有意义因为 ADC 结果正常只能说明模拟链路正常而寄存器回读才是验证数字通信链路的最直接证据。我通常在vspi_gpio_init()之后立刻读一次芯片 ID确认是 AFE4300 而不是焊接错误的 AFE4301在调试早期就能排除芯片型号问题。5.4 抗混叠滤波与数据处理顺序人体成分测量中的干扰主要来自 50Hz 工频和电极运动伪迹。AFE4300 内部低通滤波器能够抑制部分高频干扰但工频干扰仍然可能进入 ADC。软件上常用的做法是对原始阻抗值做滑动平均或者中值滤波。滑窗平均适合静态测量场景而运动过程中的人体阻抗测量则应该用中值滤波因为单个离群值会把平均值拉偏。下面是一个典型的中值滤波实现#define FILTER_WINDOW 5 uint16_t median_filter(uint16_t *buf) { uint16_t tmp[FILTER_WINDOW]; for (uint8_t i 0; i FILTER_WINDOW; i) { tmp[i] buf[i]; } /* 简单冒泡排序取中间值 */ for (uint8_t i 0; i FILTER_WINDOW - 1; i) { for (uint8_t j i 1; j FILTER_WINDOW; j) { if (tmp[j] tmp[i]) { uint16_t t tmp[i]; tmp[i] tmp[j]; tmp[j] t; } } } return tmp[FILTER_WINDOW / 2]; }中值滤波的窗口大小选择 5 到 9 之间比较合理过大的窗口会引入滞后效应导致测量值变化跟不上人体呼吸引起的阻抗微动。滤波后的数据再做阻抗换算和人体成分参数提取最终得到的体脂率数值才有参考价值。这里的经验顺序是先剔除异常值、再平滑、最后做标定换算颠倒顺序会把噪声放大。本文还有配套的精品资源点击获取