STM32 Bootloader与OTA升级

STM32 Bootloader与OTA升级

深入理解STM32程序升级原理,打造永不"变砖"的可靠系统

前言

在嵌入式开发中,程序升级是一个绕不开的话题。无论是开发阶段的频繁烧录,还是产品交付后的远程维护,一套可靠的固件更新机制都是系统稳定运行的基石。本文将结合STM32芯片特性,从最基础的启动架构讲起,逐步深入到企业级的两段式、三段式Bootloader设计,同时涵盖CAN总线、LoRa等多样化升级通道的实现。本文内容基于实际项目经验与ST官方应用笔记AN2606、AN3155等资料,力求完整、可落地。


一、STM32程序下载的三种方式

STM32微控制器的程序烧录与更新主要有三种途径:ICP、ISP和IAP。理解三者的区别和适用场景,是设计Bootloader的前提。

1.1 ICP(在线编程)

ICP(In-Circuit Programming)是指通过专用的调试/编程接口(如SWD、JTAG),在芯片焊接到电路板上之后直接烧录Flash。

  • 特点
    • 需要专用烧录器:ST-Link、J-Link、ULINK等。
    • 接口通常占用2个引脚(SWD:SWCLK、SWDIO)或更多(JTAG)。
    • 可以随时读写任意Flash地址,支持硬件断点调试。
    • 烧录速度快,适合开发阶段频繁下载和工厂量产。
  • 常见应用
    • 开发调试:Keil、IAR、STM32CubeIDE直接下载。
    • 生产测试:工装夹具配合烧录器批量烧写。
    • 维修刷机:若设备预留SWD接口,可直接刷写。

1.2 ISP(系统内编程)

ISP(In-System Programming)利用STM32芯片出厂时预置在系统存储器(System Memory)中的Bootloader程序,通过UART、USB、CAN、I2C、SPI等串行接口接收固件并写入Flash。

  • 特点
    • 不需要专用烧录器,只需一个串口/USB转串口模块。
    • 需要在上电或复位时配置BOOT引脚(BOOT0=1,BOOT1=0)进入系统存储器自举模式。
    • 支持使用官方工具如STM32CubeProgrammer、Flash Loader Demonstrator。
    • 通信协议是固定的,例如USART协议使用0x7F同步字节自动检测波特率。
  • 硬件要求 (以USART1为例):
    • PA9(TX)连接上位机RX,PA10(RX)连接上位机TX。
    • 未使用的外设RX引脚(如USART2_RX、CAN_RX等)在检测期间不能悬空,需拉高或拉低,否则会影响自动检测。
  • 适用场景
    • 现场升级(无需打开外壳,通过预留的串口接口)。
    • 无调试接口的批量生产。

1.3 IAP(应用内编程)

IAP(In-Application Programming)是指用户应用程序自己调用Flash擦写函数,在运行时更新固件。

  • 特点
    • 完全由用户程序控制,不依赖BOOT引脚配置。
    • 可实现远程升级(OTA),通过WiFi、4G、LoRa、以太网接收固件。
    • 需要自己设计Bootloader(通常位于Flash起始地址)和APP之间的跳转协议。
    • 必须处理Flash擦写过程中的中断响应、掉电保护、双备份等复杂问题。
  • 典型应用
    • 物联网设备的无线固件升级(FOTA)。
    • 本地通过SD卡、U盘自动升级。
    • 车载ECU、工业控制器的远程维护。

IAP vs OTA:IAP强调"应用程序自主更新"的能力,OTA(Over-The-Air)特指通过无线通信网络远程升级。两者本质相同,只是传输介质不同。本文统一使用IAP/OTA表示用户自编程升级。


二、STM32启动架构基础

2.1 BOOT引脚配置与启动模式

STM32通过BOOT0和BOOT1引脚(或选项字节中的nBOOT1位)在复位时刻的电平状态,决定从哪个存储区域启动。

BOOT1 BOOT0 启动模式 说明
X 0 主闪存(Main Flash) 从地址0x08000000开始执行,正常运行用户APP
0 1 系统存储器(System Memory) 执行出厂Bootloader,用于ISP下载
1 1 内置SRAM 从SRAM启动,调试或临时运行

