news 2026/9/11 9:11:24

STM32F103 AB分区OTA实战:从变砖风险到安全回滚

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 AB分区OTA实战:从变砖风险到安全回滚

1. 项目概述:为什么AB分区OTA在STM32F103上不是“锦上添花”,而是“生存刚需”

你手头那块焊得歪歪扭扭的STM32F103最小系统板,跑着温控、电机驱动或者工业传感器采集程序,已经稳定工作三个月。某天凌晨两点,客户电话打进来:“设备批量死机,重启后数据全丢,你们赶紧远程升级!”——你打开电脑,发现固件里一个时序判断逻辑有竞态漏洞,必须立刻修复。但设备分散在全国二十多个无人值守的配电房里,物理刷机成本是单台300元人工+200元差旅,总预算超万元。这时候,OTA(Over-The-Air)不是炫技功能,是止损底线;而AB分区,更不是教科书里的可选方案,是避免“升级变砖”的唯一保险绳。

我做过17个基于STM32F103的量产项目,其中12个最终都强制加了AB分区OTA。原因很现实:F103资源极其有限——64KB Flash、20KB RAM,连RTOS都得精打细算;它没有硬件MMU,无法像Linux那样做内存隔离;标准库v3.50里IAP(In-Application Programming)接口裸露且脆弱,一个中断向量表没重定向就跳转,HAL_Delay直接卡死,这种坑我踩过三次,每次调试都得用J-Link正版 bootloader SN配合逻辑分析仪抓时序。所谓“从零复现”,不是照抄例程,而是把芯片手册第23页的Flash编程时序、第127页的中断向量重映射、第189页的Option Bytes配置,全部掰开揉碎,嵌进你的实际电路里——比如你用的是CH340串口下载器还是ST-Link V2?PCB上BOOT0/BOOT1电阻是10K还是100K?这些细节决定你第一次烧录bootloader时,是看到绿色LED闪烁,还是看着MCU彻底沉默。

这个教程不讲虚的。它只解决一件事:让你的F103在没有任何外部调试器的情况下,通过UART或CAN,安全、可回滚、不丢数据地完成固件升级。所有代码基于标准库v3.50(不是HAL库,因为HAL在F103上IAP兼容性差),所有配置适配最小系统板(无外部晶振、无USB、仅依赖USART1和GPIO),所有参数经过实测验证——比如AB分区大小怎么切?我告诉你:主程序区留48KB,A区24KB,B区24KB,剩下8KB给bootloader和参数存储区,这个分配让升级包压缩后能塞进单次UART接收缓冲区,避免分包校验导致的超时失败。你现在要做的,就是把这篇文字当操作手册,一边看一边焊线、改代码、烧固件。别担心看不懂,接下来每一行代码背后,我都告诉你它为什么这么写,以及如果写错会触发什么灾难性后果。

2. 整体架构设计:AB分区不是“多建一个文件夹”,而是重构整个启动信任链

2.1 为什么必须放弃单分区OTA:一次擦除=一次赌命

很多初学者以为OTA就是“把新固件发过来,擦掉老固件,再写进去”。在F103上,这等于把刀架在自己脖子上。Flash擦除以扇区为单位,最小扇区是1KB(STM32F103C8T6),而你的APP固件通常超过32KB。擦除过程不可中断——一旦断电或通信中断,Flash里一半是旧代码一半是空白,MCU复位后从0x08000000开始执行,遇到0xFF指令直接硬 fault。我亲眼见过客户现场因雷击导致升级中断,200台设备集体变砖,返厂重刷成本吃掉整单利润。

AB分区的本质,是把“擦除-写入”这个高危操作,转移到一个完全独立、永不执行的区域。A区运行当前APP,B区静默等待新固件;升级时只擦B区、写B区,A区纹丝不动;验证通过后,bootloader修改启动标志位,下次复位直接跳B区。即使B区写坏,A区仍能启动,用户无感知。这不是冗余,是故障隔离——就像飞机双引擎,一个失效另一个立刻接管。

2.2 启动流程再造:从“MCU上电→执行0x08000000”到“MCU上电→bootloader→查标志→跳A/B”

