STM32F103备份寄存器掉电丢失与ADC采样跳变解决方案
2026/9/9 6:23:11 网站建设 项目流程

最近用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 正确配置流程与实测代码

如果是用标准外设库,配置顺序非常明确:

  1. 使能PWR和BKP时钟。
  2. 打开备份域访问权限,也就是解除写保护。
  3. 写入备份寄存器。
  4. 读回验证。
#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为主模式,速率不要超过18MHz,F1的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,超过这个值会直接导致转换结果非线性度变差。如果系统主频是72MHz,ADC预分频器建议选6分频,也就是ADCCLK=72/6=12MHz。

采样周期是影响精度最直接的参数。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自检,确认基础外设全部正常,再往上叠加电机控制逻辑。这样每层问题边界都清晰,排查速度会快很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询