最近用stm32f103c8t6做了块带无刷电机电流采样的小控制板调试时撞上了两个非常典型的问题一个是备份寄存器掉电后数据不保存另一个是ADC采样值跳得厉害、读数也不准。两个问题看着不相关追到最后都指向同几个底层环节——备份域供电、主电源去耦、参考电压稳定、采样时序配置。这篇文章就把这两个问题的排查思路、根因、解决方案和代码完整写出来。适合正在玩stm32f103c8t6最小系统板、自己做控制板或者做电流/电压采样的朋友参考内容偏工程实践不写空洞原理直接给能抄作业的配置和代码。1. 备份寄存器为什么掉电不保存1.1 先搞清楚备份寄存器挂在哪个电源域STM32F103C8T6内部有42个16位备份寄存器支持把每两个合并成一个32位寄存器使用。它的作用是在系统复位、进入待机模式甚至外部主电源掉电后依然保留关键数据。注意这里的“掉电后依然保留”有一个大前提VBAT引脚必须一直有电。很多人一开始就把备份寄存器当成普通的非易失存储器以为只要写进去就万事大吉。实际上它挂在备份域电源上跟RTC共享一套供电逻辑。VBAT引脚的供电消失备份域逻辑就跟着断电寄存器里的内容自然清零。换句话说备份寄存器并不是Flash不能脱离电源保存数据。另外还有一个很隐蔽的点备份寄存器在芯片刚上电、备份域刚建立供电的瞬间内容是不确定的。如果你的程序在上电后就直接读备份寄存器并用它做逻辑判断很可能读到0xFFFF或者随机值这就被误判成“数据没保存”。正确做法是先写一个固定的标志值比如存一个0xA5A5读的时候先判断标志再决定是否信任当前的数据。1.2 掉电不保存的常见根因清单我实际排查过不少案例掉电不保存的原因基本跑不出这几类VBAT引脚悬空或者只接了一颗滤波电容主电源掉电后VBAT电压跟着跌数据随之丢失。软件没有使能PWR和BKP时钟也没有解除备份域写保护写入操作全被硬件屏蔽了。使用了HAL库但调用顺序不对比如先写寄存器再开备份域访问权限。备份域意外复位比如NRST引脚毛刺、电源在上电过程中出现抖动、侵入检测引脚电平被拉低。VBAT虽然接了电池但电池电压过低低于备份域的最低工作电压。我最常见到的还是第一种。很多原理图为了省事直接把VBAT跟3.3V连在一起这种做法本身没错但如果你希望主电源掉电后数据还能保留就必须额外给VBAT提供持续电源。1.3 正确配置流程与实测代码如果是用标准外设库配置顺序非常明确使能PWR和BKP时钟。打开备份域访问权限也就是解除写保护。写入备份寄存器。读回验证。#include stm32f10x.h void BKP_Init(void) { // 1. 开启PWR和BKP时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); // 2. 打开备份域访问权限 PWR_BackupAccessCmd(ENABLE); // 3. 写入测试数据 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } uint16_t BKP_ReadValue(void) { return BKP_ReadBackupRegister(BKP_DR1); }如果用的是HAL库标准库那套直接操作寄存器的思路在F1上依然走通过但更贴近HAL风格的做法是这样#include stm32f1xx_hal.h void BackupRegister_Write(uint32_t regIndex, uint16_t data) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_BKP_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess(); // 以RTC备份寄存器接口写入DRx编号从0开始 HAL_RTCEx_BKUPWrite(hrtc, regIndex, data); } uint16_t BackupRegister_Read(uint32_t regIndex) { return HAL_RTCEx_BKUPRead(hrtc, regIndex); }注意HAL库在F1上并没有单独封装BKP读写函数多数教程都是借助RTC的备份寄存器接口来操作。实际写之前必须先把hrtc初始化好哪怕你根本不用RTC功能也要创建一个RTC句柄。提示调试备份寄存器时建议写完后立刻读回来验证一次。如果写入后读出来还是原来的内容或0xFFFF那说明大概率连写都没写进去而不是掉电丢数据。2. 备份寄存器容量不够时如何实现真正“掉电不丢失”的存储2.1 备份寄存器 vs 外部Flash/EEPROM备份寄存器虽然好用但容量实在有限。42个16位寄存器满打满算84字节实际能安全使用的数据量还要再打个折扣。一旦需要保存PID参数、零点校准值、扭矩曲线这类稍大一点的数据就必须考虑其他方案。外部存储方案中最常用的就是EEPROM和SPI NOR Flash。EEPROM比如AT24C02单字节写入方便容量小SPI NOR Flash比如W25Q64容量大、速度快但需要块擦除才能写入。三类方案对比存储方案典型容量掉电保存写入复杂度使用寿命STM32内部备份寄存器42 x 16位需VBAT持续供电最低无擦写限制外部EEPROM2KB~1Mb自保持低100万次擦写外部SPI Flash1Mb~128Mb自保持中10万次擦写我的习惯组合是运行中需要频繁修改、又要求掉电保留的关键参数优先放备份寄存器因为读写几乎没有延迟需要保存的配置表、校准数据放EEPROM或SPI Flash。这样既满足掉电不丢失又不会因为频繁写Flash把寿命写没了。2.2 W25Q64掉电不丢失方案实操W25Q64是8MB SPI Flash掉电不丢失在很多stm32f103c8t6项目里都会用到。接法和普通SPI设备一样CS、CLK、MOSI、MISO四根线通常还可以把WP和HOLD引脚拉高。用CubeMX配置SPI1为主模式速率不要超过18MHzF1的SPI外设最高可以跑到18M实际为了稳定建议压到9M以内。W25Q64支持的最快时钟是104MHz所以瓶颈在MCU侧。下面是一段最基础的读ID和写读测试流程// SPI发送接收一个字节 uint8_t SPI_TransferByte(SPI_HandleTypeDef *hspi, uint8_t byte) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi, byte, rx, 1, 100); return rx; } // 读取W25Q64芯片ID void W25Q64_ReadID(uint16_t *manufacturer, uint16_t *device) { uint8_t tx[4] {0x90, 0x00, 0x00, 0x00}; uint8_t rx[4] {0}; HAL_GPIO_WritePin(W25Q64_CS_PORT, W25Q64_CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx, rx, 4, 100); HAL_GPIO_WritePin(W25Q64_CS_PORT, W25Q64_CS_PIN, GPIO_PIN_SET); *manufacturer rx[3]; // 0xEF *device rx[2]; // 0x17 }这里必须强调一点W25Q64在写数据之前目标扇区必须是擦除状态。如果不先擦除写入操作会在某些bit上失败。很多第一次用的人写进去读出来不对第一反应是SPI配置有问题实际上就是忘了擦除。另外W25Q64的页编程一次最多写256字节跨越页边界时需要手动拆分。如果直接让HAL库发一个超过一页的数据数据会在页末尾被截断或回卷到页开头这是非常容易踩的坑。2.3 为什么推荐“备份寄存器优先 外部Flash补充”简单总结我的选型思路能用备份寄存器解决问题就别轻易上外部Flash因为备份寄存器不需要额外的通信接口也不涉及擦写寿命问题。但它的容量和断电保持条件决定了它不适合装大数据。当参数量超过100字节时直接上SPI Flash或EEPROM。尤其是做无刷电机驱动板FOC控制的电流环PID参数、电角度零点偏移、电流采样增益校准值这些数据加起来往往超过备份寄存器容量。把这些参数存到W25Q64或24C02里上电再加载到RAM才是成熟的做法。3. ADC采样准确性问题的根因分析3.1 12位SAR ADC是怎么工作的stm32f103c8t6内置的ADC是12位逐次逼近型也就是常说SAR架构。它的基本思路是先用采样开关把输入电压保持到内部采样电容上然后通过一个内部DAC从最高位开始逐步比较每一位比较出一个二进制结果最终得到12位数字量。这个过程中最容易被忽略的是“采样保持阶段”。采样开关导通时间不够采样电容上的电压还没来得及稳定到输入电压ADC就开始转换了结果自然会偏低。SAR ADC的转换精度在很大程度上取决于采样阶段的建立时间而不是转换阶段。一个生活化的类比用杯子接水如果水还没接满就开始量体积量出来的结果肯定不对。采样时间就是“接水”的过程不能太短。3.2 采样不准主要由哪些原因造成根据我遇到的案例ADC采样不准的表现和原因可以整理成一张排查表现象典型原因排查方向读数整体偏低采样时间太短信号没建立增大采样周期降低ADC时钟读数跳变、最后几位乱跳VDDA纹波太大或参考地不干净电源加磁珠和电容改善GND布局特定通道读数异常偏大或偏小输入引脚悬空或串阻过大给输入加RC滤波确认信号源内阻满量程误差超1%VREF实际电压与预期不符测量VREF引脚电压校准增益某个通道受相邻通道干扰多通道扫描时间间隔太短提高采样周期或改用多次采样有规律地周期性跳变ADC时钟频率过高将ADCCLK降到12MHz以下上电后第一次读数不准未执行校准调用校准函数后再启动转换3.3 硬件电路上的关键细节参考电压、电源去耦、输入阻抗软件调得再好硬件底子不行也白搭。ADC的参考电压直接决定采样精度VREF引脚必须接一个干净的电压源。stm32f103c8t6没有单独的VREF引脚参考电压等于VDDA。VDDA必须单独做滤波不要直接跟VDD连在一起。我常用的做法是VDDA通过10欧姆电阻接到VDD然后VDDA引脚对地接一个1uF陶瓷电容再加一个10nF高频电容。本质上就是在数字电源和模拟电源之间做隔离避免板上数字电路开关噪声污染参考电压。输入阻抗方面STM32F103的ADC输入阻抗不是无限大内部等效阻抗大约几十k欧姆。如果信号源内阻太大会导致采样电容在采样阶段充不满。最简单的解决方法是加一个运放做跟随器或者用一颗几百欧姆的电阻配一颗几十nF的电容做低通滤波。电流采样应用中最常用的电路是采样电阻两端差分信号经过运放放大后送到ADC输入运放输出端再串一个100欧姆电阻对地接一个1nF到10nF的电容滤掉高频毛刺。这个RC组合到ADC引脚的走线越短越好。4. 软件层面把ADC“喂准”时钟、采样周期、校准与滤波4.1 ADCCLK和采样周期的计算STM32F103的ADC最大时钟频率是14MHz超过这个值会直接导致转换结果非线性度变差。如果系统主频是72MHzADC预分频器建议选6分频也就是ADCCLK72/612MHz。采样周期是影响精度最直接的参数。F1的ADC支持从1.5周期到239.5周期的采样时间选择。总转换时间可以用下面公式估算总转换时间 采样周期 12.5个周期所以当采样周期设为13.5个周期时总转换时间就是26个ADC时钟周期换算成时间就是26/12MHz约2.17微秒。这种配置适合输入信号源阻抗比较低、信号变化不太快的场景。用CubeMX配置时在ADC参数里把Sampling Time从默认的1.5 Cycles改成55.5 Cycles或更大的值。很多人图省事用默认值采样时间太少读数忽高忽低。F103的ADC本身转换速度不慢平时采样用55.5周期完全够用慢一点换来的是稳定。4.2 ADC校准与偏移修正F1系列ADC内置校准逻辑。芯片出厂时会提供一个校准因子但这个因子不是永久固定的会受环境温度、电源电压、芯片内部特性影响。每次上电或每次ADC初始化之后都应该重新校准一次。使用HAL库时校准代码很简单void ADC_Calibrate(ADC_HandleTypeDef *hadc) { HAL_ADCEx_Calibration_Start(hadc); }校准必须在ADC上电后、开始转换之前调用而且最好在没有任何转换任务排队时进行。如果芯片运行中温度变化大可以定期重新校准。此外如果ADC读数整体偏移可以在软件里做偏移修正。一个简单做法是将ADC输入接地采样得到的值记为offset以后每个采样值都减去offset。如果做的是无刷电机双电阻电流采样零点偏移直接决定了电流为零时读到的值是否精确这个修正非常关键。4.3 常用滤波算法与代码ADC滤波算法有好几种效果差异很大。我实际项目中用得最多的是滑动平均和递推低通滤波。滑动平均适合信号变化平缓的场景核心思路是维护一个固定长度的窗口每次取最新的数据替换最旧的数据取平均值。窗口长度越大越平滑但实时性变差。做电流采样选8次到16次窗口比较合适。#define ADC_FILT_WIN 16 uint16_t adc_buf[ADC_FILT_WIN]; uint8_t adc_buf_index 0; uint32_t adc_sum 0; uint16_t ADC_SlideAverage(uint16_t new_value) { adc_sum - adc_buf[adc_buf_index]; adc_buf[adc_buf_index] new_value; adc_sum new_value; adc_buf_index (adc_buf_index 1) % ADC_FILT_WIN; return (uint16_t)(adc_sum / ADC_FILT_WIN); }递推低通滤波的实时性更好代码也非常简单float adc_filtered 0; #define ALPHA 0.2f uint16_t ADC_LowPass(uint16_t new_value) { adc_filtered adc_filtered * (1.0f - ALPHA) (float)new_value * ALPHA; return (uint16_t)adc_filtered; }ALPHA越大对新数据越敏感滤波效果越弱ALPHA越小输出越平滑但延迟也越大。我常用0.1到0.3之间的值具体根据控制环路更新频率来调。4.4 多通道采样与DMA实操多通道ADC采集时如果不用DMA程序需要反复轮询EOC标志浪费CPU而且在切换通道时容易漏数据。用DMA加扫描模式是标配做法。CubeMX里的配置要点ADC Mode设置为Scan模式。Number Of Conversion设置为需要的通道数。每个通道单独设置Rank、采样周期。DMA设置为Circular模式数据宽度Half Word。DMA中断可选一般开传输半满和全满中断方便分帧处理。DMA缓存建议使用全局数组例如#define ADC_CH_NUM 4 uint16_t adc_dma_buf[ADC_CH_NUM];启动方式是HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_dma_buf, ADC_CH_NUM);开DMA后有个常见的坑规则组扫描模式下DMA数据写入数组的顺序是固定的对应CubeMX里配置的Rank顺序。如果你在运行中更改了某个通道的Rank数组里的对应关系就会变必须同时更新处理函数。另一个坑是第一次转换结果可能不准通常的解决办法是启动后先空读一次或者丢弃前几组数据。5. 复盘无刷电机电流采样项目的两个教训5.1 现象和硬件结构我那块控制板的主控是stm32f103c8t6驱动部分用双电阻电流采样采样电阻选择的是毫欧级合金电阻运放把差分信号放大后送进ADC。备份寄存器用来保存电机的零点角度和电流环校准参数期望掉电后不丢失。结果第一次上电测试就碰到两个问题一是每次断电再上电之前靠近零点角度寄存器里的数据都会丢失必须重新校准二是电机低速转动时ADC读取的相电流值抖动非常明显尤其在小电流区间线性度很差。5.2 备份寄存器丢数据排查过程我首先检查硬件原理图发现VBAT引脚只接了一个100nF电容没有接电池也没有接法拉电容。主电源断开后100nF电容瞬间放完电备份域直接掉电。这就是丢失的根本原因之一。硬件上处理之后我再看了软件发现初始化代码里时钟使能和备份域访问使能都有了但写数据时用的是HAL_RTCEx_BKUPWrite这个接口在RTC没有被正确初始化时会一直写不进去。我在初始化RTC后重新测试写入和读回终于正常。最后我在主程序里加了一个启动标志上电先判断备份寄存器里的标志值是否等于0xA5A5等于才使用备份数据否则强制重新校准并把校准结果重新写入。这样就彻底避免了误用随机值的问题。5.3 三相/双电阻采样ADC不准的排查过程电流采样不准的排查比备份寄存器复杂很多。我先用万用表量运放输出发现静态电压有几十mV的波动说明硬件电路本身有一定噪声。在运放输出到ADC之间加了一级RC低通滤波后静态波动明显改善。接着看软件配置原来的ADC采样周期是1.5周期ADCCLK为12MHz算出来总转换时间只有1.17微秒。这么短的采样时间对一个小阻抗信号源可能够用但运放输出带了RC滤波之后信号源阻抗就变高了1.5周期的采样时间根本不够。我把采样周期改成55.5周期后数据跳变幅度从几十个LSB降到几个LSB。最后还做了零点偏移修正。双电阻采样在电机不转时理论上两个电流采样通道都应该是0A但ADC读到的数值有偏移。我在电机启动前执行一次自校准采集64次取平均把结果作为零点偏移保存到备份寄存器里。这个偏移值每块板子都不同不带修正直接算电流小电流时误差能到几百毫安。注意无刷电机电流采样的PCB布局中采样电阻的走线要用开尔文接法也就是电流走线和电压采样走线分开否则大电流在PCB铜箔上的压降会被当成采样信号的一部分导致电流读数完全不准。6. 调试这类问题需要准备的几样东西如果你也打算复现或继续深入这些问题我建议手头准备一个逻辑分析仪、一个稳压电源和一块靠谱的最小系统板。逻辑分析仪用于观察SPI、UART时序稳压电源用来单独给模拟电路供电排查电源噪声对ADC的影响会非常有用。还有一个技巧备份寄存器和ADC采样是两个看起来独立的问题但它们都涉及板级电源设计。很多板子把模拟电源、数字电源、备份电源全混在一起出问题时无从下手。设计新板子时至少应保证VDDA和VDD之间有磁珠或电阻隔离VREF和VBAT分别退耦这样才能分别隔离问题。我自己最后养成的习惯是每次画完板子先不焊接MCU单独给VBAT供电用万用表确认备份域电源没有短路焊完MCU后先烧一个最简单的例程只做备份寄存器写入读回和ADC自检确认基础外设全部正常再往上叠加电机控制逻辑。这样每层问题边界都清晰排查速度会快很多。