关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约4936字,阅读大约需要 10 分钟
几年前,我做过一款智能插座,需要带电量计量的功能, 比如有个参数是总共用了多少度电 (kWh),这个是需要实时掉存保存的数据。
那问题来了,如果家里突然停电,要怎么在断电前将数据保存至Flash?
问题的核心在于:断电往往是突然且不可预知的。当电源电压开始跌落时,单片机(MCU)还能维持运行的时间(我们称之为“Hold-up Time”或保持时间)非常短暂,通常只有几毫秒甚至更短。这段宝贵的时间主要依赖于板子上VCC电源的滤波电容储存的电荷。
而将数据写入DataFlash并不是瞬间完成的:
擦除(Erase):Flash写入前通常需要先擦除对应的块(Sector)或页(Block)。擦除操作是非常耗时的,动辄几十到几百毫秒。
编程(Program/Write):实际写入数据(通常按页Page写入)也需要时间,虽然比擦除快,但也不是零耗时,也需要若干毫秒。
矛盾点来了: 你需要在极短的、电压还在持续下降的Hold-up Time内,完成可能耗时较长的Flash擦写操作。
一、解决方案:组合拳出击
要完成这个看似不可能的任务,我们需要一套组合拳,缺一不可:
第一拳:预知危险 ,可靠的掉电检测
你得在系统彻底“挂掉”之前,尽可能早地知道“大限将至”。常用的方法有两种:
1.利用MCU内置的电压检测器(BOD/VDET/PVD):
○原理:很多MCU内部集成了电压比较器,可以监控VCC电压。当电压低于预设的阈值(Threshold)时,可以产生一个中断(或者直接复位,但我们需要的是中断)。
○优点:集成在MCU内部,无需额外硬件,响应相对较快。
○缺点:
▪阈值选择是关键。设得太低,触发时留给你的时间可能已经不够了。设得太高,可能在正常工作电压波动下就误触发。
▪某些MCU的BOD功能主要是为了防止低电压误操作,可能直接触发复位,而不是提供一个让你从容保存数据的中断。需要仔细查阅MCU手册。
○配置要点:选择一个合适的电压阈值,配置其产生中断而非复位,并给予这个中断最高或较高的优先级。
2.外部电压监控电路 + GPIO中断:
○原理:使用专门的电压监控芯片或者简单的分压电阻+比较器电路,监控系统主电源(比如+5V或电池电压,通常比MCU的VCC要高,能更早感知跌落)。当电压低于设定值时,该电路输出一个信号给MCU的一个外部中断引脚(如INTx或PCINTx)。
○优点:
▪可以更早地检测到电源跌落(监控的是源头电压)。
▪阈值设置更灵活,可以独立于MCU的BOD。
▪明确产生一个中断信号,控制权在你手里。
○缺点:需要额外的硬件成本和PCB空间。
○配置要点:选择合适的监控点和阈值,确保监控芯片或电路能在足够早的时间点、稳定地输出中断信号。GPIO中断配置为边沿触发(通常是下降沿),并设置高优先级。
无论哪种方式,核心目标是:在VCC电压还足够支撑MCU和DataFlash完成一次写入操作之前,抢到一个可靠的“预警”信号。
第二拳:快速反应 , 高效的数据保存流程
收到了“掉电预警”中断信号,接下来就是生死时速的“抢救”过程:
1.中断服务程序(ISR)要“快闪”:
○掉电检测中断的ISR必须极其简短!绝对不要在ISR里直接执行Flash擦写操作。Flash操作耗时长,且可能需要查询状态、等待,如果在ISR里做这些,会长时间阻塞CPU,甚至导致其他更紧急的中断无法响应,系统直接崩溃。
○ISR的唯一任务:
▪(可选)立即关闭全局中断或降低当前中断优先级,防止被其他不重要的中断打扰。但要小心,别关掉了维持系统运行所必须的中断(比如维持SPI通信的时钟中断等,虽然掉电时可能这些也不重要了)。
▪设置一个全局标志位(例如 volatile bool g_powerDownDetected = true;),通知主循环或其他高优先级任务“大事不好”。
▪(可选)如果使用了实时操作系统(RTOS),可以发送一个紧急信号量或事件给一个专门负责数据保存的高优先级任务。
○迅速退出ISR!让CPU尽快去处理那个标志位或响应高优先级任务。
2.主循环或高优先级任务执行“抢救”:
○在主程序的while(1)循环中,或者在一个专门设计的高优先级任务里,不断检查这个掉电标志位。
○一旦检测到标志位被置位,立即调用数据保存函数。
3.数据保存函数 (SaveCriticalDataToFlash) 的设计原则:
○只救最重要的:明确哪些数据是“非救不可”的。不要试图保存所有RAM里的东西,挑最关键的配置参数、状态变量、计数器等。数据量越小,写入时间越短,成功率越高。
○数据打包:将需要保存的数据整理到一个连续的缓冲区(Buffer)中。
○Flash操作优化 - 重中之重:
▪避免擦除!避免擦除!避免擦除!(重要的事情说三遍)掉电前的宝贵时间绝对不能浪费在耗时的擦除操作上。最佳实践是采用预擦除策略:
•系统正常运行时,或者初始化时,就将用于掉电保存的几个Flash扇区(Sector)预先擦除好,保持为空白状态(通常是全FF)。
•掉电保存时,直接在这些预擦除的扇区中找一个空白页(Page)进行写入。
▪使用页写入(Page Program):DataFlash通常按页写入,这是最快的写入方式。确保你的驱动库使用的是页写入指令。
▪SPI时钟速度:在电源电压尚能支撑的范围内,尽量使用最高的SPI时钟频率,加快数据传输。但要注意电压跌落时,过高的频率可能导致通信不稳定。需要实测权衡。
▪轮询等待对比中断等待:写入Flash后,需要等待操作完成。在掉电场景下,时间极其宝贵,使用忙等待方式查询Flash状态寄存器的“忙”标志位通常比设置中断等待更快、更直接。因为中断响应本身也有开销。
▪DMA?慎用! 虽然DMA可以减轻CPU负担,但在掉电的毫秒级时间内,配置和启动DMA本身也需要时间,且增加了复杂性。除非你的数据量特别大,且CPU在写入期间有其他绝对不能停的任务,否则直接CPU轮询控制SPI可能更简单可靠。
数据校验与恢复机制:
写入前标记,写入后确认:考虑使用一种简单的“事务”机制。例如,在一个状态字中标记“准备写入”,写入成功后再标记“写入完成”。下次上电时检查这个状态字,可以知道上次掉电时是否成功完成了保存。
添加校验和(Checksum/CRC):在保存的数据块末尾加上CRC校验值。下次上电读取数据时,先校验CRC。如果校验失败,说明上次写入可能被打断或数据损坏,此时应使用备份数据或默认值。
乒乓缓冲/双备份区域:
准备两个(或多个)存储区域(A和B)。每次掉电保存时,轮流写入其中一个区域。
同时维护一个“最新有效区域指示符”(可以存在Flash的固定位置,或者也做双备份)。
例如,当前有效数据在A区,掉电时写入B区。写入成功后,更新指示符指向B区。
下次上电时,读取指示符,就知道该从哪个区域加载数据。即使某次写入过程中掉电导致该区域数据损坏,之前的那个有效区域数据仍然存在。这是提高可靠性的常用手段。
第三拳:硬件支撑,充足的“储能”
软件跑得再快,也需要硬件提供基本的运行时间。VCC电源轨上的电容容量至关重要。
•加大电容:评估你的数据保存流程需要多少时间(通过示波器测量SPI信号和掉电中断后的执行时间),然后与硬件工程师沟通,确保VCC(尤其是MCU和DataFlash的供电引脚附近)有足够的大容量电解电容和低ESR的陶瓷电容组合,提供比所需时间更长的Hold-up Time。
•低功耗设计:系统整体功耗越低,同样电容能支撑的时间就越长。虽然掉电保存时可能需要全速运行,但平时的低功耗设计有助于维持更高的初始电压。
•注意器件工作电压下限:确保在电压跌落到触发掉电检测,到实际完成Flash写入的这段时间内,MCU和DataFlash都还能在当前的VCC电压下稳定工作。查阅器件手册中的最低工作电压要求。
二、代码模型示例
// 全局标志位,必须是volatile,防止编译器优化
volatile bool g_powerDownDetected = false;
// 假设需要保存的关键数据结构
typedef struct
{
uint32_t criticalCounter;
uint16_t userSetting1;
uint8_t systemState;
uint16_t crc16; // 数据校验和
} CriticalData_t;
CriticalData_t g_criticalData; // RAM中的关键数据
// 定义DataFlash中用于掉电保存的两个区域地址 (假设已预先擦除)
// 指示当前哪个区域是有效的 (这个状态本身也需要可靠存储,比如存在Flash的固定小块)
// 0: A有效, 1: B有效. 实际应用中这个状态本身也需要掉电保护。
// 简化起见,假设我们能读到上次的状态
uint8_t g_activeSaveArea = 0; // 假设启动时读取到A区是上次有效的
// 函数:计算CRC16 (示例,具体算法自选)
uint16_t CalculateCRC16(uint8_t* data, uint16_t length)
{
// ... 实现CRC16计算 ...
// 返回计算得到的CRC值
return 0; // Placeholder
}
// 函数:保存关键数据到Flash (高优先级任务或主循环中调用)
bool SaveCriticalDataToFlash(void)
{
uint32_t targetAddr;
// 1. 选择写入目标区域 (乒乓切换)
if (g_activeSaveArea == 0)
{
targetAddr = POWER_DOWN_SAVE_ADDR_B; // 当前A有效,准备写入B
}
else
{
targetAddr = POWER_DOWN_SAVE_ADDR_A; // 当前B有效,准备写入A
}
// 2. 准备数据
g_criticalData.crc16 = CalculateCRC16((uint8_t*)&g_criticalData, sizeof(CriticalData_t) - sizeof(uint16_t));
// 3. 写入Flash (假设驱动库函数封装了页写入和忙等待)
// 注意:这里假设Flash扇区是预先擦除好的!
// Flash_WritePage 可能需要地址、数据指针、长度等参数
if (!Flash_WritePage(targetAddr, (uint8_t*)&g_criticalData, sizeof(CriticalData_t)))
{
// 写入失败!(时间可能不够了,或者Flash出问题)
// 在这种极端情况下,能做的不多,也许可以尝试记录一个最小化的错误标志
return false;
}
// 4. 写入成功,更新活动区域指示符
// 这里需要一个非常可靠的方法来更新指示符,它本身也需要掉电保护
// 例如,也写入Flash的一个小区域,同样使用双备份或校验
// UpdateActiveAreaIndicator( (g_activeSaveArea == 0) ? 1 : 0 ); // 切换指示符
g_activeSaveArea = (g_activeSaveArea == 0) ? 1 : 0; // 仅在RAM中更新示例
return true;
}
// 掉电检测中断服务程序 (示例)
void PowerDown_ISR(void)
{
// !!! ISR 必须极简 !!!
// 1. (可选) 清除中断标志位 (根据MCU要求)
// Clear_PowerDown_InterruptFlag();
// 2. 设置全局标志位
g_powerDownDetected = true;
// 3. (可选) 禁用其他低优先级中断
// Disable_LowPriority_Interrupts();
// 4. 快速退出!
}
int main(void)
{
// 系统初始化
System_Init();
SPI_Flash_Init();
// ... 其他初始化 ...
// 初始化时,加载上次成功保存的数据
// LoadCriticalDataFromFlash(); // 需要实现读取和校验逻辑
// 配置掉电检测中断 (假设使用外部中断引脚 INT0)
// Configure_ExternalInterrupt_INT0(FALLING_EDGE, HIGHEST_PRIORITY);
// Enable_INT0_Interrupt();
// 使能全局中断
// Enable_Global_Interrupts();
while (1)
{
// 检查掉电标志位
if (g_powerDownDetected)
{
// 检测到掉电信号!
// (可选) 立即停止或简化其他正在进行的操作
// 执行数据保存
if (SaveCriticalDataToFlash())
{
// 保存成功 (虽然系统马上要关机了)
// 可以考虑进入一个极低功耗的循环,直到电源彻底耗尽
while(1) { /* Spin until power dies */ }
}
else
{
// 保存失败 (时间耗尽或硬件问题)
// 无能为力了...
while(1) { /* Spin until power dies */ }
}
// 注意:一旦进入这个分支,基本就没机会出来了
}
else
{
// 系统正常运行
// ... 执行正常的业务逻辑 ...
// Update_CriticalData(&g_criticalData); // 更新需要保存的数据
// ... 其他任务 ...
Watchdog_Feed(); // 喂狗
}
}
return 0; // 理论上不会执行到这里
}
三、测试与验证
理论说了这么多,怎么知道你的掉电保存机制真的管用?这部分是“良心活”,不能省:
1.测量时间:
用示波器或逻辑分析仪测量从掉电中断触发到Flash写入完成(例如,SPI的CS信号拉高)所需的确切时间。
同样方法,测量你的硬件在触发掉电中断的电压点开始,VCC能维持在MCU和Flash最低工作电压之上的实际Hold-up Time。
确保 Hold-up Time > Flash写入时间 + 一定的余量!
2.模拟掉电:
使用可编程电源,模拟快速的电压跌落,看系统是否能正确触发中断并完成保存。
简单粗暴的方法:直接拔插电源(注意:这可能对某些硬件有损害,且不易精确控制时机,仅作初步验证)。
在测试时,可以在保存成功后,通过串口打印或点亮一个LED(如果还有电的话)来指示成功。
3.反复开关机测试:
进行大量的、不同时间间隔的开关机测试,检查每次上电后读取的数据是否符合预期,是否是掉电前的最后状态。特别关注边界情况,比如刚写完数据就掉电。
4.数据一致性检查:
上电后,务必检查读取数据的CRC校验和或状态标志,确保数据的完整性和有效性。
实现可靠的断电前数据保存,搞起来挺头痛的。它需要软件工程师对MCU特性、Flash操作、中断处理、实时性的深刻理解,也离不开硬件工程师在电源设计、电容选择上的密切配合。这是一场软件算法与硬件物理限制之间的“协同作战”。
记住几个关键点:早检测、快响应、小数据、预擦除、硬支撑(电容)、多验证。做到了这些,产品才更加健壮可靠。
end
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细!