IustitiaeのBlog
首页项目归档照片墙音乐说说笔记关于
所有笔记本/ 嵌入式

嵌入式

cover
2026-07-07 13:59:01

有关嵌入式产品编号烧录的必要性和相关流程

### **嵌入式产品的“产品编号”是什么?** 嵌入式产品的**产品编号**通常指一个或多个用于标识设备身份的编号,常见的有: * **硬件型号编号**(Model Number / Product ID):表示产品型号,如 “SLB-001A”,用于区分不同硬件版本。 * **序列号**(Serial Number):每台设备唯一的标识,如 “SN20260315001”,用于生产追溯、售后、授权控制。 * **固件版本号**:标识当前烧录的软件版本,常与产品编号配合使用。 * **客户定制的物料码**:根据企业编码规则生成的唯一字符串。 这些编号的共同目的是**唯一标识设备、便于生产管理、售后服务和软件授权**。 **通常需要存储到 Flash(或其他非易失存储器)中,但不是写在“代码”里,而是放在 Flash 的特定区域。** **✅ 为什么需要存储到 Flash?** * 设备断电后序列号不丢失。 * 程序运行时可以读取,用于界面显示、网络上报、授权验证等。 * 生产时每个设备写入不同的序列号,不能硬编码在固件中。 **❌ 为什么不直接写死在代码里?** * 代码是通用的,同一个固件会烧录到所有设备,不能包含唯一值。 * 如果硬编码,每个设备都需要编译不同固件,效率极低且易出错。 ### **常见的存储方案** **存储位置 适用 场景特点** **MCU 内部 Flash 独立区域**(如最后几页) 低成本、无外部存储器 需占用部分 Flash,注意擦写寿命和代码分区 **外部 EEPROM / Data Flash** 需要频繁修改或寿命要求高 可靠性好,读写方便 **电子标签**(如 RFID、NFC) 需要物理标识和电子读取结合 非接触式,常用于防伪 **安全芯片**(如 ATECC508A) 需要高安全性、防克隆 序列号不可篡改,支持加密认证 **OTP / eFuse**(一次性写入区) 永久不变、防篡改 烧录后不可修改,适合唯一 ID ‍ ### **在程序里如何使用?** **1. 写入(生产时操作)** 通常由生产线工具(烧录工装)完成,流程如下: * 通过串口、USB 或 SWD 发送写序列号命令。 * 程序收到指令后,将序列号写入 Flash 预留区。 * 写入后添加校验(如 CRC),防止损坏。 * 最后置标志位表示序列号已写,防止重复写入。 示例 Flash 地址划分: ``` 0x08000000 - 0x0803FFFF : 应用程序代码 (256 KB) 0x08040000 - 0x0805FFFF : 用户数据区 (128 KB,用于存序列号、参数) ``` **2. 读取(运行时调用)** 在初始化时从固定地址读出序列号,校验通过后使用。 ``` const char* get_serial_number(void) { return (const char*)FLASH_SERIAL_NUMBER_ADDR; } ``` **3.安全注意事项** * 序列号写入区应设置**读保护**,防止被外部直接读出。 * 重要产品序列号可配合**数字签名**,防止伪造。 * 使用 OTP 区域可保证不可更改,但需提前规划。 ## **整体生产流程** 1. **开发阶段** 编写通用固件,代码中预留一个“产品信息区域”(如内部 Flash 的末尾扇区,或外部 EEPROM)。固件启动时会检查该区域是否已有有效编号。 2. **生产烧录** 将同一个固件烧录到所有设备(通过 SWD、串口 ISP 等方式),此时产品信息区域是空的(全 `0xFF` 或未初始化)。 3. **生产写入编号** 设备上电,固件检测到产品信息区域无效(空白),自动进入“等待写入编号”模式。 产线工人/自动化系统通过**串口、USB 或专用测试点**向设备发送一条指令,包含唯一的序列号。 固件收到后,将序列号写入 Flash 数据区,并校验成功,同时设置一个“已写入”标志。 写入完成后,设备重启或自动进入正常工作模式。 4. **正常运行** 设备再次上电时,固件从 Flash 数据区读取序列号,用于显示、上报、授权等。 ‍
cover
2026-07-03 16:54:47

flash分区表,中断向量偏移量和ota升级流程