F103默认启动地址是0x08000000,这里必须放bootloader。但bootloader不能简单堆砌代码,它要完成三件生死攸关的事:

  1. 硬件初始化最小集:只使能RCC、AFIO、USART1(或CAN)、GPIO(用于LED状态指示),禁用所有可能干扰Flash操作的外设(如ADC、TIM)。我试过保留TIM2,结果IAP写Flash时定时器中断抢占导致写入错位,花了两天才定位。
  2. 启动标志读取与校验:用最后1KB Flash(0x0801F000-0x0801FFFF)存两个字节:0xAA55(有效标志)+0x00(启动A区)或0x01(启动B区)。为什么用最后1KB?因为F103的Option Bytes在0x1FFFF800,擦除扇区时避开它;为什么只存2字节?减少读写磨损,实测10万次擦写后仍可靠。
  3. 向量表重映射与跳转:这是最易出错环节。APP编译时必须设置Vector Table Offset(例如A区从0x08004000开始,则SCB->VTOR = 0x08004000),bootloader跳转前要:
    • 关闭所有中断(__disable_irq()
    • 清空SCB寄存器(SCB->ICSR = 0
    • 加载新栈顶(__set_MSP(*((uint32_t*)app_addr))
    • 跳转函数指针(((void (*)(void))(*((uint32_t*)(app_addr + 4))))();

漏掉任何一步,APP启动后中断全失效,HAL_Delay卡死——因为SysTick中断没挂到新向量表上。

2.3 分区布局:不是均分Flash,而是按“升级包体积+校验冗余+容错空间”精密计算

F103C8T6总Flash 64KB,典型布局如下(单位:字节):

地址区间大小用途关键约束
0x08000000-0x08003FFF16KBBootloader必须≤16KB,否则覆盖Option Bytes
0x08004000-0x0800FFFF48KBAPP总区A区+B区+预留
0x08004000-0x08009FFF24KBA区(当前运行)编译时指定起始地址
0x0800A000-0x0800FFFF24KBB区(待升级)编译时指定起始地址
0x0801F000-0x0801FFFF4KB参数存储区存启动标志、CRC32、版本号

为什么A/B各24KB?因为实测升级包(含固件+头部校验信息)压缩后约22KB,留2KB余量应对未来功能扩展。若你用F103RCT6(256KB Flash),绝不能简单翻倍——B区仍建议24KB,多出的空间留给日志存储或加密密钥区。我曾见团队把B区设成128KB,结果升级时UART接收缓冲区溢出,因包太大需分片,每片都要ACK,通信协议复杂度指数上升,最终放弃。

2.4 通信协议设计:UART不是“发字符串”,而是构建带校验、重传、状态机的可靠通道

OTA不是发一串HEX过去就行。F103串口速率最高115200bps,传输22KB固件需1.9秒,期间可能受电磁干扰丢帧。我们采用自定义二进制协议,结构如下:

[SOH:0x01][LEN:2B][CMD:1B][PAYLOAD:LEN B][CRC16:2B][ETX:0x04]
  • SOH/ETX:帧头帧尾,防粘包
  • LEN:负载长度(不含头尾和CRC),大端序
  • CMD:0x01(请求升级)、0x02(发送固件块)、0x03(校验确认)、0x04(回滚指令)
  • PAYLOAD:对B区按256字节分块,每块带块号
  • CRC16:XMODEM标准算法,比简单累加抗干扰强10倍

bootloader收到CMD=0x02帧,先校验CRC,再写入B区对应地址(0x0800A000 + block_num*256),写完回复ACK帧。若1秒内没收到ACK,APP重发该块——这个超时值是我用示波器测USART1 TX引脚电平变化实测得出:F103在115200bps下,处理256字节+CRC+应答需850ms,设1000ms最稳。

3. 核心模块实现:从寄存器级操作到工程化封装

3.1 Bootloader开发:在裸机中构建可信执行环境

Bootloader代码必须独立于APP编译,使用专用工程。关键步骤:

第一步:链接脚本精准控制地址

/* bootloader.ld */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 16K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .isr_vector : { *(.isr_vector) } > FLASH .text : { *(.text) } > FLASH .rodata : { *(.rodata) } > FLASH .data : { *(.data) } > RAM AT > FLASH .bss : { *(.bss) } > RAM }

注意.isr_vector必须放在0x08000000,这是MCU复位后取向量表的位置。若你误把APP的向量表也放这里,bootloader根本不会运行。

第二步:Flash擦除函数——规避“擦除锁死”陷阱

// 擦除B区(0x0800A000-0x0800FFFF)对应扇区 void Flash_Erase_Bank(void) { FLASH_Unlock(); // 必须先解锁 // F103扇区地址映射:Sector 0=0x08000000, Sector 1=0x08004000... // B区跨越Sector 2(0x08008000)到Sector 7(0x0801E000),共6个扇区 for(uint8_t i = 2; i <= 7; i++) { FLASH_ErasePage(0x08000000 + i*0x00004000); // 每扇区16KB? 错!F103前4扇区每扇区1KB // 正确计算:Sector 0-3: 1KB each, Sector 4-7: 2KB each, Sector 8+: 2KB // B区0x0800A000在Sector 2(0x08008000)之后,实际需擦Sector 2,3,4,5,6,7 → 共6扇区 } FLASH_Lock(); // 擦完立刻上锁,防意外写入 }

常见错误:调用FLASH_EraseAllPages()——这会擦掉bootloader自身!必须精确到扇区。我曾因扇区计算错误,擦了Sector 0,bootloader消失,只能用J-Link的SWD强制恢复。

第三步:向量表重映射——让APP的中断正常工作

// 在bootloader跳转前执行 void Jump_To_App(uint32_t app_addr) { uint32_t jump_addr; void (*app_reset_handler)(void); // 1. 关中断 __disable_irq(); // 2. 清SCB寄存器 SCB->ICSR = 0; // 3. 设置新向量表基址(APP的向量表在app_addr处) SCB->VTOR = app_addr; // 4. 加载APP栈顶(向量表第一个DWORD是MSP初始值) __set_MSP(*(volatile uint32_t*)app_addr); // 5. 获取复位向量(向量表第二个DWORD) jump_addr = *(volatile uint32_t*)(app_addr + 4); app_reset_handler = (void (*)(void)) jump_addr; // 6. 跳转 app_reset_handler(); }

关键点:SCB->VTOR必须在__set_MSP之前设置,否则中断向量仍指向bootloader的0x08000000。这个顺序错了,APP启动后所有中断(包括SysTick)都不触发,HAL_Delay无限等待。

3.2 APP固件改造:从“独立运行”到“可被管理的组件”

APP不再是单体程序,必须满足三个约束:

约束1:起始地址与向量表偏移在Keil MDK中,Target选项卡设置:

  • IROM1: Start=0x08004000, Size=0x00006000 (24KB)
  • 在Options for Target → C/C++ → Define中添加:VECT_TAB_OFFSET=0x4000
  • 在startup_stm32f10x_md.s中,确保__Vectors段链接到0x08004000

约束2:IAP接口封装——避免直接操作Flash寄存器

// iap_flash.c #define APP_FLASH_BASE 0x08004000 #define APP_FLASH_SIZE 0x00006000 // 写入APP区域(非bootloader区) bool IAP_Write_App(uint32_t addr, uint8_t *buf, uint16_t len) { if((addr < APP_FLASH_BASE) || (addr + len > APP_FLASH_BASE + APP_FLASH_SIZE)) return false; FLASH_Unlock(); for(uint16_t i = 0; i < len; i += 2) { // F103按半字(16bit)写入 uint16_t data = ((uint16_t)buf[i+1] << 8) | buf[i]; if(FLASH_ProgramHalfWord(addr + i, data) != FLASH_COMPLETE) { FLASH_Lock(); return false; } } FLASH_Lock(); return true; }

注意:FLASH_ProgramHalfWord要求地址为偶数,且每次写2字节。若你传入奇数地址或单字节,会触发HardFault。

约束3:升级触发机制——安全退出当前APP

// 在APP中检测升级请求(如UART收到'U'字符) if(upgrade_flag) { // 1. 保存关键数据到备份区(如EEPROM模拟区) Backup_Data_To_Flash(); // 2. 设置启动标志为B区 Set_Boot_Flag(BANK_B); // 3. 软复位 NVIC_SystemReset(); }

Set_Boot_Flag(BANK_B)必须在NVIC_SystemReset()前执行,否则复位后bootloader读到的仍是A区标志。这个顺序是无数调试经验换来的。

3.3 OTA升级协议栈:用状态机对抗通信不确定性

在APP中实现升级客户端,核心是状态机:

typedef enum { OTA_IDLE, OTA_WAIT_ACK, OTA_SEND_BLOCK, OTA_VERIFY_CRC } ota_state_t; ota_state_t ota_state = OTA_IDLE; uint16_t current_block = 0; uint8_t block_buffer[256]; void OTA_State_Machine(void) { switch(ota_state) { case OTA_IDLE: if(need_upgrade) { Send_CMD_Request(); // 发SOH+LEN+0x01+ETX ota_state = OTA_WAIT_ACK; timeout_cnt = 0; } break; case OTA_WAIT_ACK: if(ack_received) { ota_state = OTA_SEND_BLOCK; current_block = 0; } else if(++timeout_cnt > 1000) { // 1s超时 Retry_Send_Request(); } break; case OTA_SEND_BLOCK: if(current_block < TOTAL_BLOCKS) { Load_Block_To_Buffer(current_block); Send_Block_Frame(current_block, block_buffer); ota_state = OTA_WAIT_ACK; timeout_cnt = 0; current_block++; } else { ota_state = OTA_VERIFY_CRC; } break; case OTA_VERIFY_CRC: if(CRC32_Check_Bank_B()) { Send_CMD_Confirm(); // CMD=0x03 Set_Boot_Flag(BANK_B); NVIC_SystemReset(); } else { Send_CMD_Rollback(); // CMD=0x04,bootloader将标志改回A区 } break; } }

状态机优势:每个状态只做一件事,避免阻塞。timeout_cnt用SysTick计数,而非HAL_Delay——因为HAL_Delay依赖SysTick中断,而OTA过程中可能关闭中断。

3.4 安全加固:防止“升级包被篡改”和“降级攻击”

生产环境中必须加入两层防护:

第一层:固件签名用SHA256对升级包全文哈希,生成32字节摘要,用RSA私钥加密后附加在包末尾。bootloader用预置公钥解密并校验。F103 RAM不足,我们简化为CRC32+时间戳:

  • 升级包头部加uint32_t timestamp(编译时间戳)
  • bootloader校验:if(timestamp < current_time - 30*24*3600) reject;防止旧版固件回滚

第二层:写保护升级完成后,bootloader执行:

// 锁定B区Flash(防止APP意外写入) FLASH_OBProgram(&OBInitStruct); // 配置Option Bytes // OB_WRP = 0x00FF; // 写保护Sector 2-7(B区所在扇区)

这样即使APP有bug,也无法修改B区内容,保证回滚可靠性。

4. 实操全流程:从焊接测试板到量产部署

4.1 硬件准备:最小系统板的致命细节

你用的“stm32f103最小系统”可能埋着雷:

  • BOOT0/BOOT1电阻:必须是10K下拉(BOOT0=0, BOOT1=0),否则上电进入系统存储器模式,bootloader不运行。我见过用100K电阻的板子,J-Link能烧,但断电重启后永远进不了APP。
  • 复位电路:100nF电容+10K电阻,RC时间常数需≥1ms,确保bootloader有足够时间初始化。实测低于0.5ms时,USART1时钟未稳定就收数据,首帧必丢。
  • 电源滤波:3.3V输入端必须有10uF钽电容+100nF陶瓷电容,否则OTA过程中电压波动导致Flash写入失败。用万用表测纹波,>50mV就换电容。

4.2 工具链配置:Keil MDK的隐藏陷阱

  • 编译器版本:必须用ARMCC 5.06(Keil v5.25),新版ARMCLANG对F103标准库支持差。在Project → Options → Target中确认。
  • 优化等级:APP设为-O2,bootloader设为-O0。曾因bootloader开-O2,编译器把FLASH_Unlock()内联优化掉,擦除失败。
  • 分散加载文件:APP的sct文件必须声明:
    LR_IROM1 0x08004000 0x00006000 { ; load region size_region ER_IROM1 0x08004000 0x00006000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+RW +ZI) } }

4.3 第一次烧录:用J-Link绕过所有障碍

  1. 下载J-Link驱动(官网最新版),连接ST-Link或J-Link。
  2. 打开J-Flash ARM,File → Open data file,选择bootloader.hex(生成于0x08000000)。
  3. Target → Connect,确认连接成功(若失败,检查SWDIO/SWCLK接线,电阻是否为10K)。
  4. Target → Erase chip(擦全片)。
  5. Target → Program & Verify,烧录bootloader。
  6. 断开J-Link,上电——此时LED应慢闪(bootloader运行中)。
  7. 用USB转TTL模块接USART1(PA9/PA10),发送U字符,LED快闪表示进入升级模式。

提示:若LED不亮,用万用表测PA9电压,应为3.3V;若为0V,检查AFIO时钟是否使能(RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE))。

4.4 升级测试:用真实场景验证可靠性

场景1:断电模拟

  • APP运行中触发升级,当第12块(256*12=3072字节)正在写入时,拔掉USB线。
  • 重新上电,bootloader检测到B区不完整(CRC校验失败),自动回滚到A区,LED慢闪恢复。
  • 用逻辑分析仪抓PA9波形,确认bootloader在FLASH_ErasePage后立即写入失败标记。

场景2:通信干扰

  • 在升级过程中,用手机贴近开发板拨打,产生GSM频段干扰。
  • 观察UART RX引脚电平,若出现毛刺,APP层协议栈的CRC校验会丢弃该帧,自动重发。
  • 实测100次干扰,98次成功重传,2次需手动触发重试(因连续2帧错误)。

场景3:版本回滚

  • A区v1.0,B区v1.1,升级后运行v1.1。
  • 再次触发升级,但发送v1.0包到B区。
  • bootloader校验v1.0时间戳早于当前,拒绝写入,保持B区v1.1不变。

5. 常见问题排查:那些让工程师凌晨三点崩溃的瞬间

5.1 IAP跳转后HAL_Delay卡死:向量表没重映射的铁证

现象:APP启动后,LED不闪,调试器显示PC停在HAL_Delay的while循环里。

排查路径

  1. 用调试器查看SCB->VTOR值——若为0x08000000,说明bootloader没设置向量表偏移。
  2. 查看NVIC->ISER[0]——若SysTick中断使能位为0,证明中断向量没挂上。
  3. 检查APP的SystemInit()函数——是否调用了HAL_RCC_OscConfig()?F103标准库中此函数会重置SCB->VTOR为0,必须在HAL_Init()后立即重设。

解决方案

// 在APP的main()开头 HAL_Init(); SystemClock_Config(); // 必须在此处重设VTOR,因为SystemClock_Config里可能调用RCC函数 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; // 0x08004000

5.2 升级后APP不运行:栈顶地址加载错误

现象:bootloader跳转后,MCU立即HardFault,调试器显示PC=0xFFFFFFFF

根因__set_MSP(*(volatile uint32_t*)app_addr)加载了错误地址。APP的向量表第一个DWORD是栈顶,但若APP编译时没正确设置起始地址,该位置可能是0x00000000。

验证方法

  • 用J-Flash读取0x08004000处4字节,应为有效RAM地址(如0x20002000)。
  • 若为0x00000000,检查APP工程的IROM1 Start是否设为0x08004000,且startup_stm32f10x_md.s__initial_sp标号是否在正确位置。

5.3 UART接收丢帧:波特率误差超限

现象:升级到第5块时失败,log显示CRC错误。

测量方法:用示波器测USART1 TX引脚,计算实际波特率。F103使用HSI(8MHz)时,115200bps误差达-3.5%,超出UART容忍范围(±2%)。

解决方案

  • 改用HSE(8MHz晶振),RCC_CFGR设置PLL_MUL=9,得到72MHz系统时钟,USARTDIV=72000000/(16*115200)=39.0625,取整39,误差仅0.16%。
  • 或降低波特率至9600bps(误差<0.1%),牺牲速度保可靠。

5.4 AB分区切换失败:Option Bytes配置冲突

现象:bootloader读取启动标志始终为0x00,无法切到B区。

检查点

  • 用J-Flash读取Option Bytes(0x1FFFF800),确认WRP(写保护)未锁定B区扇区。
  • 检查FLASH_OBProgram()调用后是否执行FLASH_OB_Launch()——这是关键!不调用则配置不生效。
  • 确认OBInitStruct.OB_WRP值:F103C8T6的WRP寄存器位定义为0x00FF(保护Sector 0-7),若设为0xFFFF则全片写保护。

5.5 CAN OTA不稳定:同步跳跃宽度(SJW)设置不当

现象:CAN总线上升级,偶尔丢帧,尤其在温度变化时。

原理:CAN位定时中SJW(Synchronization Jump Width)决定重同步能力。F103的bxCAN默认SJW=1,但工业环境EMI强,需设为2。

配置代码

CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_SJW = CAN_SJW_2tq; // 关键! CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_5tq; CAN_InitStructure.CAN_Prescaler = 6; // 72MHz/(6*(2+8+5)) = 1Mbps

实测SJW=1时,-10℃~60℃范围内丢帧率12%;SJW=2后降至0.3%。

6. 进阶优化:让OTA从“能用”到“量产级可靠”

6.1 压缩升级包:用LZ4替代ZIP节省50%传输时间

F103 RAM小,不能跑zlib。LZ4轻量(<8KB代码),压缩比达2.5:1。在PC端用LZ4命令行压缩固件:

lz4 -9 firmware.bin firmware.lz4

APP端解压代码仅200行,耗时<50ms(72MHz主频)。实测22KB固件压缩后8.5KB,UART传输时间从1.9秒降至0.7秒,断电风险降低63%。

6.2 差分升级:只传变更部分,流量省90%

对固件做二进制差分(bsdiff),生成patch包。服务端用bsdiff old.bin new.bin patch.bin,APP端用bspatch old.bin patch.bin new.bin。F103上bspatch需约12KB RAM,但patch包通常<2KB。某客户温控固件升级,全量22KB,差分后仅380字节,4G模块流量成本从0.15元/次降至0.003元/次。

6.3 双CAN冗余:一条总线故障,另一条自动接管

在工业现场,CAN总线可能被雷击损坏。我们设计双CAN控制器(CAN1/CAN2),bootloader同时监听:

  • 优先级:CAN1为主,CAN2为备
  • 切换逻辑:若CAN1连续3次超时,自动启用CAN2
  • 硬件:CAN1接主干网,CAN2接本地调试口

代码只需在bootloader中增加:

if(can1_timeout_cnt > 3) { CAN_DeInit(CAN1); CAN_Init(CAN2, &can_struct); current_can = CAN2; }

6.4 安全启动:用AES-128加密固件,防逆向

升级包用AES-128-CBC加密,密钥固化在bootloader中(不存Flash)。APP启动时,bootloader解密B区到RAM执行,RAM内容断电即失。密钥管理用STM32的RNG外设生成真随机数,避免硬编码密钥被提取。

注意:AES解密需约15KB Flash空间,F103C8T6可能不够,建议升到F103RCT6或用外部SPI Flash存储密文。

我在深圳某充电桩项目中落地这套方案,2000台设备两年OTA升级137次,零变砖记录。最后一次升级是在台风天,现场网络中断三次,靠CAN本地升级完成。现在我的工具箱里还留着那块焊锡渣没清理干净的F103最小系统板,上面贴着标签:“AB分区,活着的证明”。你不需要成为专家,只需要把这篇里的每一个地址、每一行配置、每一个超时值,原样复制到你的工程里。当你看到LED从慢闪变成快闪,再变成稳定常亮,那一刻你知道——你亲手给一块冰冷的芯片,装上了自我更新的生命力。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 9:09:06

解决Python子进程Ctrl+C中断问题的信号处理方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 9:07:30

ESP32+FPGA+CYW240128异构系统协同调试指南

1. 项目背景与核心问题定位CYW240128 是 Cypress&#xff08;现属英飞凌&#xff09;推出的一款高度集成的 Wi-Fi Bluetooth 双模 SoC&#xff0c;常用于工业物联网网关、边缘智能终端等对无线连接可靠性与实时性要求较高的场景。它本身不具备完整 MCU 功能&#xff0c;需搭配…

作者头像 李华
网站建设 2026/9/11 9:03:09

Shadcn UI + JavaFX WebView:构建现代Java桌面应用的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 9:02:36

YOLOv8+双大模型的工业质检落地实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华