news 2026/10/28 9:13:41

ESP32 OTA双分区与自动回滚:让固件升级不再变砖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32 OTA双分区与自动回滚:让固件升级不再变砖

1. 从一次刷机翻车说起:ESP32 到底会不会变砖

很多人第一次给 ESP32 刷固件,手都是抖的。尤其是用 USB-TTL 接上开发板,敲下esptool.py write_flash回车的那一瞬间,脑子里全是“万一刷坏了怎么办”“这板子是不是就废了”。我当年也是这么过来的,第一块 ESP32-WROOM-32 因为分区表写错,上电之后串口只吐乱码,当时真以为它彻底没救了。

后来折腾多了才明白,ESP32 在绝大多数情况下不会真正变砖。它和很多单片机不一样的地方在于,芯片内部固化了一段 ROM Bootloader,这段代码是出厂就烧死在硅片里的,你通过常规手段根本改不了它。只要这段 ROM 还在,芯片就永远保留了一条“最低限度的自救通道”。真正意义上的“砖”,是指连 ROM Bootloader 都无法正常进入、串口完全无响应、下载模式也拉不起来的状态,而这种状态通常只有硬件损坏、供电异常烧毁、或者把 eFuse 里的关键位写死才会出现。

那为什么大家还是这么怕刷坏?因为“不会变砖”和“刷坏了能轻松救回来”是两码事。固件刷错、分区表不匹配、OTA 升级中途断电,这些都会让设备无法正常启动应用,表现上跟砖没区别——串口没输出、WiFi 连不上、LED 不亮。你要救它,得知道怎么手动进下载模式、怎么重新烧录、怎么处理分区表。这些操作对老手是肌肉记忆,对新手就是一道坎。

这篇文章要聊的核心,就是标题里那件事:用双分区加自动回滚,把“刷坏”这件事从灾难降级成一次普通的重启。我会把 ESP32 的分区机制、OTA 升级流程、回滚原理、实操配置、踩坑经验全部拆开讲清楚。不管你是刚拿到第一块 ESP32 核心板的新手,还是已经在做量产固件升级的老手,这套思路都能直接用。关键词就几个:ESP32、固件、双分区、自动回滚、OTA,全文围绕它们展开,不跑偏。

先说结论,让你心里有底:ESP32 的 OTA 机制天生就支持 A/B 双分区,配合 ESP-IDF 里的esp_ota组件和回滚标记,可以做到“新固件启动失败自动退回旧固件”。这不是什么黑科技,是官方框架里就有的能力,只是很多人没把它配起来。下面我从设计思路开始,一层层往下拆。

2. 双分区与自动回滚的整体设计思路

2.1 为什么单分区 OTA 是个危险游戏

先说说最朴素的 OTA 做法:整个 Flash 只放一个应用分区,升级的时候把新固件直接覆盖写到这个分区上。这种做法在资源紧张的年代很常见,省空间,逻辑也简单。但它有个致命问题——写入过程中一旦断电、复位、或者新固件本身有 bug,设备就再也起不来了。因为旧固件已经被覆盖掉,新固件又不完整或不可用,等于把自己唯一的退路给堵死了。

我见过太多项目栽在这上面。有个做温湿度采集节点的朋友,设备装在户外杆子上,OTA 升级到一半遇到电压波动,结果那批设备全部失联,只能派人爬杆子拆下来重新烧录。这种成本,一次就够你记住教训。单分区 OTA 的本质问题是没有冗余,它假设升级过程永远顺利、新固件永远正确,而现实里这两个假设都不成立。

所以正确的思路是:永远保留一份可用的旧固件,新固件写到另一块区域,验证通过后再切换过去。这就是双分区(A/B 分区)的核心思想。它牺牲了一部分 Flash 空间,换来的是升级过程的安全性。对于现在动辄 4MB、8MB 甚至 16MB Flash 的 ESP32-S3 核心板来说,这点空间完全值得。

2.2 A/B 双分区到底是怎么排布的

ESP32 的分区表是一张写在 Flash 固定偏移处的表,默认在0x8000位置,长度0xC00。它描述了 Flash 被切成了哪些区域、每个区域的类型、子类型、偏移和大小。一个典型的支持 OTA 的分区表长这样:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x140000, ota_0, app, ota_0, 0x150000,0x140000, ota_1, app, ota_1, 0x290000,0x140000,