## **一、Flash 分区表 —— 给你的 Flash “划地盘”** ### **1. 为什么要分区?** 就像硬盘分 C 盘、D 盘,Flash 也得划出不同的功能区,**让升级过程中新固件的下载、校验、搬移都不会破坏当前正在运行的程序**。 ### **2. 一个最典型的 STM32F4 分区设计(1MB Flash)** 假设芯片 Flash 总大小 1MB,起始地址 `0x08000000`。 **分区名起始地址大小作用Bootloader**`0x08000000`64KB上电第一行代码,负责验签、搬运固件、跳转**App 主区**`0x08010000`448KB当前运行的应用程序 (APP)**下载暂存区**`0x08080000`448KB接收 OTA 新固件,差分还原也在这里**参数区**`0x080F0000`64KB存升级状态、版本号、断点续传偏移量等 > 如果你做 **A/B 面升级**(更稳),那就不要暂存区,而是两个同样大小的 App 分区:`APP_A (0x08010000)` 和 `APP_B (0x08060000)`,互相切换。 > 上面的例子是 **暂存区+还原** 方案,适合差分升级,ROM 刚好够用。 ### **3. 分区表如何落地到代码里?** **① 头文件中用宏定义地址和大小**(bootloader、app 代码共用) c ``` // flash_partition.h #define FLASH_BASE 0x08000000UL #define BOOTLOADER_SIZE 0x10000 // 64KB #define APP_SIZE 0x70000 // 448KB #define DOWNLOAD_SIZE 0x70000 #define PARAM_SIZE 0x10000 #define BOOTLOADER_START FLASH_BASE #define APP_START (FLASH_BASE + BOOTLOADER_SIZE) // 0x08010000 #define DOWNLOAD_START (APP_START + APP_SIZE) // 0x08080000 #define PARAM_START (DOWNLOAD_START + DOWNLOAD_SIZE) // 0x080F0000 ``` **② 链接脚本 .ld 文件里限制代码存放范围** APP 的链接脚本必须告诉编译器:**代码只能放在** `APP_START` **开始的区域**。 ld ``` /* STM32F407VG_APP.ld */ MEMORY { FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 448K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K } ``` 这样生成出来的 `app.bin`,最开头的中断向量表就天然在 `0x08010000`。 --- ## **二、中断向量偏移量 —— 让 CPU 找到“对的菜单”** ### **1. 为什么要设偏移量?** CPU 响应中断时,会到一个固定的寄存器 `SCB->VTOR` 指明的地址去找**中断服务函数入口表**。 * 芯片刚上电,`VTOR` 默认指向 `0x00000000`(映射到 `0x08000000`),那里是 **Bootloader** 的向量表。 * 一旦我们要执行 APP,CPU 必须去 `0x08010000` 找 **APP 自己的向量表**,否则中断一来,设备就死机或乱跳。 这就是“中断向量表重映射”:**把 VTOR 的值从 Bootloader 的地址改成 APP 的地址**。 ### **2. 配置位置:链接脚本 + 代码配合** **第一步:链接脚本定义 APP 的基地址** 上面 `.ld` 文件已经指定了 `FLASH` 起始地址 `0x08010000`,编译出来的第一个 32 位数(初始 SP)和第二个 32 位数(Reset\_Handler)就在这个地址。**向量表位置已经定了。** **第二步:代码里设置 VTOR 寄存器** 必须在 APP 运行的第一时间设置,通常放在 `system_stm32f4xx.c` 里的 `SystemInit()`,或者在 `main()` 最开头手动加。 **找到/修改** `system_stm32f4xx.c` **中的宏:** c ``` #define VECT_TAB_OFFSET 0x10000 /* 偏移 = APP_START - FLASH_BASE */ ``` 这个宏会在 `SystemInit` 里被写到 `SCB->VTOR`: c ``` SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; ``` 等效于 `SCB->VTOR = 0x08000000 | 0x10000; // 0x08010000` **对于 Keil 环境:** 在 Target Options → Target → IROM1 里设好 `0x08010000`,再在代码里加 `SCB->VTOR = 0x08010000;`。 **第三步:Bootloader 跳转前要不要设?** 通常 **Bootloader 只需要校验 APP,然后跳转**,跳转前可以把 `SCB->VTOR` 设为 APP 的地址,但 APP 启动后仍会自己再设一次(安全起见)。所以 **APP 自己必须设**。 跳转的典型代码(Bootloader): c ``` void jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t*)app_addr; uint32_t app_reset_handler = *(volatile uint32_t*)(app_addr + 4); // 设置栈顶指针 __set_MSP(app_sp); // 可选:将 VTOR 指向 APP 向量表 SCB->VTOR = app_addr; // 跳转到 APP 的复位处理函数 ((void(*)())app_reset_handler)(); } ``` > 注意:跳转前必须关中断、恢复时钟等,具体看正点原子等例程。 --- ## **三、OTA 升级架构(含差分、断点)—— 从头到尾跑一遍** 把上面“分区表 + 向量偏移”套进一个完整的静默升级流程里,你就能看到它们是如何联动工作的。 ### **设备正常运行时(APP 运行)** * APP 的向量表在 `0x08010000`,VTOR 已经设好。 * 后台线程检测到服务器有更新,获取 **差分包** 的 URL、大小、签名。 ### **静默下载阶段(APP 仍在前台工作)** 1. APP 通过 HTTP Range 请求,从 0 开始下载差分包,**边下载边写入下载暂存区**(`0x08080000`)。 2. 每写 1KB 或一个扇区,就把 **已接收长度** 记录到参数区(掉电续传用)。 3. 如果下载中断重启,APP 会先读参数区,知道从哪个偏移量继续请求,实现断电续传。 4. 全部下载完毕后,对整个差分包做 **SHA256**,再用预置公钥验签。通过后,在参数区写 **“升级包就绪”标志**,并注明是差分包。 ### **触发升级(策略点)** * 设备夜间重启,或用户关机后上电。 * 芯片先跑 **Bootloader**(`0x08000000`)。 ### **Bootloader 阶段 —— 核心处理** 1. **读取参数区**,发现有升级就绪标志。 2. **安全验证**:再次校验下载区差分包签名,确保不是伪造(防止参数区被篡改)。 3. **差分还原**(如果包是差分类型): * Bootloader 读取当前 APP 区固件(`0x08010000`)作为旧版本。 * 将差分包+旧固件通过差分算法还原成完整新固件,**输出到下载暂存区自身,或是另一个临时缓冲**(此处需要 RAM/Flash 空间规划,例如把暂存区后半部分当输出,小心覆盖)。 * 还原完成后,新固件就放在下载暂存区(或指定地址)。 4. **搬移/切换固件**: * **方案A(暂存区覆盖)**:把下载暂存区的新固件按扇区擦写,拷贝到 APP 主区(`0x08010000`)。此时 APP 区被覆盖,这就是 **OTA 最关键的“覆写”**。 * **方案B(A/B分区)**:假设有 APP\_B 区(`0x08060000`),直接把新固件还原/解压到 APP\_B,然后修改启动参数指向 B 区。 5. **验签新固件**(安全启动链):对新写入的 APP 区做完整镜像签名校验,以防搬运过程中出错。 6. **清除升级标志**,设置启动标志(如果 A/B)或保持默认。 7. **设置 VTOR =** `APP_START`,跳转执行新 APP。 ### **新 APP 启动** * 执行 `SystemInit`,`SCB->VTOR` 再次被设为 `0x08010000`。 * 程序正常运行,联网上报“升级成功”。 **整个过程中,分区表提供了地址,中断向量偏移保证了 APP 的中断正常响应,OTA 机制通过暂存区和标志位安全地替换了固件。**
cover
2026-06-27 17:00:00

FreeRTOS信号量与回调函数——任务间的对话