注意

  • 复位后,硬件在SYSCLK第四个上升沿锁存BOOT引脚状态。
  • 对于部分STM32型号(如F0、F3系列),BOOT1并不是一个物理引脚,而是选项字节中的nBoot1位。通过修改选项字节可将nBoot1置1或清0来实现BOOT1的逻辑电平。
  • 当选项字节中设置读保护级别2时,系统存储器自举模式会被禁用。

2.2 为什么Bootloader必须放在Flash最前面?

很多初学者疑惑:为什么Bootloader(通常称为B区)必须放在内部Flash的开头(0x08000000),而应用程序(A区)放在后面?

这完全由ARM Cortex-M内核的硬件启动机制决定:

  1. 芯片复位后,程序计数器(PC)被强制初始化为0x00000000 ,但实际物理地址映射取决于启动模式。当从Flash启动时,Flash的物理地址0x08000000被映射到0x00000000,所以CPU第一条指令取自0x08000000
  2. 0x08000000处存放的是中断向量表 。向量表的前两个条目是:
    • 第一个32位数据:初始主堆栈指针(MSP)
    • 第二个32位数据:复位向量(Reset_Handler),即复位后要执行的第一条指令地址。
  3. 因此,任何希望上电立即运行的代码,都必须放在0x08000000开始的地址。

错误的分区方案:如果把APP放在前面(0x08000000),Bootloader放在后面(例如0x08010000),则上电后会先执行APP。如果APP损坏或需要更新,系统永远无法进入Bootloader,设备直接变砖。所以正确的做法是:

  • Bootloader(B区)占用起始地址,大小为固定预留空间(如32KB)。
  • APP(A区)从Bootloader结束地址开始(如0x08008000)。

2.3 系统存储器自举模式的细节

STM32芯片出厂内置的Bootloader位于System Memory,支持多种外设接口。下表列出了常见系列的支持情况:

STM32系列 支持的ISP接口
F1系列(小/中/大容量) USART1
F1超大容量 USART1、USART2
F105/107(互连型) USART1、USART2、CAN2、DFU(USB)
F2/F4系列 USART1、USART3、CAN2、DFU
L1系列(大容量) USART1、USART2、DFU
F0系列(051/050) USART1、USART2
F3系列(302/303) USART1、USART2、DFU、I2C

以USART方式进入ISP的步骤:

  • 配置BOOT0=1,BOOT1=0,复位。
  • 上位机发送0x7F字节(一个起始位、8位数据0x7F、偶校验、一个停止位)。
  • Bootloader使用SysTick定时器测量0x7F低电平的时间,自动计算波特率。
  • 计算成功后返回0x79(ACK),表示准备好接收命令。

2.4 APP安全启动的核心流程

Bootloader跳转到APP前,必须完成以下操作:

c 复制代码
// 定义函数指针类型
typedef void (*pFunction)(void);

void JumpToApplication(uint32_t appStartAddr)
{
    // 1. 检查APP起始地址是否合法:向量表第一个字必须是有效的栈顶地址(RAM范围内)
    if ((*(__IO uint32_t*)appStartAddr & 0x2FFE0000) == 0x20000000)
    {
        // 2. 关闭所有外设中断,避免跳转后产生异常
        __disable_irq();
        // 3. 设置主堆栈指针为APP的初始栈顶
        __set_MSP(*(__IO uint32_t*)appStartAddr);
        // 4. 获取复位向量地址(向量表第二个字)
        uint32_t jumpAddr = *(__IO uint32_t*)(appStartAddr + 4);
        pFunction resetHandler = (pFunction)jumpAddr;
        // 5. 重新映射中断向量表(APP内部还需再次设置)
        SCB->VTOR = appStartAddr;
        // 6. 跳转
        resetHandler();
    }
    else
    {
        // 无效APP,停留在Bootloader或报错
    }
}

注意 :APP的链接脚本(.ld或.sct)必须将起始地址设置为0x08008000(假设Bootloader占32KB),并且在APP的启动代码中再次设置SCB->VTOR = 0x08008000,否则中断无法正确响应。


三、两段式Bootloader设计(基础IAP)

两段式Bootloader是指:Bootloader(B区)+ 应用程序(A区),外加一个外部存储(SPI Flash)和EEPROM。这是最经典、最简洁的IAP方案。

3.1 系统组成与角色分工

组件 型号举例 功能 角色
内部Flash B区 Bootloader程序 上电首先运行,负责升级决策和Flash擦写 总指挥
内部Flash A区 应用程序(APP) 实现业务逻辑,接收新固件并存储到外部Flash 执行者
SPI Flash W25Q32(4MB) 临时存放从网络/串口接收的完整固件包 中转仓库
EEPROM M24C02(256字节) 存储升级状态标志、版本号、CRC值 状态记事本

