简介:面向计算机科学与技术、电子信息工程等专业的毕设或课程作业,这是一套完整的STM32F407智能报警系统项目包,涵盖从需求分析、硬件搭建到固件编写、系统联调与测试优化的全流程。项目以Cortex-M4内核的STM32F407为主控,涉及传感器信号采集、阈值判断、蜂鸣器/LED报警触发及用户交互等核心功能,适合需要参考嵌入式系统设计或快速搭建原型的学生。压缩包共191个文件,约6.24MB,以C语言源码(52个h、51个c)、Keil工程文件(uvprojx/uvoptx)及编译生成的hex、axf、map等调试文件为主,另有LCD驱动、STM32标准外设库和批处理脚本,便于直接查看、编译与烧录验证。目前已有89人学习下载。通过这份资源,读者可以拿到完整工程结构、核心驱动代码和报警逻辑实现,理解HAL库/标准库调用、时钟及外设配置方式,并参考实物联调与排错思路,为独立完成同类设计提供扎实基础。
1. STM32F407 智能报警系统:从工程文件看这类毕设的真实构成
压缩包解开来先看到的不是 main.c,而是一串 stm32f4xx_tim.c、stm32f4xx_rtc.c、stm32f4xx_rcc.c、lcd.c、stm32f4xx_adc.c、stm32f4xx_flash.c、stm32f4xx_can.c 这样的外设驱动文件。这说明项目把最花时间的部分落在底层:时钟树怎么配、ADC 怎么走 DMA、RTC 怎么打时间戳、CAN 怎么发报文、Flash 怎么存报警记录,全都单独拆成了模块。报警主流程反而通常写在 main.c 里,把传感器读数、阈值比较、蜂鸣器和 LCD 显示串起来。对于电子信息工程、计算机类毕设来说,价值就在这个地方:一片 STM32F407 同时处理模拟量采集、人机交互和远程通信,每一段驱动都能抽出来复用到下个工程。想从点灯过渡到外设协同的嵌入式开发者,以及正在做 STM32 智能报警系统课程设计的人,这份源码的参考意义比单纯看一块开发板例程大得多。
2. 报警系统的底层顺序:RCC 时钟、TIM 定时与 RTC 时间戳
智能报警系统上电后,CPU 真正跑业务逻辑之前,必须先解决三件事:系统时钟是否稳定、定时器是否按预期产生中断、RTC 是否走了正确时间。工程里的 stm32f4xx_rcc.c、stm32f4xx_tim.c、stm32f4xx_rtc.c 恰恰对应这三件事。它们看着像“标准外设库的搬运工”,实际上决定报警系统后续所有模块的协同基础。
2.1 先看 RCC:外设时钟不是默认全开的
STM32F407 上电后默认使用内部 HSI 时钟,外设时钟大部分是关闭的。如果一上来就操作 GPIO、ADC、CAN,寄存器写进去也不生效,因为对应的总线时钟门控还没打开。所以报警系统的初始化顺序永远先是 RCC,再谈其他。这套工程用的是标准外设库,文件命名和正点原子、野火的早期例程一致,结构比 HAL 库更直白,适合毕设阅读和答辩时讲流程。
以外部 8MHz 晶振为例,标准外设库下的系统时钟配置通常长这样:
void Stm32_Clock_Init(u32 pll_m, u32 pll_n, u32 pll_p, u32 pll_q) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 打开外部高速晶振 while (RCC_WaitForHSEStartUp() != SUCCESS); // 等待起振稳定 RCC_PLLConfig(RCC_PLLSource_HSE, pll_m, pll_n, pll_p, pll_q); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); // 等 PLL 锁定 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 主时钟切到 PLL RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB = 168MHz RCC_PCLK1Config(RCC_HCLK_Div4); // APB1 = 42MHz RCC_PCLK2Config(RCC_HCLK_Div2); // APB2 = 84MHz }调用时传(8, 336, RCC_PLLP_Div2, 7),相当于把外部 8MHz 经过 PLL 倍频到 168MHz。后续 TIM、CAN、ADC 的时钟频率都以这组分频为基准。比如 APB1 定时器时钟会自动倍频到 84MHz,APB2 定时器时钟到 168MHz;CAN 和 UART 挂在 APB1 上,ADC 挂在 APB2 上。这类毕设最常见的翻车点就是 PLL 没配好,LED 不闪、LCD 白屏,先看系统时钟而不是先查外设初始化。
| 总线 | 分频 | 输出频率 | 挂载外设 | 定时器时钟 |
|---|---|---|---|---|
| AHB | 1 | 168MHz | GPIO、DMA、Flash | — |
| APB1 | 4 | 42MHz | UART、I2C、CAN、DAC | 84MHz |
| APB2 | 2 | 84MHz | ADC、SPI、TIM1/TIM8 | 168MHz |
如果工程改用了 HAL 库,SystemClock_Config()里对应的RCC_OscInitTypeDef也是干同一件事。看驱动时先确认是哪套库,再决定查哪份参考手册。
2.2 TIM 定时器:决定采样周期和报警节奏
工程里挂着 stm32f4xx_tim.c,多数情况不是为了用 PWM 驱动一个无源蜂鸣器,就是为了做周期性采样触发。智能报警系统需要对烟雾或温度传感器持续读取,不能一个while(1)死循环无脑刷,否则 ADC 值抖动、LCD 刷新频繁,整个系统的实时性都会被拖垮。
我一般会把 TIM2 配成 1ms 中断作为系统心跳,再用计数变量派生 10ms、100ms 等事件:
void TIM2_Init(void) { TIM_TimeBaseInitTypeDef t; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); t.TIM_Prescaler = 8400 - 1; // 84MHz / 8400 = 10kHz t.TIM_CounterMode = TIM_CounterMode_Up; t.TIM_Period = 10 - 1; // 10kHz / 10 = 1kHz,即 1ms 中断 t.TIM_ClockDivision = TIM_CKD_DIV1; t.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM2, &t); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 1; nvic.NVIC_IRQChannelSubPriority = 0; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); }预分频 8400 是因为这里假设 TIM2 挂在 APB1 定时器时钟 84MHz 上,先分到 10kHz,再在自动重装载里数 10 下,得到 1kHz。如果要让蜂鸣器输出不同频率,可以把 TIM 切到 PWM 模式,修改TIM_Pulse和TIM_Period,就能产生类似报警音的方波。注意有源蜂鸣器只需要电平驱动,无源蜂鸣器必须靠 PWM 或翻转 GPIO 才能发声,选型不同,驱动代码差很多。
2.3 RTC 给报警信息打时间戳
报警系统不能只报“有火警”,还要报“何时发生”。STM32F407 的 RTC 位于备份域,靠 VBAT 引脚供电,主电源掉电后如果能接纽扣电池,时间仍然可以继续走。工程里出现 stm32f4xx_rtc.c,说明作者至少考虑了时间戳功能。
标准外设库下 RTC 初始化重点是先开 PWR 和备份域访问权限:
void RTC_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_BackupAccessCmd(ENABLE); RCC_BackupResetCmd(ENABLE); RCC_BackupResetCmd(DISABLE); RCC_LSEConfig(RCC_LSE_ON); // 外部 32.768kHz 晶振 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); }读取时间时,标准库自带接口很直接:
RTC_TimeTypeDef rtc_time; RTC_GetTime(RTC_Format_BIN, &rtc_time); printf("%02d:%02d:%02d\r\n", rtc_time.RTC_Hours, rtc_time.RTC_Minutes, rtc_time.RTC_Seconds);调试时如果发现时间不走,优先查 LSE 是否起振,再看备份域是否被复位过。很多同学在调试程序时反复按复位键,备份域可能被代码里的RCC_BackupResetCmd(ENABLE)清掉,时间就会归零,这是 RTC 模块排错时最容易忽略的地方。
3. ADC 采集、阈值状态机与 LCD 界面刷新
报警系统真正做的事,是把传感器电压变成数字量,再拿数字量和阈值比较,最后在 LCD 上显示状态。工程里的 stm32f4xx_adc.c 和 lcd.c 覆盖的就是这条数据链路。很多课程作业只做到“ADC 读到值,串口打印出来”,但一个能演示的毕设必须把数据显示到屏幕,并带报警状态切换,这一章就按这个完整链路展开。
3.1 ADC 多通道采集与 DMA
烟雾传感器、气体传感器、热敏电阻这类模拟量器件,输出信号通常直接进 ADC 引脚。STM32F407 的 ADC 是 12 位,量程 0-3.3V。如果接多个传感器,最省 CPU 的做法是 ADC 扫描模式加 DMA,转换结果自动搬到内存数组里,主循环只读数组就够了。
#define ADC_CH_NUM 3 u16 adc_buf[ADC_CH_NUM]; void ADC1_DMA_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_AN; gpio.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, &gpio); adc.ADC_Resolution = ADC_Resolution_12b; adc.ADC_ScanConvMode = ENABLE; adc.ADC_ContinuousConvMode = ENABLE; adc.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; adc.ADC_DataAlign = ADC_DataAlign_Right; adc.ADC_NbrOfConversion = ADC_CH_NUM; ADC_Init(ADC1, &adc); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_84Cycles); dma.DMA_Channel = DMA_Channel_0; dma.DMA_PeripheralBaseAddr = (u32)&ADC1->DR; dma.DMA_Memory0BaseAddr = (u32)adc_buf; dma.DMA_DIR = DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize = ADC_CH_NUM; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; dma.DMA_Priority = DMA_Priority_High; DMA_Init(DMA2_Stream0, &dma); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); }这段代码里有三个关键点。第一,STM32F407 的 ADC1 DMA 请求固定在 DMA2 Stream0,外设寄存器地址是ADC1->DR,这个映射关系换型号就不一样。第二,ADC_NbrOfConversion必须和ADC_RegularChannelConfig里设定的通道数量一致,否则 DMA 搬运尺寸和转换序列对不上,读到的数据位置会错位。第三,ADC_SampleTime_84Cycles表示单次采样时间更长,对阻抗较高的传感器更稳,代价是转换速率变慢,在报警系统里通常足够。
有些同学的工程会把 DMA buffer 放在 STM32F407 的 CCM RAM 里,以为速度快。这个思路本身没毛病,但 CCM RAM 不能给 DMA 访问,adc_buf一旦被放到 0x10000000 段,DMA 搬运就会卡死或者数据全是 0。报警系统里凡是 DMA 要碰的 buffer,都老老实实放普通 SRAM。
3.2 LCD 显示与界面刷新
工程里的 lcd.c 一般对应一块 TFT 彩屏,可能走 FSMC 并行接口,也可能走模拟 SPI。F407 接并口屏常用 FSMC,把 LCD 当成外部存储器写,速度快;接 SPI 屏则更省引脚。不同屏初始化序列差异很大,但上层显示函数基本一致,最后都会落到填点和画色块上。
一个简化的报警界面刷新,不关心具体屏时序,逻辑上是这样的:
void LCD_ShowAlarmPage(u8 alarm_flag, u16 adc_value) { LCD_Clear(WHITE); LCD_ShowString(10, 10, "STM32F407 ALARM", RED, WHITE); LCD_ShowNum(10, 40, adc_value, 4, BLUE, WHITE); if (alarm_flag) LCD_ShowString(10, 70, "ALARM!", RED, WHITE); else LCD_ShowString(10, 70, "NORMAL", BLACK, WHITE); }实际工程里不会每毫秒刷一次屏,因为 TFT LCD 写一整屏要好几毫秒,主循环会被拖死。常见的做法是刷屏标志位,比如在 TIM 中断里置一个lcd_refresh = 1,主循环检测到再调用LCD_ShowAlarmPage。如果这套 LCD 还带了电阻触摸屏,网上常见的四点校准法也能用,不过工程里没有 touch.c,说明作者只做了显示这种更稳的主线功能,答辩时也更容易跑通。
3.3 阈值比较不是只写一个 if
报警功能最怕误报。传感器输出有噪声,采样值可能在阈值附近抖来抖去,如果不加处理,蜂鸣器会一阵一阵响。更稳的做法是加连续计数和滞回比较。
我一般在主循环里放一个轻量状态机:
typedef enum { ST_IDLE = 0, ST_ARMED, ST_ALARM } AlarmState; void Alarm_Task(void) { static AlarmState state = ST_ARMED; static u8 confirm_cnt = 0; u16 adc_now = adc_buf[0]; switch (state) { case ST_ARMED: if (adc_now > ALARM_THRESHOLD) { if (++confirm_cnt >= 5) // 连续 5 次超过阈值才报警 { confirm_cnt = 0; state = ST_ALARM; Buzzer_On(); LCD_ShowAlarmPage(1, adc_now); } } else { confirm_cnt = 0; } break; case ST_ALARM: if (adc_now < ALARM_THRESHOLD - HYSTERESIS) { if (++confirm_cnt >= 20) // 连续 20 次低于回差值才解除 { confirm_cnt = 0; state = ST_ARMED; Buzzer_Off(); LCD_ShowAlarmPage(0, adc_now); } } else { confirm_cnt = 0; } break; } }这个状态机解决两件事。第一,连续 5 次采样仍超阈值,过滤掉单点毛刺;第二,报警解除时用ALARM_THRESHOLD - HYSTERESIS做滞回,防止信号在阈值附近反复进入报警状态。阈值和滞回量怎么设,取决于传感器在正常环境和报警环境下的电压范围。检查ADC 读数 × 3.3 / 4096可以换算出实际电压,再根据传感器手册里的报警浓度对应电压来定阈值,比拍脑袋写一个 3000 更有说服力,答辩时老师一问也能答得上。
| 状态 | 进入条件 | 离开条件 | 输出 |
|---|---|---|---|
| ST_ARMED | 上电默认 | 连续 5 次超阈值 | 蜂鸣器关,显示 NORMAL |
| ST_ALARM | 连续 5 次超阈值 | 连续 20 次低于阈值-滞回 | 蜂鸣器开,显示 ALARM |
4. 掉电保存与 CAN 远程报警:Flash 和通讯模块的扩展
报警系统如果在现场工作,必须考虑掉电后参数还在、报警记录可查、远程能看到状态。工程里的 stm32f4xx_flash.c 和 stm32f4xx_can.c 正好对应这两个扩展点。这一章把它们展开,顺便讲一下工程里那个 keilkilll.bat 是干什么用的。
4.1 片内 Flash 保存阈值和报警日志
STM32F407 的片内 Flash 有 1MB,足以保存报警阈值、校准值和时间戳。用内部 Flash 的好处是不需要外挂 EEPROM,坏处是擦除要按扇区来,而且写入次数有限。工程里出现 stm32f4xx_flash.c,我猜测它的用途就是存日志或阈值参数。
写 Flash 的标准流程是三段式:解锁、擦除扇区、编程。以下以扇区 11 为例:
#define ALARM_LOG_SECTOR 11 #define ALARM_LOG_ADDR 0x080E0000 typedef struct { u32 timestamp; u16 adc_value; u16 reserved; } AlarmLog_t; // 保证 4 字节对齐 void Flash_WriteLog(AlarmLog_t *log) { u32 *src; int i; FLASH_Unlock(); while (FLASH_GetStatus() == FLASH_BUSY); // 等上次操作结束 FLASH_EraseSector(ALARM_LOG_SECTOR, VoltageRange_3); src = (u32 *)log; for (i = 0; i < sizeof(AlarmLog_t) / 4; i++) { FLASH_ProgramWord(ALARM_LOG_ADDR + i * 4, src[i]); } FLASH_Lock(); }这里要注意,sizeof(AlarmLog_t)必须是 4 的倍数,否则按u32指针遍历会越界读取。其次,擦除扇区期间不能发生中断里也去访问 Flash,否则可能进入 Busy 死等甚至造成数据损坏。报警日志写入在代码里应该放在主循环或事件处理中,绝对不要放在 RTC 中断里。
调试时另一个坑是 Flash 写次数。毕设演示期间反复擦写同一个扇区,短时间不明显,但如果做成长期跑的系统,建议用轮询方式分散写入区域。STM32F407 的 CCM RAM 和普通 SRAM 都可以给报警状态变量用,但 Flash 缓冲区不能放 CCM RAM,这点和 DMA buffer 的限制类似。
4.2 CAN 总线:远程报警和传感器节点级联
工程里的 stm32f4xx_can.c 是这个报警系统里比较有意思的部分。智能报警系统如果只在一块板子上,CAN 用处不明显;但要是多个房间、多个检测点,CAN 总线比串口更稳,距离更远,节点也能通过过滤器隔离无关帧。对于毕设来说,CAN 很容易扩展成“一主多从”的演示场景。
标准外设库下 CAN1 初始化,需要把 PB8/PB9 复用为 CAN 引脚,波特率由 APB1 时钟和分频参数共同决定。示例配置如下:
void CAN1_Init(void) { GPIO_InitTypeDef gpio; CAN_InitTypeDef can; CAN_FilterInitTypeDef filter; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_PinAFConfig(GPIOB, GPIO_PinSource8, GPIO_AF_CAN1); GPIO_PinAFConfig(GPIOB, GPIO_PinSource9, GPIO_AF_CAN1); gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; gpio.GPIO_Mode = GPIO_Mode_AF; gpio.GPIO_Speed = GPIO_Speed_50MHz; gpio.GPIO_OType = GPIO_OType_PP; gpio.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOB, &gpio); can.CAN_TTCM = DISABLE; can.CAN_ABOM = ENABLE; can.CAN_AWUM = DISABLE; can.CAN_NART = DISABLE; can.CAN_RFLM = DISABLE; can.CAN_TXFP = ENABLE; can.CAN_Mode = CAN_Mode_Normal; can.CAN_SJW = CAN_SJW_1tq; can.CAN_BS1 = CAN_BS1_8tq; can.CAN_BS2 = CAN_BS2_7tq; can.CAN_Prescaler = 6; CAN_Init(CAN1, &can); filter.CAN_FilterNumber = 0; filter.CAN_FilterMode = CAN_FilterMode_IdMask; filter.CAN_FilterScale = CAN_FilterScale_32bit; filter.CAN_FilterIdHigh = 0x0000; filter.CAN_FilterIdLow = 0x0000; filter.CAN_FilterMaskIdHigh = 0x0000; filter.CAN_FilterMaskIdLow = 0x0000; filter.CAN_FilterFIFOAssignment = CAN_FIFO0; filter.CAN_FilterActivation = ENABLE; CAN_FilterInit(&filter); }APB1 时钟 42MHz,Prescaler=6后位时间 = 1 + 8 + 7 = 16 TQ,最终波特率约 437.5Kbps,能够满足大多数报警节点通信。发送报警帧时,填充报文 ID 和数据,然后CAN_Transmit:
CanTxMsg tx; u8 alarm_type = 0x01; u16 adc_value = adc_buf[0]; tx.ExtId = 0x181; tx.IDE = CAN_Id_Extended; tx.RTR = CAN_RTR_Data; tx.DLC = 5; tx.Data[0] = alarm_type; tx.Data[1] = adc_value >> 8; tx.Data[2] = adc_value & 0xFF; CAN_Transmit(CAN1, &tx);CAN 通信调试时最容易忘的是终端电阻。两根 CAN 线要在总线两端各接一个 120Ω 终端电阻,否则波形反射严重,报文时通时断。另外滤波器如果设成全部屏蔽,接收缓冲区会收到大量无关帧,建议在裸机代码里先屏蔽所有 ID,再逐个放行需要处理的报警 ID。
4.3 工程维护:keilkilll.bat 与中间文件
压缩包里那个 keilkilll.bat 是作者用来清理 Keil 中间文件的。ST project 编译一次会产生 .o、.crf、.d、.htm、.dep、.axf 一类文件,打包发网盘时体积会变大,整理源码时也容易把垃圾文件代入。一个简单的清理脚本长这样:
@echo off del /s /q *.o *.d *.crf *.htm *.dep *.lst *.tmp 2>nul del /s /q Listings\*.* Objects\*.* 2>nul rmdir /s /q Listings Objects 2>nul echo Keil project cleaned.使用这段命令时要小心:如果工程目录里只有一份源文件,清理后重新编译没问题;但如果.axf被误删,调试器下一次下载会报找不到可执行文件,在 Keil 里点一次 Rebuild 就好。另外,.uvguix.Administrator是 Keil 编辑器窗口布局文件,只记录打开哪些文件、断点位置,不参与编译,删了也不影响功能。
5. 用 ITEMP.axf 反查工程:编译调试中的三个实用技巧
最后这一章不写大逻辑,只写三个我拆这类 STM32F407 工程时实测过的技巧。拿到了别人编译好的 ITEMP.axf,不一定要重新整个编译才能判断问题。
第一个技巧是直接用 ARM 的 fromelf 工具反汇编 axf 文件,确认当前程序烧录后执行路径是否符合预期。Keil 安装目录下自带 fromelf,命令行可以这样用:
fromelf --text -c -d -o dis.txt ITEMP.axf打开 dis.txt 后,搜索Alarm_Task、TIM2_IRQHandler这些函数名,可以快速确认代码里有没有被编译器优化掉关键逻辑。比如报警状态机如果被整体优化成死代码,反汇编里基本找不到对应的函数体;再比如中断处理函数名写错,向量表里没有入口,反汇编里也能看到符号缺失。这种验证方式比在源码里找半天更快,尤其适合拿到手的是已经编译过的工程。
第二个技巧是调试 RTC 时间戳类功能时,不要用复位键打断。STM32F407 的备份域可以在主电源复位后继续工作,但工程初始化代码里如果执行了RCC_BackupResetCmd(ENABLE),每次上电都会把 RTC 时间清零。我会在调试阶段直接注释掉这一行,再单独写一个RTC_SetTime设置好时间,之后断点随便打,时间戳都能连续。等到真正交作业或演示前,再把备份域复位逻辑恢复回去,保证掉电后时间不掉电不丢。
第三个技巧和 Keil 工程清理有关。工程编译时如果报“Error: L6218E: Undefined symbol”,先不要急着找源码,大概率是某个.c文件没被加进工程,或者keilkilll.bat把Listings和Objects目录删掉后,编译配置里的输出路径不存在了。这时候在 Keil 里重新全量编译一次,右键 Main Group,选择 Add Existing Files to Group,把不存在的 stm32f4xx_tim.c 等文件重新加回来。需要验证 axf 是否最新,可以看 Keil 输出窗口里编译结束后那行Program Size: Code=... RO-data=...,如果 Code 尺寸和源文件改动对不上,说明编译不是增量完成的。
我自己拆这种毕设工程时,通常不会一上来就把所有代码通读一遍,而是先看ITEMP.axf的时间戳和文件大小,再打开.uvguix.Administrator看作者最后编辑过哪些文件,然后从RCC → TIM → RTC → ADC → LCD → Flash → CAN的初始化顺序往下追。这套顺序本身就是智能报警系统的数据流,也是报警触发链路的完整骨架。拿到手无需重新造轮子,照着这个顺序逐层验证,比从 main.c 死磕一行行读更省时间。
本文还有配套的精品资源,点击获取