## 引言 多任务操作系统中,任务不是孤岛——它们需要通信、需要同步、需要共享资源。如果说上一篇的"任务管理"让每个任务独立运行,那么本文的"信号量"就是任务之间对话的语言。 ## 一、信号量(Semaphore)—— 四种类型 ### 1.1 二值信号量(Binary Semaphore)—— 任务同步 / 中断通知 最常用的信号量类型,只有 0 和 1 两个状态,用于任务间同步或中断通知任务。 **创建:** ``` SemaphoreHandle_t xSemaphore = xSemaphoreCreateBinary(); // 注意:创建后初始值为 0(空)! ``` **任务中等待(Take):** ``` if (xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) { // 获取到信号量,执行同步操作 } ``` **中断中释放(GiveFromISR):** ``` BaseType_t xHigherPriorityTaskWoken = pdFALSE; xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 判断是否需要任务切换 ``` **典型应用场景:** - 按键中断通知任务处理 - DMA 传输完成中断通知任务 - 两个任务之间的握手同步 ### 1.2 计数信号量(Counting Semaphore)—— 资源池管理 可以管理多个同类资源,计数值表示可用资源的数量。 ``` // 最大计数 5,初始计数 3(有 3 个资源可用) SemaphoreHandle_t xCountSem = xSemaphoreCreateCounting(5, 3); // 消耗一个资源 xSemaphoreTake(xCountSem, portMAX_DELAY); // 归还一个资源 xSemaphoreGive(xCountSem); ``` **典型应用场景:** - 管理固定大小的缓冲区池 - 限制同时访问某资源的任务数量 - 多生产者-多消费者模型 ### 1.3 互斥锁(Mutex)—— 资源独占 + 优先级继承 用于保护共享资源(全局变量、外设、通信总线),确保同一时刻只有一个任务访问。 ``` // 创建互斥锁 SemaphoreHandle_t xMutex = xSemaphoreCreateMutex(); // 使用模式(必须在同一任务中 Take 和 Give) xSemaphoreTake(xMutex, portMAX_DELAY); // 访问共享资源(printf、I2C、SPI、全局变量) // ... xSemaphoreGive(xMutex); ``` > ⚠️ **关键限制**: > - 互斥锁**不能在中断中使用**! > - Take 和 Give **必须在同一个任务中成对出现** > - 互斥锁自动支持**优先级继承**,防止优先级翻转问题 **优先级翻转问题**:低优先级任务持有锁 → 高优先级任务等待锁 → 中优先级任务抢走 CPU。互斥锁的优先级继承机制会将持有锁的低优先级任务临时提升,避免这种情况。 ### 1.4 递归互斥锁(Recursive Mutex) 普通互斥锁同一任务只能 Take 一次,递归互斥锁允许同一任务多次 Take。 ``` SemaphoreHandle_t xRecursiveMutex = xSemaphoreCreateRecursiveMutex(); xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); // 第一次获取 xSemaphoreTakeRecursive(xRecursiveMutex, portMAX_DELAY); // 再次获取(OK!) xSemaphoreGiveRecursive(xRecursiveMutex); // 释放一层 xSemaphoreGiveRecursive(xRecursiveMutex); // 完全释放 ``` **典型应用场景**:嵌套函数调用中,每个函数都可能需要获取同一个锁。 ## 二、信号量类型对比总结 类型Take 次数Give 次数中断可用优先级继承典型用途**二值信号量**1 次(变 0)1 次(变 1)✅ Give❌任务同步、中断通知**计数信号量**N 次(减到 0)N 次(加到最大)✅ Give❌资源池管理**互斥锁**1 次1 次(同一任务)❌✅资源独占保护**递归互斥锁**多次(同任务)对应次数❌✅嵌套函数锁 ## 三、软件定时器与回调函数 FreeRTOS 提供**软件定时器**,允许在定时器到期时执行回调函数。这些回调函数在**定时器服务任务**的上下文中运行。 ### 创建软件定时器 ``` // 定时器回调函数(不能有死循环!不能有阻塞调用!) void TimerCallback(TimerHandle_t xTimer) { // 定时执行的代码 // 只能调用带 FromISR 后缀或不阻塞的 API } // 创建定时器 TimerHandle_t xTimer = xTimerCreate( "MyTimer", // 名称 pdMS_TO_TICKS(100), // 周期 100ms pdTRUE, // pdTRUE=周期模式, pdFALSE=单次触发 (void *)0, // 定时器 ID(可传参) TimerCallback // 回调函数 ); // 启动定时器 xTimerStart(xTimer, 0); ``` ### CubeMX CMSIS-RTOS 封装方式 ``` osTimerDef(MyTimer, TimerCallback); osTimerId timerHandle = osTimerCreate(osTimer(MyTimer), osTimerPeriodic, NULL); osTimerStart(timerHandle, 5); // 5ms 周期 ``` ### 回调函数注意事项 注意点说明**不能有死循环**回调函数必须快速返回**不能阻塞**不能调用 `vTaskDelay`、`xSemaphoreTake`(带超时的也不行)**不能调用非 FromISR 的 API**可以使用 `xSemaphoreGiveFromISR`**运行在定时器服务任务中**优先级通常较高,耗时操作会影响所有定时器 ## 四、任务通知 —— 更轻量的替代方案 如果只需要简单的任务同步,**任务通知(Task Notification)** 比信号量更快,占用 RAM 更少——但一个任务只能阻塞在一个通知上。 ``` // 发送通知(任务中) xTaskNotifyGive(TaskHandle); // 发送通知(中断中) vTaskNotifyGiveFromISR(TaskHandle, &xHigherPriorityTaskWoken); // 等待通知 ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // pdTRUE → 收到通知后将值清零(类似二值信号量) // pdFALSE → 收到通知后将值减 1(类似计数信号量) ``` **任务通知 vs 信号量**: - 任务通知:更快、RAM 更少,但**一个任务只能等待一个通知源** - 信号量:稍慢但更灵活,可以等待**任意多个信号量** ## 五、信号量 vs 队列 vs 任务通知 机制用途传递数据中断安全优先级继承**队列**任务间数据传输✅ 数据✅❌**二值信号量**任务同步/中断通知❌ 仅通知✅❌**计数信号量**资源计数管理❌ 计数✅❌**互斥锁**资源独占保护❌ 锁❌✅**任务通知**轻量同步可选 32位值✅❌ ## 六、关键避坑指南 1. **中断中不能用互斥锁**,只能用信号量(带 `FromISR` 后缀的 API) 2. **中断释放信号量后必须调用** `portYIELD_FROM_ISR()` 检查是否需要上下文切换 3. **互斥锁的 Take 和 Give 必须在同一个任务中成对出现** 4. **二值信号量创建后初始为空(0)**,如需初始可用需额外调用一次 `xSemaphoreGive()` 5. **软件定时器回调中不能阻塞或死循环** 6. **共享资源必须加锁**,否则会出现数据覆盖、串口乱码、I2C/SPI 时序错乱 ## 总结 信号量是 FreeRTOS 任务间通信的基石。二值信号量实现同步,计数信号量管理资源,互斥锁保护临界区,软件定时器回调处理周期任务——这四把武器掌握好了,绝大部分 RTOS 应用场景都能从容应对。 > *交流创造秩序,共享需要规则。* —— Iustitiae ‍
#FreeRTOS#STM32#RTOS#信号量#回调函数
cover
2026-06-27 16:00:00

FreeRTOS入门——配置与任务管理