为什么需要W25Q32?

  • 内部Flash在擦写时无法同时执行代码(虽然可以执行RAM中的代码,但复杂且不稳定)。
  • 固件下载过程较慢,若边接收边擦写内部Flash,容易因通信中断导致Flash损坏。
  • 外部SPI Flash容量大(4MB~64MB),可暂存完整固件,待校验通过后再搬运。

为什么需要M24C02?

  • EEPROM支持单字节擦写,无限次读写,非常适合频繁更新状态标志。
  • 内部Flash虽然也能存储,但需要按页擦除(耗时且容易影响程序执行)。
  • 掉电后数据不丢失,可用于记录升级进度,实现断点续传和掉电恢复。

3.2 状态标志位设计(存储在EEPROM)

c 复制代码
// 定义在EEPROM固定地址,例如0x00
#define EEPROM_OTA_FLAG_ADDR   0x00
#define EEPROM_APP_CRC_ADDR    0x04
#define EEPROM_APP_VERSION     0x08

// 标志位取值
#define OTA_FLAG_NORMAL        0x00  // 正常启动
#define OTA_FLAG_PENDING       0xFF  // 需要升级
#define OTA_FLAG_SUCCESS       0xAA  // 升级成功(可正常跳转)
#define OTA_FLAG_FAIL          0x55  // 升级失败(需回滚或重试)

3.3 正常启动流程

  1. 上电/复位,CPU从0x08000000开始执行B区Bootloader。
  2. Bootloader初始化必要硬件(时钟、EEPROM I2C、调试串口等)。
  3. 读取EEPROM中的OTA_FLAG
    • 若为OTA_FLAG_NORMAL:验证APP区域的CRC(可选)。
    • 若CRC校验通过:设置SCB->VTOR,跳转到APP起始地址。
    • 若CRC失败:停留在Bootloader,等待串口/CAN/无线更新。
  4. APP开始运行业务程序。

3.4 OTA升级流程(以USART Ymodem或WiFi下载为例)

第1步:APP端准备工作

  • 当APP运行时收到升级指令(例如串口收到"UPDATE"命令,或服务器下发的MQTT消息)。
  • APP通过通信接口(WiFi/4G/串口)分块接收新固件,每包按页(256字节)写入W25Q32。
  • 接收完成后,计算整个固件的CRC32(或SHA256),与上位机/服务器提供的校验值比对。
  • 若校验通过,则将OTA_FLAG在EEPROM中写入OTA_FLAG_PENDING,并保存新固件大小、版本号。
  • 最后执行软件复位:NVIC_SystemReset()

第2步:Bootloader执行升级

  • 系统复位后,Bootloader检测到OTA_FLAG == OTA_FLAG_PENDING
  • Bootloader擦除内部Flash的A区(从APP_START_ADDRAPP_END_ADDR)。
    • 注意:STM32F103ZET6的扇区大小为2KB,需按扇区擦除。
  • Bootloader从W25Q32中循环读取固件数据(每页256字节),写入内部Flash的A区地址。
  • 每次写入后可选立即读回校验,或者整体写完后再计算CRC。
  • 全部写入完成后,计算A区的CRC32,与之前保存的CRC值比较。
    • 相等:将OTA_FLAG更新为OTA_FLAG_SUCCESS,并更新版本号、CRC。
    • 不相等:将OTA_FLAG更新为OTA_FLAG_FAIL,并保留原A区(若未擦除干净可考虑回滚)。
  • Bootloader再次复位或直接跳转到A区。若OTA_FLAGSUCCESS,跳转后APP正常运行;若为FAIL,则停留在Bootloader并上报错误。

四、三段式Bootloader(A/B分区工业级方案)

对于要求极高可靠性的设备(如工业控制器、医疗设备、汽车ECU),两段式方案仍存在风险:如果在擦除A区后、写入新固件途中断电或写入失败,设备会变砖。三段式(A/B分区)通过双备份彻底解决此问题。

4.1 Flash内存划分

假设总Flash大小为256KB,Bootloader占32KB,两个APP区各占96KB,剩余为配置信息区。

