浏览代码

ingest: STM32 HAL/StdLib 笔记 + asset 位置约定

kekeZack 1 周之前
父节点
当前提交
97fa1c0c62
共有 42 个文件被更改,包括 7646 次插入0 次删除
  1. 1 0
      AGENTS.md
  2. 0 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC.mp4.docx
  3. 0 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC_20260505013137.pptx
  4. 0 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC_笔记.pdf
  5. 78 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/01-环境搭建.md
  6. 116 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/02-GPIO输出.md
  7. 139 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/03-GPIO输入.md
  8. 156 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/04-UART串口.md
  9. 153 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/05-I2C.md
  10. 197 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/06-SPI.md
  11. 127 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/07-中断系统.md
  12. 109 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/08-时钟树.md
  13. 295 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/09-定时器.md
  14. 234 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/10-ADC.md
  15. 392 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/01-环境搭建.md
  16. 209 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/02-GPIO输出.md
  17. 199 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/03-GPIO输入.md
  18. 274 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/04-OLED调试.md
  19. 363 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/05-EXTI外部中断.md
  20. 501 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/06-TIM定时中断.md
  21. 258 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/07-TIM输出比较.md
  22. 308 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/08-TIM输入捕获.md
  23. 222 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/09-编码器接口.md
  24. 313 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/10-ADC模数转换.md
  25. 304 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/11-DMA.md
  26. 391 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/12-USART串口.md
  27. 503 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/13-I2C通信.md
  28. 383 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/14-SPI通信.md
  29. 330 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/15-BKP-RTC.md
  30. 372 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/16-PWR电源控制.md
  31. 350 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/17-WDG看门狗.md
  32. 369 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/18-FLASH闪存.md
  33. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/clock-tree.png
  34. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/interrupt-example.png
  35. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/interrupt-priority-group.png
  36. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/mcu-internal.png
  37. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-4-modes.png
  38. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-bus.png
  39. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-cpha.png
  40. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-cpol.png
  41. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/timer-block.png
  42. 二进制
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/timer-timebase.png

+ 1 - 0
AGENTS.md

@@ -21,6 +21,7 @@
 - **`wiki/`** — 整理后的知识层。可自由创建、更新、合并、重构。
 - 禁止将原始内容直接写入 `wiki/`。
 - 新旧内容冲突时必须显式标注,不得静默覆盖。
+- **`assets/` 不独立存在** — 图片等资源文件放在**所属主题目录下的 `assets/`** 子目录中,而非顶层 `assets/`。如 `raw/Joplin/嵌入式+Linux/assets/`。
 
 ## 操作后必做清单
 

+ 0 - 0
X-Knowledge-Base/raw/Joplin/兆鸣嵌入式/C语言OOP封装完整系列-1080P 60帧-AVC.mp4.docx → X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC.mp4.docx


+ 0 - 0
X-Knowledge-Base/raw/Joplin/兆鸣嵌入式/C语言OOP封装完整系列-1080P 60帧-AVC_20260505013137.pptx → X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC_20260505013137.pptx


+ 0 - 0
X-Knowledge-Base/raw/Joplin/兆鸣嵌入式/C语言OOP封装完整系列-1080P 60帧-AVC_笔记.pdf → X-Knowledge-Base/raw/Joplin/嵌入式+Linux/C语言OOP封装完整系列/C语言OOP封装完整系列-1080P 60帧-AVC_笔记.pdf


+ 78 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/01-环境搭建.md

@@ -0,0 +1,78 @@
+# 01 环境搭建(HAL 库)
+
+> STM32F103C8T6 | CubeMX → Keil MDK → DAP-Link
+
+## 开发流程总览
+
+```mermaid
+flowchart LR
+    A["CubeMX<br/>图形化配置"] --> B["生成代码<br/>MDK-ARM Project"]
+    B --> C["Keil MDK<br/>编辑/编译"]
+    C --> D["DAP-Link<br/>烧录"]
+    D --> E["调试<br/>(Debug/串口)"]
+    E -.->|修改代码| C
+    E -.->|改配置| A
+```
+
+## 工具链角色
+
+| 工具 | 在流程中的角色 |
+|------|--------------|
+| **STM32CubeMX** | 图形化配置引脚、时钟、外设参数,生成 HAL 初始化 C 代码 |
+| **Keil MDK** | 编辑代码、交叉编译(ARMCC/CLANG)、烧录、调试 |
+| **DAP-Link / ST-Link** | SWD 协议调试器,物理连接 PC USB ↔ 开发板 SWDIO/SWCLK |
+| **串口助手** | 通过 USB-TTL 查看 MCU 串口输出的调试信息 |
+
+## CubeMX 生成基础工程
+
+1. 打开 CubeMX → **New Project**
+2. MCU Selector → 搜索并选中 **STM32F103C8T6**
+3. **System Core → SYS → Debug** → 设为 **Serial Wire (SWD)**
+   - ⚠️ 这一步**必须做**!否则烧录一次后 SWD 引脚被 GPIO 占用,芯片永久锁死,只能用串口 ISP 擦除
+4. **System Core → RCC → HSE** → 设为 **Crystal/Ceramic Resonator**
+5. 时钟树暂时保留默认(HSI 8MHz),第 08 节专门讲 72MHz 配置
+6. **Project Manager → Project** 标签页:
+   - Project Name:`LED_Test`
+   - Toolchain / IDE:**MDK-ARM (Keil)**
+   - Minimum Firmware Version:不影响功能,选最新
+7. 点 **GENERATE CODE** → 选择目录 → 生成后选择 **Open Project** 直接启动 Keil
+
+## ARMCC 编译器说明
+
+CubeMX 生成 Keil 工程时默认使用 ARMCC V6(即 ARMCLANG)。如果你需要使用老版 V5 编译器(某些旧驱动库只支持 V5):
+
+1. 将 ARMCC V5.06 目录复制到 Keil 的安装目录(ARMCC 需要单独从 Keil 官网下载)
+2. Keil 中 **Project → Manage → Project Items**
+3. 在 Folder 视图顶部的 **ARM Compiler** 下拉框中新增 V5.06 路径
+4. 选中新加的 V5.06 → 重新编译
+5. ⚠️ 若首次配置编译报错,**再走一遍上述流程**即可通过(常见 bug,原因未知)
+
+> 如果你的工程使用 CubeMX 默认的 ARMCLANG(V6),不需要任何额外配置。
+
+## 烧录配置(每次新建工程需配置一次)
+
+1. **Options for Target (Alt+F7) → Debug** 标签
+2. 右侧 **Use** 下拉选择 **CMSIS-DAP Debugger**
+3. 点击 **Settings** → 如果 DAP 已正确连接,SWD 设备列表里应该能识别到
+4. 切换到 **Flash Download** 标签 → 勾选 **Reset and Run**(烧录后自动复位运行)
+5. 烧录:**Flash → Download (F8)**
+
+> 如果 Settings 里没识别到 DAP:检查 SWDIO/SWCLK/GND 三根线是否正确连接,重新插拔 USB
+
+## 调试功能(Keil Debugger)
+
+1. **Options for Target → Debug** → 确认已选 CMSIS-DAP
+2. **Debug** 标签 → 在右下角关闭编译器优化:**`-O0`**(否则变量值和单步行为不准确)
+3. **Start/Stop Debug Session (Ctrl+F5)** 进入调试模式
+4. 添加 **Watch 窗口**:View → Watch → Watch1
+5. 在 Watch 窗口输入变量名即可**实时查看和修改变量**值
+6. 常用调试操作:全速运行 (F5)、单步进入 (F11)、单步跳过 (F10)、跳出 (Ctrl+F11)、下断点 (单击行号左侧)
+
+## 常见问题
+
+| 问题 | 原因 | 解决方法 |
+|------|------|---------|
+| 烧录后无法识别 DAP | 接线松动或芯片进入低功耗 | 重新插拔 USB,检查 SWD 接线 |
+| 芯片锁死 "No target connected" | SWD 引脚被 GPIO 占用 | 按住复位键 → 点烧录 → 松开复位 |
+| 编译报错 "..\\*.h: No such file" | Include Paths 配置丢失 | CubeMX 重新生成,或在 Options → C/C++ → Include Paths 中手动添加 |
+| 串口输出乱码 | 波特率不一致 | 确认 MCU 和串口助手都设成了相同波特率(如 115200) |

+ 116 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/02-GPIO输出.md

@@ -0,0 +1,116 @@
+# 02 GPIO 输出 —— 点灯(HAL)
+
+> 目标:控制 PC13(板载 LED)以 500ms 间隔闪烁
+
+## IO 端口内部结构
+
+```text
+                      VDD
+                       │
+                 ┌─────┤
+                 │P-MOS│    P-MOS 导通 = 引脚输出高电平
+                 │     │
+    输出数据 ←──┐┴──  │
+                 │     │
+                 │N-MOS│    N-MOS 导通 = 引脚输出低电平
+                 │     │
+                 └─┬───┤
+                   │    │
+                   └────┘
+                      │
+                    GND
+```
+
+在推挽输出 (Push-Pull) 模式下,P-MOS 和 N-MOS 交替导通:
+- 写 1 → P-MOS 导通 → 引脚接 VDD (3.3V)
+- 写 0 → N-MOS 导通 → 引脚接 GND (0V)
+
+在开漏输出 (Open-Drain) 模式下,P-MOS 被断开:
+- 写 1 → 高阻态(引脚既不高也不低,需要外部上拉电阻拉到 VDD)
+- 写 0 → N-MOS 导通 → 引脚接 GND
+
+> 板载 LED 通常接 PC13,低电平点亮:`GPIO_PIN_RESET`(0) = 亮, `GPIO_PIN_SET`(1) = 灭
+
+## GPIO 8 种工作模式速查
+
+STM32 的 GPIO 有 8 种模式,分为 4 种输入 + 2 种输出 + 2 种复用:
+
+| 模式 | 宏定义 | 用途 |
+|------|--------|------|
+| 模拟输入 | `GPIO_MODE_ANALOG` | ADC 输入 |
+| 浮空输入 | `GPIO_MODE_IN_FLOATING` | 外部有上下拉时用 |
+| 上拉输入 | `GPIO_MODE_IPU` | 按键(松开高电平) |
+| 下拉输入 | `GPIO_MODE_IPD` | 按键(松开低电平) |
+| 推挽输出 | `GPIO_MODE_OUTPUT_PP` | 普通 LED、普通 IO |
+| 开漏输出 | `GPIO_MODE_OUTPUT_OD` | I2C SDA/SCL |
+| 复用推挽 | `GPIO_MODE_AF_PP` | USART TX、SPI SCK/MOSI |
+| 复用开漏 | `GPIO_MODE_AF_OD` | I2C 的复用模式 |
+
+## CubeMX 配置
+
+- Pinout → 选中 **PC13** → 设为 **GPIO_Output**
+- 参数设置:
+  - GPIO output level:**Low**(初始状态熄灭,改为 High 就是初始点亮)
+  - GPIO mode:**Output Push-Pull**
+  - Pull-up/Pull-down:**No pull**(或 Pull-up)
+  - Maximum output speed:**Low**(LED 用低速即可)
+
+## 关键 HAL API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_GPIO_WritePin(GPIOx, GPIO_PIN_x, PinState)` | 设置引脚为高/低电平 |
+| `HAL_GPIO_TogglePin(GPIOx, GPIO_PIN_x)` | 翻转引脚电平 |
+| `HAL_Delay(uint32_t Delay)` | 毫秒级阻塞延时(依赖 SysTick 中断) |
+
+`PinState`:
+- `GPIO_PIN_SET` = 高电平 (1)
+- `GPIO_PIN_RESET` = 低电平 (0)
+
+## 完整代码
+
+```c
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+
+    while (1)
+    {
+        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);  // 点亮
+        HAL_Delay(500);
+        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);    // 熄灭
+        HAL_Delay(500);
+    }
+}
+```
+
+或用 Toggle 简化:
+
+```c
+while (1)
+{
+    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+    HAL_Delay(500);
+}
+```
+
+## GPIO 输出速度的选择
+
+GPIO 有 3 档输出速度,需要正确匹配应用场景:
+
+| 速度档位 | 上升/下降时间 | 适用场景 | 理由 |
+|---------|-------------|---------|------|
+| **Low (2MHz)** | ~125ns | LED、按键、普通 GPIO | 最慢 = EMI 最小,省电 |
+| **Medium (10MHz)** | ~25ns | 几 MHz 以下的 SPI/I2C | 中等频率够用 |
+| **High (50MHz)** | ~5ns | 高速 SPI、SDIO、FSMC | 速度优先,EMI 较大 |
+
+> 输出速度指 **IO 驱动电路的响应速度**,不是信号频率上限。选过高速度会增加功耗和 EMI 辐射。能用 Low 就选 Low。
+
+## 常见坑点
+
+- ⚠️ **先查原理图确认 LED 极性**:低电平点亮(RESET=亮)还是高电平点亮(SET=亮)?
+- ⚠️ `HAL_Init()` 必须在 `HAL_Delay()` 之前调用,否则 SysTick 未初始化,延时无效
+- ⚠️ GPIO 时钟在 `MX_GPIO_Init()` 中自动开启 (`__HAL_RCC_GPIOC_CLK_ENABLE()`),不需要手动加
+- ⚠️ 复用功能(如 USART 的 TX 引脚)必须选 **AF_PP** 或 **AF_OD**,不能是普通 Output

+ 139 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/03-GPIO输入.md

@@ -0,0 +1,139 @@
+# 03 GPIO 输入(HAL)
+
+> 目标:读取按键和传感器模块电平,实现按键控制 LED、光敏传感器控制蜂鸣器
+
+## 原理简述
+
+### 按键
+
+按键是最简单的输入设备,**按下导通,松手断开**。由于内部采用机械弹簧片,按下和松手瞬间会产生**抖动**(5-10ms 的不稳定电平):
+
+```text
+理想波形: 高‾‾‾‾‾‾‾‾‾‾‾\_\_\_\_\_‾‾‾‾‾‾‾‾‾‾‾
+实际波形: 高‾‾‾‾\_‾\_‾\_‾\_\_\_\_\_\_‾\_‾\_‾‾‾‾‾
+          ↑按下       ↑稳定      ↑松手
+          抖动区      低电平     抖动区
+```
+
+**软件消抖**:检测到电平变化后,延时 10ms 再次读取确认。
+
+### 传感器模块
+
+传感器模块内部结构:
+
+```text
+传感器(光敏/热敏/红外) ──┬── 定值电阻 ─── VCC
+                        │
+                        └── 分压输出(模拟) ── 电压比较器(LM393) ── DO(数字输出)
+                                                    ↑
+                                              参考电压(电位器分压)
+```
+
+- 敏感电阻随外界物理量变化 → 分压变化 → 比较器二值化 → DO 输出数字信号(0/1)
+- AO 输出原始模拟电压(需 ADC 读取)
+
+## 电路连接
+
+- **KEY1**(轻触按键):一端接 GND,一端接 PB0,内部上拉
+  - 按下:PB0 = 低电平
+  - 松开:PB0 = 高电平(上拉)
+- **光敏传感器模块**:
+  - VCC → 3.3V, GND → GND
+  - DO → PA0(数字输出)
+  - AO → PA1(模拟输出,暂不接)
+
+## CubeMX 配置
+
+- PB0 → **GPIO_Input**,Pull-up
+- PA0 → **GPIO_Input**,Pull-up(传感器 DO)
+- 保持其他默认,不需要任何中断
+
+## 关键 HAL API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_GPIO_ReadPin(GPIOx, GPIO_PIN_x)` | 读取引脚电平,返回 `GPIO_PIN_SET`(1) 或 `GPIO_PIN_RESET`(0) |
+
+## 完整代码
+
+### 按键控制 LED
+
+```c
+/* main.c */
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();     // PB0_Input + PC13_Output
+
+    uint8_t key_flag = 0;   // 防止一次按下触发多次
+
+    while (1)
+    {
+        /* 检测按键按下(低电平有效) */
+        if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
+        {
+            HAL_Delay(10);                      // 消抖
+            if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET)
+            {
+                if (key_flag == 0)
+                {
+                    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+                    key_flag = 1;               // 防止重复触发
+                }
+            }
+        }
+        else
+        {
+            key_flag = 0;                       // 松手后清零
+        }
+    }
+}
+```
+
+**松手沿检测**(另一种更可靠的写法):
+
+```c
+uint8_t pre_key = 1, cur_key = 1;
+
+while (1)
+{
+    pre_key = cur_key;
+    cur_key = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) ? 0 : 1;
+
+    if (pre_key != cur_key)         // 电平变化
+    {
+        HAL_Delay(10);              // 消抖
+        if (cur_key == 1)           // 松手沿:上一次按下,这一次松开
+        {
+            HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+        }
+    }
+}
+```
+
+### 光敏传感器控制蜂鸣器
+
+```c
+/* PA0(DO) 读取传感器输出 */
+while (1)
+{
+    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
+    {
+        // 光线暗 → DO 输出低电平 → 蜂鸣器响
+        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_RESET);  // 低电平驱动
+    }
+    else
+    {
+        // 光线亮 → DO 输出高电平 → 蜂鸣器关
+        HAL_GPIO_WritePin(GPIOC, GPIO_PIN_9, GPIO_PIN_SET);
+    }
+}
+```
+
+## 常见坑点
+
+- ⚠️ 机械按键必须**消抖**,否则一次按下可能触发多次
+- ⚠️ GPIO 浮空输入时引脚悬空会导致电平不确定,**务必使用上拉/下拉**
+- ⚠️ 传感器模块的 DO/AO 区别:DO 是数字开关量,AO 是模拟电压(需要 ADC)
+- ⚠️ 按键按下为低电平是因为 GND 导通,设计电路时注意按键接法

+ 156 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/04-UART串口.md

@@ -0,0 +1,156 @@
+# 04 UART 串口通信(HAL)
+
+> 目标:串口发送 + 中断接收控制 LED 闪烁速度
+
+## 帧格式
+
+USART 使用异步串行通信,无时钟线,双方约定相同波特率:
+
+```text
+空闲状态:TX 保持高电平
+
+传输 1 字节 (115200-8-N-1):
+  ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
+  │ 空闲 │START │  D0  │  D1  │  D2  │  D3  │  D4  │  D5  │  D6  │  D7  │ STOP │ 空闲
+  │  (H) │  (L) │      │      │      │      │      │      │      │      │  (H) │  (H)
+  └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
+```
+
+- **起始位**:1 位,拉低(标志数据开始)
+- **数据位**:8 位,LSB 先发(低字节在前)
+- **校验位**:None(本例),也可选 Even/Odd
+- **停止位**:1 位,拉高(标志数据结束)
+
+**TTL 电平连线**:
+
+```text
+STM32 (PA9 TX) ────────→ USB-TTL (RX)
+STM32 (PA10 RX) ←────── USB-TTL (TX)
+STM32 (GND)     ──────── USB-TTL (GND)     ← 共地必须!
+```
+
+## 数据发送流程详解
+
+`HAL_UART_Transmit()` 内部做三件事:
+
+1. 把数据写入**发送数据寄存器 (TDR)**
+2. 等待 **TXE 标志**(Transmit Data Register Empty,TDR 已空,可以写下一个字节)
+3. 等待 **TC 标志**(Transmission Complete,最后一位停止位已发完)
+
+> 初学者容易混淆:TXE 表示"发送寄存器空了(可以填下一个)";TC 表示"整个字节包括停止位发完了"。`HAL_UART_Transmit` 内部两种标志都处理了,不需要手动轮询。
+
+## CubeMX 配置
+
+- **USART1** → 模式 **Asynchronous**
+- 参数:
+  - Baud Rate:**115200**
+  - Word Length:**8 Bit (including Parity)** — Parity=None 时就是纯 8 位数据
+  - Parity:**None**
+  - Stop Bits:**1**
+- 引脚自动分配:PA9(TX)、PA10(RX)
+- **NVIC Settings → USART1 global interrupt** → Enable(中断接收需要)
+
+## 关键 HAL API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_UART_Transmit(&huart, pData, Size, Timeout)` | 阻塞发送(等待 TXE+TC) |
+| `HAL_UART_Receive(&huart, pData, Size, Timeout)` | 阻塞接收(等待 RXNE) |
+| `HAL_UART_Receive_IT(&huart, pData, Size)` | 中断接收(推荐,不阻塞) |
+| `HAL_UART_RxCpltCallback(huart)` | 接收完成回调(用户重写) |
+
+参数说明:
+- `huart`:UART 句柄(如 `huart1`)
+- `pData`:数据缓冲区指针(注意类型是 `uint8_t *`)
+- `Size`:期望收发字节数
+- `Timeout`:超时 ms(`HAL_MAX_DELAY` = 无限等待)
+
+## 完整代码:阻塞发送
+
+```c
+#include <string.h>
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
+
+    while (1)
+    {
+        char msg[] = "Hello STM32!\r\n";
+        HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);
+        HAL_Delay(1000);
+    }
+}
+```
+
+## 完整代码:中断接收控制 LED 速度
+
+中断接收的核心模式:先调用 `HAL_UART_Receive_IT` 注册接收 → 硬件收到 1 字节触发中断 → HAL 自动调用回调 → 回调里处理完数据后**再调用一次 `HAL_UART_Receive_IT` 重新注册**:
+
+```c
+/* 全局变量 */
+static uint32_t blinkInterval = 1000;
+static uint8_t dataRcvd;
+
+/* 回调函数 —— 每次接收完成自动调用(由 HAL 在中断上下文调用) */
+void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
+{
+    if (huart == &huart1)
+    {
+        // 根据接收到的字符修改闪烁间隔
+        if (dataRcvd == '1')      blinkInterval = 1000;
+        else if (dataRcvd == '2') blinkInterval = 300;
+        else if (dataRcvd == '3') blinkInterval = 50;
+
+        HAL_UART_Receive_IT(&huart1, &dataRcvd, 1);  // 重新开启接收(否则只收一次)
+    }
+}
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
+
+    /* 第一次启动中断接收 —— 注册回调 */
+    HAL_UART_Receive_IT(&huart1, &dataRcvd, 1);
+
+    while (1)
+    {
+        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+        HAL_Delay(blinkInterval);
+    }
+}
+```
+
+## 重定向 printf 到串口
+
+Keil 中使用 MicroLIB 时,重写 `fputc` 即可让 `printf` 输出到串口:
+
+```c
+#include <stdio.h>
+
+/* 在 uart.c 或 main.c 中重写 fputc */
+int fputc(int ch, FILE *f)
+{
+    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
+    return ch;
+}
+```
+
+使用前提:
+1. Keil Options → Target → 勾选 **Use MicroLIB**
+2. 包含 `<stdio.h>`
+3. 之后可直接写:`printf("ADC=%d, V=%.2f\r\n", val, voltage);`
+
+## 常见坑点
+
+- ⚠️ `HAL_UART_Transmit` 的 `pData` 类型是 `uint8_t *`,传 `char *` 字符串时**必须强转**
+- ⚠️ 中断接收**每次回调后必须重新调用 `HAL_UART_Receive_IT`**,否则只收一次就停了
+- ⚠️ 串口助手与 MCU 的**波特率、数据位、校验位、停止位完全一致**才不出乱码
+- ⚠️ MCU 是 3.3V TTL 电平,接 5V 的 USB-TTL 模块可能损坏引脚,确认电压匹配
+- ⚠️ `HAL_UART_Receive`(阻塞版)会一直等到收到指定字节数或超时,不要在中断中调用

+ 153 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/05-I2C.md