这里有几个关键角色。factory是出厂固件分区,可选;ota_0和ota_1就是我们的 A/B 两个应用分区,大小相同;otadata是一个很小的数据分区,专门用来记录“当前该从哪个分区启动”。注意otadata只有0x2000(8KB),但它决定了整个启动流程的走向,非常关键。

启动时,ROM Bootloader 会先读分区表,然后看otadata里的内容。如果otadata是空的(全0xFF),它会去启动factory分区;如果otadata里记录了有效的 OTA 信息,它就会按记录去启动ota_0或ota_1。这个机制就是双分区切换的基础。你可以把它理解成电脑的“双系统引导”,otadata就是那个引导菜单,只不过它是自动的、程序可控的。

2.3 自动回滚的判定逻辑:谁来决定“这次启动算成功”

光有双分区还不够,还得有个机制来判断“新固件到底行不行”。ESP-IDF 的做法是引入一个回滚标记(rollback flag)。流程是这样的:新固件被写入ota_1后,otadata被更新为“下次从ota_1启动”,同时系统进入一个“待验证”状态。新固件启动后,你的应用代码需要主动调用esp_ota_mark_app_valid_cancel_rollback()来告诉系统“我启动成功了,别回滚”。

如果你没调用这个函数,或者新固件在启动过程中崩溃、看门狗复位、反复重启,Bootloader 就会认为这次升级失败,自动把启动目标切回原来的ota_0。这个“待验证”的窗口期由CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE控制,配合esp_ota_get_state_partition()可以查询当前状态。

这套逻辑的精妙之处在于:判断权交给了应用自己。你可以在固件里做各种自检——WiFi 能不能连上、传感器能不能读到数据、关键任务有没有正常启动——全部通过后再打标记。这样即使新固件“能启动但功能不正常”,你也能主动触发回滚。这比单纯依赖“启动失败”要可靠得多。

注意:自动回滚依赖otadata分区的正确读写。如果你在代码里手动擦除了otadata,或者分区表里没给它留位置,回滚机制会直接失效,设备可能卡在某个分区反复重启。

3. 核心细节解析与实操要点

3.1 分区表怎么配才不出错

分区表是整套机制的根基,配错了后面全白搭。我建议直接用 ESP-IDF 自带的partitions_two_ota.csv作为起点,路径在components/partition_table/下。它的内容和我上面贴的类似,你只需要根据自己芯片的 Flash 大小调整各分区尺寸。

这里有个容易踩的坑:两个 OTA 分区必须大小完全一致。因为 OTA 升级时新固件要能完整写进目标分区,如果ota_0和ota_1大小不同,切换时就会出现“这个分区装得下、那个装不下”的尴尬。另外,应用分区的大小要留足余量,一般建议实际固件体积不超过分区容量的 70%,给未来的功能扩展和 OTA 临时数据留空间。

还有一个细节是otadata的偏移。它必须紧跟在nvs之后、phy_init之前,且不能被其他分区覆盖。有些朋友自己手写分区表,把otadata放错位置,结果 OTA 升级后设备启动异常,查半天查不出来。我的习惯是:分区表改完后,用idf.py partition-table命令打印一遍,肉眼核对每个分区的偏移和大小有没有重叠。这个动作花不了十秒,能省掉几小时的排查。

对于 ESP32-S3 这类带 16MB Flash 的板子,你可以把两个 OTA 分区各开到 2MB 甚至 4MB,这样固件可以做得比较肥,不用天天担心超容量。而对于 4MB 的经典 ESP32,两个 OTA 分区各 1.25MB 左右是比较常见的配置,固件得精简着写。

3.2 OTA 升级的完整数据流