分区 地址范围 大小 说明
Bootloader 0x08000000 ~ 0x08007FFF 32KB 永不更新,负责决策和搬运
APP1区 0x08008000 ~ 0x0801FFFF 96KB 工厂版本或稳定版本
APP2区 0x08020000 ~ 0x08037FFF 96KB OTA更新目标版本
配置信息区 0x08038000 ~ 0x0803FFFF 32KB 存储状态标志、当前活动分区、版本、CRC

注意:两个APP区的链接脚本需分别设置不同的起始地址,编译出两个独立的bin文件。

4.2 关键状态机(存储在配置信息区)

定义以下状态值(可以使用结构体保存):

c 复制代码
typedef struct {
    uint8_t  upgrade_state;   // 0x00=正常, 0x01=待升级, 0x02=升级成功, 0x03=升级错误
    uint8_t  active_slot;     // 当前运行的分区:1或2
    uint16_t app1_version;
    uint16_t app2_version;
    uint32_t app1_crc;
    uint32_t app2_crc;
} BootConfig;

4.3 正常启动流程

  1. Bootloader读取配置信息,获取active_slot
  2. 计算对应APP区的CRC,与存储的CRC比较:
    • 校验通过 → 跳转到该APP区运行。
    • 校验失败 → 尝试切换另一个分区(若另一个分区也失败,则停留在Bootloader)。
  3. APP运行后,若upgrade_state == UPGRADE_SUCCESS,则将该状态清除为正常状态。

4.4 升级流程(以OTA为例)

  1. 当前APP(例如运行在APP1区)收到新固件,下载并存储到外部Flash(或直接写入非活动的APP2区)。
  2. 校验新固件完整性后,更新配置信息区:
    • upgrade_state = UPGRADE_PENDING
    • 目标分区为APP2(更新对方分区)
  3. 复位进入Bootloader。
  4. Bootloader检测到UPGRADE_PENDING,将外部Flash或暂存区的固件写入APP2区。
  5. 写入完成后校验APP2区CRC,若成功:
    • active_slot改为2,upgrade_state = UPGRADE_SUCCESS
  6. 跳转到APP2区运行。

4.5 升级失败回滚流程(核心优势)

若新固件在APP2区运行后出现异常(如死机、看门狗复位),Bootloader再次启动时检测到upgrade_state == UPGRADE_SUCCESS,但是通过某种机制(例如心跳检测失败后设置的失败标志)发现新APP不可用,则执行回滚:

  1. Bootloader将active_slot改回APP1区。
  2. upgrade_state设置为UPGRADE_ERROR
  3. 跳转到APP1区运行,设备恢复到升级前的稳定版本。
  4. 可选上报错误信息给服务器。

注意:回滚触发条件可以设计为:在APP2运行期间,若一定时间内没有向EEPROM写入"正常心跳"标志,或者手动触发恢复按键,则Bootloader判定本次升级失败并回滚。

4.6 超大容量器件的双存储区自举(BFB2位)

对于STM32F101/103超大容量器件(Flash≥768KB),硬件支持真正的双Bank机制。通过设置选项字节中的BFB2位,可以在不运行用户Bootloader的情况下实现从Bank2启动。这是一种硬件A/B切换,但本文不展开,感兴趣可查阅AN2606第6节。


五、可靠性机制详解

5.1 外置Flash(W25Q32)操作规范

W25Q32是一款4MB的SPI NOR Flash,页大小256字节,扇区大小4KB,块大小64KB。在使用时需注意:

  • 写使能 :每次写入前必须发送Write Enable (0x06)指令。
  • 擦除:写入前必须擦除,且擦除的最小单位是4KB扇区(或64KB块)。擦除后所有位变为1。
  • 页编程:一次最多写入256字节,若超过页边界会回绕覆盖开头,必须分页写入。
  • 状态检查 :写入或擦除后需轮询Read Status Register (0x05)的BUSY位,直到变为0。
  • 写保护:本设计中将WP引脚接高电平,禁用写保护。

5.2 CRC32校验实现

c 复制代码
#include "crc.h"

// 使用STM32硬件CRC32(多项式0x04C11DB7)
uint32_t CalculateCRC32(uint32_t *pData, uint32_t len)
{
    HAL_CRC_Calculate(&hcrc, pData, len);
    return hcrc.Instance->DR;
}

// 对整个APP区域进行校验
uint32_t VerifyAppCRC(uint32_t appStartAddr, uint32_t appSize, uint32_t expectedCRC)
{
    uint32_t calc = CalculateCRC32((uint32_t*)appStartAddr, appSize / 4);
    return (calc == expectedCRC);
}

