☰
STM32F103驱动DHT11温湿度传感器实战:时序精控与抗干扰设计
2026/10/20 18:48:29 网站建设 项目流程

简介:本资源是一套基于STM32F1系列微控制器实现温湿度监测与OLED显示的完整嵌入式开发工程,面向嵌入式初学者、电子类课程设计学生及STM32入门开发者,解决传感器数据采集、I2C/OLED驱动、单总线通信(DHT11)及软硬件协同调试等典型实践问题。压缩包共205个文件,含36个C源码(如stm32f10x_i2c.c、stm32f10x_adc.c)、36个头文件(.h)、38个编译中间文件(.o/.d)、37个代码覆盖率报告(.crf),以及Keil工程文件(.uvprojx/.uvoptx)、启动脚本、Hex固件与调试配置等,整体7.67MB,结构完整,覆盖从CubeMX初始化到应用层显示逻辑的全链路代码。已有672人学习下载,资源提供可直接编译运行的工程框架、DHT11时序解析与校验实现、OLED I2C驱动封装、异常处理机制及典型硬件连接说明,助读者快速掌握传感器接口开发与嵌入式人机交互实现。

1. 为什么用 STM32 + DHT11 做温湿度检测,反而比直接买成品模块更稳、更可控?

你是不是也试过插上某款“即插即用”的USB温湿度仪,软件一打开就报错驱动不兼容?或者在嵌入式项目里,把现成的串口屏模块接上去,结果发现它内部温湿度芯片采样周期固定、数据无法校准、断电后历史记录全丢?——这恰恰是“黑匣子方案”最典型的翻车现场。而基于 STM32 和 DHT11 的温湿度检测显示,本质不是“从零造轮子”,而是把感知层、处理层、显示层全部握在自己手里:你能精确控制 DHT11 的时序握手(哪怕它玄学),能用 STM32 的 ADC 或 GPIO 精确标定传感器偏移,能把采集值实时打到 OLED 屏上,还能顺手加个按键切换摄氏/华氏、存进 Flash 做掉电保存、甚至通过 UART 把原始波形发给上位机做时序诊断。这不是复古情怀,是当项目进入调试深水区时,你唯一能伸手进去拧螺丝的地方。适合正在做课程设计、毕业设计或工业边缘节点原型的开发者——尤其当你需要确认“到底是传感器坏了,还是我的延时写错了”,而不是对着成品模块的说明书干瞪眼。


2. 从原理到选型:为什么是 DHT11 而不是 DHT22?为什么是 STM32F103C8T6?

DHT11 和 DHT22 都是单总线数字温湿度传感器,但它们的底层协议、精度、响应速度和抗干扰能力差异极大,直接决定你后续代码能不能跑通、要不要反复改延时。很多新手一上来就抄“DHT22 + STM32”例程,结果在 F103 上死在第 40 个 bit 的高电平超时——因为 DHT22 要求微秒级精准延时(典型高电平持续 70μs ± 5μs),而标准库 SysTick 定时器在 72MHz 主频下最小分辨率为 ~13.9ns,看似够用,但实际调度开销、中断嵌套、编译器优化都会让裸延时漂移。DHT11 则宽松得多:它的数据位高电平持续时间为 70μs(逻辑0)或 28μs(逻辑1),低电平为 50μs,整个帧长仅约 4ms,对时序容忍度高一个数量级。更重要的是,DHT11 输出的是整数型温湿度(湿度 20–90% RH,温度 0–50℃),无小数位,非常适合资源受限的 Cortex-M3 内核(如 STM32F103C8T6)做快速解析,避免浮点运算拖慢主循环。