## 引言 当你的嵌入式项目从"点个灯"进化到"同时处理显示屏刷新、传感器数据采集、WiFi通信和用户交互"时,裸机编程的超级循环(Super Loop)就捉襟见肘了。 FreeRTOS 是一个轻量级的实时操作系统内核,它为单片机提供了**多任务调度**的能力——让多个任务看起来"同时运行"。而这一切的核心,就是任务管理。 ## 一、为什么需要 RTOS? ### 裸机编程的痛点 ``` while (1) { ReadSensor(); // 耗时 50ms UpdateDisplay(); // 耗时 100ms CheckButton(); // 必须快速响应,但被前面的任务阻塞了! SendDataToCloud(); // 传输不稳定 } ``` 当 `CheckButton()` 需要实时响应,却因为前面的任务耗时太长而无法及时执行时,用户体验就变成了"按了键没反应"。 ### RTOS 的解决方案 FreeRTOS 通过**任务调度器**在多个任务之间快速切换(通常每 1ms 一次),让每个任务都能按时得到 CPU 时间。从宏观上看,所有任务在"同时"运行。 ## 二、环境搭建与基础配置 ### CubeMX 配置步骤 1. 在 **Middleware → FREERTOS** 中,Interface 选择 `CMSIS_V1` 或 `CMSIS_V2` 2. 在 **Tasks and Queues** 选项卡中创建任务 3. 在 **Config parameters** 中调整内核参数 ### 关键配置宏(FreeRTOSConfig.h) 配置项说明推荐值`configUSE_PREEMPTION`启用抢占式调度1(启用)`configTICK_RATE_HZ`系统时钟节拍频率1000(1ms 一个 tick)`configMAX_PRIORITIES`最大优先级数5~7(视需求而定)`configMINIMAL_STACK_SIZE`最小任务栈大小128(字)`configTOTAL_HEAP_SIZE`系统堆总大小根据任务数量和栈大小计算`configUSE_COUNTING_SEMAPHORES`启用计数信号量1`configUSE_MUTEXES`启用互斥锁1 ## 三、任务(Task)—— FreeRTOS 的核心 ### 3.1 任务是什么? 任务是 FreeRTOS 中的基本执行单元。每个任务有自己的栈空间、优先级和任务控制块(TCB)。调度器根据优先级和状态决定哪个任务获得 CPU。 ### 3.2 任务状态转换 ``` ┌──────────────────────┐ │ 就绪态 │ ← 任务准备好,等待 CPU │ (Ready) │ └──┬────────▲─────────┘ │ 获得CPU │ 时间片耗尽 ▼ │ ┌────────────┴──────┐ │ 运行态 │ → 正在使用 CPU │ (Running) │ └────────┬──────────┘ │ 调用 vTaskDelay / 等待信号量 / 等待队列 ▼ ┌──────────────────────┐ │ 阻塞态 │ → 等待某个事件(时间/信号量/队列) │ (Blocked) │ └──────────┬───────────┘ │ 事件发生(时间到/信号量获得/数据到达) ▼ ┌──────────────────────┐ │ 就绪态 │ └──────────────────────┘ ``` 此外还有**挂起态(Suspended)**——任务被 `vTaskSuspend()` 挂起后,不参与调度,直到被 `vTaskResume()` 恢复。 ### 3.3 创建任务 ``` // 动态创建任务(推荐) BaseType_t xTaskCreate( TaskFunction_t pvTaskCode, // 任务函数指针 const char *pcName, // 任务名称(调试用) uint32_t usStackDepth, // 栈大小(字,不是字节!) void *pvParameters, // 传递给任务的参数 UBaseType_t uxPriority, // 优先级(数值越大优先级越高) TaskHandle_t *pxCreatedTask // 任务句柄(可为 NULL) ); // 任务函数模板 void MyTask(void *pvParameters) { for (;;) { // 任务函数必须有死循环!不能 return! // 任务逻辑 vTaskDelay(pdMS_TO_TICKS(1000)); // 延时 1000ms(以 tick 为单位) } } ``` > ⚠️ **重要**:`vTaskDelay(pdMS_TO_TICKS(1000))` 让任务进入阻塞态 1000ms,释放 CPU 给其他任务。不要用 `for` 循环做忙等待! ### 3.4 完整示例 ``` // 任务1:LED 闪烁 void TaskLED(void *pvParameters) { for (;;) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); vTaskDelay(pdMS_TO_TICKS(500)); // 500ms 闪烁 } } // 任务2:串口打印 void TaskPrint(void *pvParameters) { for (;;) { printf("Hello from FreeRTOS!\r\n"); vTaskDelay(pdMS_TO_TICKS(2000)); // 每 2 秒打印一次 } } // 主函数中创建任务 xTaskCreate(TaskLED, "LED", 128, NULL, 1, NULL); xTaskCreate(TaskPrint, "Print", 256, NULL, 2, NULL); // 启动调度器 vTaskStartScheduler(); // 调度器启动后,以下代码永远不会执行 while (1) {} ``` ## 四、任务优先级与调度 ### 抢占式调度 * **高优先级任务就绪时,立即抢占低优先级任务** * 相同优先级的任务通过**时间片轮转**(每个 tick 切换一次) * 优先级数值越大,优先级越高(在 STM32 CubeMX 配置中) ### 优先级分配建议 任务类型推荐优先级说明时间关键(电机控制、PID)高(4~5)不能被长时间阻塞用户交互(按键、显示刷新)中(2~3)需要及时响应后台任务(日志、数据上报)低(0~1)空闲时处理 ## 五、idle 任务与空闲钩子 FreeRTOS 会自动创建一个**最低优先级的 idle 任务**,当所有任务都阻塞时运行。 可以通过**空闲钩子函数**在 idle 任务中执行低优先级后台操作: ``` void vApplicationIdleHook(void) { // 进入低功耗模式 // 清理资源 // 注意:不能在此函数中调用阻塞 API! } ``` ## 总结 FreeRTOS 的任务管理是实时操作系统的基石。掌握任务创建、状态转换、优先级调度这三个核心概念,你就迈出了从裸机编程到 RTOS 编程最重要的一步。 下一篇文章我们将深入**信号量与回调函数**——任务间如何通信和同步。 > *分而治之,各司其职。* —— 计算机科学的古老智慧 ‍
#FreeRTOS#STM32#RTOS#任务管理
cover
2026-06-27 15:00:00

嵌入式通信协议UART、I2C、SPI对比详解

## 引言 在嵌入式系统中,通信协议是各模块之间对话的语言。UART、I2C 和 SPI 是最常用的三种串行通信协议——它们各自有着鲜明的性格特点,适用的场景也截然不同。理解它们的差异,是选择正确协议的前提。 ## 一、UART —— 异步通信的经典 ### 基本原理 UART(Universal Asynchronous Receiver/Transmitter)是一种**异步、全双工、点对点**的串行通信协议。它不需要共享时钟信号,通信双方通过**预先约定相同的波特率**来同步。 ### 硬件连接 * **最少 2 根线**:TX(发送)、RX(接收) * 可选硬件流控:RTS、CTS ### 数据帧格式 ``` ┌─────────┬──────────────────┬──────────┬──────────┐ │ 起始位 │ 数据位(5~9位) │ 校验位 │ 停止位 │ │ (1位) │ (通常8位) │ (可选) │ (1/2位) │ └─────────┴──────────────────┴──────────┴──────────┘ ``` * **起始位**:数据线从高拉低,通知接收方开始传输 * **数据位**:5~9 位(通常 8 位),LSB 先发 * **奇偶校验位**:可选,用于错误检测 * **停止位**:将数据线拉高,持续 1 位或 2 位时间 ### 优缺点 优点缺点仅需 2 根线,简单易用仅支持点对点通信无需时钟信号数据帧最大 9 位有奇偶校验位两端波特率偏差不能超过约 10%可扩展为 RS-232/RS-485速度较慢(通常 ≤ 115200 bps) ## 二、I²C —— 多设备总线的优雅 ### 基本原理 I²C(Inter-Integrated Circuit)是一种**同步、半双工、多主多从**的串行通信协议。它通过**地址寻址**方式选择通信目标设备。 ### 硬件连接 * **仅 2 根线**:SDA(数据)、SCL(时钟) * 两条线都需要**上拉电阻**(通常 4.7KΩ) * 总线为**开漏输出**设计,支持多设备共享 ### 通信时序 ``` ┌──────────────────────────────────────────────────────────────┐ │ 起始条件 │ 从机地址(7/10位)+R/W位 │ ACK │ 数据帧 │ ACK │ ... │ 停止条件 │ └──────────────────────────────────────────────────────────────┘ ``` * **起始条件**:SCL 高电平时,SDA 由高→低 * **停止条件**:SCL 高电平时,SDA 由低→高 * **地址帧**:7 位或 10 位从机地址 + 1 位读写位 * **ACK/NACK**:接收方拉低 SDA 表示确认 ### 优缺点 优点缺点仅需 2 根线速度比 SPI 慢支持多主多从架构数据帧固定 8 位内置 ACK/NACK 确认协议复杂度较高地址寻址,节省 GPIO通信距离短(通常板内) ## 三、SPI —— 高速通信的王者 ### 基本原理 SPI(Serial Peripheral Interface)是一种**同步、全双工、主从架构**的串行通信协议。它通过**片选信号(CS)**来选择通信目标。 ### 硬件连接 * **4 根线**: * **SCLK/SCK**:时钟线(主机产生) * **MOSI**:主机输出、从机输入 * **MISO**:主机输入、从机输出 * **SS/CS**:片选信号(每个从机一条独立线) ### 4 种工作模式 模式CPOLCPHA说明模式 000空闲时钟低,第一个边沿采样(最常用)模式 101空闲时钟低,第二个边沿采样模式 210空闲时钟高,第一个边沿采样模式 311空闲时钟高,第二个边沿采样 ### 传输过程 1. 主机输出时钟信号 2. 主机**拉低目标从机的 CS 引脚**,激活从机 3. 主机通过 MOSI 发送数据,同时从机通过 MISO 返回数据(全双工) 4. 传输完成后,主机**拉高 CS** 释放从机 ### 优缺点 优点缺点速度最快(10~50 MHz)需要 4 根线,每增加从机多一根 CS全双工,可同时收发无内置错误检测和确认机制无起始/停止位,数据可连续流不支持多主机协议简单无标准协议层 ## 四、三大协议全面对比 特性**UARTI²CSPI通信方式**异步同步同步**双工模式**全双工半双工全双工**信号线数**2 根2 根4 根**拓扑结构**点对点多主多从单主多从**设备选择**无(仅一对)地址寻址片选信号 CS**典型速率**9.6K~115.2K bps100K/400K/3.4M bps10M~50M bps**数据帧大小**5~9 位固定 8 位无限制(连续传输)**错误检测**奇偶校验位ACK/NACK无内置**通信距离**较长(RS-485 可达千米)短(板内 <1m)短(<0.5m)**复杂度**中等较高简单 ## 五、选型指南 —— 什么时候选谁? ### 🟢 选 UART 的场景 * **调试与日志输出**:嵌入式开发中最常用的调试手段 * **PC 与 MCU 通信**:通过 USB 转串口连接 * **GPS/GSM/蓝牙模块**:点对点无线通信模块 * **工业远距离通信**:配合 RS-232/RS-485 电平转换 ### 🔵 选 I²C 的场景 * **多传感器网络**:温湿度传感器(SHT30)、气压传感器(BMP280) * **EEPROM 存储**:AT24Cxx 系列 * **RTC 实时时钟**:DS3231、PCF8563 * **GPIO 扩展/ADC/DAC**:需要节省引脚资源 * **OLED 小屏幕**:SSD1306 等 ### 🔴 选 SPI 的场景 * **高速数据存储**:NOR Flash(W25Qxx)、SD 卡 * **TFT 彩屏驱动**:ST7789、ILI9341 * **高速 ADC/DAC**:音频采集、高速采样 * **WiFi 模块**:部分高速无线模块 ## 六、总结口诀 ``` 速度排名: SPI >> I²C > UART 引脚数量: SPI(4+) > UART(2~4) > I²C(2) 设备扩展: I²C(地址寻址) > SPI(片选) > UART(仅点对点) 协议复杂度:I²C > UART > SPI 通信距离: UART(远) >> I²C(短) ≈ SPI(短) 可靠性: I²C(ACK) > UART(校验) > SPI(无内置) ``` > 没有最好的协议,只有最合适的协议。在实际项目中,三种协议往往是**配合使用**的——用 UART 调试通信,用 I²C 挂载传感器,用 SPI 驱动屏幕和存储。各取所长,才是正道。 ‍
#单片机#通信协议#UART#I2C#SPI
cover
2026-06-27 14:00:00