5.3 断电保护策略(结合EEPROM状态机)

由于升级过程可能意外断电,需要设计状态机保证上电后可恢复。定义以下状态(存储在EEPROM):

状态码 含义 断电恢复后的操作
0x00 空闲,无升级 正常跳转APP
0x01 正在下载固件到W25Q32 从断点续传(记录已下载字节数)
0x02 下载完成,待校验 重新校验W25Q32中的固件
0x03 校验通过,待写入内部Flash 擦除目标分区并写入
0x04 正在写入内部Flash 从上次写入中断的地址继续(不推荐,复杂)或重新全量写入
0x05 写入完成,待验证 验证内部Flash CRC,成功则置为0x00,失败则回滚

实现原则

  • 每次进入下一个状态前,先将新状态写入EEPROM并确认写入成功。
  • 然后执行操作。如果操作中途断电,上电后Bootloader根据状态决定是重试还是回滚。
  • 对于写内部Flash操作,务必确保写入地址正确,且在擦除前备份原数据(A/B分区天然支持,无需备份)。

5.4 Flash擦写代码示例(基于HAL)

以下是参考《OTA远程升级 BootLoader.pdf》中的Inf_InteriorFlash.c实现的内部Flash操作:

c 复制代码
// 擦除指定起始地址的N页(每页大小需要根据芯片获取)
HAL_StatusTypeDef Flash_ErasePages(uint32_t startAddr, uint32_t numPages)
{
    HAL_StatusTypeDef status;
    uint32_t pageError = 0;
    FLASH_EraseInitTypeDef eraseInit = {0};
    
    HAL_FLASH_Unlock();
    eraseInit.TypeErase   = FLASH_TYPEERASE_PAGES;
    eraseInit.PageAddress = startAddr;
    eraseInit.NbPages     = numPages;
    status = HAL_FLASHEx_Erase(&eraseInit, &pageError);
    HAL_FLASH_Lock();
    return status;
}

// 半字(16位)写入
HAL_StatusTypeDef Flash_WriteHalfWord(uint32_t addr, uint16_t data)
{
    HAL_StatusTypeDef status;
    HAL_FLASH_Unlock();
    status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr, data);
    HAL_FLASH_Lock();
    return status;
}

六、多样化的升级通道

6.1 CAN总线升级(工业有线)

CAN总线具有高抗干扰、多节点、长距离(10km)特点,适合工业现场固件升级。

  • 硬件:STM32内嵌CAN控制器,外加TJA1050等收发器。
  • 协议:可采用自定义协议,每个升级包按CAN帧分段(每帧最多8字节数据)。使用AES-128加密固件,防止非法窃取。
  • 流程:上位机通过CAN发送升级指令→设备进入升级模式→接收并存储到W25Q32→校验后更新内部Flash。

6.2 LoRa无线升级(远距离低功耗)

LoRa调制技术以低速率换取长距离和低功耗。根据您提供的资料,LLCC68/SX1278等模块的负载大小受扩频因子(SF)、带宽(BW)、编码率(CR)影响:

配置参数 典型值 最大负载(字节)
SF=12, BW=125kHz, CR=4/8 约300bps 约51字节/包
SF=9, BW=250kHz, CR=4/5 约1.5kbps 约115字节/包

升级策略

  • 由于每包负载小,需设计分包协议,支持校验和重传。
  • 固件通常几十KB,整个升级可能持续数分钟到数十分钟,适合对时间不敏感的设备。
  • LoRaWAN协议标准支持Firmware Over-The-Air (FUOTA) 规范,但实现复杂,一般使用自定义简单协议。

6.3 串口(USART)IAP(本地有线)

最基础的方式,适合开发调试或本地维护。

  • 使用Ymodem协议:具有CRC校验、重传、取消等机制,广泛用于嵌入式文件传输。
  • 实现步骤:Bootloader中集成Ymodem接收函数,通过串口接收bin文件,写入内部Flash。
  • 优点:无需外部Flash,可直接写入(但边收边写需小心断电风险)。

七、完整OTA流程工程化实践

八、常见问题与避坑指南(FAQ)

Q1:OTA状态位什么时候改变为可升级?