理解了分区,再看 OTA 升级时数据是怎么流动的。假设当前运行在ota_0,我们要升级到新版本:

  1. 应用通过 WiFi 或蓝牙接收到新固件数据(可以是 HTTP 下载、BLE 传输、或者从 SD 卡读取)。
  2. 调用esp_ota_begin(),指定目标分区为ota_1,系统会擦除该分区并准备写入。
  3. 循环调用esp_ota_write(),把固件数据分块写入ota_1。
  4. 全部写完后调用esp_ota_end(),系统会校验固件镜像的完整性(包括 SHA256 校验和)。
  5. 校验通过后调用esp_ota_set_boot_partition(),把otadata更新为“下次从ota_1启动”。
  6. 重启设备,Bootloader 读取otadata,从ota_1启动新固件。
  7. 新固件启动后执行自检,通过则调用esp_ota_mark_app_valid_cancel_rollback(),回滚标记清除,升级正式生效。

这个流程里,第 4 步的校验和第 7 步的标记是两个安全闸门。校验保证固件数据没传坏,标记保证固件功能真的可用。两者缺一不可。我见过有人只做了写入不做校验,结果网络抖动导致固件缺了几个字节,设备启动后行为诡异;也有人忘了打标记,设备每次重启都回滚,白白折腾。

3.3 回滚标记的代码实现要点

回滚标记的代码其实不复杂,但位置很讲究。你不能在app_main()一开头就打标记,因为那时候 WiFi、传感器、业务逻辑都还没初始化,打了标记等于没验证。正确的做法是:把标记放在所有关键初始化完成之后,最好是在一个独立的“自检任务”里。

我一般的写法是:app_main()里先做基础初始化,然后启动一个自检任务,这个任务依次检查 WiFi 连接、NTP 对时、传感器读数、关键外设通信,全部通过后延时几秒(给系统一个稳定期),再调用esp_ota_mark_app_valid_cancel_rollback()。如果任何一项检查失败,就不打标记,让系统在下次重启时自动回滚。