ADC模数转换详解——从模拟信号到数字世界

## 引言 现实世界是模拟的——温度、湿度、压力、光线、声音,都是连续变化的物理量。而单片机只认识 0 和 1。ADC(模数转换器)就是连接这两个世界的桥梁。 ## 一、ADC 基本概念 ADC(Analog to Digital Converter)将连续的模拟信号转换为离散的数字信号。转换过程包含四个核心步骤: 步骤说明**采样(Sampling)**以固定时间间隔获取模拟信号的瞬时值**保持(Hold)**用电容保持采样值,防止转换期间信号变化**量化(Quantization)**将连续的模拟值划分为离散的数字级别**编码(Encoding)**将量化结果以二进制形式输出 ## 二、核心参数详解 ### 1. 分辨率(Resolution) * STM32 F1/F4 系列:典型 **12 位**分辨率,输出范围 0 ~ 4095(2¹² - 1) * STM32 H7 系列:最高 **16 位**分辨率,过采样可达 26 位 **LSB(最低有效位)计算公式:** ``` LSB = Vref+ / 2^分辨率位数 ``` 以 12 位分辨率、参考电压 3.3V 为例: ``` LSB = 3.3V / 4096 ≈ 0.806mV 实际电压 = ADC转换值 × (3.3 / 4096) ``` ### 2. 采样率(Sampling Rate) 关键参数F1 系列典型值说明ADC 时钟最高14 MHz超过会降低精度常用分频配置PCLK2 的 8 分频 = 9 MHzADCCLK 必须 ≤ 14MHz最短采样周期1.5 个 ADCCLK 周期可编程范围:1.5 ~ 239.5 周期 **总转换时间公式:** ``` Tconv = 采样时间 + 12.5 个周期(逐次逼近固定时间) ``` > 最快转换时间(F1):Tconv = (1.5 + 12.5) × (1/12MHz) ≈ **1.17 μs** ### 3. ADC 类型 STM32 采用**逐次逼近型(SAR)ADC**,在精度和速度之间取得良好平衡。 ## 三、工作模式 ### 转换模式 模式说明**单次转换**触发一次,执行一次转换后停止**连续转换**一次触发后,不断循环转换**扫描模式**按预设顺序轮流采集多个通道**间断模式**分组间断地进行多通道转换 ### 通道组 通道组通道数特点**规则通道组(Regular)**最多 16 路正常运行,结果存于单个数据寄存器**注入通道组(Injected)**最多 4 路类似中断,可打断规则通道转换 ### 触发方式 * **软件触发**:代码直接启动转换 * **外部事件触发**:定时器、EXTI 外部中断等 ## 四、HAL 库配置示例 ### 单通道单次转换 ``` // 1. 使能时钟 __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); // 2. GPIO 配置为模拟输入 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. ADC 初始化 ADC_HandleTypeDef hadc1; hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; HAL_ADC_Init(&hadc1); // 4. 配置通道 ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_1; sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 5. 读取转换值 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); uint16_t adc_value = HAL_ADC_GetValue(&hadc1); float voltage = adc_value * (3.3f / 4096); ``` ## 五、DMA 模式 —— 多通道/高速采集 对于多通道或高速持续采集,推荐使用 **DMA 循环传输模式**,由 DMA 自动将 ADC 数据搬移到内存数组,无需 CPU 频繁干预: ``` // DMA 关键配置 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; // ADC数据寄存器 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&adc_buffer; // 内存数组 DMA_InitStructure.DMA_BufferSize = 通道数量; ``` 这样 ADC 持续采集,DMA 自动搬运,CPU 只需在需要时读取 `adc_buffer` 即可。 ## 六、常见问题与注意事项 问题原因解决方案**外部阻抗过大导致误差**分压电阻过大(MΩ 级),与内部 50kΩ 阻抗分压使用 ≤10kΩ 分压电阻,或加运放做阻抗变换**多通道串扰**采样电容在通道切换时残留电荷增大通道间采样延时**引脚浮空**外部无输入时引脚悬空加下拉电阻**扫描模式数据覆盖**规则组只有一个数据寄存器及时读取或使用 DMA**ADC 时钟超频**ADC 时钟 >14MHz正确配置分频因子 ## 七、STM32 各系列 ADC 参数速查 指标STM32F1STM32F4STM32H7分辨率12 bit12 bit16 bitADC 时钟最高14 MHz36 MHz36 MHz最快转换时间≈ 1 μs≈ 0.5 μs更快参考电压3.3V3.3V2.5V / 3.3VLSB(12bit@3.3V)0.806 mV0.806 mV约 0.05 mV ## 总结 ADC 是单片机感知模拟世界的窗口。从 12 位到 16 位,从单次采样到 DMA 循环采集,理解分辨率、采样率与转换时间的关系,是设计可靠采样系统的基础。 > *世界是连续的,但理解世界的每一步,都是离散的。* —— Iustitiae ‍
#单片机#STM32#ADC#模数转换
cover
2026-06-27 13:00:00