@@ -0,0 +1,153 @@
+# 05 I2C 通信(HAL)
+
+> 目标:通过 I2C1 驱动 OLED 显示屏(SSD1306,地址 0x78)
+
+## 总线结构
+
+```text
+I2C 使用两根线(SDA + SCL)连接多个设备:
+  主机 (STM32) ←→ SDA ──────────────────
+                   SCL ──────────────────
+                         │       │       │
+                      从机1    从机2    从机3
+                      (OLED)  (传感器)  (其他)
+  两根线都必须外部上拉到 VDD(3.3V 或 5V,模块通常自带)
+```
+
+**为什么需要开漏+上拉?**
+- I2C 是多主机协议,任何设备都可以拉低总线
+- 开漏输出 + 上拉电阻 = 多个设备可以"线与"(谁拉低谁赢)
+- SCL 低时改变 SDA,SCL 高时采样 SDA(保证数据稳定)
+
+## 通信时序详解
+
+每一帧 I2C 通信由主机发起:
+
+```text
+主机发→从机收(写操作):
+START → [7位地址 + W(0)] → ACK ← 从机 → DATA1 → ACK → DATA2 → ACK → ... → STOP
+
+主机收←从机发(读操作):
+START → [7位地址 + R(1)] → ACK ← 从机 → DATA1 → ACK ← 主机 → ... → NAK ← 主机 → STOP
+```
+
+**关键点**:
+- **ACK**:每字节后接收方拉低 SDA 表示"收到了"——从机发 ACK 确认地址/数据
+- **NAK**:读操作的最后,主机不发 ACK 改为拉高 SDA,通知从机"不要发了"
+- 每个字节都是 **MSB 先发**
+
+**7 位地址 → 8 位地址**:I2C 规范中从机地址是 7 位,但通信时左移 1 位,最低位表示读写:
+- 0x3C (7 位) → 左移 = 0x78 → 写
+- 0x3C (7 位) → 左移 + 1 = 0x79 → 读
+
+## CubeMX 配置
+
+- **I2C1** → 模式 **I2C**
+- 参数:
+  - Speed Mode:**Standard Mode** (100kHz) — 兼容性好;快速模式 400kHz
+  - 其他保持默认
+- 引脚自动分配:PB6(SCL)、PB7(SDA)。CubeMX 自动设为**开漏输出**。
+
+## 关键 HAL API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_I2C_Master_Transmit(&hi2c, DevAddr, pData, Size, Timeout)` | 主机发送数据到从机 |
+| `HAL_I2C_Master_Receive(&hi2c, DevAddr, pData, Size, Timeout)` | 主机从从机接收数据 |
+| `HAL_I2C_Mem_Write(&hi2c, DevAddr, MemAddr, MemAddrSize, pData, Size, Timeout)` | 向从机指定寄存器写入 |
+| `HAL_I2C_Mem_Read(&hi2c, DevAddr, MemAddr, MemAddrSize, pData, Size, Timeout)` | 从从机指定寄存器读取 |
+
+`DevAddr` = 7 位地址左移 1 位,例如 `0x3C << 1 = 0x78`。
+
+## 完整代码:OLED(SSD1306)基本驱动
+
+SSD1306 的控制字节格式:`Co(1) | D/C#(1) | 0(6)`。其中:
+- `0x00` = 后续字节是**命令**(Co=0, D/C#=0)
+- `0x40` = 后续字节是**数据**(Co=0, D/C#=1)
+
+```c
+/* 写命令 */
+static void OLED_WriteCmd(uint8_t cmd)
+{
+    uint8_t data[2] = {0x00, cmd};   // 控制字节 + 命令
+    HAL_I2C_Master_Transmit(&hi2c1, 0x78, data, 2, HAL_MAX_DELAY);
+}
+
+/* 写数据 */
+static void OLED_WriteData(uint8_t dat)
+{
+    uint8_t data[2] = {0x40, dat};   // 控制字节 + 数据
+    HAL_I2C_Master_Transmit(&hi2c1, 0x78, data, 2, HAL_MAX_DELAY);
+}
+
+/* 初始化 —— 按功能分组注释 */
+static void OLED_Init(void)
+{
+    HAL_Delay(100);                  // 等待 SSD1306 上电稳定
+
+    OLED_WriteCmd(0xAE);             // [1] 关闭显示(配置期间保持关闭)
+
+    OLED_WriteCmd(0xD5);             // [2] 振荡器频率
+    OLED_WriteCmd(0x80);
+    OLED_WriteCmd(0xA8);             // [3] 多路复用比 (MUX)
+    OLED_WriteCmd(0x3F);             //     128×64: MUX=63
+    OLED_WriteCmd(0xD3);             // [4] 显示偏移
+    OLED_WriteCmd(0x00);
+    OLED_WriteCmd(0x40);             // [5] 起始行 = 0
+    OLED_WriteCmd(0x8D);             // [6] 电荷泵
+    OLED_WriteCmd(0x14);             //     使能(内部升压到 ~7V)
+    OLED_WriteCmd(0x20);             // [7] 内存地址模式
+    OLED_WriteCmd(0x00);             //     水平模式
+    OLED_WriteCmd(0xA1);             // [8] 段重映射(列 127→SEG0)
+    OLED_WriteCmd(0xC8);             // [9] COM 扫描方向(从上到下)
+    OLED_WriteCmd(0xDA);             // [10] COM 引脚配置
+    OLED_WriteCmd(0x12);
+    OLED_WriteCmd(0x81);             // [11] 对比度
+    OLED_WriteCmd(0xCF);
+    OLED_WriteCmd(0xD9);             // [12] 预充电周期
+    OLED_WriteCmd(0xF1);
+    OLED_WriteCmd(0xDB);             // [13] VCOMH 电压
+    OLED_WriteCmd(0x40);
+    OLED_WriteCmd(0xA4);             // [14] 全局显示恢复
+    OLED_WriteCmd(0xA6);             // [15] 正常显示(非反色)
+    OLED_WriteCmd(0x2E);             // [16] 停止水平滚动
+    OLED_WriteCmd(0xAF);             // [17] 开启显示!
+}
+```
+
+**清屏**(逐个像素写入 0x00):
+
+```c
+static void OLED_Clear(void)
+{
+    for (uint8_t page = 0; page < 8; page++)     // 128×64 = 8 pages × 128 cols
+    {
+        OLED_WriteCmd(0xB0 + page);  // 设置页地址
+        OLED_WriteCmd(0x00);         // 列低位
+        OLED_WriteCmd(0x10);         // 列高位
+        for (uint8_t col = 0; col < 128; col++)
+            OLED_WriteData(0x00);    // 该列全部熄灭
+    }
+}
+```
+
+## I2C 读操作示例(以读取传感器寄存器为例)
+
+```c
+uint8_t reg_addr = 0x00;
+uint8_t value;
+
+/* 写寄存器地址(不发送 STOP,而是 Repeated Start):
+   发 START→从机地址+W→ACK→寄存器地址→ACK → (Repeated Start)
+   发 START→从机地址+R→ACK→接收数据→NAK→STOP
+*/
+HAL_I2C_Mem_Read(&hi2c1, 0x78, reg_addr, I2C_MEMADD_SIZE_8BIT, &value, 1, HAL_MAX_DELAY);
+```
+
+## 常见坑点
+
+- ⚠️ I2C 引脚必须设为**开漏输出**(CubeMX 自动配置,不要手动改)
+- ⚠️ 地址**左移 1 位**:SSD1306 7 位地址 0x3C → HAL 中传 0x78
+- ⚠️ SDA/SCL 外部必须有上拉电阻(大多数模块自带 4.7kΩ 上拉到 VDD)
+- ⚠️ 如果总线卡死(SDA 一直为低),可复位从机或重新初始化 I2C 外设
+- ⚠️ STM32F1 的 I2C 硬件有已知 BUG(总线错误标志误触发),新版 HAL 已修复

+ 197 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/06-SPI.md

@@ -0,0 +1,197 @@
+# 06 SPI 通信(HAL)
+
+> 目标:SPI1 驱动 W25Q64(外置 Flash),实现按键切换 LED 状态并断电保存
+
+## 原理简述
+
+### 总线结构
+
+![SPI 总线结构](../assets/spi-bus.png)
+
+SPI 是 **全双工、同步** 串行通信协议,核心四线制:
+
+- **SCK**(Serial Clock)— 时钟线,由主机驱动
+- **MOSI**(Master Out Slave In)— 主机输出,从机输入
+- **MISO**(Master In Slave Out)— 主机输入,从机输出
+- **CS/NSS**(Chip Select)— 片选线,低电平有效,每个从机独占一根
+
+数据传输过程中,一个时钟周期同时完成一位数据的发送和接收:
+
+```text
+MOSI: 主机数据线 → 每个 SCK 周期移出一位
+MISO: 从机数据线 → 每个 SCK 周期移出一位
+全双工:收和发同时进行,互不干扰
+```
+
+### 时钟极性 (CPOL) 与时钟相位 (CPHA)
+
+SPI 有 5 个可配置参数(铁头山羊课程中的 5 参数模型):
+
+| # | 参数 | 说明 |
+|---|------|------|
+| 1 | **波特率** | SCK 频率,由主机分频产生 |
+| 2 | **数据位顺序** | MSB First / LSB First |
+| 3 | **数据位长度** | 8 位 / 16 位 |
+| 4 | **时钟极性 (CPOL)** | 空闲时 SCK 电平:0=低, 1=高 |
+| 5 | **时钟相位 (CPHA)** | 数据采样边沿:0=第一边沿, 1=第二边沿 |
+
+**CPOL(时钟极性)**:
+
+![CPOL 示意图](../assets/spi-cpol.png)
+
+- CPOL = 0:SCK 空闲时为低电平
+- CPOL = 1:SCK 空闲时为高电平
+
+**CPHA(时钟相位)** —— 数据传输分为"发送"和"采集"两个阶段:
+
+![CPHA 示意图](../assets/spi-cpha.png)
+
+- CPHA = 0:第一个边沿采集(采样),第二个边沿发送
+- CPHA = 1:第一个边沿发送,第二个边沿采集(采样)
+
+**4 种模式的完整时序**:
+
+![SPI 四种模式](../assets/spi-4-modes.png)
+
+| 模式 | CPOL | CPHA | 说明 |
+|------|------|------|------|
+| Mode 0 | 0 | 0 | 空闲低 → 上升沿采样(最常用) |
+| Mode 1 | 0 | 1 | 空闲低 → 下降沿采样 |
+| Mode 2 | 1 | 0 | 空闲高 → 下降沿采样 |
+| Mode 3 | 1 | 1 | 空闲高 → 上升沿采样 |
+
+## CubeMX 配置
+
+- **SPI1** → 模式 **Full-Duplex Master**
+- 参数:
+  - Frame Format:**Motorola**
+  - Data Size:**8 Bits**
+  - First Bit:**MSB First**
+  - Prescaler:**32**
+    - 默认 8MHz 系统时钟 → SCK = 8MHz/32 = **250kHz**
+    - 配置 72MHz 后 → SCK = 72MHz/32 = **2.25MHz**
+    - W25Q64 支持最高 104MHz,2.25MHz 完全没问题
+  - CPOL:**Low**
+  - CPHA:**1 Edge**
+  - NSS:**Software**(软件控制 CS)
+- 额外将 **PA4** 设为 **GPIO_Output**(用作软件 CS)
+
+## 关键 HAL API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_SPI_Transmit(&hspi, pData, Size, Timeout)` | 发送数据 |
+| `HAL_SPI_Receive(&hspi, pData, Size, Timeout)` | 接收数据(内部发 0xFF 产生时钟) |
+| `HAL_SPI_TransmitReceive(&hspi, pTx, pRx, Size, Timeout)` | 同时收发 |
+
+## W25Q64 操作(完整示例)
+
+```c
+/* CS 控制 */
+#define W25Q64_CS_LOW()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
+#define W25Q64_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)
+
+/* 写使能 (0x06) */
+static void W25Q64_WriteEnable(void)
+{
+    uint8_t cmd = 0x06;
+    W25Q64_CS_LOW();
+    HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
+    W25Q64_CS_HIGH();
+}
+
+/* 读状态寄存器 (0x05) */
+static uint8_t W25Q64_ReadSR(void)
+{
+    uint8_t cmd[2] = {0x05, 0xFF};
+    uint8_t rx[2] = {0};
+    W25Q64_CS_LOW();
+    HAL_SPI_TransmitReceive(&hspi1, cmd, rx, 2, HAL_MAX_DELAY);
+    W25Q64_CS_HIGH();
+    return rx[1];
+}
+
+/* 等待 BUSY 位清零(读状态寄存器 bit0) */
+static void W25Q64_WaitBusy(void)
+{
+    uint8_t sr;
+    do {
+        uint8_t cmd[2] = {0x05, 0xFF};      // 读状态寄存器 1
+        uint8_t rx[2] = {0};
+        W25Q64_CS_LOW();
+        HAL_SPI_TransmitReceive(&hspi1, cmd, rx, 2, HAL_MAX_DELAY);
+        W25Q64_CS_HIGH();
+        sr = rx[1];
+    } while (sr & 0x01);                    // bit0=1 表示正在忙
+}
+
+/* 扇区擦除 (4KB, 0x20) — 地址自动按 4KB 对齐 */
+static void W25Q64_SectorErase(uint32_t addr)
+{
+    uint8_t cmd[4] = {0x20, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
+    W25Q64_WriteEnable();
+    W25Q64_CS_LOW();
+    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
+    W25Q64_CS_HIGH();
+    W25Q64_WaitBusy();                      // 比固定延时更可靠
+}
+
+/* 页写入 (256字节以内, 0x02) — 地址必须按 256 字节对齐 */
+static void W25Q64_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
+{
+    uint8_t cmd[4] = {0x02, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
+    if (len > 256) len = 256;               // 页编程最多 256 字节
+    W25Q64_WriteEnable();
+    W25Q64_CS_LOW();
+    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
+    HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
+    W25Q64_CS_HIGH();
+    W25Q64_WaitBusy();
+}
+
+/* 读取数据 (0x03) */
+static void W25Q64_ReadData(uint32_t addr, uint8_t *buf, uint16_t len)
+{
+    uint8_t cmd[4] = {0x03, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
+    W25Q64_CS_LOW();
+    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
+    HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
+    W25Q64_CS_HIGH();
+}
+```
+
+## 按键 + Flash 保存 LED 状态
+
+```c
+uint8_t pre_key = 1, cur_key = 1;
+uint8_t led_status;
+
+led_status = LoadLEDState();               // 上电从 Flash 读取
+HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
+    led_status ? GPIO_PIN_SET : GPIO_PIN_RESET);
+
+while (1)
+{
+    pre_key = cur_key;
+    cur_key = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) ? 0 : 1;
+
+    if (pre_key != cur_key)
+    {
+        HAL_Delay(10);
+        if (cur_key == 1)                   // 松开沿
+        {
+            led_status = !led_status;
+            HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13,
+                led_status ? GPIO_PIN_SET : GPIO_PIN_RESET);
+            SaveLEDState(led_status);       // 保存到 Flash
+        }
+    }
+}
+```
+
+## 常见坑点
+
+- ⚠️ SPI 的 NSS **用软件控制**更简单,PA4 作为普通 GPIO 拉高拉低
+- ⚠️ W25Q64 写入前必须先擦除(只能 1→0,不能 0→1)
+- ⚠️ 每次写操作前必须发送 **写使能命令 (0x06)**
+- ⚠️ 分频后波特率不要超过从机支持的最大频率

+ 127 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/07-中断系统.md

@@ -0,0 +1,127 @@
+# 07 中断系统(HAL)
+
+> 目标:理解优先级分组与嵌套,实现 EXTI 外部中断和 UART 中断接收
+
+## 中断优先级
+
+### 优先级分组
+
+![中断优先级分组](../assets/interrupt-priority-group.png)
+
+NVIC 优先级分组(由 `SCB->AIRCR` 寄存器的 PRIGROUP 位决定)。STM32 把中断优先级分为**抢占优先级**和**子优先级**两部分,但两者的位数比例可以调整:
+
+| 分组 | 抢占优先级位数 | 子优先级位数 | 可配置优先级数量 |
+|------|--------------|-------------|----------------|
+| `NVIC_PRIORITYGROUP_0` | 0 | 4 | 抢占: 1种, 子: 16级 |
+| `NVIC_PRIORITYGROUP_1` | 1 | 3 | 抢占: 2种, 子: 8级 |
+| `NVIC_PRIORITYGROUP_2` | 2 | 2 | 抢占: 4种, 子: 4级 |
+| `NVIC_PRIORITYGROUP_3` | 3 | 1 | 抢占: 8种, 子: 2级 |
+| `NVIC_PRIORITYGROUP_4`(默认) | 4 | 0 | 抢占: 16种, 子: 1级 |
+
+> **数值越小,优先级越高!**("越小越优先"是 ARM Cortex 的统一规则)
+
+### 中断排队 vs 中断嵌套
+
+![中断排队与嵌套](../assets/interrupt-example.png)
+
+**中断排队**:多个中断等待 CPU 执行,NVIC 根据优先级决定谁先执行。
+
+**中断嵌套**:一个中断正在执行时,另一个更高优先级的中断到来,暂停当前中断去执行更高的中断。
+
+**判定规则**:
+
+```text
+抢占优先级不同 → 高抢占优先级可以嵌套低抢占优先级的中断
+抢占优先级相同 → 不嵌套,按子优先级排队
+子优先级也相同 → 按中断向量号(硬件固定),号小的先执行
+```
+
+### 完整示例
+
+优先级分组 Group 2(2位抢占 + 2位子优先级)。五个中断按到达时间排列:
+
+| 中断 | 抢占优先级 | 子优先级 | 到达顺序 |
+|-----|-----------|---------|---------|
+| A | 1 | 0 | ① |
+| B | 0 | 1 | ② |
+| C | 0 | 2 | ③ |
+| D | 2 | 0 | ④ |
+| E | 3 | 0 | ⑤ |
+
+**执行流**(数值越小优先级越高):
+
+```text
+① A(抢占1) 开始执行
+   └─ ② B(抢占0) 到来 → 0 > 1 → **嵌套!暂停 A,B 开始**
+      └─ ③ C(抢占0, 子2) 到来 → 同组不嵌套 → 排队(子2 < B的子1,C在B之后)
+      └─ B 完成 → C 开始执行
+   └─ C 完成 → **回到 A**(A 被抢占后恢复)
+   └─ ④ D(抢占2) 到来 → 2 < 1 → 排队(D在A后面)
+   └─ A 完成 → D 开始 → ⑤ E(抢占3) 在 D 后面排队
+
+最终执行顺序:A → B → C → A(恢复) → D → E
+```
+
+## NVIC 配置
+
+CubeMX → **NVIC Settings** 中:
+
+1. 使能要用到的中断
+2. 设置抢占优先级 (Preemption Priority) 和子优先级 (Sub Priority)
+3. `HAL_NVIC_SetPriority(IRQn, PreemptPriority, SubPriority)` 或在 CubeMX 中图形化设置
+
+## EXTI 外部中断
+
+### CubeMX 配置
+
+- PB0 → **GPIO_EXTI0**
+- GPIO 参数:Pull-up
+- **NVIC → EXTI line0 interrupt** → Enable
+- GPIO mode:**External Interrupt Mode with Rising/Falling edge detection**
+
+### 关键函数
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_GPIO_EXTI_IRQHandler(PIN)` | ISR 中调用,检测边沿并调回调 |
+| `HAL_GPIO_EXTI_Callback(PIN)` | 用户重写此回调(弱函数) |
+
+### 完整代码
+
+```c
+/* stm32f1xx_it.c — 中断服务函数 */
+void EXTI0_IRQHandler(void)
+{
+    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
+}
+
+/* main.c — 回调函数 */
+void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
+{
+    if (GPIO_Pin == GPIO_PIN_0)
+    {
+        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+    }
+}
+```
+
+## UART 中断接收
+
+已在 `04-UART串口.md` 详细说明,核心模式:
+
+```
+HAL_UART_Receive_IT() 启动中断接收
+         ↓
+硬件接收 1 字节 → 触发中断
+         ↓
+HAL_UART_RxCpltCallback() 被调用
+         ↓
+用户在回调中重新调用 HAL_UART_Receive_IT()
+```
+
+## 常见坑点
+
+- ⚠️ 中断服务函数写在 `stm32f1xx_it.c`,不要直接修改
+- ⚠️ 用户只重写回调函数(`__weak` 弱函数),不要改 ISR
+- ⚠️ 回调函数中**不要使用 `HAL_Delay()`**(它依赖 SysTick 中断,同优先级可能导致死锁)
+- ⚠️ 中断函数应**短小、快速执行**,耗时操作放主循环

+ 109 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/08-时钟树.md

@@ -0,0 +1,109 @@
+# 08 时钟树配置(HAL)
+
+> 目标:理解时钟源结构,配置 SYSCLK = 72MHz(最大频率)
+
+## 单片机内部结构
+
+![MCU 内部结构](../assets/mcu-internal.png)
+
+STM32 内部包含三大核心部件:**CPU 核心**(Cortex-M3)、**存储器**(Flash + SRAM)、**外设总线**(AHB + APB1 + APB2)。时钟树的任务就是把外部/内部时钟源通过 PLL 倍频、分频器分频后,分配到每个部件。
+
+## 时钟源
+
+STM32 有 4 个独立的时钟源:
+
+| 时钟源 | 符号 | 典型频率 | 精度 | 用途 |
+|--------|------|---------|------|------|
+| 高速内部振荡器 | **HSI** | 8MHz | 较差 (±1%) | 上电默认时钟,可作 PLL 输入 |
+| 高速外部晶振 | **HSE** | 4~16MHz | 精确 (±0.01%) | 常用 8MHz 作为 PLL 输入源 |
+| 低速内部振荡器 | **LSI** | 40kHz | 较差 | 独立看门狗 (IWDG)、RTC 备用 |
+| 低速外部晶振 | **LSE** | 32.768kHz | 精确 | RTC 专用时钟 |
+
+## 时钟树结构
+
+![STM32 时钟树](../assets/clock-tree.png)
+
+时钟树的本质是多路选择器 (MUX) + 锁相环 (PLL) + 分频器 (Divider) 组成的时钟分配网络:
+
+```text
+HSI (8MHz) ──┐
+              ├──→ MUX ──→ SYSCLK ──→ AHB Div ──→ HCLK (CPU, Flash, SRAM)
+HSE (8MHz) ──┘                              │
+                  ↑                           ├──→ APB1 Div ──→ PCLK1 (≤36MHz)
+                PLL x9                        │    外设: TIM2~4, I2C1~2, USART2, etc.
+              (8×9=72MHz)                     └──→ APB2 Div ──→ PCLK2 (≤72MHz)
+                                                   外设: GPIO, USART1, SPI1, ADC1, TIM1
+```
+
+- **SYSCLK**:系统时钟,CPU 运行的主时钟
+- **HCLK**:AHB 总线时钟,CPU、Flash、SRAM、DMA 共享
+- **PCLK1**:APB1 总线时钟,低速外设(≤36MHz)
+- **PCLK2**:APB2 总线时钟,高速外设(≤72MHz)
+
+### ⚠️ 定时器时钟 = 2× 总线时钟
+
+APB 预分频器有一个隐藏行为:
+
+```text
+APB1 预分频 = 1  → TIM 时钟 = PCLK1(36MHz → 定时器 36MHz)  ❌ 本配置
+APB1 预分频 ≠ 1  → TIM 时钟 = 2 × PCLK1(36MHz → 定时器 72MHz) ✅ 配 /2 时
+```
+
+当 APB1 分频为 **/2** 时(72MHz 配置),虽然 PCLK1 = 36MHz,但挂载在 APB1 上的定时器(TIM2/3/4/5/6/7)实际时钟为 **2 × 36 = 72MHz**。配置定时器时分频要按 72MHz 计算。
+
+同理 APB2 分频 ≠ 1 时(如/2),TIM1/TIM8 也得到 2× 时钟。**72MHz 配置下 APB2=/1,所以 TIM1/TIM8 时钟就是 72MHz。**
+
+## CubeMX 配置(8MHz HSE → 72MHz)
+
+1. **RCC → HSE:Crystal/Ceramic Resonator**
+2. 切到 **Clock Configuration** 页面
+3. 配置路径:
+   - HSE:8MHz
+   - PLL Source:**HSE**
+   - PLL Mul:**x9**(8 × 9 = 72MHz)
+   - System Clock Mux:**PLLCLK**
+   - AHB Prescaler:**/1** → HCLK = 72MHz
+   - APB1 Prescaler:**/2** → PCLK1 = 36MHz(≤36MHz ✓)
+   - APB2 Prescaler:**/1** → PCLK2 = 72MHz(≤72MHz ✓)
+4. CubeMX 中红色数字表示超限,需调分频
+
+## Flash 等待周期
+
+HCLK 频率与 Flash 等待周期的关系:
+
+| HCLK | 等待周期 |
+|------|---------|
+| 0~24MHz | 0 wait (WS=0) |
+| 24~48MHz | 1 wait (WS=1) |
+| 48~72MHz | 2 wait (WS=2) |
+
+> CubeMX 会自动计算并配置 `FLASH_ACR` 寄存器,不需要手动设置。
+
+## 验证方法
+
+```c
+while (1)
+{
+    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+    for (volatile uint32_t i = 0; i < 1000000; i++);  // 软件延时
+}
+```
+
+对比默认 8MHz 和配置后 72MHz:
+- 8MHz:LED 闪烁较慢
+- 72MHz:LED 闪烁快约 9 倍
+
+## CubeMX ↔ Keil 同步更新
+
+CubeMX 修改时钟后,Keil 需同步:
+
+1. CubeMX **GENERATE CODE** → 点 Close
+2. Keil 弹出提示文件已修改 → **是**(全部重新加载)
+3. 重新编译烧录
+
+## 常见坑点
+
+- ⚠️ APB1 最高 **36MHz**,APB2 最高 **72MHz**,不能超限
+- ⚠️ HSE 若不起振(晶振/电容问题),硬件自动回退到 HSI 8MHz(但 CubeMX 配置可能不生效)
+- ⚠️ 外设时钟来自 APB1/APB2,配置前确认外设挂在哪条总线上
+- ⚠️ 修改时钟后 SysTick 的计数值也被改变,`HAL_Delay` 仍正常工作

+ 295 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/09-定时器.md

@@ -0,0 +1,295 @@
+# 09 定时器(HAL)
+
+> 目标:掌握时基单元、PWM、输入捕获、编码器模式
+
+---
+
+## 定时器整体框图
+
+![定时器框图](../assets/timer-block.png)
+
+STM32 定时器是一个复杂的模块,从输入到输出包含:**时钟源 → 时基单元 → 输出/输入通道 → 从模式控制**。高级定时器(TIM1、TIM8)还包含死区生成、刹车输入等电机控制功能。
+
+### ⚠️ 定时器时钟源
+
+定时器实际时钟 = `2 × PCLKx`(当 APBx 预分频 > 1 时):
+
+```text
+72MHz 配置下(APB1=/2, APB2=/1):
+  TIM2/3/4/5 时钟 = 2 × PCLK1 = 2 × 36MHz = 72MHz
+  TIM1/TIM8 时钟 = PCLK2 = 72MHz(APB2=/1,不分频,时钟不变)
+```
+
+配置时基时用 **定时器时钟** 计算分频,而非 PCLK 总线频率。
+
+本笔记依次讲解:时基单元 → 输出比较/PWM → 输入捕获 → 从模式 → 编码器。
+
+---
+
+## 9.1 时基单元
+
+### 时基单元结构
+
+![时基单元](../assets/timer-timebase.png)
+
+时基单元是定时器的"心脏",由 4 个核心寄存器组成:
+
+```text
+           PSC                   CNT                   ARR
+  Clock ──→ /(PSC+1) ──→ 计数器(递增/减) ──→ 比较 = ARR ──→ 更新事件(UEV)
+                                ↑                           │
+                                └────────── 清零重来 ────────┘
+
+频率计算:
+  CK_CNT = TIM_CLK / (PSC + 1)
+
+定时周期(向上计数模式):
+  T = (PSC + 1) × (ARR + 1) / TIM_CLK
+```
+
+| 参数 | 功能 | 示例值 (1ms, 72MHz) |
+|------|------|-------------------|
+| PSC (预分频器) | 对 TIM_CLK 分频 | 71 → CK_CNT = 1MHz |
+| CNT (计数器) | 每个时钟递增/减 | 从 0 到 ARR |
+| ARR (自动重装) | 计数目标值 | 999 → (999+1)×1µs = 1ms |
+| RCR (重复计数器) | 高级定时器专用,控制 N 次 UEV 触发一次中断 | — |
+
+### 影子寄存器(预加载)
+
+> 上图中阴影部分即为影子寄存器。
+
+**影子寄存器**:PSC 和 ARR 写入后不会立即生效,而是先写入"预加载寄存器",等当前定时周期结束时(发生更新事件 UEV),影子寄存器才从预加载寄存器加载新值。
+
+**为什么要预加载?**
+- 避免在计数过程中突然改变 PSC/ARR 导致异常波形
+- 确保周期变更在"安全的边界时刻"生效
+
+**预加载可关闭**:在 CubeMX 中取消勾选 `auto-reload preload` 后,写入 ARR 立即生效(不推荐,除非你清楚后果)。
+
+### CubeMX 配置
+
+- TIM2 → **Internal Clock**
+- Prescaler (PSC):**71**
+- Counter Period (ARR):**999**
+- auto-reload preload:**Enable**
+- NVIC → TIM2 global interrupt → **Enable**
+
+### 关键 API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_TIM_Base_Start(&htim)` | 启动定时器(轮询) |
+| `HAL_TIM_Base_Start_IT(&htim)` | 启动定时器(中断) |
+| `HAL_TIM_PeriodElapsedCallback(&htim)` | 更新中断回调 |
+
+### 完整代码
+
+```c
+void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
+{
+    if (htim == &htim2)
+        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
+}
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_TIM2_Init();
+    HAL_TIM_Base_Start_IT(&htim2);
+
+    while (1) { /* 主循环休眠,中断驱动 */ }
+}
+```
+
+---
+
+## 9.2 输出比较 & PWM
+
+### 原理
+
+```text
+PWM Mode 1(向上计数):
+  CNT < CCR  → 输出有效电平(高)
+  CNT ≥ CCR  → 输出无效电平(低)
+  CNT = ARR  → 清零重来
+
+占空比 = CCR / (ARR + 1)
+频率   = TIM_CLK / (PSC+1) / (ARR+1)
+```
+
+```text
+CCR=3  —— 比较值
+ARR=9  —— 自动重装值
+
+CNT:  0  1  2  3  4  5  6  7  8  9 | 0  1  2  3  4  5  6 ...
+      ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑  ↑ | ↑  ↑  ↑  ↑  ↑  ↑  ↑
+OUT:  █  █  █  ░  ░  ░  ░  ░  ░  ░ | █  █  █  ░  ░  ░  ░
+      └──高──┘  └───────低────────┘ | └──高──┘  └──低───
+      占空比 = 3/10 = 30%
+```
+
+呼吸灯示例:PWM 频率 1kHz(不可见闪烁),占空比从 0→100%→0 循环。
+
+### CubeMX 配置
+
+- TIM2 Channel 1 → **PWM Generation CH1**
+- PSC = **71**, ARR = **999**, Pulse (CCR) = **0**
+- 引脚 PA0
+
+### 关键 API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_TIM_PWM_Start(&htim, Channel)` | 启动 PWM |
+| `__HAL_TIM_SET_COMPARE(&htim, Channel, CCR)` | 修改占空比 |
+
+### 呼吸灯代码
+
+```c
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_TIM2_Init();
+    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
+
+    uint16_t duty = 0;
+    int8_t dir = 1;
+
+    while (1)
+    {
+        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
+        HAL_Delay(5);
+
+        if (dir == 1) duty += 10;
+        else          duty -= 10;
+
+        if (duty >= 999) dir = -1;
+        if (duty == 0)   dir = 1;
+    }
+}
+```
+
+---
+
+## 9.3 输入捕获
+
+### 原理
+
+```text
+外部信号 → TIx 引脚 → 滤波 → 边沿检测 → 捕获 CNT → 存入 CCR
+
+频率测量:
+  第 1 次上升沿 → 捕获 CCR1
+  第 2 次上升沿 → 捕获 CCR2
+  周期 = CCR2 - CCR1(注意溢出处理)
+```
+
+### CubeMX 配置
+
+- TIM2 Channel 1 → **Input Capture direct mode**
+- PSC = 71 → CK_CNT = 1MHz (1µs 分辨率)
+- ARR = 65535(最大范围)
+- 引脚 PA0
+
+### 关键 API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_TIM_IC_Start_IT(&htim, Channel)` | 启动输入捕获 |
+| `HAL_TIM_IC_CaptureCallback(&htim)` | 捕获完成回调 |
+
+```c
+void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
+{
+    if (htim == &htim2)
+    {
+        uint32_t val = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_1);
+        // 计算频率/脉宽
+    }
+}
+```
+
+---
+
+## 9.4 从模式控制器
+
+### 原理
+
+```text
+主定时器 → TRGO (触发输出) → 从定时器的触发输入
+
+常见组合:
+  TIM1 主 (PWM 输出) → TRGO = 更新事件
+  TIM2 从 (从模式复位) → 每次 TIM1 更新时 CNT 复位
+
+用途:精确测量 PWM 参数
+  TIM1 CH1 输入捕获 → 捕获周期
+  TIM1 从模式复位 → 每次上升沿自动复位 CNT
+```
+
+### CubeMX 配置
+
+- TIM1:
+  - Slave Mode:**Trigger Mode**
+  - Trigger Source:**ITR0**(取决于哪个定时器做主)
+  - Channel 1:**Input Capture direct mode**
+
+---
+
+## 9.5 编码器模式
+
+### 原理
+
+```text
+正交编码器 → TI1 (A相) + TI2 (B相)
+
+定时器自动判断正反转:
+  A 相领先 B 相 → 正转 (CNT++)
+  B 相领先 A 相 → 反转 (CNT--)
+
+计数模式:
+  ×1:仅 TI1 边沿计数
+  ×2:TI1 + TI2 边沿计数
+  ×4:所有边沿计数(最高分辨率)
+```
+
+### CubeMX 配置
+
+- TIM3 → Combined Channels:**Encoder Mode TI1 and TI2**
+- PSC = **0**(不分频,直接计数)
+- ARR = **65535**(16 位最大范围)
+- Encoder Mode:**TI1 and TI2** (×4)
+
+### 代码示例
+
+```c
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_TIM3_Init();
+    HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
+
+    int16_t position;
+    while (1)
+    {
+        position = (int16_t)__HAL_TIM_GET_COUNTER(&htim3);
+        // 通过串口打印 position 值
+        HAL_Delay(50);
+    }
+}
+```
+
+---
+
+## 常见坑点
+
+- ⚠️ PSC 和 ARR 是 **16 位**寄存器(TIM2 在某些系列是 32 位),注意数值范围
+- ⚠️ 修改 PSC/ARR 后,等待**更新事件 (UEV)** 影子寄存器才加载
+- ⚠️ 输出比较修改 CCR 后立即生效(无预加载)
+- ⚠️ 编码器模式 CNT 读出来要转为 `int16_t` 才能正确表示正负
+- ⚠️ 输入捕获注意**溢出处理**:两次捕获之间 CNT 可能溢出回零

+ 234 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-HAL库笔记/10-ADC.md

@@ -0,0 +1,234 @@
+# 10 ADC(HAL)
+
+> 目标:单通道 ADC 采集 + 定时器触发注入通道
+
+## 原理简述
+
+### 逐次逼近型 (SAR) ADC 工作原理
+
+```text
+模拟输入 → 采样保持 → 比较器 ←── DAC (逐次逼近)
+                           │
+                    逐次逼近寄存器 (SAR)
+                           │
+                       ┌───┴───┐
+                       │ 数据寄存器
+                       └───────┘
+
+SAR 转换过程(以 3 位为例):
+  1. 采样保持: 锁定输入电压 (Vin)
+  2. 比较 MSB: DAC=100(½Vref) → Vin > 100? → MSB=1(或 0)
+  3. 比较 next: DAC=110(¾Vref) → Vin > 110? → 确定第二位
+  4. 比较 LSB:  确定最后一位
+  5. 转换完成 → 存入数据寄存器
+
+STM32F103: 12 位 → 需要 12 个 SAR 比较周期
+```
+
+**关键参数**:
+- 分辨率:**12 位** → 数值范围 0~4095
+- 电压范围:**0V ~ VREF**(通常 VREF = VDDA = 3.3V)
+- 转换公式:`V_in = ADC_Value / 4095 × VREF`
+- 精度:**±1 LSB**(理想条件下 3.3V/4096 ≈ 0.8mV/位)
+
+### 规则通道 vs 注入通道
+
+STM32 ADC 有两组通道,可以理解为一个"常规任务"和一个"紧急插队任务":
+
+```text
+规则通道组 (Regular Group):
+  ┌─────┐   ┌─────┐   ┌─────┐
+  │CH0  │──→│CH1  │──→│...  │──→ 16 位数据寄存器 (DR)
+  └─────┘   └─────┘   └─────┘
+  最多 16 个通道按顺序转换,结果都进同一个 DR
+  (多通道时必须用 DMA,否则后一个覆盖前一个)
+
+注入通道组 (Injected Group):
+  ┌─────┐
+  │CH0  │──→ 注入数据寄存器 (JDR1~4)
+  └─────┘   最多 4 个通道,各占独立寄存器
+  可以打断规则组正在进行的转换
+```
+
+### 扫描模式 vs 连续模式
+
+```text
+单次/单通道:  触发 → 转换 CH0 → 停止(常用)
+连续/单通道:  触发 → 转换 CH0 → 自动再转 CH0 → 一直转
+单次/多通道:  触发 → CH0 → CH1 → CH2 → 停止
+连续/多通道:  触发 → CH0→CH1→...→CHn→再CH0→...(一直转)
+```
+
+### 数据对齐
+
+```text
+12 位结果存在 16 位寄存器中,可选择对齐方式:
+
+右对齐(常用):
+  DR:  [15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]
+                                   ↑  DATA[11:0]  ↑
+                                   └──────────────┘
+                       实际值 = DR(直接可用)
+
+左对齐(某些滤波场景用):
+  DR:  [15][14][13][12][11][10][9][8][7][6][5][4][3][2][1][0]
+        ↑  DATA[11:0]  ↑
+        └──────────────┘
+                       实际值 = DR >> 4
+```
+
+## 采样时间与转换时间
+
+```text
+总转换时间 = 采样时间 + 12.5 个 ADC 时钟周期
+
+ADC 时钟 (ADCCLK) 来自 APB2 分频,必须 ≤ 14MHz:
+
+  APB2=72MHz → 分频 2→36MHz(✗超限)  → 分频 4→18MHz(✗超限)
+              → 分频 6→12MHz(✓)      → 分频 8→9MHz(✓)
+
+采样时间可选:1.5 / 7.5 / 13.5 / 28.5 / 41.5 / 55.5 / 71.5 / 239.5 周期
+
+示例 (72MHz, 分频=6, 采样=1.5):
+  ADCCLK = 72/6 = 12MHz
+  总时间 = (1.5 + 12.5) / 12MHz ≈ 1.17µs
+  每秒采样 ≈ 856k samples/s
+
+采样时间越短 → 越快,但对信号源内阻敏感
+采样时间越长 → 越准,适合高内阻信号源(如分压电阻网络)
+```
+
+## 校准
+
+ADC 每次上电后需要校准,消除内部电容阵列的偏差。CubeMX 生成的代码会自动完成:
+
+```text
+HAL_ADC_Start(&hadc1) 的内部流程:
+  1. 检查是否已校准,若未校准则自动校准
+  2. 校准完成
+  3. 开始转换
+```
+
+> 如果需要手动校准:`HAL_ADCEx_Calibration_Start(&hadc1)`(部分 HAL 版本)
+
+## 单通道转换(软件触发)
+
+### CubeMX 配置
+
+- **ADC1 → IN0 (PA0)**
+- 参数:
+  - Clock Prescaler:**PCLK2 divided by 6**(ADCCLK = 12MHz)
+  - Resolution:**12 bits**
+  - Scan Conversion Mode:**Disabled**(单通道)
+  - Continuous Conversion Mode:**Disabled**(单次转换)
+  - Data Alignment:**Right alignment**
+- Regular Conversion → Channel:**0**, Sampling Time:**1.5 Cycles**
+
+### 关键 API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_ADC_Start(&hadc)` | 启动一次转换(含自动校准) |
+| `HAL_ADC_PollForConversion(&hadc, Timeout)` | 轮询等待转换完成 |
+| `HAL_ADC_GetValue(&hadc)` | 读取 12 位转换结果 |
+
+### 完整代码
+
+```c
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_ADC1_Init();
+
+    while (1)
+    {
+        HAL_ADC_Start(&hadc1);                                    // 启动转换
+        if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK)     // 等完成
+        {
+            uint16_t val = HAL_ADC_GetValue(&hadc1);              // 读结果 (0~4095)
+            float v = (float)val / 4095.0f * 3.3f;                // 换算电压
+            printf("ADC=%d, V=%.2f\r\n", val, v);                 // 需重定向 printf
+        }
+        HAL_Delay(500);
+    }
+}
+```
+
+## 定时器触发转换(注入通道)
+
+### 原理
+
+```text
+定时器 TIM3 (TRGO=Update)
+       │
+       │  每过 N ms 触发一次
+       ↓
+ADC 注入通道 ──→ 打断正在进行的常规转换
+             ──→ 执行注入通道转换
+             ──→ 恢复常规转换
+```
+
+优势:精确的采样间隔、CPU 不需要空等轮询。
+
+### CubeMX 配置
+
+- ADC1 → **IN0 (PA0)**
+- **Injected Conversion Mode** → **Enable**
+- External Trigger Conversion Source:**Timer 3 Trigger Out**
+- External Trigger Conversion Edge:**Rising Edge**
+- NVIC Settings → **ADC1 and ADC2 global interrupt** → Enable
+- TIM3 → **Prescaler** 和 **Counter Period** 决定触发间隔
+  - 例如 PSC=7199, Period=4999 → 72MHz/7200/5000 = 2Hz
+- TIM3 → **TRGO** → **Update Event**
+
+### 关键 API
+
+| 函数 | 说明 |
+|------|------|
+| `HAL_ADCEx_InjectedStart(&hadc)` | 启动注入通道(等待触发) |
+| `HAL_ADCEx_InjectedGetValue(&hadc, InjectedRank)` | 读取注入结果 |
+| `HAL_ADCEx_InjectedConvCpltCallback(&hadc)` | 注入完成回调 |
+
+```c
+/* 注入转换完成回调 —— 由 HAL 在 ADC 中断中调用 */
+void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
+{
+    if (hadc == &hadc1)
+    {
+        uint32_t val = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
+        // 处理结果(中断中应快速处理,不要用 HAL_Delay)
+    }
+}
+```
+
+## ADC + DMA(多通道/连续采集)
+
+当需要连续采集多通道时,必须使用 DMA 把结果自动搬走,否则新数据会覆盖旧数据:
+
+```c
+#define ADC_BUF_LEN  16
+uint32_t adc_buf[ADC_BUF_LEN];          // DMA 缓冲区
+
+HAL_ADC_Start_DMA(&hadc1, adc_buf, ADC_BUF_LEN);
+// DMA 自动把每次转换结果搬入 adc_buf
+// 填满后调用 HAL_ADC_ConvCpltCallback()
+
+void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
+{
+    // adc_buf 已填满,可以处理
+    for (int i = 0; i < ADC_BUF_LEN; i++) {
+        printf("CH%d = %d\r\n", i, adc_buf[i]);
+    }
+}
+```
+
+## 常见坑点
+
+- ⚠️ ADC 输入电压**绝对不能超过 VREF**(通常 VDDA = 3.3V),否则损坏 ADC
+- ⚠️ ADCCLK **必须 ≤ 14MHz**,否则转换结果不准确(CubeMX 用红色警告)
+- ⚠️ 多通道扫描但**不使用 DMA** → 后面的结果会覆盖前面的,只能读到最后一个通道
+- ⚠️ 注入通道优先级高于常规通道,可打断常规转换。高频率注入会导致常规通道**饥饿**
+- ⚠️ `HAL_ADC_PollForConversion` 返回 `HAL_TIMEOUT` → 检查 ADC 时钟、校准是否完成
+- ⚠️ 连续模式(Continuous Conversion)开启后,ADC 会不停地转,功耗较高

+ 392 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/01-环境搭建.md

@@ -0,0 +1,392 @@
+# 01-环境搭建
+
+## STM32 简介
+
+| 项目 | 参数 |
+|------|------|
+| 内核 | ARM Cortex-M3 (ARMv7-M) |
+| 主频 | 72MHz (HSE 8MHz PLL 9倍频) |
+| SRAM | 20KB |
+| Flash | 64KB |
+| 封装 | LQFP48 |
+| 电压 | 2.0V~3.6V (典型 3.3V) |
+| 温度 | -40℃~+85℃ |
+
+Cortex-M3 特性:Thumb-2 指令集、硬件乘除法、64MHz→72MHz PLL、位带操作、SysTick 定时器、NVIC 中断嵌套。
+
+## 片上外设总览
+
+```mermaid
+graph TD
+    subgraph CORE
+        ARM_CORTEX_M3["ARM Cortex-M3<br/>72MHz"]
+    end
+    
+    subgraph BUS
+        ICODE["ICode Bus<br/>(Flash Instruction)"]
+        DCODE["DCode Bus<br/>(Flash Data)"]
+        SYSTEM["System Bus<br/>(SRAM/FSMC)"]
+        AHB["AHB<br/>72MHz"]
+        APB2["APB2<br/>72MHz"]
+        APB1["APB1<br/>36MHz"]
+    end
+    
+    subgraph PERIPHERALS_APB2
+        GPIO_A["GPIO A/B/C/D/E"]
+        USART1["USART1"]
+        SPI1["SPI1"]
+        TIM1["TIM1 (高级)"]
+        ADC1["ADC1/2"]
+        EXTI["EXTI"]
+        AFIO["AFIO"]
+    end
+    
+    subgraph PERIPHERALS_APB1
+        TIM2_4["TIM2/3/4 (通用)"]
+        TIM5_7["TIM5/6/7"]
+        USART2_3["USART2/3"]
+        SPI2_3["SPI2/3"]
+        I2C1_2["I2C1/2"]
+        PWR["PWR"]
+        BKP["BKP"]
+        IWDG["IWDG"]
+        WWDG["WWDG"]
+        RTC["RTC"]
+        DMA["DMA"]
+        CAN["CAN"]
+        USB["USB"]
+    end
+    
+    ARM_CORTEX_M3 --> ICODE
+    ARM_CORTEX_M3 --> DCODE
+    ARM_CORTEX_M3 --> SYSTEM
+    SYSTEM --> AHB
+    AHB --> APB2
+    AHB --> APB1
+    APB2 --> PERIPHERALS_APB2
+    APB1 --> PERIPHERALS_APB1
+```
+
+### 系统总线结构
+
+| 总线 | 频率 | 连接 |
+|------|------|------|
+| ICode | 72MHz | Cortex-M3 → Flash(指令) |
+| DCode | 72MHz | Cortex-M3 → Flash(数据) |
+| System | 72MHz | Cortex-M3 → SRAM / FSMC |
+| AHB | 72MHz | 桥接 APB2 / APB1 / DMA / RCC |
+| APB2 | 72MHz | 高速外设 (GPIO, USART1, SPI1, TIM1, ADC, EXTI, AFIO) |
+| APB1 | 36MHz | 低速外设 (TIM2~7, USART2/3, SPI2/3, I2C1/2, PWR, BKP, IWDG, WWDG, RTC, DMA, CAN, USB) |
+
+> 注意:APB1 最高 36MHz,如需 72MHz 必须使用 APB2。
+
+## 引脚定义 (LQFP48)
+
+```mermaid
+graph TD
+    subgraph LQFP48_top
+        direction LR
+        PD0["PD0 (OSC_IN)"] --- PD1["PD1 (OSC_OUT)"]
+        PD1 --- NRST["NRST"]
+        NRST --- PC13["PC13 (TAMPER-RTC)"]
+    end
+```
+
+### 功能引脚速查
+
+| 引脚 | 默认功能 | 复用功能 |
+|------|----------|----------|
+| PA0 | GPIO | TIM2_CH1_ETR / USART2_CTS / WKUP / ADC12_IN0 |
+| PA1 | GPIO | TIM2_CH2 / USART2_RTS / ADC12_IN1 |
+| PA2 | GPIO | TIM2_CH3 / USART2_TX / ADC12_IN2 |
+| PA9 | GPIO | USART1_TX / TIM1_CH2 |
+| PA10 | GPIO | USART1_RX / TIM1_CH3 |
+| PB6 | GPIO | I2C1_SCL / TIM4_CH1 |
+| PB7 | GPIO | I2C1_SDA / TIM4_CH2 |
+| PB8 | GPIO | I2C1_SCL / TIM4_CH3 |
+| PB9 | GPIO | I2C1_SDA / TIM4_CH4 |
+| PC13 | GPIO | RTC_TAMPER |
+
+### 完整引脚序号
+
+| 编号 | 引脚 | 编号 | 引脚 | 编号 | 引脚 | 编号 | 引脚 |
+|------|------|------|------|------|------|------|------|
+| 1 | PE2(VDD_1) | 13 | PA13(SWDIO) | 25 | PA0 | 37 | PB9 |
+| 2 | PE3(VSS_1) | 14 | PA14(SWCLK) | 26 | PA1 | 38 | PB8 |
+| 3 | NRST | 15 | PA15(JTDI) | 27 | PA2 | 39 | BOOT0 |
+| 4 | PC13 | 16 | PB3(JTDO) | 28 | ... | 40 | PB7 |
+| 5 | PC14(OSC32_IN) | 17 | PB4(JNTRST) | ... | ... | 41 | PB6 |
+| 6 | PC15(OSC32_OUT) | 18 | PB5 | ... | ... | ... | ... |
+
+## BOOT 启动配置
+
+| BOOT1 | BOOT0 | 启动区域 |
+|-------|-------|----------|
+| X | 0 | 主Flash (0x08000000) |
+| 0 | 1 | 系统存储器(System Loader, 0x1FFF0000) |
+| 1 | 1 | 内嵌SRAM (0x20000000) |
+
+实际使用:BOOT0=0 (接GND) 从Flash启动。ISP下载时:BOOT0=1, BOOT1=0。
+
+## 最小系统电路
+
+```mermaid
+graph TD
+    subgraph POWER
+        VDD33["VDD (3.3V)"] --> VDD1["VDD_1"]
+        VDD33 --> VDD2["VDD_2"]
+        VDD33 --> VDDA["VDDA"]
+        VSS1["VSS_1"] --> GND["GND"]
+        VSS2["VSS_2"] --> GND
+        VSSA["VSSA"] --> GND
+    end
+    
+    subgraph OSCILLATOR
+        HSE["8MHz HSE"] --> X1["X1<br/>PD0(OSC_IN)"]
+        HSE --> X2["X2<br/>PD1(OSC_OUT)"]
+        LSE["32.768kHz LSE"] --> X32_IN["PC14(OSC32_IN)"]
+        LSE --> X32_OUT["PC15(OSC32_OUT)"]
+    end
+    
+    subgraph RESET
+        NRST_PIN["NRST"] --> R10k["10kΩ → 3.3V"]
+        NRST_PIN --> C100nF["100nF → GND"]
+    end
+    
+    subgraph BOOT
+        BOOT0_PIN["BOOT0"] --> R10k_GND["10kΩ → GND"]
+    end
+```
+
+| 元件 | 参数 | 备注 |
+|------|------|------|
+| HSE 晶振 | 8MHz + 20pF ×2 | 主时钟源 |
+| LSE 晶振 | 32.768kHz + 12.5pF ×2 | RTC 时钟源 |
+| 复位电容 | 100nF | NRST 对地 |
+| 复位上拉 | 10kΩ | NRST 接 3.3V |
+| 去耦电容 | 100nF × N | 每个 VDD/VSS 对各放一个 |
+
+## 开发环境
+
+| 工具 | 用途 |
+|------|------|
+| Keil MDK v5.38+ | 编译环境 (AC5 / AC6) |
+| STM32F1xx_DFP | Keil 器件包 |
+| ST-Link Utility / STM32CubeProgrammer | 调试下载 |
+| CH340 / FT232 | 串口 ISP 下载 (FlyMcu) |
+| USB-TTL 模块 | 串口通信 |
+
+## 标准库工程搭建步骤
+
+### 1. 固件库文件下载
+
+从 ST 官网下载 `STM32F10x_StdPeriph_Lib_V3.5.0` 或更高版本。
+
+### 2. 目录结构
+
+```
+Project/
+├── CMSIS/
+│   ├── core_cm3.c
+│   ├── core_cm3.h
+│   ├── startup_stm32f10x_md.s
+│   ├── stm32f10x.h
+│   ├── system_stm32f10x.c
+│   └── system_stm32f10x.h
+├── STM32F10x_StdPeriph_Driver/
+│   ├── inc/              (所有外设头文件)
+│   └── src/              (所有外设源文件)
+├── User/
+│   ├── main.c
+│   ├── stm32f10x_conf.h
+│   ├── stm32f10x_it.c
+│   └── stm32f10x_it.h
+└── Output/               (编译输出)
+```
+
+### 3. 关键宏定义
+
+Keil C/C++ Compiler → Define:
+
+```
+USE_STDPERIPH_DRIVER, STM32F10X_MD
+```
+
+| 宏 | 说明 |
+|---|------|
+| `USE_STDPERIPH_DRIVER` | 启用标准外设驱动库 |
+| `STM32F10X_MD` | Medium Density (64KB~128KB Flash, C8T6 用此) |
+| `STM32F10X_HD` | High Density (256KB~512KB Flash) |
+| `STM32F10X_CL` | Connectivity Line (STM32F105/107) |
+
+### 4. 启动文件选择
+
+| Flash容量 | 芯片后缀 | 启动文件 |
+|-----------|----------|----------|
+| 16~32KB | C4/C6/T4/T6 | `startup_stm32f10x_ld.s` |
+| 64~128KB | C8/CB/T8/TB | `startup_stm32f10x_md.s` |
+| 256~512KB | RC/RE/RF/VC/VE/VF/ZC/ZE/ZF | `startup_stm32f10x_hd.s` |
+| 768KB~1MB | RG/VG/ZG | `startup_stm32f10x_xl.s` |
+
+**C8T6 → `startup_stm32f10x_md.s`(中容量)**
+
+### 5. Keil 分组设置
+
+```
+Project Targets:
+├── Startup
+│   └── startup_stm32f10x_md.s
+├── StdPeriph
+│   ├── misc.c
+│   ├── stm32f10x_gpio.c
+│   ├── stm32f10x_rcc.c
+│   ├── stm32f10x_tim.c
+│   ├── stm32f10x_usart.c
+│   ├── stm32f10x_exti.c
+│   ├── stm32f10x_i2c.c
+│   ├── stm32f10x_spi.c
+│   ├── stm32f10x_adc.c
+│   ├── stm32f10x_dma.c
+│   ├── stm32f10x_pwr.c
+│   ├── stm32f10x_bkp.c
+│   └── stm32f10x_flash.c
+├── User
+│   ├── main.c
+│   ├── stm32f10x_it.c
+│   ├── system_stm32f10x.c
+│   └── Delay.c
+└── Doc
+    └── readme.txt
+```
+
+### 6. C/C++ 头文件路径
+
+```
+$PROJ_DIR$\CMSIS
+$PROJ_DIR$\STM32F10x_StdPeriph_Driver\inc
+$PROJ_DIR$\User
+```
+
+### 7. 魔术棒设置
+
+| Tab | 设置 |
+|-----|------|
+| Device | STM32F103C8 |
+| Target | Xtal: 8.0MHz, Use Cross-Module Optimization |
+| Output | Select Folder for Objects... → Output |
+| Listing | Select Folder for Listings... → Output |
+| Debug | ST-Link Debugger → Settings → SW |
+
+## 启动文件流程
+
+```
+Reset_Handler
+  → SystemInit()           (system_stm32f10x.c)
+    → RCC配置: HSE→PLL(x9)→SYSCLK=72MHz
+    → AHB/APB1/APB2 分频器设置
+    → Flash 预取缓冲/等待周期配置
+  → __main                  (C库初始化)
+    → 变量初始化 (ZI/BSS清零, RW复制)
+    → 跳转 main()
+```
+
+### SystemInit 时钟树 (默认配置 HSE 8MHz)
+
+```
+HSE (8MHz)
+  → PLLSRC (HSE)
+  → PLLMUL (x9)
+  → PLLCLK = 8MHz × 9 = 72MHz
+  → SYSCLK = 72MHz
+  → AHB Prescaler (/1) → HCLK = 72MHz
+    → APB1 Prescaler (/2) → PCLK1 = 36MHz
+    → APB2 Prescaler (/1) → PCLK2 = 72MHz
+  → ADC Prescaler (/6) → ADCCLK = 12MHz
+```
+
+## SysTick 延时函数
+
+SysTick 是 Cortex-M3 内核的 24 位递减计数器,挂载在 AHB (72MHz)。
+
+```c
+// Delay.h
+#ifndef __DELAY_H
+#define __DELAY_H
+
+#include "stm32f10x.h"
+
+void Delay_Init(void);
+void Delay_us(uint32_t us);
+void Delay_ms(uint32_t ms);
+
+#endif
+
+// Delay.c
+#include "Delay.h"
+
+static uint32_t fac_us;
+static uint32_t fac_ms;
+
+void Delay_Init(void)
+{
+    fac_us = SystemCoreClock / 1000000;         // 72
+    fac_ms = SystemCoreClock / 1000;            // 72000
+}
+
+void Delay_us(uint32_t us)
+{
+    uint32_t load = us * fac_us;
+    // 最大 2^24-1 = 16777215, us 超出会复位
+    if (load > 0xFFFFFF) return;
+    
+    SysTick->LOAD = load - 1;
+    SysTick->VAL = 0;
+    SysTick->CTRL = 0x01;                       // 使能, 不中断, 使用 HCLK
+    while (!(SysTick->CTRL & (1 << 16)));       // 等待 COUNTFLAG
+    SysTick->CTRL = 0;                          // 关闭
+}
+
+void Delay_ms(uint32_t ms)
+{
+    while (ms--) {
+        Delay_us(1000);
+    }
+}
+```
+
+> 或用轮回方式(简单无 SysTick 冲突):
+
+```c
+void Delay_ms(uint32_t ms)
+{
+    uint32_t i, j;
+    for (i = 0; i < ms; i++)
+        for (j = 0; j < 7200; j++);    // 72MHz 实测约 1ms
+}
+```
+
+## 编译与下载
+
+### ST-Link 方式(推荐)
+
+1. 连接 SWD: PA13(SWDIO), PA14(SWCLK), GND, 3.3V(可选供电)
+2. Keil → Flash → Download (Ctrl+F8)
+3. 使用 ST-Link Utility 或 STM32CubeProgrammer 也可以
+
+### ISP 串口下载 (FlyMcu)
+
+1. BOOT0=1, BOOT1=0 → 上电/复位 → 进入系统 Loader
+2. 使用 CH340/FT232 USB-TTL 连接 PA9(TX)→模块RX, PA10(RX)→模块TX
+3. FlyMcu 选择串口, 勾选"编程后校验", 下载
+4. 下载完成后 BOOT0=0 → 复位 → 从 Flash 启动
+
+## 常见坑点/注意事项
+
+1. **启动文件选错** — C8T6 用 `_md.s`,用 `_hd.s` 会导致 FLASH 地址映射错误无法运行
+2. **忘记添加 `USE_STDPERIPH_DRIVER`** — 编译报错 `stm32f10x.h` 不包含外设定义
+3. **忘记添加 `STM32F10X_MD`** — 外设寄存器地址映射错误
+4. **C/C++ 头文件路径未配置** — 找不到 `stm32f10x.h`
+5. **晶振频率与 SystemInit 不符** — 8MHz 用 PLL×9=72MHz;如用 12MHz 晶振需改成 PLL×6 或改配置(HSE_VALUE 宏)
+6. **SysTick 优先级** — 默认内核优先级最高,会影响 HAL_Delay 等;标准库手动实现无冲突
+7. **ST-Link 驱动** — Win10/11 可能需禁用驱动签名或安装旧版驱动
+8. **ISP 下载失败** — 检查 BOOT0/BOOT1 电平,串口 TX/RX 交叉连接,波特率不宜过高(115200或57600)

+ 209 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/02-GPIO输出.md

@@ -0,0 +1,209 @@
+# 02-GPIO输出
+
+## GPIO 基本结构
+
+```mermaid
+graph TD
+    subgraph APB2_BUS
+        APB2["APB2 (72MHz)"]
+    end
+    
+    subgraph GPIO_REG
+        CRL["CRL (配置低8位)"]
+        CRH["CRH (配置高8位)"]
+        IDR["IDR (输入数据)"]
+        ODR["ODR (输出数据)"]
+        BSRR["BSRR (位设置/复位)"]
+        BRR["BRR (位复位)"]
+        LCKR["LCKR (锁定)"]
+    end
+    
+    APB2 --> GPIO_REG
+    GPIO_REG --> GPIO_PIN
+```
+
+### GPIO 位结构
+
+```mermaid
+graph LR
+    subgraph PIN_STRUCT
+        PAD["I/O Pad"] --> D1["保护二极管\nVDD"]
+        PAD --> D2["保护二极管\nVSS"]
+        D1 --> PULL_UP["上拉电阻\n(~40kΩ)"]
+        D1 --> PULL_DOWN["下拉电阻\n(~40kΩ)"]
+        PAD --> SCHMITT["施密特触发器"]
+        SCHMITT --> INPUT_REG["输入数据寄存器<br/>(IDR)"]
+        
+        OUTPUT_REG["输出数据寄存器<br/>(ODR)"] --> DRIVER["输出驱动器"]
+        DRIVER --> MUX["复用功能输出"]
+        MUX --> PAD
+        
+        OUTPUT_REG --> BIT_SET["位设置/复位<br/>(BSRR)"]
+        BIT_SET --> DRIVER
+    end
+```
+
+| 组件 | 作用 |
+|------|------|
+| 保护二极管 | 钳位电压,防止 VDD+0.3V / VSS-0.3V 烧毁 IO |
+| 上拉/下拉电阻 | 约 30kΩ~50kΩ,固定空闲电平 |
+| 施密特触发器 | 整形,防抖动,有滞后特性(VT+ ≈ 0.4VDD, VT- ≈ 0.3VDD) |
+| 输出驱动器 | 推挽(Push-Pull) 或 开漏(Open-Drain) |
+| BSRR | 原子操作,一次写同时设置和复位不同位 |
+
+## 8种 GPIO 模式
+
+| 模式 | GPIO_Mode | 结构特点 | 典型用途 |
+|------|-----------|----------|----------|
+| 模拟输入 | `GPIO_Mode_AIN` | 关闭施密特触发器,不接上/下拉 | ADC 采样输入 |
+| 浮空输入 | `GPIO_Mode_IN_FLOATING` | 施密特触发,无上下拉 | 外部信号按键(需外部上拉) |
+| 上拉输入 | `GPIO_Mode_IPU` | 施密特触发 + 上拉电阻 | 按键(GND导通) |
+| 下拉输入 | `GPIO_Mode_IPD` | 施密特触发 + 下拉电阻 | 按键(VCC导通) |
+| 推挽输出 | `GPIO_Mode_Out_PP` | PMOS+NMOS, 高低电平 | LED, 蜂鸣器, 总线控制 |
+| 开漏输出 | `GPIO_Mode_Out_OD` | 仅NMOS, 低=GND, 高=高阻 | I2C SDA/SCL, 电平转换 |
+| 复用推挽 | `GPIO_Mode_AF_PP` | 外设驱动推挽输出 | USART TX, SPI SCK/MOSI |
+| 复用开漏 | `GPIO_Mode_AF_OD` | 外设驱动开漏输出 | I2C(STM32做主控) |
+
+### 输出速度
+
+| 速度宏 | 频率 | 适用场景 |
+|--------|------|----------|
+| `GPIO_Speed_2MHz` | 2MHz | 低频,EMI 最小的通用 IO |
+| `GPIO_Speed_10MHz` | 10MHz | 常规通信 |
+| `GPIO_Speed_50MHz` | 50MHz | SPI/高速 PWM |
+
+> EMI 随速度增大而增大,不用高频时用 `GPIO_Speed_2MHz` 即可。
+
+## API 表格
+
+### RCC 时钟使能
+
+```c
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);
+RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
+```
+
+### GPIO 初始化
+
+```c
+void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);
+```
+
+| GPIO_InitTypeDef 成员 | 取值 | 说明 |
+|----------------------|------|------|
+| `GPIO_Pin` | `GPIO_Pin_0` ~ `GPIO_Pin_15` | 引脚号,支持 OR 组合 |
+| `GPIO_Speed` | `GPIO_Speed_2MHz` / `10MHz` / `50MHz` | 输出速率 |
+| `GPIO_Mode` | 见上方 8 种模式 | IO 工作模式 |
+
+### 输出控制函数
+
+| 函数 | 原型 | 说明 |
+|------|------|------|
+| `GPIO_SetBits` | `GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)` | 指定引脚输出高 |
+| `GPIO_ResetBits` | `GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)` | 指定引脚输出低 |
+| `GPIO_WriteBit` | `GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)` | 按 BitVal 输出 |
+| `GPIO_Write` | `GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)` | 写整个 ODR |
+| `GPIO_SetBits` | 用 BSRR 实现,原子操作 | 推荐使用 |
+
+## 完整代码示例
+
+### LED 流水灯 (PC13~PC10)
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+
+int main(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    
+    // 开启 GPIOC 时钟
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+    
+    // PC13 - PC10 推挽输出
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_12 | GPIO_Pin_11 | GPIO_Pin_10;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOC, &GPIO_InitStructure);
+    
+    // 初始全部熄灭 (板载 LED 低电平亮)
+    GPIO_SetBits(GPIOC, GPIO_Pin_13 | GPIO_Pin_12 | GPIO_Pin_11 | GPIO_Pin_10);
+    
+    Delay_Init();
+    
+    while (1)
+    {
+        GPIO_ResetBits(GPIOC, GPIO_Pin_13);     // PC13 亮
+        Delay_ms(500);
+        GPIO_SetBits(GPIOC, GPIO_Pin_13);
+        
+        GPIO_ResetBits(GPIOC, GPIO_Pin_12);     // PC12 亮
+        Delay_ms(500);
+        GPIO_SetBits(GPIOC, GPIO_Pin_12);
+        
+        GPIO_ResetBits(GPIOC, GPIO_Pin_11);     // PC11 亮
+        Delay_ms(500);
+        GPIO_SetBits(GPIOC, GPIO_Pin_11);
+        
+        GPIO_ResetBits(GPIOC, GPIO_Pin_10);     // PC10 亮
+        Delay_ms(500);
+        GPIO_SetBits(GPIOC, GPIO_Pin_10);
+    }
+}
+```
+
+### 蜂鸣器驱动 (PC9)
+
+电路连接:PC9 → NPN (S8050) Base → 集电极接蜂鸣器负极 → 蜂鸣器正极接 3.3V。
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+
+int main(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+    
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOC, &GPIO_InitStructure);
+    
+    GPIO_ResetBits(GPIOC, GPIO_Pin_9);      // 关闭 (低电平截止)
+    
+    while (1)
+    {
+        // 有源蜂鸣器: 高电平导通发声
+        GPIO_SetBits(GPIOC, GPIO_Pin_9);
+        Delay_ms(500);
+        GPIO_ResetBits(GPIOC, GPIO_Pin_9);
+        Delay_ms(500);
+    }
+}
+```
+
+### 常用板载 LED 引脚
+
+| 开发板 | LED 引脚 | 电平 |
+|--------|----------|------|
+| STM32F103C8T6 蓝色板 | PC13 | 低电平亮 |
+| STM32F103C8T6 红板(Mini) | PA1 | 高电平亮 |
+| STM32F103ZET6 正点原子 | PD2 (DS0) / PG15 (DS1) | 低电平亮 |
+| STM32F103VET6 野火 | PB5 | 低电平亮 |
+
+## 常见坑点/注意事项
+
+1. **外设时钟必须先开启** — 不开启时钟读写寄存器会 HardFault
+2. **GPIO 速度与 EMI** — 不需要高速时用 `GPIO_Speed_2MHz`,过高速度引入 EMI 且增加功耗
+3. **开漏输出高阻** — `Out_OD` 输出高电平必须加外部上拉电阻,否则读不到高电平
+4. **BSRR vs ODR** — 多线程/中断中修改位用 BSRR 是原子操作,ODR 非原子(读-改-写)
+5. **`GPIO_SetBits` 底层用 BSRR** — 比 `GPIO_WriteBit`(用 ODR)更安全
+6. **IO 初始化时默认电平** — 推挽输出初始化后 ODR 复位值 0x0000,引脚默认低电平
+7. **PC13 特殊** — 部分型号 PC13 驱动能力弱,LED 亮度可能不如其他引脚
+8. **BSRR 写入高位 Hi 复位,低位 Lo 置位** — `BSRR[31:16]`=BRx 复位, `BSRR[15:0]`=BSx 置位

+ 199 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/03-GPIO输入.md

@@ -0,0 +1,199 @@
+# 03-GPIO输入
+
+## 按键原理
+
+```mermaid
+graph LR
+    subgraph KEY_CIRCUIT
+        VCC["3.3V"] --> R_PULL["上拉电阻\n(10kΩ)"]
+        R_PULL --> KEY_PIN["GPIO Pin"]
+        KEY_PIN --> BUTTON["按键\n(按下→GND)"]
+        BUTTON --> GND["GND"]
+    end
+    
+    subgraph SIGNAL_FLOW
+        KEY_PRESSED["按下"] --> LOW["低电平 (0V)"]
+        KEY_RELEASED["松开"] --> HIGH["高电平 (3.3V)"]
+    end
+```
+
+### 机械抖动
+
+| 阶段 | 时间 | 电平状态 |
+|------|------|----------|
+| 按下 | ~1ms | 多次跳动(振荡) |
+| 稳定 | >5ms | 稳定低电平 |
+| 松开 | ~1ms | 多次跳动(振荡) |
+
+### 消抖方式
+
+| 方式 | 电路/代码 | 延时 |
+|------|-----------|------|
+| 硬件消抖 | RC 低通滤波 (R=10kΩ, C=100nF, τ=1ms) | 无软件延时 |
+| 软件消抖 | 检测到变化后 Delay 10~20ms 再读一次 | 占用 CPU |
+
+软件消抖流程:
+
+```
+检测电平变化
+  → Delay_ms(15)
+  → 再次读取
+  → 确认电平一致 → 有效按键
+```
+
+## 传感器模块原理
+
+```mermaid
+graph TD
+    subgraph SENSOR_MODULE
+        LIGHT_DEP["光敏电阻"] --> DIVIDER["分压电路"]
+        THERMISTOR["热敏电阻"] --> DIVIDER
+        MIC["麦克风+放大"] --> DIVIDER
+        DIVIDER --> COMPARATOR["LM393 电压比较器"]
+        REF["电位器设定阈值"] --> COMPARATOR
+        COMPARATOR --> DO["DO (数字输出)"]
+        COMPARATOR --> AO["AO (模拟输出)"]
+    end
+    
+    DO --> MCU_PIN["STM32 GPIO"]
+```
+
+LM393 比较器:同相端(+) > 反相端(-) → 输出高电平(开漏,需上拉)。
+
+## API 表格
+
+### GPIO 输入函数
+
+| 函数 | 原型 | 说明 |
+|------|------|------|
+| `GPIO_ReadInputDataBit` | `uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)` | 读取单个引脚电平 |
+| `GPIO_ReadInputData` | `uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)` | 读取整个 IDR 寄存器 |
+| `GPIO_ReadOutputDataBit` | `uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)` | 读取 ODR 位 |
+| `GPIO_ReadOutputData` | `uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx)` | 读取整个 ODR |
+
+### GPIO 输入模式配置
+
+```c
+// 上拉输入(常用 — 按键接 GND 适用)
+GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
+GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   // 上拉输入
+GPIO_Init(GPIOB, &GPIO_InitStructure);
+
+// 下拉输入(按键接 VCC 适用)
+GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
+GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;   // 下拉输入
+GPIO_Init(GPIOB, &GPIO_InitStructure);
+```
+
+## 完整代码示例
+
+### 按键控制 LED (PB0 → PC13)
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+
+int main(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    
+    // 开启时钟
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+    
+    // PC13 推挽输出 (LED)
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOC, &GPIO_InitStructure);
+    
+    // PB0 上拉输入 (按键 → GND)
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    // LED 初始熄灭 (板载低电平亮)
+    GPIO_SetBits(GPIOC, GPIO_Pin_13);
+    
+    uint8_t KeyFlag = 0;        // 按键释放标志
+    uint8_t LedState = 0;       // LED 状态
+    
+    while (1)
+    {
+        // 检测按键按下 (PB0 到 GND, 低电平)
+        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
+        {
+            Delay_ms(15);       // 软件消抖
+            if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0 && KeyFlag == 0)
+            {
+                KeyFlag = 1;    // 标记按下
+                LedState = !LedState;
+                
+                if (LedState)
+                    GPIO_ResetBits(GPIOC, GPIO_Pin_13);  // 亮
+                else
+                    GPIO_SetBits(GPIOC, GPIO_Pin_13);     // 灭
+            }
+        }
+        else
+        {
+            KeyFlag = 0;        // 松手重置
+        }
+    }
+}
+```
+
+### 光敏传感器控制蜂鸣器
+
+传感器 DO 接 PA0,检测到光强低于阈值时(DO=低电平)蜂鸣器响。
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+
+int main(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+    
+    // PC9 推挽输出 (蜂鸣器)
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOC, &GPIO_InitStructure);
+    
+    // PA0 上拉输入 (光敏 DO, 未触发=高, 触发=低)
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+    
+    GPIO_ResetBits(GPIOC, GPIO_Pin_9);  // 蜂鸣器初始关闭
+    
+    while (1)
+    {
+        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
+        {
+            // 光线不足 → 蜂鸣器
+            GPIO_SetBits(GPIOC, GPIO_Pin_9);
+        }
+        else
+        {
+            GPIO_ResetBits(GPIOC, GPIO_Pin_9);
+        }
+        Delay_ms(50);   // 防抖采样间隔
+    }
+}
+```
+
+## 常见坑点/注意事项
+
+1. **浮空输入 (`IN_FLOATING`) 电平不确定** — 引脚悬空时电平振荡,建议用 `IPU`/`IPD` 或外接上拉/下拉
+2. **按键抖动造成多次触发** — 必须软件消抖(10~20ms 延时再读)或硬件 RC 滤波
+3. **传感器模块 DO 引脚电平** — 多数模块 DO 高电平表示未触发(比较器输出开路需板上拉),读取时注意逻辑
+4. **`GPIO_ReadInputDataBit` 返回值** — 返回 `Bit_SET(1)` 或 `Bit_RESET(0)`,可直接和 `0`/`1` 比较
+5. **LED 极性** — 确认板载 LED 是低电平亮还是高电平亮,否则效果相反
+6. **上拉/下拉输入读取的是外部电平** — 和 `GPIO_ReadOutputDataBit` 读 ODR 不同
+7. **按键按下无反应** — 检查按键连接:一端接 GND,另一端接 GPIO;用万用表测按下时引脚对地电阻
+8. **按键悬空时误触发** — 内部上拉电阻约 30kΩ~50kΩ,若环境干扰强需外接 10kΩ 上拉/下拉