static void self_check_task(void *arg) { // 等待 WiFi 连接 if (!wait_wifi_connected(30000)) { ESP_LOGE(TAG, "WiFi 自检失败,不回滚标记"); vTaskDelete(NULL); return; } // 检查传感器 if (!sensor_probe()) { ESP_LOGE(TAG, "传感器自检失败"); vTaskDelete(NULL); return; } // 全部通过,延时稳定后打标记 vTaskDelay(pdMS_TO_TICKS(5000)); esp_ota_mark_app_valid_cancel_rollback(); ESP_LOGI(TAG, "固件自检通过,回滚标记已清除"); vTaskDelete(NULL); }

这段代码的关键点是:失败路径直接返回,不打标记。这样 Bootloader 在下次启动时发现标记还在,就会回滚。另外,自检任务本身要有超时保护,不能无限等待,否则设备卡在自检里,看门狗复位后同样会触发回滚,效果是一样的,但日志会更难看。

提示:esp_ota_mark_app_valid_cancel_rollback()只能在“待验证”状态下调用。如果当前不是待验证状态(比如正常运行中的重启),调用它会返回错误,这是正常的,不用慌。

4. 实操过程与核心环节实现

4.1 环境准备与工程配置

我用的环境是 ESP-IDF v5.x,配合 VS Code 的 ESP-IDF 插件。如果你还在用 Arduino IDE,也能做 OTA,但双分区和回滚的配置没有 IDF 这么直观,后面我会单独提一句。先按 IDF 的路子走。

第一步,创建工程后打开menuconfig,找到Bootloader config,确认Enable app rollback support是打开的。这个选项对应CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE,不开的话回滚机制根本不生效。然后在Partition Table里选择Custom partition table CSV,指向你自己的分区表文件。

第二步,确认Component config -> ESP32-specific -> Support for external, SPI-connected RAM之类的选项按你的板子实际情况配。如果你用的是 ESP32-S3 核心板板载 1-N16R8(16MB Flash + 8MB PSRAM),Flash 大小要选 16MB,分区表也要相应放大。这个配置在 PlatformIO 里对应board_build.partitions和board_upload.flash_size,别选错开发板型号,否则烧录地址会偏。

第三步,把 OTA 相关的组件加进来。IDF 里主要是esp_ota_ops.h和esp_https_ota.h(如果你用 HTTPS 升级)。我一般还会加一个esp_ota_ops的状态查询,方便调试时看当前在哪个分区、回滚标记是什么状态。

4.2 分区表文件的具体写法

我拿一块 4MB Flash 的 ESP32 举例,分区表这样写:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x100000, ota_0, app, ota_0, 0x110000,0x100000, ota_1, app, ota_1, 0x210000,0x100000,

算一下:factory从0x10000开始,大小0x100000(1MB),结束在0x110000;ota_0从0x110000开始,1MB,结束在0x210000;ota_1从0x210000开始,1MB,结束在0x310000。总共用了约 3.1MB,4MB Flash 还剩一些余量给其他数据分区。这个配置里factory是可选的,如果你不需要出厂固件,可以把factory去掉,把空间分给两个 OTA 分区,各开到 1.5MB。

对于 16MB 的 S3 板子,我会把两个 OTA 分区各开到 4MB,factory也留 4MB,剩下的给 SPIFFS 或 FATFS 存数据。这样固件可以塞进很多功能,OTA 升级时也不用担心容量。

4.3 一个可复现的 OTA 升级示例

下面这段代码演示了从 HTTP 服务器下载固件并写入 OTA 分区的核心逻辑。我把它简化成最关键的几步,你可以直接拿去改。

#include "esp_ota_ops.h" #include "esp_http_client.h" #include "esp_log.h" static const char *TAG = "OTA_DEMO"; void ota_update_task(void *pvParameter) { const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL); if (update_partition == NULL) { ESP_LOGE(TAG, "找不到可用的 OTA 分区"); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "目标分区: %s, 偏移: 0x%lx", update_partition->label, update_partition->address); esp_ota_handle_t ota_handle; esp_err_t err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &ota_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err)); vTaskDelete(NULL); return; } // 这里用 HTTP 客户端读取固件数据,实际项目里换成你的数据源 esp_http_client_config_t config = { .url = "http://your-server/firmware.bin", .timeout_ms = 10000, }; esp_http_client_handle_t client = esp_http_client_init(&config); esp_http_client_open(client, 0); int content_length = esp_http_client_fetch_headers(client); char buffer[1024]; int total_read = 0; while (1) { int read_len = esp_http_client_read(client, buffer, sizeof(buffer)); if (read_len <= 0) break; err = esp_ota_write(ota_handle, buffer, read_len); if (err != ESP_OK) { ESP_LOGE(TAG, "写入失败: %s", esp_err_to_name(err)); esp_ota_abort(ota_handle); esp_http_client_cleanup(client); vTaskDelete(NULL); return; } total_read += read_len; } esp_http_client_close(client); esp_http_client_cleanup(client); err = esp_ota_end(ota_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ota_end 失败,固件校验不通过: %s", esp_err_to_name(err)); vTaskDelete(NULL); return; } err = esp_ota_set_boot_partition(update_partition); if (err != ESP_OK) { ESP_LOGE(TAG, "设置启动分区失败: %s", esp_err_to_name(err)); vTaskDelete(NULL); return; } ESP_LOGI(TAG, "OTA 写入完成,共 %d 字节,准备重启", total_read); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); }

这段代码里,esp_ota_get_next_update_partition(NULL)会自动帮你找到“当前没在运行的那个 OTA 分区”。比如现在跑在ota_0,它就返回ota_1,反之亦然。这个函数省去了手动判断的麻烦,强烈建议用。

esp_ota_begin的第二个参数我用了OTA_WITH_SEQUENTIAL_WRITES,这是告诉系统按顺序写入,适合流式下载。如果你的固件是先完整下载到内存再写入,可以用OTA_SIZE_UNKNOWN。写入过程中如果出错,一定要调用esp_ota_abort释放句柄,否则下次 OTA 会失败。

4.4 自检与回滚的联动实测

我把上面这套代码烧进一块 ESP32-WROOM-32,做了几组测试。第一组:正常升级,新固件自检通过,打标记,重启后稳定运行在新分区。第二组:故意在新固件里让 WiFi 连接失败,自检不通过,不打标记,重启后设备自动回到旧分区,串口日志显示Rollback相关提示。第三组:升级过程中拔掉电源,重新上电后设备从旧分区启动,因为otadata还没被更新到新分区。

实测下来,回滚的触发非常可靠。唯一需要注意的是,回滚后otadata会被更新回旧分区,但新分区里的固件还在。如果你再次 OTA,系统会重新写入那个分区,没问题。但如果你想手动清理,可以用esp_ota_erase_last_boot_app_partition()擦掉它。

还有一点,回滚发生时,设备会重启一次,这次重启的日志里会有boot: Rollback或类似的字样。如果你在串口看不到,可以调高 bootloader 日志级别。这个日志对排查问题很有用,别忽略。

5. 常见问题与排查技巧实录

5.1 OTA 升级后设备反复重启怎么办

这是最常见的问题,表现是设备升级后不断重启,串口日志显示abort() was called或者看门狗复位。原因通常是新固件有 bug,但自检逻辑没拦住它,或者根本没配回滚。排查步骤:先看otadata状态,用esp_ota_get_state_partition()查询当前是不是ESP_OTA_IMG_PENDING_VERIFY。如果是,说明回滚标记没打,系统在等你确认。这时候要么修复自检逻辑,要么手动调用标记函数。

如果otadata显示已经是ESP_OTA_IMG_VALID,但设备还是重启,那就是固件本身的问题,跟 OTA 机制无关了。这时候得回退到旧固件,用串口重新烧录,或者通过 OTA 推一个修复版本。

5.2 分区表不匹配导致启动失败

症状是设备上电后串口只输出invalid header: 0xffffffff或者partition table not found。这通常是分区表偏移写错,或者 Flash 大小配置和实际芯片不符。比如你按 4MB 配的分区表,烧到 8MB 的板子上,理论上能跑,但如果分区表里某个分区超出了实际 Flash 范围,就会出问题。反过来,8MB 的分区表烧到 4MB 板子上,直接崩。

解决办法:用esptool.py flash_id确认芯片实际 Flash 大小,然后在menuconfig里选对。分区表改完后,用idf.py partition-table打印核对。烧录时注意esptool.py的--flash_size参数要和实际一致。

5.3 回滚标记打了但没生效

有朋友反馈说调用了esp_ota_mark_app_valid_cancel_rollback(),但设备还是回滚了。查下来发现是CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE没开。这个选项默认可能是关的,必须在menuconfig里手动打开。另外,如果你用的是自定义 bootloader,也要确认它支持回滚。

还有一种情况是otadata分区被意外擦除。比如你的代码里有nvs_flash_erase()之类的操作,如果分区表里otadata和nvs挨得太近,或者偏移算错,可能误擦。检查分区表,确保otadata独立且不被其他操作波及。

5.4 常见问题速查表

问题现象可能原因排查方法解决方式
升级后反复重启自检未通过或未打回滚标记查otadata状态修复自检逻辑,调用标记函数
串口输出乱码分区表偏移错误idf.py partition-table核对修正分区表,重新烧录
OTA 写入失败目标分区空间不足查固件体积和分区大小扩大 OTA 分区或精简固件
回滚不触发回滚选项未开启查menuconfig配置打开APP_ROLLBACK_ENABLE
设备完全无响应供电或硬件问题测电压,换 USB 线检查硬件,重新上电
升级后 WiFi 连不上新固件配置错误看串口日志回滚或修复固件

5.5 几个我踩过的坑

第一个坑:OTA 分区大小算错。我早期做项目时,两个 OTA 分区一个 1MB 一个 1.2MB,结果升级时新固件 1.1MB,写不进 1MB 的那个分区,直接失败。后来统一成一样大,问题消失。所以再强调一遍,两个 OTA 分区必须等大。

第二个坑:自检任务优先级太低。我把自检任务设成最低优先级,结果系统里其他任务把 CPU 占满,自检迟迟不执行,看门狗先复位了,触发误回滚。后来把自检任务优先级调到中等,并加了超时保护,稳定了。

第三个坑:HTTP 下载没做断点续传。网络不稳时下载中断,esp_ota_write写了一半,esp_ota_end校验失败,设备回滚。虽然回滚救回来了,但用户体验差。后来加了重试和断点续传,升级成功率高了很多。

第四个坑:忘了擦除旧分区。有一次我手动往ota_1写固件,没先擦除,结果新旧数据混在一起,校验失败。esp_ota_begin默认会擦除目标分区,但如果你绕过它直接写 Flash,就得自己擦。这个坑不常见,但一旦遇到很迷惑。

6. 关于固件安全与量产的一些延伸

6.1 固件加密和签名要不要上

双分区和回滚解决的是“升级失败能恢复”,但没解决“固件被篡改”。如果你的设备部署在不可控环境,建议开启Secure Boot和Flash Encryption。Secure Boot 保证只有签名的固件能启动,Flash Encryption 保证 Flash 里的内容被加密,别人读出来也没用。这两个功能都依赖 eFuse,一旦开启不可逆,所以量产前要想清楚。

开启 Secure Boot 后,OTA 升级的固件必须用对应的密钥签名,否则 Bootloader 拒绝启动。这增加了升级流程的复杂度,但安全性提升明显。我的建议是:消费类产品可以先用双分区加回滚,把稳定性做扎实;涉及敏感数据或商业机密的产品,再上 Secure Boot。

6.2 量产时的 OTA 策略

量产设备多了之后,OTA 不能一台台手动推。通常的做法是搭一个固件分发服务器,设备定期查询版本,有更新就下载升级。服务器端要记录每台设备的当前版本、升级状态、回滚次数,方便排查问题。设备端要做好退避策略,升级失败后不要立刻重试,等一段时间再试,避免把服务器打挂。

另外,量产固件建议默认带 factory 分区,出厂时烧录 factory 固件,第一次 OTA 升级到 ota_0,后续在 ota_0 和 ota_1 之间切换。这样即使 OTA 机制出问题,还能退回 factory 分区,多一层保险。

6.3 和 Arduino 环境的差异

如果你用 Arduino IDE 开发 ESP32,OTA 也能做,但双分区和回滚的配置没有 IDF 直观。Arduino 的Update库封装了 OTA 写入,但回滚标记需要手动调esp_ota_mark_app_valid_cancel_rollback(),而且分区表要在boards.txt或 PlatformIO 的board_build.partitions里指定。我建议做正式产品还是用 IDF,Arduino 适合快速验证。

PlatformIO 里配置分区表,在platformio.ini里加board_build.partitions = partitions_two_ota.csv,然后把分区表文件放在工程根目录。烧录时 PlatformIO 会自动处理。注意选对开发板型号,ESP32-S3 的 1-N16R8 要选对应的 board 定义,否则 Flash 大小和 PSRAM 配置会不对。

7. 最后聊几句实际体会

这套双分区加自动回滚的方案,我在好几个项目里用了两年多,最大的感受是:它把 OTA 升级从“提心吊胆”变成了“例行公事”。以前推固件要挑半夜、要盯着日志、要准备好救砖工具;现在推完该干嘛干嘛,失败了设备自己回滚,第二天看日志再修。这种心态上的变化,比省下的那点 Flash 空间值钱得多。

当然,它也不是万能的。如果新固件把分区表本身写坏了,或者把 bootloader 覆盖了,回滚也救不了。所以升级逻辑里要严格限制写入范围,只动 OTA 分区,别碰 bootloader 和分区表。另外,自检逻辑要写得务实,别搞太复杂的检查,否则启动慢、误判多。我的经验是:检查最关键的 2 到 3 项功能,比如网络和核心外设,通过了就打标记,剩下的交给运行时监控。

如果你刚开始接触 ESP32 的 OTA,建议先拿一块开发板,按这篇文章的步骤走一遍,故意制造一次失败升级,亲眼看它回滚。这个过程走通了,你对整套机制的理解会比看十篇文档都深。后面再上量产、上加密、上签名,都是在这个基础上加东西,心里有底。

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

硬件测试工程师岗位详解:从技能栈到实战排错全指南

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

作者头像 李华
网站建设 2026/10/28 9:11:37

热泵谐波治理全流程拆解:从实测到APF选型与调试

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

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

STM32消防预警系统实战:WiFi联网与MQTT远程报警全解析

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

作者头像 李华
网站建设 2026/10/28 9:09:34

MATLAB例程包跑不通?从解压到参数改造的完整避坑指南

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

作者头像 李华
网站建设 2026/10/28 9:09:07

自托管博客系统源码实战:Markdown渲染、SQLite搜索与部署避坑

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

作者头像 李华
网站建设 2026/10/28 9:08:44

STM32寄存器白话手册:从GPIO到定时器的实战避坑指南

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

作者头像 李华