再看 MCU 选型:STM32F103C8T6 是“蓝 pill”开发板的核心芯片,48MHz/72MHz 可配、64KB Flash、20KB RAM、2×SPI、3×USART、1×I²C、12-bit ADC ×16 —— 它不是性能最强的,但却是生态最稳、资料最多、引脚复用冲突最少、且 GPIO 翻转速度足以满足 DHT11 单总线协议的黄金平衡点。实测中,用GPIO_ResetBits()+GPIO_SetBits()控制 PB1 做 DHT11 DATA 线,在 Keil MDK 下开启 O2 优化后,高低电平切换延迟稳定在 1.2μs 内,完全覆盖 DHT11 所需的 ≥ 80μs 低电平启动信号与 ≥ 80μs 高电平响应窗口。相比之下,某些国产 RISC-V MCU 虽然主频更高,但 GPIO 寄存器操作未做位带映射,一次置位需读-改-写,耗时达 3~5μs,极易触发 DHT11 的“无响应”错误。

提示:DHT11 的供电电压必须严格控制在 3.3V~5.5V,但其输出电平为 VDD 相关。若 STM32 使用 3.3V 供电,DHT11 必须也接 3.3V(不能直连 5V!),否则高电平可能超过 STM32 GPIO 输入耐压(通常为 3.6V)。实测中,有开发者用 5V 供电 DHT11 后接 10kΩ 上拉至 3.3V,结果在高温高湿环境下出现间歇性通信失败——根本原因是上拉电阻导致上升沿变缓,DHT11 内部比较器误判逻辑电平。正确做法是:DHT11 与 STM32 共用同一组 3.3V LDO(如 AMS1117-3.3),DATA 线直连,无需上拉(DHT11 内部已集成 5.1kΩ 上拉)。

2.1 电路连接:三线制接法与关键滤波设计

DHT11 为 4 引脚封装(VCC, DATA, NC, GND),其中 NC 悬空。实际仅需连接三根线:

DHT11 引脚接 STM32 引脚说明
VCC3.3V(非 5V!)必须由 STM32 板载 3.3V LDO 供电,禁用 USB 5V 直供
DATAPB1(任意可复用推挽输出+输入的 GPIO)建议避开 JTAG/SWD 复用引脚(如 PA13/PA14),防止调试时冲突
GNDGND必须与 STM32 共地,不可经长导线分地

关键滤波设计(常被忽略的翻车点):
DHT11 对电源噪声极其敏感。实测中,当 STM32 驱动 OLED 屏(SSD1306)或继电器动作时,DHT11 常返回 0x0000 或 0xFF00 类错误码。根本原因是开关噪声通过共地阻抗耦合进传感器供电回路。解决方案是在 DHT11 的 VCC-GND 之间并联两级滤波:

  • 100nF X7R 陶瓷电容(贴片 0603,就近焊接在 DHT11 底部)
  • 10μF 钽电容(耐压 6.3V,ESR < 1Ω,位置紧邻 DHT11)

注意:该滤波电容必须焊在 DHT11 本体引脚处,而非 STM32 开发板的 3.3V 排针上。曾有某高校课程设计组因将电容焊在板端,导致 30% 样机在实验室空调启停瞬间失联,排查三天才发现是地弹噪声未被本地去耦。

2.2 协议时序精解:DHT11 的“握手”为什么必须手撕,不能靠库?

DHT11 采用单总线异步通信,无时钟线,所有时序均由主机(STM32)发起并严格控制。整个交互分为四阶段:主机启动 → 传感器响应 → 数据传输 → 校验结束。其中最容易出错的是前两阶段的微秒级窗口匹配。

阶段1:主机启动(关键!)
STM32 必须先将 DATA 线拉低 ≥ 18ms(典型值 20ms),再拉高 20–40μs(我们取 30μs),此时 DHT11 才开始准备响应。若拉低时间 <18ms,DHT11 视为无效请求;若拉高时间 >40μs,DHT11 可能误判为下一帧起始。

阶段2:传感器响应(玄学高发区)
DHT11 收到有效启动信号后,会将 DATA 拉低 80μs 作为响应,再拉高 80μs 表示“我准备好了”。注意:这个 80μs 高电平不是数据位,而是“准备就绪”标志。很多初学者在此处直接开始读数据,导致后续所有 bit 解析错位。