PWM脉冲宽度调制详解

## 引言 PWM(Pulse Width Modulation,脉冲宽度调制)是利用数字输出来模拟模拟电路控制的一种技术。它看似简单——只是控制高低电平的时间比例——但几乎是嵌入式系统中最常用的信号输出方式。从呼吸灯到电机调速,从舵机控制到音频合成,PWM 无处不在。 ## 一、PWM 基本概念 ### 关键参数 参数定义公式**周期(T)**一个完整波形从开始到重复的时间T = 1/f**频率(f)**单位时间内完整周期的次数f = 1/T**占空比(D)**高电平时间占整个周期的比例D = Ton / T × 100%**分辨率**占空比可调节的最小步距由 ARR 决定 > 例:5V 系统,占空比 50% → 等效输出电压 ≈ 2.5V ## 二、STM32 PWM 生成原理 STM32 通过**定时器输出比较的 PWM 模式**生成 PWM 信号。核心寄存器: 寄存器作用**TIMx\_PSC**(预分频器)对时钟频率进行分频**TIMx\_ARR**(自动重装载)决定 PWM 的**周期/频率TIMx\_CCRx**(捕获/比较)决定 PWM 的**占空比TIMx\_CNT**(计数器)当前计数值,不断与 CCRx 比较 ### 工作流程(向上计数、PWM 模式 1) ``` 高电平 ↑ ┌──────┐ ┌──────┐ │ │ │ │ │ │─────┘ └────────┘ └────→ │ ← CCRx →←── (ARR-CCRx) ──→ │ ←─────── 一个周期 (ARR+1) ─────→ ``` 1. 计数器 CNT 从 0 向上累加 2. **CNT < CCRx 时** → 输出高电平 3. **CNT ≥ CCRx 时** → 输出低电平 4. **CNT = ARR 时** → CNT 清零,开始下一个周期 ## 三、频率与占空比计算公式 ### PWM 频率 ``` PWM频率 = 定时器时钟频率 / [(PSC + 1) × (ARR + 1)] ``` ### 占空比(PWM 模式 1) ``` 占空比 = CCRx / (ARR + 1) × 100% ``` ### 配置示例 > **目标**:产生 1KHz、50% 占空比的 PWM > **时钟**:72MHz > • 设 PSC = 71 → 计数时钟 = 72MHz / 72 = 1MHz > • 设 ARR = 999 → PWM频率 = 1MHz / 1000 = 1KHz > • 设 CCRx = 500 → 占空比 = 500/1000 = 50% ## 四、频率与分辨率的权衡 这是 PWM 配置中最需要理解的设计权衡: 取舍说明**频率 ↑**ARR ↓ → 分辨率 ↓**分辨率 ↑**ARR ↑ → 频率 ↓ > **示例**:计数时钟 1MHz > 若要 100KHz PWM → ARR=9,分辨率仅 10 级 > 若要 1KHz PWM → ARR=999,分辨率达 1000 级(0.1% 精度) ## 五、PWM 模式选择 模式行为适用场景**PWM Mode 1**CNT < CCR → 高电平通用场合**PWM Mode 2**CNT ≥ CCR → 高电平需要反相波形时 极性设置(`TIM_OCPolarity_High` / `TIM_OCPolarity_Low`)可进一步反转输出电平。 ## 六、HAL 库配置与代码 ### ① CubeMX 配置 1. 选择定时器(如 TIM3),Clock Source 设为 `Internal Clock` 2. 选择通道(如 Channel 1),设为 `PWM Generation CH1` 3. 设置参数:Prescaler、Counter Period(ARR)、Pulse(初始 CCRx) ### ② 呼吸灯示例 ``` // 启动 PWM 输出 HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 动态修改占空比实现呼吸灯 while (1) { for (uint16_t i = 0; i < 1000; i += 10) { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i); HAL_Delay(20); } for (uint16_t i = 1000; i > 0; i -= 10) { __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, i); HAL_Delay(20); } } ``` ### ③ 常用 API 函数/宏功能`HAL_TIM_PWM_Start()`启动 PWM 输出`HAL_TIM_PWM_Stop()`停止 PWM 输出`__HAL_TIM_SET_COMPARE()`动态修改占空比`__HAL_TIM_GET_AUTORELOAD()`获取 ARR 值 ## 七、典型应用 应用PWM 频率占空比范围说明**LED 呼吸灯**1-2KHz0-100%频率高于人眼闪烁感知**舵机 SG90**50Hz(20ms周期)2.5%-12.5%0.5ms=0°,2.5ms=180°**直流电机调速**16-20KHz0-100%避开人耳听觉范围**无源蜂鸣器**1-4KHz50%输出音频频率 ## 八、舵机控制详解 舵机是 PWM 最经典的应用之一。以 SG90 为例: ``` 周期 = 20ms(50Hz) 高电平持续 0.5ms → 舵机角度 0° 高电平持续 1.0ms → 舵机角度 45° 高电平持续 1.5ms → 舵机角度 90° 高电平持续 2.0ms → 舵机角度 135° 高电平持续 2.5ms → 舵机角度 180° ``` **50Hz PWM 配置(72MHz 时钟):** ``` PSC = 144 - 1 = 143 → CK_CNT = 72MHz / 144 = 500KHz ARR = 10000 - 1 = 9999 → PWM周期 = 10000/500K = 20ms CCR值范围 = 250 ~ 1250 → 0.5ms ~ 2.5ms ``` ## 九、常见问题排查 问题检查项无 PWM 输出①定时器时钟是否使能 ②GPIO 是否配置为复用功能 ③是否调用了 `HAL_TIM_PWM_Start()`频率偏差检查 APB 预分频器设置、确认未启用 CKD 分频占空比异常确认 PWM 模式(Mode 1/2)与极性设置是否匹配高级定时器无输出TIM1/TIM8 需设置 `TIM_BDTR->MOE` 位使能主输出 ## 总结 PWM 是输出比较最实用的形态。掌握了频率、占空比、分辨率三者的关系,你就能用一根信号线控制 LED 的明暗、电机的快慢、舵机的角度——这是数字世界对模拟世界最优雅的控制方式。 > *控制不在于力的强弱,而在于节奏的精妙。* —— Iustitiae ‍
#单片机#STM32#PWM#舵机
cover
2026-06-27 12:00:00

输出比较与输入捕获

