简介:面向STM32初学者的震动检测报警完整工程代码,基于单片机、震动传感器、OLED屏与蜂鸣器,实现震动信号采集、阈值判断、OLED信息显示、声音报警,以及数据经串口发送至调试助手。可应用于防盗报警、机械设备状态监测、便携式震动检测等方向,既适合练习外设驱动,也可作为物联网报警项目的起点。压缩包共232个文件,大小6.44MB,内含C语言源码、头文件、工程配置文件、编译中间文件与清理脚本,能够满足从阅读代码到重新编译运行的完整流程,对工程分析很有帮助。目前已有138人浏览学习,整体代码结构较规范,包括OLED显示、定时器扫描、串口发送等模块,保留的编译中间文件也便于检查编译细节,适合在开发板验证或毕业设计中作为基础框架复用并扩展功能,也有助于深入理解编译与链接过程。
1. 震动报警系统的构成与选型
快递运输、机房设备、机械加工这几类场景里,监控震动最怕的不是传感器没检测到,而是误报把正常颠簸当成破坏性撞击。这套基于 STM32F103 的震动报警系统,把震动传感器、OLED、蜂鸣器和串口串成一条完整链路,核心就是要解决采到什么信号、怎么判、怎么报、怎么把数据交给上位机这四个环节。适合做安防设备、工业监测或智能硬件原型,也适合正在学标准外设库的开发者拆开来读。从工程文件能明显看出它是 Keil + 标准外设库的项目,不是现在常见的 HAL 库。标准库的代码更贴近寄存器,配合 Keil 调试视图可以直接看到外设寄存器变化,对理解 GPIO、ADC、I2C、USART 之间的时序配合比 HAL 库直观得多。先看信号链路的第一环:震动传感器怎么接入。
2. 震动传感器接入与ADC采集阈值判定
2.1 震动传感器的输出类型与STM32接口
市场上的震动传感器模块大致分两类。一类是带比较器的数字模块,比如 SW-420,模块上的 LM393 会把振动信号整形成高低电平,DOUT 直接接 GPIO 就能用;另一类是模拟输出,比如压电陶瓷片加信号调理电路,输出幅度随振动强度连续变化。这套工程源码里包含 stm32f10x_adc.c,说明采集走了 ADC 方式,接的是模拟输出信号或者电位器分压后的波形。接线时 PA1 配置为模拟输入,传感器 VCC 接 3.3V,如果传感器内部没有电平转换,不建议直接接 5V,因为部分模拟输出在 3.3V 单片机上会被钳位,采样值线性度会变差。
注意区分:数字传感器的 DOUT 接 GPIO 后只能判断“有无震动”,而 ADC 采集可以保留振幅信息,方便后面做两级阈值区分“轻微碰撞”和“持续震动”。这也是为什么该工程宁可占用 ADC 和采样时间而不直接读电平的原因。对工业监测来说,这个区分是有实际价值的,轻微碰撞可能只是维护操作,持续强烈震动才需要报警。
2.2 ADC1 单次转换模式配置
ADC 配置用标准库的 ADC_InitTypeDef 结构体,关键是把采样模式定在独立单次上,触发方式选软件触发,这样主循环什么时候想采就什么时候采,不需要额外开 DMA 或中断,代码结构最直观。代码如下:
/* 基于标准外设库的 ADC1 初始化,PA1 模拟输入 */ void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; /* 打开 GPIOA 和 ADC1 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); /* ADC 时钟 = 72MHz / 6 = 12MHz,必须低于 14MHz */ RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_Cmd(ADC1, ENABLE); } /* 单次转换读取采样值,返回 0~4095 的 12 位结果 */ uint16_t ADC_Read(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发转换 while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成 return ADC_GetConversionValue(ADC1); // 读数据寄存器 }几个参数值得解释。RCC_PCLK2_Div6 把系统主频 72MHz 分频到 12MHz,超过 14MHz 会有转换不准的问题,这是超频后 ADC 数值漂移的一个隐蔽原因。ADC_SampleTime_239Cycles5 是最大采样时长,震动信号本身不是高频信号,采样时间拉长能压掉一部分毛刺,等效于硬件上的低通滤波。连续转换模式在这种场景不需要,主循环里按需采一次就行,否则还要额外处理数据存储逻辑,给后面的 OLED、串口带来不必要的耦合。
2.3 阈值分段与连续采样防抖
震动判定最容易踩的坑是单次采样超限就触发报警。真实场景里一个轻微的机械抖动或者电磁干扰,都会在 ADC 值上产生一个尖峰。所以我一般会把阈值分成三段,并且采用“连续 N 次中至少 N-1 次超限才确认”的方式:
| 场景 | 参考范围(0~4095) | 处理动作 |
|---|---|---|
| 静态或环境噪声 | 小于 800 | 正常显示 |
| 轻微碰撞或敲击 | 800~2000 | OLED 警示,不报警 |
| 持续强烈震动 | 大于 2000 | 蜂鸣器报警 + 串口上报 |
具体判定代码:
/* 连续采样判定:threshold 为阈值,count 为采样次数 */ uint8_t Vibe_Check(uint16_t threshold, uint8_t count) { uint8_t i, overtime = 0; for (i = 0; i < count; i++) { if (ADC_Read() > threshold) overtime++; Delay_ms(2); // 打散采样点,避免在同一个振动周期内重复计数 } if (overtime >= count - 1) // 连续 count 次里超过阈值达到 count-1 次 return 1; return 0; }count 一般取 5,间隔 2ms 到 5ms 比较合适。间隔太短等于在同一个震动波形上采多次,失去防抖意义;间隔太长又会把真实的短促撞击漏掉。如果应用场景是电机这类固定频率震源,还可以把间隔设为与震源周期错开的数值,做异步采样来避免共振频率干扰。
3. I2C OLED显示驱动与状态界面布局
3.1 SSD1306 I2C 接口与硬件配置
0.91 寸 128×32 的 OLED 模块是这类报警系统最常见的显示配置。工程文件里有 stm32f10x_i2c.c,说明 OLED 接到 STM32 的硬件 I2C1 上,默认引脚是 PB6(SCL)和 PB7(SDA)。SSD1306 芯片的 I2C 7 位地址出厂通常是 0x3C,若模块背面 SA0 电阻焊接位置不同,也可能是 0x3D。通信时发送地址需要左移一位,即0x3C << 1,这是新手最容易卡住的地方,逻辑分析仪上看到一直 NACK,多半就是地址没移位或者地址本身就错了。
提示:0.91 英寸 OLED 的 I2C 地址不是固定的,SA0 引脚悬空或接法差异会产生 0x3C 和 0x3D 两个地址,先确认模块丝印或扫描 I2C 总线后再改代码。
| 引脚 | 功能 | 配置 |
|---|---|---|
| PB6 | I2C1_SCL | 复用开漏输出,外接上拉 |
| PB7 | I2C1_SDA | 复用开漏输出,外接上拉 |
硬件 I2C 在 F103 上有个经典坑:如果仿真单步执行时卡在 I2C_CheckEvent 的 while 循环里,先确认引脚是不是配成了复用开漏,而不是推挽;再检查模块是否自带 I2C 上拉。这两点都没问题仍然卡死的话,换成 IO 模拟 I2C 是大家最常用的应急方案,驱动接口不变,只改底层读写函数。
3.2 驱动初始化序列与单字节写入
SSD1306 初始化命令序列不用把手册里所有配置都搬来,最小集是关显示、设置寻址模式、开启电荷泵、设置对比度、开显示,顺序错乱会导致花屏或不满屏。写入时控制字节 0x00 表示后续是命令,0x40 表示后续是数据,标准库 I2C 里这一步很容易被忽略:
/* 向 SSD1306 写入一个字节,type=0 命令,type=1 数据 */ void OLED_WriteByte(uint8_t type, uint8_t dat) { uint8_t control = type ? 0x40 : 0x00; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0x3C << 1, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, control); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, dat); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); } /* 初始化序列:关显示、水平寻址、电荷泵开、开显示 */ void OLED_Init(void) { Delay_ms(100); // 等待模块内部 VDD 稳定 OLED_WriteByte(0, 0xAE); // 关闭显示 OLED_WriteByte(0, 0x20); OLED_WriteByte(0, 0x00); // 水平寻址模式 OLED_WriteByte(0, 0x8D); OLED_WriteByte(0, 0x14); // 开启电荷泵 OLED_WriteByte(0, 0xAF); // 打开显示 OLED_Clear(); // 清空显存 }驱动流程里最容易被忽略的是 OLED_Init 开头的延时。模块上电瞬间如果马上发命令,部分 SSD1306 会丢首个字节,导致后续画面花屏。Delay_ms(100) 是对各批次屏幕最保险的取值。OLED_Clear 是对 128×32 的显存区域全部写入 0x00,SSD1306 带内部显存,写完数据后不需要额外刷新指令。
3.3 字符输出与 128×32 界面分区
128×32 只有 4 行 8 像素高的字符空间,布局上我会把它分成两个区域:第一屏显示状态和阈值,第二屏显示震动计数值。字符输出函数的核心是取模数据。西文字符用 5×8 点阵,每个字符 8 字节;中文用 16×16 点阵,每个汉字 32 字节,取模方式选择“列行式 + 阳码”,生成的数据才能直接按列写入 SSD1306 的页地址。网上大量 OLED 汉字取模教程的问题,基本都出在行列模式选择不一致上。用以下方式输出 16×16 汉字:
/* 按 16x16 点阵在 (x, y) 处显示汉字,HZ_Data 为取模数组 */ void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *HZ_Data) { uint8_t i, j; for (i = 0; i < 2; i++) // 上下两页 { OLED_SetPos(x, y + i * 8); // 设置页地址和列地址 for (j = 0; j < 16; j++) // 每页 16 列 OLED_WriteByte(1, HZ_Data[i * 16 + j]); } }OLED_SetPos 实际就是设置列地址低字节、高字节和页地址。显示“震动报警”四个字时,注意 128 像素宽最多放 8 个 16×16 汉字,行高 32 像素放 2 行。我在工程里通常把状态行放在第 0 行,显示 NORMAL 或 ALARM,第 1 行显示阈值范围,第 2、3 行留给计数。这样信息密度正好,不会因为刷新频繁导致闪烁。
4. 蜂鸣器驱动电路与间断报警时序
4.1 有源蜂鸣器与无源蜂鸣器的区别
蜂鸣器报警部分要先判断有源还是无源类型。有源蜂鸣器内部自带振荡电路,给它一个直流高电平就会响,控制逻辑最简单;无源蜂鸣器的“无源”指的是内部没有振荡源,需要外部提供一定频率的方波或 PWM 信号才能发声。判断方法很简单:拿 3.3V 直流电接上去,直接响的是有源,不响的是无源。工程里有 stm32f10x_tim.c,如果板子上用无源蜂鸣器,这个 TIM 大概率服务于 PWM 发声;如果用有源蜂鸣器,TIM 更多是提供延时或系统节拍。
从报警效果看,无源蜂鸣器可以通过改变 PWM 频率形成长短不同的声音序列,比单频长响更能传递紧急程度。代价是代码多一层定时器配置,而且 PWM 占空比和频率需要标定,不同批次的蜂鸣器谐振频率差异不小,装机后往往还要实际听一下再调参数。
4.2 三极管驱动电路与 PWM 输出配置
STM32 GPIO 的推挽输出灌电流能力只有 ±25mA 上下,而蜂鸣器工作电流通常在 30mA 到 80mA 之间,直接驱动不仅拉低引脚电压导致声音发闷,长时间还可能损坏 IO。常用做法是用 NPN 三极管做开关,参数如下:
| 元件 | 参数 | 作用 |
|---|---|---|
| R1 | 1kΩ | 基极限流电阻,I_B 约 3.3mA |
| R2 | 10kΩ | 基极下拉,防止上电误触发 |
| Q1 | S8050 NPN | 高电平导通,低电平关断 |
| D1 | 1N4148 | 并联在蜂鸣器两端,续流防反向电动势 |
| Buzzer | 5V 有源/无源 | 集电极回路负载 |
寄存器配置上有源和无源完全不同。有源蜂鸣器只需要普通推挽 GPIO:
void Buzzer_Init(void) { GPIO_InitTypeDef g; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); g.GPIO_Pin = GPIO_Pin_0; g.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 g.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &g); GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 默认关闭 }参数上 GPIO_Speed_50MHz 在这里是 IO 翻转速度上限,蜂鸣器开关频率很低,用 10MHz 也够,统一 50MHz 是为了和工程其他 GPIO 保持一致,没什么坏处。无源蜂鸣器则要配置 TIM2 的 PWM 输出,PA0 是 TIM2_CH1 的复用引脚,必须设置为复用推挽而不是普通推挽,这是最容易出错的一步:
void Buzzer_PWM_Init(void) { GPIO_InitTypeDef g; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); g.GPIO_Pin = GPIO_Pin_0; g.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽,不是 Out_PP g.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &g); tb.TIM_Prescaler = 72 - 1; // 72MHz / 72 = 1MHz 计数时钟 tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_Period = 1000; // 1kHz PWM 频率 tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 500; // 50% 占空比 oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }这里 TIM_Prescaler 等于 71 时计数时钟是 1MHz,TIM_Period 设为 1000 得到 1kHz。无源蜂鸣器的谐振点通常在 2kHz 到 4kHz,如果声音偏小,把 Period 改小到 500 甚至 250 试试,占空比保持 50% 左右即可。
4.3 间断报警时序设计与自动恢复
报警不能设计成长响,长响会掩盖后续状态变化,长时间运行还容易让操作员听觉疲劳。工程里更实用的设计是“间断报警 + 窗口恢复”:触发报警后蜂鸣器以 200ms 响、200ms 停的节奏工作,循环若干次后自动停,如果震动状态仍处于报警阈值以上,下一个主循环周期会再次进入报警分支。
void Alarm_Task(uint8_t burst) { uint8_t i; for (i = 0; i < burst; i++) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // 蜂鸣器响 Delay_ms(200); GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 蜂鸣器停 Delay_ms(200); } } void Alarm_Reset(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); // 强制关断蜂鸣器 TIM_Cmd(TIM2, DISABLE); // 若使用 PWM,则关闭定时器 }如果板子上是无源蜂鸣器,把 GPIO_SetBits 换成 TIM_Cmd(TIM2, ENABLE),GPIO_ResetBits 换成 TIM_Cmd(TIM2, DISABLE),延时逻辑不变。报警恢复逻辑在震动监测里值得多说一句。震动触发报警后立即恢复是常见误报源:断电瞬间、测试时手碰传感器,都会产生一个短暂脉冲。我一般用一个分钟级计数器做窗口,报警后 30 秒内再次检测到超限,就把报警计数累加并重新计时;超过 30 秒没有再次超限才回到正常状态。这个窗口值按应用场景调,室内安防可以短到 5 秒,机械监测建议拉长到 60 秒。
5. 串口报警数据帧设计与调试助手联调
5.1 帧结构设计
串口上报的核心不是把数字发出去,而是让接收端在任意字节流里都能可靠切分出报警事件。裸发一个 ADC 值没有任何边界,接收方分不清是数据还是噪声。工程适合定义固定长度的帧结构:帧头、类型、状态、计数、校验。报警帧定为 7 字节,大端顺序发送:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| 0 | 0xAA | 帧头 1 |
| 1 | 0x55 | 帧头 2 |
| 2 | 0x01 | 类型:0x01 报警帧 |
| 3 | 状态 | 0x00 正常,0x01 报警 |
| 4 | 计数高字节 | 震动累计次数 uint16 大端 |
| 5 | 计数低字节 | |
| 6 | 校验 | 前 6 字节累加和的低 8 位 |
0xAA 0x55 双帧头是为了和普通数据区分,两个不同字节避免连续相同位导致接收端误同步。校验用累加和低 8 位足够,不需要上 CRC16,这类短帧在 UART 场景下误码率很低,校验的目的是过滤错误帧而不是纠错。
5.2 标准库 USART1 的发送实现
串口初始化走标准库方式,PA9 配置为复用推挽,PA10 配置为浮空输入。波特率 115200,8 数据位,1 停止位,无校验,这是大多数串口调试助手的默认参数,也能避免奇偶校验位带来的帧开销:
void USART1_Init(void) { GPIO_InitTypeDef g; USART_InitTypeDef u; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); g.GPIO_Pin = GPIO_Pin_9; g.GPIO_Mode = GPIO_Mode_AF_PP; // TX 复用推挽 g.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &g); g.GPIO_Pin = GPIO_Pin_10; g.GPIO_Mode = GPIO_Mode_IN_FLOATING; // RX 浮空输入 GPIO_Init(GPIOA, &g); u.USART_BaudRate = 115200; u.USART_WordLength = USART_WordLength_8b; u.USART_StopBits = USART_StopBits_1; u.USART_Parity = USART_Parity_No; u.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; u.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(USART1, &u); USART_Cmd(USART1, ENABLE); } void USART_SendFrame(uint8_t state, uint16_t count) { uint8_t i; uint8_t buf[7]; buf[0] = 0xAA; buf[1] = 0x55; buf[2] = 0x01; buf[3] = state; buf[4] = count >> 8; // 高字节在前 buf[5] = count & 0xFF; buf[6] = buf[0] + buf[1] + buf[2] + buf[3] + buf[4] + buf[5]; for (i = 0; i < 7; i++) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等待数据寄存器空 USART_SendData(USART1, buf[i]); } }用 TXE 标志而不是 TC 标志是个细节。TXE 表示数据已经移入移位寄存器、数据寄存器可以写下一个字节,适合连续发送;TC 表示整个字节包括停止位都送出去了,适合发送完成后立刻关闭串口或进入低功耗的场合。如果混用,可能在最后一个字节上出现丢帧。
5.3 串口调试助手联调与乱码排查
收到数据后,用 SSCOM、正点原子或 XCOM 这类调试助手打开对应串口,把显示模式切到 HEX,才能看到 16 进制的 0xAA 0x55 帧头。联调时有几个高频问题,按下面的表排查:
| 现象 | 最常见原因 | 验证方法 |
|---|---|---|
| 全部乱码 | 波特率与实际主频不匹配 | 先发单个字符看是否稳定 |
| 能发不能收 | TX/RX 交叉接错 | 交换两根线测试 |
| 帧对齐不上 | 上电时序导致首字节丢失 | 帧头用双字节并允许跳过 |
| 偶发丢字节 | 中断优先级配置不当 | 串口中断优先级调到最高 |
乱码问题多说一句。如果代码写的 115200 但接收端全是乱码,优先检查主时钟:外部晶振 8MHz 的标准配置,如果换成了其他频率晶振或者 PLL 倍频配错,USART 波特率会系统性偏移。这种错误在 OLED 和蜂鸣器上几乎看不出来,只有在串口这种对时钟精度敏感的外设上才会暴露。
6. keilkill脚本、工程排错与Flash阈值存储
6.1 工程文件与 keilkill.bat
这包源码目录里有几个一眼看不出用途的文件。project.uvguix.86187 是 Keil uVision 的窗口布局参数,删掉不影响编译;project_sct.Bak 是分散加载文件备份,改过 Bootloader 的人会用它救回配置;project.axf 是链接输出的调试文件。keilkill.bat 就是清理这些编译中间产物的脚本,常见内容:
@echo off del /s /q *.o *.d *.crf *.dep *.axf *.htm *.build_log.htm rd /s /q Listings rd /s /q Objects pause编译一次工程会产生几十个 .o 和 .crf 临时文件,提交代码前跑一遍 keilkill.bat,仓库里就不会混进一堆二进制垃圾。脚本只删编译产物和 Keil 自动生成的 Listings、Objects 目录,源码和 .uvprojx 工程文件不受影响。
6.2 调试连接与芯片型号配置排错
烧录时如果 Keil 报Error: No STM32 target found!,先查 ST-Link 四根线 SWDIO、SWCLK、GND、3V3 是否接全,再检查 Options for Target -> Debug 里选的是不是 ST-Link Debugger。改过引脚复用或进过低功耗模式后连不上,把 BOOT0 拉高到 3.3V,上电后芯片走系统存储器启动,就能重新擦除用户程序。
6.3 报警阈值掉电保存到内部 Flash
震动报警阈值每次上电都要重新设置,实际使用中非常影响体验。更合理的做法是把阈值写到 STM32 内部 Flash 最后一页。工程里的 stm32f10x_flash.c 正好提供这层能力。中容量 STM32F103 的 Flash 页大小为 1KB,擦除必须整页执行,写入只能按半字进行:
void Thresh_Save(uint16_t th) { FLASH_Unlock(); // 解锁 Flash 控制器 FLASH_ErasePage(0x0800F800); // 擦除最后一页 FLASH_ProgramHalfWord(0x0800F800, th); // 半字写入阈值 FLASH_Lock(); // 重新上锁 } uint16_t Thresh_Load(void) { return *(volatile uint16_t *)0x0800F800; // 上电后直接读 }写入前必须整页擦除,这是 Flash 的物理性质决定的,只能把 1 写为 0。0x0800F800 对应 128KB 中容量型号的最后一个页,换成 512KB 高容量型号时页大小变 2KB,地址要重新计算。上电启动时调用一次 Thresh_Load 读到上次保存的值,OLED 上直接显示,串口收到特定命令后修改阈值并写回 Flash,之后就不用为调一个参数反复烧录整包固件了。
本文还有配套的精品资源,点击获取