阶段3:数据传输(8bit × 4 = 32bit)
每位数据以 50μs 低电平起始,随后是可变高电平:

  • 若高电平 ≈ 28μs → 逻辑 0
  • 若高电平 ≈ 70μs → 逻辑 1
    每个 bit 总宽 ≈ 120μs,相邻 bit 间隔 ≥ 50μs。

阶段4:校验
最后 8bit 为前 32bit 的字节和(低 8bit),用于验证完整性。例如:[RH_H][RH_L][T_H][T_L]四字节相加后取& 0xFF,应等于校验字节。

提示:DHT11 的时序参数允许 ±10% 容差,但实测发现其内部 RC 振荡器受温度影响显著——在 0℃ 环境下,其响应高电平可能缩短至 65μs,在 45℃ 下则延长至 85μs。因此,硬编码延时(如for(i=0;i<100;i++);)必然失效。必须使用 SysTick 或定时器做基准,且在初始化后执行一次“时序自适应校准”:发送启动信号后,用输入捕获测量 DHT11 返回的 80μs 响应高电平实际宽度,据此动态调整后续数据位的采样窗口。


3. Keil MDK 工程搭建:从新建工程到 DHT11 初始化函数落地

我们以标准外设库(Standard Peripheral Library v3.5)为基础,在 Keil MDK-ARM v5.37 环境下构建最小可行工程。不使用 HAL 库,因其抽象层引入额外中断延迟,对 DHT11 这类时序敏感器件反而增加不确定性。