## 引言 如果说定时器的基本定时功能是它的"心跳",那么输出比较和输入捕获就是它的"双手"——一手创造信号,一手测量信号。理解这两个功能,是掌握 PWM、频率计、超声波测距等应用的前提。 ## 一、核心概念:CNT 与 CCR 理解输出比较和输入捕获,关键在于两个寄存器的关系: 寄存器含义在 IC 中在 OC 中**CNT**计数器时间标尺时间标尺**CCR**捕获/比较寄存器**保存** CNT 的快照值**与** CNT 比较,匹配时触发动作 > 💡 **一句话理解:** > - **输入捕获(IC)**:外部信号边沿到来时,**把 CNT 的当前值"拍照"存入 CCR** > - **输出比较(OC)**:CNT 数到与 CCR 相等时,**触发硬件动作** ## 二、输入捕获(Input Capture) ### 2.1 工作原理 ``` ┌─────── 捕获上升沿 ───────┐ │ │ 外部信号: ────┘ └─────── │ │ CNT值: ... 100 ... 200 ... 300 ... 400 ... 500 ... ↑ ↑ CCR=100 CCR=500 脉冲宽度 = 500 - 100 = 400 个计数周期 ``` **工作流程:** 1. 配置通道为输入捕获模式,选择触发边沿(上升沿 / 下降沿 / 双边沿) 2. 当指定边沿到来时,硬件自动将当前 **CNT 的值复制到 CCR** 中 3. 连续捕获两次,**两次 CCR 的差值**就是两个边沿之间的时间 4. 配合计数频率换算出实际时间:`时间 = 差值 × 计数周期` ### 2.2 测量频率 频率测量常用**两次上升沿捕获法**: ``` f_signal = f_cnt / (CCR2 - CCR1) ``` 其中 `f_cnt` 是 CNT 的计数频率 = `TIM_CLK / (PSC + 1)` ### 2.3 典型应用 应用说明频率计测量外部信号的频率占空比测量同时测量 PWM 信号的高电平与周期红外遥控解码测量编码脉冲的宽度超声波测距测量 Echo 引脚高电平持续时间编码器测速通过捕获正交编码器脉冲测量转速 ### 2.4 HAL 配置示例 ``` TIM_IC_InitTypeDef sConfigIC = {0}; sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING; // 上升沿捕获 sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直接映射到 TIx sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 不分频 sConfigIC.ICFilter = 0x0; // 数字滤波 HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1); // 启动输入捕获中断 HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); ``` ## 三、输出比较(Output Compare) ### 3.1 工作原理 ``` CNT: 0 ──→ 30 ──→ 60 ──→ 90 ──→ ARR ──→ 0 ──→ ... ↑ CNT == CCR (假设CCR=30) │ ┌────┴────┐ │ 触发动作 │ → 翻转/置高/置低/中断 └─────────┘ ``` 当 CNT 的值等于 CCR 时,发生"比较匹配"事件,触发预先配置的动作。 ### 3.2 八种输出比较模式 模式匹配时行为典型用途**冻结 (Frozen)**输出保持不变暂停 PWM 输出**匹配时置有效电平**输出置高单次脉冲**匹配时置无效电平**输出置低单次脉冲**匹配时翻转 (Toggle)**电平取反生成 50% 占空比方波**强制无效电平**强制输出低强制停机**强制有效电平**强制输出高强制输出**PWM 模式 1**CNT < CCR 高, CNT ≥ CCR 低**最常用的 PWM 输出PWM 模式 2**CNT < CCR 低, CNT ≥ CCR 高PWM 模式 1 的反相版本 ### 3.3 典型应用 应用说明LED 调光改变 PWM 占空比控制亮度电机调速PWM 控制直流电机转速舵机控制用特定脉宽控制角度生成参考方波Toggle 模式生成 50% 占空比方波外设同步信号匹配时触发 ADC 采样 / DMA 传输 ## 四、输入捕获 vs 输出比较 —— 对比总结 维度输入捕获 (IC)输出比较 (OC)**数据流向**外部 → MCU(**测量**外部信号)MCU → 外部(**产生**信号)**CNT 与 CCR 的关系**边沿到来时,CNT → CCRCNT == CCR 时触发动作**引脚方向**输入输出(复用推挽)**核心用途**测频率、周期、脉宽、占空比输出 PWM、方波、脉冲**中断事件**捕获中断比较匹配中断**共用资源**同一通道同一时刻只能配置为 IC 或 OC ## 五、常见问题与排查 ### CNT 溢出处理(输入捕获) 当被测信号周期很长时,CNT 会在两次捕获之间溢出。必须统计溢出次数: ``` 实际时间 = 溢出次数 × (ARR + 1) + 最后一次 CCR 值 ``` 需要在更新中断中累加溢出计数。 ### PWM 输出无波形? 常见原因排查: 1. **GPIO 未配置为复用推挽输出** 2. **未调用** `HAL_TIM_PWM_Start()` 启动输出 3. **高级定时器(TIM1/TIM8)未使能主输出**:需设置 MOE 位 4. **时钟未使能** ## 总结 输出比较和输入捕获是定时器最强大的两个功能。一个向外创造,一个向内感知。理解了 CNT 与 CCR 之间"比较"与"捕获"的两种关系,你就掌握了定时器 80% 的应用场景。 下一篇我们将深入 **PWM**——输出比较最重要的应用形态。 > *测量即是理解,输出即是创造。* —— Iustitiae ‍
#单片机#STM32#输出比较#输入捕获
cover
2026-06-27 11:00:00

单片机定时器与中断系统详解——从CNT到NVIC