+ 274 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/04-OLED调试.md

@@ -0,0 +1,274 @@
+# 04-OLED调试
+
+## 调试方式对比
+
+| 方式 | 优点 | 缺点 | 适用场景 |
+|------|------|------|----------|
+| 串口(USART) | 只需 TX/RX 两根线 | 需 PC 端串口助手,不直观 | 日志输出,数据结构 |
+| OLED 屏幕 | 实时显示,无需 PC | 占 GPIO,I2C/SPI 总线 | 调试参数显示,仪表 |
+| Keil Debug | 单步/断点/变量观察 | 停止运行,实时性差 | 逻辑错误,内存检查 |
+| 逻辑分析仪 | 精确时序 | 需额外硬件 | 时序协议分析 |
+
+## OLED 模块规格 (0.96寸)
+
+| 参数 | 值 |
+|------|-----|
+| 分辨率 | 128 × 64 |
+| 驱动 IC | SSD1306 |
+| 颜色 | 蓝/白/黄蓝双色 |
+| 接口 | I2C / SPI |
+| 供电 | 3.3V~5V |
+| 功耗 | ~20mA |
+| 可视角度 | >160° |
+
+### 4针 I2C 版本引脚
+
+| 引脚 | 功能 | 连接 |
+|------|------|------|
+| VCC | 电源 | 3.3V |
+| GND | 地 | GND |
+| SCL | I2C 时钟线 | PB8 (软件I2C) |
+| SDA | I2C 数据线 | PB9 (软件I2C) |
+
+## SSD1306 与 I2C 协议
+
+```mermaid
+sequenceDiagram
+    participant MCU as STM32 Master
+    participant OLED as SSD1306 Slave
+    
+    MCU->>OLED: START Condition (SCL高, SDA下降沿)
+    MCU->>OLED: 0x78 (设备地址 0x3C<<1 + W=0)
+    OLED-->>MCU: ACK
+    MCU->>OLED: 0x00 (Control Byte: Co=0, D/C#=0 → Command)
+    OLED-->>MCU: ACK
+    MCU->>OLED: Command Byte
+    OLED-->>MCU: ACK
+    MCU->>OLED: STOP Condition (SCL高, SDA上升沿)
+```
+
+## 软件 I2C 模拟
+
+### 通信协议层
+
+```c
+// 起始信号
+void I2C_Start(void)
+{
+    GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9);  // SCL=H, SDA=H
+    Delay_us(5);
+    GPIO_ResetBits(GPIOB, GPIO_Pin_9);              // SDA↓
+    Delay_us(5);
+    GPIO_ResetBits(GPIOB, GPIO_Pin_8);              // SCL↓
+}
+
+// 停止信号
+void I2C_Stop(void)
+{
+    GPIO_ResetBits(GPIOB, GPIO_Pin_9);              // SDA=L
+    GPIO_SetBits(GPIOB, GPIO_Pin_8);                // SCL=H
+    Delay_us(5);
+    GPIO_SetBits(GPIOB, GPIO_Pin_9);                // SDA↑
+    Delay_us(5);
+}
+
+// 发送一个字节 (MSB first)
+void I2C_SendByte(uint8_t byte)
+{
+    for (uint8_t i = 0; i < 8; i++)
+    {
+        if (byte & 0x80)
+            GPIO_SetBits(GPIOB, GPIO_Pin_9);
+        else
+            GPIO_ResetBits(GPIOB, GPIO_Pin_9);
+        
+        byte <<= 1;
+        GPIO_SetBits(GPIOB, GPIO_Pin_8);            // SCL=H (从机采样)
+        Delay_us(2);
+        GPIO_ResetBits(GPIOB, GPIO_Pin_8);          // SCL=L
+        Delay_us(2);
+    }
+    
+    // 接收 ACK
+    GPIO_SetBits(GPIOB, GPIO_Pin_9);                // SDA=H (释放总线)
+    GPIO_SetBits(GPIOB, GPIO_Pin_8);
+    Delay_us(2);                                    // 等待从机拉低 SDA
+    // 若 SDA 未被拉低 (NACK),可在此处理
+    GPIO_ResetBits(GPIOB, GPIO_Pin_8);
+    GPIO_ResetBits(GPIOB, GPIO_Pin_9);              // 恢复
+}
+
+// 接收一个字节
+uint8_t I2C_ReceiveByte(void)
+{
+    uint8_t byte = 0;
+    GPIO_SetBits(GPIOB, GPIO_Pin_9);                // SDA=H (输入模式需设为浮空输入)
+    
+    for (uint8_t i = 0; i < 8; i++)
+    {
+        byte <<= 1;
+        GPIO_SetBits(GPIOB, GPIO_Pin_8);
+        Delay_us(2);
+        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9))
+            byte |= 0x01;
+        GPIO_ResetBits(GPIOB, GPIO_Pin_8);
+        Delay_us(2);
+    }
+    
+    // 发送 ACK (0) 或 NACK (1)
+    GPIO_ResetBits(GPIOB, GPIO_Pin_9);              // ACK
+    GPIO_SetBits(GPIOB, GPIO_Pin_8);
+    Delay_us(2);
+    GPIO_ResetBits(GPIOB, GPIO_Pin_8);
+    GPIO_SetBits(GPIOB, GPIO_Pin_9);
+    
+    return byte;
+}
+```
+
+## OLED 驱动函数表
+
+### 底层命令函数
+
+```c
+void OLED_WriteCmd(uint8_t cmd);        // 写入命令
+void OLED_WriteData(uint8_t data);      // 写入数据
+void OLED_SetCursor(uint8_t x, uint8_t y);  // 设置坐标 (x:0~127, y:0~7 页)
+```
+
+### 用户接口函数
+
+| 函数 | 原型 | 说明 |
+|------|------|------|
+| `OLED_Init` | `void OLED_Init(void)` | 初始化 SSD1306 |
+| `OLED_Clear` | `void OLED_Clear(void)` | 清屏 (全黑) |
+| `OLED_ShowChar` | `void OLED_ShowChar(uint8_t x, uint8_t y, char ch)` | 显示一个字符 (6x8) |
+| `OLED_ShowString` | `void OLED_ShowString(uint8_t x, uint8_t y, char* str)` | 显示字符串 |
+| `OLED_ShowNum` | `void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示十进制数字 |
+| `OLED_ShowSignedNum` | `void OLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t num, uint8_t len)` | 显示有符号数 |
+| `OLED_ShowHexNum` | `void OLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示十六进制数 |
+| `OLED_ShowBinNum` | `void OLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len)` | 显示二进制数 |
+| `OLED_SetCursor` | `void OLED_SetCursor(uint8_t x, uint8_t y)` | 设置写位置 |
+| `OLED_DisplayOn` | `void OLED_DisplayOn(void)` | 开启显示 |
+| `OLED_DisplayOff` | `void OLED_DisplayOff(void)` | 关闭显示(省电) |
+
+## SSD1306 初始化序列
+
+```c
+void OLED_Init(void)
+{
+    Delay_ms(100);  // 等待 SSD1306 上电稳定
+    
+    // PB8(SCL), PB9(SDA) 推挽输出
+    GPIO_InitTypeDef GPIO_InitStructure;
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+    
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;   // 开漏,需外部上拉
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    GPIO_SetBits(GPIOB, GPIO_Pin_8 | GPIO_Pin_9);
+    
+    // SSD1306 初始化命令序列
+    OLED_WriteCmd(0xAE);    // 关闭显示
+    OLED_WriteCmd(0xD5);    // 设置振荡频率
+    OLED_WriteCmd(0x80);    // 默认值
+    OLED_WriteCmd(0xA8);    // 多路复用比
+    OLED_WriteCmd(0x3F);    // 1/64 duty
+    OLED_WriteCmd(0xD3);    // 显示偏移
+    OLED_WriteCmd(0x00);
+    OLED_WriteCmd(0x40);    // 起始行
+    OLED_WriteCmd(0x8D);    // 电荷泵
+    OLED_WriteCmd(0x14);    // 开启
+    OLED_WriteCmd(0x20);    // 内存寻址模式
+    OLED_WriteCmd(0x00);    // 水平寻址
+    OLED_WriteCmd(0xA0);    // 段重映射
+    OLED_WriteCmd(0xA1);    // 列127→SEG0 (左右翻转)
+    OLED_WriteCmd(0xC0);    // COM扫描方向
+    OLED_WriteCmd(0xC8);    // 从COM[N-1]到COM0 (上下翻转)
+    OLED_WriteCmd(0xDA);    // COM引脚配置
+    OLED_WriteCmd(0x12);    // 增强驱动
+    OLED_WriteCmd(0x81);    // 对比度
+    OLED_WriteCmd(0xCF);    // 值
+    OLED_WriteCmd(0xD9);    // 预充电周期
+    OLED_WriteCmd(0xF1);
+    OLED_WriteCmd(0xDB);    // VCOMH解除选择级别
+    OLED_WriteCmd(0x40);
+    OLED_WriteCmd(0xA4);    // 全局显示开启(跟随RAM)
+    OLED_WriteCmd(0xA6);    // 正常显示(不反色)
+    OLED_WriteCmd(0x2E);    // 停止滚动
+    OLED_WriteCmd(0xAF);    // 开启显示
+}
+```
+
+## 完整代码示例
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+#include "OLED.h"
+
+int main(void)
+{
+    Delay_Init();
+    OLED_Init();
+    
+    uint32_t count = 0;
+    
+    OLED_ShowString(1, 1, "STM32 OLED Demo");
+    OLED_ShowString(2, 1, "Count:");
+    OLED_ShowString(3, 1, "Hex:0x");
+    OLED_ShowString(4, 1, "Bin:");
+    
+    while (1)
+    {
+        OLED_ShowNum(2, 7, count, 5);
+        OLED_ShowHexNum(3, 5, count, 8);
+        OLED_ShowBinNum(4, 5, count, 16);
+        
+        count++;
+        
+        // 显示浮点数(间接)
+        // OLED_ShowNum(5, 1, 1234, 4);  // 整数部分
+        // OLED_ShowChar(5, 5, '.');
+        // OLED_ShowNum(5, 6, 56, 2);    // 小数部分
+        
+        Delay_ms(100);
+    }
+}
+```
+
+## OLED.H 参考实现
+
+```c
+#ifndef __OLED_H
+#define __OLED_H
+
+#include "stm32f10x.h"
+
+void OLED_Init(void);
+void OLED_Clear(void);
+void OLED_DisplayOn(void);
+void OLED_DisplayOff(void);
+void OLED_ShowChar(uint8_t x, uint8_t y, char ch);
+void OLED_ShowString(uint8_t x, uint8_t y, char* str);
+void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);
+void OLED_ShowSignedNum(uint8_t x, uint8_t y, int32_t num, uint8_t len);
+void OLED_ShowHexNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);
+void OLED_ShowBinNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len);
+void OLED_SetCursor(uint8_t x, uint8_t y);
+
+#endif
+```
+
+## 常见坑点/注意事项
+
+1. **I2C地址** — SSD1306 7位地址 `0x3C`,写入地址 `0x78` (`0x3C << 1`),读取地址 `0x79`。部分模块 SA0=1 时地址为 `0x7A`
+2. **Control Byte** — 命令 `0x00` (Co=0, D/C=0),数据 `0x40` (Co=0, D/C=1)
+3. **延时不宜过短** — 软件 I2C 时钟周期 `>2.5μs` 最稳定,`Delay_us(5)` 约 100kHz,太快会导致通信失败
+4. **SCL/SDA 模式** — 推荐 `GPIO_Mode_Out_OD`(开漏),配合板载 4.7kΩ 上拉电阻;如果用推挽,SDA 双向时需注意切换方向
+5. **OLED 上电慢** — 初始化前至少 `Delay_ms(100)` 等待 SSD1306 内部复位完成
+6. **显示位置越界** — x: 0~127, y: 0~7(页模式,每页8像素),写超出会绕回
+7. **字库** — 字符显示需要 6x8 或 8x16 点阵字库数组,使用标准库时需自己包含 `OLED_Font.c`
+8. **OLED_SetCursor** — SSD1306 页地址模式默认水平寻址,SetCursor 需发送 `0xB0+y`、高4位列 `0x10+(x>>4)`、低4位列 `x&0x0F`

+ 363 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/05-EXTI外部中断.md

@@ -0,0 +1,363 @@
+# 05-EXTI外部中断
+
+## 中断系统概念
+
+```mermaid
+graph TD
+    subgraph NORMAL
+        A["主程序<br/>Task A"] --> B["Task B"]
+    end
+    
+    subgraph INTERRUPT
+        IRQ["中断请求<br/>(硬件事件)"] --> SAVE["保存现场<br/>(自动压栈)"]
+        SAVE --> SERVICE["中断服务函数<br/>ISR"]
+        SERVICE --> RESTORE["恢复现场<br/>(出栈)"]
+        RESTORE --> RETURN["返回主程序"]
+    end
+    
+    NORMAL --> IRQ
+    RETURN --> NORMAL
+```
+
+### 关键术语
+
+| 术语 | 说明 |
+|------|------|
+| 中断 | CPU 暂停当前任务,处理紧急事件 |
+| 中断优先级 | 决定多个中断同时触发时的响应顺序 |
+| 中断嵌套 | 高优先级中断可以打断低优先级中断服务函数 |
+| 向量表 | 存储所有中断函数入口地址的表(位于 0x08000000 起始) |
+| 尾链(Tail Chaining) | Cortex-M3 优化:两个中断连续进出时省去多余出栈入栈 |
+
+## STM32 中断架构
+
+| 特性 | STM32F103 |
+|------|-----------|
+| 可屏蔽中断通道 | 68 个 |
+| 可编程优先级 | 16 级 (4bit) |
+| 固定优先级 | 中断号越小优先级越高 |
+| 系统异常 | 8 个 (Reset, NMI, HardFault, MemManage, BusFault, UsageFault, SVCall, PendSV, SysTick) |
+| 外部中断 | EXTI0~EXTI15 (16 条线) |
+
+## NVIC 优先级分组
+
+```c
+void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);
+```
+
+| 分组宏 | 抢占位 | 子优先级位 | 抢占级数 | 子优先级数 |
+|--------|--------|-----------|----------|-----------|
+| `NVIC_PriorityGroup_0` | 0 | 4 | 1 | 16 |
+| `NVIC_PriorityGroup_1` | 1 | 3 | 2 | 8 |
+| `NVIC_PriorityGroup_2` | 2 | 2 | 4 | 4 |
+| `NVIC_PriorityGroup_3` | 3 | 1 | 8 | 2 |
+| `NVIC_PriorityGroup_4` | 4 | 0 | 16 | 1 |
+
+> **建议**:全系统只用一种分组,在 `main()` 入口配置一次。推荐 `NVIC_PriorityGroup_2`。
+
+### 中断抢占规则
+
+1. 抢占优先级不同 → 高抢占优先级的先响应,可打断低优先级
+2. 抢占优先级相同、子优先级不同 → 不能嵌套,等当前 ISR 执行完再响应
+3. 抢占、子优先级都相同 → 按中断号排队(号小先响应)
+
+## EXTI 基本结构
+
+```mermaid
+graph TD
+    subgraph GPIO_PINS
+        PA0["PA0"]
+        PB0["PB0"]
+        PC0["PC0"]
+        PA1["PA1"]
+        PB14["PB14"]
+        PE0["PE0"]
+    end
+    
+    subgraph AFIO
+        AFIO_MUX["AFIO 中断引脚选择"]
+    end
+    
+    subgraph EXTI
+        EXTI0["EXTI0"]
+        EXTI1["EXTI1"]
+        EXTI14["EXTI14"]
+    end
+    
+    subgraph NVIC
+        NVIC0["NVIC EXTI0_IRQn"]
+        NVIC1["NVIC EXTI1_IRQn"]
+        NVIC9_5["NVIC EXTI9_5_IRQn"]
+        NVIC15_10["NVIC EXTI15_10_IRQn"]
+    end
+    
+    subgraph CORE
+        CPU["Cortex-M3"]
+    end
+    
+    PA0 --> AFIO_MUX --> EXTI0 --> NVIC0 --> CPU
+    PB0 --> AFIO_MUX --> EXTI0
+    PC0 --> AFIO_MUX --> EXTI0
+    PA1 --> AFIO_MUX --> EXTI1 --> NVIC1 --> CPU
+    PB14 --> AFIO_MUX --> EXTI14 --> NVIC9_5 --> CPU
+    PE0 --> AFIO_MUX --> EXTI0
+```
+
+### 中断线分配
+
+| EXTI 线 | 中断号 | NVIC 通道 | IRQHandler |
+|---------|--------|-----------|------------|
+| EXTI0 | 6 | `EXTI0_IRQn` | `EXTI0_IRQHandler` |
+| EXTI1 | 7 | `EXTI1_IRQn` | `EXTI1_IRQHandler` |
+| EXTI2 | 8 | `EXTI2_IRQn` | `EXTI2_IRQHandler` |
+| EXTI3 | 9 | `EXTI3_IRQn` | `EXTI3_IRQHandler` |
+| EXTI4 | 10 | `EXTI4_IRQn` | `EXTI4_IRQHandler` |
+| EXTI5~9 | 23 | `EXTI9_5_IRQn` | `EXTI9_5_IRQHandler` |
+| EXTI10~15 | 40 | `EXTI15_10_IRQn` | `EXTI15_10_IRQHandler` |
+
+> **EXTI5-9 共用中断函数 `EXTI9_5_IRQHandler`**,需在函数内判断 `EXTI_GetITStatus(EXTI_Line5) ~ EXTI_GetITStatus(EXTI_Line9)`。
+> **EXTI10-15 共用 `EXTI15_10_IRQHandler`**,同理。
+
+### EXTI 输入线选择
+
+```c
+// 将 PB14 连接到 EXTI14
+GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
+```
+
+## API 表格
+
+### EXTI 初始化
+
+```c
+void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
+```
+
+| EXTI_InitTypeDef 成员 | 取值 | 说明 |
+|----------------------|------|------|
+| `EXTI_Line` | `EXTI_Line0` ~ `EXTI_Line15` | 中断线 |
+| `EXTI_Mode` | `EXTI_Mode_Interrupt` / `EXTI_Mode_Event` | 中断模式 / 事件模式 |
+| `EXTI_Trigger` | `EXTI_Trigger_Rising` / `EXTI_Trigger_Falling` / `EXTI_Trigger_Rising_Falling` | 触发方式 |
+| `EXTI_LineCmd` | `ENABLE` / `DISABLE` | 使能 |
+
+### NVIC 初始化
+
+```c
+void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
+```
+
+| NVIC_InitTypeDef 成员 | 取值 | 说明 |
+|----------------------|------|------|
+| `NVIC_IRQChannel` | `EXTI0_IRQn` 等 | 中断通道 |
+| `NVIC_IRQChannelPreemptionPriority` | 0~15 (取决于分组) | 抢占优先级 |
+| `NVIC_IRQChannelSubPriority` | 0~15 (取决于分组) | 子优先级 |
+| `NVIC_IRQChannelCmd` | `ENABLE` / `DISABLE` | 使能 |
+
+### 中断标志位函数
+
+| 函数 | 原型 | 说明 |
+|------|------|------|
+| `EXTI_GetITStatus` | `ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)` | 获取中断标志位 |
+| `EXTI_ClearITPendingBit` | `void EXTI_ClearITPendingBit(uint32_t EXTI_Line)` | 清除中断标志位 |
+
+## 标准中断函数模板
+
+```c
+void EXTIx_IRQHandler(void)
+{
+    if (EXTI_GetITStatus(EXTI_LineX) != RESET)
+    {
+        // 处理中断事件
+        
+        EXTI_ClearITPendingBit(EXTI_LineX);     // 清除标志位
+    }
+}
+
+void EXTI9_5_IRQHandler(void)
+{
+    if (EXTI_GetITStatus(EXTI_Line14) != RESET)
+    {
+        // PB14 中断处理
+        
+        EXTI_ClearITPendingBit(EXTI_Line14);
+    }
+}
+```
+
+## 完整代码示例
+
+### 对射式红外传感器计次 (PB14)
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+#include "OLED.h"
+
+uint16_t SensorCount = 0;
+
+void Sensor_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    EXTI_InitTypeDef EXTI_InitStructure;
+    NVIC_InitTypeDef NVIC_InitStructure;
+    
+    // 1. 开启时钟
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
+    
+    // 2. GPIO 配置 (上拉输入)
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    // 3. AFIO 中断引脚选择
+    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
+    
+    // 4. EXTI 配置 (下降沿触发)
+    EXTI_InitStructure.EXTI_Line = EXTI_Line14;
+    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
+    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
+    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
+    EXTI_Init(&EXTI_InitStructure);
+    
+    // 5. NVIC 配置
+    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
+    
+    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+}
+
+uint16_t Sensor_Get(void)
+{
+    return SensorCount;
+}
+
+// EXTI15_10 中断服务函数 (PB14 在 EXTI14 上)
+void EXTI15_10_IRQHandler(void)
+{
+    if (EXTI_GetITStatus(EXTI_Line14) != RESET)
+    {
+        SensorCount++;
+        EXTI_ClearITPendingBit(EXTI_Line14);
+    }
+}
+
+int main(void)
+{
+    Delay_Init();
+    OLED_Init();
+    Sensor_Init();
+    
+    OLED_ShowString(1, 1, "Sensor Count:");
+    
+    while (1)
+    {
+        OLED_ShowNum(2, 1, Sensor_Get(), 5);
+        Delay_ms(100);
+    }
+}
+```
+
+### 旋转编码器 (A相PB12, B相PB13)
+
+```mermaid
+graph TD
+    subgraph ENCODER
+        A_PHASE["A 相 (PB12)"] -->|"EXTI12<br/>双边缘触发"| MCU
+        B_PHASE["B 相 (PB13)"] -->|"读取电平<br/>判断方向"| MCU
+    end
+    MCU --> COUNT["计数值<br/>+1 / -1"]
+```
+
+```c
+#include "stm32f10x.h"
+
+int16_t EncoderCount = 0;
+
+void Encoder_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    EXTI_InitTypeDef EXTI_InitStructure;
+    NVIC_InitTypeDef NVIC_InitStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
+    
+    // A相 PB12 — 上拉输入
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    // B相 PB13 — 同上
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    // AFIO 映射
+    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource12);
+    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource13);
+    
+    // EXTI12 — 双边缘触发
+    EXTI_InitStructure.EXTI_Line = EXTI_Line12;
+    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
+    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
+    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
+    EXTI_Init(&EXTI_InitStructure);
+    
+    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
+    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+}
+
+int16_t Encoder_Get(void)
+{
+    int16_t temp;
+    temp = EncoderCount;
+    EncoderCount = 0;       // 读取后清零
+    return temp;
+}
+
+void EXTI15_10_IRQHandler(void)
+{
+    if (EXTI_GetITStatus(EXTI_Line12) != RESET)
+    {
+        // 判断方向: 读取 B 相电平
+        if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13) == 0)
+            EncoderCount++;
+        else
+            EncoderCount--;
+        
+        EXTI_ClearITPendingBit(EXTI_Line12);
+    }
+}
+```
+
+## 完整初始化步骤总结
+
+```
+1. RCC_APB2PeriphClockCmd(GPIOx, ENABLE);
+2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);  // AFIO 时钟
+3. GPIO_Init() — 配置输入模式
+4. GPIO_EXTILineConfig() — 选择引脚到 EXTI 线
+5. EXTI_Init() — 配置触发方式
+6. NVIC_PriorityGroupConfig() — 一次
+7. NVIC_Init() — 配置中断优先级
+8. 写 ISR 函数, 检查标志位并清除
+```
+
+## 常见坑点/注意事项
+
+1. **AFIO 时钟必须开启** — `RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE)`,否则 `GPIO_EXTILineConfig` 无效
+2. **同一条 EXTI 线只能对应一个 GPIO** — 例如 EXTI0 可以选 PA0/PB0/PC0,但同一时间只能选一个。选了 PB0 后 PA0 不再触发
+3. **EXTI5-9 和 EXTI10-15 共用中断函数** — 必须在 ISR 内判断具体哪条线的标志位
+4. **清除标志位** — `EXTI_ClearITPendingBit` 必须在 ISR 结束前调用,否则一直进中断
+5. **中断优先级分组只设一次** — 重复调用 `NVIC_PriorityGroupConfig` 行为未定义
+6. **中断函数名必须正确** — Startup 文件中已定义弱符号,写错名字不会编译报错但不会进入中断
+7. **不要在 ISR 中做复杂操作** — 中断应短而快,标志位+主循环处理;延时、打印等在 ISR 中要尽量避免
+8. **下拉输入与上升沿** — 如果按键接 VCC 按下高电平,用 `IPD` + `EXTI_Trigger_Rising`;按键接 GND 用 `IPU` + `EXTI_Trigger_Falling`
+9. **中断同时触发** — 同优先级中断按中断号排队响应,不同级中断按抢占/子优先响应