3.1 创建工程与基础配置

  1. 新建 uVision 工程,Device 选择STM32F103C8

  2. 在Manage Project Items中勾选CMSIS → CORE、Device → Startup、StdPeriph Drivers → Framework

  3. 添加以下源文件到工程:

    • stm32f10x_conf.h(启用#define USE_STDPERIPH_DRIVER)
    • stm32f10x_it.c(保留空SysTick_Handler)
    • main.c(主逻辑)
    • dht11.c/dht11.h(自定义驱动)
  4. 在Options for Target → C/C++ → Define中添加:

    USE_STDPERIPH_DRIVER, STM32F10X_MD
  5. 关键编译选项设置:

    • Optimization→ Level 2(O2,平衡速度与体积)
    • Misc Controls→--c99(支持 C99 语法)
    • Plain 'char' is→Unsigned(DHT11 数据为无符号字节,避免符号扩展错误)

3.2 DHT11 驱动核心:手写时序控制函数

dht11.h中定义关键结构与宏:

#ifndef __DHT11_H #define __DHT11_H #include "stm32f10x.h" // DHT11 连接引脚定义(可按需修改) #define DHT11_PORT GPIOB #define DHT11_PIN GPIO_Pin_1 #define DHT11_CLK RCC_APB2Periph_GPIOB // 时序参数(单位:微秒,基于 72MHz SysTick) #define DHT11_START_LOW_US 20000 // 主机拉低 20ms #define DHT11_START_HIGH_US 30 // 主机拉高 30us #define DHT11_RESPONSE_LOW_US 80 // 传感器拉低 80us #define DHT11_RESPONSE_HIGH_US 80 // 传感器拉高 80us #define DHT11_BIT_LOW_US 50 // 每位起始低电平 50us #define DHT11_BIT_0_HIGH_US 28 // 逻辑0高电平 28us #define DHT11_BIT_1_HIGH_US 70 // 逻辑1高电平 70us typedef struct { uint8_t humidity_h; // 湿度高字节(整数部分) uint8_t humidity_l; // 湿度低字节(小数部分,DHT11恒为0) uint8_t temp_h; // 温度高字节(整数部分) uint8_t temp_l; // 温度低字节(小数部分,DHT11恒为0) uint8_t checksum; // 校验和 } DHT11_Data_TypeDef; uint8_t DHT11_Read_Data(DHT11_Data_TypeDef *data); void DHT11_Init(void); #endif

dht11.c中实现核心时序控制(关键:用 SysTick 做微秒级延时):

#include "dht11.h" #include "core_cm3.h" // 用于 SysTick static __IO uint32_t uwTimingDelay = 0; // SysTick 中断服务函数(仅用于延时,不处理其他逻辑) void SysTick_Handler(void) { if (uwTimingDelay != 0x00) { uwTimingDelay--; } } // 微秒级延时(SysTick 配置为 1us 基准) void Delay_us(__IO uint32_t nTime) { uwTimingDelay = nTime; while (uwTimingDelay != 0); } // 初始化 DHT11 引脚为推挽输出(默认高电平) void DHT11_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DHT11_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin = DHT11_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, &GPIO_InitStructure); // 初始状态:DATA 线为高电平(DHT11 内部上拉生效) GPIO_SetBits(DHT11_PORT, DHT11_PIN); } // 主机启动:拉低20ms → 拉高30us static uint8_t DHT11_Start(void) { // 1. 拉低至少 18ms GPIO_ResetBits(DHT11_PORT, DHT11_PIN); Delay_us(DHT11_START_LOW_US); // 2. 拉高 20~40us GPIO_SetBits(DHT11_PORT, DHT11_PIN); Delay_us(DHT11_START_HIGH_US); // 3. 切换为输入模式,等待传感器响应 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_PORT, &GPIO_InitStructure); // 4. 检测传感器拉低 80us 响应(超时 100us) uint32_t timeout = 0; while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) && (timeout++ < 100)); if (timeout >= 100) return 1; // 无响应 // 5. 等待传感器拉高 80us(超时 100us) timeout = 0; while (!GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) && (timeout++ < 100)); if (timeout >= 100) return 1; // 响应异常 return 0; // 成功 } // 读取单个 bit(在传感器拉高后,延时 40us 采样) static uint8_t DHT11_Read_Bit(void) { uint32_t timeout = 0; // 等待低电平结束(即位起始) while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) && (timeout++ < 100)); if (timeout >= 100) return 0xFF; // 延时 40us,进入高电平采样窗口 Delay_us(40); // 读取此时电平:高则为1,低则为0 if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN)) { // 再延时 30us,确保跨过逻辑1的70us高电平(避免误判) Delay_us(30); return 1; } else { // 逻辑0高电平仅28us,40us后已回落,此处应为低 return 0; } } // 主读取函数 uint8_t DHT11_Read_Data(DHT11_Data_TypeDef *data) { uint8_t i, j; uint8_t buf[5]; // 存储 RH_H, RH_L, T_H, T_L, Checksum // 1. 发送启动信号 if (DHT11_Start() != 0) return 1; // 2. 读取 40bit 数据(5字节) for (i = 0; i < 5; i++) { buf[i] = 0; for (j = 0; j < 8; j++) { buf[i] <<= 1; buf[i] |= DHT11_Read_Bit(); } } // 3. 校验 uint8_t sum = buf[0] + buf[1] + buf[2] + buf[3]; if (sum != buf[4]) return 2; // 校验失败 // 4. 赋值 >DHT11_Init(); Delay_ms(200); // 注意:此为毫秒级延时,需另行实现(如 SysTick 1ms 中断)

4.2 现象:在 OLED 显示刷新时,DHT11 返回全 0xFF 或全 0x00

原因:OLED(SSD1306)SPI 通信期间,SCK 线高频翻转产生强电磁干扰,耦合进 DHT11 的 DATA 线。尤其当 OLED 与 DHT11 共用同一块 PCB 且走线平行超过 2cm 时,干扰强度足以让 DHT11 误触发。
解决:

  • 物理隔离:DHT11 DATA 线走线远离 OLED SPI 信号线(≥5mm),并在 DHT11 附近铺铜接地;
  • 时序错峰:在DHT11_Read_Data()前关闭 OLED 刷新中断(若使用 DMA 刷新,则暂停 DMA 请求);
  • 软件加固:读取失败时自动重试 2 次,且每次重试间隔 ≥ 1s(DHT11 最小响应间隔为 1s)。

4.3 现象:在高温(>35℃)或高湿(>85%RH)环境下,读数跳变剧烈(如 25→0→45℃ 乱跳)

原因:DHT11 的感湿元件为湿敏电阻聚合物,高温高湿下阻值变化非线性加剧,且内部 ADC 参考电压受温漂影响。出厂校准值(存储在 OTP 中)在极端环境失效。
解决:实施两点温度校准(无需拆芯片):

  1. 在 25℃ 恒温箱中读取 DHT11 值T25;
  2. 在 50℃ 恒温箱中读取T50;
  3. 计算线性校准系数:T_real = T25 + (T_read - T25) * (50-25)/(T50-T25);
  4. 将系数存入 STM32 Flash 的最后一页(需解锁 Flash 编程),开机时加载。

4.4 现象:使用 J-Link 调试时,DHT11 通信完全中断

原因:JTAG/SWD 调试接口与 DHT11 共用 PA13/PA14(SWDIO/SWCLK)或 PB3/PB4(JTDI/JTDO)引脚,调试器在后台频繁读取寄存器导致 GPIO 状态被意外修改。
解决:

  • 硬件层面:将 DHT11 DATA 线坚决避开所有调试复用引脚(查《STM32F103xx Datasheet》Table 9 “Alternate function mapping”);
  • 软件层面:在DHT11_Read_Data()前添加:
    // 禁用 SWD 调试(仅限调试阶段,量产时删除) AFIO->MAPR &= ~AFIO_MAPR_SWJ_CFG; AFIO->MAPR |= 0x02; // SWJ_CFG = 010: JTAG-DP Disabled and SW-DP Enabled
    并确保DHT11_PORT未被调试器占用。

提示:以上四坑均有对应示波器波形证据。例如坑4,用 Saleae Logic Pro 8 抓取 PA13 波形,可见 J-Link 每 200ms 发送一次IDCODE指令,导致 PA13 瞬间拉低 150ns,恰好打断 DHT11 的 80μs 响应高电平,造成握手失败。这不是玄学,是电磁兼容(EMC)的硬约束。


5. 实时显示与工程闭环:把 DHT11 数据刷到 OLED 屏上,并加入掉电保存

光有传感器读取还不够,真正的闭环是让用户“看见”数据、“信任”数据、“追溯”数据。我们选用 0.96 英寸 SSD1306 OLED(I²C 接口),因其功耗低、对比度高、无需背光驱动,且 I²C 协议比 SPI 更少干扰 DHT11。关键在于:显示不能抢走 DHT11 的 CPU 时间,历史数据必须在断电前安全落盘。

5.1 OLED 驱动精简策略:DMA + 双缓冲防撕裂

SSD1306 分辨率为 128×64,共 1024 字节显存。若每次刷新都用 CPU 逐字节写 I²C,需约 120ms(按 100kHz I²C 计算),远超 DHT11 的 1s 最小间隔,导致传感器被饿死。解决方案是:用 DMA 搬运显存,CPU 只负责更新双缓冲中的前台帧。

// oled.h 中定义双缓冲 #define OLED_BUFFER_SIZE 1024 extern uint8_t OLED_Buffer_A[OLED_BUFFER_SIZE]; // 前台缓冲(DMA 正在刷) extern uint8_t OLED_Buffer_B[OLED_BUFFER_SIZE]; // 后台缓冲(CPU 更新) // oled.c 中初始化 I²C + DMA void OLED_Init_DMA(void) { I2C_InitTypeDef I2C_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // I2C1 配置为标准模式(100kHz) I2C_InitStructure.I2C_ClockSpeed = 100000; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); // DMA1 Channel6 用于 I2C1 TX(需查《RM0008》Table 42) DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(I2C1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)OLED_Buffer_A; // 初始指向 A DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = OLED_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel6, &DMA_InitStructure); // 使能 DMA 传输完成中断,用于缓冲区切换 DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, ENABLE); NVIC_EnableIRQ(DMA1_Channel6_IRQn); }

关键技巧:在DMA1_Channel6_IRQHandler()中,收到 TC(Transfer Complete)中断后,立即切换 DMA 目标地址到另一缓冲区,并触发 OLED 刷新命令:

void DMA1_Channel6_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC6) != RESET) { // 切换缓冲区指针 if (DMA_GetCurrDataCounter(DMA1_Channel6) == 0) { // 当前刷的是 Buffer_A,则下次刷 Buffer_B DMA_SetMemoryBaseAddr(DMA1_Channel6, (uint32_t)OLED_Buffer_B); } else { DMA_SetMemoryBaseAddr(DMA1_Channel6, (uint32_t)OLED_Buffer_A); } // 发送 OLED 刷新指令(写入显存起始地址) OLED_Write_Cmd(0x20); // Set Memory Addressing Mode OLED_Write_Cmd(0x00); // Horizontal Addressing Mode OLED_Write_Cmd(0x21); // Set Column Address OLED_Write_Cmd(0x00); // Start column = 0 OLED_Write_Cmd(0x7F); // End column = 127 OLED_Write_Cmd(0x22); // Set Page Address OLED_Write_Cmd(0x00); // Start page = 0 OLED_Write_Cmd(0x07); // End page = 7 DMA_ClearITPendingBit(DMA1_IT_TC6); } }

这样,CPU 可在任意时刻安全地往OLED_Buffer_B写文本(如OLED_ShowString(2,2,"Temp: 25C");),而 DMA 在后台静默刷OLED_Buffer_A,完全不抢占 DHT11 的时序窗口。

5.2 掉电保存:用 STM32F103 的最后 1KB Flash 存历史极值

DHT11 本身无存储能力,但用户常需知道“今天最高温多少度”。我们利用 STM32F103C8T6 的 64KB Flash,将最后 1KB(地址0x0800FC00–0x0800FFFF)划为数据区,存储:

  • max_temp(uint16_t,单位 0.1℃)
  • min_temp(uint16_t)
  • max_humid(uint16_t,单位 0.1%RH)
  • min_humid(uint16_t)
  • last_update_time(uint32_t,Unix 时间戳,需外接 RTC 或用 SysTick 模拟)

Flash 操作要点:

  • Flash 写入前必须擦除整页(1KB),且擦除会清零所有位;
  • 写入只能将 1→0,不能 0→1,故需用“标记位”管理有效数据;
  • 为防突然断电,采用“双页备份 + 原子写入”策略(Page A 与 Page B 轮流使用)。
// flash_data.h #define FLASH_DATA_PAGE_A ((uint32_t)0x0800FC00) #define FLASH_DATA_PAGE_B ((uint32_t)0x0800F800) typedef struct { uint16_t max_temp; // ×10 uint16_t min_temp; // ×10 uint16_t max_humid; // ×10 uint16_t min_humid; // ×10 uint32_t last_update; // Unix timestamp uint8_t valid_flag; // 0xAA 表示有效 } Flash_Data_TypeDef; void Flash_Save_Data(const Flash_Data_TypeDef *data); void Flash_Load_Data(Flash_Data_TypeDef *data);

Flash_Save_Data()实现核心逻辑(省略擦除细节,聚焦原子性):

void Flash_Save_Data(const Flash_Data_TypeDef *data) { FLASH_Status status; uint32_t addr; Flash_Data_TypeDef backup; // 1. 读取当前有效页(假设 Page A 有效) if (*(uint8_t*)(FLASH_DATA_PAGE_A + sizeof(Flash_Data_TypeDef)) == 0xAA) { addr = FLASH_DATA_PAGE_A; } else { addr = FLASH_DATA_PAGE_B; } // 2. 将新数据写入另一页(保证原页仍有效) uint32_t new_addr = (addr == FLASH_DATA_PAGE_A) ? FLASH_DATA_PAGE_B : FLASH_DATA_PAGE_A; // 擦除新页(必须!) FLASH_ErasePage(new_addr); // 写入数据(按字写入,每字需解锁/锁定) FLASH_Unlock(); status = FLASH_ProgramHalfWord(new_addr, style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

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

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

立即咨询