## 引言 定时器和中断是嵌入式系统的"心跳"与"神经系统"。定时器让单片机感知时间的流逝,中断让单片机能够及时响应外部事件。两者结合,构成了实时嵌入式系统的核心基础。 ## 一、核心组件概览 STM32 定时器中断系统由三个核心部件协同工作: 部件作用**定时器 (TIM)**产生定时中断请求**NVIC (嵌套向量中断控制器)**判断优先级并决定是否响应中断**CPU**暂停当前任务,跳转到中断服务函数执行 ## 二、定时器核心寄存器 寄存器全称功能**CNT**计数器根据时钟信号递增/递减计数**PSC**预分频器对输入时钟分频,扩展定时范围**ARR**自动重装载寄存器设定计数上限,CNT 达到 ARR 时触发更新事件**DIER**中断使能寄存器控制是否允许定时器事件触发中断**SR**状态寄存器包含 UIF(更新中断标志)等标志位 ### 定时计算公式 ``` 实际计数频率 CK_CNT = TIMxCLK / (PSC + 1) 中断周期 = (ARR + 1) / CK_CNT ``` > **示例**:APB1 时钟 72MHz,PSC=71,ARR=999 > → CK\_CNT = 72MHz / 72 = 1MHz > → 中断周期 = 1000 / 1MHz = **1ms** ## 三、中断触发完整流程 ``` 1. CNT 计数到达 ARR 值 ↓ 2. 硬件自动置位 UIF(更新事件标志) ↓ 3. 若 UIE(更新中断使能位)已使能 ↓ 4. 定时器向 NVIC 发送中断请求 ↓ 5. NVIC 根据优先级判断是否响应 ↓ (若响应) 6. CPU 暂停当前任务,保存现场,跳转到中断服务函数 ↓ 7. ISR 中执行业务代码 ↓ 8. 【关键】手动清除 UIF 标志位 ``` ## 四、NVIC 中断优先级详解 ### 抢占优先级 vs 响应优先级 优先级类型作用**抢占优先级**决定中断是否可嵌套。高抢占优先级可以打断低抢占优先级的中断**响应优先级(子优先级)**抢占优先级相同时,若多个中断同时到达,响应优先级高者先处理 ### 优先级分组(5 组,4 位分配) 分组抢占优先级位数子优先级位数抢占级数子优先级级数NVIC\_PriorityGroup\_004016NVIC\_PriorityGroup\_11328NVIC\_PriorityGroup\_22244NVIC\_PriorityGroup\_33182NVIC\_PriorityGroup\_440160 > 数值越小,优先级越高。推荐使用 **Group 2**(4 级抢占 + 4 级子优先级),在嵌套能力和响应顺序之间取得平衡。 ## 五、完整配置步骤 ### 第1步:使能外设时钟 ``` // TIM2 挂载在 APB1 总线上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); ``` ### 第2步:配置 NVIC 中断优先级 ``` void TIM2_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } ``` ### 第3步:初始化定时器时基参数 ``` void TIM2_Config(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); } ``` ### 第4步:使能中断并启动定时器 ``` TIM_ClearFlag(TIM2, TIM_FLAG_Update); // 清除标志位 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断 TIM_Cmd(TIM2, ENABLE); // 启动定时器 ``` ### 第5步:编写中断服务函数 ``` void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 必须清除标志位! // 用户逻辑 LED_TOGGLE(); } } ``` ## 六、注意事项 注意事项说明**必须手动清除中断标志**不调用 `TIM_ClearITPendingBit()` 会导致反复进入中断,程序卡死**中断函数名要正确**必须与启动文件中定义的中断向量名称一致**ISR 内代码尽量简短**复杂逻辑应设置标志位,在主循环中处理**初始化时清除标志位**在 `TIM_ITConfig` 之前调用 `TIM_ClearFlag` 防止误触发**确认时钟总线**TIM1/TIM8 挂 APB2,TIM2~TIM7 挂 APB1 ## 七、STM32 定时器分类 类型定时器功能**高级控制定时器**TIM1、TIM83 对 PWM 互补输出,常用于三相电机驱动**通用定时器**TIM2~TIM5定时、PWM 输出、输入捕获、编码器接口**基本定时器**TIM6、TIM7仅定时功能,无外部 IO ## 总结 定时器与中断是嵌入式系统的脉搏。理解了 CNT 如何计数、ARR 如何设定周期、NVIC 如何仲裁优先级,你就掌握了实时系统最核心的"时间感"。配合下一篇文章的**输出比较与输入捕获**,定时器的威力才能真正发挥。 > *时间不在于你拥有多少,而在于你怎样使用。* —— 艾克 ‍
#单片机#STM32#定时器#中断#NVIC
cover
2026-06-27 10:00:00

单片机GPIO原理与实战——从寄存器到HAL库

## 引言 GPIO(General Purpose Input/Output,通用输入输出)是单片机最基础也最常用的外设。无论是点亮一颗LED、读取一个按键,还是驱动复杂的通信总线,都离不开对GPIO的理解。 > 如果说单片机是一个微型计算机,那么GPIO就是它与物理世界对话的窗口。 ## 一、GPIO 内部结构 STM32 的每个 I/O 引脚内部包含以下关键电路: 结构功能**保护二极管**防止引脚电压高于 VDD 或低于 VSS,保护芯片**上拉/下拉电阻**内部弱电阻(约 30~50kΩ),可通过寄存器配置**施密特触发器**将模拟信号整形成数字 0/1,输入模式下开启**P-MOS + N-MOS 管**实现推挽输出和开漏输出的核心驱动电路**输入数据寄存器 (IDR)**每个 AHB 时钟周期采样一次引脚电平**输出数据寄存器 (ODR)**控制输出电平状态**位设置/清除寄存器 (BSRR)**原子操作 ODR 的各个位,不会被中断打断 ## 二、8 种 GPIO 工作模式 ### 📥 4 种输入模式 1. 浮空输入(Input Floating) * 上拉和下拉电阻**均断开** * 引脚悬空时电平不确定,易受外部噪声干扰 * **典型应用**:I2C、USART 的 RX 端、ADC 输入 2. 上拉输入(Input Pull-Up) * 内部**上拉电阻接通**到 VDD * 引脚悬空时保持**高电平** * **典型应用**:按键检测(按键接地,平时高电平,按下低电平) 3. 下拉输入(Input Pull-Down) * 内部**下拉电阻接通**到 VSS * 引脚悬空时保持**低电平** * **典型应用**:按键检测(按键接 VDD,平时低电平,按下高电平) 4. 模拟输入(Analog) * 上拉/下拉断开,施密特触发器关闭 * 电压信号直接传入 ADC 模块 * **典型应用**:ADC 电压采集、省电模式下的未用引脚 ### 📤 4 种输出模式 5. 推挽输出(Push-Pull) * **P-MOS + N-MOS 双管工作**:输出 1 时 P-MOS 导通,输出 0 时 N-MOS 导通 * 驱动能力强,上升沿速度快 * **典型应用**:LED 驱动、普通 GPIO 输出 6. 开漏输出(Open-Drain) * **只有 N-MOS 管工作**:输出 0 强拉到地,输出 1 需外部上拉 * 具备**线与**功能,可实现电平转换 * **典型应用**:I2C 总线、电平不匹配场合 7. 复用推挽输出(AF Push-Pull) * 与推挽输出相同,但信号源来自片上外设(SPI、USART、定时器) * **典型应用**:USART TX、SPI MOSI/SCK 8. 复用开漏输出(AF Open-Drain) * 与开漏输出相同,信号源来自片上外设 * **典型应用**:I2C SCL/SDA ## 三、推挽 vs 开漏 —— 核心对比 特性推挽输出开漏输出驱动管P-MOS + N-MOS仅 N-MOS输出高电平**强高电平**(芯片内部驱动)**需外部上拉电阻**输出低电平强低电平强低电平电平转换不支持**支持**线与功能不支持(短路风险)**支持**典型应用LED、普通 IOI2C、电平匹配 ## 四、关键寄存器速查 寄存器功能**GPIOx\_CRL/CRH**配置引脚模式(每 4 位控制一个引脚)**GPIOx\_IDR**读取引脚输入电平(只读)**GPIOx\_ODR**设置/读取输出电平**GPIOx\_BSRR**原子位操作(低 16 位置位,高 16 位清零)**GPIOx\_BRR**位清零寄存器**GPIOx\_PUPDR**配置上拉/下拉(F4/F7/H7 系列)**GPIOx\_OTYPER**配置输出类型:0=推挽,1=开漏 ## 五、常用配置速查表 应用场景推荐模式按键检测(接地)上拉输入按键检测(接 VDD)下拉输入LED 驱动推挽输出I2C 通信复用开漏输出 + 外部上拉USART TX复用推挽输出USART RX浮空输入ADC 采样模拟输入未用引脚模拟输入(省电 + 抗干扰)需要 5V 高电平输出开漏输出 + 外部 5V 上拉 ## 六、HAL 库配置示例 ``` // 1. 使能 GPIO 时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 2. 配置 GPIO GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 控制输出 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 输出高 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 输出低 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转 // 4. 读取输入 GPIO_PinState state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); ``` ## 总结 GPIO 看似简单,但推挽与开漏的选择、上拉与下拉的理解、复用功能的配置,是通往更复杂外设(I2C、SPI、PWM)的基石。掌握了 GPIO,你就掌握了单片机与物理世界对话的第一门语言。 > *工欲善其事,必先利其器。* —— 《论语》 ‍
#单片机#STM32#GPIO