:必须在固件完整下载到W25Q32并完成CRC校验无误后 ,才将EEPROM中的标志改为PENDING。绝对不要在下载中途设置,否则断电后Bootloader会以为有完整固件而开始升级,导致写入损坏。

Q2:升级过程中断电怎么办?

:利用EEPROM记录多级状态(下载中、校验通过、写入中、完成)。上电后Bootloader根据状态执行续传或重新下载。详细状态机见第五章5.3节。

Q3:如何防止升级失败后设备变砖?

:采用三段式A/B分区设计。即使新分区写入失败或运行崩溃,Bootloader仍然可以启动旧分区。配合硬件看门狗,若新APP长时间不喂狗,则复位后自动回滚。

Q4:中断向量表需要注意什么?

:APP必须重映射中断向量表。方法:在APP的启动代码(或main开头)调用SCB->VTOR = APP_BASE_ADDRESS;。否则,当APP运行时发生中断(如串口中断、定时器中断),CPU会去0x08000000处找中断服务函数,而那里是Bootloader的向量表,导致HardFault。

Q5:如何测试Bootloader是否稳定?

:建议进行以下测试:

  • 正常跳转测试:上电后能否稳定进入APP。
  • 升级测试:模拟完整升级10次以上,每次都校验CRC。
  • 断电测试:在升级过程中不同阶段(擦除前、擦除后、写入第1/2/...页)随机断电,重新上电后观察是否能恢复或回滚。
  • 边界测试:固件大小刚好是扇区整数倍、非整数倍,写入是否正常。

Q6:W25Q32的页回绕问题是什么?

:W25Q32的Page Program指令每次最多写入256字节。如果你从页内偏移200开始写,数据长度60字节,那么会从200写到255(56字节),剩下的4字节会写到该页的0~3地址,覆盖原有数据。因此必须自己处理分页:每次写入前检查剩余长度,若超出页尾则分段写入。


九、总结与推荐实践

一套完善的Bootloader系统是企业级嵌入式产品的基石。基于上述分析,我们推荐:

应用场景 推荐方案 理由
开发调试 ICP (SWD) + 串口IAP 快速调试,简单可靠
成本敏感消费品(无远程升级需求) 两段式Bootloader + 串口ISP 占用Flash小,无需外部Flash
工业设备(需远程升级) 三段式A/B分区 + W25Q32 + EEPROM 高可靠,支持回滚和断电恢复
物联网终端(无线升级) 三段式 + LoRa/4G/WiFi下载 远程便捷,双备份防变砖
汽车ECU 硬件双Bank + 带硬件安全模块的Bootloader 满足功能安全标准

无论选择哪种方案,核心要点可归纳为:

  1. 启动顺序决定分区:Bootloader必须在Flash最前端。
  2. 状态持久化:用EEPROM或内部Flash专用页存储关键状态,支持断电恢复。
  3. 双重校验:下载后校验 + 写入后校验,防止静默错误。
  4. 安全回滚:A/B分区确保任何时候都至少有一个可用版本。
  5. 冗余设计:关键操作前备份重要数据,出错可恢复。

希望本文能帮助您构建稳定可靠的固件升级系统。如有疑问或需要更具体的代码实现,欢迎留言交流。


相关推荐
天才程序YUAN1 小时前
Windows 11 C 盘扩容完整教程:恢复分区拦路、页面文件锁盘、WinRE 重建全记录
c语言·开发语言·windows
Industio_触觉智能1 小时前
瑞芯微RK3576机器视觉场景之割草机+无人清扫车
嵌入式硬件·硬件工程·边缘计算·智能硬件·rk3576·割草机·rk3576j
TDengine (老段)1 小时前
TDengine Cache 与 Last 查询加速 — CACHEMODEL 机制与 RocksDB 缓存层
大数据·数据库·物联网·struts·缓存·时序数据库·tdengine
Wallystech-Linda1 小时前
[特殊字符] How Mesh WiFi Is Tested: A Complete Engineering Validation Guide
嵌入式硬件
我是一颗柠檬1 小时前
C语言最全面复习:从入门到精通(2026年)
c语言·开发语言
嵌入式-老费1 小时前
esp开发与应用(数码管类应用)
嵌入式硬件
Luminous.1 小时前
C语言--day26
c语言·开发语言
fie88891 小时前
51单片机 NRF24L01 接收程序
嵌入式硬件·mongodb·51单片机
luj_17681 小时前
硝酸体系核关联假说解析
服务器·c语言·开发语言·经验分享·算法