+ 501 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/06-TIM定时中断.md

@@ -0,0 +1,501 @@
+# 06-TIM定时中断
+
+## 定时器分类 (STM32F103C8T6)
+
+| 类型 | 实例 | 总线 | 分辨率 | 主要特性 |
+|------|------|------|--------|----------|
+| 高级 | TIM1 | APB2 (72MHz) | 16位 | 互补PWM、刹车、死区、编码器接口、重复计数器 |
+| 通用 | TIM2 / TIM3 / TIM4 | APB1 (36MHz) | 16位 | PWM输入/输出、编码器、中断/DMA触发 |
+| 基本 | TIM6 / TIM7 | APB1 (36MHz) | 16位 | 仅定时中断(无外部引脚) |
+
+> **注意**:TIM2 在 F103xC/D/E 及 F107 是 32 位计数器。C8T6 为 16 位。
+
+## 时基单元
+
+```mermaid
+graph LR
+    subgraph TIMER_CLOCK
+        CLK_SRC["时钟源"] --> PSC["PSC<br/>预分频器<br/>16位"]
+    end
+    
+    subgraph TIMER_CORE
+        PSC --> CNT["CNT<br/>计数器<br/>16位"]
+        CNT --> ARR["ARR<br/>自动重装<br/>寄存器<br/>16位"]
+        CNT --> RCR["RCR<br/>重复计数器<br/>(仅高级TIM)"]
+    end
+    
+    ARR --> UPDATE["更新事件/中断<br/>UEV"]
+    RCR --> UPDATE
+    
+    UPDATE --> UPDATE_EVENT["更新事件"]
+    UPDATE_EVENT --> SR["SR 寄存器<br/>UIF 标志位"]
+```
+
+| 寄存器 | 位宽 | 作用 |
+|--------|------|------|
+| PSC (Prescaler) | 16bit | 对时钟分频,`timer_clk = CLK / (PSC+1)` |
+| CNT (Counter) | 16bit | 计数当前值,可向上/向下/中央对齐 |
+| ARR (Auto-Reload) | 16bit | 自动重装值,CNT 达到 ARR 后溢出发送更新事件 |
+| RCR (Repetition) | 16bit | 高级定时器专用,每 (RCR+1) 个溢出才产生更新事件 |
+
+### 定时时间计算
+
+```
+定时频率:     f_timer = TIM_CLK / (PSC + 1) / (ARR + 1)
+定时时间:     T       = (PSC + 1) × (ARR + 1) / TIM_CLK
+
+示例 (TIM2, 36MHz):
+  PSC = 36 - 1 = 35  →  f_cnt = 36MHz / 36 = 1MHz → 1μs
+  ARR = 1000 - 1     →  T = 1000 × 1μs = 1ms
+```
+
+## 时钟源选择
+
+```mermaid
+graph TD
+    subgraph CLOCK_SOURCES
+        INT_CLK["内部时钟<br/>(CK_INT)"] --> TIM_CLK["TIM_CLK"]
+        ETR_MODE2["外部模式2<br/>(ETR 引脚)"] --> TIM_CLK
+        
+        subgraph EXTERNAL_MODE1
+            TI1F_ED["TI1F_ED<br/>(双边沿)"] --> TRGI
+            TI1FP1["TI1FP1"] --> TRGI
+            TI2FP2["TI2FP2"] --> TRGI
+            ETRF["ETRF<br/>(外部触发)"] --> TRGI
+        end
+        TRGI["TRGI"] --> TIM_CLK
+    end
+    
+    TIM_CLK --> PSC_INPUT["→ PSC"]
+```
+
+### 内部时钟(默认)
+
+```c
+void TIM_InternalClockConfig(TIM_TypeDef* TIMx);   // 默认已使能,可不用调用
+```
+
+使用 RCC 提供的时钟:TIM1→PCLK2(72MHz),TIM2/3/4→PCLK1(36MHz)。
+
+### 外部时钟模式2 (ETR)
+
+通过外部引脚(如 TIM2_ETR = PA0)提供时钟源。
+
+```c
+void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, 
+                              uint16_t TIM_ExtTRGPrescaler,
+                              uint16_t TIM_ExtTRGPolarity,
+                              uint16_t ExtTRGFilter);
+```
+
+### 外部时钟模式1 (TIx)
+
+通过 CH1/CH2 引脚输入时钟,可用于编码器模式。
+
+## API 表格
+
+### 定时器时基初始化
+
+```c
+void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
+```
+
+| TIM_TimeBaseInitTypeDef 成员 | 取值 | 说明 |
+|-----------------------------|------|------|
+| `TIM_Prescaler` | 0~65535 | 预分频值 |
+| `TIM_CounterMode` | `TIM_CounterMode_Up` / `Down` / `CenterAligned1/2/3` | 计数模式 |
+| `TIM_Period` | 0~65535 | ARR 自动重装值 |
+| `TIM_ClockDivision` | `TIM_CKD_DIV1` / `DIV2` / `DIV4` | 时钟分频(滤波采样) |
+| `TIM_RepetitionCounter` | 0~255 | 重复计数值(仅 TIM1/TIM8) |
+
+### 定时器控制函数
+
+| 函数 | 原型 | 说明 |
+|------|------|------|
+| `TIM_Cmd` | `void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)` | 开启/关闭定时器 |
+| `TIM_ITConfig` | `void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)` | 中断使能 |
+| `TIM_SetCounter` | `void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter)` | 设置 CNT 值 |
+| `TIM_GetCounter` | `uint16_t TIM_GetCounter(TIM_TypeDef* TIMx)` | 读取 CNT 值 |
+
+### 中断标志位
+
+```c
+ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
+void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
+```
+
+### 输出比较 (OC - Output Compare)
+
+```c
+void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
+void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare);
+```
+
+### 输入捕获 (IC - Input Capture)
+
+```c
+void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
+uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
+```
+
+### 编码器接口
+
+```c
+void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, 
+                                 uint16_t TIM_EncoderMode,
+                                 uint16_t TIM_IC1Polarity, 
+                                 uint16_t TIM_IC2Polarity);
+```
+
+## 完整代码示例
+
+### 1. 定时中断 1ms 翻转 LED (TIM2)
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+#include "OLED.h"
+
+void Timer_Init(void)
+{
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    NVIC_InitTypeDef NVIC_InitStructure;
+    
+    // 1. 开启 TIM2 时钟 (APB1)
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
+    
+    // 2. 时基配置: PCLK1=36MHz, PSC=36-1=36分频→1MHz, ARR=1000-1→1kHz(1ms)
+    TIM_TimeBaseStructure.TIM_Prescaler = 36 - 1;
+    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
+    TIM_TimeBaseStructure.TIM_Period = 1000 - 1;
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
+    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
+    
+    // 3. 清除更新标志位 (防止初始化后立刻进中断)
+    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
+    
+    // 4. 使能更新中断
+    TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
+    
+    // 5. NVIC 配置
+    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
+    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+    
+    // 6. 启动定时器
+    TIM_Cmd(TIM2, ENABLE);
+}
+
+void TIM2_IRQHandler(void)
+{
+    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
+    {
+        // 翻转 PC13 板载 LED
+        GPIO_WriteBit(GPIOC, GPIO_Pin_13, 
+                      (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13)));
+        
+        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
+    }
+}
+
+int main(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOC, &GPIO_InitStructure);
+    
+    GPIO_SetBits(GPIOC, GPIO_Pin_13);      // 初始灭
+    
+    Timer_Init();
+    
+    while (1)
+    {
+        // 主循环空闲 (LED 在中断中翻转)
+    }
+}
+```
+
+### 2. 外部时钟计次 (TIM2_ETR = PA0)
+
+对射式红外传感器接 PA0,每次遮挡计数器 +1。
+
+```c
+#include "stm32f10x.h"
+#include "OLED.h"
+
+void Timer_ETR_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    
+    // 1. GPIO 配置 (PA0 作为 TIM2_ETR)
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;   // 外部信号输入
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+    
+    // 2. 定时器时钟
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
+    
+    // 3. 时基: 不分频, ARR=65535 (最大)
+    TIM_TimeBaseStructure.TIM_Prescaler = 0;                // 不分频
+    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
+    TIM_TimeBaseStructure.TIM_Period = 65535;               // 最大范围
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
+    
+    // 4. 外部时钟模式2: ETR 引脚输入
+    TIM_ETRClockMode2Config(TIM2, 
+                            TIM_ExtTRGPSC_OFF,              // 无预分频
+                            TIM_ExtTRGPolarity_NonInverted, // 不反相
+                            0x0F);                          // 滤波 15 个采样
+    // 5. 启动
+    TIM_Cmd(TIM2, ENABLE);
+}
+
+uint16_t Timer_GetCounter(void)
+{
+    return TIM_GetCounter(TIM2);
+}
+
+int main(void)
+{
+    OLED_Init();
+    Timer_ETR_Init();
+    
+    uint16_t lastCount = 0;
+    
+    OLED_ShowString(1, 1, "ETR Count:");
+    
+    while (1)
+    {
+        uint16_t now = Timer_GetCounter();
+        if (now != lastCount)
+        {
+            lastCount = now;
+            OLED_ShowNum(2, 1, now, 5);
+        }
+    }
+}
+```
+
+### 3. 输出比较 — PWM 呼吸灯 (TIM3_CH1 = PA6)
+
+```c
+#include "stm32f10x.h"
+#include "Delay.h"
+
+void PWM_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_OCInitTypeDef TIM_OCInitStructure;
+    
+    // 1. GPIO: PA6 = TIM3_CH1 复用推挽
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+    
+    // 2. TIM3 时钟
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
+    
+    // 3. 时基: 36MHz / 36 = 1MHz, ARR=1000 → PWM 频率=1kHz
+    TIM_TimeBaseStructure.TIM_Prescaler = 36 - 1;
+    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
+    TIM_TimeBaseStructure.TIM_Period = 1000 - 1;
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
+    
+    // 4. OC1 配置: PWM1 模式
+    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
+    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
+    TIM_OCInitStructure.TIM_Pulse = 0;                      // 初始占空比 0
+    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
+    TIM_OC1Init(TIM3, &TIM_OCInitStructure);
+    
+    TIM_Cmd(TIM3, ENABLE);
+}
+
+void PWM_SetCompare1(uint16_t compare)
+{
+    TIM_SetCompare1(TIM3, compare);
+}
+
+int main(void)
+{
+    PWM_Init();
+    
+    while (1)
+    {
+        // 渐亮
+        for (uint16_t i = 0; i < 1000; i++)
+        {
+            PWM_SetCompare1(i);
+            Delay_ms(1);
+        }
+        // 渐灭
+        for (uint16_t i = 1000; i > 0; i--)
+        {
+            PWM_SetCompare1(i);
+            Delay_ms(1);
+        }
+    }
+}
+```
+
+### 4. 输入捕获 — 频率测量 (TIM4_CH1 = PB6)
+
+```c
+#include "stm32f10x.h"
+#include "OLED.h"
+
+void IC_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_ICInitTypeDef TIM_ICInitStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
+    GPIO_Init(GPIOB, &GPIO_InitStructure);
+    
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
+    
+    TIM_TimeBaseStructure.TIM_Prescaler = 36 - 1;           // 1μs
+    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
+    TIM_TimeBaseStructure.TIM_Period = 65535;
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
+    
+    // IC1: 上升沿捕获, 不分频, 无滤波
+    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
+    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
+    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
+    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
+    TIM_ICInitStructure.TIM_ICFilter = 0x0;
+    TIM_ICInit(TIM4, &TIM_ICInitStructure);
+    
+    TIM_Cmd(TIM4, ENABLE);
+}
+
+int main(void)
+{
+    OLED_Init();
+    IC_Init();
+    
+    OLED_ShowString(1, 1, "Freq (Hz):");
+    
+    while (1)
+    {
+        // 基础频率测量:等待捕获完成,读 CCR1
+        // 实际应用中应使用更新中断 + 捕获中断计算频率
+        uint16_t cap1 = TIM_GetCapture1(TIM4);
+        OLED_ShowNum(2, 1, cap1, 5);
+        Delay_ms(100);
+    }
+}
+```
+
+### 5. 编码器接口 (TIM3_CH1=PA6, TIM3_CH2=PA7)
+
+```c
+#include "stm32f10x.h"
+#include "OLED.h"
+
+void Encoder_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStructure;
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
+    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+    
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
+    
+    TIM_TimeBaseStructure.TIM_Prescaler = 0;                // 不分频
+    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
+    TIM_TimeBaseStructure.TIM_Period = 65535;               // 最大
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
+    
+    // 编码器模式: TI1+TI2, 双边缘
+    TIM_EncoderInterfaceConfig(TIM3,
+                               TIM_EncoderMode_TI12,
+                               TIM_ICPolarity_Rising,
+                               TIM_ICPolarity_Rising);
+    
+    TIM_Cmd(TIM3, ENABLE);
+}
+
+int16_t Encoder_Get(void)
+{
+    return (int16_t)TIM_GetCounter(TIM3);
+}
+
+int main(void)
+{
+    OLED_Init();
+    Encoder_Init();
+    
+    OLED_ShowString(1, 1, "Encoder:");
+    
+    while (1)
+    {
+        int16_t val = Encoder_Get();
+        OLED_ShowSignedNum(2, 1, val, 6);
+        Delay_ms(50);
+    }
+}
+```
+
+## 通用定时器配置步骤总结
+
+```
+1. RCC_APB1PeriphClockCmd(TIMx, ENABLE)
+2. GPIO_Init() — 根据功能配复用推挽(AF_PP)或浮空输入(IN_FLOATING)
+3. TIM_TimeBaseInit() — PSC, ARR, CounterMode
+4. 可选: TIM_ITConfig(TIMx, TIM_IT_Update, ENABLE) — 中断使能
+5. 可选: NVIC_Init() — 中断优先级
+6. 可选: TIM_OCxInit() — 输出比较
+7. 可选: TIM_ICInit() — 输入捕获
+8. 可选: TIM_EncoderInterfaceConfig() — 编码器
+9. TIM_Cmd(TIMx, ENABLE) — 启动定时器
+```
+
+## TIM 中断 NVIC 通道
+
+| 定时器 | 中断通道 | IRQn |
+|--------|----------|------|
+| TIM1 | TIM1_BRK_IRQn / TIM1_UP_IRQn / TIM1_TRG_COM_IRQn / TIM1_CC_IRQn | 24/25/26/27 |
+| TIM2 | TIM2_IRQn | 28 |
+| TIM3 | TIM3_IRQn | 29 |
+| TIM4 | TIM4_IRQn | 30 |
+| TIM5 | TIM5_IRQn | 50 |
+| TIM6 | TIM6_IRQn | 54 |
+| TIM7 | TIM7_IRQn | 55 |
+
+## 常见坑点/注意事项
+
+1. **ARR/PSC 16位范围** — 最大 65535。如需更长定时:级联两个定时器或软件计数器累加
+2. **影子寄存器** — PSC 和 ARR 有影子寄存器,写后在下一次更新事件生效。`TIM_Prescaler` 写后立即对时钟分频生效,但 PSC 缓冲到 UE 才更新;ARR 预加载可通过 `TIM_ARRPreloadConfig` 控制
+3. **TIMxCLK 频率** — TIM1 在 APB2(72MHz),TIM2/3/4 在 APB1(36MHz)。若 APB1 分频不为 1,则定时器时钟 = PCLK1 × 2(36MHz × 2 = 72MHz 仅当 APB1 分频 > 1 时)。`SystemInit` 默认 APB1/2,所以 TIM2/3/4 时钟 = 36MHz × 2 = 72MHz?**不** — 标准库 SystemInit 中 APB1 预分频=2(除2),则 PCLK1 = 36MHz。定时器时钟 = PCLK1 × 2 = 72MHz(当 APB1 预分频≠1 时)
+4. **初始化后立刻进中断** — TIM_TimeBaseInit 会置位 UIF,需要在 TIM_ITConfig 前调用 `TIM_ClearITPendingBit`
+5. **`TIM_OC1Init` 会重设 PSC/ARR** — 如果在 OC Init 之前已经设置了 PSC/ARR,OC Init 内部会重新调用 TimeBaseInit,需在 OC Init 之后重新配置 PSC/ARR 或先设好。正确做法是 TimeBaseInit → OCInit 顺序
+6. **输出比较极性** — `TIM_OCPolarity_High`:CCR ≥ CNT 时输出高;`TIM_OCPolarity_Low` 反之
+7. **编码器模式方向** — 读取 CNT 得相对位置,正转 +1 反转 -1;编码器为 16 位,超过范围会溢出
+8. **输入捕获必须正确选择 TIx** — `TIM_ICSelection_DirectTI` 选择对应通道的输入引脚,`TIM_ICSelection_IndirectTI` 选择交叉输入(CH1 用 CH2 的引脚)
+9. **PWM 频率** — PWM 频率 = TIM_CLK / (PSC+1) / (ARR+1),分辨率 = ARR+1 级。如需 1kHz 且 ARR=999,则占空比步进 0.1%
+10. **OC 输出与 GPIO** — 一定要用 `GPIO_Mode_AF_PP`,否则 PWM 信号不会从引脚输出

+ 258 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/07-TIM输出比较.md

@@ -0,0 +1,258 @@
+---
+title: TIM输出比较
+tags: [STM32, TIM, PWM, 电机]
+created: 2026-07-06
+---
+
+# TIM — 输出比较
+
+## 原理
+
+输出比较(Output Compare)的基本工作流程:
+
+```
+TIM_CLK → PSC分频 → CNT计数器 → 比较器(CNT vs CCR) → OC输出(高/低/翻转/PWM)
+                                    ↑
+                                CCR(捕获比较寄存器)
+```
+
+**核心逻辑**:计数器 CNT 不断递增,与捕获比较寄存器 CCR 进行比较,根据比较结果控制 OC 引脚输出电平。
+
+### PWM 模式
+
+PWM 模式是最常用的输出比较模式:
+
+```
+ARR=999, CCR=300, 向上计数模式
+
+CNT: 0 → 300 → 900 → 999 → 0 → 300 → ...
+     ████████████████░░░░░░░░░░░░░░░░░
+     ↑              ↑
+     有效电平      无效电平
+```
+
+- **PWM模式1**:向上计数时,CNT < CCR 输出有效电平,CNT ≥ CCR 输出无效电平
+- **PWM模式2**:向上计数时,CNT < CCR 输出无效电平,CNT ≥ CCR 输出有效电平
+- **中央对齐模式**:计数器先向上再向下,输出对称 PWM
+
+### 频率与占空比
+
+```
+PWM频率 = TIM_CLK / (PSC + 1) / (ARR + 1)
+占空比  = CCR / (ARR + 1)
+分辨率  = 1 / (ARR + 1)
+```
+
+例:TIM_CLK=72MHz, PSC=71, ARR=999 → 1kHz, 占空比精度 0.1%
+
+### 输出比较模式
+
+| 模式 | 描述 |
+|------|------|
+| `TIM_OCMode_Timing` | 冻结,不影响 OC 引脚 |
+| `TIM_OCMode_Active` | 匹配时强制输出高电平 |
+| `TIM_OCMode_Inactive` | 匹配时强制输出低电平 |
+| `TIM_OCMode_Toggle` | 匹配时翻转输出 |
+| `TIM_OCMode_PWM1` | CNT < CCR 有效,否则无效 |
+| `TIM_OCMode_PWM2` | CNT < CCR 无效,否则有效 |
+
+---
+
+## API 表格
+
+### TIM_OCInitTypeDef
+
+```c
+typedef struct {
+    uint16_t TIM_OCMode;      // TIM_OCMode_PWM1/PWM2/Toggle/Active/Inactive/Timing
+    uint16_t TIM_OutputState; // TIM_OutputState_Enable / Disable
+    uint16_t TIM_OutputNState;// TIM_OutputNState_Enable / Disable (互补输出)
+    uint16_t TIM_Pulse;       // 初始 CCR 值
+    uint16_t TIM_OCPolarity;  // TIM_OCPolarity_High / Low
+    uint16_t TIM_OCNPolarity; // 互补输出极性
+    uint16_t TIM_OCIdleState; // 空闲时状态
+    uint16_t TIM_OCNIdleState;
+} TIM_OCInitTypeDef;
+```
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `TIM_OC1Init(TIMx, &TIM_OCInitStructure)` | 初始化通道1输出比较 |
+| `TIM_OC2Init(TIMx, &)` | 通道2 |
+| `TIM_OC3Init(TIMx, &)` | 通道3 |
+| `TIM_OC4Init(TIMx, &)` | 通道4 |
+| `TIM_SetCompare1(TIMx, uint16_t CCR)` | 设置通道1比较值 |
+| `TIM_SetCompare2(TIMx, uint16_t CCR)` | 设置通道2比较值 |
+| `TIM_SetCompare3(TIMx, uint16_t CCR)` | 设置通道3比较值 |
+| `TIM_SetCompare4(TIMx, uint16_t CCR)` | 设置通道4比较值 |
+| `TIM_OC1PreloadConfig(TIMx, TIM_OCPreload_Enable)` | 使能 CCR 预装载 |
+| `TIM_ARRPreloadConfig(TIMx, ENABLE)` | 使能 ARR 预装载 |
+
+### GPIO 复用映射
+
+| 定时器 | 通道 | GPIO | 备注 |
+|--------|------|------|------|
+| TIM2 | CH1 | PA0 | C8T6可用 |
+| TIM2 | CH2 | PA1 | |
+| TIM2 | CH3 | PA2 | |
+| TIM2 | CH4 | PA3 | |
+| TIM3 | CH1 | PA6 | |
+| TIM3 | CH2 | PA7 | |
+| TIM3 | CH3 | PB0 | |
+| TIM3 | CH4 | PB1 | |
+
+---
+
+## 代码示例
+
+### 1. PWM 呼吸灯 (PA0, TIM2_CH1, 1kHz)
+
+```c
+#include "stm32f10x.h"
+
+void Delay_ms(uint32_t ms)
+{
+    SysTick->LOAD = 72000 - 1;
+    SysTick->VAL  = 0;
+    SysTick->CTRL = 0x05; // HCLK/8=9MHz → 1ms
+    for (uint32_t i = 0; i < ms; i++) {
+        while (!(SysTick->CTRL & (1 << 16)));
+    }
+    SysTick->CTRL = 0;
+}
+
+void TIM2_PWM_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;  // 复用推挽输出
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_TimeBaseStructure.TIM_Prescaler     = 71;      // 72MHz/(71+1) = 1MHz
+    TIM_TimeBaseStructure.TIM_Period        = 999;     // 1MHz/(999+1) = 1kHz
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_CounterMode   = TIM_CounterMode_Up;
+    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
+
+    TIM_OCInitTypeDef TIM_OCInitStructure;
+    TIM_OCInitStructure.TIM_OCMode     = TIM_OCMode_PWM1;
+    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
+    TIM_OCInitStructure.TIM_Pulse       = 0;           // 初始占空比0
+    TIM_OCInitStructure.TIM_OCPolarity  = TIM_OCPolarity_High;
+    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
+
+    TIM_Cmd(TIM2, ENABLE);
+}
+
+int main(void)
+{
+    TIM2_PWM_Init();
+    uint8_t dir = 1;
+    uint16_t ccr = 0;
+
+    while (1) {
+        TIM_SetCompare1(TIM2, ccr);
+        Delay_ms(5);
+
+        if (dir) {
+            ccr += 10;
+            if (ccr >= 999) dir = 0;
+        } else {
+            ccr -= 10;
+            if (ccr <= 0) dir = 1;
+        }
+    }
+}
+```
+
+### 2. SG90 舵机驱动 (PA0, TIM2_CH1, 50Hz)
+
+```c
+#include "stm32f10x.h"
+
+void Delay_ms(uint32_t ms) { /* 同呼吸灯 */ }
+
+void Servo_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_TimeBaseStructure.TIM_Prescaler     = 71;      // 1MHz
+    TIM_TimeBaseStructure.TIM_Period        = 19999;   // 1MHz/20000 = 50Hz
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_CounterMode   = TIM_CounterMode_Up;
+    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
+
+    TIM_OCInitTypeDef TIM_OCInitStructure;
+    TIM_OCInitStructure.TIM_OCMode     = TIM_OCMode_PWM1;
+    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
+    TIM_OCInitStructure.TIM_Pulse       = 1500;        // 中位 1.5ms
+    TIM_OCInitStructure.TIM_OCPolarity  = TIM_OCPolarity_High;
+    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
+
+    TIM_Cmd(TIM2, ENABLE);
+}
+
+// angle: 0~180°
+void Servo_SetAngle(uint16_t angle)
+{
+    // 0° → 500 (0.5ms), 90° → 1500 (1.5ms), 180° → 2500 (2.5ms)
+    uint16_t pulse = 500 + (uint16_t)((uint32_t)angle * 2000 / 180);
+    TIM_SetCompare1(TIM2, pulse);
+}
+
+int main(void)
+{
+    Servo_Init();
+    while (1) {
+        Servo_SetAngle(0);
+        Delay_ms(1000);
+        Servo_SetAngle(90);
+        Delay_ms(1000);
+        Servo_SetAngle(180);
+        Delay_ms(1000);
+    }
+}
+```
+
+### 3. TB6612 直流电机驱动 (两路PWM: PA0=正转, PA1=反转)
+
+```c
+#include "stm32f10x.h"
+// AIN1/AIN2 控制正反转,PWM 控制速度
+// 初始化 PA0(CH1) 和 PA1(CH2) 为 PWM 输出
+// 正转:TIM_SetCompare2(TIM2, 0) → 关闭反转PWM
+//       TIM_SetCompare1(TIM2, speed) → 设置正转速度
+```
+
+---
+
+## 常见坑点
+
+1. **CCR 修改立即生效**:默认无预加载,`TIM_SetCompare1()` 写入后下一周期立即生效;如需同步更新,请使能 `TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable)`
+
+2. **PWM 频率避开人耳范围**:20Hz–20kHz 是听觉范围,电机/PWM 尽量避免此区间(用 >20kHz 或 <20Hz),否则会听到啸叫
+
+3. **互补通道**:TIM1/TIM8 才有互补输出(`TIM_OutputNState`),普通定时器 TIM2–TIM5 没有,配置后无效
+
+4. **GPIO 模式必须为 AF_PP**:复用推挽输出,定时器才能控制引脚;如果设为普通推挽(GPIO_Mode_Out_PP),引脚不受定时器控制
+
+5. **ARR 预装载**:修改 ARR 后建议使能 `TIM_ARRPreloadConfig(TIMx, ENABLE)`,否则可能立即改变周期造成异常波形
+
+6. **TIM_OCPolarity** 决定有效电平是高还是低:`TIM_OCPolarity_High` 时 CCR 越大占空比越大;`TIM_OCPolarity_Low` 时 CCR 越大占空比越小
+
+7. **PWM 初始电平**:定时器使能前,引脚为 GPIO 默认状态;可先配置 GPIO 初始电平避免上电抖动

+ 308 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/08-TIM输入捕获.md

@@ -0,0 +1,308 @@
+---
+title: TIM输入捕获
+tags: [STM32, TIM, 输入捕获, 频率测量]
+created: 2026-07-06
+---
+
+# TIM — 输入捕获
+
+## 原理
+
+输入捕获(Input Capture)的工作流程:
+
+```
+外部信号 → GPIO(TIx) → 滤波器 → 边沿检测 → 捕获CNT → 存入CCR
+                 ↑           ↑           ↑
+            ICx配置     ICFilter    ICPolarity
+```
+
+三种捕获模式:
+
+```
+直接模式: TI1 → IC1    (捕获通道1)
+间接模式: TI1 → IC2    (交换捕获通道)
+PWMI模式: TI1 → IC1 + TI2 → IC2  (同时捕获频率和占空比)
+```
+
+### 频率测量
+
+```
+上升沿    上升沿    上升沿
+  ↓         ↓         ↓
+  |----T----|----T----|
+      捕获1    捕获2
+
+频率 = TIM_CLK / (PSC + 1) / (Capture2 - Capture1)
+```
+
+### PWMI 模式测占空比
+
+```
+          ______              ______
+         |      |            |      |
+_________|      |____________|      |
+↑ 捕获CH1 ↑ 捕获CH2            ↑ 捕获CH1
+(上升沿)   (下降沿)            (下次上升沿)
+
+频率 = 1 / (CCR1_2 - CCR1_1)
+占空比 = (CCR2 - CCR1) / (CCR1_2 - CCR1_1)
+```
+
+### 溢出处理
+
+16位计数器(ARR=65535)在1MHz时钟下约65ms溢出一次:
+
+```
+CNT: ───0────65535────0────65535────
+                 ↑溢出中断
+记录溢出次数 overflow_cnt,则:
+总时间 = overflow_cnt × 65536 + CCR
+```
+
+---
+
+## API 表格
+
+### TIM_ICInitTypeDef
+
+```c
+typedef struct {
+    uint16_t TIM_Channel;     // TIM_Channel_1/2/3/4
+    uint16_t TIM_ICPolarity;  // TIM_ICPolarity_Rising / Falling / Both
+    uint16_t TIM_ICSelection; // TIM_ICSelection_DirectTI / IndirectTI / TRC
+    uint16_t TIM_ICPrescaler; // 0/1/2/4/8 分频 (ICxPSC)
+    uint16_t TIM_ICFilter;    // 0x00~0x0F 数字滤波
+} TIM_ICInitTypeDef;
+```
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `TIM_ICInit(TIMx, &TIM_ICInitStructure)` | 初始化输入捕获通道 |
+| `TIM_PWMIConfig(TIMx, &TIM_ICInitStructure)` | 配置 PWMI 模式(同时配 CH1+CH2) |
+| `TIM_GetCapture1(TIMx)` | 读取 CCR1 |
+| `TIM_GetCapture2(TIMx)` | 读取 CCR2 |
+| `TIM_GetCapture3(TIMx)` | 读取 CCR3 |
+| `TIM_GetCapture4(TIMx)` | 读取 CCR4 |
+| `TIM_ITConfig(TIMx, TIM_IT_Update/CC1/CC2, ENABLE)` | 使能中断 |
+| `TIM_GetITStatus(TIMx, TIM_IT_Update)` | 获取中断状态 |
+| `TIM_ClearITPendingBit(TIMx, TIM_IT_Update)` | 清除中断标志 |
+| `TIM_SelectInputTrigger(TIMx, TIM_TS_TI1FP1)` | 选择触发输入(从模式) |
+| `TIM_SelectSlaveMode(TIMx, TIM_SlaveMode_Reset)` | 选择从模式(复位模式自动清零CNT) |
+
+### TIM_ICPolarity 枚举
+
+| 宏 | 说明 |
+|----|------|
+| `TIM_ICPolarity_Rising` | 上升沿捕获 |
+| `TIM_ICPolarity_Falling` | 下降沿捕获 |
+| `TIM_ICPolarity_Both` | 双边沿捕获(需 TIM5 或高级定时器) |
+
+### TIM_ICSelection 枚举
+
+| 宏 | 说明 |
+|----|------|
+| `TIM_ICSelection_DirectTI` | 直接模式,TIx → ICx |
+| `TIM_ICSelection_IndirectTI` | 间接模式,TIx → ICy |
+| `TIM_ICSelection_TRC` | 内部触发输入 |
+
+### 滤波器配置 (`TIM_ICFilter`)
+
+| 值 | 采样频率 | 采样次数 | 说明 |
+|----|----------|----------|------|
+| 0x00 | — | — | 无滤波 |
+| 0x01–0x0F | fDTS/N | 递增 | N 次一致才输出有效电平 |
+
+---
+
+## 代码示例
+
+### 1. 频率测量 (PA6, TIM3_CH1, 1MHz分辨率)
+
+```c
+#include "stm32f10x.h"
+
+volatile uint16_t overflows = 0;
+
+void Delay_ms(uint32_t ms)
+{
+    SysTick->LOAD = 72000 - 1;
+    SysTick->VAL  = 0;
+    SysTick->CTRL = 0x05;
+    for (uint32_t i = 0; i < ms; i++) {
+        while (!(SysTick->CTRL & (1 << 16)));
+    }
+    SysTick->CTRL = 0;
+}
+
+void TIM3_InputCapture_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;    // 上拉输入
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_TimeBaseStructure.TIM_Prescaler     = 71;      // 1MHz
+    TIM_TimeBaseStructure.TIM_Period        = 65535;   // 最大周期
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_CounterMode   = TIM_CounterMode_Up;
+    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
+
+    TIM_ICInitTypeDef TIM_ICInitStructure;
+    TIM_ICInitStructure.TIM_Channel     = TIM_Channel_1;
+    TIM_ICInitStructure.TIM_ICPolarity  = TIM_ICPolarity_Rising;
+    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
+    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
+    TIM_ICInitStructure.TIM_ICFilter    = 0x0F;        // 最大滤波
+    TIM_ICInit(TIM3, &TIM_ICInitStructure);
+
+    // 使能更新中断(用于溢出处理)
+    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
+
+    NVIC_InitTypeDef NVIC_InitStructure;
+    NVIC_InitStructure.NVIC_IRQChannel                   = TIM3_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
+    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+
+    TIM_Cmd(TIM3, ENABLE);
+}
+
+uint32_t IC_GetFreq(void)
+{
+    // 等待两个上升沿
+    TIM_SetCounter(TIM3, 0);
+    overflows = 0;
+    while (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1) == RESET); // 等待第一次捕获
+    TIM_ClearFlag(TIM3, TIM_FLAG_CC1);
+    while (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1) == RESET); // 等待第二次捕获
+    TIM_ClearFlag(TIM3, TIM_FLAG_CC1);
+
+    uint32_t capture = TIM_GetCapture1(TIM3);
+    uint32_t total   = overflows * 65536UL + capture;
+    // 频率 = 1MHz / total(单位Hz)
+    return 1000000UL / total;
+}
+
+void TIM3_IRQHandler(void)
+{
+    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
+        overflows++;
+        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
+    }
+}
+
+int main(void)
+{
+    TIM3_InputCapture_Init();
+    while (1) {
+        uint32_t freq = IC_GetFreq();
+        Delay_ms(500);
+        // 此处可用串口打印 freq
+    }
+}
+```
+
+### 2. PWMI 模式测频率和占空比 (PA6, TIM3_CH1)
+
+```c
+#include "stm32f10x.h"
+
+volatile uint16_t overflows = 0;
+
+void TIM3_PWMI_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_TimeBaseStructure.TIM_Prescaler     = 71;
+    TIM_TimeBaseStructure.TIM_Period        = 65535;
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_CounterMode   = TIM_CounterMode_Up;
+    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
+
+    // PWMI 模式:CH1 上升沿测频率,CH2 下降沿测占空比
+    TIM_ICInitTypeDef TIM_ICInitStructure;
+    TIM_ICInitStructure.TIM_Channel     = TIM_Channel_1;
+    TIM_ICInitStructure.TIM_ICPolarity  = TIM_ICPolarity_Rising;
+    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
+    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
+    TIM_ICInitStructure.TIM_ICFilter    = 0x0F;
+    TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);  // 自动配置 CH1+CH2
+
+    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
+
+    NVIC_InitTypeDef NVIC_InitStructure;
+    NVIC_InitStructure.NVIC_IRQChannel                   = TIM3_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
+    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+
+    TIM_Cmd(TIM3, ENABLE);
+}
+
+// 返回频率(Hz)和占空比(0.01%)
+void IC_GetFreqAndDuty(uint32_t *freq, uint16_t *duty)
+{
+    TIM_SetCounter(TIM3, 0);
+    overflows = 0;
+
+    while (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1) == RESET);
+    TIM_ClearFlag(TIM3, TIM_FLAG_CC1);
+
+    while (TIM_GetFlagStatus(TIM3, TIM_FLAG_CC1) == RESET);
+    TIM_ClearFlag(TIM3, TIM_FLAG_CC1);
+
+    uint16_t ccr1_2 = TIM_GetCapture1(TIM3);  // 第二次上升沿
+    uint16_t ccr2   = TIM_GetCapture2(TIM3);  // 下降沿
+    uint32_t period = overflows * 65536UL + ccr1_2;
+
+    if (period == 0) {
+        *freq = 0;
+        *duty = 0;
+        return;
+    }
+
+    *freq = 1000000UL / period;
+    *duty = (uint16_t)((uint32_t)ccr2 * 10000 / period);
+}
+
+void TIM3_IRQHandler(void)
+{
+    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
+        overflows++;
+        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
+    }
+}
+```
+
+---
+
+## 常见坑点
+
+1. **溢出处理**:16位计数器在1MHz下每65.5ms溢出一次。测低频时必须用更新中断记录溢出次数,否则结果完全错误
+
+2. **滤波影响高频**:`TIM_ICFilter` 过大会滤掉窄脉冲。测高频(>100kHz)时设 `TIM_ICFilter = 0` 或小值
+
+3. **从模式复位**:使用 `TIM_SlaveMode_Reset` 可让捕获到边沿时自动清零 CNT,无需手动 `TIM_SetCounter(0)`,但初始化较复杂
+
+4. **PSC 影响分辨率**:PSC 越小分辨率越高,但计数器溢出更快。测低频需要大 PSC + 大 ARR,测高频需要小 PSC + 小 ARR
+
+5. **PWMI 模式注意**:`TIM_PWMIConfig()` 会同时配置 CH1 和 CH2,不要再去手动调用 `TIM_ICInit()` 配置 CH2,否则覆盖
+
+6. **输入引脚**:确保 GPIO 模式正确(浮空或上拉输入),不要误用为输出模式
+
+7. **捕获中断 vs 更新中断**:需要分别使能,不要混淆 `TIM_IT_CC1` 和 `TIM_IT_Update`

+ 222 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/09-编码器接口.md

@@ -0,0 +1,222 @@
+---
+title: 编码器接口
+tags: [STM32, TIM, 编码器, 正交解码]
+created: 2026-07-06
+---
+
+# TIM — 编码器接口
+
+## 原理
+
+### 正交编码器
+
+正交编码器输出两路相位差 90° 的脉冲信号:
+
+```
+A相 ──┬───┬───┬───┬───
+      │   │   │   │
+      └───┴───┴───┴───
+
+B相 ────┬───┬───┬───┬───
+      │   │   │   │   │
+      └───┴───┴───┴───┘
+
+正转时 A 领先 B 90°:
+A: ──┬──┬──┬──
+     │  │  │  │
+     └──┴──┴──┘
+B: ──┴──┬──┬──┬─
+        │  │  │  │
+        └──┴──┴──┘
+
+反转时 B 领先 A 90°:
+A: ──┴──┬──┬──┬─
+        │  │  │  │
+        └──┴──┴──┘
+B: ──┬──┬──┬──
+     │  │  │  │
+     └──┴──┴──┘
+```
+
+### 编码器模式
+
+STM32 定时器自带编码器接口,自动解码正交信号:
+
+| 模式 | 计数方式 | 倍频 | 描述 |
+|------|----------|------|------|
+| `TIM_EncoderMode_TI1` | 仅在 TI1 边沿计数 | ×2 | 只根据 A 相计数 |
+| `TIM_EncoderMode_TI2` | 仅在 TI2 边沿计数 | ×2 | 只根据 B 相计数 |
+| `TIM_EncoderMode_TI12` | TI1+TI2 双边沿计数 | ×4 | 最高分辨率 |
+
+计数值的正负由两相信号相位差自动判断:
+
+```
+正转 → 计数器递增
+反转 → 计数器递减
+停止 → 计数器不变
+```
+
+定时器内部结构:
+
+```
+A相(TI1) ──→ 边沿检测 ──→ 编码器控制逻辑 ──→ CNT(递增/递减)
+B相(TI2) ──→ 边沿检测 ──→                    ↑
+                                        编码器模式配置
+```
+
+---
+
+## API 表格
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `TIM_EncoderInterfaceConfig(TIMx, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising)` | 配置编码器接口模式 |
+| `TIM_GetCounter(TIMx)` | 读取当前计数值(返回 `uint16_t`) |
+| `TIM_SetCounter(TIMx, uint16_t)` | 设置计数器值 |
+| `TIM_Cmd(TIMx, ENABLE)` | 使能定时器 |
+
+### 参数说明
+
+**TIM_EncoderInterfaceConfig** 参数:
+
+```c
+void TIM_EncoderInterfaceConfig(
+    TIM_TypeDef* TIMx,
+    uint16_t TIM_EncoderMode,  // TIM_EncoderMode_TI1/TI2/TI12
+    uint16_t TIM_IC1Polarity,  // TIM_ICPolarity_Rising/Falling
+    uint16_t TIM_IC2Polarity
+);
+```
+
+**`TIM_EncoderMode` 可选项**:
+
+| 宏 | 倍频 | 计数更新条件 |
+|----|------|-------------|
+| `TIM_EncoderMode_TI1` | ×2 | TI1 上升沿+下降沿 |
+| `TIM_EncoderMode_TI2` | ×2 | TI2 上升沿+下降沿 |
+| `TIM_EncoderMode_TI12` | ×4 | TI1+TI2 上升沿+下降沿 |
+
+---
+
+## 代码示例
+
+### 编码器读取位置 (TIM3_CH1=PA6, TIM3_CH2=PA7)
+
+```c
+#include "stm32f10x.h"
+
+void Encoder_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_6 | GPIO_Pin_7;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING;  // 浮空输入
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    // 编码器模式—定时器时基无需PSC分频
+    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
+    TIM_TimeBaseStructure.TIM_Prescaler     = 0;         // 不分频
+    TIM_TimeBaseStructure.TIM_Period        = 65535;     // 最大值
+    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
+    TIM_TimeBaseStructure.TIM_CounterMode   = TIM_CounterMode_Up;
+    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
+
+    // 配置编码器接口: TI1+TI2 ×4 模式
+    TIM_EncoderInterfaceConfig(TIM3,
+        TIM_EncoderMode_TI12,
+        TIM_ICPolarity_Rising,  // TI1 不反相
+        TIM_ICPolarity_Rising   // TI2 不反相
+    );
+
+    TIM_Cmd(TIM3, ENABLE);
+}
+
+int16_t Encoder_GetCount(void)
+{
+    // 转 int16_t 才能体现正负
+    return (int16_t)TIM_GetCounter(TIM3);
+}
+
+int main(void)
+{
+    Encoder_Init();
+    while (1) {
+        int16_t pos = Encoder_GetCount();
+        // 此处可用串口打印 pos
+        // 正转 → pos 正数递增
+        // 反转 → pos 负数递减
+        Delay_ms(10);
+    }
+}
+```
+
+### 清零并读取角度
+
+```c
+// 编码器归零
+void Encoder_Reset(void)
+{
+    TIM_SetCounter(TIM3, 0);
+}
+
+// 读取并自动清零(用于周期性采样)
+int16_t Encoder_GetCountAndReset(void)
+{
+    int16_t val = (int16_t)TIM_GetCounter(TIM3);
+    TIM_SetCounter(TIM3, 0);
+    return val;
+}
+```
+
+### 外部中断与编码器对比
+
+```c
+// 外部中断方式(不推荐):
+//   分别配置 A/B 相为外部中断,在中断中判断另一相电平
+//   缺点:占用 CPU,高频时丢步
+
+// 编码器接口方式(推荐):
+//   硬件自动加减,不占用 CPU,不丢步
+//   缺点:需要占用定时器通道
+```
+
+---
+
+## 常见坑点
+
+1. **读出值要转 int16_t**:`TIM_GetCounter()` 返回 `uint16_t`(0–65535),编码器可能递减到负数。强制转为 `int16_t` 才能正确体现正反转
+
+2. **PSC 必须为 0**:编码器模式下 PSC 必须为 0(不分频),因为计数器靠外部脉冲计数,不能用内部时钟分频。设 PSC>0 会导致完全不计数或计数异常
+
+3. **最大值 65535 → -1 回绕**:反转时计数器从 0 递减到 65535,int16_t 解释为 -1。这是正常的,不要当成错误
+
+4. **TIM_ICPolarity 含义不同**:编码器模式下,极性参数 `TIM_ICPolarity_Falling` 的含义变为**反相输入信号**,而不是捕获边沿。设为 Falling 相当于交换 A/B 相
+
+5. **GPIO 必须为浮空或上拉输入**:不要配成输出模式。编码器模块输出的是开漏/推挽信号,通常用浮空输入
+
+6. **和外部中断的区别**:
+   - 外部中断:需要 CPU 参与每条边沿,高频(>10kHz)必定丢步
+   - 编码器接口:硬件自动处理,可应对 >100kHz 编码器信号
+
+7. **单次最大计数范围**:16位定时器 ±32767(int16_t)。如果需要更大范围,在代码中扩展为 int32_t:
+
+```c
+volatile int32_t encoder_pos = 0;
+volatile int16_t last_cnt = 0;
+
+void Encoder_Poll(void)
+{
+    int16_t cur = (int16_t)TIM_GetCounter(TIM3);
+    int16_t diff = cur - last_cnt;
+    last_cnt = cur;
+    encoder_pos += diff; // 自动处理溢出回绕
+}
+```
+
+8. **多个编码器**:STM32F103C8T6 只有一个定时器支持编码器模式(TIM3)。TIM2 不支持编码器模式(仅 TIM3/4 有编码器功能)
+
+> 纠正:TIM2、TIM3、TIM4、TIM5 都支持编码器模式(STM32F1 通用定时器均支持)。高级定时器 TIM1/TIM8 不支持。

+ 313 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/10-ADC模数转换.md

@@ -0,0 +1,313 @@
+---
+title: ADC模数转换
+tags: [STM32, ADC, 模拟量采集]
+created: 2026-07-06
+---
+
+# ADC — 模数转换
+
+## 原理
+
+### 逐次逼近型 (SAR) ADC
+
+```
+Vin ─→ 采样保持 ─→ 比较器 ─→ 逐次逼近寄存器(SAR) ─→ N位数字输出
+                    ↑               ↑
+                DAC输出      SAR控制逻辑
+
+工作原理:
+1. MSB 先置 1,DAC 输出与 Vin 比较
+2. Vin > DAC? → 保留该位为 1;否则置 0
+3. 依次比较下一个位,直到 LSB
+```
+
+### STM32F103C8T6 ADC 特性
+
+| 参数 | 值 |
+|------|-----|
+| ADC 数量 | 2 (ADC1, ADC2) |
+| 分辨率 | 12 位 (0–4095) |
+| 外部通道数 | 10 个 (PA0–PA7, PB0, PB1) |
+| 内部通道 | 温度传感器(ADC1_CH16), Vrefint(ADC1_CH17) |
+| 转换时间 | 最短 1µs (14 周期 @ 14MHz) |
+| ADC_CLK 最大 | 14MHz |
+
+### 规则组 vs 注入组
+
+```
+规则组(Regular Group):
+  最多16个通道,按顺序转换
+  一次配置可以转换多个通道(扫描模式)
+  结果存入唯一的 ADC_DR 寄存器
+
+注入组(Injected Group):
+  最多4个通道,可打断规则组转换
+  有独立的数据寄存器 ADC_JDRx
+  优先级高于规则组
+```
+
+### 转换模式
+
+```
+单次转换(Single):
+  触发一次 → 转换一个通道 → 停止
+  适合单通道电压读取
+
+连续转换(Continuous):
+  触发一次 → 持续转换 → 数据不断更新
+  适合持续采集一个通道
+
+扫描模式(Scan):
+  配合规则组,依次转换多个通道
+  可单次扫描或连续扫描
+```
+
+### 触发方式
+
+```
+软件触发:
+  ADC_SoftwareStartConvCmd(ADC1, ENABLE)
+  写一次触发一次
+
+外部触发(定时器/EXTI):
+  TIMx_CCx 事件触发
+  EXTI 线触发
+  适合固定频率采样
+```
+
+### 数据对齐
+
+```
+右对齐:
+  ┌────────────────────────────┐
+  │  0 │  0 │  0 │  0 │ D11···D0 │
+  └────────────────────────────┘
+  直接读取 value = ADC_GetConversionValue()
+
+左对齐(8位精度):
+  ┌────────────────────────────┐
+  │ D11···D0 │  0 │  0 │  0 │  0 │
+  └────────────────────────────┘
+  高位对齐,适合 8 位读取
+```
+
+---
+
+## API 表格
+
+### ADC_InitTypeDef
+
+```c
+typedef struct {
+    uint32_t ADC_Mode;             // ADC_Mode_Independent / Dual模式
+    FunctionalState ADC_ScanConvMode;    // ENABLE/DISABLE 扫描模式
+    FunctionalState ADC_ContinuousConvMode; // ENABLE/DISABLE 连续转换
+    uint32_t ADC_ExternalTrigConv; // ADC_ExternalTrigConv_T1_CC1 / None_Software
+    uint32_t ADC_DataAlign;        // ADC_DataAlign_Right / Left
+    uint8_t  ADC_NbrOfChannel;     // 规则组通道数 (1–16)
+} ADC_InitTypeDef;
+```
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `ADC_Init(ADCx, &ADC_InitStructure)` | 初始化 ADC |
+| `ADC_RegularChannelConfig(ADCx, ADC_Channel_x, Rank, SampleTime)` | 配置规则组通道 |
+| `ADC_SoftwareStartConvCmd(ADCx, ENABLE)` | 软件触发转换 |
+| `ADC_GetConversionValue(ADCx)` | 读取转换结果 |
+| `ADC_Cmd(ADCx, ENABLE)` | 使能 ADC |
+| `ADC_ResetCalibration(ADCx)` | 复位校准 |
+| `ADC_StartCalibration(ADCx)` | 开始校准 |
+| `ADC_GetCalibrationStatus(ADCx)` | 获取校准状态 |
+| `ADC_DMACmd(ADCx, ENABLE)` | 使能 DMA 请求(配合 ADC+DMA) |
+| `ADC_ITConfig(ADCx, ADC_IT_EOC, ENABLE)` | 使能转换结束中断 |
+
+### ADC_ExternalTrigConv 枚举
+
+| 宏 | 说明 |
+|----|------|
+| `ADC_ExternalTrigConv_T1_CC1` | TIM1 CC1 触发 |
+| `ADC_ExternalTrigConv_T2_CC2` | TIM2 CC2 触发 |
+| `ADC_ExternalTrigConv_T3_TRGO` | TIM3 TRGO 触发 |
+| `ADC_ExternalTrigConv_None` | 软件触发 |
+
+### ADC_SampleTime 枚举
+
+| 宏 | 周期数 | 转换时间(@14MHz) |
+|----|--------|-----------------|
+| `ADC_SampleTime_1Cycles5` | 1.5 | ≈ 1.07µs |
+| `ADC_SampleTime_7Cycles5` | 7.5 | ≈ 1.54µs |
+| `ADC_SampleTime_13Cycles5` | 13.5 | ≈ 1.93µs |
+| `ADC_SampleTime_28Cycles5` | 28.5 | ≈ 2.79µs |
+| `ADC_SampleTime_41Cycles5` | 41.5 | ≈ 3.57µs |
+| `ADC_SampleTime_55Cycles5` | 55.5 | ≈ 4.43µs |
+| `ADC_SampleTime_71Cycles5` | 71.5 | ≈ 5.36µs |
+| `ADC_SampleTime_239Cycles5` | 239.5 | ≈ 17.1µs |
+
+---
+
+## 代码示例
+
+### 1. 单通道电压测量 (PA0=ADC1_CH0, 软件触发)
+
+```c
+#include "stm32f10x.h"
+
+void Delay_ms(uint32_t ms)
+{
+    SysTick->LOAD = 72000 - 1;
+    SysTick->VAL  = 0;
+    SysTick->CTRL = 0x05;
+    for (uint32_t i = 0; i < ms; i++) {
+        while (!(SysTick->CTRL & (1 << 16)));
+    }
+    SysTick->CTRL = 0;
+}
+
+void ADC1_Init(void)
+{
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
+    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  // 72MHz / 6 = 12MHz (< 14MHz)
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;  // 模拟输入
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    ADC_InitTypeDef ADC_InitStructure;
+    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
+    ADC_InitStructure.ADC_ScanConvMode       = DISABLE;
+    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
+    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
+    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
+    ADC_InitStructure.ADC_NbrOfChannel       = 1;
+    ADC_Init(ADC1, &ADC_InitStructure);
+
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
+
+    ADC_Cmd(ADC1, ENABLE);
+
+    // 校准
+    ADC_ResetCalibration(ADC1);
+    while (ADC_GetResetCalibrationStatus(ADC1));
+    ADC_StartCalibration(ADC1);
+    while (ADC_GetCalibrationStatus(ADC1));
+}
+
+uint16_t ADC1_GetValue(void)
+{
+    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
+    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
+    return ADC_GetConversionValue(ADC1);
+}
+
+// 转换结果为电压值(mV)
+uint16_t ADC1_GetVoltage(void)
+{
+    uint32_t adc_val = ADC1_GetValue();
+    // 3.3V / 4096 * adc_val * 1000
+    return (uint16_t)(adc_val * 3300 / 4096);
+}
+
+int main(void)
+{
+    ADC1_Init();
+    while (1) {
+        uint16_t voltage_mV = ADC1_GetVoltage();
+        // voltage_mV: 0~3300 对应 0~3.3V
+        Delay_ms(100);
+    }
+}
+```
+
+### 2. 多通道扫描 + DMA
+
+```c
+#include "stm32f10x.h"
+
+#define ADC_CHANNELS 4
+
+uint16_t ADC_Values[ADC_CHANNELS]; // PA0(0), PA1(1), PA2(2), PA3(3)
+
+void ADC1_DMA_Init(void)
+{
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_DMA1, ENABLE);
+    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    ADC_InitTypeDef ADC_InitStructure;
+    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
+    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;
+    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
+    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
+    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
+    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CHANNELS;
+    ADC_Init(ADC1, &ADC_InitStructure);
+
+    // 配置各通道(采样顺序:0→1→2→3)
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
+
+    // DMA 配置
+    DMA_InitTypeDef DMA_InitStructure;
+    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
+    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)ADC_Values;
+    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
+    DMA_InitStructure.DMA_BufferSize         = ADC_CHANNELS;
+    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
+    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
+    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
+    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
+    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
+    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
+    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
+    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
+
+    DMA_Cmd(DMA1_Channel1, ENABLE);
+
+    ADC_DMACmd(ADC1, ENABLE);
+    ADC_Cmd(ADC1, ENABLE);
+
+    ADC_ResetCalibration(ADC1);
+    while (ADC_GetResetCalibrationStatus(ADC1));
+    ADC_StartCalibration(ADC1);
+    while (ADC_GetCalibrationStatus(ADC1));
+
+    ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动连续扫描
+}
+
+// ADC_Values[0] = PA0,  ADC_Values[1] = PA1, ...
+// DMA 自动更新,无需手动干预
+```
+
+---
+
+## 常见坑点
+
+1. **ADC 输入阻抗**:STM32F103 ADC 采样电容约 4pF,建议外部信号源阻抗 < 10kΩ。信号源阻抗过大时,加缓冲器(运放跟随)或增大采样时间
+
+2. **转换结果公式**:`电压 = 3.3 / 4096 × ADC值`,**不是** `3.3 / 4095`。12位ADC的LSB对应 3.3V/4096 ≈ 0.8mV
+
+3. **校准必须做**:每次上电后必须做 `ADC_ResetCalibration` + `ADC_StartCalibration`,否则结果可能有 ±10LSB 误差
+
+4. **RCC_ADCCLKConfig 限制**:ADC 时钟最大 14MHz。72MHz 主频可选分频系数:`RCC_PCLK2_Div2`(36MHz 超限)、`Div4`(18MHz 超限)、`Div6`(12MHz 可用)、`Div8`(9MHz 可用)。必须用 Div6 或 Div8
+
+5. **GPIO 必须为 AIN 模式**:`GPIO_Mode_AIN` 将 GPIO 配置为模拟输入,关闭施密特触发器降低功耗;如果用浮空输入,测量值会偏小
+
+6. **单次转换需重新触发**:每次 `ADC_GetValue()` 都要调用 `ADC_SoftwareStartConvCmd(ADC1, ENABLE)`,再等待 EOC 标志。连续模式下则只需触发一次
+
+7. **连续转换 + 扫描模式**:结合使用可实现后台自动采集所有通道,配合 DMA 读走即可。注意 `ADC_NbrOfChannel` 必须与 DMA `BufferSize` 一致
+
+8. **DMA 传输完成**:多通道扫描模式下,EOC 在每个通道转换完成后置位,需要等所有通道转换完(或用 DMA 传输完成中断)再处理数据
+
+9. **双 ADC 模式**`ADC_Mode`:`ADC_Mode_Independent` 为独立模式;双 ADC 可配置为规则同步/注入同步/交替触发等,芯片内部自动同步
+
+10. **VBAT 测量**:VBAT 通道(ADC1_CH18)测量电池电压时注意分压系数(1/2),且 VBAT 和 VREFINT 共用通道选择

+ 304 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/11-DMA.md

@@ -0,0 +1,304 @@
+---
+title: DMA
+tags: [STM32, DMA, 数据传输]
+created: 2026-07-06
+---
+
+# DMA — 直接存储器访问
+
+## 原理
+
+### DMA 是什么
+
+Direct Memory Access(直接存储器访问),在不占用 CPU 的情况下在外设和存储器之间搬运数据:
+
+```
+传统方式 (CPU搬运):
+  CPU:  读取 外设DR → CPU寄存器 → 写入 内存数组
+  CPU忙 → 无法处理其他任务
+
+DMA方式:
+  CPU:  配置DMA → 继续运行其他代码
+  DMA:  自动 外设DR → 内存数组 (不占用CPU)
+  CPU:  DMA传输完成 → 中断通知CPU
+```
+
+### 基本结构
+
+```
+DMA基本传输单元:
+
+外设地址寄存器    DMA_CPARx
+     ↓
+外设 ←→ DMA 通道 ←→ 存储器
+              ↑
+           传输方向
+
+DMA1 (STM32F103C8T6):
+  ├─ 通道1: ADC1 / TIM2_CH3 / TIM4_CH1
+  ├─ 通道2: USART3_TX / SPI1_RX / TIM1_CH1
+  ├─ 通道3: USART3_RX / SPI1_TX / TIM1_CH2
+  ├─ 通道4: USART1_TX / SPI2_RX / TIM2_CH4
+  ├─ 通道5: USART1_RX / SPI2_TX / TIM3_CH2
+  ├─ 通道6: USART2_TX / SPI3_RX / TIM3_CH3
+  └─ 通道7: USART2_RX / SPI3_TX / TIM3_CH4
+
+DMA2: 仅大容量芯片有 (F103RC以上)
+```
+
+### 传输方向
+
+```
+内存到内存 (M2M):
+  源: 数组A  目标: 数组B
+  DMA直接复制,不经过CPU
+
+外设到内存 (P2M):
+  源: ADC_DR/USART_DR  目标: 用户数组
+  典型:ADC多通道扫描
+
+内存到外设 (M2P):
+  源: 发送缓冲区  目标: USART_DR/SPI_DR
+  典型:串口发送大批量数据
+```
+
+### 循环模式 vs 正常模式
+
+```
+正常模式 (Normal):
+  传输 BufferSize 次后停止
+  需要重新配置 BufferSize 和 Cmd
+
+循环模式 (Circular):
+  传输完 BufferSize 次后自动重置
+  地址回到起点,持续传输
+  适合 ADC 连续扫描 + DMA
+```
+
+---
+
+## API 表格
+
+### DMA_InitTypeDef
+
+```c
+typedef struct {
+    uint32_t DMA_PeripheralBaseAddr;   // 外设基地址 (&USART1->DR, &ADC1->DR)
+    uint32_t DMA_MemoryBaseAddr;       // 内存基地址 (uint32_t)&数组
+    uint32_t DMA_DIR;                  // DMA_DIR_PeripheralSRC / PeripheralDST
+    uint32_t DMA_BufferSize;           // 传输次数
+    uint32_t DMA_PeripheralInc;        // DMA_PeripheralInc_Enable / Disable
+    uint32_t DMA_MemoryInc;            // DMA_MemoryInc_Enable / Disable
+    uint32_t DMA_PeripheralDataSize;   // Byte / HalfWord / Word
+    uint32_t DMA_MemoryDataSize;       // Byte / HalfWord / Word
+    uint32_t DMA_Mode;                 // DMA_Mode_Normal / Circular
+    uint32_t DMA_Priority;             // VeryHigh / High / Medium / Low
+    uint32_t DMA_M2M;                  // DMA_M2M_Enable / Disable
+} DMA_InitTypeDef;
+```
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `DMA_Init(DMA_Channelx, &DMA_InitStructure)` | 初始化 DMA 通道 |
+| `DMA_Cmd(DMA_Channelx, ENABLE)` | 使能 DMA 通道 |
+| `DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE)` | 使能传输完成中断 |
+| `DMA_GetFlagStatus(DMA_Channelx, DMA_FLAG_TCx)` | 获取传输完成标志 |
+| `DMA_ClearFlag(DMA_Channelx, DMA_FLAG_TCx)` | 清除传输完成标志 |
+| `DMA_GetITStatus(DMA_Channelx, DMA_IT_TC)` | 获取传输完成中断状态 |
+| `DMA_ClearITPendingBit(DMA_Channelx, DMA_IT_TC)` | 清除传输完成中断标志 |
+| `DMA_SetCurrDataCounter(DMA_Channelx, uint16_t)` | 设置剩余传输次数 |
+
+### DMA 通道与硬件请求映射
+
+| 通道 | 请求1 | 请求2 | 请求3 | 请求4 | 请求5 |
+|------|-------|-------|-------|-------|-------|
+| DMA1_CH1 | ADC1 | TIM2_CH3 | TIM4_CH1 | — | — |
+| DMA1_CH2 | USART3_TX | SPI1_RX | TIM1_CH1 | TIM2_UP | TIM3_CH3 |
+| DMA1_CH3 | USART3_RX | SPI1_TX | TIM1_CH2 | TIM2_CH1 | TIM3_CH4 |
+| DMA1_CH4 | USART1_TX | SPI2_RX | TIM1_CH3 | TIM2_CH2 | TIM4_CH3 |
+| DMA1_CH5 | USART1_RX | SPI2_TX | TIM1_CH4 | TIM2_CH4 | — |
+| DMA1_CH6 | USART2_TX | SPI3_RX | TIM1_UP | TIM3_CH1 | — |
+| DMA1_CH7 | USART2_RX | SPI3_TX | TIM1_TRIG | TIM3_CH2 | — |
+
+---
+
+## 代码示例
+
+### 1. 内存到内存搬运 (数组A → 数组B)
+
+```c
+#include "stm32f10x.h"
+
+#define BUFFER_SIZE 32
+
+const uint32_t src_buf[BUFFER_SIZE] = {
+    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+    10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+    20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+    30, 31
+};
+uint32_t dst_buf[BUFFER_SIZE] = {0};
+
+void DMA_M2M_Init(void)
+{
+    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
+
+    DMA_InitTypeDef DMA_InitStructure;
+
+    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)src_buf;
+    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)dst_buf;
+    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
+    DMA_InitStructure.DMA_BufferSize         = BUFFER_SIZE;
+    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Enable;
+    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
+    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
+    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Word;
+    DMA_InitStructure.DMA_Mode               = DMA_Mode_Normal;
+    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
+    DMA_InitStructure.DMA_M2M                = DMA_M2M_Enable;  // 内存到内存
+    DMA_Init(DMA1_Channel6, &DMA_InitStructure); // CH6 可用于 M2M
+
+    DMA_Cmd(DMA1_Channel6, ENABLE);
+
+    // 等待传输完成
+    while (DMA_GetFlagStatus(DMA1_Channel6, DMA1_FLAG_TC6) == RESET);
+
+    // 此时 dst_buf 内容应与 src_buf 相同
+}
+
+int main(void)
+{
+    DMA_M2M_Init();
+    while (1);
+}
+```
+
+### 2. ADC + DMA 多通道连续采集
+
+```c
+#include "stm32f10x.h"
+
+#define ADC_CH_COUNT 4
+
+uint16_t adc_values[ADC_CH_COUNT]; // PA0–PA3
+
+void ADC1_DMA_Init(void)
+{
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE);
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_DMA1, ENABLE);
+    RCC_ADCCLKConfig(RCC_PCLK2_Div6);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    // ADC 配置:扫描+连续
+    ADC_InitTypeDef ADC_InitStructure;
+    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
+    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;
+    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
+    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
+    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
+    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CH_COUNT;
+    ADC_Init(ADC1, &ADC_InitStructure);
+
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
+    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
+
+    // DMA 配置
+    DMA_InitTypeDef DMA_InitStructure;
+    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
+    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)adc_values;
+    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;
+    DMA_InitStructure.DMA_BufferSize         = ADC_CH_COUNT;
+    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
+    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
+    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
+    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
+    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;
+    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
+    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
+    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
+
+    ADC_DMACmd(ADC1, ENABLE);
+    DMA_Cmd(DMA1_Channel1, ENABLE);
+    ADC_Cmd(ADC1, ENABLE);
+
+    ADC_ResetCalibration(ADC1);
+    while (ADC_GetResetCalibrationStatus(ADC1));
+    ADC_StartCalibration(ADC1);
+    while (ADC_GetCalibrationStatus(ADC1));
+
+    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
+    // DMA 自动将 4 通道结果循环写入 adc_values[]
+}
+
+// 读取最新值
+// adc_values[0]=PA0, [1]=PA1, [2]=PA2, [3]=PA3
+// DMA 在 Circular 模式下自动更新
+```
+
+### 3. DMA + USART 发送
+
+```c
+#include "stm32f10x.h"
+
+const uint8_t tx_buffer[] = "Hello DMA!\r\n";
+
+void USART1_DMA_Init(void)
+{
+    // ... USART1 初始化同上节课内容 ...
+
+    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
+
+    DMA_InitTypeDef DMA_InitStructure;
+    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
+    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)tx_buffer;
+    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralDST;
+    DMA_InitStructure.DMA_BufferSize         = sizeof(tx_buffer) - 1;
+    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
+    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;
+    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
+    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
+    DMA_InitStructure.DMA_Mode               = DMA_Mode_Normal;
+    DMA_InitStructure.DMA_Priority           = DMA_Priority_Medium;
+    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
+    DMA_Init(DMA1_Channel4, &DMA_InitStructure); // CH4: USART1_TX
+
+    DMA_Cmd(DMA1_Channel4, ENABLE);
+    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
+    // DMA 自动将 tx_buffer 数据发送出去
+}
+```
+
+---
+
+## 常见坑点
+
+1. **DMA_BufferSize 是传输次数**:不是字节数。HalfWord 模式下传输 10 次 = 20 字节,Word 模式下传输 10 次 = 40 字节
+
+2. **外设地址不要配错**:外设地址必须使用 `(uint32_t)&外设->DR` 形式。例:ADC 是 `&ADC1->DR`,USART 是 `&USART1->DR`
+
+3. **DMA 通道与硬件请求对应**:ADC1 只能使用 DMA1_Channel1。USART1_TX 只能用 DMA1_Channel4。通道选错不会报错但也不会传输
+
+4. **内存到内存(M2M)模式限制**:
+   - M2M 模式使用 DMA1,且仅**通道 2/6/7** 可用于 M2M(不同芯片有差异)
+   - M2M 模式下 `DMA_PeripheralBaseAddr` 指向源地址,`DMA_MemoryBaseAddr` 指向目标地址
+   - M2M 模式下 `DMA_DIR` 必须是 `DMA_DIR_PeripheralSRC`
+
+5. **循环模式(Circular)注意**:使能循环后传输永不停,不触发正常传输完成中断(只有半传输和错误中断)。想要周期性处理数据时,用半传输中断(`DMA_IT_HT`)或定时器中断
+
+6. **传输完成中断**:正常模式下每传输 `BufferSize` 次触发一次 TC 中断;使用 `DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE)` 使能;中断服务函数中先清除标志再处理数据
+
+7. **DMA 使能顺序**:先配置 DMA,再使能外设的 DMA 请求。如果先使能外设再使能 DMA,外设可能在 DMA 未就绪时产生第一次请求
+
+8. **DMA 半传输中断**:`DMA_IT_HT` 在传输到一半时触发,适合双缓冲(乒乓缓冲)场景
+
+9. **ADC 连续模式 + DMA 循环**:ADC 设置 `ContinuousConvMode=ENABLE`、DMA 设 `Mode=Circular`,可做到全程无 CPU 参与采集。但注意 ADC_CLK 越快,DMA 带宽占用越高
+
+10. **DMA 不会自动停止**:正常模式传输完成后自动停止(`DMA_Cmd` 自动变 DISABLE),再次传输前需重新使能 `DMA_Cmd(DMA_Channelx, ENABLE)`;循环模式不会停止,即使重新配置也需要先 Disable 再 Enable

+ 391 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/12-USART串口.md

@@ -0,0 +1,391 @@
+---
+title: USART串口
+tags: [STM32, USART, 串口, 通信协议]
+created: 2026-07-06
+---
+
+# USART — 串口通信
+
+## 原理
+
+### 异步串行通信协议
+
+```
+TX 空闲 ───┐ ┌─ D0 ─ D1 ─ D2 ─ D3 ─ D4 ─ D5 ─ D6 ─ D7 ─┐ ┌─ 空闲
+           │ │                                               │ │
+           └─┘ ← 起始位(0)        数据位(8位)           校验位 → 停止位(1)
+
+一帧数据:
+起始位(1位) + 数据位(8/9位) + 校验位(0/1位) + 停止位(1/1.5/2位)
+```
+
+波特率定义单位时间传输的位数:
+
+```
+波特率 = 1 / 位时间
+115200 bps → 每位 ≈ 8.68µs
+```
+
+### USART 外设结构
+
+```
+发送:
+CPU → TDR(数据寄存器) → 发送移位寄存器 → TX 引脚
+                      ↑
+                  波特率发生器控制移位时钟
+
+接收:
+RX 引脚 → 接收移位寄存器 → RDR(数据寄存器) → CPU
+                      ↑
+                  波特率发生器采样
+
+USART_BRR:
+  USART_BRR = PCLK / (16 × 波特率)
+  
+  例: 72MHz / (16 × 115200) = 39.0625 → USART_BRR = 0x271
+      DIV_Mantissa = 39 (0x27)
+      DIV_Fraction = 1  (0x1)
+```
+
+### 中断标志
+
+```
+TXE (发送寄存器空):     TDR 已空,可写入新数据
+TC  (发送完成):        移位寄存器也移位完成
+RXNE (接收寄存器非空):  RDR 有数据可读
+IDLE (总线空闲):        RX 空闲超过一帧时间
+ORE (溢出错误):         RDR 未读时新数据覆盖
+```
+
+---
+
+## API 表格
+
+### USART_InitTypeDef
+
+```c
+typedef struct {
+    uint32_t USART_BaudRate;            // 波特率 2400/4800/9600/19200/38400/57600/115200/...
+    uint16_t USART_WordLength;          // USART_WordLength_8b / 9b
+    uint16_t USART_StopBits;            // USART_StopBits_1 / 0.5 / 2 / 1.5
+    uint16_t USART_Parity;              // USART_Parity_No / Even / Odd
+    uint16_t USART_Mode;                // USART_Mode_Rx / Tx / Rx | Tx
+    uint16_t USART_HardwareFlowControl; // USART_HardwareFlowControl_None / RTS / CTS / RTS_CTS
+} USART_InitTypeDef;
+```
+
+### 核心函数
+
+| 函数 | 描述 |
+|------|------|
+| `USART_Init(USARTx, &USART_InitStructure)` | 初始化 USART |
+| `USART_Cmd(USARTx, ENABLE)` | 使能 USART |
+| `USART_SendData(USARTx, uint16_t Data)` | 发送一个字节 |
+| `USART_ReceiveData(USARTx)` | 接收一个字节(返回 uint16_t) |
+| `USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)` | 使能接收中断 |
+| `USART_GetFlagStatus(USARTx, uint16_t USART_FLAG)` | 获取状态标志 |
+| `USART_ClearFlag(USARTx, uint16_t USART_FLAG)` | 清除状态标志 |
+| `USART_GetITStatus(USARTx, uint16_t USART_IT)` | 获取中断状态 |
+| `USART_ClearITPendingBit(USARTx, uint16_t USART_IT)` | 清除中断挂起位 |
+
+### USART_FLAG 枚举
+
+| 宏 | 含义 |
+|----|------|
+| `USART_FLAG_TXE` | 发送数据寄存器空 (TDR empty) |
+| `USART_FLAG_TC` | 发送完成 (Transmission Complete) |
+| `USART_FLAG_RXNE` | 接收数据寄存器非空 |
+| `USART_FLAG_IDLE` | 总线空闲 |
+| `USART_FLAG_ORE` | 溢出错误 |
+| `USART_FLAG_NE` | 噪声错误 |
+| `USART_FLAG_FE` | 帧错误 |
+| `USART_FLAG_PE` | 校验错误 |
+
+### GPIO 复用映射
+
+| USART | TX | RX | CTS | RTS |
+|-------|----|----|-----|-----|
+| USART1 | PA9 | PA10 | PA11 | PA12 |
+| USART2 | PA2 | PA3 | PA0 | PA1 |
+| USART3 | PB10 | PB11 | PB13 | PB14 |
+
+---
+
+## 代码示例
+
+### 1. USART1 基本收发 (查询方式)
+
+```c
+#include "stm32f10x.h"
+
+void Delay_ms(uint32_t ms)
+{
+    SysTick->LOAD = 72000 - 1;
+    SysTick->VAL  = 0;
+    SysTick->CTRL = 0x05;
+    for (uint32_t i = 0; i < ms; i++) {
+        while (!(SysTick->CTRL & (1 << 16)));
+    }
+    SysTick->CTRL = 0;
+}
+
+void USART1_Init(void)
+{
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
+
+    // TX: PA9 复用推挽输出
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    // RX: PA10 浮空输入
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    USART_InitTypeDef USART_InitStructure;
+    USART_InitStructure.USART_BaudRate            = 115200;
+    USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
+    USART_InitStructure.USART_StopBits            = USART_StopBits_1;
+    USART_InitStructure.USART_Parity              = USART_Parity_No;
+    USART_InitStructure.USART_Mode                = USART_Mode_Tx | USART_Mode_Rx;
+    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
+    USART_Init(USART1, &USART_InitStructure);
+
+    USART_Cmd(USART1, ENABLE);
+}
+
+void USART1_SendByte(uint8_t data)
+{
+    USART_SendData(USART1, data);
+    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
+}
+
+void USART1_SendString(const char *str)
+{
+    while (*str) {
+        USART1_SendByte(*str++);
+    }
+}
+
+uint8_t USART1_ReceiveByte(void)
+{
+    while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);
+    return (uint8_t)USART_ReceiveData(USART1);
+}
+
+int main(void)
+{
+    USART1_Init();
+    USART1_SendString("Hello STM32!\r\n");
+
+    while (1) {
+        uint8_t ch = USART1_ReceiveByte();
+        USART1_SendByte(ch); // 回显
+    }
+}
+```
+
+### 2. USART1 中断接收
+
+```c
+#include "stm32f10x.h"
+
+#define RX_BUF_SIZE 64
+uint8_t rx_buffer[RX_BUF_SIZE];
+volatile uint8_t rx_index = 0;
+volatile uint8_t rx_flag   = 0; // 收到一帧完成
+
+void USART1_IRQHandler(void)
+{
+    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
+        uint8_t ch = (uint8_t)USART_ReceiveData(USART1);
+
+        if (rx_index < RX_BUF_SIZE) {
+            rx_buffer[rx_index++] = ch;
+        }
+
+        // 简单帧结束判断:换行符
+        if (ch == '\n' || ch == '\r') {
+            rx_buffer[rx_index - 1] = '\0'; // 替换结束符
+            rx_flag = 1;
+            rx_index = 0;
+        }
+
+        USART_ClearITPendingBit(USART1, USART_IT_RXNE);
+    }
+}
+
+void USART1_Init_IT(void)
+{
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitTypeDef GPIO_InitStructure;
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;
+    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;
+    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IN_FLOATING;
+    GPIO_Init(GPIOA, &GPIO_InitStructure);
+
+    USART_InitTypeDef USART_InitStructure;
+    USART_InitStructure.USART_BaudRate            = 115200;
+    USART_InitStructure.USART_WordLength          = USART_WordLength_8b;
+    USART_InitStructure.USART_StopBits            = USART_StopBits_1;
+    USART_InitStructure.USART_Parity              = USART_Parity_No;
+    USART_InitStructure.USART_Mode                = USART_Mode_Tx | USART_Mode_Rx;
+    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
+    USART_Init(USART1, &USART_InitStructure);
+
+    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
+
+    NVIC_InitTypeDef NVIC_InitStructure;
+    NVIC_InitStructure.NVIC_IRQChannel                   = USART1_IRQn;
+    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStructure.NVIC_IRQChannelSubPriority        = 0;
+    NVIC_InitStructure.NVIC_IRQChannelCmd                = ENABLE;
+    NVIC_Init(&NVIC_InitStructure);
+
+    USART_Cmd(USART1, ENABLE);
+}
+
+int main(void)
+{
+    USART1_Init_IT();
+    while (1) {
+        if (rx_flag) {
+            rx_flag = 0;
+            // 处理 rx_buffer 中的数据
+            // USART1_SendString(rx_buffer); // 回显收到的字符串
+        }
+    }
+}
+```
+
+### 3. printf 重定向 (使用 MicroLIB)
+
+```c
+#include "stm32f10x.h"
+
+// 前提:Keil 中勾选 "Use MicroLIB"
+// 或使用 GCC 的 -specs=nano.specs -u _printf_float
+
+int fputc(int ch, FILE *f)
+{
+    USART_SendData(USART1, (uint8_t)ch);
+    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
+    return ch;
+}
+
+// 重定向后可直接使用:
+// printf("Hello %d\r\n", 123);
+// printf("Voltage: %.2fV\r\n", 3.28);
+```
+
+### 4. HEX 数据包协议
+
+```c
+// 自定义数据包格式:
+// 帧头(0xAA) + 长度(1字节) + 数据(N字节) + 校验和(1字节)
+
+#define PACKET_HEADER 0xAA
+
+uint8_t tx_packet[] = {
+    PACKET_HEADER,
+    0x04,
+    0x01, 0x02, 0x03, 0x04,
+    0x00  // 校验和占位
+};
+
+void USART_SendPacket(uint8_t *data, uint8_t len)
+{
+    uint8_t checksum = 0;
+    USART1_SendByte(PACKET_HEADER);
+    USART1_SendByte(len);
+
+    for (uint8_t i = 0; i < len; i++) {
+        USART1_SendByte(data[i]);
+        checksum += data[i];
+    }
+
+    USART1_SendByte(checksum);
+}
+
+// 接收端状态机
+typedef enum {
+    PKT_WAIT_HEADER,
+    PKT_WAIT_LEN,
+    PKT_WAIT_DATA,
+    PKT_WAIT_CHECKSUM
+} PacketState;
+
+PacketState pkt_state = PKT_WAIT_HEADER;
+uint8_t pkt_len = 0;
+uint8_t pkt_index = 0;
+uint8_t pkt_buf[64];
+uint8_t pkt_checksum = 0;
+
+void USART_RxProcess(uint8_t ch)
+{
+    switch (pkt_state) {
+        case PKT_WAIT_HEADER:
+            if (ch == PACKET_HEADER) pkt_state = PKT_WAIT_LEN;
+            break;
+        case PKT_WAIT_LEN:
+            pkt_len = ch;
+            pkt_index = 0;
+            pkt_checksum = 0;
+            pkt_state = (pkt_len > 0) ? PKT_WAIT_DATA : PKT_WAIT_CHECKSUM;
+            break;
+        case PKT_WAIT_DATA:
+            pkt_buf[pkt_index++] = ch;
+            pkt_checksum += ch;
+            if (pkt_index >= pkt_len) pkt_state = PKT_WAIT_CHECKSUM;
+            break;
+        case PKT_WAIT_CHECKSUM:
+            if (ch == pkt_checksum) {
+                // 校验成功,处理 pkt_buf[0..pkt_len-1]
+            }
+            pkt_state = PKT_WAIT_HEADER;
+            break;
+    }
+}
+```
+
+---
+
+## 常见坑点
+
+1. **TXE vs TC**:
+   - `TXE`(发送寄存器空):数据从 TDR 转移到移位寄存器后置位,此时 TDR 可写入新数据
+   - `TC`(发送完成):移位寄存器也移位完成(所有位发送完毕)后才置位
+   - 发单个字节等待 TXE 足够;发完数据后关串口或进入低功耗前等 TC
+
+2. **USART_SendData 参数**:函数原型 `void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)`,数据位 8 位时用低 8 位;数据位 9 位时需要用 `(uint16_t)` 强制转换,且根据 USART_WordLength_9b 选择 `USART_Parity` 配置
+
+3. **USART_ReceiveData 返回值**:返回 `uint16_t`,8 位模式取低 8 位即可 `(uint8_t)USART_ReceiveData(USARTx)`
+
+4. **波特率误差 < 3%**:USART 使用 16 倍过采样,理论最大误差容限约 3%。72MHz 下计算:
+   - 115200: 72000000/(16×115200) = 39.0625 → DIV_Mantissa=39, DIV_Fraction=1 → 实际 ≈ 115384 → 误差 0.16%
+   - 250000: 72000000/(16×250000) = 18 → 精确
+   - 460800: 72000000/(16×460800) = 9.765 → 误差约 0.2%
+
+5. **GPIO 模式**:
+   - TX(发送):`GPIO_Mode_AF_PP`(复用推挽输出),USART 控制引脚
+   - RX(接收):`GPIO_Mode_IN_FLOATING` 或 `GPIO_Mode_IPU`。浮空输入更适合外部有上拉的情况;如果外部没有上拉,用 `IPU` 避免不确定电平
+
+6. **MicroLIB 注意**:使用 printf 时必须勾选 Use MicroLIB(Keil),否则链接失败。GCC 使用 `-specs=nano.specs` 和 `-u _printf_float`
+
+7. **USART_Cmd 必须最后调用**:所有 USART 配置完成后才调用 `USART_Cmd(USARTx, ENABLE)`。如果在配置中断或 GPIO 之前就使能,可能产生错误中断
+
+8. **中断处理**:RXNE 中断中必须尽早读取 DR(`USART_ReceiveData`),否则 ORE 溢出标志置位,后续接收停止
+
+9. **CH340 连接**:交叉连接 — STM32 TX → CH340 RX,STM32 RX → CH340 TX,GND 共地
+
+10. **USART_GetFlagStatus vs USART_GetITStatus**:
+    - `USART_GetFlagStatus` 检查所有状态标志(包括不产生中断的)
+    - `USART_GetITStatus` 检查已使能的中断标志,不可混用

+ 503 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/13-I2C通信.md

@@ -0,0 +1,503 @@
+# 13-I2C通信
+
+## 原理
+
+### I2C 协议
+
+- **类型**:同步半双工
+- **总线**:SCL (时钟) + SDA (数据)
+- **设备**:主设备 (Master) 发起通信,从设备 (Slave) 响应地址
+
+```
+         +---+
+SCL -----+   +---+
+         |   |
+SDA -----+   +---------
+         |   |
+      Master    Slave
+```
+
+### 通信时序
+
+```
+        起始条件       字节传输(MSB first)     应答       停止条件
+         _____       _____   _____   _____      _____    _____
+SCL     |     |_____|     |_|     |_|     |____|     |__|     |
+        _       ___     ___     ___     ___    ___       ___
+SDA    X_| 7位地址 |_| R/W |_| 数据MSB |_|  ... |_| 应答 |_X_| 
+       |_|       |_|     |_|        |_|      |_|     |_|    |_|
+```
+
+**时序规则**:
+1. **起始条件 (Start)**:SCL 高电平时,SDA 下降沿
+2. **停止条件 (Stop)**:SCL 高电平时,SDA 上升沿
+3. **数据变化**:SCL 低电平时 SDA 变化
+4. **数据采样**:SCL 高电平时采样 SDA
+
+**数据帧格式**:
+- 起始条件
+- 7位从机地址 + R/W 位 (0=写,1=读)
+- 从机应答 (ACK/NACK)
+- 数据字节 (MSB first) + ACK/NACK
+- 停止条件
+
+### MPU6050
+
+- **类型**:6轴运动跟踪 (3轴加速度 + 3轴陀螺仪)
+- **接口**:I2C (最大 400kHz)
+- **从机地址**:0x68 (7位地址)
+- **关键寄存器**:
+
+| 寄存器 | 地址 | 说明 |
+|--------|------|------|
+| WHO_AM_I | 0x75 | 读回 0x68 |
+| PWR_MGMT_1 | 0x6B | 电源管理 (写 0x00 唤醒) |
+| SMPLRT_DIV | 0x19 | 采样率分频 |
+| CONFIG | 0x1A | 配置 |
+| GYRO_CONFIG | 0x1B | 陀螺仪配置 |
+| ACCEL_CONFIG | 0x1C | 加速度计配置 |
+| ACCEL_XOUT_H | 0x3B | 加速度 X 高字节 |
+| ACCEL_XOUT_L | 0x3C | 加速度 X 低字节 |
+| ACCEL_YOUT_H | 0x3D | 加速度 Y 高字节 |
+| ACCEL_YOUT_L | 0x3E | 加速度 Y 低字节 |
+| ACCEL_ZOUT_H | 0x3F | 加速度 Z 高字节 |
+| ACCEL_ZOUT_L | 0x40 | 加速度 Z 低字节 |
+| GYRO_XOUT_H | 0x43 | 陀螺仪 X 高字节 |
+| GYRO_XOUT_L | 0x44 | 陀螺仪 X 低字节 |
+| GYRO_YOUT_H | 0x45 | 陀螺仪 Y 高字节 |
+| GYRO_YOUT_L | 0x46 | 陀螺仪 Y 低字节 |
+| GYRO_ZOUT_H | 0x47 | 陀螺仪 Z 高字节 |
+| GYRO_ZOUT_L | 0x48 | 陀螺仪 Z 低字节 |
+
+---
+
+## API 表格
+
+### 软件 I2C (GPIO 模拟)
+
+| 函数 | 说明 |
+|------|------|
+| `void I2C_Start(void)` | 起始条件 |
+| `void I2C_Stop(void)` | 停止条件 |
+| `void I2C_SendByte(uint8_t Byte)` | 发送一个字节 |
+| `uint8_t I2C_ReceiveByte(void)` | 接收一个字节 |
+| `void I2C_SendAck(uint8_t AckBit)` | 发送应答 (0=ACK, 1=NACK) |
+| `uint8_t I2C_ReceiveAck(void)` | 接收应答 |
+| `void MPU6050_Init(void)` | 初始化 MPU6050 |
+| `uint8_t MPU6050_GetID(void)` | 读取 WHO_AM_I |
+| `void MPU6050_GetData(int16_t* accX, ..., int16_t* gyroZ)` | 读取全部原始数据 |
+
+### 硬件 I2C (STM32 I2C 外设)
+
+| 函数 | 说明 |
+|------|------|
+| `I2C_InitTypeDef` | 配置结构体 |
+| `void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct)` | 初始化 I2C |
+| `void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState)` | 使能 I2C |
+| `void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState)` | 产生起始条件 |
+| `void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState)` | 产生停止条件 |
+| `void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data)` | 发送数据 |
+| `uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx)` | 接收数据 |
+| `void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState)` | 配置应答 |
+| `ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)` | 检测事件 |
+| `ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT)` | 获取中断状态 |
+| `void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction)` | 发送7位地址 |
+
+**关键事件标志**:
+
+| 事件宏 | 说明 |
+|--------|------|
+| `I2C_EVENT_MASTER_MODE_SELECT` | EV5: 起始条件已发送 |
+| `I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED` | EV6: 地址已发送 (主发模式) |
+| `I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED` | EV6: 地址已发送 (主收模式) |
+| `I2C_EVENT_MASTER_BYTE_TRANSMITTING` | EV8: 数据已发送 (DR 空) |
+| `I2C_EVENT_MASTER_BYTE_TRANSMITTED` | EV8_2: 数据已发送 (BTF=1) |
+| `I2C_EVENT_MASTER_BYTE_RECEIVED` | EV7: 数据已接收 (RXNE=1) |
+
+**I2C_InitTypeDef**:
+
+| 成员 | 可选值 | 说明 |
+|------|--------|------|
+| `I2C_Mode` | `I2C_Mode_I2C` | I2C 模式 |
+| `I2C_ClockSpeed` | 0~400000 | SCL 时钟频率 (Hz) |
+| `I2C_DutyCycle` | `I2C_DutyCycle_16_9` / `I2C_DutyCycle_2` | 快速模式占空比 |
+| `I2C_Ack` | `I2C_Ack_Enable` / `I2C_Ack_Disable` | 应答使能 |
+| `I2C_AcknowledgedAddress` | `I2C_AcknowledgedAddress_7bit` / `_10bit` | 应答地址位数 |
+
+---
+
+## 完整代码示例
+
+### 软件 I2C 读写 MPU6050
+
+```c
+/*------------------------------------------------
+ * SoftI2C_MPU6050.c
+ * 软件 I2C (GPIO 模拟) 读写 MPU6050
+ * GPIO: PB0(SCL), PB1(SDA)
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+#include <stdint.h>
+
+#define MPU6050_ADDR     0x68    // 7 位地址
+#define MPU6050_ADDR_W   (MPU6050_ADDR << 1)      // 写地址 0xD0
+#define MPU6050_ADDR_R   ((MPU6050_ADDR << 1) | 1) // 读地址 0xD1
+
+/* 引脚定义 */
+#define I2C_SCL_PORT     GPIOB
+#define I2C_SCL_PIN      GPIO_Pin_0
+#define I2C_SDA_PORT     GPIOB
+#define I2C_SDA_PIN      GPIO_Pin_1
+
+/* 简易延时 (约 5us @72MHz) */
+static void delay_us(void)
+{
+    uint32_t i;
+    for (i = 0; i < 10; i++);
+}
+
+/* ---- I2C 时序函数 ---- */
+
+/* 起始条件:SCL 高时 SDA 下降沿 */
+void I2C_Start(void)
+{
+    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
+    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    delay_us();
+    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);  // SDA 下降沿
+    delay_us();
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+}
+
+/* 停止条件:SCL 高时 SDA 上升沿 */
+void I2C_Stop(void)
+{
+    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
+    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    delay_us();
+    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);     // SDA 上升沿
+    delay_us();
+}
+
+/* 发送一个字节 (MSB first) */
+void I2C_SendByte(uint8_t Byte)
+{
+    uint8_t i;
+    for (i = 0; i < 8; i++)
+    {
+        GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+        if (Byte & 0x80)
+            GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
+        else
+            GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
+        Byte <<= 1;
+        delay_us();
+        GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+        delay_us();
+    }
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+}
+
+/* 接收一个字节 (MSB first) */
+uint8_t I2C_ReceiveByte(void)
+{
+    uint8_t i, Byte = 0;
+    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);   // 释放 SDA
+    for (i = 0; i < 8; i++)
+    {
+        GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+        delay_us();
+        GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+        Byte <<= 1;
+        if (GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN))
+            Byte |= 0x01;
+        delay_us();
+    }
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    return Byte;
+}
+
+/* 发送应答:0=ACK, 1=NACK */
+void I2C_SendAck(uint8_t AckBit)
+{
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    if (AckBit == 0)
+        GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);   // ACK
+    else
+        GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);     // NACK
+    delay_us();
+    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    delay_us();
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);         // 释放 SDA
+}
+
+/* 接收应答:返回 0=ACK, 1=NACK */
+uint8_t I2C_ReceiveAck(void)
+{
+    uint8_t AckBit;
+    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);       // 释放 SDA
+    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    delay_us();
+    AckBit = GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN);
+    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
+    return AckBit;
+}
+
+/* ---- MPU6050 操作 ---- */
+
+/* 向 MPU6050 写一个字节 */
+void MPU6050_WriteByte(uint8_t RegAddr, uint8_t Data)
+{
+    I2C_Start();
+    I2C_SendByte(MPU6050_ADDR_W);
+    I2C_ReceiveAck();
+    I2C_SendByte(RegAddr);
+    I2C_ReceiveAck();
+    I2C_SendByte(Data);
+    I2C_ReceiveAck();
+    I2C_Stop();
+}
+
+/* 从 MPU6050 读一个字节 */
+uint8_t MPU6050_ReadByte(uint8_t RegAddr)
+{
+    uint8_t Data;
+    I2C_Start();
+    I2C_SendByte(MPU6050_ADDR_W);
+    I2C_ReceiveAck();
+    I2C_SendByte(RegAddr);
+    I2C_ReceiveAck();
+    I2C_Start();                                // 重复起始
+    I2C_SendByte(MPU6050_ADDR_R);
+    I2C_ReceiveAck();
+    Data = I2C_ReceiveByte();
+    I2C_SendAck(1);                             // NACK
+    I2C_Stop();
+    return Data;
+}
+
+/* 读取多个连续字节 */
+void MPU6050_ReadMultiBytes(uint8_t RegAddr, uint8_t* pData, uint8_t Len)
+{
+    uint8_t i;
+    I2C_Start();
+    I2C_SendByte(MPU6050_ADDR_W);
+    I2C_ReceiveAck();
+    I2C_SendByte(RegAddr);
+    I2C_ReceiveAck();
+    I2C_Start();
+    I2C_SendByte(MPU6050_ADDR_R);
+    I2C_ReceiveAck();
+    for (i = 0; i < Len; i++)
+    {
+        pData[i] = I2C_ReceiveByte();
+        if (i < Len - 1)
+            I2C_SendAck(0);                     // ACK
+        else
+            I2C_SendAck(1);                     // NACK (最后一个字节)
+    }
+    I2C_Stop();
+}
+
+/* MPU6050 初始化 */
+void MPU6050_Init(void)
+{
+    /* 唤醒:写 0x00 到 PWR_MGMT_1 */
+    MPU6050_WriteByte(0x6B, 0x00);
+    /* 关闭 SLEEP 模式 (已包含在 0x00) */
+}
+
+/* 读 WHO_AM_I 寄存器 */
+uint8_t MPU6050_GetID(void)
+{
+    return MPU6050_ReadByte(0x75);
+}
+
+/* 读取 6 轴原始数据 */
+void MPU6050_GetData(int16_t* accX, int16_t* accY, int16_t* accZ,
+                     int16_t* gyroX, int16_t* gyroY, int16_t* gyroZ)
+{
+    uint8_t buf[14];
+    MPU6050_ReadMultiBytes(0x3B, buf, 14);
+
+    *accX  = ((int16_t)buf[0]  << 8) | buf[1];
+    *accY  = ((int16_t)buf[2]  << 8) | buf[3];
+    *accZ  = ((int16_t)buf[4]  << 8) | buf[5];
+    *gyroX = ((int16_t)buf[8]  << 8) | buf[9];
+    *gyroY = ((int16_t)buf[10] << 8) | buf[11];
+    *gyroZ = ((int16_t)buf[12] << 8) | buf[13];
+}
+
+/* ---- GPIO 初始化 ---- */
+void SoftI2C_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct;
+
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;       // 开漏输出
+    GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
+    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+    GPIO_SetBits(GPIOB, I2C_SCL_PIN | I2C_SDA_PIN);     // 释放总线 (高电平)
+}
+
+/* ---- 主函数 ---- */
+int main(void)
+{
+    int16_t accX, accY, accZ;
+    int16_t gyroX, gyroY, gyroZ;
+    uint8_t id;
+
+    SoftI2C_Init();
+    MPU6050_Init();
+    id = MPU6050_GetID();
+
+    /* 若 id == 0x68, 表示 MPU6050 正常 */
+    while (1)
+    {
+        MPU6050_GetData(&accX, &accY, &accZ, &gyroX, &gyroY, &gyroZ);
+        /* 处理数据 ... */
+    }
+}
+```
+
+### 硬件 I2C 读写 MPU6050
+
+```c
+/*------------------------------------------------
+ * HW_I2C_MPU6050.c
+ * 硬件 I2C1 (PB6=SCL, PB7=SDA)
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+#define MPU6050_ADDR    0x68
+
+static void I2C_Config(void)
+{
+    I2C_InitTypeDef I2C_InitStruct;
+
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
+
+    /* I2C1: PB6=SCL, PB7=SDA, 复用开漏输出 */
+    GPIO_InitTypeDef GPIO_InitStruct;
+    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;        // 复用开漏
+    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
+    I2C_InitStruct.I2C_ClockSpeed = 100000;             // 100kHz 标准模式
+    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
+    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
+    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
+    I2C_Init(I2C1, &I2C_InitStruct);
+
+    I2C_Cmd(I2C1, ENABLE);
+}
+
+static void MPU6050_WriteByte(uint8_t RegAddr, uint8_t Data)
+{
+    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
+
+    I2C_GenerateSTART(I2C1, ENABLE);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));     // EV5
+
+    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Transmitter);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // EV6
+
+    I2C_SendData(I2C1, RegAddr);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // EV8_2
+
+    I2C_SendData(I2C1, Data);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
+
+    I2C_GenerateSTOP(I2C1, ENABLE);
+}
+
+static uint8_t MPU6050_ReadByte(uint8_t RegAddr)
+{
+    uint8_t Data;
+    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
+
+    /* 写寄存器地址 */
+    I2C_GenerateSTART(I2C1, ENABLE);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
+
+    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Transmitter);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
+
+    I2C_SendData(I2C1, RegAddr);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
+
+    /* 重复起始,读数据 */
+    I2C_GenerateSTART(I2C1, ENABLE);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
+
+    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Receiver);
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
+
+    /* 最后一个字节前 NACK + STOP */
+    I2C_AcknowledgeConfig(I2C1, DISABLE);
+    I2C_GenerateSTOP(I2C1, ENABLE);
+
+    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));   // EV7
+    Data = I2C_ReceiveData(I2C1);
+
+    I2C_AcknowledgeConfig(I2C1, ENABLE);
+    return Data;
+}
+
+int main(void)
+{
+    uint8_t id;
+
+    I2C_Config();
+    /* 唤醒 MPU6050 */
+    MPU6050_WriteByte(0x6B, 0x00);
+    id = MPU6050_ReadByte(0x75);
+
+    while (1);
+}
+```
+
+---
+
+## 常见坑点
+
+1. **应答标志位处理**
+   - 主接收最后一个字节**必须发 NACK**(非应答),否则从机继续发数据导致总线异常
+   - 硬件 I2C 下:最后一个字节接收前关 ACK (I2C_AcknowledgeConfig(DISABLE)),同时发 STOP
+
+2. **超时处理**
+   - While 轮询事件标志时务必加**超时**,否则从机无响应会死循环
+   - 建议实现 `while(!event && timeout--)` 模式
+
+3. **I2C 时钟不要超过 400kHz**
+   - 标准模式 100kHz,快速模式 400kHz
+   - 超频可能导致通信不稳定
+
+4. **软件 I2C 的 GPIO 模式**
+   - SDA 和 SCL 必须用**开漏输出 (GPIO_Mode_Out_OD)** + 外部上拉电阻 (4.7kΩ)
+   - 开漏才能实现"线与",多主机时不会短路
+
+5. **硬件 I2C 的 GPIO 模式**
+   - 使用**复用开漏 (GPIO_Mode_AF_OD)**,不是普通推挽
+   - 时钟使能注意:I2C1 挂 APB1,GPIOB 挂 APB2
+
+6. **重复起始条件**
+   - 读操作时必须发重复起始 (Restart),不可先停再启
+   - 软件 I2C 只需再调一次 I2C_Start()
+   - 硬件 I2C 重复调 I2C_GenerateSTART 即可
+
+7. **MPU6050 唤醒**
+   - 上电后 MPU6050 默认 SLEEP 状态,必须写 PWR_MGMT_1 (0x6B) = 0x00 唤醒
+
+8. **地址左移**
+   - STM32 库函数 I2C_Send7bitAddress 要求**7位地址**(0x68),库内部左移加 R/W
+   - 软件模拟时必须手动左移:0x68 << 1 = 0xD0 (写) / 0xD1 (读)

+ 383 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/14-SPI通信.md

@@ -0,0 +1,383 @@
+# 14-SPI通信
+
+## 原理
+
+### SPI 协议
+
+- **类型**:同步全双工
+- **线制**:四线制 (SCK + MOSI + MISO + CS)
+- **设备**:一主多从 (通过 CS 片选)
+
+```
+           +-------+
+           |Master |---+ MISO
+           |       |---+ MOSI
+           |       |---+ SCK
+           |       |---+ CS1
+           |       |---+ CS2
+           +-------+   |
+             |  |  |   |
+           +-------+   |
+           |Slave1 |---+---+
+           +-------+   |   |
+           +-------+   |   |
+           |Slave2 |---+---+
+           +-------+
+```
+
+### 5 参数模型
+
+| 参数 | 可选值 | 说明 |
+|------|--------|------|
+| 波特率 | f_PCLK / {2,4,8,16,32,64,128,256} | 通信速率 |
+| 数据位 | 8 位 / 16 位 | 每帧数据宽度 |
+| 比特序 | MSB first / LSB first | 高位/低位先发 |
+| CPOL (时钟极性) | 0=空闲低 / 1=空闲高 | SCK 空闲电平 |
+| CPHA (时钟相位) | 0=前沿采样 / 1=后沿采样 | 采样时刻 |
+
+### 4 种 SPI 模式
+
+```
+Mode 0 (CPOL=0, CPHA=0) ★最常用
+  SCK   ____/``\__/``\__/``\__
+  MOSI      X---X---X---X
+  MISO      X---X---X---X
+  采样在上升沿 (前沿)
+
+Mode 1 (CPOL=0, CPHA=1)
+  SCK   ____/``\__/``\__/``\__
+  采样在下降沿 (后沿)
+
+Mode 2 (CPOL=1, CPHA=0)
+  SCK   ```\____/``\____/``\__
+  采样在下降沿 (前沿)
+
+Mode 3 (CPOL=1, CPHA=1)
+  SCK   ```\____/``\____/``\__
+  采样在上升沿 (后沿)
+```
+
+### W25Q64
+
+| 参数 | 值 |
+|------|------|
+| 容量 | 64 Mbit = 8 MB |
+| 页 | 256 字节 |
+| 扇区 (Sector) | 4 KB (16 页) |
+| 块 (Block) | 64 KB (16 扇区) |
+| 最大时钟 | 104 MHz |
+| 擦除寿命 | 约 10 万次 |
+
+**关键指令**:
+
+| 指令 | 码值 | 说明 |
+|------|------|------|
+| Write Enable | 0x06 | 写使能 (每写之前必发) |
+| Write Disable | 0x04 | 写禁止 |
+| Read Status Register | 0x05 | 读状态寄存器 (BUSY=bit0) |
+| Read Data | 0x03 | 读数据 (3 字节地址) |
+| Page Program | 0x02 | 页编程 (最多 256 字节) |
+| Sector Erase | 0x20 | 扇区擦除 (4KB) |
+| Block Erase 64KB | 0xD8 | 块擦除 |
+| Chip Erase | 0xC7 | 整片擦除 |
+| Power Down | 0xB9 | 掉电 |
+| Release Power Down | 0xAB | 唤醒 |
+| Read ID | 0x90 | 读制造商/设备 ID |
+| Read Unique ID | 0x4B | 读唯一 ID |
+
+**Flash 特性**:只能 1→0 (写 0),擦除恢复为 1。写入前必须擦除。
+
+---
+
+## API 表格
+
+### 软件 SPI
+
+| 函数 | 说明 |
+|------|------|
+| `void SPI_Start(void)` | CS 拉低 |
+| `void SPI_Stop(void)` | CS 拉高 |
+| `uint8_t SPI_SwapByte(uint8_t Byte)` | 交换一个字节 (发送+接收) |
+
+### 硬件 SPI (STM32 SPI)
+
+| 函数 | 说明 |
+|------|------|
+| `SPI_InitTypeDef` | 配置结构体 |
+| `void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct)` | 初始化 SPI |
+| `void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState)` | 使能 SPI |
+| `void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data)` | 发送数据 |
+| `uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx)` | 接收数据 |
+| `FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint32_t SPI_I2S_FLAG)` | 获取标志状态 |
+| `void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint32_t SPI_I2S_IT, FunctionalState NewState)` | 中断配置 |
+
+**SPI_InitTypeDef**:
+
+| 成员 | 可选值 | 说明 |
+|------|--------|------|
+| `SPI_Direction` | `SPI_Direction_2Lines_FullDuplex` | 双线全双工 |
+| `SPI_Mode` | `SPI_Mode_Master` / `SPI_Mode_Slave` | 主/从模式 |
+| `SPI_DataSize` | `SPI_DataSize_8b` / `SPI_DataSize_16b` | 数据位 |
+| `SPI_CPOL` | `SPI_CPOL_Low` / `SPI_CPOL_High` | 时钟极性 |
+| `SPI_CPHA` | `SPI_CPHA_1Edge` / `SPI_CPHA_2Edge` | 时钟相位 |
+| `SPI_NSS` | `SPI_NSS_Soft` / `SPI_NSS_Hard` | NSS 控制方式 |
+| `SPI_BaudRatePrescaler` | `SPI_BaudRatePrescaler_*` | 波特率预分频 |
+| `SPI_FirstBit` | `SPI_FirstBit_MSB` / `SPI_FirstBit_LSB` | 比特序 |
+| `SPI_CRCPolynomial` | 7 (默认) | CRC 多项式 |
+
+**常用标志**:
+
+| 标志 | 说明 |
+|------|------|
+| `SPI_FLAG_TXE` | 发送缓冲区空 (可写 DR) |
+| `SPI_FLAG_RXNE` | 接收缓冲区非空 (可读 DR) |
+| `SPI_FLAG_BSY` | 忙标志 |
+| `SPI_FLAG_OVR` | 溢出错误 |
+
+---
+
+## 完整代码示例
+
+### 硬件 SPI 读写 W25Q64
+
+```c
+/*------------------------------------------------
+ * SPI_W25Q64.c
+ * 硬件 SPI1: PA5(SCK), PA6(MISO), PA7(MOSI), PA4(CS)
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+#include <stdint.h>
+#include <string.h>
+
+/* SPI1 引脚 */
+#define W25Q64_CS_PORT      GPIOA
+#define W25Q64_CS_PIN       GPIO_Pin_4
+
+/* W25Q64 指令 */
+#define W25X_WriteEnable    0x06
+#define W25X_WriteDisable   0x04
+#define W25X_ReadStatusReg  0x05
+#define W25X_ReadData       0x03
+#define W25X_PageProgram    0x02
+#define W25X_SectorErase    0x20
+#define W25X_ReadID         0x90
+
+/* ---- CS 控制 ---- */
+static void W25Q64_CS_Low(void)
+{
+    GPIO_ResetBits(W25Q64_CS_PORT, W25Q64_CS_PIN);
+}
+static void W25Q64_CS_High(void)
+{
+    GPIO_SetBits(W25Q64_CS_PORT, W25Q64_CS_PIN);
+}
+
+/* ---- 硬件 SPI 初始化 ---- */
+static void SPI_Config(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct;
+    SPI_InitTypeDef  SPI_InitStruct;
+
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);
+
+    /* PA5(SCK), PA7(MOSI): 复用推挽 */
+    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
+    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
+    GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+    /* PA6(MISO): 浮空输入 */
+    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
+    GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+    /* PA4(CS): 推挽输出 */
+    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
+    GPIO_Init(GPIOA, &GPIO_InitStruct);
+    GPIO_SetBits(GPIOA, GPIO_Pin_4);   // CS 高 = 释放
+
+    /* SPI1: Mode0, 主模式, 8位, 18MHz */
+    SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
+    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
+    SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
+    SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
+    SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
+    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
+    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 72/4=18MHz
+    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
+    SPI_InitStruct.SPI_CRCPolynomial = 7;
+    SPI_Init(SPI1, &SPI_InitStruct);
+
+    SPI_Cmd(SPI1, ENABLE);
+}
+
+/* ---- SPI 字节交换 ---- */
+static uint8_t SPI_SwapByte(uint8_t Byte)
+{
+    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
+    SPI_I2S_SendData(SPI1, Byte);
+
+    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
+    return (uint8_t)SPI_I2S_ReceiveData(SPI1);
+}
+
+/* ---- 写指令/地址 (CS 已拉低) ---- */
+static void W25Q64_WriteCmd(uint8_t cmd)
+{
+    SPI_SwapByte(cmd);
+}
+
+/* ---- 写使能 (每次擦/写之前必须) ---- */
+static void W25Q64_WriteEnable(void)
+{
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(W25X_WriteEnable);
+    W25Q64_CS_High();
+}
+
+/* ---- 等待空闲 (BUSY=0) ---- */
+static void W25Q64_WaitBusy(void)
+{
+    uint8_t sr;
+    do {
+        W25Q64_CS_Low();
+        W25Q64_WriteCmd(W25X_ReadStatusReg);
+        sr = SPI_SwapByte(0xFF);
+        W25Q64_CS_High();
+    } while (sr & 0x01);
+}
+
+/* ---- 读 ID ---- */
+uint16_t W25Q64_ReadID(void)
+{
+    uint16_t id;
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(W25X_ReadID);
+    SPI_SwapByte(0x00); SPI_SwapByte(0x00); SPI_SwapByte(0x00); // dummy
+    id  = (uint16_t)SPI_SwapByte(0xFF) << 8;   // 制造商 ID
+    id |= SPI_SwapByte(0xFF);                   // 设备 ID
+    W25Q64_CS_High();
+    return id;
+    // W25Q64: 制造商=0xEF, 设备=0x16 (EF16)
+}
+
+/* ---- 读数据 ---- */
+void W25Q64_ReadData(uint32_t addr, uint8_t* buf, uint32_t len)
+{
+    uint32_t i;
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(W25X_ReadData);
+    SPI_SwapByte((addr >> 16) & 0xFF);
+    SPI_SwapByte((addr >> 8)  & 0xFF);
+    SPI_SwapByte(addr         & 0xFF);
+    for (i = 0; i < len; i++)
+        buf[i] = SPI_SwapByte(0xFF);
+    W25Q64_CS_High();
+}
+
+/* ---- 扇区擦除 (4KB) ---- */
+void W25Q64_SectorErase(uint32_t addr)
+{
+    W25Q64_WriteEnable();
+    W25Q64_WaitBusy();
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(W25X_SectorErase);
+    SPI_SwapByte((addr >> 16) & 0xFF);
+    SPI_SwapByte((addr >> 8)  & 0xFF);
+    SPI_SwapByte(addr         & 0xFF);
+    W25Q64_CS_High();
+    W25Q64_WaitBusy();
+}
+
+/* ---- 页编程 (最多 256 字节) ---- */
+void W25Q64_PageProgram(uint32_t addr, const uint8_t* data, uint32_t len)
+{
+    uint32_t i;
+    if (len > 256) len = 256;
+
+    W25Q64_WriteEnable();
+    W25Q64_WaitBusy();
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(W25X_PageProgram);
+    SPI_SwapByte((addr >> 16) & 0xFF);
+    SPI_SwapByte((addr >> 8)  & 0xFF);
+    SPI_SwapByte(addr         & 0xFF);
+    for (i = 0; i < len; i++)
+        SPI_SwapByte(data[i]);
+    W25Q64_CS_High();
+    W25Q64_WaitBusy();
+}
+
+/* ---- 整片擦除 ---- */
+void W25Q64_ChipErase(void)
+{
+    W25Q64_WriteEnable();
+    W25Q64_WaitBusy();
+    W25Q64_CS_Low();
+    W25Q64_WriteCmd(0xC7);
+    W25Q64_CS_High();
+    W25Q64_WaitBusy();
+}
+
+/* ---- 主函数 ---- */
+int main(void)
+{
+    uint8_t buf[256];
+    uint16_t chipID;
+
+    /* 初始化 SysTick 延时 (假设已在其他地方实现 Delay_ms) */
+    SPI_Config();
+
+    chipID = W25Q64_ReadID();
+
+    /* 写入示例 */
+    uint8_t writeData[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
+
+    W25Q64_SectorErase(0x000000);       // 擦除扇区 0
+    W25Q64_PageProgram(0x000000, writeData, 8);  // 写 8 字节
+    W25Q64_ReadData(0x000000, buf, 8);            // 读回验证
+
+    while (1);
+}
+```
+
+---
+
+## 常见坑点
+
+1. **SPI1 与 SPI2 挂载不同总线**
+   - SPI1 → APB2 (72MHz)
+   - SPI2 → APB1 (36MHz)
+   - 配置波特率时注意 APB 时钟不同,SPI2 最高 18MHz (36/2)
+
+2. **NSS 软件控制更灵活**
+   - `SPI_NSS_Soft` 模式下,CS 用**普通 GPIO 推挽输出**手动拉低/拉高
+   - 硬件 NSS 模式 (NSS_Hard) 在多从机时麻烦,不推荐
+
+3. **发送的同时也在接收**
+   - SPI 是全双工,发送一个字节的同时会收到一个字节
+   - 读数据时也必须发送 (dummy byte = 0xFF)
+   - 写数据时收到的字节应丢弃
+
+4. **W25Q64 写入前必擦除**
+   - Flash 只能 1→0,写入前需要先擦除 (恢复全 1)
+   - 擦除以扇区 (4KB) 为单位,不能单独擦字节
+   - 只写 0 不擦除也能成功,但读回会不对
+
+5. **等待 BUSY**
+   - 每次擦除/写入后必须查状态寄存器 (0x05) bit0,直到为 0
+   - 擦除扇区约 45~400ms,页编程约 1~3ms
+
+6. **页编程不可跨页**
+   - PageProgram 只能在 256 字节页内连续写
+   - 跨页地址会自动回卷,不是续写到下一页
+
+7. **Mode0 最常用**
+   - 绝大多数 SPI 芯片默认 Mode0 (CPOL=0, CPHA=0)
+   - CPOL/CPHA 不匹配会导致通信失败
+
+8. **GPIO 模式区分**
+   - MOSI/SCK/CS → 推挽输出 (GPIO_Mode_Out_PP) 或复用推挽 (GPIO_Mode_AF_PP)
+   - MISO → 浮空输入 (GPIO_Mode_IN_FLOATING)

+ 330 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/15-BKP-RTC.md

@@ -0,0 +1,330 @@
+# 15-BKP-RTC
+
+## 原理
+
+### Unix 时间戳
+
+- **定义**:从 1970-01-01 00:00:00 UTC 到现在的秒数
+- **2038 问题**:32位有符号整数将在 2038-01-19 03:14:07 溢出
+- **闰秒**:IETF 已废弃闰秒,Unix 时间戳连续递增
+
+### BKP (Backup Registers)
+
+```
+           ┌──────────────┐
+VBAT ─────┤              │
+           │  BKP Domain  │
+VDD 断电    │ 10×16bit Reg │
+不影响     │ RTC 寄存器   │
+           └──────────────┘
+```
+
+- **容量**:10 个 16 位备份寄存器 (BKP_DR1 ~ BKP_DR10)
+- **供电**:VDD 断电后由 VBAT 供电
+- **功能**:存储系统断电不丢失的数据 (如复位次数、校准值)
+- **保护**:受写保护,需 `PWR_BackupAccessCmd(ENABLE)` 解锁
+
+### RTC (Real-Time Clock)
+
+```
+           ┌──────────────┐
+LSE 32.768kHz ──┤              │
+(或 LSI/HSE)    │  RTC Prescaler│── 1Hz ──▶ 32位计数器
+           │  (RL[14:0])  │
+           └──────────────┘
+                │
+       ┌────────┴────────┐
+       │                 │
+   秒中断(SEC)       闹钟中断(ALR)
+```
+
+**时钟源选择**:
+
+| 时钟源 | 频率 | 特点 |
+|--------|------|------|
+| LSE | 32.768 kHz | 最精确,需外接晶振 |
+| LSI | 40 kHz | 内置 RC,精度差 |
+| HSE/128 | HSE/128 | 需 HSE 存在 |
+
+**RTC 配置流程**:
+```
+1. 使能 PWR + BKP 时钟
+2. 使能备份域访问
+3. 选择 RTC 时钟源 (LSE)
+4. 使能 RTC 时钟
+5. 等待 RTC 同步 (WaitForSynchro)
+6. 配置预分频 (32767 → 1秒)
+7. 设置计数值 (即时间)
+```
+
+---
+
+## API 表格
+
+### BKP
+
+| 函数 | 说明 |
+|------|------|
+| `void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data)` | 写备份寄存器 |
+| `uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR)` | 读备份寄存器 |
+| `void BKP_TamperPinCmd(FunctionalState NewState)` | 侵入检测引脚使能 |
+| `void BKP_ITConfig(FunctionalState NewState)` | 侵入中断使能 |
+| `FlagStatus BKP_GetFlagStatus(void)` | 获取侵入检测标志 |
+
+### RTC
+
+| 函数 | 说明 |
+|------|------|
+| `void RTC_SetPrescaler(uint32_t Prescaler)` | 设置预分频值 (RL) |
+| `void RTC_SetCounter(uint32_t CounterValue)` | 设置计数值 |
+| `uint32_t RTC_GetCounter(void)` | 获取计数值 |
+| `void RTC_SetAlarm(uint32_t AlarmValue)` | 设置闹钟值 |
+| `uint32_t RTC_GetDivider(void)` | 获取当前分频值 |
+| `void RTC_WaitForLastTask(void)` | 等待上次操作完成 |
+| `void RTC_WaitForSynchro(void)` | 等待 RTC 同步 |
+| `void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState)` | 中断配置 |
+
+**RTC 中断类型**:
+
+| 宏 | 说明 |
+|----|------|
+| `RTC_IT_OW` (Overflow) | 溢出中断 | ← 注意:标准库中为 `RTC_IT_OW` |
+| `RTC_IT_SEC` (Second) | 秒中断 |
+| `RTC_IT_ALR` (Alarm) | 闹钟中断 |
+
+**RTC 中断映射到 EXTI**:
+- RTC 秒中断 → EXTI 线 17
+- RTC 闹钟中断 → EXTI 线 17
+- RTC 溢出中断 → EXTI 线 19
+
+---
+
+## 完整代码示例
+
+### 读写备份寄存器 — 保存复位次数
+
+```c
+/*------------------------------------------------
+ * BKP_Example.c
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void BKP_Config(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
+    PWR_BackupAccessCmd(ENABLE);    // 使能备份域访问
+}
+
+uint16_t BKP_ReadResetCount(void)
+{
+    return BKP_ReadBackupRegister(BKP_DR1);
+}
+
+void BKP_SaveResetCount(uint16_t count)
+{
+    BKP_WriteBackupRegister(BKP_DR1, count);
+}
+
+int main(void)
+{
+    uint16_t resetCount;
+
+    BKP_Config();
+    resetCount = BKP_ReadResetCount();
+    resetCount++;
+    BKP_SaveResetCount(resetCount);
+
+    while (1);
+}
+```
+
+### RTC 实时时钟 + 秒中断显示
+
+```c
+/*------------------------------------------------
+ * RTC_Example.c
+ * LSE 32.768kHz → 1秒中断, USART1 打印时间
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+#include <stdio.h>
+
+static void Delay_ms(uint32_t ms);
+
+/* ---- RTC 初始化 ---- */
+/* 时间参考:假设 Unix 时间戳基准 2025-01-01 00:00:00 = 1735689600 */
+#define TIME_EPOCH  1735689600UL
+
+void RTC_Config(void)
+{
+    /* 使能 PWR + BKP */
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
+    PWR_BackupAccessCmd(ENABLE);
+
+    /* 检查是否已初始化 (通过备份寄存器标记) */
+    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
+    {
+        /* ----- 首次配置 RTC ----- */
+        RCC_LSEConfig(RCC_LSE_ON);                     // 开启 LSE
+        while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
+
+        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);        // RTC 时钟源 = LSE
+        RCC_RTCCLKCmd(ENABLE);                         // 使能 RTC 时钟
+
+        RTC_WaitForSynchro();                          // 等待同步
+        RTC_WaitForLastTask();
+
+        RTC_SetPrescaler(32767);                       // 32.768kHz / 32768 = 1Hz
+        RTC_WaitForLastTask();
+
+        RTC_SetCounter(TIME_EPOCH);                    // 设置初始时间
+        RTC_WaitForLastTask();
+
+        /* 秒中断 */
+        RTC_ITConfig(RTC_IT_SEC, ENABLE);
+        RTC_WaitForLastTask();
+
+        /* EXTI 17 配置 (RTC 秒中断) */
+        EXTI_InitTypeDef EXTI_InitStruct;
+        EXTI_InitStruct.EXTI_Line = EXTI_Line17;
+        EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
+        EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;
+        EXTI_InitStruct.EXTI_LineCmd = ENABLE;
+        EXTI_Init(&EXTI_InitStruct);
+
+        /* NVIC 配置 (RTC 中断) */
+        NVIC_InitTypeDef NVIC_InitStruct;
+        NVIC_InitStruct.NVIC_IRQChannel = RTC_IRQn;
+        NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
+        NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
+        NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
+        NVIC_Init(&NVIC_InitStruct);
+
+        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);      // 标记已配置
+    }
+}
+
+/* ---- RTC 中断处理 ---- */
+void RTC_IRQHandler(void)
+{
+    if (RTC_GetITStatus(RTC_IT_SEC, RESET) != RESET)
+    {
+        RTC_ClearITPendingBit(RTC_IT_SEC);
+        /* 秒中断每 1 秒触发一次 */
+        /* 此处可打印或更新显示 */
+    }
+}
+
+/* ---- 读取 RTC 时间 ---- */
+uint32_t RTC_GetTime(void)
+{
+    return RTC_GetCounter();
+}
+
+/* ---- 时间转换 (Unix → 可读) ---- */
+void RTC_UnixToDateTime(uint32_t unix, uint16_t* year, uint8_t* month,
+                        uint8_t* day, uint8_t* hour, uint8_t* min, uint8_t* sec)
+{
+    static const uint8_t daysInMon[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
+    uint32_t remaining = unix;
+    uint16_t y;
+
+    *sec  = remaining % 60; remaining /= 60;
+    *min  = remaining % 60; remaining /= 60;
+    *hour = remaining % 24; remaining /= 24;
+
+    for (y = 1970; ; y++)
+    {
+        uint16_t days = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 ? 366 : 365;
+        if (remaining < days) break;
+        remaining -= days;
+    }
+    *year = y;
+
+    uint8_t leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
+    for (*month = 0; *month < 12; (*month)++)
+    {
+        uint8_t d = daysInMon[*month];
+        if (*month == 1 && leap) d = 29;
+        if (remaining < d) break;
+        remaining -= d;
+    }
+    (*month)++;
+    *day = remaining + 1;
+}
+
+int main(void)
+{
+    uint32_t t;
+    uint16_t y; uint8_t M, d, h, m, s;
+
+    RTC_Config();
+
+    while (1)
+    {
+        t = RTC_GetTime();
+        RTC_UnixToDateTime(t, &y, &M, &d, &h, &m, &s);
+        /* 打印或显示 y-M-d h:m:s */
+        Delay_ms(1000);
+    }
+}
+```
+
+### 备份寄存器保存 RTC 配置标记
+
+```c
+/* 复用备份寄存器:
+ *   BKP_DR1: 已初始化标记 (0xA5A5)
+ *   BKP_DR2: 复位次数
+ *   BKP_DR3: 用户校准值
+ */
+void SaveUserData(void)
+{
+    BKP_WriteBackupRegister(BKP_DR2, BKP_ReadBackupRegister(BKP_DR2) + 1);
+}
+```
+
+---
+
+## 常见坑点
+
+1. **VBAT 必须接电池或电容**
+   - 不接 VBAT 时,VDD 断电后 BKP/RTC 数据丢失
+   - 最小系统留 VBAT 引脚悬空时,RTC 无法保持
+
+2. **复位后 BKP 内容不变 (需 VBAT 供电)**
+   - 系统复位或看门狗复位不丢失
+   - 上电复位如果 VBAT 有电也不丢失
+
+3. **RTC 寄存器需等待同步**
+   - 每次读写 RTC 寄存器前必须等 RTC 与 APB1 同步
+   - `RTC_WaitForSynchro()` — 上电后调用一次
+   - `RTC_WaitForLastTask()` — 每次操作后调用
+
+4. **RTC 配置只能做一次**
+   - RTC 预分频器和时钟源选择在备份域中,一旦配置,主复位不会复位
+   - 使用备份寄存器标记 (如 0xA5A5) 避免重复配置
+
+5. **RTC 中断配置 EXTI**
+   - RTC 中断信号需要映射到 EXTI 线 17/19 才能从 Stop 模式唤醒
+   - 在 NVIC 中要开 RTC_IRQn,同步在 EXTI 中配置对应线
+
+6. **备份域访问保护**
+   - 操作 BKP 和 RTC 前必须 `PWR_BackupAccessCmd(ENABLE)`
+   - BKP 寄存器不能用指针直接访问,必须用库函数
+
+7. **LSE 起振慢**
+   - LSE 晶振起振需要数百 ms,轮询 LSERDY 标志时要给足够时间
+   - 硬件上 LSE 负载电容需匹配 (通常 6pF~12.5pF)
+
+8. **时间基准**
+   - STM32 RTC 是简单的 32 位计数器,不是年月日时钟
+   - 需要手动将计数值转换为可读时间 (或使用 `time.h` 库)
+
+9. **RTC_IT_OW 注意**
+   - 标准库中溢出中断宏是 `RTC_IT_OW`,不是 `RTC_IT_OVF`
+
+10. **闹钟中断**
+    - RTC 闹钟比较的是完整 32 位计数器值,不是时分秒
+    - 设置闹钟前需要计算对应的 Unix 时间戳

+ 372 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/16-PWR电源控制.md

@@ -0,0 +1,372 @@
+# 16-PWR电源控制
+
+## 原理
+
+### STM32 电源系统
+
+```
+           3.3V
+            │
+  ┌─────────┼──────────┐
+  │         │          │
+  VDD      VDDA      VREF      VBAT
+  (主电源)  (模拟电源)  (参考电压)  (备份电池)
+  │         │          │          │
+  │    ┌────┘          │     ┌────┘
+  │    │               │     │
+  ├─ 内置稳压器 ── 1.8V内核 ─┤
+  │                         │
+  └─── BKP + RTC ───────────┘
+```
+
+| 电源引脚 | 说明 |
+|----------|------|
+| VDD | 3.3V 主数字电源 |
+| VDDA | 3.3V 模拟电源 (ADC/DAC/PLL) |
+| VREF | ADC 参考电压 (部分封装内部接 VDDA) |
+| VBAT | 备份电池 (RTC + BKP 后备供电) |
+
+### 修改主频流程
+
+```
+1. 配置 Flash 等待周期 (72MHz 需 2 个等待周期)
+2. 修改 HCLK 预分频 (AHB)
+3. 修改 APB1/APB2 预分频
+4. 修改 PLL 倍频系数 (或直接使用 HSI/HSE)
+```
+
+### 3 种低功耗模式
+
+```
+                     ┌─────────────────┐
+                     │    睡眠 Sleep    │
+                     │ CPU 停,外设正常  │
+                     │ 任意中断唤醒     │
+                     └────────┬────────┘
+                              │
+                     ┌────────▼────────┐
+                     │    停止 Stop     │
+                     │  HSI/HSE/PLL 停  │
+                     │ SRAM/寄存器保留   │
+                     │ EXTI/外部中断唤醒 │
+                     └────────┬────────┘
+                              │
+                     ┌────────▼────────┐
+                     │   待机 Standby   │
+                     │  全部停 (包括调压器)│
+                     │ SRAM/寄存器丢失   │
+                     │ WKUP/RTC/复位唤醒│
+                     └─────────────────┘
+```
+
+| 特性 | Sleep | Stop | Standby |
+|------|-------|------|---------|
+| CPU | 停 | 停 | 停 |
+| HSI/HSE/PLL | 运行 | 停 | 停 |
+| 电压调节器 | 运行 | 低功耗/运行 | 停 |
+| SRAM/寄存器 | 保留 | 保留 | 丢失 |
+| 备份域 | 保留 | 保留 | 保留 |
+| 唤醒源 | 任意中断 | EXTI (GPIO/RTC) | WKUP/RTC/NRST |
+| 唤醒后 | 从中断处继续 | 重新配置时钟 | 相当于复位 |
+
+**电流对比 (典型值)**:
+- 运行模式:约 50mA (72MHz)
+- Sleep:约 20mA
+- Stop:约 20μA (调压器低功耗)
+- Standby:约 2μA
+
+---
+
+## API 表格
+
+### PWR
+
+| 函数 | 说明 |
+|------|------|
+| `void PWR_DeInit(void)` | 复位 PWR 寄存器 |
+| `void PWR_BackupAccessCmd(FunctionalState NewState)` | 使能备份域访问 |
+| `void PWR_PVDCmd(FunctionalState NewState)` | 使能电源电压检测 |
+| `void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel)` | 配置 PVD 阈值 |
+| `void PWR_WakeUpPinCmd(FunctionalState NewState)` | 使能 WKUP 引脚 (PA0) |
+| `void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint32_t PWR_STOPEntry)` | 进入停止模式 |
+| `void PWR_EnterSTANDBYMode(void)` | 进入待机模式 |
+| `void PWR_EnterSleepMode(void)` | 进入睡眠模式 (实际用 __WFI() 更方便) |
+
+**PWR_EnterSTOPMode 参数**:
+
+| 参数 | 可选值 | 说明 |
+|------|--------|------|
+| PWR_Regulator | `PWR_Regulator_ON` | 调压器运行 (唤醒快) |
+| | `PWR_Regulator_LowPower` | 调压器低功耗 (省电) |
+| PWR_STOPEntry | `PWR_STOPEntry_WFI` | WFI 指令进入 |
+| | `PWR_STOPEntry_WFE` | WFE 指令进入 |
+
+### 其他配合函数
+
+| 函数 | 说明 |
+|------|------|
+| `__WFI(void)` | 汇编 WFI 指令,进入 Sleep/Stop |
+| `__WFE(void)` | 汇编 WFE 指令 |
+| `__SEV(void)` | 汇编 SEV 指令 (发送事件) |
+| `SCB->SCR` | 系统控制寄存器 (SLEEPONEXIT 位) |
+
+---
+
+## 完整代码示例
+
+### 修改主频 (72MHz → 36MHz)
+
+```c
+/*------------------------------------------------
+ * SetClock_36MHz.c
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void SetSysClockTo36MHz(void)
+{
+    /* 1. Flash 等待周期:36MHz 需 1 个等待周期 */
+    FLASH_SetLatency(FLASH_Latency_1);
+
+    /* 2. 修改 AHB 预分频:HCLK = SYSCLK / 2 = 36MHz */
+    RCC->CFGR &= ~RCC_CFGR_HPRE;
+    RCC->CFGR |= RCC_CFGR_HPRE_Div2;
+
+    /* 3. APB1 = HCLK / 1 = 36MHz (APB1 最高 36MHz) */
+    RCC->CFGR &= ~RCC_CFGR_PPRE1;
+    RCC->CFGR |= RCC_CFGR_PPRE1_Div1;
+
+    /* 4. APB2 = HCLK / 1 = 36MHz */
+    RCC->CFGR &= ~RCC_CFGR_PPRE2;
+    RCC->CFGR |= RCC_CFGR_PPRE2_Div1;
+}
+
+int main(void)
+{
+    SetSysClockTo36MHz();
+
+    /* 此时系统时钟为 36MHz */
+    while (1);
+}
+```
+
+### 睡眠模式 + 外部中断唤醒
+
+```c
+/*------------------------------------------------
+ * SleepMode_Example.c
+ * 睡眠模式,PA0 外部中断唤醒
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void GPIO_Config(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct;
+
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
+
+    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
+    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;       // 上拉输入
+    GPIO_Init(GPIOA, &GPIO_InitStruct);
+}
+
+void EXTI_Config(void)
+{
+    EXTI_InitTypeDef EXTI_InitStruct;
+
+    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
+    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
+
+    EXTI_InitStruct.EXTI_Line = EXTI_Line0;
+    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
+    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
+    EXTI_InitStruct.EXTI_LineCmd = ENABLE;
+    EXTI_Init(&EXTI_InitStruct);
+
+    NVIC_InitTypeDef NVIC_InitStruct;
+    NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
+    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
+    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
+    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStruct);
+}
+
+void EXTI0_IRQHandler(void)
+{
+    if (EXTI_GetITStatus(EXTI_Line0) != RESET)
+    {
+        EXTI_ClearITPendingBit(EXTI_Line0);
+        /* 唤醒后从此继续执行 (不是 main 开头) */
+    }
+}
+
+int main(void)
+{
+    GPIO_Config();
+    EXTI_Config();
+
+    while (1)
+    {
+        /* 进入睡眠模式 (Sleep) */
+        __WFI();
+        /* 中断唤醒后继续往下执行 */
+    }
+}
+```
+
+### 停止模式 + WKUP 唤醒
+
+```c
+/*------------------------------------------------
+ * StopMode_Example.c
+ * 停止模式,PA0 (WKUP) 上升沿唤醒
+ * 注意:唤醒后必须重新配置系统时钟!
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void SystemClock_Reconfig(void)
+{
+    /* 重新配置系统时钟到 72MHz */
+    /* 省略标准 SystemInit() 调用,或调用 HSI/HSE+PLL 启动函数 */
+    SystemInit();
+}
+
+int main(void)
+{
+    /* 使能 WKUP 引脚 (PA0) */
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
+    PWR_WakeUpPinCmd(ENABLE);
+
+    while (1)
+    {
+        /* 进入停止模式 (调压器低功耗, WFI) */
+        PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);
+
+        /* ---- 唤醒后从这里继续 ---- */
+
+        SystemClock_Reconfig();                     // ★ 必须重新配置时钟
+        /* 继续其他操作 */
+    }
+}
+```
+
+### 待机模式 + RTC 闹钟唤醒
+
+```c
+/*------------------------------------------------
+ * StandbyMode_Example.c
+ * 待机模式,RTC 闹钟 5 秒后唤醒
+ * 注意:待机唤醒 = 复位,从 main 开头执行!
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void RTC_ConfigForStandby(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
+    PWR_BackupAccessCmd(ENABLE);
+
+    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
+    {
+        /* 初始化 RTC: LSE, 1Hz */
+        RCC_LSEConfig(RCC_LSE_ON);
+        while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
+
+        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
+        RCC_RTCCLKCmd(ENABLE);
+        RTC_WaitForSynchro();
+        RTC_WaitForLastTask();
+
+        RTC_SetPrescaler(32767);
+        RTC_WaitForLastTask();
+
+        /* 设置闹钟 (当前时间 + 5 秒) */
+        RTC_SetAlarm(RTC_GetCounter() + 5);
+        RTC_WaitForLastTask();
+
+        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
+    }
+    else
+    {
+        /* 已配置,等 RTC 同步 */
+        RTC_WaitForSynchro();
+        /* 唤醒后 (待机=复位) 需重新设置闹钟 */
+        RTC_SetAlarm(RTC_GetCounter() + 5);
+        RTC_WaitForLastTask();
+    }
+}
+
+int main(void)
+{
+    /* 重启后检查是否是待机唤醒 */
+    if (RCC_GetFlagStatus(RCC_FLAG_SFTRST) || RCC_GetFlagStatus(RCC_FLAG_PORRST) ||
+        RCC_GetFlagStatus(RCC_FLAG_PINRST))
+    {
+        /* 上电复位,正常启动 */
+    }
+
+    if (RCC_GetFlagStatus(RCC_FLAG_WU) != RESET)
+    {
+        /* 待机唤醒复位 */
+        RCC_ClearFlag();
+    }
+
+    SystemInit();
+
+    RTC_ConfigForStandby();
+
+    /* 使能 WKUP (待机模式也需) */
+    PWR_WakeUpPinCmd(ENABLE);
+
+    /* 5 秒后闹钟触发唤醒 */
+    PWR_EnterSTANDBYMode();
+
+    /* ---- 不会执行到这里 ---- */
+    while (1);
+}
+```
+
+---
+
+## 常见坑点
+
+1. **待机模式相当于复位**
+   - 从 Standby 唤醒后 MCU 从 **main 开头执行**(不是中断处)
+   - SRAM 和寄存器内容全部丢失
+   - 备份域 (BKP/RTC) 内容保留
+
+2. **停止模式唤醒后需重配时钟**
+   - Stop 模式 HSI/HSE/PLL 都停了
+   - 唤醒后必须重新调用 `SystemInit()` 配置系统时钟
+   - 否则外设时钟异常 (默认 HSI 8MHz)
+
+3. **WKUP 引脚是 PA0**
+   - `PWR_WakeUpPinCmd(ENABLE)` 固定使用 PA0
+   - 用于 Standby 唤醒(上升沿)
+   - 在 Stop 模式也可用作唤醒源
+
+4. **SLEEPONEXIT 位**
+   - `SCB->SCR |= 1 << 1` 设置 SLEEPONEXIT
+   - 中断退出时自动进入睡眠 (用于中断驱动的应用)
+   - 不清除该位时,中断唤醒后继续执行主循环
+
+5. **PVD (可编程电压检测)**
+   - 可配置电压阈值 (2.2V~2.9V)
+   - 电压低于阈值时产生 PVD 中断
+   - 用于断电保护 (保存数据到 Flash)
+
+6. **停止模式调试**
+   - 调试器在停止模式下可能断开 (SWD 停)
+   - 可用 `DBGMCU_Config(DBGMCU_STOP, ENABLE)` 保持调试
+   - 进入停止模式前可设断点观察
+
+7. **PWR 时钟必须使能**
+   - 调用 `PWR_EnterSTOPMode()` / `PWR_EnterSTANDBYMode()` 前
+   - 必须使能 PWR 时钟: `RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE)`
+
+8. **WFI 与 WFE 区别**
+   - `WFI` (Wait For Interrupt): 有中断挂起时退出
+   - `WFE` (Wait For Event): 有事件挂起时退出
+   - `__SEV()` 发送事件唤醒 WFE

+ 350 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/17-WDG看门狗.md

@@ -0,0 +1,350 @@
+# 17-WDG看门狗
+
+## 原理
+
+### 看门狗工作模式
+
+```
+                    ┌──────────────┐
+                    │  计数器递减   │
+                    │  CNT ──▶ 0   │
+                    └──────┬───────┘
+                           │
+                  ┌────────▼────────┐
+                  │   CNT == 0 ?    │
+                  └────────┬────────┘
+                           │
+              ┌────────────┴────────────┐
+              │ 是                      │ 否
+              ▼                         ▼
+        ┌───────────┐              ┌───────────┐
+        │ 系统复位   │              │ 继续递减   │
+        └───────────┘              └───────────┘
+              ▲
+              │
+        在 CNT 到 0 前
+        "喂狗" (重载计数器)
+```
+
+**核心概念**:
+- 看门狗是一个递减计数器
+- 计到 0 时触发系统复位
+- 软件需要在溢出前"喂狗"(重载计数器值)
+- 防止程序跑飞后无法恢复
+
+### IWDG (独立看门狗)
+
+**时钟源**:LSI (40kHz RC 振荡器,独立于主时钟)
+
+```
+LSI 40kHz ──▶ 预分频器 (4/8/16/.../256) ──▶ 12位递减计数器 ──▶ 复位
+                    ▲                                │
+                    │                                │
+                IWDG_SetPrescaler()             IWDG_ReloadCounter()
+```
+
+**溢出时间公式**:
+```
+T_ovf = (4 × 2^PR) × RL / 40kHz
+= 预分频系数 × 重载值 / 40kHz
+```
+
+| 预分频 (PR) 系数 | 4×2^PR | 取值 |
+|:---:|:---:|:---:|
+| 0 | 4 | 4 |
+| 1 | 8 | 8 |
+| 2 | 16 | 16 |
+| 3 | 32 | 32 |
+| 4 | 64 | 64 |
+| 5 | 128 | 128 |
+| 6 | 256 | 256 |
+| 7 | 256 | 保留 (不推荐) |
+
+**示例**:PR=4 (64分频), RL=625
+```
+T = 64 × 625 / 40000 ≈ 1.0 秒
+```
+
+### WWDG (窗口看门狗)
+
+**时钟源**:APB1 (36MHz),经 4096 分频后
+
+```
+APB1 ──▶ /4096 ──▶ 预分频器 (/1/2/4/8) ──▶ 7位递减计数器 ──▶ 复位
+                          ▲                      │
+                          │                      │
+                     WWDG_SetPrescaler()    WWDG_SetCounter()
+                          │                      │
+                          └──────────────────────┘
+                          窗口期喂狗条件:
+                          0x40 < CNT ≤ W[6:0]
+```
+
+```
+计数器值
+  │
+0x7F ─── 最大计数值
+  │     ┌───────────────┐
+  │     │  允许喂狗区域   │  ← 窗口期 (CNT > W[6:0])
+  W ────┤               │
+  │     │  禁止喂狗      │  ← 在此喂狗 → 复位
+  │     │  (必须继续减)   │
+0x40 ───┤               │  ← EWIF 置位 (早期唤醒中断)
+  │     └───────────────┘
+0x3F ─── 复位阈值
+  │
+  ▼  复位
+```
+
+**WWDG 特性**:
+- 7 位递减计数器 (0x7F ~ 0x3F)
+- 只能在 **CNT > W[6:0]** 时喂狗
+- CNT 到 0x40 时触发早期唤醒中断 (EWI)
+- CNT 到 0x3F 时触发复位
+- **窗口外喂狗也会复位** (这是与 IWDG 最大的区别)
+
+---
+
+## API 表格
+
+### IWDG
+
+| 函数 | 说明 |
+|------|------|
+| `void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess)` | 使能写访问 (必须先调用) |
+| `void IWDG_SetPrescaler(uint8_t IWDG_Prescaler)` | 设置预分频系数 |
+| `void IWDG_SetReload(uint16_t Reload)` | 设置重载值 (12位, 0~0xFFF) |
+| `void IWDG_ReloadCounter(void)` | 喂狗 (重载计数器) |
+| `void IWDG_Enable(void)` | 使能 IWDG (一旦使能无法软件关闭) |
+| `FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG)` | 获取状态标志 (PVU/RVU) |
+
+**IWDG_Prescaler 可选值**:
+
+| 宏 | 分频系数 |
+|----|:--------:|
+| `IWDG_Prescaler_4` | 4 |
+| `IWDG_Prescaler_8` | 8 |
+| `IWDG_Prescaler_16` | 16 |
+| `IWDG_Prescaler_32` | 32 |
+| `IWDG_Prescaler_64` | 64 |
+| `IWDG_Prescaler_128` | 128 |
+| `IWDG_Prescaler_256` | 256 |
+
+### WWDG
+
+| 函数 | 说明 |
+|------|------|
+| `void WWDG_Enable(uint8_t Counter)` | 使能 WWDG 并设置初始计数值 |
+| `void WWDG_SetWindowValue(uint8_t WindowValue)` | 设置窗口值 (W[6:0]) |
+| `void WWDG_SetCounter(uint8_t Counter)` | 喂狗 (重载计数器) |
+| `void WWDG_EnableIT(void)` | 使能早期唤醒中断 (EWI) |
+| `void WWDG_SetPrescaler(uint32_t WWDG_Prescaler)` | 设置预分频 |
+| `FlagStatus WWDG_GetFlagStatus(void)` | 获取 EWIF 标志 |
+| `void WWDG_ClearFlag(void)` | 清除 EWIF 标志 |
+
+**WWDG_Prescaler 可选值**:
+
+| 宏 | 分频系数 |
+|----|:--------:|
+| `WWDG_Prescaler_1` | 1 |
+| `WWDG_Prescaler_2` | 2 |
+| `WWDG_Prescaler_4` | 4 |
+| `WWDG_Prescaler_8` | 8 |
+
+---
+
+## 完整代码示例
+
+### IWDG 独立看门狗
+
+```c
+/*------------------------------------------------
+ * IWDG_Example.c
+ * 溢出时间 ≈ 1 秒, 主循环 500ms 喂狗
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+static void Delay_ms(uint32_t ms);
+
+/* IWDG 初始化:溢出约 1 秒 */
+void IWDG_Init(void)
+{
+    /* 计算:LSI ≈ 40kHz, PR=4 (64分频), RL=625 */
+    /* T = 64 × 625 / 40000 = 1.0 秒 */
+    IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);   // 允许写
+    IWDG_SetPrescaler(IWDG_Prescaler_64);            // 64 分频
+    IWDG_SetReload(625);                            // 重载值 625
+    IWDG_ReloadCounter();                           // 喂第一次
+    IWDG_Enable();                                  // 使能 IWDG
+}
+
+int main(void)
+{
+    IWDG_Init();
+
+    while (1)
+    {
+        /* 用户代码 */
+
+        IWDG_ReloadCounter();                       // 500ms 喂一次狗
+
+        Delay_ms(500);                              // 模拟工作延时
+        /* 如果在 1 秒内没有喂狗,系统复位 */
+    }
+}
+```
+
+### WWDG 窗口看门狗
+
+```c
+/*------------------------------------------------
+ * WWDG_Example.c
+ * 窗口值 0x5F, 计数器 0x7F, 早期唤醒中断
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+static void Delay_ms(uint32_t ms);
+
+void WWDG_Init(void)
+{
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
+
+    /* 窗口值 = 0x5F, 计数值 = 0x7F, 预分频 = /8 */
+    /* 超时时间 ≈ 4096 × 8 × (0x7F - 0x3F) / 36MHz ≈ 5.8ms */
+    /* 窗口上限 ≈ 4096 × 8 × (0x7F - 0x5F) / 36MHz ≈ 2.9ms */
+    WWDG_SetPrescaler(WWDG_Prescaler_8);
+    WWDG_SetWindowValue(0x5F);                      // 窗口值
+    WWDG_EnableIT();                                // 使能 EWI 中断
+
+    NVIC_InitTypeDef NVIC_InitStruct;
+    NVIC_InitStruct.NVIC_IRQChannel = WWDG_IRQn;
+    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
+    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
+    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStruct);
+
+    WWDG_Enable(0x7F);                              // 使能 WWDG,计数器=0x7F
+}
+
+/* WWDG 早期唤醒中断处理 */
+void WWDG_IRQHandler(void)
+{
+    if (WWDG_GetFlagStatus())
+    {
+        WWDG_SetCounter(0x7F);                      // 喂狗
+        WWDG_ClearFlag();                           // 清标志
+    }
+}
+
+int main(void)
+{
+    WWDG_Init();
+
+    while (1)
+    {
+        /* 用户代码 */
+        /* WWDG 在中断中喂狗 */
+    }
+}
+```
+
+### IWDG + WWDG 同时使用
+
+```c
+/*------------------------------------------------
+ * DualWatchdog_Example.c
+ * IWDG: 长超时 (1秒), 主循环喂狗
+ * WWDG: 短超时 (5.8ms), 中断喂狗
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+void Watchdog_Init(void)
+{
+    /* ---- IWDG: 1秒 ---- */
+    IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
+    IWDG_SetPrescaler(IWDG_Prescaler_64);
+    IWDG_SetReload(625);
+    IWDG_ReloadCounter();
+    IWDG_Enable();
+
+    /* ---- WWDG: 5.8ms ---- */
+    RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
+    WWDG_SetPrescaler(WWDG_Prescaler_8);
+    WWDG_SetWindowValue(0x5F);
+    WWDG_EnableIT();
+    WWDG_Enable(0x7F);
+
+    NVIC_InitTypeDef NVIC_InitStruct;
+    NVIC_InitStruct.NVIC_IRQChannel = WWDG_IRQn;
+    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
+    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
+    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
+    NVIC_Init(&NVIC_InitStruct);
+}
+
+void WWDG_IRQHandler(void)
+{
+    if (WWDG_GetFlagStatus())
+    {
+        WWDG_SetCounter(0x7F);
+        WWDG_ClearFlag();
+    }
+}
+
+int main(void)
+{
+    Watchdog_Init();
+
+    while (1)
+    {
+        IWDG_ReloadCounter();           // 主循环喂 IWDG
+
+        /* 如果主循环卡死超过 1 秒,IWDG 复位 */
+        /* 如果中断卡死超过 5.8ms,WWDG 复位 */
+        /* 如果喂狗太早 (窗口期外),WWDG 复位 */
+    }
+}
+```
+
+---
+
+## 常见坑点
+
+1. **IWDG 一旦使能无法软件关闭**
+   - 除非发生系统复位,否则 IWDG 不可停止
+   - 调试时注意:芯片会不断复位
+   - 调试期间可注释掉 `IWDG_Enable()` 或使用 `DBGMCU_Config`
+
+2. **WWDG 窗口期外喂狗也复位**
+   - 与 IWDG 不同,WWDG 不是任意时刻都能喂狗
+   - 必须在 CNT > W[6:0] 时喂狗
+   - **太早喂狗**也会触发复位!
+
+3. **IWDG 预分频和重载寄存器写保护**
+   - 修改 PR 和 RL 前必须先调 `IWDG_WriteAccessCmd(ENABLE)`
+   - 且必须在 IWDG_Enable() 之前
+
+4. **IWDG 的 LSI 精度差**
+   - LSI 典型 40kHz,实际 30~60kHz
+   - 溢出时间不精确,不适用于精确计时
+
+5. **WWDG 时序依赖 APB1 时钟**
+   - 修改 APB1 时钟会影响 WWDG 超时时间
+   - 如果 APB1 不是 36MHz,需重新计算
+
+6. **早期唤醒中断 (EWI)**
+   - WWDG_EnableIT() 使能后,CNT=0x40 时触发 WWDG_IRQHandler
+   - 在中断中喂狗可避免复位
+   - 但如果在中断中没喂狗,CNT=0x3F 时仍复位
+
+7. **调试期间看门狗干扰**
+   - 调试暂停时看门狗仍在走
+   - 可用 `DBGMCU_Config(DBGMCU_IWDG_STOP | DBGMCU_WWDG_STOP, ENABLE)`
+   - 这样调试暂停时看门狗停止计数
+
+8. **IWDG 寄存器更新需要时间**
+   - 设置 PR 或 RL 后,硬件需要几个 LSI 周期更新
+   - 可通过 `IWDG_GetFlagStatus(IWDG_FLAG_PVU/RVU)` 轮询等待

+ 369 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32-标准库笔记/18-FLASH闪存.md

@@ -0,0 +1,369 @@
+# 18-FLASH闪存
+
+## 原理
+
+### STM32F103C8T6 Flash 结构
+
+**容量**:64KB
+
+```
+起始地址          结束地址
+0x08000000  ┌────────────────────┐
+            │  主存储器 (64KB)    │ ← 用户程序+数据
+            │  页 0~63 (1KB/页)  │
+0x08010000  ├────────────────────┤
+            │                    │
+0x1FFFF000  ├────────────────────┤
+            │  系统存储器 (2KB)   │ ← Bootloader (出厂固件)
+0x1FFFF800  ├────────────────────┤
+            │  选项字节 (16B)     │ ← 读保护/看门狗配置
+0x1FFFF810  └────────────────────┘
+```
+
+| 区域 | 地址范围 | 大小 | 说明 |
+|------|----------|------|------|
+| 主存储器 | 0x0800 0000 ~ 0x0800 FFFF | 64KB | 用户代码和数据 |
+| 系统存储器 | 0x1FFF F000 ~ 0x1FFF F7FF | 2KB | 系统 Bootloader |
+| 选项字节 | 0x1FFF F800 ~ 0x1FFF F80F | 16B | 配置位 |
+| 唯一 ID | 0x1FFF F7E8 ~ 0x1FFF F7F0 | 12B | 芯片唯一序列号 |
+
+**页大小**:1KB (STM32F103C8T6)
+
+### Flash 特性
+
+```
+读 (任意时刻)
+  │
+Flash 存储单元: 浮栅晶体管
+  │ 初始状态 = 1 (已擦除)
+  │
+写操作 (只能 1→0):
+  ┌──────────────┐
+  │   1 → 0 ✅   │ ← 写入 0 可以
+  │   0 → 1 ❌   │ ← 写入 1 不行!必须先擦除
+  └──────────────┘
+  │
+擦除操作 (恢复为全 1):
+  页擦除 (1KB) 或 整片擦除
+```
+
+**关键结论**:
+- Flash 写满 0xFF 的地址写入任意值都可以
+- Flash 写非 0xFF 的地址,如果想清 0 可以,想置 1 **不行**
+- 修改数据时:**先擦除 → 再写入**
+
+### Flash 操作流程
+
+```
+FLASH_Unlock()
+  │
+  ├── 写 KEY1 = 0x45670123 到 FLASH_KEYR
+  └── 写 KEY2 = 0xCDEF89AB 到 FLASH_KEYR
+  │
+  ▼
+检查 FLASH_SR_BSY (忙)
+  │
+  ├── 忙 → 等待
+  │
+  ▼
+FLASH_ErasePage(addr)    ← 擦除指定页 (1KB)
+  │
+  ▼
+FLASH_ProgramHalfWord/Word(addr, data)    ← 编程 (16/32 位)
+  │
+  ▼
+FLASH_Lock()
+```
+
+### 读取芯片唯一 ID
+
+```c
+// 唯一 ID 地址 (96 位 = 12 字节)
+// X/Y: wafer X/Y 坐标, W: wafer 编号, LOT: 批次号
+uint32_t* pUID = (uint32_t*)0x1FFFF7E8;
+uint32_t uid0 = pUID[0];   // LOT 号 (低位)
+uint32_t uid1 = pUID[1];   // LOT 号 (高位) + W 编号
+uint32_t uid2 = pUID[2];   // X + Y 坐标
+```
+
+---
+
+## API 表格
+
+### FLASH
+
+| 函数 | 说明 |
+|------|------|
+| `void FLASH_Unlock(void)` | 解锁 Flash 控制器 |
+| `void FLASH_Lock(void)` | 锁定 Flash 控制器 |
+| `FLASH_Status FLASH_ErasePage(uint32_t Page_Address)` | 擦除指定页 (1KB) |
+| `FLASH_Status FLASH_EraseAllPages(void)` | 擦除所有页 |
+| `FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)` | 16 位编程 |
+| `FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data)` | 32 位编程 |
+| `FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data)` | 编程选项字节 |
+| `FLASH_Status FLASH_EraseOptionBytes(void)` | 擦除选项字节 |
+| `void FLASH_ITConfig(uint32_t FLASH_IT, FunctionalState NewState)` | 中断配置 |
+| `void FLASH_SetLatency(uint32_t FLASH_Latency)` | 设置等待周期 |
+| `FLASH_Status FLASH_GetStatus(void)` | 获取状态 |
+| `FlagStatus FLASH_GetFlagStatus(uint32_t FLASH_FLAG)` | 获取标志 |
+| `void FLASH_ClearFlag(uint32_t FLASH_FLAG)` | 清除标志 |
+
+**FLASH_Status 返回值**:
+
+| 状态 | 说明 |
+|------|------|
+| `FLASH_BUSY` | 忙 (BSY=1) |
+| `FLASH_COMPLETE` | 操作完成 |
+| `FLASH_ERROR_PG` | 编程错误 |
+| `FLASH_ERROR_WRP` | 写保护错误 |
+| `FLASH_TIMEOUT` | 超时 |
+
+**FLASH_FLAG**:
+
+| 标志 | 说明 |
+|------|------|
+| `FLASH_FLAG_BSY` | 忙标志 |
+| `FLASH_FLAG_PGERR` | 编程错误 |
+| `FLASH_FLAG_WRPRTERR` | 写保护错误 |
+| `FLASH_FLAG_EOP` | 操作结束 |
+
+**FLASH_Latency** (Flash 等待周期):
+
+| 系统时钟频率 | 等待周期 |
+|:------------:|:--------:|
+| 0~24MHz | 0 (FLASH_Latency_0) |
+| 24~48MHz | 1 (FLASH_Latency_1) |
+| 48~72MHz | 2 (FLASH_Latency_2) |
+
+---
+
+## 完整代码示例
+
+### 基本 Flash 读写
+
+```c
+/*------------------------------------------------
+ * FLASH_Example.c
+ * 保存/加载用户配置参数到 Flash 最后一页
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+#include <stdint.h>
+#include <string.h>
+
+/* 存储位置:Flash 最后一页 (64KB - 1KB = 63KB 偏移) */
+#define CONFIG_FLASH_ADDR  0x0800FC00    // 第 63 页起始 (0x08000000 + 63*1024)
+
+/* 用户配置结构体 (不超过 1KB) */
+typedef struct {
+    uint16_t magic;         // 校验魔数 0x55AA
+    uint16_t baudRate;      // 波特率
+    uint8_t  channel;       // 通道号
+    uint8_t  reserved[21];  // 保留 (对齐)
+} UserConfig;
+
+static UserConfig g_config;
+
+/* ---- 从 Flash 读取配置 ---- */
+void Config_Load(void)
+{
+    uint16_t* pFlash = (uint16_t*)CONFIG_FLASH_ADDR;
+    uint16_t* pDest  = (uint16_t*)&g_config;
+    uint32_t  i;
+
+    for (i = 0; i < sizeof(UserConfig) / 2; i++)
+        pDest[i] = pFlash[i];
+
+    if (g_config.magic != 0x55AA)
+    {
+        /* 无效配置,恢复默认 */
+        g_config.magic    = 0x55AA;
+        g_config.baudRate = 115200;
+        g_config.channel  = 1;
+    }
+}
+
+/* ---- 保存配置到 Flash ---- */
+void Config_Save(void)
+{
+    uint16_t* pSrc = (uint16_t*)&g_config;
+    uint32_t  i;
+
+    g_config.magic = 0x55AA;
+
+    FLASH_Unlock();
+
+    /* 擦除整个页 */
+    FLASH_ErasePage(CONFIG_FLASH_ADDR);
+
+    /* 逐 16 位写入 */
+    for (i = 0; i < sizeof(UserConfig) / 2; i++)
+    {
+        FLASH_ProgramHalfWord(CONFIG_FLASH_ADDR + i * 2, pSrc[i]);
+    }
+
+    FLASH_Lock();
+}
+
+/* ---- 写验证 ---- */
+uint8_t Config_Verify(void)
+{
+    uint16_t* pFlash = (uint16_t*)CONFIG_FLASH_ADDR;
+    uint16_t* pDest  = (uint16_t*)&g_config;
+    uint32_t  i;
+
+    for (i = 0; i < sizeof(UserConfig) / 2; i++)
+    {
+        if (pFlash[i] != pDest[i])
+            return 0;   // 验证失败
+    }
+    return 1;           // 验证通过
+}
+
+int main(void)
+{
+    Config_Load();
+
+    /* 修改配置 */
+    g_config.baudRate = 9600;
+    g_config.channel  = 2;
+
+    Config_Save();
+
+    if (Config_Verify())
+    {
+        /* 保存成功 */
+    }
+
+    while (1);
+}
+```
+
+### 读取芯片唯一 ID
+
+```c
+/*------------------------------------------------
+ * UID_Example.c
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+typedef struct {
+    uint32_t lotNumLow;     // LOT 号低 32 位
+    uint32_t lotNumHigh;    // LOT 号高 16 位 + Wafer 编号
+    uint32_t coordXY;       // X 坐标 + Y 坐标
+} ChipUID;
+
+void ReadChipUID(ChipUID* uid)
+{
+    uint32_t* pUID = (uint32_t*)0x1FFFF7E8;
+    uid->lotNumLow  = pUID[0];
+    uid->lotNumHigh = pUID[1];
+    uid->coordXY    = pUID[2];
+}
+
+int main(void)
+{
+    ChipUID uid;
+    ReadChipUID(&uid);
+
+    /* uid 可用于软件加密、设备识别等 */
+
+    while (1);
+}
+```
+
+### Flash 编程错误处理
+
+```c
+/*------------------------------------------------
+ * FLASH_ErrorHandling.c
+ *------------------------------------------------*/
+
+#include "stm32f10x.h"
+
+FLASH_Status Flash_ProgramSafe(uint32_t addr, uint16_t data)
+{
+    FLASH_Status status;
+
+    status = FLASH_ProgramHalfWord(addr, data);
+    if (status != FLASH_COMPLETE)
+    {
+        /* 错误处理 */
+        if (status == FLASH_ERROR_WRP)
+        {
+            /* 写保护错误:检查该页是否被保护 */
+        }
+        else if (status == FLASH_ERROR_PG)
+        {
+            /* 编程错误:地址未擦除或对齐问题 */
+        }
+    }
+    return status;
+}
+
+int main(void)
+{
+    FLASH_Unlock();
+
+    FLASH_ErasePage(0x0800FC00);
+
+    if (Flash_ProgramSafe(0x0800FC00, 0x1234) == FLASH_COMPLETE)
+    {
+        /* 编程成功 */
+    }
+
+    FLASH_Lock();
+
+    while (1);
+}
+```
+
+---
+
+## 常见坑点
+
+1. **擦除以页为单位 (1KB)**
+   - STM32F103C8T6 每页 1KB,不能单独擦除几个字节
+   - 修改一个字节也要擦除整页,然后重写整页
+   - 建议:用一整页存配置,不要和其他数据混用
+
+2. **编程必须 16 位对齐**
+   - `FLASH_ProgramHalfWord` 要求地址是**半字对齐** (偶数地址)
+   - `FLASH_ProgramWord` 要求地址是**字对齐** (4 的倍数)
+   - 不对齐会导致硬 fault 或写入错误
+
+3. **不要在 Flash 中执行程序时擦写同一页**
+   - 如果代码正在执行的地址位于要擦除的页中,擦除后 CPU 取指失败
+   - 解决方法:擦写代码放到 RAM 中执行,或避开当前代码所在页
+   - 用户配置存到**最后一页**(远离代码区)
+
+4. **Flash 等待周期**
+   - 72MHz 时需设置 `FLASH_SetLatency(FLASH_Latency_2)` (2 个等待周期)
+   - 在 `SystemInit()` 中自动配置,手动改主频后也要重新设置
+
+5. **写保护**
+   - 默认没有写保护,可直接写入
+   - 如果返回 `FLASH_ERROR_WRP`,需检查选项字节中的写保护位
+   - 可用 `FLASH_EraseOptionBytes()` 清除保护
+
+6. **Flash 擦写寿命**
+   - 约 1 万次擦写 (STM32F103 数据手册)
+   - 频繁保存配置会损耗 Flash
+   - 建议:只有配置变化时才保存,不要在循环中反复擦写
+
+7. **操作后锁定 Flash**
+   - 擦写完成后建议调 `FLASH_Lock()` 防止误操作
+   - 否则意外代码跑飞可能破坏 Flash 内容
+
+8. **唯一 ID 只读**
+   - `0x1FFFF7E8` 是只读区域,不能擦写
+   - 可用于生成设备唯一序列号、软件加密绑定
+
+9. **读 Flash 直接用指针**
+   - 读 Flash 不需要解锁,直接用 `*(uint16_t*)addr` 或 `*(uint32_t*)addr`
+   - 不需要任何初始化,上电即可读
+
+10. **系统存储器与选项字节**
+    - 系统存储器 (`0x1FFFF000`) 是出厂 Bootloader,不可擦写
+    - 选项字节 (`0x1FFFF800`) 可用 `FLASH_ProgramOptionByteData` 修改(需谨慎)

二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/clock-tree.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/interrupt-example.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/interrupt-priority-group.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/mcu-internal.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-4-modes.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-bus.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-cpha.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/spi-cpol.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/timer-block.png


二进制
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/assets/timer-timebase.png