LZ-SG/embedded-dev-tutorials
0
1#!/usr/bin/env python32"""Expand remaining 7 thin chapters: ch03, ch06, ch07, ch08, ch11, ch12, ch13"""3import os4 5BASE = r"C:\Users\LYS\.qclaw\workspace\embedded-dev-site\static\tutorials\guide"6 7# Each entry: (filename, h2_section_title, expansion_html)8# The expansion is inserted before the first chnav div in each file9 10EXPANSIONS = {11 "guide-ch03.html": r"""12<h2>🔧 自动变量详解</h2>13 14<p>Makefile最强大的特性之一是自动变量——它们在规则执行时自动替换,让你写出简洁、可维护的构建系统。</p>15 16<table>17<tr><th>变量</th><th>含义</th><th>示例</th></tr>18<tr><td><code>$@</code></td><td>目标文件名</td><td>规则 <code>main.o: main.c</code> 中$@ = "main.o"</td></tr>19<tr><td><code>$<</code></td><td>第一个依赖文件名</td><td>规则中$< = "main.c"</td></tr>20<tr><td><code>$^</code></td><td>所有依赖文件(去重)</td><td>规则 <code>app: a.o b.o</code> 中$^ = "a.o b.o"</td></tr>21<tr><td><code>$+</code></td><td>所有依赖文件(保留重复)</td><td>与$^类似但不去重</td></tr>22<tr><td><code>$?</code></td><td>比目标更新的依赖</td><td>增量编译核心变量</td></tr>23<tr><td><code>$*</code></td><td>模式匹配的stem部分</td><td>模式 <code>%.o: %.c</code> 匹配main.o时$* = "main"</td></tr>24<tr><td><code>$(@D)</code></td><td>目标文件的目录部分</td><td>目标 "obj/main.o" 中$(@D) = "obj"</td></tr>25<tr><td><code>$(@F)</code></td><td>目标文件的文件名部分</td><td>目标 "obj/main.o" 中$(@F) = "main.o"</td></tr>26</table>27 28<pre><code># 模式规则实战:一条规则编译所有.c文件29# %.o: %.c → 匹配任意.c文件生成对应的.o文件30 31OBJ_DIR = build32SRCS = $(wildcard src/*.c)33OBJS = $(patsubst src/%.c,$(OBJ_DIR)/%.o,$(SRCS))34 35# 自动变量对比36$(OBJ_DIR)/%.o: src/%.c37 @echo "编译: $< → $@" # $< = src/main.c, $@ = build/main.o38 arm-none-eabi-gcc -c $< -o $@39 40# 链接步骤41$(OBJ_DIR)/app.elf: $(OBJS)42 @echo "链接 $^" # $^ = 所有.o文件43 arm-none-eabi-gcc $^ -o $@</code></pre>44 45<div class="tip">💡 <b>记忆口诀:</b>$@ = 目标(At),$< = 第一个依赖(<表示输入),$^ = 所有(^像屋顶盖住全部)</div>46 47<h2>📂 函数与文本处理</h2>48 49<p>Makefile内置了大量函数,让你像写脚本一样处理文件名、字符串和条件判断。</p>50 51<h3>常用函数速查</h3>52 53<table>54<tr><th>函数</th><th>功能</th><th>示例</th></tr>55<tr><td><code>$(wildcard pattern)</code></td><td>通配符匹配文件</td><td><code>$(wildcard src/*.c)</code></td></tr>56<tr><td><code>$(patsubst pat,rep,text)</code></td><td>模式替换</td><td><code>$(patsubst %.c,%.o,$(SRCS))</code></td></tr>57<tr><td><code>$(notdir names)</code></td><td>去掉路径,只留文件名</td><td><code>$(notdir src/main.c)</code> → main.c</td></tr>58<tr><td><code>$(dir names)</code></td><td>只取目录部分</td><td><code>$(dir src/main.c)</code> → src/</td></tr>59<tr><td><code>$(addprefix pre,names)</code></td><td>加前缀</td><td><code>$(addprefix build/,$(OBJS))</code></td></tr>60<tr><td><code>$(addsuffix suf,names)</code></td><td>加后缀</td><td><code>$(addsuffix .o,$(MODULES))</code></td></tr>61<tr><td><code>$(shell cmd)</code></td><td>执行shell命令</td><td><code>GIT_HASH := $(shell git rev-parse --short HEAD)</code></td></tr>62<tr><td><code>$(foreach var,list,text)</code></td><td>循环处理</td><td><code>$(foreach d,$(DIRS),build/$(d))</code></td></tr>63<tr><td><code>$(if condition,then,else)</code></td><td>条件判断</td><td><code>$(if $(DEBUG),-g,-O2)</code></td></tr>64<tr><td><code>$(call macro,arg1,arg2)</code></td><td>调用自定义宏</td><td>见下方示例</td></tr>65</table>66 67<pre><code># 自定义宏(Makefile的"函数")68# 定义:生成编译命令的宏69define compile_rule70$(1)/%.o: $(2)/%.c71 @echo "[CC] $$<"72 arm-none-eabi-gcc -c $$< -o $$@ $(CFLAGS)73endef74 75# 调用:为源目录和目标目录生成规则76$(eval $(call compile_rule,build,src))77 78# 批量生成目录79DIRS = src/main src/driver src/hal80BUILD_DIRS = $(addprefix build/,$(DIRS))81$(BUILD_DIRS):82 mkdir -p $@</code></pre>83 84<h2>🐛 Makefile排错实战</h2>85 86<div class="trouble">🔍 <b>错误:Makefile:2: *** missing separator. Stop.</b><br>87最常见的Makefile错误!规则下的命令行必须用<b>TAB键</b>(不是空格)开头。检查你的编辑器是否把TAB自动转换成了空格。<br>88<pre><code># 正确 ✓ (前面是TAB)89target:90	command91 92# 错误 ✗ (前面是空格)93target:94 command</code></pre>95<b>解决:</b>在VS Code中按 <code>Ctrl+Shift+P</code> → "Convert Indentation to Tabs"</div>96 97<div class="trouble">🔍 <b>错误:No rule to make target 'xxx.o'</b><br>98Make找不到如何生成xxx.o的规则。可能原因:①.c文件不存在或路径拼错 ②模式规则没写对 ③文件名大小写不匹配。</div>99 100<div class="trouble">🔍 <b>错误:overriding recipe for target / warning: ignoring old recipe</b><br>101同一个目标被定义了两次。检查是否有重复的规则或include了重复的.mk文件。</div>102 103<h2>🎯 实战练习</h2>104 105<div class="tip">📝 <b>练习1:</b>写一个Makefile,支持 <code>make debug</code> 编译调试版本(-O0 -g3)和 <code>make release</code> 编译发布版本(-O2 -DNDEBUG)。要求默认目标是debug。<br><br>106📝 <b>练习2:</b>在上面的Makefile中添加 <code>make flash</code> 目标,自动调用OpenOCD烧录生成的.elf文件。<br><br>107📝 <b>练习3:</b>添加 <code>make size</code> 目标,编译后自动运行arm-none-eabi-size显示内存占用。</div>108 109<h2>📋 知识点回顾</h2>110<div class="summary">111<b>✅ Makefile核心概念:</b><br>112• 规则 = 目标 + 依赖 + 命令(命令前必须是TAB)<br>113• 自动变量:$@(目标)、$<(第一个依赖)、$^(所有依赖)<br>114• 模式规则:%.o: %.c 一条规则处理所有同类文件<br>115• 变量赋值:=(递归)、:=(立即)、?=(条件)、+=(追加)<br>116• 函数:wildcard/patsubst/foreach/shell/if/call<br>117• 伪目标:.PHONY声明非文件目标(clean/flash/all)<br>118• 条件编译:ifeq/ifneq/ifdef实现Debug/Release切换<br>119<b>🛠 常用技巧:</b><br>120• make -n 空跑查看命令不执行(调试神器)<br>121• make -j4 4线程并行编译(快4倍)<br>122• make V=1 显示详细编译命令<br>123• 善用$(info ...)和$(warning ...)打印调试信息124</div>125""",126 127 "guide-ch06.html": r"""128<h2>🔧 HAL库深度解析</h2>129 130<p>HAL(Hardware Abstraction Layer)是STM32官方的硬件抽象层,但很多细节文档不讲。这一节补充那些让你从"能用"到"会用"的关键知识。</p>131 132<h3>HAL库状态机</h3>133 134<pre><code>// 每个HAL函数返回HAL_StatusTypeDef135typedef enum {136 HAL_OK = 0x00, // 成功137 HAL_ERROR = 0x01, // 一般错误138 HAL_BUSY = 0x02, // 资源忙(DMA/外设正在使用)139 HAL_TIMEOUT = 0x03 // 超时(通常外设没响应)140} HAL_StatusTypeDef;141 142// 黄金法则:永远检查返回值!143if (HAL_UART_Transmit(&huart2, data, len, 100) != HAL_OK) {144 // 发送失败,记录错误或重试145 Error_Handler();146}</code></pre>147 148<h3>Tick定时器与HAL_Delay</h3>149 150<div class="warn">⚠ <b>关键坑:</b>HAL_Delay依赖SysTick中断。如果你在中断服务函数(尤其是优先级高于SysTick的中断)里调用HAL_Delay,会死循环——因为SysTick中断被阻塞了,计数器永远不会更新!</div>151 152<pre><code">// 正确做法:用非阻塞延时153uint32_t tickstart = HAL_GetTick();154while ((HAL_GetTick() - tickstart) < timeout) {155 // 可以做其他事,或者直接等156 if (HAL_UART_Transmit(&huart2, data, 1, 1) == HAL_OK) break;157}158 159// 或者实现自己的微秒延时(不用SysTick)160void delay_us(uint32_t us) {161 uint32_t cnt = us * (SystemCoreClock / 1000000) / 5;162 while (cnt--) __NOP();163}</code></pre>164 165<h3>GPIO速度等级</h3>166 167<table>168<tr><th>速度</th><th>最大频率</th><th>适用场景</th></tr>169<tr><td>GPIO_SPEED_FREQ_LOW</td><td>2 MHz</td><td>LED、按钮、低速信号</td></tr>170<tr><td>GPIO_SPEED_FREQ_MEDIUM</td><td>25 MHz</td><td>普通IO、UART、I2C</td></tr>171<tr><td>GPIO_SPEED_FREQ_HIGH</td><td>50 MHz</td><td>SPI、PWM、高速信号</td></tr>172<tr><td>GPIO_SPEED_FREQ_VERY_HIGH</td><td>100 MHz</td><td>FMC/SDRAM、高速并行总线</td></tr>173</table>174 175<div class="note">💊 <b>EMI提示:</b>不要把不必要的高速IO设太高——速度越快,电磁干扰(EMI)越大,功耗也更高。LED用Low就够了。</div>176 177<h2>📡 DMA(直接存储器访问)实战</h2>178 179<p>DMA是STM32最强大的特性之一。理解它,你的程序效率能提升10倍。</p>180 181<pre><code>// ADC + DMA 连续采样:CPU零干预采集2048个样本182#define ADC_BUF_SIZE 2048183uint16_t adc_buffer[ADC_BUF_SIZE];184 185void ADC_DMA_Init(void) {186 // 1. 配置ADC(略)187 // 2. 配置DMA188 __HAL_RCC_DMA1_CLK_ENABLE();189 190 hdma_adc.Instance = DMA1_Channel1;191 hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; // 外设→内存192 hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不变193 hdma_adc.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增194 hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 16位195 hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;196 hdma_adc.Init.Mode = DMA_CIRCULAR; // 循环模式197 hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;198 HAL_DMA_Init(&hdma_adc);199 200 __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);201 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUF_SIZE);202}203 204// DMA传输完成回调205void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {206 // 2048个样本采集完毕,可以处理了207 // adc_buffer[] 已经自动填充,无需CPU逐个读取!208 float avg = 0;209 for (int i = 0; i < ADC_BUF_SIZE; i++) avg += adc_buffer[i];210 avg /= ADC_BUF_SIZE;211}</code></pre>212 213<div class="card">214<div class="h"><span class="name">DMA传输方向</span><span class="tag b">进阶</span></div>215<div class="b">216<div class="desc">DMA不只支持外设→内存,完整方向如下:</div>217<table>218<tr><th>方向</th><th>典型应用</th></tr>219<tr><td>内存→外设 (MEMORY_TO_PERIPH)</td><td>UART发送大段数据、SPI发送显示缓冲区</td></tr>220<tr><td>外设→内存 (PERIPH_TO_MEMORY)</td><td>ADC连续采样、UART接收不定长数据</td></tr>221<tr><td>内存→内存 (MEMORY_TO_MEMORY)</td><td>快速memcpy(不用CPU,但占DMA带宽)</td></tr>222</table>223</div></div>224 225<h2>🎛 系统时钟配置实战</h2>226 227<div class="analogy">💡 <b>类比:</b>STM32的时钟树就像一棵倒着长的大树——<b>树干</b>(HSE 8MHz/HSI 16MHz) → <b>粗枝</b>(PLL倍频到72/168/480MHz) → <b>细枝</b>(AHB/APB1/APB2分频器) → <b>树叶</b>(各外设)</div>228 229<pre><code>// F103:8MHz外部晶振 → PLL ×9 → 72MHz系统时钟230void SystemClock_Config(void) {231 RCC_OscInitTypeDef RCC_OscInitStruct = {0};232 RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};233 234 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;235 RCC_OscInitStruct.HSEState = RCC_HSE_ON;236 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;237 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;238 RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8×9=72MHz239 HAL_RCC_OscConfig(&RCC_OscInitStruct);240 241 // F4: 8MHz → PLL_M=8 → PLL_N=336 → PLL_P=2 → 168MHz242 // F7: 8MHz → PLL_M=8 → PLL_N=432 → PLL_P=2 → 216MHz243}</code></pre>244 245<div class="trouble">🔍 <b>常见错误:时钟配置不对,程序跑飞或外设不工作</b><br>246用CubeMX生成时钟配置后,务必检查:①HSE_VALUE宏要等于你的晶振频率(常见8MHz/25MHz)②APB1最大36MHz(F103)/42MHz(F4),APB2最大72MHz(F103)/84MHz(F4) ③USB要用48MHz,需要精确的PLL配置</div>247 248<h2>🎯 实战练习</h2>249 250<div class="tip">📝 <b>练习1:</b>用CubeMX配置STM32F103C8T6工程,实现USART1+DMA发送 + USART2+DMA接收,组成一个数据转发器。<br><br>251📝 <b>练习2:</b>实现TIM2产生1KHz PWM(占空比50%),用TIM3输入捕获测量这个PWM的频率和占空比。<br><br>252📝 <b>练习3:</b>用ADC+DMA连续采样4个通道,配合TIM触发实现1KHz固定采样率,通过USART上传到串口绘图工具。</div>253 254<h2>📋 知识点回顾</h2>255<div class="summary">256<b>✅ STM32开发核心:</b><br>257• HAL_OK检查:每个HAL函数必须检查返回值<br>258• ISR中不用HAL_Delay:死循环的根因,用非阻塞轮询<br>259• DMA三方向:P→M(ADC采样)、M→P(UART发送)、M→M(快速memcpy)<br>260• 时钟树:HSE→PLL→AHB→APB1/APB2→外设<br>261• GPIO速度:低速设备不要设高速,省电减EMI<br>262<b>🛠 CubeMX技巧:</b><br>263• 生成代码前检查Pinout→列出所有引脚冲突<br>264• Clock Configuration→检查48MHz for USB<br>265• Project Manager→Code Generator→勾选"Generate peripheral initialization as a pair of .c/.h"266</div>267""",268 269 "guide-ch07.html": r"""270<h2>📶 WiFi开发实战</h2>271 272<p>ESP32的核心优势是双模WiFi+蓝牙。这一节从零开始掌握WiFi编程。</p>273 274<h3>WiFi Station模式(连接路由器)</h3>275 276<pre><code>#include "esp_wifi.h"277#include "esp_event.h"278#include "nvs_flash.h"279 280#define WIFI_SSID "YourWiFi"281#define WIFI_PASS "YourPassword"282 283static void wifi_event_handler(void* arg, esp_event_base_t base,284 int32_t id, void* data) {285 if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START)286 esp_wifi_connect();287 else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {288 ip_event_got_ip_t* ev = (ip_event_got_ip_t*)data;289 printf("Got IP: " IPSTR "\n", IP2STR(&ev->ip_info.ip));290 }291}292 293void wifi_init_sta(void) {294 nvs_flash_init();295 esp_netif_init();296 esp_event_loop_create_default();297 esp_netif_create_default_wifi_sta();298 299 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();300 esp_wifi_init(&cfg);301 302 esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID,303 &wifi_event_handler, NULL);304 esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP,305 &wifi_event_handler, NULL);306 307 wifi_config_t wifi_cfg = {308 .sta = {.ssim = WIFI_SSID, .password = WIFI_PASS}309 };310 esp_wifi_set_mode(WIFI_MODE_STA);311 esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg);312 esp_wifi_start();313}</code></pre>314 315<h3>HTTP客户端(GET请求)</h3>316 317<pre><code>#include "esp_http_client.h"318 319void http_get_example(void) {320 esp_http_client_config_t cfg = {321 .url = "http://api.openweathermap.org/data/2.5/weather?q=Beijing",322 .method = HTTP_METHOD_GET,323 .timeout_ms = 5000,324 };325 esp_http_client_handle_t client = esp_http_client_init(&cfg);326 esp_err_t err = esp_http_client_perform(client);327 328 if (err == ESP_OK) {329 int status = esp_http_client_get_status_code(client);330 int len = esp_http_client_get_content_length(client);331 printf("HTTP %d, Response: %d bytes\n", status, len);332 // 读取响应体333 char buf[512];334 int read = esp_http_client_read(client, buf, sizeof(buf)-1);335 buf[read] = 0;336 printf("%s\n", buf);337 }338 esp_http_client_cleanup(client);339}</code></pre>340 341<h2>🔷 蓝牙BLE实战</h2>342 343<div class="card">344<div class="h"><span class="name">BLE核心概念</span><span class="tag g">必须理解</span></div>345<div class="b">346<div class="desc">BLE不是传统蓝牙!理解这些概念是入门关键:</div>347<table>348<tr><th>概念</th><th>说明</th><th>类比</th></tr>349<tr><td>GAP</td><td>设备发现与连接</td><td>握手和交换名片</td></tr>350<tr><td>GATT</td><td>数据传输协议</td><td>谈话的内容和格式</td></tr>351<tr><td>Service</td><td>功能集合(UUID标识)</td><td>一个"部门"</td></tr>352<tr><td>Characteristic</td><td>具体数据点</td><td>部门里的"办公桌"</td></tr>353<tr><td>Descriptor</td><td>Characteristic的元数据</td><td>桌上的"标签说明"</td></tr>354</table>355</div></div>356 357<pre><code>// 创建自定义BLE服务:温度传感器358// Service UUID: 0000180a-0000-1000-8000-00805f9b34fb359// Characteristic UUID: 00002a6e-0000-1000-8000-00805f9b34fb360 361static const uint16_t GATTS_SERVICE_UUID = 0x00FF;362static const uint16_t GATTS_CHAR_UUID = 0xFF01;363 364// BLE广播数据设置365static void gap_event_handler(esp_gap_ble_cb_event_t event,366 esp_ble_gap_cb_param_t* param) {367 switch (event) {368 case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:369 esp_ble_gap_start_advertising(&adv_params);370 break;371 }372}373 374void ble_server_init(void) {375 esp_ble_gatts_app_register(PROFILE_APP_ID);376 // 配置GATT数据库:添加Service + Characteristic377 // 设置Characteristic值:NOTIFY + READ属性378 // 开始广播,手机APP扫描即可发现379}</code></pre>380 381<h2>🔄 OTA远程升级</h2>382 383<div class="warn">⚠ <b>OTA是ESP32最具商业价值的特性。</b>有了它,部署后的设备可以远程更新固件,无需物理接触。</div>384 385<pre><code>// OTA升级流程386// 1. 下载新固件到OTA分区387// 2. 验证固件完整性(checksum/签名)388// 3. 设置启动分区,重启389 390#include "esp_ota_ops.h"391#include "esp_https_ota.h"392 393void do_ota_update(const char* firmware_url) {394 esp_http_client_config_t cfg = {395 .url = firmware_url,396 .cert_pem = server_cert_pem_start, // HTTPS证书397 };398 esp_https_ota_config_t ota_cfg = {399 .http_config = &cfg,400 };401 esp_err_t ret = esp_https_ota(&ota_cfg);402 if (ret == ESP_OK) {403 printf("OTA成功,准备重启...\n");404 esp_restart(); // 重启进入新固件405 }406}407 408// 关键配置:分区表必须有ota_0和ota_1两个app分区409// partitions.csv:410// # Name, Type, SubType, Offset, Size411// nvs, data, nvs, 0x9000, 0x6000412// otadata, data, ota, 0xf000, 0x2000413// phy_init, data, phy, 0x11000, 0x1000414// factory, app, factory, 0x12000, 0x1E0000415// ota_0, app, ota_0, 0x20000, 0x1E0000 # 运行区416// ota_1, app, ota_1, , 0x1E0000 # 下载区</code></pre>417 418<h2>🎯 实战练习</h2>419 420<div class="tip">📝 <b>练习1:</b>实现WiFi SmartConfig配网功能(按一个按钮自动连接WiFi,手机APP配网)。<br><br>421📝 <b>练习2:</b>创建一个BLE温度计服务:ESP32读取DHT11 → BLE广播温度值 → 手机nRF Connect APP接收显示。<br><br>422📝 <b>练习3:</b>搭建简易OTA服务器(可以用Python的http.server),实现ESP32从服务器下载并更新固件。</div>423 424<h2>📋 知识点回顾</h2>425<div class="summary">426<b>✅ ESP32核心能力:</b><br>427• WiFi双模:Station(连路由) + SoftAP(自建热点)<br>428• BLE:GAP(广播/扫描/连接) + GATT(Service/Characteristic/Descriptor)<br>429• OTA:远程固件升级,双分区(ota_0/ota_1)保证升级安全<br>430• ESP-IDF vs Arduino:专业项目用IDF(性能好/功能全),快速原型用Arduino<br>431<b>🛠 常用资源:</b><br>432• ESP-IDF Programming Guide (docs.espressif.com)<br>433• ESP RainMaker:官方IoT云平台<br>434• ESP-MDF/Matter:智能家居协议支持<br>435• nRF Connect App:BLE调试必备工具436</div>437""",438 439 "guide-ch08.html": r"""440<h2>📊 队列(Queue)详解</h2>441 442<p>队列是FreeRTOS任务间通信的主力工具。它是一个FIFO缓冲区,一个任务往里放,另一个从里取,天然线程安全。</p>443 444<pre><code>// 创建队列:存放10个float值445QueueHandle_t sensor_queue;446sensor_queue = xQueueCreate(10, sizeof(float));447 448// 任务A:发送数据(生产者)449void vSensorTask(void* pv) {450 float temp;451 for (;;) {452 temp = read_temperature();453 // 发送到队列,等最多100ms454 if (xQueueSend(sensor_queue, &temp, pdMS_TO_TICKS(100)) != pdTRUE) {455 // 队列满了!消费者太慢456 }457 vTaskDelay(pdMS_TO_TICKS(1000)); // 1秒采样一次458 }459}460 461// 任务B:接收数据(消费者)462void vDisplayTask(void* pv) {463 float temp;464 for (;;) {465 // 阻塞等待数据(无限等)466 if (xQueueReceive(sensor_queue, &temp, portMAX_DELAY) == pdTRUE) {467 printf("温度: %.1f°C\n", temp);468 }469 }470}</code></pre>471 472<h3>队列API变体</h3>473 474<table>475<tr><th>API</th><th>行为</th><th>适用场景</th></tr>476<tr><td><code>xQueueSend</code></td><td>队列满则等待</td><td>一般发送</td></tr>477<tr><td><code>xQueueSendToBack</code></td><td>发送到队尾(同xQueueSend)</td><td>FIFO发送</td></tr>478<tr><td><code>xQueueSendToFront</code></td><td>发送到队首(插队)</td><td>紧急消息</td></tr>479<tr><td><code>xQueueOverwrite</code></td><td>队列满则覆盖最旧数据</td><td>只关心最新值(如传感器读数)</td></tr>480<tr><td><code>xQueuePeek</code></td><td>读取但不移除</td><td>预览下一个数据</td></tr>481<tr><td><code>xQueueReceive</code></td><td>读取并移除</td><td>一般接收</td></tr>482</table>483 484<h2>🔐 信号量(Semaphore)详解</h2>485 486<div class="analogy">💡 <b>类比:</b>二值信号量像<b>一个令牌</b>——拿到令牌的才能进入临界区,其他人排队。计数信号量像<b>停车场的车位</b>——有几个空位就能进几辆车。</div>487 488<h3>二值信号量:互斥访问</h3>489 490<pre><code">SemaphoreHandle_t i2c_mutex;491 492// 初始化493i2c_mutex = xSemaphoreCreateBinary();494xSemaphoreGive(i2c_mutex); // 初始化为"可用"495 496// 任务A:使用I2C497void vTaskA(void* pv) {498 for (;;) {499 // 获取I2C使用权(等最多50ms)500 if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {501 read_sensor_i2c(); // 使用I2C502 xSemaphoreGive(i2c_mutex); // 释放503 }504 }505}506 507// 任务B:也用I2C(不会冲突!)508void vTaskB(void* pv) {509 for (;;) {510 if (xSemaphoreTake(i2c_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {511 write_display_i2c(); // 使用I2C512 xSemaphoreGive(i2c_mutex); // 释放513 }514 }515}</code></pre>516 517<h3>信号量 vs 互斥量(Mutex)</h3>518 519<div class="warn">⚠ <b>关键区别:</b>用互斥量(Mutex)而不是二值信号量做资源互斥!Mutex有<b>优先级继承</b>机制(防止优先级反转),二值信号量没有。</div>520 521<table>522<tr><th>特性</th><th>二值信号量</th><th>互斥量(Mutex)</th></tr>523<tr><td>用途</td><td>任务同步(通知信号)</td><td>资源互斥(保护临界区)</td></tr>524<tr><td>优先级继承</td><td>❌ 无</td><td>✅ 有(自动提升持有者优先级)</td></tr>525<tr><td>递归锁</td><td>❌ 不支持</td><td>✅ 支持(xSemaphoreCreateRecursiveMutex)</td></tr>526<tr><td>ISR中使用</td><td>✅ 可以</td><td>❌ 不能(只能任务间使用)</td></tr>527<tr><td>典型场景</td><td>ISR通知任务"数据就绪"</td><td>保护I2C/SPI/串口等共享外设</td></tr>528</table>529 530<h2>⏱ 软件定时器</h2>531 532<pre><code>// 不需要单独任务!FreeRTOS Daemon任务帮你执行回调533TimerHandle_t led_timer;534 535void led_timer_callback(TimerHandle_t xTimer) {536 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 每秒翻转LED537}538 539void create_timers(void) {540 led_timer = xTimerCreate(541 "LED_Blink", // 名称542 pdMS_TO_TICKS(1000), // 周期1000ms543 pdTRUE, // 自动重载(周期性)544 (void*)0, // 定时器ID545 led_timer_callback // 回调函数546 );547 xTimerStart(led_timer, 0); // 启动548}549 550// 一次性定时器:超时后自动删除551TimerHandle_t oneshot = xTimerCreate("Oneshot",552 pdMS_TO_TICKS(5000), pdFALSE, (void*)0, callback);553xTimerStart(oneshot, 0);554// 5秒后callback执行一次,定时器自毁</code></pre>555 556<div class="card">557<div class="h"><span class="name">优先级反转问题</span><span class="tag r">进阶</span></div>558<div class="b">559<div class="desc">560<b>场景:</b>低优先级任务A持有锁 → 高优先级任务C也想拿锁 → C被阻塞 → 中优先级任务B抢占A(A放不了锁) → C无限等待!<br><br>561<b>解决:</b>用<b>Mutex</b>代替二值信号量,Mutex会自动将持有者A的优先级临时提升到C的级别(优先级继承),让A快速完成释放锁。<br><br>562<b>CMSIS RTOS2 API:</b><br>563<pre><code>// CMSIS RTOS2 (STM32Cube默认)564osMutexId_t i2c_mutex = osMutexNew(NULL);565osMutexAcquire(i2c_mutex, osWaitForever);566osMutexRelease(i2c_mutex);</code></pre>567</div></div></div>568 569<h2>🎯 实战练习</h2>570 571<div class="tip">📝 <b>练习1:</b>创建3个任务:①LED闪烁(100ms) ②按键扫描(20ms轮询) ③OLED显示(1s刷新)。用队列传递按键事件给显示任务。<br><br>572📝 <b>练习2:</b>用互斥量保护I2C总线,让两个任务(温湿度传感器读取 + OLED显示)安全共享I2C外设。<br><br>573📝 <b>练习3:</b>实现一个软件定时器+计数信号量:定时器每100ms Give一次信号量,任务Take到后累加计数器并显示。</div>574 575<h2>📋 知识点回顾</h2>576<div class="summary">577<b>✅ FreeRTOS核心IPC:</b><br>578• 队列:任务间数据传递,FIFO,线程安全<br>579• 二值信号量:任务/ISR同步(通知信号)<br>580• 互斥量(Mutex):资源互斥,有优先级继承<br>581• 计数信号量:管理多个同类资源<br>582• 软件定时器:不需要单独任务,Daemon任务执行回调<br>583<b>⚠ 关键原则:</b><br>584• 资源互斥用Mutex,不要用二值信号量<br>585• ISR中只能Give信号量,不能Take<br>586• 永远不要在高优先级ISR里调用阻塞API<br>587• 队列满时不丢数据用xQueueSend,只关心最新值用xQueueOverwrite588</div>589""",590 591 "guide-ch11.html": r"""592<h2>🔋 STM32低功耗模式详解</h2>593 594<p>电池供电设备的核心竞争力就是功耗。STM32提供多层睡眠模式,选对模式能省90%以上的电。</p>595 596<h3>三种睡眠模式对比</h3>597 598<table>599<tr><th>模式</th><th>唤醒方式</th><th>电流(F103)</th><th>唤醒时间</th><th>唤醒后</th></tr>600<tr><td>Sleep</td><td>任意中断/事件</td><td>~5mA</td><td>即时</td><td>继续执行下一条</td></tr>601<tr><td>Stop</td><td>EXTI线 / RTC唤醒</td><td>~20μA</td><td>~10μs</td><td>从WFI处继续</td></tr>602<tr><td>Standby</td><td>WKUP引脚 / RTC / NRST</td><td>~2μA</td><td>~50μs</td><td>相当于复位</td></tr>603</table>604 605<pre><code>// Sleep模式:最简单,CPU停下来但外设继续运行606__WFI(); // Wait For Interrupt607// 任意中断唤醒后从这里继续执行608 609// Stop模式:最常用,大部分时钟停了610void enter_stop_mode(void) {611 // 1. 关闭不必要的外设时钟612 __HAL_RCC_GPIOA_CLK_DISABLE();613 __HAL_RCC_USART1_CLK_DISABLE();614 // 2. 将所有IO设为模拟输入(最低功耗)615 // 3. 进入Stop模式616 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);617 // 唤醒后需要重新配置时钟618 SystemClock_Config();619}620 621// Standby模式:电流最低,但唤醒=复位622void enter_standby_mode(void) {623 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // PA0上升沿唤醒624 __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);625 HAL_PWR_EnterSTANDBYMode();626 // 永远不会到这里——唤醒后从头执行!627}</code></pre>628 629<h3>低功耗设计黄金法则</h3>630 631<div class="card">632<div class="h"><span class="name">功耗优化检查清单</span><span class="tag y">进阶</span></div>633<div class="b">634<div class="desc">635✅ <b>外设:</b>不用就关时钟,不是"不用就放那"<br>636✅ <b>GPIO:</b>悬空引脚设为模拟输入(AN模式),避免浮空耗电<br>637✅ <b>时钟:</b>降频!72MHz → 8MHz 电流降80%<br>638✅ <b>看门狗:</b>独立看门狗(IWDG)在Stop模式下仍运行,功耗~1μA<br>639✅ <b>LED:</b>×100μA,不用就关<br>640✅ <b>ADC/DAC:</b>采样完就关,它们是耗电大户<br>641✅ <b>DMA:</b>用DMA做数据搬运,CPU多睡觉<br>642✅ <b>通信:</b>打包数据批量发送,减少无线电/收发器工作时间643</div></div></div>644 645<h2>⏲ RTC闹钟唤醒</h2>646 647<pre><code>// 每5分钟自动唤醒采集一次温湿度,其余时间STOP模式648// 平均功耗 < 50μA!649 650void setup_rtc_wakeup(void) {651 // 配置RTC闹钟:5分钟后唤醒652 RTC_AlarmTypeDef sAlarm = {0};653 sAlarm.Alarm = RTC_ALARM_A;654 sAlarm.AlarmTime.Hours = 0;655 sAlarm.AlarmTime.Minutes = 5;656 sAlarm.AlarmTime.Seconds = 0;657 HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);658}659 660void main_loop(void) {661 while (1) {662 // 1. 采集数据(10ms)663 float temp = read_temperature();664 float hum = read_humidity();665 666 // 2. 发送数据(50ms)667 send_data_over_ble(temp, hum);668 669 // 3. 设置下次唤醒闹钟670 setup_rtc_wakeup();671 672 // 4. 深度睡眠 → 功耗降到20μA!673 enter_stop_mode();674 // ... 5分钟后RTC闹钟唤醒,回到步骤1675 }676}</code></pre>677 678<h2>📊 功耗测量方法</h2>679 680<div class="tip">💡 <b>实用技巧:</b>用STM32L系列(L0/L4/L5)替代F系列,Stop模式功耗从20μA降到<1μA。L系列专为低功耗设计。</div>681 682<pre><code>// 测量实际功耗的简单方法683// 1. 断开开发板上的VDD跳线684// 2. 万用表串联在VDD路径上(mA/μA档)685// 3. 分别测量:运行态 → Sleep → Stop → Standby686 687// 或者用专业工具:688// - Nordic Power Profiler Kit II (PPK2): ~$100, 精度1μA689// - Joulescope JS220: ~$800, 精度nA级690// - STM32CubeMonitor-Power: 免费,但只能用在ST-LINK V3 + NUCLEO板</code></pre>691 692<h2>🎯 实战练习</h2>693 694<div class="tip">📝 <b>练习1:</b>用RTC闹钟实现每10秒唤醒一次、翻转LED、再进入Sleep模式。用电表测量平均电流。<br><br>695📝 <b>练习2:</b>实现一个BLE温湿度信标:每30秒从Stop模式唤醒→读DHT11→BLE广播→回到Stop模式。目标平均功耗<100μA。<br><br>696📝 <b>练习3:</b>对比72MHz vs 8MHz运行时的功耗差异(HAL_Delay相同时间闪烁LED,测量电流)。</div>697 698<h2>📋 知识点回顾</h2>699<div class="summary">700<b>✅ 低功耗核心策略:</b><br>701• Sleep: ~5mA, 任意中断唤醒,外设继续运行<br>702• Stop: ~20μA, EXTI/RTC唤醒,需重新配时钟<br>703• Standby: ~2μA, 仅WKUP/RTC/NRST唤醒,等于复位<br>704• 外设不用就关时钟 + GPIO悬空设AN模式<br>705• 降频是最直接的省电方式<br>706• RTC闹钟是最常用的低功耗唤醒源<br>707<b>🛠 STM32L系列额外特性:</b><br>708• BAM(Batch Acquisition Mode): DMA采集时CPU休眠<br>709• LPUART: 低功耗串口,Stop模式下接收数据<br>710• LPTIM: 低功耗定时器,Stop模式下运行711</div>712""",713 714 "guide-ch12.html": r"""715<h2>📐 传感器通信协议选型</h2>716 717<div class="card">718<div class="h"><span class="name">嵌入式三大传感器接口</span><span class="tag g">基础</span></div>719<div class="b">720<div class="desc">721<table>722<tr><th>接口</th><th>线数</th><th>速度</th><th>距离</th><th>典型传感器</th></tr>723<tr><td>I2C</td><td>2线(SCL/SDA)</td><td>100k/400k/1MHz</td><td><1m</td><td>BMP280, MPU6050, OLED</td></tr>724<tr><td>SPI</td><td>4线(SCK/MOSI/MISO/CS)</td><td>1-80MHz</td><td><0.5m</td><td>BME680, ICM-20948, SD卡</td></tr>725<tr><td>UART</td><td>2线(TX/RX)</td><td>9600-1Mbps</td><td><15m</td><td>GPS模块, 指纹模块, PM2.5</td></tr>726</table>727</div></div></div>728 729<h3>I2C总线:共享与寻址</h3>730 731<pre><code>// I2C精髓:一个总线挂多个设备,7位地址区分732// 同一个SCL/SDA上挂6个传感器!733 734// 扫描I2C总线所有设备735void i2c_scan(void) {736 printf("I2C 扫描开始...\n");737 for (uint8_t addr = 1; addr < 127; addr++) {738 if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) {739 printf("发现设备: 0x%02X\n", addr);740 }741 }742}743// 常见I2C地址:744// BMP280: 0x76/0x77, MPU6050: 0x68, OLED SSD1306: 0x3C745// BH1750: 0x23, SHT30: 0x44, VL53L0X: 0x29</code></pre>746 747<h2>🎯 传感器融合:MPU6050姿态解算</h2>748 749<p>单个传感器有局限——加速度计有重力干扰,陀螺仪有漂移。融合两者才能得到稳定姿态。</p>750 751<h3>互补滤波器(简单但有效)</h3>752 753<pre><code>// 互补滤波:陀螺仪积分(短期准) + 加速度计(长期准)754// angle = α*(angle + gyro*dt) + (1-α)*accel_angle755// α = 0.98 (陀螺仪权重)756 757#define ALPHA 0.98f758float pitch = 0, roll = 0;759float dt = 0.01; // 10ms采样间隔760 761void complementary_filter(float ax, float ay, float az,762 float gx, float gy, float gz) {763 // 加速度计计算角度(静态准,但动态不准)764 float acc_pitch = atan2(ay, sqrt(ax*ax + az*az)) * 180/M_PI;765 float acc_roll = atan2(-ax, az) * 180/M_PI;766 767 // 互补滤波融合768 pitch = ALPHA * (pitch + gx * dt) + (1-ALPHA) * acc_pitch;769 roll = ALPHA * (roll + gy * dt) + (1-ALPHA) * acc_roll;770}</code></pre>771 772<div class="card">773<div class="h"><span class="name">Mahony/Madgwick AHRS算法</span><span class="tag p">进阶</span></div>774<div class="b">775<div class="desc">776互补滤波适合玩具级应用,无人机/机器人推荐用<b>Mahony</b>或<b>Madgwick</b>算法。两者都是成熟的AHRS(姿态和航向参考系统)算法:777• <b>Mahony:</b> 基于PI控制器,计算量小,适合F103<br>778• <b>Madgwick:</b> 基于梯度下降,收敛更快,需要更多RAM<br>779• 两者代码都已开源(C语言),直接移植即可<br>780• 需要9轴传感器(加速度+陀螺+磁力计)才能得到真实的Yaw角781</div></div></div>782 783<h3>卡尔曼滤波器(数学上最优)</h3>784 785<pre><code>// 一维卡尔曼滤波:传感器数据平滑786// 预测:x = x, P = P + Q787// 更新:K = P/(P+R), x = x + K*(z-x), P = (1-K)*P788 789typedef struct {790 float x; // 状态估计值791 float P; // 估计误差协方差792 float Q; // 过程噪声(调大→更信任模型)793 float R; // 测量噪声(调小→更信任传感器)794} Kalman1D;795 796float kalman_update(Kalman1D* k, float measurement) {797 // 预测798 k->P = k->P + k->Q;799 // 更新800 float K = k->P / (k->P + k->R);801 k->x = k->x + K * (measurement - k->x);802 k->P = (1 - K) * k->P;803 return k->x;804}805 806// 使用示例807Kalman1D temp_kf = {.x=25.0, .P=1.0, .Q=0.01, .R=1.0};808float smooth_temp = kalman_update(&temp_kf, raw_adc_temp);809// Q小→温度变化平滑但滞后 | R小→快速反应但噪声大</code></pre>810 811<h2>⚡ 传感器驱动实战:BMP280</h2>812 813<pre><code>// BMP280 温度+气压传感器 I2C驱动814#define BMP280_ADDR 0x76 // SDO接地时地址为0x76815#define BMP280_REG_ID 0xD0816 817// 校准参数(BMP280出厂预存)818typedef struct {819 uint16_t dig_T1; int16_t dig_T2, dig_T3;820 uint16_t dig_P1; int16_t dig_P2, dig_P3, dig_P4, dig_P5, dig_P6, dig_P7, dig_P8, dig_P9;821} BMP280_Calib;822 823BMP280_Calib calib;824 825uint8_t bmp280_init(void) {826 uint8_t chip_id;827 HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR<<1, BMP280_REG_ID, 1, &chip_id, 1, 100);828 if (chip_id != 0x58) return 0; // 不是BMP280!829 830 // 读取校准参数(共24字节)831 uint8_t buf[24];832 HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR<<1, 0x88, 1, buf, 24, 100);833 // 解析校准参数...(省略解析代码)834 835 // 配置:强制模式,16x过采样836 HAL_I2C_Mem_Write(&hi2c1, BMP280_ADDR<<1, 0xF4, 1, (uint8_t[]){0x57}, 1, 100);837 return 1;838}839 840float bmp280_read_temperature(void) {841 uint8_t buf[3];842 HAL_I2C_Mem_Read(&hi2c1, BMP280_ADDR<<1, 0xFA, 1, buf, 3, 100);843 int32_t adc_T = ((uint32_t)buf[0]<<12) | ((uint32_t)buf[1]<<4) | (buf[2]>>4);844 // 补偿计算(BME280 datasheet公式)845 int32_t var1, var2, T;846 var1 = ((((adc_T>>3) - ((int32_t)calib.dig_T1<<1))) * ((int32_t)calib.dig_T2)) >> 11;847 var2 = (((((adc_T>>4) - ((int32_t)calib.dig_T1)) * ((adc_T>>4) - ((int32_t)calib.dig_T1))) >> 12) * ((int32_t)calib.dig_T3)) >> 14;848 T = (var1 + var2) / 5120.0f;849 return T;850}</code></pre>851 852<h2>🎯 实战练习</h2>853 854<div class="tip">📝 <b>练习1:</b>用MPU6050实现互补滤波姿态角计算,通过UART以100Hz发送pitch/roll到串口绘图工具实时显示。<br><br>855📝 <b>练习2:</b>用BMP280做简易高度计:气压→海拔换算(公式:h = 44330*(1-(P/P0)^(1/5.255))),比较室内和室外的高度差。<br><br>856📝 <b>练习3:</b>实现一维卡尔曼滤波,平滑DHT11的温度读数,对比原始值和滤波后值。</div>857 858<h2>📋 知识点回顾</h2>859<div class="summary">860<b>✅ 传感器开发核心:</b><br>861• I2C共享总线:最多128个设备,靠7位地址区分<br>862• 传感器融合:互补滤波(简单)/Mahony(中等)/Madgwick(精度高)<br>863• 卡尔曼滤波:数学最优估计,调Q和R参数是关键<br>864• 校准参数:传感器出厂预存的修正系数,必须读取并使用<br>865<b>🛠 调试技巧:</b><br>866• 先用Saleae逻辑分析仪/示波器看I2C波形<br>867• 检查ACK/NACK:NACK=设备地址错了或没上电<br>868• 串口以固定频率输出数据,用SerialPlot/Arduino IDE绘图看波形<br>869• 传感器读数突然跳变→检查电源去耦电容(100nF+10μF)870</div>871""",872 873 "guide-ch13.html": r"""874<h2>🏗 嵌入式项目架构设计</h2>875 876<p>从demo到产品,最大的区别不是代码量,而是<b>架构</b>。一个好的架构让代码可维护、可测试、可扩展。</p>877 878<h3>分层架构模型</h3>879 880<pre><code>┌─────────────────────────────────────┐881│ 应用层 (Application Layer) │882│ 业务逻辑、状态机、算法 │883├─────────────────────────────────────┤884│ 中间件层 (Middleware) │885│ FreeRTOS, LVGL, FATFS, LwIP, MQTT │886├─────────────────────────────────────┤887│ 硬件抽象层 (HAL / BSP) │888│ 驱动程序、外设封装 │889├─────────────────────────────────────┤890│ 硬件层 (Hardware) │891│ MCU, 传感器, 执行器, 电源 │892└─────────────────────────────────────┘893 894// 黄金法则:上层不直接访问寄存器895// 应用层调用 bsp_led_on(),不是 HAL_GPIO_WritePin()</code></pre>896 897<h3>模块化设计模式</h3>898 899<pre><code>// 每个模块= .h(接口) + .c(实现)900// 示例:LED模块901 902// led.h —— 只暴露用户需要的接口903#ifndef LED_H904#define LED_H905 906#include <stdint.h>907 908// 错误码909typedef enum {910 LED_OK = 0,911 LED_ERR_INVALID,912 LED_ERR_HW913} led_status_t;914 915// 公开API916led_status_t led_init(uint8_t pin);917void led_on(uint8_t pin);918void led_off(uint8_t pin);919void led_toggle(uint8_t pin);920 921#endif922// led.c —— 实现细节隐藏起来923// 用户不需要知道用了哪个GPIO口和哪个定时器</code></pre>924 925<h2>🧵 状态机设计</h2>926 927<div class="analogy">💡 <b>类比:</b>状态机就像<b>洗衣机</b>——每个阶段(注水→洗涤→排水→脱水)是一个状态,只有在特定的触发条件下才会跳转到下一个状态。用状态机管理复杂的系统行为,代码清晰100倍。</div>928 929<pre><code>// 智能花盆控制器:有限状态机930typedef enum {931 STATE_IDLE, // 空闲932 STATE_MEASURING, // 正在测量土壤湿度933 STATE_WATERING, // 浇水中934 STATE_LOW_POWER, // 低功耗待机(夜间)935 STATE_ERROR // 错误状态936} system_state_t;937 938system_state_t state = STATE_IDLE;939 940void state_machine_run(void) {941 switch (state) {942 case STATE_IDLE:943 if (is_time_to_measure()) {944 state = STATE_MEASURING;945 } else if (is_night_time()) {946 state = STATE_LOW_POWER;947 }948 break;949 950 case STATE_MEASURING:951 moisture = read_soil_moisture();952 if (moisture < THRESHOLD_DRY) {953 state = STATE_WATERING;954 } else {955 state = STATE_IDLE; // 湿度够,回空闲956 }957 break;958 959 case STATE_WATERING:960 pump_on();961 if (pump_timer_expired() || moisture >= THRESHOLD_WET) {962 pump_off();963 state = STATE_IDLE;964 } else if (pump_fault()) {965 state = STATE_ERROR;966 }967 break;968 969 case STATE_LOW_POWER:970 if (is_day_time() || is_time_to_measure()) {971 wake_from_low_power();972 state = STATE_IDLE;973 }974 break;975 976 case STATE_ERROR:977 led_error_blink();978 if (error_cleared()) state = STATE_IDLE;979 break;980 }981}</code></pre>982 983<h2>📊 RTOS任务优先级设计</h2>984 985<div class="warn">⚠ <b>优先级规划是RTOS项目成功的关键。</b>优先级设置不当会导致:高优先级饿死低优先级 / 优先级反转 / 实时性不满足。</div>986 987<pre><code>// 任务优先级分配原则(数字越大优先级越高)988// ┌──────┬──────┬─────────────────────────┐989// │ 优先级 │ 任务 │ 说明 │990// ├──────┼──────┼─────────────────────────┤991// │ 3 │ 控制 │ 电机控制、PID(硬实时) │992// │ 2 │ 通信 │ WiFi/BLE协议栈 │993// │ 2 │ 采集 │ ADC采样 + DMA(1KHz) │994// │ 1 │ 显示 │ OLED/LCD刷新(10-30Hz) │995// │ 1 │ 存储 │ SD卡/FATFS写入 │996// │ 0 │ 监控 │ 心跳LED、看门狗喂狗 │997// └──────┴──────┴─────────────────────────┘998 999// 关键规则:1000// 1. 硬实时任务给最高优先级1001// 2. 计算密集型任务给低优先级1002// 3. "运行→阻塞→运行"的任务给高优先级1003// 4. 永不阻塞的任务给最低优先级(如空闲任务)</code></pre>1004 1005<h2>🔬 调试与日志系统</h2>1006 1007<p>嵌入式没有printf控制台?自己建一个!</p>1008 1009<pre><code>// 分级日志系统1010typedef enum {1011 LOG_DEBUG, // 调试信息1012 LOG_INFO, // 一般信息1013 LOG_WARN, // 警告1014 LOG_ERROR // 错误1015} log_level_t;1016 1017#define LOG_LEVEL LOG_DEBUG // 编译时设置日志级别1018 1019#define LOG(level, fmt, ...) do { \1020 if (level >= LOG_LEVEL) { \1021 printf("[%s] %s:%d " fmt "\r\n", \1022 #level, __FILE__, __LINE__, ##__VA_ARGS__); \1023 } \1024} while(0)1025 1026// 使用1027LOG(LOG_INFO, "系统启动,版本 %s", VERSION);1028LOG(LOG_DEBUG, "ADC原始值: %d", adc_val);1029LOG(LOG_WARN, "WiFi信号弱: %d dBm", rssi);1030LOG(LOG_ERROR, "传感器初始化失败");1031 1032// 发布版本改 LOG_LEVEL = LOG_WARN,调试信息全部移除!</code></pre>1033 1034<div class="card">1035<div class="h"><span class="name">调试工具链</span><span class="tag y">必备</span></div>1036<div class="b">1037<div class="desc">1038<b>硬件调试:</b><br>1039• J-Link / ST-Link: 断点、单步、变量查看<br>1040• SEGGER RTT: 高速printf,比SWO快10倍<br>1041• 逻辑分析仪(Saleae): I2C/SPI/UART通信抓包<br>1042• 示波器: PWM波形、电源噪声、时序<br><br>1043<b>软件工具:</b><br>1044• assert()宏: 开发期捕获非法状态<br>1045• HardFault分析器: 从栈帧还原崩溃现场<br>1046• Watchdog: 最后一个安全网<br>1047• 内存保护单元(MPU): 防野指针破坏1048</div></div></div>1049 1050<h2>🎯 实战练习</h2>1051 1052<div class="tip">📝 <b>完整项目:智能家居环境监测仪</b><br>1053组合所有学过的知识做一个完整项目:<br>1054<b>硬件:</b>STM32 + DHT22(温湿度) + BMP280(气压) + OLED + ESP8266(WiFi)<br>1055<b>软件架构:</b>①传感器采集任务(1s/次) ②MQTT上报任务(WiFi) ③OLED显示任务(500ms/次) ④LED指示任务<br>1056<b>进阶功能:</b>①OTA远程升级 ②RTC定时唤醒(低功耗) ③JSON数据格式 ④断网本地缓存<br><br>1057📝 <b>练习2:</b>使用J-Link RTT代替UART printf,体验不占用串口的高速日志输出。<br><br>1058📝 <b>练习3:</b>故意制造一个HardFault(如访问空指针),用调试器分析栈帧定位崩溃位置。</div>1059 1060<h2>📋 终极知识点回顾</h2>1061<div class="summary">1062<b>✅ 项目架构核心:</b><br>1063• 分层设计:应用→中间件→HAL→硬件<br>1064• 模块化:.h公开接口 + .c隐藏实现<br>1065• 状态机:switch-case管理复杂流程<br>1066• 任务优先级:硬实时高→计算低,永不阻塞给最低<br>1067<b>🛠 调试武器库:</b><br>1068• 分级日志(DEBUG/INFO/WARN/ERROR) + 编译时开关<br>1069• J-Link RTT + 逻辑分析仪 + 示波器<br>1070• HardFault分析 + Watchdog + MPU1071<b>📚 学习路线下一步:</b><br>1072• 继续深入:Linux嵌入式、Zephyr RTOS、Matter协议<br>1073• 硬件进阶:4层PCB设计、EMC测试、量产DFM<br>1074• 软技能:Git协作、需求分析、技术文档撰写1075</div>1076""",1077}1078 1079def expand_chapter(filepath, expansion):1080 with open(filepath, 'r', encoding='utf-8') as f:1081 content = f.read()1082 1083 # Find the last <h2> or first chnav (insert before navigation)1084 # Insert before the first chnav div1085 insert_marker = '<div class="chnav">'1086 idx = content.rfind(insert_marker)1087 if idx < 0:1088 idx = content.rfind('</body>')1089 1090 # Insert expansion before the marker1091 new_content = content[:idx] + expansion + "\n" + content[idx:]1092 1093 with open(filepath, 'w', encoding='utf-8', newline='') as f:1094 f.write(new_content)1095 1096 return len(new_content)1097 1098guide_dir = os.path.join(BASE)1099for filename, expansion in EXPANSIONS.items():1100 filepath = os.path.join(guide_dir, filename)1101 before_size = os.path.getsize(filepath)1102 new_size = expand_chapter(filepath, expansion)1103 print(f"{filename}: {before_size/1024:.1f}KB → {new_size/1024:.1f}KB (+{(new_size-before_size)/1024:.1f}KB)")1104 1105print("\nDone!")1106 