Prechádzať zdrojové kódy

ingest: 尚硅谷STM32全套笔记导入完成(含基础/进阶/高级/扩展篇 共21篇笔记)

OpenCode 2 dní pred
rodič
commit
69ee409b12
18 zmenil súbory, kde vykonal 3824 pridanie a 603 odobranie
  1. 240 113
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/07-I2C通信与EEPROM 24C02.md
  2. 198 86
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/08-SysTick与通用定时器TIM.md
  3. 240 74
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/09-TIM高级应用:PWM与输入捕获.md
  4. 295 75
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/10-DMA数据传输.md
  5. 318 78
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/11-ADC模数转换.md
  6. 328 103
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/12-SPI通信与FSMC总线.md
  7. 363 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/13-CAN通信协议与bxCAN外设.md
  8. 253 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/14-以太网通信与W5500.md
  9. 227 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/15-WiFi通信与ESP32-C3.md
  10. 179 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/16-BLE低功耗蓝牙.md
  11. 202 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/17-LoRa远距离通信.md
  12. 237 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/18-电源管理与低功耗模式.md
  13. 279 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/19-BKP备份寄存器与RTC实时时钟.md
  14. 223 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/20-看门狗IWDG与WWDG.md
  15. 206 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/21-附录:HAL库架构与Keil MDK深入.md
  16. 16 0
      X-Knowledge-Base/wiki/index.md
  17. 20 0
      X-Knowledge-Base/wiki/log.md
  18. 0 74
      docs/superpowers/specs/2026-07-08-video-note-extractor-design.md

+ 240 - 113
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/07-I2C通信与EEPROM 24C02.md

@@ -9,100 +9,153 @@ created: 2026-07-15
 
 # I2C通信与EEPROM 24C02
 
-> **用生活理解**:I2C 就像教室里的传纸条规则——SCL 是老师拍手的节奏(时钟),SDA 是传的纸条(数据)。每次老师拍一下手(时钟跳变),纸条就传递一位信息。每个同学有唯一的学号(设备地址),只有学号对得上的人才能接收纸条
+> **用生活理解**:I2C 就像教室里的传纸条游戏——SCL 是老师拍手的节奏(时钟线),SDA 是传的纸条(数据线)。老师每拍一次手(时钟周期),大家就传递一位。每个同学有唯一的学号(设备地址 7 位),老师喊学号时对应同学才回答。STOP 条件就像下课铃——老师拍手节奏变了,表示传纸条结束
 
 ---
 
-## I2C协议基础
+## I2C 协议基础
 
-I2C(Inter-Integrated Circuit)是一种同步、半双工、多主从的串行通信总线,仅需 **2 根线**:SCL(时钟线)和 SDA(数据线)
+I2C(Inter-Integrated Circuit,读作 I-squared-C)是一种**同步、半双工、多主从**的串行通信总线,由 Philips 公司(现 NXP)发明
 
 | 特性 | 说明 |
 |------|------|
-| 线数 | SCL + SDA 两根 |
-| 模式 | 半双工 |
-| 速率 | 标准 100Kbps,快速 400Kbps |
-| 主从 | 多主多从(通过仲裁解决冲突) |
-| 地址 | 7位地址(最多 127 个设备)或 10位地址 |
-| 上拉 | 两根线都需要外部上拉电阻(4.7KΩ 典型) |
+| **线数** | SCL(时钟)+ SDA(数据)两根线 |
+| **模式** | 半双工(同一时刻只能单向传输) |
+| **速率** | 标准 100Kbps / 快速 400Kbps / 高速 3.4MHz |
+| **主从** | 多主多从(通过仲裁解决冲突) |
+| **地址** | 7 位地址(最多 127 个设备)或 10 位地址 |
+| **上拉电阻** | 两根线都需要外部上拉电阻(4.7KΩ 典型) |
 
-### I2C时序协议详解
+> **参考**:参考手册 §16(I2C 寄存器描述)
+> **参考**:NXP I2C 总线规范(UM10204)
 
-I2C 协议由几个基本操作组成:
+### I2C 时序协议详解
+
+#### 空闲状态
+
+SCL 和 SDA 都是高电平(由上拉电阻拉高)。
 
 #### 起始条件(Start Condition)
+
 ```
-SCL 在高电平时,SDA 从高电平切换到低电平
-```
-```
-SCL  ────┐  ┌────
-          │  │
-SDA  ────┘──┘────
+SCL 为高电平时,SDA 从高电平切换到低电平
 ```
 
-#### 终止条件(Stop Condition)
+**代码实现**:
+```c
+SCL_HIGH;       // SCL = 1
+SDA_HIGH;       // SDA = 1
+I2C_DELAY;      // 等待(数据建立时间)
+SDA_LOW;        // SDA = 0 → 起始条件(在 SCL 高电平时 SDA 变低)
+I2C_DELAY;      // 等待(保持时间)
 ```
-SCL 在高电平时,SDA 从低电平切换到高电平
+
+#### 停止条件(Stop Condition)
+
 ```
+SCL 为高电平时,SDA 从低电平切换到高电平
 ```
-SCL  ────┐  ┌────
-          │  │
-SDA  ────┘──┘────
+
+**代码实现**:
+```c
+SCL_LOW;        // SCL = 0(准备)
+SDA_LOW;        // SDA = 0
+SCL_HIGH;       // SCL = 1
+I2C_DELAY;
+SDA_HIGH;       // SDA = 0 → 1 → 终止条件(在 SCL 高电平时 SDA 变高)
+I2C_DELAY;
 ```
 
 #### 数据字节传输
-每个字节 8 位,MSB 先发。
-SCL 低电平时 SDA 改变数据,SCL 高电平时 SDA 保持稳定。
+
+每个字节 8 位,MSB(最高位)先发。SCL 低电平时允许 SDA 变化,SCL 高电平时 SDA 必须**稳定**(接收方采集数据)。
+
+```
+SCL:  ████░░░░████░░░░████░░░░....████░░░░██
+SDA:  ██░░██░░██░░██░░        ██░░██░░
+        ↑MSB                    ↑LSB
+       SCL低时SDA可以变     SCL高时采集
+```
 
 #### 应答(ACK/NACK)
-每个字节后,接收方在第 9 个 SCL 周期拉低 SDA 表示应答(ACK),释放 SDA 表示非应答(NACK)。
 
-### I2C完整通信流程
+每个字节后,接收方在第 9 个 SCL 周期控制 SDA:
+- **ACK**:SDA 拉低(表示"收到,继续")
+- **NACK**:SDA 释放(高电平,表示"收完"或"不再接收")
+
+```
+主机发送8位数据后:
+主机释放SDA → 从机在第9个SCL脉冲拉低SDA → 应答(ACK)
+```
+
+### I2C 完整通信流程
 
 **主机写数据到从机**:
 ```
-START + 从机地址(W) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP
+START + 7位从机地址 + R/W(0=写) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP
 ```
 
 **主机从从机读数据**:
 ```
-START + 从机地址(R) + ACK + 数据1 + ACK + 数据2 + NACK + STOP
+START + 7位从机地址 + R/W(1=读) + ACK + 从机发数据1 + 主机ACK + ... + 从机发数据N + 主机NACK + STOP
 ```
 
----
+**组合格式(先写地址再读)**:
+```
+START + 从机地址(W) + ACK + 寄存器地址 + ACK + RESTART + 从机地址(R) + ACK + 数据 + NACK + STOP
+```
 
-## I2C硬件电路设计
+---
 
-I2C 总线需要外部上拉电阻(RPU):
-```
-VCC(3.3V)
-  ├── RPU(4.7KΩ)
-  ├──── SCL
-  ├── RPU(4.7KΩ)  
-  └──── SDA
+## I2C 硬件电路设计
+
+```ascii
+VCC(3.3V)           VCC(3.3V)
+  ├── Rpu(4.7KΩ)     ├── Rpu(4.7KΩ)
+  │                  │
+  ├──── SCL ─────────┤
+  │   PB10           │  24C02 SCL
+  ├──── SDA ─────────┤
+  │   PB11           │  24C02 SDA
+  │                  │
+  GND ────────────── GND
 ```
 
-软件模拟 I2C 使用 GPIO 的**开漏输出**模式(CNF=01, MODE=11):
-- 开漏输出允许"线与"(多个设备可同时拉低总线)
-- 高电平靠外部上拉电阻实现
+I2C 总线需要外部上拉电阻(典型值 4.7KΩ),因为 I2C 引脚使用**开漏输出**:
+- 输出低电平:N-MOS 导通,拉低总线
+- 输出高电平:N-MOS 截止,靠上拉电阻拉高(开漏原理)
+- 这种设计允许多个设备"线与"(任意设备可拉低总线)
 
-> **参考**:原理图 I2C 接口部分(24C02 电路),参考手册 §16
+> **参考**:开发板原理图 I2C 接口部分(24C02 电路)
+> **参考**:参考手册 §16(I2C 寄存器)
 
 ---
 
-## 24C02 EEPROM
+## 24C02 EEPROM 详解
+
+24C02 是一个 **2Kbit(256 字节)** 的 I2C 串行 EEPROM。
 
 | 参数 | 值 |
 |------|-----|
 | 容量 | 256 字节(2Kbit) |
-| 页大小 | 8 字节(页写最多 8 字节) |
-| 地址 | 0xA0(写)/ 0xA1(读) |
-| 接口 | I2C |
+| 页大小 | **8 字节**(一次页写最多 8 字节) |
+| 写周期 | 5ms(内部擦写时间,期间不响应) |
+| I2C 地址 | 0xA0(写)/ 0xA1(读) |
+| 地址引脚 | A0/A1/A2(可配置,最多 8 个同型号设备共用总线) |
 | 工作电压 | 1.8V~5.5V |
+| 接口 | I2C(标准/快速模式) |
 
-**页写限制**:24C02 每页 8 字节,跨页写时需要手动分页(超过页边界会自动卷回页开头)。
+**地址计算**:
+```
+设备地址 = 1010 + A2 + A1 + A0 + R/W
+           [7:4]  [3]  [2]  [1]  [0]
+               1010  0    0    0   0/1
 
-**写周期**:每个写操作后需要等待 5ms(内部擦写时间),在此期间不响应任何 I2C 命令。
+写地址 = 1010 0000 = 0xA0
+读地址 = 1010 0001 = 0xA1
+```
+
+**页写注意事项**:24C02 的页大小为 8 字节,一次页写操作只能在**同一页内**。如果尝试跨页写入(如从地址 0x07 开始写 3 字节),地址会**自动卷回**到页开头(地址 0x00),覆盖已有数据。跨页时必须**手动分页写入**。
 
 ---
 
@@ -120,18 +173,20 @@ VCC(3.3V)
 
 #include "stm32f10x.h"
 
-// 引脚宏定义:PB10=SCL, PB11=SDA
-#define SCL_HIGH  (GPIOB->ODR |= GPIO_ODR_ODR10)   // |= SCL置1
-#define SCL_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR10)  // &=~ SCL清0
-#define SDA_HIGH  (GPIOB->ODR |= GPIO_ODR_ODR11)   // |= SDA置1
-#define SDA_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR11)  // &=~ SDA清0
-
-#define READ_SDA  (GPIOB->IDR & GPIO_IDR_IDR11)    // & 读取SDA电平
+// GPIO 引脚: PB10=SCL, PB11=SDA
+// 使用宏定义简化位操作,提升代码可读性
+#define SCL_HIGH  (GPIOB->ODR |=  GPIO_ODR_ODR10)   // |= 将ODR10置1
+#define SCL_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR10)   // &=~ 将ODR10清0
+#define SDA_HIGH  (GPIOB->ODR |=  GPIO_ODR_ODR11)   // |= 将ODR11置1
+#define SDA_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR11)   // &=~ 将ODR11清0
+#define READ_SDA  (GPIOB->IDR &   GPIO_IDR_IDR11)   // & 读取IDR11位
 
 #define ACK  0
 #define NACK 1
 
-#define I2C_DELAY  Delay_us(10)  // 100KHz:半周期延时10us
+// I2C 延时: 100KHz 模式下 10us ≈ 半个时钟周期
+// 调整此值可改变 I2C 通信速率
+#define I2C_DELAY  Delay_us(10)
 
 void I2C_Init(void);
 void I2C_Start(void);
@@ -145,71 +200,134 @@ uint8_t I2C_ReadByte(void);
 #endif
 ```
 
-**文件:`stm32_base/13_i2c_software_register/Hardware/I2C/i2c.c`**(核心函数)
+**文件:`stm32_base/13_i2c_software_register/Hardware/I2C/i2c.c`**
 
 ```c
 #include "i2c.h"
 
+// I2C 初始化: PB10(SCL) + PB11(SDA) 开漏输出
 void I2C_Init(void)
 {
-    // 开启GPIOB时钟
-    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
-    // PB10(PB11) 开漏输出:MODE=11(50MHz), CNF=01(开漏)
-    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);   // MODE位置1
-    GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1); // CNF[1]清0
-    GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0);  // CNF[0]置1
+    // 1. 开启 GPIOB 时钟
+    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;       // |=: IOPBEN位置1
+
+    // 2. PB10, PB11 开漏输出 50MHz
+    // CNF=01(开漏输出), MODE=11(50MHz)
+    // MODE10[1:0] 在 CRH 第 8~9 位, CNF10[1:0] 在第 10~11 位
+    // MODE11[1:0] 在 CRH 第 12~13 位, CNF11[1:0] 在第 14~15 位
+    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);       // MODE=11
+    GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1);    // CNFx[1]=0
+    GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0);     // CNFx[0]=1
+    // CNF[1:0]=01 → 开漏输出
 }
 
-// 起始:SCL高电平时,SDA高→低
+// 起始条件: SCL=1 时 SDA 下降沿
 void I2C_Start(void)
 {
-    SCL_HIGH;  // 先确保SCL高
-    SDA_HIGH;  // SDA先高
+    SCL_HIGH;   // 先确保 SCL 电平(准备总线)
+    SDA_HIGH;   // SDA 先高
     I2C_DELAY;
-    SDA_LOW;   // SDA从高变低 → 起始条件
+    SDA_LOW;    // SDA 从高变低 → 起始条件
     I2C_DELAY;
+    // 起始后 SCL 可拉低准备传输数据
+    SCL_LOW;
 }
 
-// 停止:SCL高电平时,SDA低→高
+// 停止条件: SCL=1 时 SDA 上升沿
 void I2C_Stop(void)
 {
-    SCL_HIGH;  // SCL拉高
-    SDA_LOW;   // SDA先低
+    SCL_LOW;    // 先确保 SCL 低
+    SDA_LOW;    // SDA 拉低
+    I2C_DELAY;
+    SCL_HIGH;   // SCL 先高
     I2C_DELAY;
-    SDA_HIGH;  // SDA从低变高 → 停止条件
+    SDA_HIGH;   // SDA 从低变高 → 停止条件
     I2C_DELAY;
 }
 
-// 发送ACK:在第9个时钟将SDA拉低
+// 发送应答: 在第 9 个时钟拉低 SDA
 void I2C_Ack(void)
 {
-    SDA_LOW;   // SDA拉低表示应答
-    SCL_HIGH;  // SCL产生脉冲
+    SDA_LOW;    // SDA 拉低 = 应答
+    I2C_DELAY;
+    SCL_HIGH;   // SCL 脉冲(从机采样)
     I2C_DELAY;
     SCL_LOW;
     I2C_DELAY;
-    SDA_HIGH;  // 释放SDA
+    SDA_HIGH;   // 释放 SDA
 }
 
-// 发送NACK:在第9个时钟释放SDA
+// 发送非应答: 在第 9 个时钟释放 SDA
 void I2C_NAck(void)
 {
-    SDA_HIGH;  // SDA高表示非应答
-    SCL_HIGH;  // SCL产生脉冲
+    SDA_HIGH;   // SDA 高 = 非应答
+    I2C_DELAY;
+    SCL_HIGH;   // SCL 脉冲
     I2C_DELAY;
     SCL_LOW;
+    I2C_DELAY;
 }
 
-// 等待从机ACK:返回0=有ACK, 1=无ACK
+// 等待从机应答: 返回 0=ACK, 1=NACK
 uint8_t I2C_Wait4Ack(void)
 {
-    SDA_HIGH;  // 释放SDA(让从机控制)
-    SCL_HIGH;  // SCL高电平采样
+    SDA_HIGH;   // 释放 SDA(让从机控制)
     I2C_DELAY;
-    uint8_t ack = READ_SDA;  // &测试: 读取SDA电平
-    SCL_LOW;
+    SCL_HIGH;   // SCL 高 = 从机可应答
+    I2C_DELAY;
+
+    uint8_t ack = READ_SDA;   // & 测试: 读取 SDA 电平
+    // SDA=0 → 从机应答(ACK); SDA=1 → 从机非应答(NACK)
+
+    SCL_LOW;    // 完成应答位
     I2C_DELAY;
-    return ack ? NACK : ACK;  // SDA=0: 从机应答; SDA=1: 无应答
+
+    return ack ? NACK : ACK;
+}
+
+// 发送 1 字节(MSB first)
+void I2C_SendByte(uint8_t byte)
+{
+    for (uint8_t i = 0; i < 8; i++)
+    {
+        // SCL 低电平时改变 SDA
+        if (byte & 0x80)     // &测试: 最高位=1?
+            SDA_HIGH;
+        else
+            SDA_LOW;
+
+        byte <<= 1;           // 左移, 准备下一位
+
+        // SCL 高电平期间 SDA 保持稳定(从机采样)
+        SCL_HIGH;
+        I2C_DELAY;
+        SCL_LOW;
+        I2C_DELAY;
+    }
+}
+
+// 读取 1 字节(MSB first)
+uint8_t I2C_ReadByte(void)
+{
+    uint8_t byte = 0;
+
+    SDA_HIGH;   // 释放 SDA(让从机控制)
+
+    for (uint8_t i = 0; i < 8; i++)
+    {
+        byte <<= 1;           // 左移留出低位
+
+        SCL_HIGH;             // SCL 高 → 从机将数据放到 SDA
+        I2C_DELAY;
+
+        if (READ_SDA)         // & 测试: 读取 SDA 电平
+            byte |= 1;        // 如果 SDA=1, 置当前位为 1
+
+        SCL_LOW;              // SCL 低 → 从机准备下一位
+        I2C_DELAY;
+    }
+
+    return byte;
 }
 ```
 
@@ -220,35 +338,43 @@ uint8_t I2C_Wait4Ack(void)
 
 int main(void)
 {
-    usart1_init();         // 串口初始化(用于打印调试
-    I2C_Init();            // I2C GPIO初始化
+    USART1_Init();     // 串口初始化(调试输出用
+    I2C_Init();        // I2C GPIO 初始化
 
-    // 测试:向24C02地址0x00写入0x55
+    /* ========== 写操作: 向 24C02 地址 0x00 写入 0x55 ========== */
     I2C_Start();
-    I2C_SendByte(0xA0);    // 从机地址+写 (0xA0)
-    I2C_Wait4Ack();        // 等待24C02应答
-    I2C_SendByte(0x00);    // 写入地址(24C02的字节地址)
+    I2C_SendByte(0xA0);      // 从机地址 + 写标志 (0xA0 = 1010 0000)
+    I2C_Wait4Ack();          // 等待 24C02 应答
+
+    I2C_SendByte(0x00);      // 要写入的 24C02 内部字节地址
     I2C_Wait4Ack();
-    I2C_SendByte(0x55);    // 写入数据
+
+    I2C_SendByte(0x55);      // 要写入的数据
     I2C_Wait4Ack();
     I2C_Stop();
 
-    Delay_ms(10);          // 等待内部写周期完成
+    // 等待 24C02 内部写周期完成(约 5ms)
+    Delay_ms(10);
 
-    // 读回验证:从地址0x00读取
+    /* ========== 读操作: 从 24C02 地址 0x00 读取数据 ========== */
+    // 组合格式: START + 写地址 + 寄存器地址 + RESTART + 读地址 + 数据
     I2C_Start();
-    I2C_SendByte(0xA0);
+    I2C_SendByte(0xA0);      // 写地址(先写寄存器地址)
     I2C_Wait4Ack();
-    I2C_SendByte(0x00);    // 再次发出地址
+    I2C_SendByte(0x00);      // 要读取的字节地址
     I2C_Wait4Ack();
-    I2C_Start();           // 重启(组合格式)
-    I2C_SendByte(0xA1);    // 从机地址+读 (0xA1)
+
+    I2C_Start();              // RESTART: 不发送 STOP 直接发第二个 START
+    I2C_SendByte(0xA1);      // 读地址
     I2C_Wait4Ack();
+
     uint8_t data = I2C_ReadByte();  // 读取数据
-    I2C_NAck();            // 主机发NACK表示读取结束
+    I2C_NAck();               // 最后一字节主机发 NACK
     I2C_Stop();
 
-    printf("Read data: 0x%02X\r\n", data);  // 应输出0x55
+    printf("写入 0x55, 读出 0x%02X\r\n", data);
+    // 预期输出: "写入 0x55, 读出 0x55"
+
     while (1);
 }
 ```
@@ -257,21 +383,22 @@ int main(void)
 
 ## 核心函数速查表
 
-| 函数 | 说明 |
-|------|------|
-| `I2C_Init()` | PB10(SCL)+PB11(SDA) 开漏输出初始化 |
-| `I2C_Start()` | SCL高→SDA高→低切换,产生起始条件 |
-| `I2C_Stop()` | SCL高→SDA低→高切换,产生停止条件 |
-| `I2C_SendByte(byte)` | 逐位发送8位数据(MSB first) |
-| `I2C_ReadByte()` | 逐位读取8位数据 |
-| `I2C_Wait4Ack()` | 等待从机ACK(返回0=应答) |
-| `I2C_Ack()` | 主机发送ACK |
-| `I2C_NAck()` | 主机发送NACK(读最后一个字节后) |
+| I2C 操作 | 函数 | 说明 |
+|----------|------|------|
+| 初始化 | `I2C_Init()` | PB10(SCL)+PB11(SDA) 开漏输出 |
+| 起始条件 | `I2C_Start()` | SCL高→SDA高→低切换 |
+| 停止条件 | `I2C_Stop()` | SCL高→SDA低→高切换 |
+| 发送字节 | `I2C_SendByte(byte)` | 8 位逐位发送,MSB first |
+| 读取字节 | `I2C_ReadByte()` | 8 位逐位读取 |
+| 等待应答 | `I2C_Wait4Ack()` | 返回 ACK(0) 或 NACK(1) |
+| 发应答 | `I2C_Ack()` | 拉低 SDA 表示继续接收 |
+| 发非应答 | `I2C_NAck()` | 释放 SDA 表示停止接收 |
 
 ## 常见问题与避坑
 
-1. **I2C 总线卡死(SDA 一直为低)** → 从机异常,发 9 个以上的 SCL 脉冲让从机释放 SDA
-2. **24C02 写入后读回不对** → 页写溢出(跨页写会自动卷回),需分页写入
-3. **NACK 持续** → 设备地址错误、上拉电阻缺失、SCL 频率太高
-4. **Start 条件后无 ACK** → 检查从机电源、I2C 地址(7位+读写位 = 8位)
-5. **软件 I2C 有时钟干扰** → I2C_DELAY 时间不够,增加至 20~50us
+1. **I2C 总线卡死(SDA 一直被拉低)** → 从机异常,连续发 9~18 个 SCL 脉冲让从机释放 SDA,然后发 STOP
+2. **NACK 持续(从机不应答)** → 检查设备地址(7 位+读写位是否正确)、上拉电阻是否焊接、SCL 频率是否太高
+3. **24C02 写入后读回不对** → 页写跨页溢出(每页 8 字节,跨页自动卷回!),需分页写入或逐字节写
+4. **写操作后需等待** → 24C02 需要约 5ms 内部写周期,在此期间不响应任何 I2C 操作
+5. **组合格式(先写后读)** → 中间的 RESTART 不是 STOP+START,必须用 `I2C_Start()` 但不先发 `I2C_Stop()`
+6. **延时偏短导致时序异常** → I2C_DELAY 不够时增加(10us→20us),标准模式至少 5us 半周期

+ 198 - 86
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/08-SysTick与通用定时器TIM.md

@@ -9,185 +9,297 @@ created: 2026-07-15
 
 # SysTick与通用定时器TIM
 
-> **用生活理解**:SysTick 就像秒表倒计时——设定一个时间(LOAD值),它倒数到 0 就提醒你(中断)。TIM通用定时器就像高级秒表,可以预分频(把太快的时间变成慢的)、自动重装(倒计时到0自动重新开始),适合精确控制 LED 闪烁频率
+> **用生活理解**:SysTick 就像厨房里的倒计时秒表——设好时间(LOAD 值),它倒数到 0 就"叮"一声(中断)。TIM 通用定时器就像高级可编程定时器——可以预分频(时间压缩/扩展)、自动重装(倒计时到 0 自动重新开始)、还能输出 PWM 或测量外部脉冲。SysTick 是内核自带的"简单秒表",TIM 是片上外设的"多功能定时器"
 
 ---
 
 ## SysTick 系统定时器
 
-SysTick 是 Cortex-M3 内核自带的**24 位递减计数器**,属于内核外设(不依赖具体芯片厂商)。
+SysTick 是 **Cortex-M3 内核自带的 24 位递减计数器**,属于内核外设,与具体芯片厂商无关。**在所有 Cortex-M3 产品间处理方式相同**。
+
+> **参考**:Cortex-M3 权威指南 §8(SysTick 定时器)
 
 ### SysTick 寄存器
 
 | 寄存器 | 地址偏移 | 功能 |
 |--------|---------|------|
-| CTRL | 0x00 | 控制与状态寄存器 |
-| LOAD | 0x04 | 重装载数值寄存器 |
-| VAL | 0x08 | 当前数值寄存器 |
-| CALIB | 0x0C | 校准数值寄存器(很少用) |
+| **CTRL** | 0x00 | 控制与状态寄存器 |
+| **LOAD** | 0x04 | 重装载数值寄存器(24 位有效) |
+| **VAL** | 0x08 | 当前数值寄存器 |
+| CALIB | 0x0C | 校准寄存器(一般不用) |
 
 **CTRL 寄存器位说明**:
-| 位 | 名称 | 说明 |
-|----|------|------|
-| 0 | ENABLE | 1=开启定时器,0=关闭 |
-| 1 | TICKINT | 1=计数到0时产生SysTick异常 |
-| 2 | CLKSOURCE | 1=AHB时钟(72MHz),0=AHB/8(9MHz) |
-| 16 | COUNTFLAG | 计数到0时硬件置1,读后自动清0 |
+
+| 位 | 名称 | 值含义 | 说明 |
+|----|------|--------|------|
+| 0 | **ENABLE** | 1=启动, 0=停止 | 计数器使能 |
+| 1 | **TICKINT** | 1=允许中断, 0=禁止 | 计数到 0 时是否产生 SysTick 异常 |
+| 2 | **CLKSOURCE** | 1=AHB(72MHz), 0=AHB/8(9MHz) | 时钟源选择 |
+| 16 | **COUNTFLAG** | 自动置 1 | 计数到 0 时硬件置 1,读后或写 CTRL 时自动清 0 |
 
 **LOAD 寄存器**:24 位有效(最大值 16,777,215)。
-`VAL 从 LOAD 值开始递减 → 到 0 时产生 COUNTFLAG → 自动重新装载 LOAD 值`
+
+**VAL 寄存器**:读返回当前计数值;写任意值清 0 并清除 COUNTFLAG。
+
+### SysTick 的两种用法
+
+| 用法 | 配置 | 特点 |
+|------|------|------|
+| **查询延时** | CLKSOURCE=AHB, TICKINT=0 | 轮询 COUNTFLAG 标志,阻塞式 |
+| **周期性中断** | CLKSOURCE=AHB/8, TICKINT=1 | 产生 1ms 定时中断(FreeRTOS 时基) |
 
 ### SysTick 延时实现(查询方式)
 
 ```c
-// 微秒延时(AHB=72MHz)
+// 微秒延时(基于 SysTick 查询,AHB=72MHz)
 void Delay_us(uint32_t us)
 {
-    SysTick->LOAD = 72 * us;        // 72MHz下,1us需72次计数
-    SysTick->VAL  = 0;              // 清当前值
-    SysTick->CTRL = 0x05;           // ENABLE=1, CLKSOURCE=1(AHB)
+    // LOAD: 72MHz 下每 1us 需要计数 72 次
+    // LOAD 最大 24 位 = 16,777,215,单次最大延时 ≈ 233ms
+    SysTick->LOAD = 72 * us;
+
+    // VAL: 清当前值(写任何值都清 0,同时清除 COUNTFLAG)
+    SysTick->VAL  = 0;
+
+    // CTRL: ENABLE=1, CLKSOURCE=1(AHB), TICKINT=0(无中断)
+    // 0x05 = 二进制 0000 0101
+    SysTick->CTRL = 0x05;
 
-    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG))  // 等待计数到0
+    // 轮询等待 COUNTFLAG 置 1(计数到 0)
+    // SysTick_CTRL_COUNTFLAG = (1UL << 16)
+    while (!(SysTick->CTRL & SysTick_CTRL_COUNTFLAG))
     {
     }
-    SysTick->CTRL &= ~SysTick_CTRL_ENABLE;  // 关闭定时器
+
+    // 关闭定时器(写 0 到 ENABLE 位)
+    SysTick->CTRL &= ~SysTick_CTRL_ENABLE;  // &=~: ENABLE位清0
+}
+
+// 毫秒级延时(累加 1000 次微秒)
+void Delay_ms(uint32_t ms)
+{
+    while (ms--)
+    {
+        Delay_us(1000);  // 1ms = 1000us
+    }
 }
-```
 
-> **注意**:LOAD 最大 24 位(16,777,215),最大单次延时 = 16,777,215 / 72 ≈ 233ms
+void Delay_s(uint32_t s)
+{
+    while (s--)
+    {
+        Delay_ms(1000);
+    }
+}
+```
 
 ---
 
 ## 通用定时器 TIM
 
-STM32F103ZET6 有多个定时器:
+STM32F103ZET6 有 7 个定时器:
 
 | 定时器 | 类型 | 位数 | 主要特性 |
 |--------|------|------|---------|
-| TIM1 | 高级 | 16 | 带互补输出、死区插入、刹车功能 |
-| TIM2~5 | 通用 | 16 | 时基、PWM、捕获/比较 |
-| TIM6~7 | 基本 | 16 | 仅时基(无PWM/捕获) |
+| **TIM1** | 高级 | 16 | 带互补 PWM 输出、死区插入、刹车功能(电机控制) |
+| **TIM2~5** | 通用 | 16 | 时基、PWM、捕获/比较(最常用) |
+| **TIM6~7** | 基本 | 16 | 仅时基功能,无外部 I/O |
 
 ### TIM 时基单元结构
 
 ```
-预分频器(PSC) → 计数器(CNT) → 自动重装载(ARR) → 更新事件(UEV)
+APBx 时钟 → PSC(预分频器) → CK_CNT → CNT(计数器) → ARR(自动重装)
+                         ↓                       ↓
+                      控制计数器时钟          溢出/更新事件(UEV)
 ```
 
-**计数器时钟频率**:`CK_CNT = CK_PSC / (PSC + 1)`
+**关键寄存器**:
+| 寄存器 | 说明 | 范围 |
+|--------|------|------|
+| **PSC** (预分频器) | 对时钟分频,产生 CK_CNT | 16 位(0~65535) |
+| **CNT** (计数器) | 当前计数值 | 16 位(0~65535) |
+| **ARR** (自动重装载) | 计数到该值时产生溢出 | 16 位(0~65535) |
+| **CR1** (控制寄存器1) | CEN(使能)、UDIS(更新禁止)、URS(更新请求源)、OPM(单脉冲)、ARPE(自动重装预装载) | — |
+| **DIER** (中断使能) | UIE(更新中断)、CCxIE(捕获比较中断) | — |
+| **SR** (状态寄存器) | UIF(更新标志)、CCxIF(捕获比较标志) | — |
 
-**溢出时间**:`Tout = (ARR + 1) × (PSC + 1) / CK_PSC`
+### 计数器时钟频率计算
 
-**示例**:CK_PSC=72MHz, PSC=7199, ARR=9999
-`CK_CNT = 72MHz / 7200 = 10KHz`
-`Tout = 10000 × 10KHz = 1s`(1秒溢出一次)
+```
+CK_CNT = CK_PSC / (PSC + 1)
+```
 
-### TIM 关键寄存器
+其中:
+- CK_PSC = APB1 时钟(TIM2~7)或 APB2 时钟(TIM1)
+- **注意**:如果 APB1 分频系数 ≠ 1,则 TIM2~7 的时钟 = APB1×2
 
-| 寄存器 | 功能 |
-|--------|------|
-| TIMx_PSC | 预分频器(16位,0~65535) |
-| TIMx_CNT | 当前计数值 |
-| TIMx_ARR | 自动重装载值 |
-| TIMx_CR1 | 控制寄存器1(CEN使能、DIR方向、ARPE自动重装载预装载) |
-| TIMx_DIER | 中断使能寄存器(UIE更新中断、CCxIE捕获比较中断) |
-| TIMx_SR | 状态寄存器(UIF更新标志、CCxIF捕获比较标志) |
+**示例**(APB1=36MHz,分频=2 → TIM 时钟=72MHz):
+```
+PSC = 7199 → CK_CNT = 72MHz / 7200 = 10KHz (0.1ms/计数)
+PSC = 71999 → CK_CNT = 72MHz / 72000 = 1KHz (1ms/计数)
+```
+
+### 溢出时间公式
+
+```
+Tout = (ARR + 1) × (PSC + 1) / CK_PSC
+
+CK_PSC = TIM 时钟频率(通常是 72MHz)
+所以: Tout = (ARR + 1) × (PSC + 1) / 72,000,000
+```
+
+| ARR | PSC | 定时时长 | 分辨率 |
+|-----|-----|---------|--------|
+| 999 | 7199 | 1000×7200/72M = 100ms | 0.1ms |
+| 9999 | 7199 | 10000×7200/72M = 1s | 0.1ms |
+| 999 | 71999 | 1000×72000/72M = 1s | 1ms |
+| 65535 | 65535 | 65536×65536/72M = ~59.6s | 0.91μs |
 
 > **参考**:参考手册 §14(通用定时器寄存器描述)
 
+### CR1 寄存器关键位
+
+| 位 | 名称 | 功能说明 |
+|----|------|----------|
+| 0 | **CEN** | 计数器使能(0=停止,1=启动) |
+| 4 | **DIR** | 计数方向(0=向上计数,1=向下计数) 仅 TIM1/8 等可配 |
+| 7 | **ARPE** | Auto-Reload Preload Enable(0=ARR立即更新,1=ARR在下次更新事件时更新) |
+| 5 | **OPM** | 单脉冲模式(1=计数到 0 后自动停止) |
+
 ---
 
 ## 实验:SysTick/TIM 定时控制 LED 闪烁
 
 ### 软件设计(SysTick 中断版)
 
+**项目路径**:`stm32_base/16_systick_led_twinkle_register`
+
 **文件:`stm32_base/16_systick_led_twinkle_register/Hardware/SysTick/systick.c`**
 
 ```c
 #include "systick.h"
 
-// SysTick 初始化:配置1ms中断一次
+// SysTick 初始化:产生 1ms 周期中断
 void Systick_Init(void)
 {
-    // 72MHz / 72000 = 1KHz = 1ms中断一次
-    // LOAD = 72000 - 1 = 71999
-    SysTick->LOAD = 72000 - 1;
-    SysTick->VAL  = 0;
-    // CTRL: ENABLE=1, TICKINT=1(中断使能), CLKSOURCE=1(AHB)
-    SysTick->CTRL |= (SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_CLKSOURCE_Msk);
+    // 选择 AHB/8 = 9MHz(更准确,且与 FreeRTOS 常用配置兼容)
+    // 1ms 中断: LOAD = 9MHz / 1000 - 1 = 8999
+    // 使用 AHB/8 可避免 SysTick 频率过高导致频繁中断
+
+    // CLKSOURCE=0 (AHB/8=9MHz), TICKINT=1, ENABLE=1
+    SysTick->LOAD = 9000 - 1;             // 9MHz/1000 = 9K, 9000-1
+    SysTick->VAL  = 0;                    // 清当前值
+    SysTick->CTRL = SysTick_CTRL_TICKINT_Msk  // |= TICKINT=1
+                  | SysTick_CTRL_ENABLE_Msk;  // |= ENABLE=1
+    // 注意: CLKSOURCE 默认为 0(上电复位值)
 }
 
-// SysTick 中断服务函数(在启动文件中注册)
+// SysTick 中断服务函数(由启动文件的中断向量表引用
 void SysTick_Handler(void)
 {
-    static uint16_t tick = 0;
-    tick++;
-    if (tick >= 1000)  // 1000ms = 1s
+    static uint16_t tick_count = 0;
+
+    tick_count++;
+    if (tick_count >= 1000)               // 1000 × 1ms = 1s
     {
-        LED_Toggle(LED_1);  // 翻转LED
-        tick = 0;
+        LED_Toggle(LED_1);               // 翻转 LED(每秒闪烁一次)
+        tick_count = 0;
     }
 }
 ```
 
 ### 软件设计(TIM 中断版)
 
+**项目路径**:`stm32_base/18_tim_led_twinkle_register`
+
 **文件:`stm32_base/18_tim_led_twinkle_register/Hardware/TIM5/tim5.c`**
 
 ```c
 #include "tim5.h"
 
+// TIM5 初始化:1s 溢出一次,产生更新中断
 void TIM5_Init(void)
 {
-    // 1. 开启TIM5时钟(APB1总线)
-    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
-
-    // 2. 配置预分频器:72MHz / (7199+1) = 10KHz
-    TIM5->PSC = 7200 - 1;
-
-    // 3. 配置自动重装载值:10KHz / (10000+1) ≈ 1秒溢出一次
-    TIM5->ARR = 10000 - 1;
-
-    // 4. 清计数器
-    TIM5->CNT = 0;
-
-    // 5. 使能更新中断
-    TIM5->DIER |= TIM_DIER_UIE;
-
-    // 6. 使能TIM5中断(NVIC)
-    NVIC_EnableIRQ(TIM5_IRQn);
-
-    // 7. 使能计数器(CEN=1)
-    TIM5->CR1 |= TIM_CR1_CEN;
+    /* ========== 1. 开启 TIM5 时钟 ========== */
+    // TIM5 挂载在 APB1 总线上(APB1 时钟 = 36MHz)
+    // 但若 APB1 分频≠1,则定时器时钟 = APB1 × 2 = 72MHz
+    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;     // |=: TIM5时钟使能
+
+    /* ========== 2. 配置 PSC 和 ARR ========== */
+    // 目标: Tout = 1s
+    // CK_PSC = 72MHz, PSC = 7200 - 1 → CK_CNT = 72M/7200 = 10KHz
+    // ARR = 10000 - 1 → Tout = 10000 / 10KHz = 1s
+    TIM5->PSC = 7200 - 1;                   // 预分频: 72MHz → 10KHz
+    TIM5->ARR = 10000 - 1;                  // 自动重装载值
+
+    /* ========== 3. 清计数器 ========== */
+    TIM5->CNT = 0;                           // 从 0 开始计数
+
+    /* ========== 4. 使能更新中断 ========== */
+    // UIE = 1: 允许更新事件产生中断
+    TIM5->DIER |= TIM_DIER_UIE;             // |=: UIE位置1
+
+    /* ========== 5. 使能 NVIC 中断 ========== */
+    // TIM5 的中断号 = TIM5_IRQn
+    NVIC_EnableIRQ(TIM5_IRQn);              // 在 NVIC 中使能 TIM5 中断
+
+    /* ========== 6. 启动定时器 ========== */
+    // CEN=1: 计数器开始工作
+    TIM5->CR1 |= TIM_CR1_CEN;               // |=: CEN位置1
 }
 
 // TIM5 中断服务函数
 void TIM5_IRQHandler(void)
 {
-    if (TIM5->SR & TIM_SR_UIF)  // 检查更新中断标志
+    // 检查 SR.UIF(更新中断标志位)
+    // UIF=1 表示计数器从 ARR 溢出回 0,产生了更新事件
+    if (TIM5->SR & TIM_SR_UIF)              // &测试: UIF位=1?
     {
-        LED_Toggle(LED_1);      // 翻转LED
-        TIM5->SR &= ~TIM_SR_UIF;  // 清标志(写0清除)
+        LED_Toggle(LED_1);                  // 翻转 LED
+
+        // 清除 UIF 标志(必须软件清除,否则持续进中断)
+        // 注意: SR 寄存器是"写 0 清除"
+        TIM5->SR &= ~TIM_SR_UIF;            // &=~: UIF位清0
     }
 }
 ```
 
 ---
 
+## SysTick vs TIM 对比
+
+| 对比项 | SysTick | TIM 通用定时器 |
+|--------|---------|--------------|
+| **属于** | Cortex-M3 内核 | 芯片厂家外设 |
+| **位数** | 24 位 | 16 位 |
+| **预分频** | 固定 AHB 或 AHB/8 | 可编程 PSC (0~65535) |
+| **多通道** | 无 | 4 通道(捕获/比较/PWM) |
+| **中断** | 仅更新中断 | 更新 + 多通道捕获比较中断 |
+| **精度** | 一般 | 高 |
+| **用途** | 时基、裸机延时、RTOS 心跳 | PWM、捕获、精确计时 |
+| **数量** | 1 个(全芯片唯一) | 7 个(多定时器可并行) |
+
+---
+
 ## 核心速查表
 
-| 操作 | SysTick | TIM通用定时器 |
+| 操作 | SysTick | TIM 通用定时器 |
 |------|---------|--------------|
-| 初始化 | `SysTick->CTRL = 0x07` | `TIMx->CR1 |= TIM_CR1_CEN` |
-| 预分频 | 不可配(固定AHB或AHB/8) | `TIMx->PSC = value` |
-| 重装载 | `SysTick->LOAD = value` | `TIMx->ARR = value` |
-| 中断使能 | CTRL.TICKINT=1 | DIER.UIE=1 + NVIC |
-| 清标志 | 读CTRL自动清 | `SR &= ~UIF` |
+| 开启时钟 | 无需开启(内核自带) | `RCC->APB1ENR |= TIMxEN` |
+| 配置预分频 | 无(AHB 或 AHB/8 固定) | `TIMx->PSC = value` |
+| 配置重装载 | `SysTick->LOAD = value` | `TIMx->ARR = value` |
+| 计数方向 | 只减 | 可增可减 |
+| 使能中断 | `CTRL |= TICKINT` | `DIER |= UIE` + `NVIC_EnableIRQ()` |
+| 启动 | `CTRL |= ENABLE` | `CR1 |= CEN` |
 | 停止 | `CTRL &= ~ENABLE` | `CR1 &= ~CEN` |
+| 清中断标志 | 自动(读 CTRL) | `SR &= ~UIF`(写 0) |
+| 当前计数值 | 读 `VAL` | 读 `CNT` |
 
 ## 常见问题与避坑
 
-1. **SysTick 中断不触发** → 检查 CTRL.TICKINT 是否置 1、NVIC 优先级是否设置
-2. **TIM 溢出时间不对** → 检查 PSC/ARR 计算、确认时钟源频率(APB1=36MHz 还是 72MHz?)
-3. **AHB/APB1 时钟差异** → TIM2~5 挂载 APB1,若 APB1 分频≠1 则定时器时钟 = APB1×2
-4. **更新标志不清导致只中断一次** → ISR 中必须手动清除 SR.UIF
+1. **SysTick 中断不触发** → 检查 CTRL.TICKINT 是否置 1、NVIC 优先级配置(SysTick 属于异常,优先级在 SHPR3 寄存器)
+2. **TIM 定时时间不对** → 检查 APB1 分频系数:如果 APB1 预分频 ≠1,则 TIM 时钟 = APB1×2(不要直接按 APB1 频率算 CK_PSC)
+3. **中断只进一次** → ISR 中忘了清除 SR.UIF 标志(SR 是写 0 清除)
+4. **修改 ARR 后定时时间不变** → CR1.ARPE=0(ARR 立即更新)或 ARPE=1(等待下次更新事件)
+5. **TIM 与 SysTick 中断冲突** → 注意优先级配置,SysTick 常用最低优先级
+6. **Delay_us 最大延时限制** → LOAD 最大 0xFFFFFF(16,777,215),72MHz 下单次最大 ≈ 233ms。要更长时间需循环累积

+ 240 - 74
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/09-TIM高级应用:PWM与输入捕获.md

@@ -9,89 +9,163 @@ created: 2026-07-15
 
 # TIM高级应用:PWM与输入捕获
 
-> **用生活理解**:PWM 就像快速开关灯——如果你每秒开关 1000 次,眼睛就感觉灯光变暗了。开的时间比例(占空比)越大就越亮。呼吸灯就是占空比从 0% 逐渐变到 100% 再回来,像人呼吸一样平滑
+> **用生活理解**:PWM 就像快速开关灯——如果你的手足够快(每秒 1000 次),眼睛看见的就不是闪烁而是灯光变暗了。开的时间比例(占空比)越大就越亮。**呼吸灯**就是占空比从 0% 逐渐增加到 100% 再回来,像呼吸一样平滑。**输入捕获**就像用第二个秒表测量第一个秒表的读数——测量外部信号的频率或脉宽
 
 ---
 
 ## PWM 原理
 
-PWM = Pulse Width Modulation(脉冲宽度调制),通过调节脉冲宽度(占空比)控制平均电压。
+PWM = Pulse Width Modulation(脉冲宽度调制),通过调节脉冲宽度(占空比)控制平均电压。
 
 ### 基本参数
 
-| 参数 | 公式 | 示例(50%占空比, 1KHz) |
-|------|------|------------------------|
-| 频率 | `Freq = CK_PSC / (PSC+1) / (ARR+1)` | 72MHz/72/1000 = 1KHz |
-| 占空比 | `Duty = CCR / (ARR+1) × 100%` | 500/1000 = 50% |
-| 分辨率 | `Resolution = 1 / (ARR+1) × 100%` | 1/1000 = 0.1% |
+```
+输出: ████░░░░░░░░  频率 = 1KHz, 占空比 = 40%
+     ↑CCR    ↑ARR
+     └ 高电平 ┘
+```
+
+**PWM 频率**:`Freq = CK_PSC / (PSC + 1) / (ARR + 1)`
+
+**PWM 占空比**:`Duty = CCR / (ARR + 1) × 100%`
 
-- **PSC**(预分频器):决定计数时钟频率
-- **ARR**(自动重装载值):决定 PWM 周期
-- **CCR**(捕获/比较值):决定 PWM 占空比
+**PWM 分辨率**:`Resolution = 1 / (ARR + 1) × 100%`
+
+**典型配置示例**:
+```
+目标: 1KHz PWM, 可用占空比 0~999 (0.1% 分辨率)
+
+CK_PSC = 72MHz
+PSC = 72 - 1    → CK_CNT = 72M / 72 = 1MHz
+ARR = 1000 - 1  → Freq = 1MHz / 1000 = 1KHz
+CCR = 0~999     → Duty = 0~99.9%
+```
 
 ### 输出比较模式
 
-| 模式 | 说明 | 用途 |
-|------|------|------|
-| PWM 模式 1 | 向上计数时 CNT<CCR 输出有效电平 | 常用 |
-| PWM 模式 2 | 向上计数时 CNT<CCR 输出无效电平 | 互补 |
+TIM 的输出比较模式由 CCMR1/2 寄存器的 OCxM[2:0] 位控制:
 
-通常有效电平 = 高电平(由 CCERP 决定极性)。
+| OCxM | 模式 | 计数器条件 | 输出 |
+|------|------|-----------|------|
+| 000 | 冻结 | — | 无影响 |
+| 001 | 强制有效 | — | 输出高电平 |
+| 010 | 强制无效 | — | 输出低电平 |
+| 011 | 翻转 | CNT=CCR | 翻转输出 |
+| **110** | **PWM 模式1** | CNT<CCR 有效, CNT≥CCR 无效 | 向上计数时前半周期高 |
+| **111** | **PWM 模式2** | CNT<CCR 无效, CNT≥CCR 有效 | 向上计数时前半周期低 |
 
-**PWM 波形生成过程**:
+**PWM 模式 1(向上计数)波形**:
 ```
-CNT 计数: 0__1__2__...__CCR__...__ARR__0__1__...
-输出(模1): 高          低             高
-              ← 占空比 → ← 剩余 →
-              ←—————— 周期(ARR+1) ——————→
+CNT:  0 → 1 → ... → CCR → ... → ARR → 0 → ...
+输出: ████████████████░░░░░░░░░░░░░░░████
+                      ↑CCR
+       ← 占空比(CCR/ARR+1) → ← 剩余 →
+       ←—————— 周期(ARR+1) × CK_CNT ——————→
 ```
 
+### TIM 输出比较寄存器
+
+| 寄存器 | 功能 |
+|--------|------|
+| **CCMR1** | 捕获/比较模式寄存器1(CH1/CH2 模式配置、预装载使能) |
+| **CCMR2** | 捕获/比较模式寄存器2(CH3/CH4 模式配置) |
+| **CCER** | 捕获/比较使能寄存器(CCxE 输出使能、CCxP 输出极性) |
+| **CCR1~4** | 捕获/比较值寄存器(PWM 时存占空比,捕获时存事件发生时的 CNT) |
+
+> **参考**:参考手册 §14(TIM 寄存器描述)
+
+### 影子寄存器
+
+TIM 的 ARR 和 CCRx 都有**影子寄存器**机制:
+- **预装载寄存器**:软件可读写(如 ARR, CCR1)
+- **影子寄存器**:真正驱动硬件的内部寄存器
+- 更新时机:`CR1.ARPE=1` 时,影子寄存器在 UEV 时更新(推荐)
+- `CR1.ARPE=0` 时,预装载值立即传送到影子(可能有毛刺)
+
+> 建议 ARPE=1,在修改 ARR/CCR 时等待下次更新事件后才生效,避免中途改变造成波形异常。
+
 ---
 
 ## 实验:呼吸灯
 
 ### 软件设计(寄存器版)
 
+**项目路径**:`stm32_base/20_tim_led_breathe_register`
+
+**需求**:使用 TIM5_CH1 输出 PWM 到 PA0(LED1),占空比 0~99% 循环渐变。
+
+**硬件电路**:LED1 = PA0,TIM5_CH1 的输出通道可通过重映射连接到 PA0。
+
+**文件:`stm32_base/20_tim_led_breathe_register/Hardware/TIM5/tim5.h`**
+
+```c
+#ifndef __TIM5_H
+#define __TIM5_H
+
+#include "stm32f10x.h"
+
+void TIM5_Init(void);
+void TIM5_Start(void);
+void TIM5_SetDutyCycle(uint8_t duty);  // duty: 0~99
+
+#endif
+```
+
 **文件:`stm32_base/20_tim_led_breathe_register/Hardware/TIM5/tim5.c`**
 
 ```c
 #include "tim5.h"
 
-// TIM5 初始化:配置PWM输出到PA0(LED1)
 void TIM5_Init(void)
 {
-    // 1. 时钟
-    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;   // TIM5时钟
-
-    // 2. 配置PA0为复用功能推挽输出
-    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
-    GPIOA->CRL &= ~GPIO_CRL_CNF0;         // CNF=10(复用推挽)
-    GPIOA->CRL |= GPIO_CRL_CNF0_1;
-    GPIOA->CRL |= GPIO_CRL_MODE0;          // MODE=11(50MHz)
-    // TIM5_CH1 输出到 PA0(通过AFIO重映射)
-
-    // 3. 配置TIM5时基
-    TIM5->PSC = 72 - 1;                    // 72MHz/72 = 1MHz
-    TIM5->ARR = 1000 - 1;                  // 1MHz/1000 = 1KHz PWM
-
-    // 4. 配置输出比较模式
-    TIM5->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;  // PWM模式1
-    TIM5->CCMR1 |= TIM_CCMR1_OC1PE;        // 预装载使能
-    TIM5->CCER |= TIM_CCER_CC1E;           // CH1输出使能
-
-    // 5. 初始占空比0
-    TIM5->CCR1 = 0;
+    /* ========== 1. 开启时钟 ========== */
+    RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;      // TIM5 时钟使能(APB1)
+    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;       // GPIOA 时钟使能
+
+    /* ========== 2. 配置 PA0 为复用功能推挽输出 ========== */
+    // TIM5_CH1 输出到 PA0(需要 AFIO 重映射)
+    // CNF=10(复用推挽), MODE=11(50MHz)
+    GPIOA->CRL &= ~GPIO_CRL_CNF0;             // &=~: CNF0清0
+    GPIOA->CRL |= GPIO_CRL_CNF0_1;            // |=: CNF0[1]=1
+    // CNF[1:0] = 10 → 复用功能推挽
+    GPIOA->CRL |= GPIO_CRL_MODE0;             // |=: MODE0[1:0]=11
+
+    /* ========== 3. 配置 TIM5 时基 ========== */
+    // PWM 频率 = 72MHz / 72 / 1000 = 1KHz
+    TIM5->PSC = 72 - 1;                       // 预分频: 72MHz → 1MHz
+    TIM5->ARR = 1000 - 1;                     // 重装载: 1MHz/1000 = 1KHz
+
+    /* ========== 4. 配置通道1输出比较模式 ========== */
+    // CCMR1 控制 CH1 和 CH2
+    // OC1M[2:0] = 110 → PWM 模式1
+    TIM5->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2;  // 设置 OC1M bit2=1, bit1=1
+    // OC1PE = 1 → 预装载使能(CCR1 修改后等待 UEV 才更新)
+    TIM5->CCMR1 |= TIM_CCMR1_OC1PE;           // |=: OC1PE位置1
+
+    // CCER: CC1E = 1 → CH1 输出使能
+    // CC1P = 0 → 高电平有效(CNT<CCR 输出高)
+    TIM5->CCER |= TIM_CCER_CC1E;              // |=: CC1E位置1
+
+    /* ========== 5. 初始占空比 ========== */
+    TIM5->CCR1 = 0;                           // 初始占空比 0%
+
+    /* ========== 6. 使能自动重装载预装载 ========== */
+    TIM5->CR1 |= TIM_CR1_ARPE;               // |=: ARPE=1
 }
 
+// 启动 TIM5 计数器
 void TIM5_Start(void)
 {
-    TIM5->CR1 |= TIM_CR1_CEN;              // 启动定时器
+    TIM5->CR1 |= TIM_CR1_CEN;                 // |=: CEN=1, 开始计数
 }
 
-// 设置占空比(0~99)
+// 设置占空比
+// duty: 0~99 对应占空比 0%~99%
+// CCR1 = duty × 10(因为 ARR=999, 1% 对应 10 个计数)
 void TIM5_SetDutyCycle(uint8_t duty)
 {
-    TIM5->CCR1 = duty * 10;                // CCR1 = 0~990
+    if (duty > 99) duty = 99;                 // 限制范围
+    TIM5->CCR1 = duty * 10;                  // CCR1 = 0, 10, 20, ..., 990
 }
 ```
 
@@ -99,67 +173,159 @@ void TIM5_SetDutyCycle(uint8_t duty)
 
 ```c
 #include "tim5.h"
+#include "delay.h"
 
 int main(void)
 {
     TIM5_Init();
     TIM5_Start();
 
-    uint8_t duty = 0;
-    uint8_t dir = 0;    // 0=增加, 1=减小
-    uint8_t step = 1;
+    uint8_t duty = 0;     // 当前占空比 (0~99)
+    uint8_t dir  = 0;     // 方向: 0=增加占空比, 1=减小
+    uint8_t step = 1;     // 每次改变量
 
     while (1)
     {
+        // 修改占空比
         if (dir == 0) {
             duty += step;
-            if (duty >= 99) dir = 1;   // 到最大转向减小
+            if (duty >= 99) dir = 1;   // 到 99% 转向减小
         } else {
             duty -= step;
-            if (duty <= 0)  dir = 0;   // 到最小转向增加
+            if (duty <= 0)  dir = 0;   // 到 0% 转向增加
         }
-        TIM5_SetDutyCycle(duty);        // 设置新占空比
-        Delay_ms(10);                   // 每10ms改变一次
+
+        TIM5_SetDutyCycle(duty);        // 更新 CCR1
+        Delay_ms(10);                   // 每 10ms 改变一次,呼吸周期 ≈ 2s
     }
 }
 ```
 
 ---
 
+## 实验:PWM 周期/占空比独立控制
+
+**项目路径**:`stm32_base/22_tim_pwm_cycle_register`(周期控制)
+**项目路径**:`stm32_base/24_tim_pwm_duty_register`(占空比控制)
+
+```c
+// 周期控制:固定占空比 50%,改变频率
+// 通过修改 PSC 或 ARR 改变频率
+void PWM_SetFreq(uint16_t freq)
+{
+    // Freq = 72MHz / (PSC+1) / (ARR+1)
+    // 固定 PSC=72-1, 调整 ARR
+    TIM5->ARR = (72000000 / 72 / freq) - 1;
+    // 保持占空比 50%: CCR1 = ARR/2
+    TIM5->CCR1 = TIM5->ARR / 2;
+}
+
+// 占空比控制:固定频率 1KHz,改变占空比
+void PWM_SetDuty(uint8_t percent)
+{
+    // ARR 固定 999, 改变 CCR1
+    TIM5->CCR1 = (uint16_t)((uint32_t)999 * percent / 100);
+}
+```
+
+---
+
 ## 实验:输入捕获测频率
 
-TIM 的输入捕获可以测量外部信号的脉宽或频率。
+输入捕获可测量外部信号的频率或脉宽。**原理**:记录两次边沿事件发生时的 CNT 值,差值 = 脉冲宽度或周期。
+
+### 配置流程
 
-**配置流程**:
 ```
-GPIO输入 → TIM_CHx引脚 → 输入滤波器 → 边沿检测 → 捕获CCR
+1. GPIO 配置为输入(浮空/上拉)
+2. TIM 启用输入捕获模式(CCMRx.ICx=1)
+3. 配置捕获边沿(CCER.CCxP 上升/下降沿)
+4. 使能捕获中断(DIER.CCxIE)
+5. 在 ISR 中读取 CCRx,计算差值
 ```
 
-**测频率原理**:
-1. 配置 CHx 捕获上升沿
-2. 第一次捕获到上升沿 → 记录 CCR1 值
-3. 第二次捕获到上升沿 → 记录 CCR2 值
-4. 频率 = CK_CNT / (CCR2 - CCR1)
+```c
+// 初始化 TIM2_CH1(PA0) 为输入捕获模式
+void TIM2_Capture_Init(void)
+{
+    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
+    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
+
+    // PA0 浮空输入
+    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
+
+    // TIM2_CH1 映射到 PA0(默认)
+    TIM2->PSC = 72 - 1;             // CK_CNT = 1MHz (分辨率 1us)
+    TIM2->ARR = 0xFFFF;             // 最大 16 位
 
-> **参考**:参考手册 §14.3(输入捕获模式)
+    // CCMR1: CH1 输入捕获模式(IC1=1 映射到 TI1)
+    TIM2->CCMR1 |= TIM_CCMR1_CC1S_0;  // CC1S=01: CH1→TI1
+
+    // 捕获边沿: 上升沿
+    TIM2->CCER &= ~TIM_CCER_CC1P;      // CC1P=0: 上升沿
+
+    // 使能捕获中断
+    TIM2->DIER |= TIM_DIER_CC1IE;
+
+    // NVIC
+    NVIC_EnableIRQ(TIM2_IRQn);
+
+    // 启动
+    TIM2->CR1 |= TIM_CR1_CEN;
+}
+
+// 捕获中断:记录两次上升沿的 CNT 差值
+volatile uint16_t cap1, cap2;
+volatile uint8_t cap_count = 0;
+
+void TIM2_IRQHandler(void)
+{
+    if (TIM2->SR & TIM_SR_CC1IF)      // 捕获事件发生
+    {
+        if (cap_count == 0) {
+            cap1 = TIM2->CCR1;         // 第一次捕获值
+            cap_count = 1;
+        } else {
+            cap2 = TIM2->CCR1;         // 第二次捕获值
+            cap_count = 2;
+
+            // 频率 = CK_CNT / (cap2 - cap1)
+            // = 1MHz / 差值
+            uint16_t diff = (cap2 > cap1) ? (cap2 - cap1) : (0xFFFF - cap1 + cap2);
+            uint32_t freq = 1000000 / diff;
+        }
+        TIM2->SR &= ~TIM_SR_CC1IF;     // 清标志
+    }
+}
+```
 
 ---
 
 ## 核心速查表
 
-| 功能 | 配置方法 |
-|------|---------|
-| PWM模式1 | `CCMR1.OC1M = 110`(PWM模式1) |
-| PWM使能输出 | `CCER.CC1E = 1` |
-| 预装载使能 | `CCMR1.OC1PE = 1` |
-| ARPE使能 | `CR1.ARPE = 1`(ARR影子寄存器) |
-| 极性反转 | `CCER.CC1P = 1`(低电平有效) |
-| 更新占空比 | 写 `CCR1` 值(0~ARR) |
+| 操作 | 代码 |
+|------|------|
+| PWM 模式1(CH1) | `CCMR1 |= OC1M_1 + OC1M_2`(=0x60) |
+| PWM 输出使能(CH1) | `CCER |= CC1E` |
+| 设置极性(高有效) | `CCER &= ~CC1P` |
+| 预装载使能 | `CCMR1 |= OC1PE` |
+| 修改占空比 | `CCR1 = value`(0~ARR) |
+| 修改频率(改ARR) | `ARR = new_value`(CR1.ARPE=1) |
+| 输入捕获模式 | `CCMR1 |= CC1S_0` |
+| 捕获使能 | `CCER &= ~CC1P`(上升沿)+ `CCER |= CC1E` |
+
+| 呼吸灯参数 | 频率 | 占空比范围 | 渐变步长 |
+|-----------|------|-----------|---------|
+| PWM 频率 | 1KHz | 0~99% | 1% |
+| 渐变间隔 | 10ms | — | — |
+| 呼吸周期 | ~2s | — | — |
 
 ## 常见问题与避坑
 
-1. **PWM 无输出** → 检查 GPIO 模式(复用推挽)、TIM CH 输出是否使能(CCER.CCxE)
-2. **占空比 0 时仍有微弱电平** → 检查 CCR1=0 是否真为低电平(取决于极性配置)
-3. **PWM 频率不对** → APB1 定时器时钟 = APB1×2(如果 APB1 预分频≠1)
-4. **输入捕获值不准确** → 检查输入滤波器和边沿极性配置
-5. **CCR 更新不及时** → 检查 ARPE 影子寄存器是否使能(建议使能,防止中途改变ARR/CCR引起波形异常)
+1. **PWM 无输出** → 检查 GPIO 模式(必须**复用推挽** CNF=10,不是通用推挽)、CCER.CCxE 是否使能
+2. **占空比 0% 时引脚仍有电平** → 检查 CCER.CCxP 极性配置、CH 是否确实连接到了目标引脚
+3. **PWM 频率与预期不符** → 确认 APB1 定时器时钟公式:如果 APB1 预分频≠1 则 TIM 时钟 = APB1×2
+4. **修改 CCR 后占空比不立即变化** → ARPE=1 时需等到下次 UEV;如需立即更新可先清除 ARPE
+5. **呼吸灯非预期渐变** → 确保 PWM 频率 > 100Hz(否则人眼能看见闪烁),推荐 1KHz 以上
+6. **输入捕获值不准** → 捕获事件太频繁(计数溢出),增大 PSC 降低 CK_CNT 分辨率换范围
+7. **输入捕获只进一次中断** → CNT 计数溢出(ARR=0xFFFF 最大),需添加溢出中断 UIF 计数

+ 295 - 75
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/10-DMA数据传输.md

@@ -9,43 +9,107 @@ created: 2026-07-15
 
 # DMA数据传输
 
-> **用生活理解**:DMA 就像公司里的快递员——以前你要自己去取文件(CPU 轮询/中断读外设),现在快递员帮你送了(DMA 控制器自动搬运),你就可以专心做其他重要工作。快递送到后喊你一声(完成中断)即可
+> **用生活理解**:DMA 就像公司里的快递员——原来你要自己去取文件(CPU 轮询/中断读外设),DMA 就是专门配了个快递员自动帮你搬数据,搬完了喊你一声(完成中断),你就可以专心做核心工作。CPU 只负责"下单"(配置 DMA 参数),不负责"搬货"
 
 ---
 
 ## DMA 概述
 
-DMA(Direct Memory Access)允许外设与存储器之间直接传输数据,**不需要 CPU 参与**。
+DMA = Direct Memory Access(直接存储器访问),允许外设和存储器之间直接传输数据,**传输过程不需要 CPU 参与**。
 
-**传统方式(无 DMA)**:
+### 传统方式 vs DMA 方式
+
+**轮询方式**(无 DMA):
+```c
+for (i = 0; i < 100; i++) {
+    while (!(USART1->SR & USART_SR_RXNE));  // CPU 一直等
+    buffer[i] = USART1->DR;                  // CPU 亲自搬
+}
+// CPU 在此过程中无法做其他任何事
 ```
-CPU 读取外设数据寄存器 → CPU 写入内存 → CPU 继续下一字节
+
+**中断方式**:
+```c
+// CPU 每次只搬 1 字节(进一次中断),搬 100 次要进 100 次中断
 ```
 
 **DMA 方式**:
+```c
+// CPU 配置好 DMA(源地址+目的地址+长度)后,去干别的事
+// DMA 控制器自动搬完 100 字节后产生中断通知 CPU
+// 100 次传输只进 1 次中断
 ```
-CPU 配置 DMA 通道后 → DMA 控制器自动搬运 → 搬运完成通知 CPU
-```
 
-### STM32F103 DMA 特性
+### STM32F103 DMA 控制器
+
+| 控制器 | 通道数 | 可访问的存储器和外设 |
+|--------|--------|-------------------|
+| **DMA1** | 7 通道 | SRAM、APB1/APB2 外设 |
+| **DMA2** | 5 通道 | SRAM、APB1/APB2 外设、FSMC(仅 F103ZET6 等大容量型号有) |
+
+### DMA 通道映射表(DMA1)
+
+| 通道 | 外设请求 |
+|------|---------|
+| CH1 | **ADC1**、TIM2_CH3、TIM4_CH1 |
+| CH2 | **USART1_TX**、SPI1_RX、TIM1_CH1 |
+| CH3 | **USART1_RX**、SPI1_TX、TIM1_CH2 |
+| CH4 | SPI2_RX、SPI1_RX、**USART1_TX** |
+| CH5 | SPI2_TX、**USART2_TX**、TIM1_CH4 |
+| CH6 | **USART2_RX**、ADC3、TIM1_CH1 |
+| CH7 | **USART3_TX**、ADC3、TIM1_CH2 |
+
+> **注意**:同一时刻一个 DMA 通道只能服务一个外设。需要查参考手册 §10 确认当前通道哪个外设请求有效。
 
-- 2 个 DMA 控制器:DMA1(7 通道)、DMA2(5 通道)
-- 支持:外设到内存、内存到外设、内存到内存
-- 存储器增量模式(地址自动递增)
-- 循环模式(自动重传)
-- 优先级可配(4 级:低/中/高/最高)
+### DMA 传输模式
 
-### 通道映射
+| 模式 | 说明 | 适用场景 |
+|------|------|---------|
+| **内存→内存** | 源地址和目的地址都是内存 | Flash→SRAM 数据搬移 |
+| **内存→外设** | 源是内存,目标是外设数据寄存器 | 向串口发送一批数据 |
+| **外设→内存** | 源是外设数据寄存器,目标是内存 | 从 ADC/串口接收一批数据 |
 
-| DMA1 通道 | 外设请求 |
-|-----------|---------|
-| CH1 | ADC1、TIM2_CH3、TIM4_CH1 |
-| CH2 | USART1_TX、SPI1_RX、TIM1_CH1 |
-| CH3 | USART1_RX、SPI1_TX、TIM1_CH2 |
-| CH4 | SPI2_RX、SPI1_RX、USART1_TX |
-| CH5 | SPI2_TX、USART2_TX、TIM1_CH4 |
-| CH6 | USART2_RX、ADC3、TIM1_CH1 |
-| CH7 | USART3_TX、ADC3、TIM1_CH2 |
+---
+
+## DMA 关键寄存器
+
+| 寄存器 | 功能 | 重要位域 |
+|--------|------|---------|
+| **CCR** | 通道配置寄存器 | EN(使能)、DIR(方向)、PL[1:0](优先级)、MSIZE[1:0](存储单元宽度)、PSIZE[1:0](外设宽度)、MINC(存储地址增量)、PINC(外设地址增量)、CIRC(循环模式)、MEM2MEM(内存到内存) |
+| **CNDTR** | 传输数量寄存器 | 16 位(最大 65535),每次传输后自减 |
+| **CPAR** | 外设地址寄存器 | 外设数据寄存器的地址(如 USART1->DR = 0x40013804) |
+| **CMAR** | 存储器地址寄存器 | 内存缓冲区的地址 |
+| **ISR** | 中断状态寄存器 | TCIFx(通道x传输完成)、HTIFx(通道x半传输)、TEIFx(通道x传输错误) |
+| **IFCR** | 中断标志清除寄存器 | CTCIFx(写1清完成)、CHTIFx(写1清半传) |
+
+> **参考**:参考手册 §10(DMA 寄存器描述)完整位域表
+
+### CCR 关键位详解
+
+| 位 | 名称 | 值 | 说明 |
+|----|------|-----|------|
+| 0 | EN | 1 | 通道使能(配置完所有参数后再置 1) |
+| 4 | DIR | 0=外设→内存, 1=内存→外设 | M2M=1 时此位忽略 |
+| 6:5 | MSIZE | 00=8位, 01=16位, 10=32位 | 存储器数据宽度 |
+| 7:6 | PSIZE | 00=8位, 01=16位, 10=32位 | 外设数据宽度(必须与外设寄存器宽度匹配) |
+| 9 | MINC | 1 | 存储器地址自动递增(增量=MSIZE 字节) |
+| 8 | PINC | 1 | 外设地址自动递增 |
+| 10 | CIRC | 1 | 循环模式(CNDTR 到 0 后自动重装,持续传输) |
+| 14 | MEM2MEM | 1 | 内存到内存模式(不需要外设请求,配置完即启动) |
+
+---
+
+## DMA 传输流程
+
+```
+1. 开启 DMA 时钟(AHBENR 寄存器)
+2. 设置 CPAR(外设地址)和 CMAR(内存地址)
+3. 设置 CNDTR(传输次数)
+4. 配置 CCR(优先级、数据宽度、增量模式、传输方向)
+5. 使能通道(CCR.EN = 1)
+6. DMA 自动开始传输(外设请求触发或 MEM2MEM 立即启动)
+7. 传输完成 → CNDTR=0 → 产生 TC 中断(如果使能)或 CIRC 重装
+```
 
 ---
 
@@ -53,78 +117,234 @@ CPU 配置 DMA 通道后 → DMA 控制器自动搬运 → 搬运完成通知 CP
 
 ### 软件设计(寄存器版)
 
+**项目路径**:`stm32_base/28_dma_mem2mem_register`
+
+**需求**:将 SRAM 中源数组的数据通过 DMA 复制到目的数组,完成后翻转 LED。
+
+**文件:`stm32_base/28_dma_mem2mem_register/Hardware/DMA/dma.h`**
+
+```c
+#ifndef __DMA_H
+#define __DMA_H
+
+#include "stm32f10x.h"
+
+void DMA_Init(void);
+void DMA_Start(void);
+uint8_t DMA_IsComplete(void);
+
+#endif
+```
+
 **文件:`stm32_base/28_dma_mem2mem_register/Hardware/DMA/dma.c`**
 
 ```c
 #include "dma.h"
 
-#define SRC_ADDR  ((uint32_t)0x20000000)   // 源地址(SRAM)
-#define DST_ADDR  ((uint32_t)0x20000010)   // 目的地址(SRAM偏移)
+// 定义源和目的缓冲区(放在 SRAM 中)
+#define BUFFER_SIZE  10
+uint32_t src_buffer[BUFFER_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+uint32_t dst_buffer[BUFFER_SIZE] = {0};
 
 void DMA_Init(void)
 {
-    // 1. 开启DMA1时钟
-    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
+    // 1. 开启 DMA1 时钟
+    // DMA 挂载在 AHB 总线上,时钟由 AHBENR 控制
+    RCC->AHBENR |= RCC_AHBENR_DMA1EN;       // |=:DMA1时钟使能
 
-    // 2. 清除通道1的所有标志
-    DMA1->IFCR |= DMA_IFCR_CGIF1;           // |= 写1清除所有标志
-
-    // 3. 配置DMA通道1
-    DMA1_Channel1->CPAR  = SRC_ADDR;         // 外设地址 = 源地址
-    DMA1_Channel1->CMAR  = DST_ADDR;         // 存储器地址 = 目的地址
-    DMA1_Channel1->CNDTR = 10;               // 传输次数 = 10(10个32位字)
-
-    // 4. 配置控制寄存器CCR
-    DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;  // MEM2MEM=1: 内存到内存模式
-    DMA1_Channel1->CCR |= DMA_CCR1_PL;        // PL[1:0]=11: 最高优先级
-    DMA1_Channel1->CCR |= DMA_CCR1_MSIZE;     // MSIZE=10: 存储器数据宽度32位
-    DMA1_Channel1->CCR |= DMA_CCR1_PSIZE;     // PSIZE=10: 外设数据宽度32位
-    DMA1_Channel1->CCR |= DMA_CCR1_MINC;      // MINC=1: 存储器地址递增
-    DMA1_Channel1->CCR |= DMA_CCR1_PINC;      // PINC=1: 外设地址递增
-    DMA1_Channel1->CCR |= DMA_CCR1_CIRC;      // CIRC=1: 循环模式
-    DMA1_Channel1->CCR |= DMA_CCR1_TCIE;      // TCIE=1: 传输完成中断使能
-
-    // 5. 使能通道
-    DMA1_Channel1->CCR |= DMA_CCR1_EN;
-
-    // 6. NVIC使能DMA中断
-    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
+    // 2. 清除通道1的所有中断标志
+    // IFCR 写 1 清除对应标志位
+    // CGIF1 = 清除通道1全局标志(包含 TCIF/HTIF/TEIF)
+    DMA1->IFCR |= DMA_IFCR_CGIF1;            // |=:写1清除所有标志
+
+    // 3. 配置 DMA1 通道1
+    // 内存到内存传输:从 src_buffer 复制到 dst_buffer
+    // 注意:必须先配置 CPAR/CMAR/CNDTR,再配 CCR
+
+    // 外设地址寄存器 ← 源地址(在内存到内存模式下,"外设"端就是源)
+    DMA1_Channel1->CPAR = (uint32_t)src_buffer;
+
+    // 存储器地址寄存器 ← 目的地址
+    DMA1_Channel1->CMAR = (uint32_t)dst_buffer;
+
+    // 传输数量寄存器 ← 传输次数(一次传输的宽度由 MSIZE 决定)
+    // CNDTR 是 16 位,最大 65535
+    DMA1_Channel1->CNDTR = BUFFER_SIZE;
+
+    // 4. 配置 CCR
+    // MEM2MEM=1: 内存到内存模式(不需要外设请求信号,配置完立即开始)
+    // PL[1:0]=11: 最高优先级
+    // MSIZE[1:0]=10: 存储器数据宽度 = 32 位
+    // PSIZE[1:0]=10: 外设数据宽度 = 32 位
+    // MINC=1: 存储器地址递增(每传一次 +4 字节)
+    // PINC=1: 外设地址递增(每传一次 +4 字节)
+    // CIRC=1: 循环模式(传输完成后自动重新开始,适用于持续采集)
+    // TCIE=1: 传输完成中断使能
+    DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM   // MEM2MEM 位置 1
+                       | DMA_CCR1_PL          // 优先级 11
+                       | DMA_CCR1_MSIZE       // MSIZE = 32位
+                       | DMA_CCR1_PSIZE       // PSIZE = 32位
+                       | DMA_CCR1_MINC        // 存储器地址递增
+                       | DMA_CCR1_PINC        // 外设地址递增
+                       | DMA_CCR1_CIRC        // 循环模式
+                       | DMA_CCR1_TCIE;       // 传输完成中断使能
+
+    // 5. 使能通道1
+    // EN=1 表示通道启动。配置完所有参数后最后才置 EN
+    // 对于 MEM2MEM 模式,EN=1 后 DMA 立即开始传输
+    DMA1_Channel1->CCR |= DMA_CCR1_EN;        // |=: EN位置1
+}
+
+// 检查传输是否完成(查询方式)
+uint8_t DMA_IsComplete(void)
+{
+    // ISR.TCIF1 = 1 表示通道1传输完成
+    if (DMA1->ISR & DMA_ISR_TCIF1)    // &测试: TCIF1位=1?
+    {
+        DMA1->IFCR |= DMA_IFCR_CTCIF1; // 清除完成标志(写1)
+        return 1;
+    }
+    return 0;
 }
+```
+
+**文件:`stm32_base/28_dma_mem2mem_register/User/main.c`**
+
+```c
+#include "dma.h"
+#include "led.h"
 
-// DMA传输完成中断
-void DMA1_Channel1_IRQHandler(void)
+int main(void)
 {
-    if (DMA1->ISR & DMA_ISR_TCIF1)           // 检查传输完成标志
+    LED_Init();
+    DMA_Init();   // DMA 配置完立即开始传输
+
+    while (1)
     {
-        DMA1->IFCR |= DMA_IFCR_CTCIF1;       // 清除完成标志
-        // 传输完成:目的地址已包含源数据的副本
+        if (DMA_IsComplete())
+        {
+            // 传输完成:dst_buffer 数组已包含 src_buffer 的副本
+            // 验证第一个元素是否复制正确
+            if (dst_buffer[0] == 0x00) {
+                LED_On(LED_1);     // 正确则点亮 LED1
+            }
+        }
     }
 }
 ```
 
 ---
 
-## 核心速查表
-
-| DMA寄存器 | 功能 |
-|-----------|------|
-| CCR.EN | 通道使能 |
-| CCR.PL[1:0] | 通道优先级 |
-| CCR.MSIZE[1:0] / PSIZE[1:0] | 数据宽度(8/16/32位) |
-| CCR.MINC / PINC | 地址递增使能 |
-| CCR.CIRC | 循环模式 |
-| CCR.DIR | 传输方向(外设→内存 / 内存→外设) |
-| CCR.MEM2MEM | 内存到内存模式(无视外设请求) |
-| CNDTR | 传输次数(每次传输后自减) |
-| CPAR | 外设地址 |
-| CMAR | 存储器地址 |
-| ISR | 中断状态寄存器(TCIF/HTIF/TEIF等) |
-| IFCR | 中断标志清除寄存器(写1清除) |
+## 实验:DMA + USART 自动发送
+
+### 软件设计(寄存器版 — 内存到外设)
+
+**项目路径**:`stm32_base/30_dma_mem2usart_register`
+
+```c
+// 通过 DMA 自动将缓冲区数据发送到 USART1
+// CPU 仅需配置一次,DMA 自动搬完所有数据
+
+#define TX_BUFFER_SIZE  100
+uint8_t tx_buffer[TX_BUFFER_SIZE] = "Hello from DMA! This is sent without CPU intervention!\r\n";
+
+void DMA_USART_Init(void)
+{
+    // 1. DMA 时钟
+    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
+
+    // 2. 配置 DMA 通道2(USART1_TX 使用 DMA1 通道2)
+    // 外设地址 = USART1 的数据寄存器地址(0x40013804)
+    DMA1_Channel2->CPAR = (uint32_t)&(USART1->DR);
+    // 存储器地址 = 待发送的缓冲区
+    DMA1_Channel2->CMAR = (uint32_t)tx_buffer;
+    // 传输次数 = 缓冲区大小
+    DMA1_Channel2->CNDTR = TX_BUFFER_SIZE;
+
+    // 3. 配置 CCR
+    // DIR = 1: 内存→外设方向
+    // MSIZE=8位, PSIZE=8位(USART1->DR 是 8 位寄存器)
+    // MINC=1: 地址递增(从 buffer 中逐字节取数据)
+    // PINC=0: 外设地址不递增(始终写入同一个 DR 寄存器)
+    DMA1_Channel2->CCR |= DMA_CCR2_DIR;      // DIR=1: 内存→外设
+    DMA1_Channel2->CCR |= DMA_CCR2_MINC;     // MINC: 内存地址递增
+    DMA1_Channel2->CCR |= DMA_CCR2_TCIE;     // TCIE: 完成后中断
+
+    // 4. 注意:此时不使能 DMA 通道
+    // DMA 传输由 USART 的发送请求触发
+    // USART 配置中使能 DMA 发送(USART_CR3_DMAT)后,DMA 才开始工作
+}
+
+void USART_Init_With_DMA(void)
+{
+    // USART 常规初始化 ...(同之前的 USART 配置)
+
+    // 关键是 USART CR3 寄存器的 DMAT 位
+    // DMAT=1: 使能 USART 发送的 DMA 请求
+    // 当 USART 的 TXE 置 1 时,自动向 DMA 发送请求
+    // DMA 收到请求后,从内存搬 1 字节到 USART->DR
+    USART1->CR3 |= USART_CR3_DMAT;           // |=: DMAT位置1
+
+    // 使能 DMA 通道(必须在 USART 使能 DMA 之后)
+    DMA1_Channel2->CCR |= DMA_CCR2_EN;       // |=: EN位置1
+    // 此后,DMA 自动将 tx_buffer 中的数据逐字节发送到串口
+}
+```
+
+---
+
+## HAL 库版 DMA 传输
+
+HAL 库提供了 `HAL_UART_Transmit_DMA()` 函数,封装了上述所有寄存器操作:
+
+```c
+// HAL 库 DMA 发送
+HAL_UART_Transmit_DMA(&huart1, tx_buffer, size);
+// 参数: uart句柄, 数据指针, 数据长度
+// 异步: 函数立即返回,传输在后台进行
+// 完成时回调: HAL_UART_TxCpltCallback(&huart1)
+
+// HAL 库 DMA 接收
+HAL_UART_Receive_DMA(&huart1, rx_buffer, size);
+// 完成时回调: HAL_UART_RxCpltCallback(&huart1)
+```
+
+---
+
+## DMA 中断优先级注意事项
+
+DMA 中断默认优先级较低,但具有硬件优先级:
+
+| DMA 优先级设置(CCR.PL) | 说明 |
+|------------------------|------|
+| 00 (最低) | 次于所有外设中断 |
+| 01 (低) | 与低优先级外设中断同级 |
+| 10 (高) | 与高优先级外设中断同级 |
+| 11 (最高) | 优于外设中断 |
+
+当多个 DMA 通道同时请求时,优先级高的先执行;同级时通道号小的优先。
+
+---
+
+## 核心函数速查表
+
+| 寄存器操作 | 代码 | 说明 |
+|-----------|------|------|
+| 时钟使能 | `RCC->AHBENR |= RCC_AHBENR_DMA1EN` | 开启 DMA1 时钟 |
+| 清除标志 | `DMA1->IFCR |= DMA_IFCR_CGIF1` | 清除通道1所有标志 |
+| 设外设地址 | `DMA1_Channel1->CPAR = (uint32_t)&USART1->DR` | 外设数据寄存器地址 |
+| 设内存地址 | `DMA1_Channel1->CMAR = (uint32_t)buffer` | 内存缓冲区地址 |
+| 设传输次数 | `DMA1_Channel1->CNDTR = size` | 16 位,传输次数 |
+| 使能通道 | `DMA1_Channel1->CCR |= DMA_CCR1_EN` | 最后一步才置 EN |
+| 查询完成 | `DMA1->ISR & DMA_ISR_TCIF1` | TCIF=1 表示完成 |
+| 清完成标志 | `DMA1->IFCR |= DMA_IFCR_CTCIF1` | 写 1 清除 |
 
 ## 常见问题与避坑
 
-1. **DMA 传输没开始** → 检查 CCR.EN 是否使能、CNDTR 是否设置、外设请求是否使能
-2. **传输完没有中断** → 检查 CCR.TCIE
-3. **CNDTR 一直为 0** → 传输完成后的值;循环模式下复位后重新装载
-4. **DMA 与外设配合错误** → 注意 MSIZE/PSIZE 要与外设寄存器宽度匹配
-5. **内存到内存模式启动失败** → 先配置好所有寄存器再 EN=1;注意通道启动后 CPAR/CMAR 不可修改
+1. **DMA 传输没开始** → 检查 CCR.EN 是否使能、CNDTR≠0、外设是否已发出 DMA 请求(如 USART 的 CR3.DMAT=1)
+2. **传输数据错了** → 检查 MSIZE/PSIZE 是否与外设匹配(8/16/32 位)、地址增量方向是否正确
+3. **中断只进一次** → 检查 ISR.GIF 是否清除、NVIC 是否使能、CCR.TCIE 是否置 1
+4. **DMA 和 Cache 一致性** → STM32F103 无 Cache,不必担心(Cortex-M7 需注意)
+5. **CNDTR 初始值 = 最大 65535** → 如果传输超过 65535 次,需分段传输或使用循环模式
+6. **MEM2MEM 传输方向** → "外设地址寄存器"放源地址,"存储器地址寄存器"放目的地址(命名反直觉但实际如此)
+7. **DMA 通道冲突** → 同一通道同一时间只能服务一个外设请求

+ 318 - 78
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/11-ADC模数转换.md

@@ -9,137 +9,377 @@ created: 2026-07-15
 
 # ADC模数转换
 
-> **用生活理解**:ADC 就像一把电压尺子——输入一个模拟电压(0~3.3V),输出一个数字(0~4095,12位精度)。就像用尺子量高度,尺子的刻度数决定了精度(12位=4096个刻度)
+> **用生活理解**:ADC 就像一把精密的电压尺子——输入 0~3.3V 的模拟电压,输出 0~4095 的数字(12 位)。就像用一把刻有 4096 个刻度的尺子量高度,输入 1.65V 时输出 2048(中间值)。采样时间就是"尺子读数需要的时间",采样时间越长读数越准。规则组和注入组就像"普通队列"和"VIP 通道"——VIP 可以插队
 
 ---
 
 ## ADC 概述
 
-ADC = Analog-to-Digital Converter(模数转换器)。
+ADC = Analog-to-Digital Converter(模数转换器),将连续的模拟信号转换为离散的数字信号
 
-STM32F103ZET6 有 **3 个 ADC**(ADC1/2/3),各有
+STM32F103ZET6 有 **3 个 ADC 模块**(ADC1/ADC2/ADC3):
 
 | 特性 | 值 |
 |------|-----|
-| 分辨率 | 12 位(输出 0~4095) |
-| 通道数 | 最多 18 个(16 外部 + 2 内部) |
-| 转换模式 | 单次/连续/扫描/间断 |
-| 触发方式 | 软件触发 / 定时器触发 / 外部引脚触发 |
-| ADC 时钟 | 来自 APB2,最大 14MHz |
-| 采样时间 | 可编程 1.5~239.5 个周期 |
+| 分辨率 | **12 位**(输出范围 0~4095) |
+| 通道数 | 最多 18 个通道(16 个外部引脚 + 2 个内部) |
+| 转换时间 | 最快 1.17μs(14MHz 时钟下) |
+| 输入电压 | 0 ~ VREF+(通常 = VDDA = 3.3V) |
+| ADC 时钟 | 来自 APB2,经预分频器,**最大 14MHz** |
 
-### SAR ADC 原理
+### 内部通道
 
-STM32F103 使用**逐次逼近型**(SAR, Successive Approximation Register)ADC:
+| 通道 | 连接 | 说明 |
+|------|------|------|
+| 通道16 | 内部温度传感器 | 测量芯片内部温度 |
+| 通道17 | VREFINT | 内部参考电压(1.2V) |
+
+---
+
+## SAR ADC 工作原理
+
+STM32F103 使用**逐次逼近型**(SAR)ADC:
 
 ```
-1. 采样:对输入电压进行采样保持
-2. 比较:从最高位(bit 11)开始,逐位比较
-   第1步:VIN > VREF/2 ? → bit11=1 : bit11=0
-   第2步:VIN > 调整后的参考值 ? → bit10=1 : bit10=0
-   ... 共12步 → 得到12位结果
-3. 转换时间 = 采样时间 + 12.5 个 ADC 时钟周期
+输入电压 VIN = 1.8V, VREF = 3.3V
+
+第1步: VIN > 3.3V/2=1.65V ? → YES → Bit11=1, 余量 = 1.8-1.65 = 0.15V
+第2步: VIN > 1.65+1.65/2=2.475V ? → NO → Bit10=0
+第3步: VIN > 1.65+0.825/2=2.0625V ? → NO → Bit9=0
+第4步: VIN > 1.65+0.4125/2=1.85625V ? → NO → Bit8=0
+第5步: VIN > 1.65+0.20625/2=1.753125V ? → YES → Bit7=1
+...
+
+最终: 12 次比较后得到 12 位结果
+数字值 = VIN / VREF × 4095 = 1.8 / 3.3 × 4095 ≈ 2234
 ```
 
-### 规则组与注入组
+**特点**:
+- 优点:转换时间确定(固定周期数),无延迟
+- 缺点:对输入噪声敏感(需要适当的采样时间)
+- 转换时间 = **采样时间 + 12.5 个 ADC 时钟周期**
+
+---
+
+## 关键概念
+
+### 规则组 vs 注入组
+
+ADC 有两种转换序列:
+
+| 组 | 最大通道数 | 特点 | 用途 |
+|----|-----------|------|------|
+| **规则组** | 16 通道 | 常规采集,按顺序转换 | 周期性采集传感器数据 |
+| **注入组** | 4 通道 | **可打断规则组**的转换 | 紧急事件触发采集 |
 
-| 组 | 说明 | 通道数 | 用途 |
-|----|------|--------|------|
-| **规则组** | 常规转换序列 | 最多16通道 | 主要数据采集 |
-| **注入组** | 可打断规则组的"高优先级"转换 | 最多4通道 | 紧急数据采集 |
+**类比**:规则组是普通队列(一个个来),注入组是 VIP 通道——来了就插队。
 
-### ADC 关键寄存器
+### 转换模式
 
-| 寄存器 | 功能 |
+| 模式 | 说明 | 适用场景 |
+|------|------|---------|
+| **单次转换** | 每触发一次转换一个通道 | 按键触发采集 |
+| **连续转换** | 转换完自动开始下一次 | 持续监测电压 |
+| **扫描模式** | 按序列依次转换所有通道 | 多通道循环采集 |
+| **间断模式** | 每触发一次转换一部分通道 | 超多通道分批次 |
+
+### 触发方式
+
+| 触发源 | 说明 |
 |--------|------|
-| SR | 状态(EOC转换结束、STRT开始) |
-| CR1 | 控制(SCAN扫描模式、RES分辨率) |
-| CR2 | 控制(ADON开启、CONT连续、EXTTRIG外部触发) |
-| SMPR1/2 | 采样时间(每个通道单独配置) |
-| SQR1~3 | 规则序列(通道顺序与数量) |
-| DR | 转换结果(12位右对齐) |
+| **软件触发** | `CR2.SWSTART=1` 启动转换 |
+| **定时器触发** | TIM1/2/3/4 的 TRGO 事件 |
+| **外部引脚触发** | EXTI 线上升沿/下降沿 |
+
+### ADC 时钟配置
+
+```
+ADC_CLK = APB2_CLK / ADCPRE[1:0]
+
+ADCPRE 分频系数:
+  00: /2 (36MHz)   → 超限! ADC_CLK > 14MHz
+  01: /4 (18MHz)   → 超限!
+  10: /6 (12MHz)   → ✅ 推荐(12MHz < 14MHz)
+  11: /8 (9MHz)    → ✅ 较慢但更准
+```
 
 > **参考**:参考手册 §11(ADC 寄存器描述)
+> **APB2 时钟 = 72MHz,ADCPRE=Div6 → ADC_CLK=12MHz 是最均衡选择**
+
+### 采样时间
+
+每个通道可独立配置采样时间(SMPR1/SMPR2 寄存器):
+
+| SMP[2:0] | 采样周期数 | 总转换时间(12MHz ADC_CLK) |
+|----------|-----------|---------------------------|
+| 000 | 1.5 | (1.5+12.5) / 12MHz = 1.17μs |
+| 001 | 7.5 | (7.5+12.5) / 12MHz = 1.67μs |
+| 010 | 13.5 | 2.17μs |
+| 011 | 28.5 | 3.42μs |
+| 100 | 41.5 | 4.50μs |
+| 101 | 55.5 | 5.67μs |
+| 110 | 71.5 | 7.00μs |
+| **111** | **239.5** | **21.00μs(最准)** |
+
+**采样时间选择原则**:
+- 低阻抗信号源(<10KΩ):短采样时间(1.5~7.5 周期)
+- 高阻抗信号源(>10KΩ):长采样时间(28.5~239.5 周期)
+- 追求速度选短时间,追求精度选长时间
 
 ---
 
-## 实验:单通道 ADC 采集
+## ADC 关键寄存器
 
-### 软件设计(寄存器版 — 单次采集)
+| 寄存器 | 地址偏移 | 功能 |
+|--------|---------|------|
+| **SR** | 0x00 | 状态寄存器(EOC=转换结束,STRT=开始) |
+| **CR1** | 0x04 | 控制1(SCAN=扫描,JAUTO=自动注入,RES=分辨率) |
+| **CR2** | 0x08 | 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐) |
+| **SMPR1** | 0x0C | 采样时间寄存器1(通道10~17) |
+| **SMPR2** | 0x10 | 采样时间寄存器2(通道0~9) |
+| **SQR1** | 0x2C | 规则序列寄存器1(L=序列长度,SQ13~16) |
+| **SQR2** | 0x30 | 规则序列寄存器2(SQ7~12) |
+| **SQR3** | 0x34 | 规则序列寄存器3(SQ1~6) |
+| **DR** | 0x4C | 规则组数据寄存器(12 位结果,右/左对齐) |
+
+> **参考**:参考手册 §11 完整寄存器位域定义
+
+---
+
+## ADC 校准
+
+ADC 上电后需要校准以提高精度:
 
 ```c
-// ADC1 配置:通道0(PA0),软件触发,单次转换
-void ADC1_Init(void)
+// 校准流程
+ADC1->CR2 |= ADC_CR2_ADON;           // 开启 ADC
+// 等待稳定(约 2 个 ADC 时钟)
+Delay_us(1);
+
+ADC1->CR2 |= ADC_CR2_CAL;             // 开始校准
+while (ADC1->CR2 & ADC_CR2_CAL)       // 等待校准完成(CAL 位硬件自动清 0)
 {
-    // 1. 开启ADC1时钟 + GPIOA时钟
-    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
-    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
+}
+// 校准完成,ADC 准备就绪
+```
 
-    // 2. PA0 模拟输入模式(CNF=00, MODE=00)
-    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
+> 校准原理:ADC 内部测量一次 VREFINT,根据误差调整内部电容阵列。
 
-    // 3. 复位ADC1配置
-    RCC->APB2RSTR |= RCC_APB2RSTR_ADC1RST;  // 复位
-    RCC->APB2RSTR &= ~RCC_APB2RSTR_ADC1RST;
+---
+
+## 实验:单通道 ADC 采集
 
-    // 4. ADC 时钟分频(APB2=72MHz, ADC_CLK=72/6=12MHz)
-    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;
+### 软件设计(寄存器版 — 单次采集)
+
+**项目路径**:`stm32_base/32_adc_single_register`
 
-    // 5. 配置ADC1
-    ADC1->CR2 |= ADC_CR2_ADON;               // 开启ADC1
-    Delay_us(10);
+**需求**:使用 ADC1 通道 0(PA0)采集模拟电压,通过串口打印原始值和电压值。
 
-    // 6. 校准
-    ADC1->CR2 |= ADC_CR2_CAL;                // 开始校准
-    while (ADC1->CR2 & ADC_CR2_CAL);         // 等待校准结束
+**文件:`stm32_base/32_adc_single_register/Hardware/ADC/adc.c`**
 
-    // 7. 配置规则组序列长度=1,通道0为第1个
-    ADC1->SQR1 &= ~ADC_SQR1_L;               // L[3:0]=0: 1个转换
-    ADC1->SQR3 &= ~ADC_SQR3_SQ1;             // SQ1[4:0]=0: 通道0
+```c
+#include "adc.h"
 
-    // 8. 采样时间:239.5周期
-    ADC1->SMPR2 |= ADC_SMPR2_SMP0;           // SMP0[2:0]=111: 239.5周期
+// ADC1 初始化:PA0(通道0),软件触发,单次转换
+void ADC1_Init(void)
+{
+    /* ========== 1. 开启时钟 ========== */
+    // ADC1 挂载在 APB2 总线上
+    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;     // |=: ADC1时钟使能
+    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;      // |=: GPIOA时钟使能
+
+    /* ========== 2. 配置 PA0 为模拟输入 ========== */
+    // 模拟输入模式: MODE=00, CNF=00
+    // 注意: 模拟输入下施密特触发器关闭以降低功耗
+    GPIOA->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);  // &=~: 同时清除MODE0和CNF0
+
+    /* ========== 3. 配置 ADC 时钟分频 ========== */
+    // APB2=72MHz, ADCPRE=Div6 → ADC_CLK=12MHz(≤14MHz ✅)
+    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;      // |=: ADCPRE[1:0]=10
+
+    /* ========== 4. 复位 ADC 配置 ========== */
+    // 通过 RSTR 寄存器复位(可选但推荐,确保从干净状态开始)
+    RCC->APB2RSTR |= RCC_APB2RSTR_ADC1RST;   // |=: 复位ADC1
+    RCC->APB2RSTR &= ~RCC_APB2RSTR_ADC1RST;  // &=~: 解除复位
+
+    /* ========== 5. 开启 ADC + 校准 ========== */
+    ADC1->CR2 |= ADC_CR2_ADON;               // |=: ADON=1 开启ADC
+    Delay_us(10);                             // 等待稳定
+
+    // 校准
+    ADC1->CR2 |= ADC_CR2_CAL;               // |=: 开始校准
+    while (ADC1->CR2 & ADC_CR2_CAL);         // 等待CAL硬件清0(校准结束)
+
+    /* ========== 6. 配置规则组序列 ========== */
+    // SQ1[4:0] = 通道号 0(PA0)
+    ADC1->SQR3 &= ~ADC_SQR3_SQ1;             // &=~: 清除SQ1位域
+    ADC1->SQR3 |= (0 << 0);                   // |=: SQ1 = 通道 0(0 是默认值,此行示意)
+    // SEQ_LEN[3:0] = 0(表示序列长度为 1)
+    ADC1->SQR1 &= ~ADC_SQR1_L;               // &=~: L清0(1个转换)
+
+    /* ========== 7. 配置采样时间 ========== */
+    // 通道0的采样时间: SMP0[2:0]=111 → 239.5周期(最准)
+    ADC1->SMPR2 |= ADC_SMPR2_SMP0;           // |=: SMP0位全部置1
 }
 
-// 单次转换(软件触发)
+// 读取 ADC 转换结果(软件触发单次转换
 uint16_t ADC1_Read(void)
 {
-    ADC1->CR2 |= ADC_CR2_ADON;               // 再次写ADON启动转换
-    while (!(ADC1->SR & ADC_SR_EOC));        // 等待转换结束(EOC=1)
-    return ADC1->DR;                         // 读取结果(0~4095)
+    // 1. 启动转换:再次写 ADON(当 CR2 已有 ADON=1 时,再次写 1 触发转换)
+    ADC1->CR2 |= ADC_CR2_ADON;               // |=: 软件触发
+
+    // 2. 等待转换结束
+    // SR.EOC = 1 表示转换完成
+    while (!(ADC1->SR & ADC_SR_EOC))          // &测试: EOC位=1?
+    {
+    }
+
+    // 3. 读取 DR 寄存器(读取后 EOC 自动清 0)
+    return ADC1->DR;                          // 返回 12 位结果(0~4095)
+}
+
+// 将 ADC 原始值转换为电压(mV)
+uint16_t ADC_GetVoltage(uint16_t adc_value)
+{
+    // V = adc_value × 3300mV / 4095
+    return (uint16_t)((uint32_t)adc_value * 3300 / 4095);
 }
+```
+
+**文件:`stm32_base/32_adc_single_register/User/main.c`**
+
+```c
+#include "adc.h"
+#include "usart.h"
+#include <stdio.h>
 
 int main(void)
 {
+    USART_Init();
     ADC1_Init();
+
+    printf("ADC 单通道采集实验\r\n");
+
     while (1)
     {
-        uint16_t val = ADC1_Read();
-        float voltage = val * 3.3f / 4095.0f;  // 转为电压值
-        printf("ADC: %d, %.2fV\r\n", val, voltage);
-        Delay_ms(500);
+        uint16_t adc_val = ADC1_Read();
+        uint16_t volt_mv = ADC_GetVoltage(adc_val);
+
+        printf("ADC: %4d  (0~4095)  |  %.3fV\r\n",
+               adc_val, volt_mv / 1000.0f);
+
+        Delay_ms(500);  // 每 500ms 采集一次
     }
 }
 ```
 
 ---
 
-## 核心速查表
+## 实验:双通道 ADC + DMA 采集
+
+**项目路径**:`stm32_base/34_adc_double_register`
+
+多通道 + DMA 是 ADC 的典型高效用法:
+
+```c
+// 配置 ADC1 通道 0 和通道 1(PA0, PA1)
+// 使用 DMA 自动搬运转换结果,无需 CPU 轮询
+
+#define ADC_CHANNELS  2
+uint16_t adc_values[ADC_CHANNELS];  // DMA 自动写入此数组
+
+void ADC_DMA_Init(void)
+{
+    // ADC 配置(同单通道,但做以下修改)
+
+    // 1. 规则序列: 通道0 → 通道1 依次转换
+    ADC1->SQR3 = (1 << 0) | (0 << 5);  // SQ1=通道1, SQ2=通道0(此处仅为示例)
+    // SQR1.L = 1 → 序列长度为 2
+    ADC1->SQR1 |= (1 << 20);            // L[3:0]=1: 共 2 个转换
+
+    // 2. 开启 DMA 模式
+    ADC1->CR2 |= ADC_CR2_DMA;          // DMA=1: 使能DMA传输
+
+    // 3. 开启扫描模式(多通道必须开启)
+    ADC1->CR1 |= ADC_CR1_SCAN;         // SCAN=1: 扫描模式
+
+    // 4. 开启连续转换
+    ADC1->CR2 |= ADC_CR2_CONT;         // CONT=1: 连续转换
+}
+
+// DMA 配置
+void DMA_ADC_Init(void)
+{
+    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
+
+    // ADC1 使用 DMA1 通道1
+    DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);      // 外设: ADC数据寄存器
+    DMA1_Channel1->CMAR = (uint32_t)adc_values;         // 内存: 存储数组
+    DMA1_Channel1->CNDTR = ADC_CHANNELS;                // 传输次数 = 通道数
+    DMA1_Channel1->CCR |= DMA_CCR1_MINC                 // 内存地址递增
+                        | DMA_CCR1_CIRC                 // 循环模式
+                        | DMA_CCR1_TCIE;                // 完成中断
+
+    DMA1_Channel1->CCR |= DMA_CCR1_EN;                  // 使能
+
+    // 注意: 每次转换完 ADC 会自动发 DMA 请求
+    // DMA 将 ADC1->DR 的值搬到 adc_values[x] 然后地址+1
+    // 所有通道转换完一轮 → DMA 中断
+}
+```
+
+---
+
+## HAL 库版 ADC
+
+```c
+// HAL 库 ADC 初始化(CubeMX 生成)
+ADC_HandleTypeDef hadc1;
+
+void MX_ADC1_Init(void)
+{
+    hadc1.Instance = ADC1;
+    hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;     // 非扫描
+    hadc1.Init.ContinuousConvMode = ENABLE;          // 连续转换
+    hadc1.Init.DiscontinuousConvMode = DISABLE;
+    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;// 软件触发
+    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;       // 右对齐
+    hadc1.Init.NbrOfConversion = 1;                   // 1个转换
+    HAL_ADC_Init(&hadc1);
+}
+
+// 阻塞式读取
+HAL_ADC_Start(&hadc1);
+HAL_ADC_PollForConversion(&hadc1, 100);
+uint16_t val = HAL_ADC_GetValue(&hadc1);
+
+// DMA 方式
+HAL_ADC_Start_DMA(&hadc1, buffer, size);
+// 完成回调: HAL_ADC_ConvCpltCallback(&hadc1)
+```
+
+---
+
+## 核心函数速查表
 
-| 步骤 | 寄存器操作 |
-|------|----------|
-| 开启ADC | `CR2 |= ADON` |
-| 校准 | `CR2 |= CAL → 等待CAL=0` |
-| 配置通道 | `SQR3.SQ1 = 通道编号` |
-| 配置采样时间 | `SMPRx.SMPy = 周期数` |
-| 启动转换 | `CR2 |= ADON`(软件)或 `SWSTART` |
-| 查询EOC | `while (!(SR & EOC))` |
-| 读取结果 | `val = DR`(12位右对齐) |
+| 操作 | 寄存器版 | HAL 库版 |
+|------|----------|---------|
+| 开启 ADC | `CR2 |= ADON` | `HAL_ADC_Init()` |
+| 校准 | `CR2 |= CAL; while(CR2 & CAL)` | 自动包含在 Init 中 |
+| 配置通道 | `SQR3.SQ1 = ch` | `ADC_ChannelConfTypeDef` |
+| 启动转换 | `CR2 |= ADON`(或 SWSTART) | `HAL_ADC_Start()` |
+| 查询完成 | `while(!(SR & EOC))` | `HAL_ADC_PollForConversion()` |
+| 读取结果 | `val = DR` | `HAL_ADC_GetValue()` |
+| 启动+DMA | `CR2 |= DMA + CONT` + `DMA_Init()` | `HAL_ADC_Start_DMA()` |
+| 电压换算 | `val × 3300 / 4095` | 同上 |
+| 停止 ADC | `CR2 &= ~ADON` | `HAL_ADC_Stop()` |
 
 ## 常见问题与避坑
 
-1. **ADC 值跳动大** → 电源去耦不良、采样时间太短、建议开启硬件平均
-2. **ADC 值始终为 0** → 检查 GPIO 模式(必须模拟输入)、ADC 是否校准
-3. **ADC 值始终为 4095** → 输入电压超过 VREF+ 或 ADC 输入引脚悬空
-4. **多通道采样顺序错乱** → 检查 SQR 序列配置(SQ1~SQ16 的顺序)
-5. **ADC 时钟超过 14MHz** → ADC 转换结果会不准确(ADCPRE 分频必须使 ADC_CLK ≤ 14MHz)
+1. **ADC 值始终为 0** → 检查 GPIO 模式(必须是**模拟输入**,不是浮空输入)、ADC 是否校准
+2. **ADC 值始终为 4095** → 输入引脚悬空(感应噪声)、输入电压超过 VREF+
+3. **ADC 值跳动很大** → 采样时间太短(增加采样周期数)、电源去耦不良、PCB 布线噪声
+4. **多通道值错乱** → SQR 序列配置错误、DMA 缓冲区大小与通道数不匹配
+5. **ADC 时钟 >14MHz** → 转换结果不准确,必须确保 ADCPRE 分频后 ADC_CLK ≤ 14MHz
+6. **电压换算不准** → VREF+ 实际电压可能是 3.28V 而非 3.30V,需用万用表测量后修正
+7. **DMA 传输不停止** → CIRC(循环模式)导致持续传输,停止时需要先清 DMA EN 再清 CIRC

+ 328 - 103
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/12-SPI通信与FSMC总线.md

@@ -9,166 +9,391 @@ created: 2026-07-15
 
 # SPI通信与FSMC总线
 
-> **用生活理解**:SPI 是全双工的同步通信,就像两个人用两根线同时说话和听。四条线各司其职:SCK(节奏)、MOSI(你说)、MISO(我听)、NSS(点名)。FSMC 就是芯片的外部数据总线,把外部设备(Flash/SRAM/LCD)映射到 STM32 的地址空间,像访问内部变量一样访问外部设备
+> **用生活理解**:SPI 是全双工同步通信,就像两个人用两根电话线同时说话和听——一根你说(MOSI),一根你听(MISO),SCK 是节拍器控制节奏,NSS 是点名器(叫到谁谁回答)。FSMC 就像给芯片外接了一个"内存扩展槽"——外部 SRAM/Flash/LCD 映射到 CPU 的地址空间,访问它们就像访问内部变量一样直接
 
 ---
 
 ## SPI 通信协议
 
-SPI = Serial Peripheral Interface(串行外设接口)。
+SPI = Serial Peripheral Interface(串行外设接口),**同步、全双工**
 
-### 四线制
+### 四线制信号
 
-| 信号 | 功能 |
-|------|------|
-| SCK | 时钟线(由主机产生) |
-| MOSI | 主机输出/从机输入 |
-| MISO | 主机输入/从机输出 |
-| NSS/CS | 从机选择(低电平有效) |
+| 信号 | 全称 | 功能 |
+|------|------|------|
+| **SCK** | Serial Clock | 时钟线,由主机产生 |
+| **MOSI** | Master Out Slave In | 主机输出/从机输入 |
+| **MISO** | Master In Slave Out | 主机输入/从机输出 |
+| **NSS/CS** | Chip Select | 从机选择线,低电平有效 |
 
-### 四种模式(CPOL/CPHA)
+**多设备拓扑**:
+```
+主机(SCK) ──── SCK ────┬── 从机1 CS1
+                        ├── 从机2 CS2
+                        └── 从机3 CS3
+    (MOSI) ── MOSI ────┼── 所有从机共享
+    (MISO) ── MISO ────┼── 所有从机共享
+```
+> 各从机独立片选(CS),SCK/MOSI/MISO 共享
+
+### SPI 四种工作模式
+
+SPI 模式由 CPOL(时钟极性)和 CPHA(时钟相位)决定:
+
+| 模式 | CPOL | CPHA | 空闲 SCK | 数据采集边沿 | NSS 有效后数据变化边沿 |
+|------|------|------|----------|-------------|---------------------|
+| **0** | 0 | 0 | 低电平 | 上升沿(第1个) | 下降沿 |
+| 1 | 0 | 1 | 低电平 | 下降沿(第2个) | 上升沿 |
+| 2 | 1 | 0 | 高电平 | 下降沿(第1个) | 上升沿 |
+| 3 | 1 | 1 | 高电平 | 上升沿(第2个) | 下降沿 |
+
+> **W25Q64 Flash** 支持模式 0 和模式 3。配置时注意主从必须一致。
+> **参考**:参考手册 §23(SPI 寄存器描述)、W25Q64 数据手册
+
+### SPI 关键寄存器(SPIx)
+
+| 寄存器 | 地址偏移 | 功能 |
+|--------|---------|------|
+| **CR1** | 0x00 | 控制1(CPOL、CPHA、BR[2:0]波特率分频、MSTR主从、SPE使能、LSBFIRST位序、SSI、SSM) |
+| **CR2** | 0x04 | 控制2(SSOE、TXEIE/RXNEIE中断使能、DMA 使能) |
+| **SR** | 0x08 | 状态(BSY忙、TXE发送空、RXNE接收非空、MODF模式错误、OVR溢出) |
+| **DR** | 0x0C | 数据寄存器(读写共用,写 = 发送缓冲区,读 = 接收缓冲区) |
+| **CRCPR** | 0x10 | CRC 多项式寄存器 |
+
+**CR1.BR[2:0] 波特率分频**:
+| BR[2:0] | 分频系数 | SPI1(72MHz) | SPI2/3(36MHz) |
+|---------|---------|-------------|--------------|
+| 000 | /2 | 36MHz | 18MHz |
+| 001 | /4 | 18MHz | 9MHz |
+| 010 | /8 | 9MHz | 4.5MHz |
+| 011 | /16 | 4.5MHz | 2.25MHz |
+| 100 | /32 | 2.25MHz | 1.125MHz |
+| 101 | /64 | 1.125MHz | 562.5KHz |
+| 110 | /128 | 562.5KHz | 281.25KHz |
+| 111 | /256 | 281.25KHz | 140.625KHz |
+
+### SPI 收发原理
+
+SPI 数据寄存器 DR 是**双缓冲**结构:
+```
+写 DR → TX 缓冲区 → 移位寄存器(8位) → MOSI 逐位输出
+                             ↓
+MISO 逐位输入 ← 移位寄存器(8位) ← RX 缓冲区 → 读 DR
+```
 
-| 模式 | CPOL | CPHA | 特性 |
-|------|------|------|------|
-| 0 | 0 | 0 | SCK空闲低,第一个边沿采集 |
-| 1 | 0 | 1 | SCK空闲低,第二个边沿采集 |
-| 2 | 1 | 0 | SCK空闲高,第一个边沿采集 |
-| 3 | 1 | 1 | SCK空闲高,第二个边沿采集 |
+**发送 1 字节的时序**:
+```
+1. 写 DR (CPU) → 数据进入 TX 缓冲区
+2. TX 缓冲区 → 移位寄存器 (TXE=1, 可写入下一字节)
+3. 移位寄存器逐位移出 (SCK 控制)
+4. 同时逐位移入 MISO 数据
+5. 8 位完成后数据进入 RX 缓冲区 (RXNE=1)
+```
 
-> 与 W25Q64 Flash 通信常用**模式 0**(CPOL=0, CPHA=0)或**模式 3**(CPOL=1, CPHA=3)
+**读写对称性**:SPI 是**环形移位**——发 1 字节的同时必定收到 1 字节。
+要读取从机数据,主机必须同时发送 1 字节(通常发 0x00 或 0xFF 占位)。
 
-### SPI 关键寄存器
+---
 
-| 寄存器 | 功能 |
-|--------|------|
-| CR1 | 控制(CPOL/CPHA、BR波特率分频、MSTR主从、SPE使能、LSBFIRST) |
-| CR2 | 控制(SSOE、TXEIE/RXNEIE中断使能) |
-| SR | 状态(BSY忙、TXE发送空、RXNE接收非空) |
-| DR | 数据寄存器(发送/接收共用) |
+## 软件 SPI 实现
 
-> **参考**:参考手册 §23(SPI 寄存器描述)
+### 软件设计(寄存器版 — 软件模拟 SPI)
 
----
+**项目路径**:`stm32_base/36_spi_software_register`
 
-## W25Q64 Flash
+**文件:`stm32_base/36_spi_software_register/Hardware/SPI/spi.h`**
 
-| 参数 | 值 |
-|------|-----|
-| 容量 | 8MB(64Mbit) |
-| 接口 | SPI |
-| 页 | 256 字节 |
-| 扇区 | 4KB(16 页) |
-| 擦除 | 扇区擦除(20ms)、块擦除、全擦除 |
-| 写寿命 | 100,000 次 |
+```c
+#ifndef __SPI_H
+#define __SPI_H
 
-**常用指令**:
-| 指令 | 代码 | 功能 |
-|------|------|------|
-| WREN | 0x06 | 写使能 |
-| RDSR | 0x05 | 读状态寄存器 |
-| PAGE_PROG | 0x02 | 页编程(256字节) |
-| READ | 0x03 | 读数据 |
-| SECTOR_ERASE | 0xD8 | 扇区擦除(4KB) |
+#include "stm32f10x.h"
 
----
+// GPIO 引脚宏定义
+// PA5=SCK, PA6=MISO, PA7=MOSI, PA4=CS(片选)
+#define SPI_SCK_HIGH  (GPIOA->ODR |= GPIO_ODR_ODR5)   // |= SCK置1
+#define SPI_SCK_LOW   (GPIOA->ODR &= ~GPIO_ODR_ODR5)  // &=~ SCK清0
+#define SPI_MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)   // |= MOSI置1
+#define SPI_MOSI_LOW  (GPIOA->ODR &= ~GPIO_ODR_ODR7)  // &=~ MOSI清0
+#define SPI_MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)     // & 读取MISO电平
+#define SPI_CS_HIGH   (GPIOA->ODR |= GPIO_ODR_ODR4)   // |= CS置1
+#define SPI_CS_LOW    (GPIOA->ODR &= ~GPIO_ODR_ODR4)  // &=~ CS清0
 
-## 软件设计(软件模拟 SPI 写 Flash)
+void SPI_Init(void);
+uint8_t SPI_SendByte(uint8_t byte);
 
-**文件:`stm32_base/36_spi_software_register/Hardware/SPI/spi.h`**
+#endif
+```
 
-```c
-#define SCK_HIGH  (GPIOA->ODR |= GPIO_ODR_ODR5)
-#define SCK_LOW   (GPIOA->ODR &= ~GPIO_ODR_ODR5)
-#define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)
-#define MOSI_LOW  (GPIOA->ODR &= ~GPIO_ODR_ODR7)
-#define MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)
-#define CS_HIGH   (GPIOA->ODR |= GPIO_ODR_ODR4)
-#define CS_LOW    (GPIOA->ODR &= ~GPIO_ODR_ODR4)
+**文件:`stm32_base/36_spi_software_register/Hardware/SPI/spi.c`**
 
-// SPI 模式0:CPOL=0, CPHA=0
-// 空闲时SCK=低, 上升沿采集数据
+```c
+#include "spi.h"
 
+// SPI 初始化(模式0: CPOL=0, CPHA=0)
 void SPI_Init(void)
 {
+    // 1. 开启 GPIOA 时钟
     RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
-    // PA5(SCK)+PA7(MOSI): 推挽输出
-    // PA6(MISO): 浮空输入
-    // PA4(CS): 推挽输出
+
+    // 2. 配置 PA4(CS), PA5(SCK), PA7(MOSI) 为推挽输出 50MHz
+    // PA4: CRL 控制,Pin4 的 MODE 在 CRL 的第 18~19 位
+    GPIOA->CRL |= GPIO_CRL_MODE4;      // MODE4=11: 50MHz输出
+    GPIOA->CRL &= ~GPIO_CRL_CNF4;      // CNF4=00: 通用推挽
+
+    // PA5(SCK)
+    GPIOA->CRL |= GPIO_CRL_MODE5;
+    GPIOA->CRL &= ~GPIO_CRL_CNF5;
+
+    // PA7(MOSI)
+    GPIOA->CRL |= GPIO_CRL_MODE7;
+    GPIOA->CRL &= ~GPIO_CRL_CNF7;
+
+    // 3. 配置 PA6(MISO) 为浮空输入
+    GPIOA->CRL &= ~GPIO_CRL_MODE6;     // MODE6=00: 输入
+    GPIOA->CRL &= ~GPIO_CRL_CNF6_1;    // CNF6[1]=0
+    GPIOA->CRL |= GPIO_CRL_CNF6_0;     // CNF6[0]=1 → 浮空输入
+
+    // 4. 初始状态: SCK=低(CPOL=0), CS=高(不选中)
+    SPI_SCK_LOW;
+    SPI_CS_HIGH;
 }
 
+// SPI 模式0: CPOL=0(空闲SCK低), CPHA=0(上升沿采集)
+// 发送 1 字节的同时接收 1 字节
 uint8_t SPI_SendByte(uint8_t byte)
 {
-    for (uint8_t i = 0; i < 8; i++)
+    uint8_t i;
+
+    // 循环 8 次,每次处理 1 位
+    for (i = 0; i < 8; i++)
     {
-        if (byte & 0x80) MOSI_HIGH; else MOSI_LOW;  // MSB first
-        byte <<= 1;
-        SCK_HIGH;                      // 上升沿 → 从机采集
-        if (MISO_READ) byte |= 1;      // 读取MISO
-        SCK_LOW;                       // 下降沿 → 从机改变数据
+        // 第 1 步: 在 SCK 低电平时改变 MOSI 数据
+        // MSB first (先发最高位)
+        if (byte & 0x80)              // &测试: 最高位=1?
+            SPI_MOSI_HIGH;            // 输出高
+        else
+            SPI_MOSI_LOW;             // 输出低
+
+        byte <<= 1;                   // 左移1位,准备下一位
+
+        // 第 2 步: SCK 上升沿 → 从机采集 MOSI
+        SPI_SCK_HIGH;                 // SCK上升沿
+
+        // 同时,主机在上升沿采集 MISO(从机的数据)
+        if (SPI_MISO_READ)            // 读取MISO电平
+            byte |= 1;                // 如果MISO=高,将当前位(已左移后的bit0)置1
+
+        // 第 3 步: SCK 下降沿 → 从机改变 MISO 数据
+        SPI_SCK_LOW;                  // SCK下降沿
     }
-    return byte;
+
+    return byte;  // 返回接收到的数据
 }
 ```
 
 ---
 
+## 硬件 SPI(stm32_base/37_spi_hardware_register)
+
+硬件 SPI 使用 STM32 片内外设,更高效(硬件自动移位):
+
+```c
+void SPI1_Init(void)
+{
+    // 1. 开启 SPI1 + GPIOA 时钟
+    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;      // SPI1在APB2
+    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
+
+    // 2. PA5(SCK) 复用推挽, PA6(MISO) 浮空输入, PA7(MOSI) 复用推挽
+
+    // 3. 配置 SPI1
+    // CR1: CPOL=0, CPHA=0, BR=011(72/16=4.5MHz), MSTR=1(主机), SPE=1(使能)
+    SPI1->CR1 = 0;                              // 先清0
+    SPI1->CR1 |= SPI_CR1_CPOL & 0;             // CPOL=0
+    SPI1->CR1 |= SPI_CR1_CPHA & 0;             // CPHA=0
+    SPI1->CR1 |= SPI_CR1_BR_1 | SPI_CR1_BR_0; // BR=011: fPCLK/16
+    SPI1->CR1 |= SPI_CR1_MSTR;                  // MSTR=1: 主机模式
+    SPI1->CR1 |= SPI_CR1_SSI;                   // SSI=1: 软件NSS高电平
+    SPI1->CR1 |= SPI_CR1_SSM;                   // SSM=1: 软件NSS管理
+    SPI1->CR1 |= SPI_CR1_SPE;                   // SPE=1: SPI使能
+}
+
+// 硬件 SPI 收发
+uint8_t SPI1_SendByte(uint8_t byte)
+{
+    while (!(SPI1->SR & SPI_SR_TXE)) {}   // 等待 TX 缓冲区空
+    SPI1->DR = byte;                       // 写入数据 → 开始发送
+    while (!(SPI1->SR & SPI_SR_RXNE)) {}  // 等待 RX 缓冲区非空
+    return SPI1->DR;                       // 读取接收数据
+}
+```
+
+---
+
+## W25Q64 Flash 驱动
+
+| 参数 | 值 |
+|------|-----|
+| 容量 | 8MB (64Mbit) |
+| 页(Page) | 256 字节 |
+| 扇区(Sector) | 4KB (16 页) |
+| 块(Block) | 64KB (16 扇区) |
+| 擦除时间(扇区) | 典型 45ms |
+| 写寿命 | 100,000 次 |
+
+### 常用指令集
+
+| 指令 | 代码 | 功能 | 后接参数 |
+|------|------|------|---------|
+| WREN | 0x06 | 写使能(每次写前必须发) | — |
+| WRDI | 0x04 | 写禁止 | — |
+| RDSR | 0x05 | 读状态寄存器 | 1字节返回 |
+| WRSR | 0x01 | 写状态寄存器 | 1字节 |
+| **READ** | **0x03** | **读数据** | 3字节地址 + N字节数据 |
+| **PAGE_PROG** | **0x02** | **页编程(≤256字节)** | 3字节地址 + 数据 |
+| **SECTOR_ERASE** | **0xD8** | **扇区擦除(4KB)** | 3字节地址 |
+| BLOCK_ERASE_32 | 0x52 | 32KB 块擦除 | 3字节地址 |
+| BLOCK_ERASE_64 | 0xD8 | 64KB 块擦除 | 3字节地址 |
+| CHIP_ERASE | 0xC7 | 全片擦除 | — |
+| RDID | 0x9F | 读芯片 ID | 3字节返回 |
+
+### W25Q64 擦写(读写前必须擦除)
+
+```c
+// W25Q64 写入流程:
+// 1. 读状态寄存器,检查 BUSY 位(正在擦除/写入?)
+// 2. 发送 WREN (0x06) 使能写
+// 3. 发送 SECTOR_ERASE (0xD8) + 3字节地址 → 等待 BUSY=0
+// 4. 再次 WREN → PAGE_PROG (0x02) + 3字节地址 + 数据(≤256字节)
+// 5. 等待 BUSY=0
+
+uint8_t W25Q64_ReadSR(void)
+{
+    uint8_t sr;
+    CS_LOW;
+    SPI_SendByte(RDSR);           // 发指令 0x05
+    sr = SPI_SendByte(0xFF);      // 发占住位,收状态
+    CS_HIGH;
+    return sr;
+}
+
+void W25Q64_WaitBusy(void)
+{
+    while (W25Q64_ReadSR() & 0x01);  // BUSY=1 表示忙
+}
+
+void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len)
+{
+    W25Q64_WaitBusy();          // 等上次完成
+    CS_LOW;
+    SPI_SendByte(WREN);         // 写使能
+    CS_HIGH;
+
+    CS_LOW;
+    SPI_SendByte(PAGE_PROG);    // 页编程指令
+    SPI_SendByte(addr >> 16);   // 地址高8位
+    SPI_SendByte(addr >> 8);    // 地址中8位
+    SPI_SendByte(addr);         // 地址低8位
+    for (uint16_t i = 0; i < len; i++) {
+        SPI_SendByte(data[i]);  // 发送数据
+    }
+    CS_HIGH;
+    W25Q64_WaitBusy();          // 等写入完成
+}
+```
+
+> **注意**:W25Q64 不支持"写覆盖"——必须先擦除再写。最小擦除单位是扇区(4KB)。
+> 页编程不能跨页(256 字节边界)。如果数据跨页,需要分多次写入。
+
+---
+
 ## FSMC 总线
 
 FSMC = Flexible Static Memory Controller(灵活的静态存储器控制器)。
 
 ### 地址映射
 
-| Bank | 地址范围 | 目标设备 |
+| Bank | 地址范围 | 容量 | 目标设备 |
+|------|---------|------|---------|
+| **Bank1** | **0x60000000~0x6FFFFFFF** | **256MB** | **NOR Flash / PSRAM / SRAM / LCD** |
+| Bank2 | 0x70000000~0x7FFFFFFF | 128MB | NAND Flash |
+| Bank3 | 0x80000000~0x8FFFFFFF | 128MB | NAND Flash |
+| Bank4 | 0x90000000~0x9FFFFFFF | 128MB | PC Card |
+
+Bank1 分为 4 个子区(片选 NE1~NE4),各 64MB:
+
+| 子区 | 片选引脚 | 地址范围 |
 |------|---------|---------|
-| Bank1 | 0x60000000~0x6FFFFFFF | NOR Flash / PSRAM / SRAM / LCD |
-| Bank2 | 0x70000000~0x7FFFFFFF | NAND Flash |
-| Bank3 | 0x80000000~0x8FFFFFFF | NAND Flash |
-| Bank4 | 0x90000000~0x9FFFFFFF | PC Card |
+| NE1 | FSMC_NE1 | 0x60000000~0x63FFFFFF(最常用) |
+| NE2 | FSMC_NE2 | 0x64000000~0x67FFFFFF |
+| NE3 | FSMC_NE3 | 0x68000000~0x6BFFFFFF |
+| NE4 | FSMC_NE4 | 0x6C000000~0x6FFFFFFF |
 
-Bank1 又分为 4 个子区(NE1~NE4),每个 64MB:
-```
-NE1: 0x60000000~0x63FFFFFF
-NE2: 0x64000000~0x67FFFFFF
-NE3: 0x68000000~0x6BFFFFFF
-NE4: 0x6C000000~0x6FFFFFFF
-```
+### FSMC 信号线
+
+| 信号 | 功能 |
+|------|------|
+| FSMC_A[25:0] | 地址总线 |
+| FSMC_D[15:0] | 数据总线(16 位模式) |
+| FSMC_NE[4:1] | 片选(低电平有效) |
+| FSMC_NOE | 读使能(低电平有效) |
+| FSMC_NWE | 写使能(低电平有效) |
 
 ### FSMC 关键寄存器
 
 | 寄存器 | 功能 |
 |--------|------|
-| BC1~4 | 存储片控制寄存器(MTYP设备类型、MWID数据宽度) |
-| BT1~4 | 存储片时序寄存器(ADDSET地址建立、DATAST数据保持) |
-
-**LCD 应用**:将 LCD 的 D0~D15 数据线接 FSMC_D0~D15,RS 接 A0(地址线),
-这样:
-- 写 `*(uint16_t*)0x60000000 = data` → 写命令(RS=0)
-- 写 `*(uint16_t*)0x60020000 = data` → 写数据(RS=1,A0=1)
-
-> **参考**:参考手册 §19(FSMC 寄存器描述)
+| **BCRx** | SRAM/NOR 控制寄存器(MTYP 设备类型、MWID 数据宽度) |
+| **BTRx** | SRAM/NOR 时序寄存器(ADDSET 地址建立时间、DATAST 数据保持时间) |
+| BWTRx | 写时序寄存器(写操作时序,与读独立) |
+
+**控制寄存器 (BCR1~BCR4)**:
+| 位 | 名称 | 说明 |
+|----|------|------|
+| 0 | MBKEN | 存储区使能 |
+| 1:2 | MTYP | 设备类型:00=SRAM, 01=PSRAM, 10=NOR |
+| 3:4 | MWID | 数据宽度:00=8位, 01=16位 |
+| 12 | WEN | 写使能 |
+| 14 | FACCEN | Flash 访问使能(NOR Flash 时用) |
+
+**时序寄存器 (BTR1~BTR4)**:
+| 位 | 名称 | 说明 |
+|----|------|------|
+| 0:3 | ADDSET | 地址建立时间(0~15个HCLK周期) |
+| 8:15 | DATAST | 数据保持时间(1~255个HCLK周期) |
+| 16:19 | BUSTURN | 总线周转时间 |
+
+### FSMC + LCD 应用
+
+将 LCD 连接到 FSMC,利用地址线 A0 区分命令和数据:
+```
+写 (uint16_t *)0x60000000 = cmd   → RS=0 → 写命令
+写 (uint16_t *)0x60020000 = data  → RS=1(A0=1) → 写数据
+```
 
 ---
 
 ## 核心速查表
 
-| SPI操作 | 代码 |
-|---------|------|
-| 使能SPI | `CR1 |= SPE` |
-| 发送1字节 | `DR = byte; while(!(SR & TXE))` |
-| 接收1字节 | `while(!(SR & RXNE)); byte = DR` |
-| 波特率分频 | `CR1.BR[2:0]` = fPCLK/2~(fPCLK/256) |
+| SPI 操作 | 软件模拟 | 硬件 SPI |
+|---------|---------|---------|
+| 初始化 | GPIO 开漏/推挽 | `CR1 |= MSTR + SPE` + GPIO 复用 |
+| 发 1 字节 | 8 次 GPIO 位操作 | `DR = byte; while(!RXNE); val = DR` |
+| 片选 | GPIO 位操作 | 同(NSS 由软件控制) |
+| 速度调整 | 修改延时 | `CR1.BR[2:0]` |
 
-| FSMC操作 | 代码 |
+| FSMC 操作 | 代码 |
 |----------|------|
-| 写SRAM | `*(uint16_t*)0x60000000 = data` |
-| 读SRAM | `data = *(uint16_t*)0x60000000` |
-| 写LCD命令 | `*(uint16_t*)0x60000000 = cmd` |
-| 写LCD数据 | `*(uint16_t*)0x60020000 = data` |
+| 写 SRAM | `*(uint16_t *)0x60000000 = data` |
+| 读 SRAM | `data = *(uint16_t *)0x60000000` |
+| 写 LCD 命令 | `*(uint16_t *)0x60000000 = cmd` |
+| 写 LCD 数据 | `*(uint16_t *)0x60020000 = data` |
 
 ## 常见问题与避坑
 
-1. **SPI 接收数据为 0xFF** → 检查 MISO 连接、从机是否选中(CS)、时钟是否匹配
-2. **W25Q64 写入失败** → 必须先发 WREN(0x06)、扇区必须先擦除(不能写覆盖)
-3. **FSMC 读写时序不对** → 参考 SRAM/LCD 数据手册的时序参数配置 ADDSET/DATAST
-4. **多个 SPI 设备冲突** → 各设备独立 CS、SCK/MOSI/MISO 共享
-5. **FSMC 地址线错位** → 16位模式下 HADDR[0] 不连接到 FSMC_A[0](内部是对齐的)
+1. **SPI 接收数据为 0xFF** → MISO 连接断开、从机未选中(CS 拉低后才有输出)、从机忙于内部操作
+2. **W25Q64 写入失败** → 每次写操作前必须发 WREN(0x06);扇区必须事先擦除(Flash 不能写覆盖)
+3. **FSMC 读写时序不对** → 查 SRAM/LCD 数据手册的时序参数(ADDSET 和 DATAST),STM32 的 HCLK 对应关系
+4. **软件 SPI 速度太慢** → 软件 SPI 受限于 GPIO 翻转速度(约 2~4MHz),大批量数据建议用硬件 SPI
+5. **NSS 软件管理模式** → 多从机时必须用 SSM=1 软件管理 NSS,否则硬件自动管理可能产生冲突

+ 363 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/13-CAN通信协议与bxCAN外设.md

@@ -0,0 +1,363 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — 第1章:CAN + 配套代码01~04"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# CAN通信协议与bxCAN外设
+
+> **用生活理解**:CAN 总线就像一场没有主持人的圆桌会议——谁都可以发言(多主),但发言时先报自己的"优先级数字"(ID)。数字小的优先级高,即使两个人同时开口,优先级低的那位会自动闭嘴让优先级高的先说。这就是 CAN 的**无损仲裁**——不会因为冲突而浪费任何数据。
+
+---
+
+## CAN 协议基础
+
+CAN = Controller Area Network(控制器局域网),由 Bosch 公司 1986 年发明,主要用于汽车电子通信。
+
+| 特性 | 说明 |
+|------|------|
+| 总线类型 | **多主从、广播式**(所有节点同时接收) |
+| 物理层 | 差分信号(CAN_H / CAN_L),抗干扰强 |
+| 速率 | 最高 1Mbps(40m 以内) |
+| 距离 | 5Kbps 时可达 10km |
+| 仲裁 | 无损位仲裁(ID 小的优先级高) |
+| 可靠性 | CRC 校验 + 错误重传 + 错误节点自动脱离 |
+
+> **参考**:https://www.kvaser.cn/about-can/can-protocol-tutorial/(CAN 协议教程中文版)
+
+### 物理层 — 差分信号
+
+```
+CAN_H = 3.5V   CAN_L = 1.5V  →  显性 = 逻辑 0
+CAN_H = 2.5V   CAN_L = 2.5V  →  隐性 = 逻辑 1
+
+显性电平会"覆盖"隐性电平(仲裁机制的基础)
+```
+
+总线两端需要各接一个 **120Ω 终端电阻**(匹配阻抗,防止信号反射)。
+
+### 协议层 — 5 种帧类型
+
+| 帧类型 | 用途 | 软件控制? |
+|--------|------|-----------|
+| **数据帧** | 发送数据 | ✅ |
+| **远程帧** | 请求具有相同 ID 的节点发送数据 | ✅ |
+| 错误帧 | 检测到错误时通知所有节点 | ❌ 硬件自动 |
+| 过载帧 | 接收方未准备好时延迟发送 | ❌ 硬件自动 |
+| 帧间隔 | 分隔数据帧/远程帧 | ❌ 硬件自动 |
+
+### 数据帧结构(标准格式 11 位 ID)
+
+```
+┌──┬──────────┬───┬───┬──┬────┬──────────┬─────────────┬───────┬──┬──────────┐
+│SOF│ ARBITRATION │CTRL│DATA(0~64)│    CRC    │ACK│    EOF   │
+│ 1 │  ID 11+RTR1│ 6  │  0~8B   │ 15+1(del)│ 2  │    7     │
+└──┴──────────┴───┴───┴──┴────┴──────────┴─────────────┴───────┴──┴──────────┘
+```
+
+**逐段详解**:
+
+| 字段 | 位数 | 说明 |
+|------|------|------|
+| **SOF** | 1 | 帧起始,显性电平(0),同步所有节点 |
+| **ID** | 11 | 标识符,决定优先级(值越小优先级越高) |
+| **RTR** | 1 | 0=数据帧,1=远程帧 |
+| **IDE** | 1 | 0=标准格式(11位ID),1=扩展格式(29位ID) |
+| **r0** | 1 | 保留位,显性 |
+| **DLC** | 4 | 数据长度(0~8),单位字节 |
+| **DATA** | 0~64 | 实际数据(0~8 字节,DLC 指定长度) |
+| **CRC** | 15 | 循环冗余校验(含 CRC 界定符 1 位) |
+| **ACK** | 2 | 应答:ACK 槽 + ACK 界定符。接收节点在 ACK 槽拉低(显性)= 正常收到 |
+| **EOF** | 7 | 帧结束,全部隐性 |
+
+**扩展格式**:29 位 ID(11 位基本 ID + 18 位扩展 ID),IDE=1。
+
+### CAN 总线仲裁
+
+**仲裁原则**:谁先发 SOF → 谁先发 ID → ID 小的持续发显性(0) → ID 大的在隐性位(1)时丢失仲裁 → 自动转为接收。
+
+**示例(两个节点同时发送)**:
+```
+节点A: ID = 0x066  (二进制 000 0110 0110)
+节点B: ID = 0x077  (二进制 000 0111 0111)
+
+仲裁过程:
+  位1-5: A=00001, B=00001 → 相同,继续
+  位6:   A=1(隐性), B=1(隐性) → 相同,继续
+  位7:   A=0(显性), B=1(隐性) → A 输出显性,B 输出隐性
+         总线被 A 拉为显性 → B 读到总线和自身不同 → B 失去仲裁
+         立即停止发送,转为接收
+
+结果: 节点A 胜出,继续发送。节点B 等总线空闲自动重试。
+```
+
+### CAN 的位时序
+
+```
+1 位 = Tq × (SYNC_SEG + BS1 + BS2)
+     = Tq × (1 + TS1[3:0] + TS2[2:0])
+```
+
+STM32 把传播时间段合并到 BS1:
+
+```
+     ┌─────── SYNC_SEG ──────┐
+     │   BS1(传播+相位缓冲1)  │
+     │       BS2             │
+     └───────────────────────┘
+```
+
+**波特率计算**:
+```
+Baud = 1 / 位时间 = fAPB / BRP / (1 + BS1 + BS2)
+
+fAPB = 36MHz (CAN1 在 APB1)
+BRP = 35 → Tq = (35+1) / 36MHz = 1us
+BS1 = 3 Tq, BS2 = 5 Tq → 位 = (1+3+5) × 1us = 9us
+Baud = 1/9us ≈ 111Kbps
+```
+
+---
+
+## STM32 bxCAN 外设
+
+bxCAN = Basic Extended CAN,支持 CAN 2.0A(标准帧)和 2.0B Active(标准+扩展帧)。
+
+### 主要特性
+
+| 特性 | 值 |
+|------|-----|
+| 支持标准 | CAN 2.0A + 2.0B Active |
+| 最高速率 | 1Mbps |
+| **发送邮箱** | **3 个**(每个可缓存 1 帧) |
+| **接收 FIFO** | **2 个**(每个 3 级深度,共 6 帧缓存) |
+| **过滤器组** | **14 个**(0~13) |
+| DMA | ❌ 不支持 |
+| 自动重发 | ✅ 可配置 |
+
+### bxCAN 功能框图
+
+```
+主动内核(配置/控制/状态寄存器)
+    ↓
+发送邮箱[0~2] → CAN 内核(发送调度) → Tx 引脚
+                                       ↓
+接收滤波器(14组) ← CAN 内核(接收) ← Rx 引脚
+    ↓
+接收 FIFO[0~1] (各3级深度) → CPU 通过寄存器读取
+```
+
+### 3 种工作模式
+
+| 模式 | 如何进入 | 特点 |
+|------|---------|------|
+| **初始化** | MCR.INRQ=1, 等待 MSR.INAK=1 | 可配置寄存器(波特率、过滤器等) |
+| **正常** | MCR.INRQ=0, 等待 MSR.INAK=0 | 正常收发 |
+| **睡眠** | MCR.SLEEP=1 | 低功耗,检测到 CAN 总线活动自动唤醒 |
+
+### 3 种测试模式
+
+| 模式 | BTR 配置 | 用途 |
+|------|---------|------|
+| **静默** | SILM=1 | Rx 正常 → 可监听总线但不发送("只听不说的第三者") |
+| **环回** | LBKM=1 | Tx 内部回连到 Rx → 自收发不影响总线 |
+| **环回静默** | SILM=1 + LBKM=1 | 自收发且不影响总线(推荐调试用) |
+
+### 接收过滤器
+
+14 个过滤器组(0~13),每组由 2 个 32 位寄存器(FR1, FR2)组成。
+
+| 模式 | 说明 | 类比 |
+|------|------|------|
+| **掩码模式**(32位) | FR1=ID, FR2=掩码。掩码位=1 必须匹配,=0 不关心 | 模糊搜索 |
+| **列表模式**(32位) | FR1 和 FR2 各存一个 ID,精确匹配 | 白名单 |
+| **掩码模式**(16位) | FR1 存 2 个 ID,FR2 存 2 个掩码 | 双通道模糊搜索 |
+| **列表模式**(16位) | FR1 和 FR2 各存 2 个 ID,共 4 个精确匹配 | 4 个白名单 |
+
+> **参考**:参考手册 §22(bxCAN 寄存器描述)
+
+---
+
+## 软件设计(寄存器版 — 环回静默测试)
+
+**项目路径**:`stm32/stm32/01_can_test_register`
+
+**文件:`stm32/stm32/01_can_test_register/Hardware/CAN/can.h`**
+
+```c
+#ifndef __CAN_H
+#define __CAN_H
+
+#include "stm32f10x.h"
+
+// 接收数据结构
+typedef struct {
+    uint16_t stdID;       // 标准 ID
+    uint8_t  data[8];     // 数据
+    uint8_t  len;         // 数据长度
+} RxMsg;
+
+void CAN_Init(void);
+void CAN_SendMsg(uint16_t stdID, uint8_t *data, uint8_t len);
+void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t *msgCount);
+
+#endif
+```
+
+**文件:`stm32/stm32/01_can_test_register/Hardware/CAN/can.c`**
+
+```c
+#include "can.h"
+
+// 过滤器配置(内部函数)
+static void CAN_FilterConfig(void);
+
+// CAN 初始化(环回静默模式,125Kbps)
+void CAN_Init(void)
+{
+    /* ========== 1. 开启时钟 ========== */
+    // CAN1 挂载在 APB1 总线上(36MHz)
+    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;      // |=: CAN1时钟使能
+    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;       // |=: GPIOB时钟
+    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;       // |=: AFIO时钟(重映射用)
+
+    /* ========== 2. 引脚重映射(PB8=RX, PB9=TX) ========== */
+    // CAN 默认引脚是 PA11(RX)/PA12(TX)
+    // 开发板使用重映射到 PB8(RX)/PB9(TX)
+    // MAPR[25:24] = 10 → CAN 重映射到 PB8/PB9
+    AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_1;      // |=: 置位REMAP[1]
+    AFIO->MAPR &= ~AFIO_MAPR_CAN_REMAP_0;     // &=~: 清零REMAP[0]
+
+    /* ========== 3. GPIO 模式 ========== */
+    // PB8(CAN_RX): 浮空输入 MODE=00, CNF=01
+    GPIOB->CRH &= ~GPIO_CRH_MODE8;            // MODE8=00(输入)
+    GPIOB->CRH &= ~GPIO_CRH_CNF8_1;           // CNF8[1]=0
+    GPIOB->CRH |= GPIO_CRH_CNF8_0;            // CNF8[0]=1 → 浮空输入
+
+    // PB9(CAN_TX): 复用推挽输出 MODE=11, CNF=10
+    GPIOB->CRH |= GPIO_CRH_MODE9;             // MODE9=11(50MHz输出)
+    GPIOB->CRH |= GPIO_CRH_CNF9_1;            // CNF9[1]=1
+    GPIOB->CRH &= ~GPIO_CRH_CNF9_0;           // CNF9[0]=0 → 复用推挽
+
+    /* ========== 4. CAN 初始化 ========== */
+    // 4.1 进入初始化模式(INRQ=1, 等待 INAK=1)
+    CAN1->MCR |= CAN_MCR_INRQ;               // |=: INRQ位置1
+    while ((CAN1->MSR & CAN_MSR_INAK) == 0)  // 等待INAK=1确认
+    {}
+
+    // 4.2 退出睡眠模式(SLEEP=0, 等待 SLAK=0)
+    CAN1->MCR &= ~CAN_MCR_SLEEP;             // &=~: SLEEP位清0
+    while ((CAN1->MSR & CAN_MSR_SLAK) != 0)  // 等待SLAK=0
+    {}
+
+    // 4.3 自动离线管理
+    CAN1->MCR |= CAN_MCR_ABOM;               // ABOM=1: 错误节点自动恢复
+
+    // 4.4 自动唤醒管理
+    CAN1->MCR |= CAN_MCR_AWUM;               // AWUM=1: 检测总线活动自动唤醒
+
+    /* ========== 5. 配置位时序 ========== */
+    // 5.1 测试模式:环回 + 静默
+    CAN1->BTR |= CAN_BTR_SILM;               // SILM=1: 静默模式
+    CAN1->BTR |= CAN_BTR_LBKM;               // LBKM=1: 环回模式
+
+    // 5.2 波特率: APB1=36MHz, BRP=35 → Tq=1us
+    // BS1=3, BS2=5 → 位时间=1+3+5=9us → Baud≈111Kbps
+    CAN1->BTR &= ~CAN_BTR_BRP;               // 清除BRP位域
+    CAN1->BTR |= (35 << 0);                   // BRP[9:0]=35
+    CAN1->BTR &= ~CAN_BTR_TS1;               // 清除TS1位域
+    CAN1->BTR |= (3 << 16);                   // TS1[3:0]=3 (BS1=4Tq)
+    CAN1->BTR &= ~CAN_BTR_TS2;               // 清除TS2位域
+    CAN1->BTR |= (5 << 20);                   // TS2[2:0]=5 (BS2=6Tq)
+    CAN1->BTR &= ~CAN_BTR_SJW;               // 清除SJW位域
+    CAN1->BTR |= (1 << 24);                   // SJW[1:0]=1
+
+    // 5.3 退出初始化模式,进入正常模式
+    CAN1->MCR &= ~CAN_MCR_INRQ;              // &=~: INRQ位清0
+    while ((CAN1->MSR & CAN_MSR_INAK) != 0)  // 等待INAK=0确认
+    {}
+
+    /* ========== 6. 过滤器配置 ========== */
+    CAN_FilterConfig();
+}
+
+static void CAN_FilterConfig(void)
+{
+    // 1. 进入过滤器初始化模式
+    CAN1->FMR |= CAN_FMR_FINIT;              // FINIT=1: 允许配置过滤器
+
+    // 2. 过滤器0: 掩码模式(32位)
+    CAN1->FM1R &= ~CAN_FM1R_FBM0;            // FBM0=0: 掩码模式
+
+    // 3. 过滤器位宽: 32位
+    CAN1->FS1R |= CAN_FS1R_FSC0;             // FSC0=1: 32位宽
+
+    // 4. 关联 FIFO: FIFO0
+    CAN1->FFA1R &= ~CAN_FFA1R_FFA0;          // FFA0=0: 匹配帧→FIFO0
+
+    // 5. 过滤器ID (FR1) = 0x06e ← 只接收 ID=0x06e 的帧
+    CAN1->sFilterRegister[0].FR1 = 0x06e << 21;
+
+    // 6. 过滤器掩码 (FR2) = 0x7f1 ← 高11位全部必须匹配
+    CAN1->sFilterRegister[0].FR2 = 0x7f1 << 21;
+
+    // 7. 激活过滤器0
+    CAN1->FA1R |= CAN_FA1R_FACT0;            // FACT0=1: 激活
+
+    // 8. 退出过滤器初始化模式
+    CAN1->FMR &= ~CAN_FMR_FINIT;
+}
+
+// 发送报文(使用发送邮箱0)
+void CAN_SendMsg(uint16_t stdID, uint8_t *data, uint8_t len)
+{
+    // 1. 等待邮箱0为空
+    while ((CAN1->TSR & CAN_TSR_TME0) == 0)  // TME0=1 表示空
+    {}
+
+    // 2. 写入 ID
+    CAN1->sTxMailBox[0].TIR &= ~CAN_TI0R_STID;       // 清除STID位域
+    CAN1->sTxMailBox[0].TIR |= (uint32_t)stdID << 21; // 写入11位标准ID
+
+    // 3. 写入 DLC(数据长度)
+    CAN1->sTxMailBox[0].TDTR &= ~CAN_TDT0R_DLC;       // 清除DLC
+    CAN1->sTxMailBox[0].TDTR |= len;                   // 写入长度
+
+    // 4. 写入数据(最多8字节)
+    for (uint8_t i = 0; i < len && i < 8; i++)
+    {
+        CAN1->sTxMailBox[0].TDLR = data[0];   // 低32位寄存器存data[0~3]
+        if (len > 4)                           // 超过4字节的存入高32位
+            CAN1->sTxMailBox[0].TDHR = *(uint32_t *)&data[4];
+    }
+
+    // 5. 请求发送(TXRQ=1)
+    CAN1->sTxMailBox[0].TIR |= CAN_TI0R_TXRQ;
+}
+```
+
+---
+
+## 核心速查表
+
+| 操作 | 寄存器操作 |
+|------|----------|
+| 初始化模式 | `MCR |= INRQ; while(!MSR.INAK)` |
+| 退出初始化 | `MCR &= ~INRQ; while(MSR.INAK);` |
+| 设置波特率 | `BTR = BRP + (TS1<<16) + (TS2<<20) + (SJW<<24)` |
+| 环回静默 | `BTR |= SILM + LBKM` |
+| 发送 | `TIR = ID<<21 + TXRQ` + `TDTxR = DLC` + `TDxR = data` |
+| 接收 | 读 `sFIFOMailBox[0]` 的 `TIR/TDTxR/TDxR` |
+| 配置过滤器 | `FINIT=1` → 设 FR1/FR2 → `FINIT=0` |
+
+## 常见问题与避坑
+
+1. **CAN 初始化卡死在 INAK 等待** → 检查 CAN1 时钟是否开启(APB1ENR.CAN1EN)、引脚配置是否正确
+2. **环回正常但双机不通信** → 检查终端电阻(120Ω×2)、波特率一致、退出环回模式(清除 SILM/LBKM)
+3. **过滤器不生效** → 必须在 FINIT=1 模式下配置过滤器,激活后 FINIT=0
+4. **发送不出去** → 3 个邮箱全满(检查 TSR.TME[2:0])、未请求发送(TXRQ=0)
+5. **接收不到数据** → FIFO 溢出(RFOMR.FOVR)、过滤器未匹配、中断未使能
+6. **CAN 错误状态** → 查询 ESR(错误状态寄存器),REC/TEC 计数(>127 进入错误被动,>255 脱离总线)

+ 253 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/14-以太网通信与W5500.md

@@ -0,0 +1,253 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — 第2章:以太网 + 配套代码05~09"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# 以太网通信与W5500
+
+> **用生活理解**:W5500 就像一个自带"同声传译"的网卡芯片——你只需要用 SPI 告诉它"我要连哪台机器、哪个端口",它自己就把 TCP/IP 协议栈这些复杂工作全干了。你的单片机完全不用关心三次握手、重传、分片这些事,只管发/收数据就好。
+
+---
+
+## 互联网基础
+
+### OSI 7 层模型 vs TCP/IP 4 层模型
+
+| OSI 7 层 | TCP/IP 4 层 | 代表协议 | 功能 |
+|----------|------------|---------|------|
+| 应用层 | **应用层** | HTTP, FTP, MQTT, DNS | 为用户提供网络应用服务 |
+| 表示层 | ↓ | SSL/TLS | 数据加密/解密、压缩 |
+| 会话层 | ↓ | SOCKS | 建立/管理/终止会话 |
+| 传输层 | **传输层** | **TCP, UDP** | 端到端通信、可靠传输 |
+| 网络层 | **网络层** | **IP, ICMP, ARP** | 路由寻址、分组转发 |
+| 数据链路层 | **网络接口层** | Ethernet, MAC | 帧封装、介质访问 |
+| 物理层 | ↓ | 以太网物理层 | 比特流传输、信号编码 |
+
+### TCP vs UDP
+
+| 特性 | TCP | UDP |
+|------|-----|-----|
+| 连接 | 面向连接(三次握手) | 无连接 |
+| 可靠性 | 可靠(确认重传) | 不可靠(最大努力交付) |
+| 顺序 | 按序交付 | 不保证顺序 |
+| 速度 | 较慢 | 快速 |
+| 适用场景 | Web、文件传输、邮件 | 视频流、DNS、IoT 传感器数据 |
+
+### 常见协议
+
+| 协议 | 端口 | 功能 |
+|------|------|------|
+| HTTP | 80 | Web 网页访问 |
+| HTTPS | 443 | 加密 Web 访问 |
+| DNS | 53 | 域名→IP 解析 |
+| DHCP | 67/68 | 自动分配 IP 地址 |
+| MQTT | 1883 | IoT 轻量级消息协议 |
+
+---
+
+## W5500 芯片详解
+
+W5500 是韩国 WIZnet 公司生产的**全硬件 TCP/IP 协议栈**芯片。不需要在单片机端运行 TCP/IP 协议栈——W5500 内部硬件实现。
+
+| 特性 | 值 |
+|------|-----|
+| **接口** | **SPI**(最高 80MHz,可变长数据帧模式) |
+| **协议** | TCP, UDP, ICMP, IPv4, ARP, IGMP, PPPoE |
+| **Socket** | **8 个独立 Socket**(同时支持 8 个连接) |
+| **内部 RAM** | 32KB(Tx/Rx 可分配给各 Socket) |
+| **MAC + PHY** | 内置(10/100Mbps 自适应) |
+| **封装** | LQFP48 |
+
+> **参考**:W5500 数据手册(WIZnet 官网)
+
+### W5500 接入框图
+
+```
+STM32F103ZET6                           W5500
+┌──────────────┐                      ┌──────────┐    ┌──────┐
+│ SPI2_SCK     ├────── PB13(SCK) ────→│ SCLK     │    │RJ45 │
+│ SPI2_MOSI    ├────── PB15(MOSI) ───→│ MOSI     ├────┤带有 │
+│ SPI2_MISO    ├────── PB14(MISO) ←──┤ MISO     │    │网络 │
+│ PD3(CS)      ├────────────────────→│ nSCS     │    │变压 │
+│ PG6(INT)     │←────────────────────┤ INTn     │    │器  │
+│ PG7(RST)     ├────────────────────→│ RSTn     │    └──────┘
+│              │                      │          │
+│ SPI2         │                      │内置MAC+PHY│
+└──────────────┘                      └──────────┘
+```
+
+### W5500 内部架构
+
+```
+SPI 接口 ←→ 寄存器管理单元 ←→ 通用寄存器(模式/MAC/IP等)
+                          ←→ Socket 寄存器(8个Socket,各512字节)
+                          ←→ Tx/Rx 存储器(32KB 可分配)
+                          ←→ TCP/IP 内核(硬件实现)
+                                  ↓
+                               MAC + PHY ←→ 以太网口
+```
+
+### W5500 SPI 可变长数据帧模式
+
+W5500 的 SPI 数据帧与普通 SPI Flash 不同,分为**控制字节 + 数据**:
+
+```
+帧格式: [地址偏移(1B)] [控制字节(1B)] [数据(N字节)]
+
+控制字节:
+  [7:5] = 块选择 (000=通用寄存器, 001=Socket寄存器, 010=Tx缓冲区, 011=Rx缓冲区)
+  [4:2] = 读写模式
+  [1]   = 可变长模式 1=VDM (推荐)
+  [0]   = 1=读, 0=写
+```
+
+### W5500 官方库移植步骤
+
+1. 从 WIZnet 官网下载 W5500 官方库
+2. 复制 `Ethernet/` 目录到项目
+3. 修改 `wizchip_conf.h` 中的 SPI 接口函数
+4. 实现以下底层函数:
+   ```c
+   void SPI_CS_Select(void);      // 芯片选择
+   void SPI_CS_Deselect(void);    // 取消片选
+   uint8_t SPI_ReadWrite(uint8_t byte); // SPI 读写1字节
+   ```
+5. 调用 `wizchip_init()` 和 `wizchip_setnetinfo()` 初始化
+
+---
+
+## 实验:W5500 TCP Server
+
+### 软件设计
+
+**项目路径**:`stm32/05_ethernet_test_register` ~ `stm32/09_ethernet_webserver_register`
+
+**文件:`stm32/05_ethernet_test_register/User/spi.c`**(SPI 初始化)
+
+```c
+#include "spi.h"
+
+// W5500 使用 STM32 SPI2:PB13(SCK), PB14(MISO), PB15(MOSI)
+void SPI_Init(void)
+{
+    RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;       // SPI2 在 APB1
+    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
+
+    // PB13(SCK) 复用推挽, PB14(MISO) 浮空输入, PB15(MOSI) 复用推挽
+    GPIOB->CRH |= GPIO_CRH_CNF13_1;            // PB13 复用: CNF=10
+    GPIOB->CRH |= GPIO_CRH_MODE13;             // MODE=11(50MHz)
+    GPIOB->CRH &= ~GPIO_CRH_CNF14;             // PB14 浮空: CNF=01
+    GPIOB->CRH |= GPIO_CRH_CNF14_0;
+    GPIOB->CRH |= GPIO_CRH_CNF15_1;            // PB15 复用: CNF=10
+    GPIOB->CRH |= GPIO_CRH_MODE15;
+
+    // SPI2 配置:模式0(CPOL=0,CPHA=0), 主机, 分频=8 → 4.5MHz
+    SPI2->CR1 = 0;                              // 复位配置
+    SPI2->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI;   // 软件 NSS
+    SPI2->CR1 |= SPI_CR1_MSTR;                 // 主机模式
+    SPI2->CR1 |= SPI_CR1_BR_0 | SPI_CR1_BR_1; // BR=011: PCLK/16=2.25MHz
+    SPI2->CR1 |= SPI_CR1_SPE;                  // SPI 使能
+}
+
+uint8_t SPI_ReadWrite(uint8_t byte)
+{
+    while (!(SPI2->SR & SPI_SR_TXE)) {}
+    SPI2->DR = byte;
+    while (!(SPI2->SR & SPI_SR_RXNE)) {}
+    return SPI2->DR;
+}
+```
+
+**文件:`stm32/09_ethernet_webserver_register/User/App_ETH.c`**(W5500 初始化与应用)
+
+```c
+#include "w5500/wizchip_conf.h"
+#include "w5500/socket.h"
+
+// 网络配置
+wiz_NetInfo netInfo = {
+    .mac = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56},  // MAC 地址
+    .ip  = {192, 168, 1, 100},                      // IP 地址
+    .sn  = {255, 255, 255, 0},                      // 子网掩码
+    .gw  = {192, 168, 1, 1},                        // 默认网关
+    .dns = {8, 8, 8, 8},                            // DNS 服务器
+};
+
+void W5500_Init(void)
+{
+    // 1. 硬件复位 W5500
+    RSTn_LOW;  Delay_ms(1);
+    RSTn_HIGH; Delay_ms(10);
+
+    // 2. 注册 SPI 通信函数
+    reg_wizchip_cs_cbfunc(SPI_CS_Select, SPI_CS_Deselect);
+    reg_wizchip_spi_cbfunc(SPI_ReadWrite);
+
+    // 3. W5500 初始化
+    uint8_t tmp;
+    wizchip_init(NULL, NULL);
+
+    // 4. 配置网络参数
+    wizchip_setnetinfo(&netInfo);
+
+    // 5. 配置 WOL 和 PPPoE(默认关闭)
+}
+
+// 创建 TCP Server,监听 80 端口
+void HTTP_Server_Init(void)
+{
+    int32_t sock = socket(0, Sn_MR_TCP, 80, 0);  // Socket0, TCP, 80端口
+    if (sock < 0) { printf("Socket create failed\r\n"); return; }
+
+    listen(sock);                                   // 开始监听
+    printf("HTTP Server listening on port 80\r\n");
+
+    while (1)
+    {
+        int32_t client = accept(sock);              // 等待客户端连接
+        if (client >= 0)
+        {
+            printf("Client connected\r\n");
+            uint8_t buffer[1460];
+            int32_t len = recv(client, buffer, sizeof(buffer));  // 接收 HTTP 请求
+
+            // 回复 HTTP 响应
+            char *response = "HTTP/1.1 200 OK\r\n"
+                             "Content-Type: text/html\r\n\r\n"
+                             "<h1>Hello from STM32 + W5500!</h1>";
+            send(client, (uint8_t *)response, strlen(response));
+
+            close(client);                           // 关闭连接
+        }
+    }
+}
+```
+
+---
+
+## 核心函数速查表(W5500 Socket API)
+
+| 函数 | 说明 |
+|------|------|
+| `socket(sn, protocol, port, flag)` | 创建 Socket,返回句柄(-1 失败) |
+| `close(sn)` | 关闭 Socket |
+| `listen(sn)` | TCP Server 开始监听 |
+| `connect(sn, ip, port)` | TCP Client 连接远端 |
+| `accept(sn)` | 接受连接(返回新 Socket 句柄) |
+| `send(sn, data, len)` | 发送数据 |
+| `recv(sn, buffer)` | 接收数据(返回实际接收长度) |
+| `setsockopt(sn, option, value)` | 设置 Socket 选项 |
+| `getsockopt(sn, option)` | 获取 Socket 状态 |
+
+## 常见问题与避坑
+
+1. **W5500 不工作** → 确认 SPI 通信(示波器看 SCK/MOSI)、检查晶振(25MHz)是否起振
+2. **Ping 不通** → 检查 IP/子网掩码/网关配置、检查以太网物理层(RJ45 链接灯亮?)
+3. **TCP 连接频繁断开** → W5500 在无数据时可能进入节能模式,需定期发送心跳包
+4. **W5500 官方库移植注意** → 必须实现 `SPI_CS_Select/Deselect` 和 `SPI_ReadWrite`,并注册到库中
+5. **Socket 资源不足** → W5500 最多 8 个 Socket,每个需要分配 Tx/Rx 缓冲区,注意 32KB 的总量分配
+6. **HTTP 响应只发一次** → HTTP/1.1 默认为 keep-alive,如需短连接在响应头加 `Connection: close`

+ 227 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/15-WiFi通信与ESP32-C3.md

@@ -0,0 +1,227 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — WiFi章节 + 配套代码10~11"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# WiFi通信与ESP32-C3
+
+> **用生活理解**:ESP32-C3 就像一个随身 WiFi 翻译官——你的单片机不会说 WiFi,但 ESP32-C3 会。你用串口告诉它:"连这个路由器"、"发这串数据到这个 IP",它帮你搞定所有 WiFi 通信。AT 指令就是你和翻译官之间的暗号手册。
+
+---
+
+## ESP32-C3 简介
+
+ESP32-C3 是乐鑫科技(Espressif)推出的**RISC-V 32 位**WiFi + BLE 5.0 芯片。
+
+### 核心规格
+
+| 特性 | 值 |
+|------|-----|
+| **内核** | RISC-V 32 位单核,最高 160MHz |
+| **WiFi** | 802.11 b/g/n, 2.4GHz(仅 2.4GHz,不支持 5GHz) |
+| **BLE** | Bluetooth 5.0 (LE) |
+| **RAM** | 400KB SRAM |
+| **Flash** | 4MB |
+| **接口** | UART、SPI、I2C、I2S、PWM、ADC |
+| **工作模式** | Station(连接路由器)/ SoftAP(自建热点)/ Station+AP |
+
+> **参考**:乐鑫 ESP32-C3 数据手册、ESP32-C3 AT 指令集用户手册
+> **注意**:ESP32-C3 与 ESP8266 不兼容(不同内核、不同指令集)
+
+### AT 指令工作模式
+
+ESP32-C3 默认出厂固件为**AT 指令模式**——芯片从 Flash 启动后,通过串口接收 AT 指令并返回响应。
+
+**通信方式**:
+```
+STM32(USART) ── TX ──→ RX(ESP32-C3)
+             ── RX ←── TX(ESP32-C3)
+             ── GND ── GND
+```
+
+**响应格式**:每条指令执行后返回 `\r\nOK\r\n` 或 `\r\nERROR\r\n`
+
+---
+
+## AT 指令集详解
+
+### 基础测试指令
+
+| AT 指令 | 预期响应 | 说明 |
+|---------|---------|------|
+| `AT\r\n` | `OK` | 测试连接是否正常 |
+| `AT+RST\r\n` | `ready` | 软复位模块 |
+| `AT+GMR\r\n` | 版本信息 | 查看 AT 固件版本 |
+| `ATE0\r\n` | `OK` | 关闭回显(默认开启,ATE1=开) |
+| `AT+UART_CUR=115200,8,1,0,0\r\n` | `OK` | 设置当前串口参数(波特率/数据位/停止位/校验/流控) |
+
+### WiFi 操作指令
+
+| AT 指令 | 说明 | 响应 |
+|---------|------|------|
+| `AT+CWMODE=1\r\n` | Station 模式(连路由器) | `OK` |
+| `AT+CWMODE=2\r\n` | SoftAP 模式(自建热点) | `OK` |
+| `AT+CWMODE=3\r\n` | Station + AP 模式 | `OK` |
+| `AT+CWJAP="SSID","password"\r\n` | 连接指定 WiFi | `WIFI CONNECTED`/`WIFI DISCONNECT` |
+| `AT+CWQAP\r\n` | 断开当前 WiFi | `OK` |
+| `AT+CWLAP\r\n` | 扫描附近 WiFi 列表 | `+CWLAP:enc,auth,ssid,rssi,mac` |
+| `AT+CIFSR\r\n` | 查询本地 IP 地址 | `+CIFSR:STAIP,"192.168.x.x"` |
+
+### TCP/UDP 通信指令
+
+| AT 指令 | 说明 | 响应 |
+|---------|------|------|
+| `AT+CIPSTART="TCP","ip",port\r\n` | 建立 TCP 连接 | `CONNECT` |
+| `AT+CIPSTART="UDP","ip",port\r\n` | 建立 UDP 连接 | `CONNECT` |
+| `AT+CIPMODE=1\r\n` | 开启**透传模式** | `OK` |
+| `AT+CIPSEND\r\n` | 透传模式下开始发送(出现 `>` 提示符) | `>` |
+| `AT+CIPSEND=len\r\n` | 发送指定长度的数据(非透传) | `>` 后发数据 |
+| `AT+CIPSTATUS\r\n` | 查询连接状态 | `STATUS:3`(已连接)/`STATUS:4`(已断开) |
+| `AT+CIPCLOSE\r\n` | 关闭当前连接 | `CLOSED` |
+| `AT+CIPMUX=0\r\n` | 单连接模式 | `OK` |
+| `AT+CIPMUX=1\r\n` | 多连接模式(最多 5 个) | `OK` |
+
+### 透传模式详解
+
+透传模式(Transparent Transmission)下,进入 `AT+CIPSEND` 后出现的 `>` 提示符,此后所有串口输入数据**自动转发**到 TCP 连接,收到的 TCP 数据也自动从串口输出。
+
+**退出透传模式**:**在 20ms 内没有数据的时间间隔后**,发送 `+++`(不需要换行回车),可退出透传返回 AT 指令模式。
+
+```
+非透传模式(CIPMODE=0):
+  AT+CIPSEND=5\r\n       → 发送指令
+  >                       → 提示符
+  Hello                   → 发送 5 字节
+  SEND OK                 → 完成
+
+透传模式(CIPMODE=1):
+  AT+CIPMODE=1\r\n
+  OK
+  AT+CIPSEND\r\n
+  >                        → 进入透传
+  Hello World...           → 所有串口数据直接变为 TCP 数据发送
+  +++(间隔20ms)            → 退出透传
+```
+
+---
+
+## 实验:ESP32-C3 TCP 通信
+
+**项目路径**:`stm32/10_wifi_test_hal`、`stm32/11_wifi_tcp_server_hal`
+
+### 软件设计(HAL 库版)
+
+```c
+#include "usart.h"
+#include <string.h>
+#include <stdio.h>
+
+// 串口接收缓冲区
+uint8_t rx_buffer[512];
+uint8_t rx_len = 0;
+
+// 发送 AT 指令并等待期望响应
+// cmd: AT 指令字符串(以 \r\n 结尾)
+// expect: 期望响应中的关键字(如 "OK", "CONNECT", "WIFI GOT IP")
+// timeout: 超时 ms
+uint8_t AT_SendCmd(char *cmd, char *expect, uint32_t timeout)
+{
+    // 清空接收缓冲区
+    memset(rx_buffer, 0, sizeof(rx_buffer));
+    rx_len = 0;
+
+    // 发送 AT 指令
+    HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), 1000);
+
+    // 等待响应
+    uint32_t start = HAL_GetTick();
+    while (HAL_GetTick() - start < timeout)
+    {
+        uint8_t ch;
+        if (HAL_UART_Receive(&huart2, &ch, 1, 10) == HAL_OK)
+        {
+            rx_buffer[rx_len++] = ch;
+            rx_buffer[rx_len] = '\0';
+
+            // 检查是否包含期望响应
+            if (strstr((char *)rx_buffer, expect))
+            {
+                return 1;  // 成功
+            }
+        }
+    }
+    return 0;  // 超时
+}
+
+// WiFi + TCP 通信完整流程
+void WiFi_TCP_Test(void)
+{
+    /* ===== 阶段1: 测试 AT 连接 ===== */
+    if (!AT_SendCmd("AT\r\n", "OK", 2000)) {
+        printf("ESP32 not responding!\r\n");
+        return;
+    }
+
+    /* ===== 阶段2: 设置 Station 模式 ===== */
+    AT_SendCmd("AT+CWMODE=1\r\n", "OK", 1000);
+
+    /* ===== 阶段3: 连接 WiFi ===== */
+    printf("Connecting to WiFi...\r\n");
+    if (!AT_SendCmd("AT+CWJAP=\"MyWiFi\",\"password123\"\r\n",
+                     "WIFI GOT IP", 15000)) {
+        printf("WiFi connect failed!\r\n");
+        return;
+    }
+    printf("WiFi connected!\r\n");
+
+    /* ===== 阶段4: 查询 IP ===== */
+    AT_SendCmd("AT+CIFSR\r\n", "OK", 1000);
+    printf("IP: %s\r\n", rx_buffer);
+
+    /* ===== 阶段5: 建立 TCP 连接 ===== */
+    if (!AT_SendCmd("AT+CIPSTART=\"TCP\",\"192.168.1.100\",8080\r\n",
+                     "CONNECT", 10000)) {
+        printf("TCP connect failed!\r\n");
+        return;
+    }
+    printf("TCP connected!\r\n");
+
+    /* ===== 阶段6: 发送数据 ===== */
+    AT_SendCmd("AT+CIPSEND=11\r\n", ">", 3000);
+    AT_SendCmd("Hello World", "SEND OK", 3000);
+    printf("Data sent!\r\n");
+
+    /* ===== 阶段7: 关闭连接 ===== */
+    AT_SendCmd("AT+CIPCLOSE\r\n", "CLOSED", 3000);
+}
+```
+
+---
+
+## 核心速查表
+
+| 操作 | AT 指令 | 等待响应 | 超时 |
+|------|---------|---------|------|
+| 测试连接 | `AT` | `OK` | 2s |
+| 设置 Station | `AT+CWMODE=1` | `OK` | 1s |
+| 扫描 WiFi | `AT+CWLAP` | `OK` | 10s |
+| 连接 WiFi | `AT+CWJAP="SSID","pwd"` | `WIFI GOT IP` | 15s |
+| 查询 IP | `AT+CIFSR` | `OK` | 2s |
+| TCP 连接 | `AT+CIPSTART="TCP",ip,port` | `CONNECT` | 10s |
+| 发送数据 | `AT+CIPSEND=len` → 数据 | `SEND OK` | 5s |
+| 进入透传 | `AT+CIPMODE=1` → `AT+CIPSEND` | `>` | 3s |
+| 退出透传 | `+++`(无换行,20ms 间隔) | `OK` | — |
+| 关闭连接 | `AT+CIPCLOSE` | `CLOSED` | 5s |
+
+## 常见问题与避坑
+
+1. **AT 指令无响应** → 检查波特率(确保一致)、TX↔RX 交叉连接、模块电源(峰值 500mA 需足够供电)
+2. **WiFi 连不上** → 仅支持 2.4GHz(不支持 5GHz)、检查 SSID 密码、AP 是否隐藏
+3. **TCP 连接失败** → 检查目标 IP 可达性、端口是否开放、防火墙规则
+4. **透传模式数据回显** → `ATE0` 关闭回显;否则发送的数据会同时从 ESP 返回
+5. **+++ 退不出透传** → 要求前后 20ms 无数据,保证足够间隔
+6. **数据长度错** → 非透传下,AT+CIPSEND=len 的 len 必须与后续发送的数据字节数严格一致

+ 179 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/16-BLE低功耗蓝牙.md

@@ -0,0 +1,179 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — BLE章节 + 配套代码12"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# BLE低功耗蓝牙
+
+> **用生活理解**:BLE 就像一台"静音待机"的对讲机——平时完全关闭喇叭省电(休眠),每隔几秒悄悄喊一声"我在这呢"(广播)。当有人听到广播后说"跟我连个线吧"(连接请求),通信就建立起来了。连上之后也不是一直说,而是每隔 30ms 说一句(连接间隔),不说话时极低功耗。
+
+---
+
+## BLE vs 经典蓝牙
+
+| 特性 | 经典蓝牙 (BR/EDR) | BLE (Bluetooth Low Energy) |
+|------|------------------|---------------------------|
+| 功耗 | ~1W(持续发送) | **~0.01W**(广播时 5~10mA,休眠时 μA 级) |
+| 峰值速率 | 1~3 Mbps | 125Kbps~2Mbps |
+| 连接延迟 | ~100ms | **~3ms** |
+| 数据包 | 长包(可达 339 字节) | 短包(最大 251 字节 ATT MTU) |
+| 信道 | 79 个 1MHz 信道 | **40 个 2MHz 信道**(3 个广播 + 37 个数据) |
+| 语音 | 支持(SCO 链路) | 不支持(需通过协议栈上层实现) |
+| 主要应用 | 音频流、文件传输 | **IoT、传感器、可穿戴设备** |
+
+> BLE 和经典蓝牙工作在相同 2.4GHz ISM 频段,但调制方式和信道划分不同,互不兼容。
+
+---
+
+## BLE 协议栈架构
+
+```
+Application(应用层)
+    ↓
+GAP (Generic Access Profile) — 广播、扫描、连接、角色
+GATT (Generic Attribute Profile) — 数据读写、服务/特征值结构
+    ↓
+ATT (Attribute Protocol) — 属性读/写/通知/指示
+L2CAP (Logical Link Control and Adaptation Protocol) — 数据分包/重组
+    ↓
+HCI (Host Controller Interface) — 主机与控制器通信接口
+    ↓
+Link Layer (链路层) — 广播/数据信道管理、加密
+Physical Layer (物理层) — 2.4GHz GFSK 调制
+```
+
+### GAP 角色模型
+
+| GAP 角色 | 行为 | 典型设备 |
+|----------|------|---------|
+| **Broadcaster** | 只发广播(不能连) | 信标(Beacon) |
+| **Observer** | 只扫描广播(不连) | 扫描仪 |
+| **Peripheral** | 广播 + 接受连接 | 传感器节点 |
+| **Central** | 扫描 + 发起连接 | 手机、网关 |
+
+**典型连接流程**:
+```
+Peripheral 广播 → Central 扫描到 → Central 发起连接请求
+→ 连接建立 → 双方按连接间隔周期性交互数据 → 一方断开
+```
+
+### GATT 数据架构
+
+BLE 的数据按以下层级组织:
+
+```
+Service (服务)         如 "心率服务" UUID: 0x180D
+├── Characteristic     如 "心率测量" UUID: 0x2A37
+│   ├── Properties       read / write / notify / indicate
+│   ├── Value            实际数据(心率值)
+│   └── Descriptor       配置描述(如 CCCD 使能 notify)
+└── Characteristic
+    ├── ...
+```
+
+**UUID(唯一标识符)**:
+- 16 位标准 UUID:由 Bluetooth SIG 定义(0x180D = 心率服务)
+- 128 位自定义 UUID:用户自定义服务/特征
+
+**GATT 角色**:
+- **GATT Server**:提供数据(如传感器)
+- **GATT Client**:读写数据(如手机 App)
+
+---
+
+## 实验:STM32 + BLE 模块通信
+
+STM32 通过串口连接 BLE 透传模块(支持 AT 指令),实现 BLE 数据收发。
+
+**项目路径**:`stm32/12_ble_server_hal`
+
+### 软件设计
+
+```c
+// BLE 模块初始化(Peripheral 模式 + 广播)
+void BLE_Init(void)
+{
+    // 1. 设置为 Peripheral(从机)模式
+    AT_SendCmd("AT+BLEINIT=2\r\n", "OK", 2000);
+    // AT+BLEINIT: 0=关闭BLE, 1=Central, 2=Peripheral
+
+    // 2. 设置设备名称(广播时显示)
+    AT_SendCmd("AT+BLENAME=\"STM32_BLE_Device\"\r\n", "OK", 1000);
+
+    // 3. 设置广播间隔(单位 0.625ms,默认 100 = 62.5ms)
+    AT_SendCmd("AT+BADVINT=100\r\n", "OK", 1000);
+    // 值越小 → 被发现越快 → 功耗越高
+
+    // 4. 设置广播数据(自定义数据)
+    AT_SendCmd("AT+BLEADVDATA=\"020106\"\r\n", "OK", 1000);
+    // 02=长度, 01=标志类型, 06=LE General Discoverable + BR/EDR Not Support
+
+    // 5. 启动广播
+    AT_SendCmd("AT+BLEADVSTART\r\n", "OK", 2000);
+    printf("BLE 广播中...\r\n");
+}
+
+// 发送数据(通过 GATT 通知)
+void BLE_SendData(uint8_t *data, uint16_t len)
+{
+    char cmd[64];
+    // 格式: AT+BLESEND=conn_id,len\r\n
+    // 之后发送 len 字节的数据
+    sprintf(cmd, "AT+BLESEND=0,%d\r\n", len);
+    AT_SendCmd(cmd, ">", 3000);
+
+    // 发送实际数据
+    HAL_UART_Transmit(&huart2, data, len, 1000);
+}
+
+// BLE 数据接收(串口主动上报)
+// 当手机发送数据给 STM32 时,BLE 模块通过串口上报:
+// +BLEDATA: conn_id,len,data
+void BLE_ProcessRxData(uint8_t *buffer, uint16_t len)
+{
+    if (strstr((char *)buffer, "+BLEDATA:"))
+    {
+        // 解析数据并处理
+        char *data_ptr = strstr((char *)buffer, ",");
+        if (data_ptr)
+        {
+            data_ptr = strstr(data_ptr + 1, ",");
+            if (data_ptr)
+            {
+                data_ptr++;  // 跳过逗号,指向实际数据
+                printf("Received BLE: %s\r\n", data_ptr);
+            }
+        }
+    }
+}
+```
+
+---
+
+## 核心速查表
+
+| BLE 操作 | AT 指令 | 说明 |
+|---------|---------|------|
+| 初始化(Peripheral) | `AT+BLEINIT=2` | 2=从机模式 |
+| 初始化(Central) | `AT+BLEINIT=1` | 1=主机模式 |
+| 设置名称 | `AT+BLENAME="name"` | 最长 29 字节 |
+| 设置广播间隔 | `AT+BADVINT=N` | N×0.625ms,默认 100 (62.5ms) |
+| 设置广播数据 | `AT+BLEADVDATA="hex"` | 16 进制字符串 |
+| 开始广播 | `AT+BLEADVSTART` | Peripheral 模式 |
+| 停止广播 | `AT+BLEADVSTOP` | — |
+| 发送数据 | `AT+BLESEND=conn,len` → 数据 | — |
+| 查询连接状态 | `AT+BLECONN?` | 返回当前连接 |
+| 断开连接 | `AT+BLEDISCONN=n` | n=连接 ID |
+| 扫描设备 | `AT+BLESCAN=0` | 0=被动扫描 |
+
+## 常见问题与避坑
+
+1. **手机扫描不到设备** → 检查广播是否启动、名称是否设置、广播功率是否太低
+2. **连接后数据发不出去** → 确保 GATT Server 已启动、通知/写属性已配置、MTU 大小(默认 23 字节)
+3. **功耗高** → 广播间隔增大(100ms+)、连接间隔增大(30ms+)、不用时停止广播
+4. **断开连接后需要显式重连** → BLE 不会自动重连,Peripheral 需重新调用 `AT+BLEADVSTART`
+5. **连接间隔与功耗的关系**:间隔越短→延迟越低→功耗越高;传感器场景推荐 30~50ms

+ 202 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/17-LoRa远距离通信.md

@@ -0,0 +1,202 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — LoRa章节 + 配套代码13~14"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# LoRa远距离通信
+
+> **用生活理解**:LoRa 就像"对讲机里的长跑冠军"——说话速度很慢(50bps),但声音能传出几公里,而且电池用几年不换。适合田野里的温度传感器每隔一小时报一次数。LoRa 是在水下吹气泡——气泡(信号)虽然升得慢,但能飘很远。
+
+---
+
+## LPWAN 技术概览
+
+LPWAN = Low Power Wide Area Network(低功耗广域网),是 IoT 中连接远距离、低功耗设备的无线技术。
+
+| 技术 | 频段 | 速率 | 优点 | 缺点 |
+|------|------|------|------|------|
+| **LoRa** | 470~510MHz(CN) / 868MHz(EU) / 915MHz(US) | 0.3~50Kbps | 私有化部署、成本低 | 速率低 |
+| NB-IoT | 运营商授权频段 | ~250Kbps | 覆盖广、运营商维护 | 需 SIM 卡、有月费 |
+| Sigfox | 868/915MHz | ~100bps | 超低功耗 | 速率极低、依赖 Sigfox 网络 |
+| LTE-M | 运营商授权频段 | ~1Mbps | 速率较高 | 模组贵、功耗较高 |
+
+---
+
+## LoRa 扩频技术原理
+
+LoRa 使用**CSS(Chirp Spread Spectrum,啁啾扩频)**调制。普通无线信号像"喊话"(功率集中在一小段频率),LoRa 像"吹口哨从低到高再从高到低"(信号散布在宽频带上)。
+
+### 关键参数
+
+| 参数 | 可取值范围 | 说明 | 对性能的影响 |
+|------|-----------|------|-------------|
+| **SF** (扩频因子) | 6~12 | 每个 Chirp 符号代表的位数 | SF↑ → 灵敏度↑ → 速率↓ |
+| **BW** (带宽) | 125 / 250 / 500 KHz | 调制信号的频率宽度 | BW↑ → 速率↑ → 灵敏度↓ |
+| **CR** (编码率) | 4/5 ~ 4/8 | 前向纠错(FEC)冗余度 | CR↑ → 抗干扰↑ → 冗余↑ |
+
+**典型参数速查表**:
+
+| SF | BW(KHz) | 速率(bps) | 灵敏度(dBm) | 参考距离 |
+|----|---------|-----------|-------------|---------|
+| 7 | 125 | ~5470 | -123 | ~2km |
+| 9 | 125 | ~1460 | -129 | ~3km |
+| **12** | **125** | **~290** | **-137** | **~5km** |
+| 12 | 250 | ~580 | -134 | ~3.5km |
+
+> **SF12 + BW125** 是最常见的高灵敏度配置,适合需要最远距离但数据量很少的场景。
+
+### CAD(Channel Activity Detection)
+
+CAD 是 LoRa 的一个特殊功能——在极低功耗下**周期性监听空中是否有 LoRa 信号**。如果检测到信号,唤醒 MCU 接收;无信号则快速返回休眠。
+
+CAD 的意义:普通 LoRa 接收模式下,接收机须持续打开(功耗 ~10mA),而 CAD 的监听功耗极低(~5μA)。
+
+---
+
+## LoRa 星型组网
+
+LoRa 网络采用**星型拓扑**:
+```
+         ┌─→ 节点1 (SF12, 上报温度)
+         │
+┌──────┐ ├─→ 节点2 (SF9, 上报湿度)     ┌──────────┐
+│ 网关  │←┼─→ 节点3 (SF7, 上报位移) ───→│ 云服务器 │
+│(接收) │ └─→ ...                       │(数据存储)│
+└──────┘                                └──────────┘
+         ↓
+    STM32(处理数据)
+```
+
+- **节点**:传感器,定时发送数据
+- **网关**:集中接收所有节点数据,转发到 STM32 或云端
+- LoRa 本身只定义了**物理层**(调制方式),MAC 层协议(如 LoRaWAN)定义了网络架构
+
+---
+
+## 实验:LoRa 节点与网关通信
+
+### 软件设计(AT 指令模式)
+
+**项目路径**:`stm32/13_lora_node_hal`(节点)、`stm32/14_lora_gateway_hal`(网关)
+
+节点通过串口配置 LoRa 模块参数,定时发送传感器数据。
+
+**文件:`stm32/13_lora_node_hal`(节点发送端)**
+
+```c
+// LoRa 模块初始化 AT 配置
+void LoRa_Node_Init(void)
+{
+    // 1. 设置模块工作模式
+    AT_SendCmd("AT+MODE=0\r\n", "OK", 1000);
+    // MODE: 0=透明传输(透传), 1=定点传输(指定地址)
+
+    // 2. 设置本机地址(不同类型设备用不同地址区分)
+    AT_SendCmd("AT+ADDR=1\r\n", "OK", 1000);     // 节点地址=1
+
+    // 3. 设置射频参数
+    AT_SendCmd("AT+BAND=470000000\r\n", "OK", 1000);  // 频率 470MHz
+    AT_SendCmd("AT+SF=12\r\n", "OK", 1000);            // 扩频因子 12
+    AT_SendCmd("AT+BW=125\r\n", "OK", 1000);           // 带宽 125KHz
+    AT_SendCmd("AT+CR=1\r\n", "OK", 1000);              // 编码率 4/5
+
+    // 4. 设置发射功率(0~20dBm,默认 20dBm)
+    AT_SendCmd("AT+POWER=20\r\n", "OK", 1000);
+}
+
+// 发送数据到网关(地址=0, 因为网关是透明接收模式)
+void LoRa_Node_Send(uint8_t *data, uint16_t len)
+{
+    char cmd[256];
+    // 透明传输模式: AT+SEND=dest_addr,len,data
+    // dest_addr=0 表示所有设备都能收
+    sprintf(cmd, "AT+SEND=0,%d,", len);
+    AT_SendCmd(cmd, "OK", 2000);
+    // 发送实际数据
+    HAL_UART_Transmit(&huart2, data, len, 1000);
+}
+
+// 应用程序:每 60 秒上报一次温度值
+void Node_Application(void)
+{
+    LoRa_Node_Init();
+
+    while (1)
+    {
+        uint16_t adc_val = ADC1_Read();          // 读取 ADC(温度传感器)
+        float    temp    = adc_val * 3.3f / 4095 * 100;  // 温度转换
+
+        char buf[64];
+        sprintf(buf, "TEMP:%.1f\r\n", temp);
+        LoRa_Node_Send((uint8_t *)buf, strlen(buf));
+
+        HAL_Delay(60000);  // 60s 发送一次(超低功耗场景)
+    }
+}
+```
+
+**文件:`stm32/14_lora_gateway_hal`(网关接收端)**
+
+```c
+// 网关初始化(使用与节点相同的射频参数)
+void LoRa_Gateway_Init(void)
+{
+    // 与节点相同:470MHz, SF12, BW125
+    AT_SendCmd("AT+MODE=0\r\n", "OK", 1000);
+    AT_SendCmd("AT+ADDR=0\r\n", "OK", 1000);      // 网关地址=0
+    AT_SendCmd("AT+BAND=470000000\r\n", "OK", 1000);
+    AT_SendCmd("AT+SF=12\r\n", "OK", 1000);
+    AT_SendCmd("AT+BW=125\r\n", "OK", 1000);
+}
+
+// 数据接收中断(串口主动上报)
+// LoRa 模块收到数据后通过串口打印:
+// +RCV=src_addr,len,RSSI,SNR,data
+void Process_LoRa_RxData(uint8_t *buffer)
+{
+    if (strstr((char *)buffer, "+RCV="))
+    {
+        // 解析 RSSI(接收信号强度)和 SNR(信噪比)
+        // e.g., +RCV=1,6,-115,5,HELLO
+        //         ↑地址 ↑RSSI ↑SNR
+
+        int src_addr, len, rssi, snr;
+        char data[64];
+
+        sscanf((char *)buffer, "+RCV=%d,%d,%d,%d,%s",
+               &src_addr, &len, &rssi, &snr, data);
+
+        printf("Node %d: %s (RSSI=%d, SNR=%d)\r\n",
+               src_addr, data, rssi, snr);
+    }
+}
+```
+
+---
+
+## 核心参数速查表
+
+| 参数 | AT 指令 | 说明 |
+|------|---------|------|
+| 工作模式 | `AT+MODE=0` | 0=透传, 1=定点传 |
+| 本机地址 | `AT+ADDR=1` | 1~65535 |
+| 目标地址 | `AT+SEND=addr,len,data` | 定点模式时指定 |
+| 中心频率 | `AT+BAND=470000000` | 单位 Hz(470~510MHz) |
+| 扩频因子 | `AT+SF=12` | 6~12 |
+| 带宽 | `AT+BW=125` | 125/250/500 KHz |
+| 编码率 | `AT+CR=1` | 1=4/5, 2=4/6, 3=4/7, 4=4/8 |
+| 发射功率 | `AT+POWER=20` | 0~20dBm |
+| CAD 使能 | `AT+CAD=1` | 0=关, 1=开(低功耗监听) |
+
+## 常见问题与避坑
+
+1. **通信距离短** → 增大 SF(用 12)、降低 BW(用 125)、检查天线匹配(阻抗 50Ω)
+2. **丢包率高** → 降低数据速率(高 SF + 低 BW)、增加重传机制、避免信噪比过低的链路
+3. **节点功耗高** → 开启 CAD(低功耗监听模式)、降低发射功率(10dBm 比 20dBm 省一半)、延长发送间隔
+4. **多节点冲突** → 不同节点错开发送时间(TDMA)、使用不同的扩频因子(CDMA 效果,互不干扰)
+5. **LoRa 模块不响应** → 检查波特率匹配、模块供电(峰值 > 120mA)、天线是否接好(长时间无天线可能烧 PA)
+6. **SF/BW/CR 参数必须一致** → 节点和网关的射频参数完全一致才能通信

+ 237 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/18-电源管理与低功耗模式.md

@@ -0,0 +1,237 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — 第1章:电源控制 + 配套代码15~20"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# 电源管理与低功耗模式
+
+> **用生活理解**:STM32 有多种低功耗模式就像你家里的电器——**运行模式**是所有电器全开(费电),**睡眠模式**是关灯但空调继续开(CPU 停,外设继续),**停止模式**是只留冰箱(维持寄存器),**待机模式**是拉总闸(只剩下 Vbat 供 RTC)。越省电的模式唤醒越慢。
+
+---
+
+## 电源框图
+
+STM32 的电源系统分为多个供电区域:
+
+| 区域 | 电压 | 供电对象 | 特点 |
+|------|------|---------|------|
+| **VDD 区域** | 3.3V | I/O 电路、待机电路、电压调节器 | 数字部分主电源 |
+| **VDDA 区域** | 3.3V | ADC、温度传感器、复位模块、PLL | 模拟部分独立电源(去耦更好) |
+| **1.8V 区域** | 1.8V | CPU 内核、SRAM、内部数字外设 | 由电压调节器从 3.3V 降压得到 |
+| **备份区域 (VBAT)** | 3.3V/电池 | RTC、LSE 振荡器、BKP 寄存器、PC13~15 | VDD 断电时由 VBAT 维持 |
+
+> **参考**:参考手册 §5(电源控制 PWR 寄存器)、原理图电源部分
+> **参考**:AMS1117 数据手册(板上 5V→3.3V LDO)
+
+### 电压调节器
+
+电压调节器将 VDD(3.3V) 转换为内核所需的 1.8V,有 3 种工作模式:
+
+| 调节器模式 | 输出状态 | 对应的 MCU 模式 |
+|-----------|---------|----------------|
+| **运转模式 (MR)** | 正常输出 1.8V | 运行/睡眠 |
+| **停止模式 (LPR)** | 低功耗输出 1.8V(电流小) | 停止 |
+| **待机模式** | 关闭(无输出) | 待机(内核掉电) |
+
+---
+
+## 上电复位(POR)与掉电复位(PDR)
+
+内部复位电路监控 VDD/VDDA 电压:
+
+```
+电压升高到 VPOR(~1.8V) → 解除复位 → MCU 启动
+电压下降到 VPDR(~1.5V) → 进入复位 → MCU 停止
+```
+
+两者之间有 **40mV 的迟滞**,防止电压在阈值附近抖动时频繁复位。
+
+> **参考**:参考手册 §5.2(上电/掉电复位特性)
+
+---
+
+## 可编程电压检测器(PVD)
+
+PVD(Programmable Voltage Detector)监控 VDD 电压,当电压低于可编程阈值时,可以产生 **中断**(不是复位)。
+
+| PVD 阈值选择 (PLS[2:0]) | 阈值电压 |
+|------------------------|---------|
+| 000 | 2.2V |
+| 001 | 2.3V |
+| 010 | 2.4V |
+| 011 | 2.5V |
+| 100 | 2.6V |
+| 101 | 2.7V |
+| 110 | 2.8V |
+| 111 | 2.9V |
+
+```c
+// PVD 配置示例:阈值 2.7V,下降沿中断
+PWR->CR |= PWR_CR_PLS_2 | PWR_CR_PLS_1;   // PLS[2:0]=110 → 2.8V
+PWR->CR |= PWR_CR_PVDE;                    // PVDE=1 使能PVD
+// PVD 中断: EXTI16 线
+EXTI->RTSR &= ~EXTI_RTSR_TR16;             // 下降沿触发(电压下降)
+EXTI->FTSR |= EXTI_FTSR_TR16;
+EXTI->IMR |= EXTI_IMR_MR16;
+NVIC_EnableIRQ(PVD_IRQn);
+
+// PVD 中断处理
+void PVD_IRQHandler(void)
+{
+    if (EXTI->PR & EXTI_PR_PR16)
+    {
+        // 电压低于 2.8V → 紧急保存数据到备份寄存器
+        SaveEmergencyData();
+        EXTI->PR |= EXTI_PR_PR16;
+    }
+}
+```
+
+---
+
+## 三种低功耗模式对比
+
+| 特性 | **睡眠 (Sleep)** | **停止 (Stop)** | **待机 (Standby)** |
+|------|----------------|----------------|--------------------|
+| **CPU 内核** | 停止 | 停止 | **断电** |
+| **SRAM/寄存器** | 保持 | **保持** | **丢失** |
+| **备份域** | 保持 | 保持 | 保持 |
+| **电压调节器** | 运转模式 | **低功耗模式** | **关闭** |
+| **典型功耗** | ~10mA | ~30μA | **~2μA** |
+| **唤醒源** | 任意中断/事件 | EXTI 中断 | RTC 闹钟、WKUP 引脚、NRST |
+| **唤醒时间** | ~0.9μs | ~5μs | ~50ms(需重新初始化) |
+
+### 睡眠模式(Sleep)
+
+CPU 停止,外设继续工作。任何中断或事件都能唤醒。
+
+**进入方式**:
+```c
+// 执行 WFI 指令(Wait For Interrupt)
+__WFI();          // 等待中断唤醒
+
+// 或执行 WFE 指令(Wait For Event)
+__WFE();          // 等待事件唤醒
+```
+
+**SLEEP-NOW vs SLEEP-ON-EXIT**:
+```c
+SCB->SCR &= ~SCB_SCR_SLEEPONEXIT_Msk;  // SLEEPDEEP=0, SLEEPONEXIT=0
+//   SLEEP-NOW: 执行 WFI 立即进睡眠
+//   SLEEP-ON-EXIT: 进中断→退出中断时自动睡眠
+
+SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk;    // SLEEPONEXIT=1
+//   SLEEP-ON-EXIT: 执行 WFI → 最低优先级 ISR 退出后进睡眠
+```
+
+### 停止模式(Stop)
+
+CPU + 所有外设时钟都停止,但 **SRAM 和寄存器内容保留**。电压调节器切到低功耗模式。
+
+**配置与进入**:
+```c
+// 1. 设置电源控制寄存器:进入停止模式时调压器模式
+// LPDS=1: 调压器低功耗模式(更省电)
+PWR->CR |= PWR_CR_LPDS;
+
+// 2. 设置 SLEEPDEEP 位
+SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
+
+// 3. 选择停止模式(PDDS=0)
+PWR->CR &= ~PWR_CR_PDDS;
+
+// 4. 执行 WFI 进入停止模式
+__WFI();
+```
+
+**唤醒后**:触发 EXTI 中断后,系统从停止模式恢复,执行 ISR,恢复运行主程序。
+
+### 待机模式(Standby)
+
+最省电模式。1.8V 内核域完全断电(SRAM 和寄存器内容丢失),仅备份域(VBAT)和待机电路保持。
+
+**配置与进入**:
+```c
+// 1. 设置 SLEEPDEEP
+SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
+
+// 2. 选择待机模式(PDDS=1)
+PWR->CR |= PWR_CR_PDDS;
+
+// 3. 执行 WFI(或 WFE)
+__WFI();
+
+// 唤醒后相当于系统复位!从 startup 重新执行。
+```
+
+**唤醒后检测**:
+```c
+if (PWR->CSR & PWR_CSR_SBF)      // SBF=1: 从待机模式唤醒
+{
+    PWR->CSR |= PWR_CSR_WUF;     // 清除唤醒标志
+    // 执行待机后恢复程序
+}
+```
+
+---
+
+## 实验:睡眠/停止/待机模式
+
+### 项目路径
+
+| 实验 | 寄存器版 | HAL 库版 |
+|------|---------|---------|
+| 睡眠模式 | `stm32/15_lowpower_sleep_register` | `stm32/16_lowpower_sleep_hal` |
+| 停止模式 | `stm32/17_lowpower_stop_register` | `stm32/18_lowpower_stop_hal` |
+| 待机模式 | `stm32/19_lowpower_standby_register` | `stm32/20_lowpower_standby_hal` |
+
+**文件:`stm32/17_lowpower_stop_register/User/main.c`**(停止模式示例)
+
+```c
+#include "stm32f10x.h"
+
+int main(void)
+{
+    // 初始化 USART 和按键
+    USART1_Init();
+    KEY_Init();  // 配置 PF10 为 EXTI 中断(作为唤醒源)
+
+    printf("进入停止模式,按 KEY 唤醒...\r\n");
+
+    // 进入停止模式
+    PWR->CR |= PWR_CR_LPDS;          // 调压器低功耗
+    SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;  // SLEEPDEEP=1
+    PWR->CR &= ~PWR_CR_PDDS;         // PDDS=0 → 停止模式
+    __WFI();                         // 进入停止模式
+
+    // 唤醒后从此继续执行
+    printf("已从停止模式唤醒\r\n");
+
+    while (1);
+}
+```
+
+---
+
+## 核心速查表
+
+| 操作 | 寄存器操作 | HAL 库函数 |
+|------|----------|-----------|
+| 进入睡眠 | `__WFI()`(或 __WFE) | `HAL_PWR_EnterSLEEPMode()` |
+| 进入停止 | `PWR.LPDS=1 + SCR.SLEEPDEEP=1 + __WFI()` | `HAL_PWR_EnterSTOPMode()` |
+| 进入待机 | `SCR.SLEEPDEEP=1 + PWR.PDDS=1 + __WFI()` | `HAL_PWR_EnterSTANDBYMode()` |
+| 配置 PVD | `PWR.PLS + PWR.PVDE + EXTI16` | `HAL_PWR_ConfigPVD()` |
+| 清除唤醒标志 | `PWR.CSR.WUF=1` | `__HAL_PWR_CLEAR_FLAG()` |
+| 检测待机唤醒 | `PWR.CSR.SBF` | `__HAL_PWR_GET_FLAG()` |
+
+## 常见问题与避坑
+
+1. **停止模式功耗高** → 检查所有 GPIO 是否关闭上下拉、所有外设时钟是否关闭(`RCC->APBxENR=0`)
+2. **无法唤醒** → 检查 EXTI 配置(必须配置为事件或中断)、NVIC 优先级
+3. **待机后 GPIO 输出乱** → 待机模式 GPIO 复位为默认浮空输入,恢复后需重新初始化
+4. **待机模式功耗还是很高** → 检查 VBAT 引脚是否接 VDD、WKUP 引脚是否有漏电路径
+5. **多次进出停止模式后异常** → 检查唤醒标志是否清除(PWR.CSR.WUF)

+ 279 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/19-BKP备份寄存器与RTC实时时钟.md

@@ -0,0 +1,279 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — BKP/RTC章节 + 配套代码21~26"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# BKP备份寄存器与RTC实时时钟
+
+> **用生活理解**:BKP 就像一块随身带的**防水笔记本**——即使主人在游泳池里(主电源断电),笔记本也不会泡坏(VBAT 电池供电)。RTC 就像手表上的秒针——一直在走(32.768KHz 晶振驱动),永远不会停。两个配合使用,可以做到断电后时间不丢失、数据不丢失。
+
+---
+
+## 备份域(Backup Domain)
+
+备份域是 STM32 中一个**独立的供电区域**,由 VBAT 引脚供电。
+
+```
+VDD(3.3V) ────┐
+               ├──→ 选择器 ──→ 备份域
+VBAT(电池) ────┘
+```
+
+- VDD 正常时:备份域由 VDD 供电
+- VDD 断电时:备份域**自动切换**到 VBAT(外接电池)
+- **LSE 晶振**(32.768KHz)也属于备份域——RTC 靠它走时
+
+> **参考**:参考手册 §6(BKP 寄存器)、§17(RTC 寄存器)
+> **参考**:原理图 VBAT 电路
+
+### 备份域包含的内容
+
+| 模块 | 说明 |
+|------|------|
+| **BKP 寄存器** | 42 个 16 位寄存器(共 84 字节) |
+| **RTC** | 实时时钟(日历/闹钟) |
+| **LSE 振荡器** | 32.768KHz 晶振 |
+| **PC13~PC15** | 备份域 I/O(由 BKP 控制) |
+
+---
+
+## BKP 备份寄存器
+
+BKP(Backup)寄存器共 **42 个 16 位寄存器**(总容量 84 字节),可在系统复位/待机/主电源断电时保持内容。
+
+### 数据存储(写入 BKP 寄存器)
+
+```c
+// 写 BKP 前必须使能对备份域的写访问
+// PWR_CR 的 DBP 位控制备份域的写保护
+void BKP_Write(uint16_t reg_index, uint16_t data)
+{
+    // 1. 使能对备份寄存器和 RTC 的访问
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;      // 开启 PWR 时钟
+    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;      // 开启 BKP 时钟
+
+    // 2. 解锁备份域写保护
+    PWR->CR |= PWR_CR_DBP;                   // DBP=1: 允许写备份域
+
+    // 3. 写入备份寄存器(BKP_DR1~BKP_DR42)
+    // BKP 寄存器地址偏移: 0x04 + (reg_index-1) * 4
+    BKP->DR[reg_index - 1] = data;           // 写入
+
+    // 4. 重新加锁(可选的,安全考虑)
+    // PWR->CR &= ~PWR_CR_DBP;
+}
+
+uint16_t BKP_Read(uint16_t reg_index)
+{
+    // 读取不需要 DBP 解锁
+    return BKP->DR[reg_index - 1];
+}
+```
+
+### 防侵入功能(TAMPER)
+
+BKP 支持防侵入检测——当 TAMPER 引脚被触发时,自动清除所有 BKP 寄存器内容(防止篡改)。
+
+```c
+// 配置 TAMPER 引脚(PC13)为防侵入模式
+BKP->CR |= BKP_CR_TPE;        // TPE=1: 使能防侵入
+// 当 PC13 检测到上升沿时 → 自动清除 BKP 寄存器 → 产生中断
+```
+
+---
+
+## RTC 实时时钟
+
+RTC(Real-Time Clock)提供精确的**日历/时钟功能**,具有闹钟中断、秒中断等功能。
+
+### RTC 功能框图
+
+```
+时钟源选择 → RTC 预分频器 → 32位计数器 → 比较器 → 闹钟中断
+                ↓                              ↓
+              1Hz 基准时钟                  秒中断
+```
+
+### RTC 时钟源选择
+
+RTC 有三个时钟源可选:
+
+| 时钟源 | 频率 | 精度 | 特点 |
+|--------|------|------|------|
+| **LSE** | **32.768KHz** | **高** | **推荐**,需外部晶振 |
+| **LSI** | ~40KHz | 差(±10%) | 不需要外部元件 |
+| **HSE** | 8MHz(/128=62.5KHz) | 中 | 需要外部晶振 |
+
+> **推荐 LSE**:32.768KHz 经过 32768 分频可精确获得 1Hz,最适合 RTC。
+
+### RTC 预分频器
+
+RTC 预分频器由两部分组成:
+
+```c
+// 异步预分频(7 位,最大 128 分频)
+// 同步预分频(15 位,最大 32768 分频)
+// 总预分频 = (ASYNC_PREDIV + 1) × (SYNC_PREDIV + 1)
+
+// LSE = 32.768KHz 时:
+RTC->PRLH = 0x0000;            // 异步预分频高(很少用)
+RTC->PRLL = 127;               // 异步 = 127+1 = 128
+RTC->DIVH = 0x0000;            // 同步预分频高(很少用)
+RTC->DIVL = 255;               // 同步 = 255+1 = 256
+// 总 = 128 × 256 = 32768 → 1Hz 基准时钟
+```
+
+### RTC 寄存器
+
+| 寄存器 | 说明 |
+|--------|------|
+| **RTC_CRH** | 控制寄存器高(OWIE 溢出中断、ALRIE 闹钟中断、SECIE 秒中断) |
+| **RTC_CRL** | 控制寄存器低(CNF 配置模式、RSF 寄存器同步、SECF/RTOFF) |
+| **RTC_PRL** | 预分频装载寄存器(RTC_PRLL 低 16 位) |
+| **RTC_DIV** | 余数寄存器(当前分频计数值) |
+| **RTC_CNT** | 32 位计数器(秒数) |
+| **RTC_ALR** | 闹钟寄存器(与 CNT 匹配时触发闹钟中断) |
+
+> **注意**:RTC 寄存器通过 **APB1 接口**访问,但 APB1 频率可能低于 RTC 频率,因此读取前需等待 RSF 标志同步。
+
+### RTC 读写流程
+
+```c
+// RTC 写入流程(RTC 寄存器属于备份域,写入需特殊时序)
+void RTC_SetCounter(uint32_t count)
+{
+    // 1. 电源和时钟使能
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
+    PWR->CR |= PWR_CR_DBP;                // 允许写备份域
+
+    // 2. RTC 配置模式
+    RTC->CRL |= RTC_CRL_CNF;              // CNF=1: 进入配置模式
+
+    // 3. 写入新值
+    RTC->CNTH = count >> 16;              // 高 16 位
+    RTC->CNTL = count & 0xFFFF;           // 低 16 位
+
+    // 4. 退出配置模式
+    RTC->CRL &= ~RTC_CRL_CNF;             // CNF=0
+
+    // 5. 等待写入完成
+    while ((RTC->CRL & RTC_CRL_RTOFF) == 0) {}  // RTOFF=1 才完成
+}
+
+uint32_t RTC_GetCounter(void)
+{
+    // 读取 RTC 不必进配置模式,但需要等待同步
+    // 等待 RSF 标志
+    RTC->CRL |= RTC_CRL_RSF;
+    while ((RTC->CRL & RTC_CRL_RSF) == 0) {}
+
+    // 读取计数器
+    uint16_t high = RTC->CNTH;
+    uint16_t low  = RTC->CNTL;
+
+    // 如果高 16 位在读低 16 位时改变了(进位),重新读
+    if (high != RTC->CNTH)
+    {
+        high = RTC->CNTH;
+        low  = RTC->CNTL;
+    }
+
+    return ((uint32_t)high << 16) | low;
+}
+```
+
+### Unix 时间戳转换
+
+RTC 计数器存储的是从指定基准时间(如 1970-01-01)开始的**秒数**(Unix 时间戳)。
+
+```c
+// Unix 时间戳 → 年月日时分秒(标准库 time.h 处理)
+#include <time.h>
+
+time_t raw_time = RTC_GetCounter();     // 读取 RTC 秒数
+struct tm *info = localtime(&raw_time);  // 转换为日期时间
+
+printf("%04d-%02d-%02d %02d:%02d:%02d\r\n",
+    info->tm_year + 1900,                // 年份
+    info->tm_mon + 1,                    // 月份 (0~11)
+    info->tm_mday,                       // 日
+    info->tm_hour,                       // 时
+    info->tm_min,                        // 分
+    info->tm_sec);                       // 秒
+
+// 反向:年月日→Unix 时间戳
+// 使用 mktime() 函数
+```
+
+---
+
+## 实验:BKP 数据存储 + RTC 日历
+
+**项目路径**:`stm32/21_bkp_test_register` ~ `stm32/26_rtc_calendar_hal`
+
+```c
+// RTC + BKP 初始化
+void RTC_BKP_Init(void)
+{
+    // 1. 开启 PWR 和 BKP 时钟
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
+    PWR->CR |= PWR_CR_DBP;                 // 解锁备份域
+
+    // 2. 如果 LSE 未启动,初始化 RTC
+    if ((RCC->BDCR & RCC_BDCR_RTCEN) == 0)
+    {
+        // 复位备份域
+        RCC->BDCR |= RCC_BDCR_BDRST;
+        RCC->BDCR &= ~RCC_BDCR_BDRST;
+
+        // 配置 LSE 并等待就绪
+        RCC->BDCR |= RCC_BDCR_LSEON;
+        while (!(RCC->BDCR & RCC_BDCR_LSERDY));
+
+        // 选择 LSE 为 RTC 时钟源
+        RCC->BDCR |= RCC_BDCR_RTCSEL_0;    // RTCSEL=01: LSE
+
+        // 配置 RTC 预分频器
+        RTC->CRL |= RTC_CRL_CNF;            // 进入配置
+        RTC->PRLL = 32768 - 1;              // 32.768KHz → 1Hz
+        RTC->CRL &= ~RTC_CRL_CNF;           // 退出配置
+        while (!(RTC->CRL & RTC_CRL_RTOFF));
+
+        // 使能 RTC
+        RCC->BDCR |= RCC_BDCR_RTCEN;
+
+        // 设置初始时间(示例:2026-01-01 00:00:00)
+        RTC_SetCounter(1767225600);          // Unix 时间戳
+    }
+}
+```
+
+---
+
+## 核心速查表
+
+| 操作 | 代码 |
+|------|------|
+| 解锁备份域 | `PWR->CR |= PWR_CR_DBP` |
+| 写 BKP 寄存器 | `BKP->DR[i] = data` |
+| 读 BKP 寄存器 | `data = BKP->DR[i]` |
+| 配置 LSE | `RCC->BDCR |= RCC_BDCR_LSEON; while(!LSERDY);` |
+| 选择 RTC 时钟 | `RCC->BDCR |= RCC_BDCR_RTCSEL_0`(LSE) |
+| 使能 RTC | `RCC->BDCR |= RCC_BDCR_RTCEN` |
+| 读 RTC 秒数 | `RTC_GetCounter()` |
+| 写 RTC 秒数 | `RTC_SetCounter(value)`(需 CNF 模式) |
+
+## 常见问题与避坑
+
+1. **BKP 写入不成功** → 检查 PWR_CR_DBP 是否置 1(必须解锁备份域)
+2. **RTC 不走时** → 检查 LSE 是否起振(LSERDY 标志)、RTCEN 是否使能
+3. **RTC 读值不对** → 读取前等待 RSF 同步(APB1 比 RTC 慢时)
+4. **VDD 断电 RTC 停走** → 检查 VBAT 引脚是否接电池或 VDD
+5. **写 RTC 后系统挂死** → RTC 写入需等待 RTOFF=1(未完成前不可再次写入)
+6. **Unix 时间戳时区问题** → localtime() 使用系统时区,跨时区应用用 gmtime()

+ 223 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/20-看门狗IWDG与WWDG.md

@@ -0,0 +1,223 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — 看门狗章节 + 配套代码27~28"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# 看门狗IWDG与WWDG
+
+> **用生活理解**:看门狗就像社区里的保安——你得定时去保安亭刷卡(喂狗),证明你还在正常工作。如果你一直没来刷卡(程序跑飞/卡死),保安就拉响警报(系统复位)。**IWDG** 是独立供电的保安(不怕停电),**WWDG** 是严格规定只能在特定时间窗口内刷卡的保安。
+
+---
+
+## 为什么要看门狗
+
+嵌入式系统运行中可能因电磁干扰、电源波动、软件 bug 等原因导致程序**跑飞或死循环**。看门狗可以在程序异常时自动复位系统,保证设备不会"死机"。
+
+---
+
+## 独立看门狗 IWDG
+
+IWDG(Independent Watchdog)使用**独立的 LSI 时钟(~40KHz)**,与主时钟独立——即使主时钟停振,IWDG 仍然工作。
+
+### IWDG 框图
+
+```
+LSI(~40KHz) → 8位预分频器 → 12位递减计数器(RL[11:0]) → 复位
+                                   ↑
+                              键寄存器(KRW)写 0xAAAA 重装
+```
+
+### IWDG 寄存器
+
+| 寄存器 | 功能 |
+|--------|------|
+| **KR** (键寄存器) | 写 0xCCCC 启动、写 0xAAAA 喂狗、写 0x5555 允许写 PR/RLR |
+| **PR** (预分频器) | 分频系数(3 位) |
+| **RLR** (重装载寄存器) | 12 位重装值(0~4095) |
+
+### IWDG 超时时间计算
+
+```
+Tout = (RLR + 1) × 4 × 2^PR / LSI
+LSI ≈ 40KHz
+```
+
+**常见超时时间表(LSI=40KHz)**:
+
+| PR[2:0] | 分频系数 | RLR=0xFFF(4095) | RLR=0xFFF(4095)+计算 | 分辨率 |
+|---------|---------|----------------|-------------------|--------|
+| 000 | 4 | 4096×4/40K = **0.41s** |  | 0.1ms |
+| 001 | 8 | 4096×8/40K = **0.82s** |  | 0.2ms |
+| 010 | 16 | 4096×16/40K = **1.64s** |  | 0.4ms |
+| 011 | 32 | 4096×32/40K = **3.28s** |  | 0.8ms |
+| 100 | 64 | 4096×64/40K = **6.55s** |  | 1.6ms |
+| 101 | 128 | 4096×128/40K = **13.11s** |  | 3.2ms |
+| 110 | 256 | 4096×256/40K = **26.21s** |  | 6.4ms |
+| 111 | 256 | 4096×256/40K = **26.21s** |  | 6.4ms |
+
+**示例**:PR=011(分频32), RLR=1000
+```
+Tout = 1001 × 32 × 4 / 40000 = 3.2s
+```
+
+### IWDG 配置示例
+
+```c
+void IWDG_Init(void)
+{
+    // 1. 写 0x5555 到 KR,允许修改 PR 和 RLR
+    IWDG->KR = 0x5555;
+
+    // 2. 设置预分频系数 = 32 (PR=011)
+    IWDG->PR = 3;        // PR[2:0]=011
+
+    // 3. 设置重装值 = 1000 → Tout ≈ 3.2s (40KHz)
+    IWDG->RLR = 1000;
+
+    // 4. 写 0xCCCC 启动 IWDG
+    IWDG->KR = 0xCCCC;
+
+    // 5. 喂狗(必须在 3.2s 内执行一次,否则复位)
+    // 通常在 main 循环中调用
+}
+
+void IWDG_Feed(void)
+{
+    IWDG->KR = 0xAAAA;   // 写 0xAAAA 重装计数器
+}
+
+int main(void)
+{
+    IWDG_Init();          // 启动看门狗
+
+    while (1)
+    {
+        // 主循环逻辑...
+
+        IWDG_Feed();      // 每 3s 内喂一次狗(Tout=3.2s)
+
+        // 如果程序卡死在此循环后面,3.2s 后系统复位
+    }
+}
+```
+
+> 注意:IWDG 一旦启动**无法停止**,只能通过系统复位关闭。
+
+---
+
+## 窗口看门狗 WWDG
+
+WWDG(Window Watchdog)更加灵活——它要求必须在特定的**时间窗口内**喂狗:
+
+| 特性 | 说明 |
+|------|------|
+| 时钟源 | APB1(36MHz),不是独立时钟 |
+| 计数器 | 7 位递减(T[6:0],最大值 127) |
+| 窗口上限 | 由 CFR 寄存器 W[6:0] 设定 |
+| 喂狗时机 | **必须在 CNT > W[6:0] 且 CNT ≠ 0x3F 时喂狗** |
+| 早期唤醒中断 (EWI) | CNT=0x40 时产生中断(可提前预警) |
+
+### WWDG 工作时序
+
+```
+                   窗口区(允许喂狗)          禁止区
+CNT:  127 ← ... ← [W+1] ← ... ← 0x40 ← 0x3F → 复位
+                    ↑                  ↑
+              最早喂狗时机         最晚喂狗时机
+```
+
+- 喂狗太早(CNT > W+1):复位
+- 喂狗太晚(CNT < 0x40/H):复位
+- 必须在 **W < CNT ≤ 0x7F** 范围内喂狗 → 正常
+
+### WWDG 超时计算
+
+```
+Tout = (CNT - W) × (PSC + 1) / APB1
+```
+
+WWDG 的最小喂狗间隔和最大喂狗间隔由 PSC 预分频系数和窗口值决定。
+
+### WWDG 配置示例
+
+```c
+void WWDG_Init(void)
+{
+    // 1. 开启 WWDG 时钟
+    RCC->APB1ENR |= RCC_APB1ENR_WWDGEN;
+
+    // 2. 设置预分频系数 = 8 (APB1=36MHz → CK=4.5MHz)
+    WWDG->CFR |= WWDG_CFR_WDGTB_1 | WWDG_CFR_WDGTB_0;  // WDGTB[1:0]=11: /8
+
+    // 3. 设置窗口值 = 0x50(CNT 必须 < 0x50 才能喂狗,即最早喂狗点)
+    WWDG->CFR &= ~WWDG_CFR_W;            // 清除窗口位域
+    WWDG->CFR |= (0x50 << 0);
+
+    // 4. 设置计数器初始值 = 0x7F(最大值)
+    WWDG->CR = 0x7F;
+
+    // 5. 使能窗口看门狗(WDGA=1)
+    WWDG->CR |= WWDG_CR_WDGA;
+}
+
+void WWDG_Feed(void)
+{
+    // 必须在窗口内喂狗(CNT < W + 1)
+    WWDG->CR = 0x7F;      // 重装计数器(必须在主循环写入此值)
+}
+
+void WWDG_IRQHandler(void)
+{
+    if (WWDG->SR & WWDG_SR_EWIF)         // 早期唤醒中断
+    {
+        // 距离复位还有 0x40 个计数周期,赶紧处理紧急事务
+        // 注意:在 ISR 中不能喂狗!
+        WWDG->SR = 0x00;                  // 清标志
+    }
+}
+```
+
+---
+
+## IWDG vs WWDG 对比
+
+| 对比项 | IWDG | WWDG |
+|--------|------|------|
+| **时钟源** | LSI (~40KHz) 独立 | APB1(依赖主时钟) |
+| **计数器** | 12 位递减 | 7 位递减 |
+| **超时范围** | ~0.1ms ~ 26s | ~0.1ms ~ 若干 ms |
+| **喂狗窗口** | 无窗口(任意时间喂即可) | **有窗口:太早太晚都复位** |
+| **可停止** | ❌ 启动后不可停 | ✅ 可通过软件停止 |
+| **中断** | ❌ 无 | ✅ 有 EWI 早期唤醒中断 |
+| **独立供电** | ✅ 完全独立 | ❌ 依赖主电源 |
+| **选型建议** | 需要高可靠性、长时间定时的系统 | 需要精确检测程序执行时序的系统 |
+
+---
+
+## 核心速查表
+
+| IWDG 操作 | 代码 |
+|-----------|------|
+| 启动 | `KR = 0xCCCC` |
+| 喂狗 | `KR = 0xAAAA` |
+| 配置 PR/RLR | 先 `KR = 0x5555` 解锁 |
+
+| WWDG 操作 | 代码 |
+|-----------|------|
+| 启动 | `CR |= WDGA` |
+| 喂狗 | `CR = 0x7F` |
+| 配窗口 | `CFR.W = window_value` |
+| EWI 中断 | `CFR.EWI=1; NVIC_EnableIRQ(WWDG_IRQn)` |
+
+## 常见问题与避坑
+
+1. **IWDG 一直在复位** → main 循环中忘了喂狗、喂狗间隔超过超时时间
+2. **IWDG 启动后无法调试** → IWDG 一旦启动无法暂停,调试时需在断点前禁用 IWDG(Keil 中配置 DBGMCU)
+3. **WWDG 喂狗太早也复位** → 检查窗口值配置(W 值必须小于 CNT 初始值)
+4. **WWDG 应用场景**:适合检测程序的**执行时序是否正常**(太早或太晚说明程序逻辑异常)
+5. **LSI 频率不准** → LSI 约 30~60KHz(典型 40KHz),实际超时时间 ±50%,重要场合需实测
+6. **Keil 调试关闭 IWDG/WWDG**:在 Debug → Settings → Debug 选项卡中勾选 "Reset and Halt" 或配置 DBGMCU 寄存器

+ 206 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/21-附录:HAL库架构与Keil MDK深入.md

@@ -0,0 +1,206 @@
+---
+tags: [source-summary]
+type: source
+source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — 附录章节"
+author: "尚硅谷研究院"
+date: 2026-07-15
+created: 2026-07-15
+---
+
+# 附录:HAL库架构与Keil MDK深入
+
+> **用生活理解**:HAL 库就像汽车上的自动变速箱——你不用关心底下齿轮怎么换挡(寄存器操作),只管踩油门(调 API 函数)就行。Keil MDK 的编译过程就像做菜——编译(切菜)→ 链接(配菜摆盘)→ 格式转换(装盘)→ 下载(上菜)。
+
+---
+
+## 附录1:HAL库详解
+
+### HAL 库是什么
+
+HAL = Hardware Abstraction Layer(硬件抽象层),是 ST 官方提供的**跨平台硬件驱动库**。与标准库(StdPeriph)不同,HAL 库的 API 风格在所有 STM32 系列(F0/F1/F4/F7/H7)上**统一**。
+
+### 三层驱动模式
+
+HAL 库为每个外设提供三种操作模式:
+
+| 模式 | 特点 | 适用场景 |
+|------|------|---------|
+| **轮询(Polling)** | 阻塞式,简单直接 | 调试、低速通信(如 I2C 读 EEPROM) |
+| **中断(IT)** | 非阻塞,CPU 可做其他事 | 接收不定长数据(如串口 RX) |
+| **DMA** | 非阻塞,硬件自动搬运 | 大批量数据传输(如 ADC 多通道扫描) |
+
+### STM32CubeMX 项目结构
+
+CubeMX 自动生成的项目目录结构:
+
+```
+Project/
+├── Core/
+│   ├── Inc/              # 头文件
+│   │   ├── main.h
+│   │   ├── gpio.h
+│   │   ├── usart.h
+│   │   └── stm32f1xx_it.h
+│   └── Src/              # 源文件
+│       ├── main.c        # 主函数 + USER CODE
+│       ├── gpio.c        # GPIO 初始化(CubeMX 自动生成)
+│       ├── usart.c       # USART 初始化
+│       ├── stm32f1xx_it.c # 中断服务函数
+│       ├── stm32f1xx_hal_msp.c # 外设 MSP 初始化
+│       └── system_stm32f1xx.c # 系统时钟配置
+├── Drivers/
+│   ├── CMSIS/            # 内核定义(core_cm3.h 等)
+│   └── STM32F1xx_HAL_Driver/ # HAL 库源码
+│       ├── Inc/          # HAL 头文件
+│       └── Src/          # HAL 源文件
+└── Project.ioc           # CubeMX 配置文件
+```
+
+### main() 执行流程
+
+```c
+int main(void)
+{
+    /* USER CODE BEGIN 1 */
+    // 用户预初始化代码(如变量声明)
+    /* USER CODE END 1 */
+
+    // ==== MCU 配置(由 CubeMX 生成,不要修改)====
+    HAL_Init();                     // HAL 库初始化(SysTick 时基、NVIC 优先级分组等)
+    SystemClock_Config();           // 系统时钟配置(72MHz)
+    MX_GPIO_Init();                 // GPIO 初始化
+    MX_USART1_UART_Init();          // USART1 初始化
+    /* 其他外设初始化... */
+
+    /* USER CODE BEGIN 2 */
+    // 用户初始化代码(如自定义变量初始化)
+    /* USER CODE END 2 */
+
+    while (1)
+    {
+        /* USER CODE BEGIN WHILE */
+        // 主循环逻辑
+        /* USER CODE END WHILE */
+
+        /* USER CODE BEGIN 3 */
+    }
+    /* USER CODE END 3 */
+}
+```
+
+> **重要**:CubeMX 生成的代码在 `/* USER CODE BEGIN */` 和 `/* USER CODE END */` 之间保留,重新生成时不会被覆盖。
+
+### HAL 库关键宏
+
+| 宏 | 说明 |
+|----|------|
+| `UNUSED(var)` | 标记未被使用的变量(避免编译器警告) |
+| `IS_XXX(VAL)` | 参数校验宏(如 `IS_GPIO_PIN(VAL)`) |
+| `__HAL_RCC_GPIOA_CLK_ENABLE()` | 开启外设时钟(替代直接操作 RCC) |
+| `__HAL_UART_ENABLE_IT(huart, IT)` | 使能中断 |
+| `MODIFY_REG(REG, CLEARMASK, SETMASK)` | 安全修改寄存器位域 |
+
+---
+
+## 附录2:Keil MDK 编译过程
+
+### 编译四阶段
+
+```
+源文件(.c/.s) → 编译(armcc/armasm) → 目标文件(.o)
+目标文件(.o) + 库 → 链接(armlink) → 镜像文件(.axf/.elf)
+镜像文件(.axf) → 格式转换(fromelf) → 烧录文件(.bin/.hex)
+烧录文件(.bin/.hex) → 下载 → 芯片 Flash
+```
+
+**各阶段详解**:
+
+| 阶段 | 工具 | 输入 | 输出 | 说明 |
+|------|------|------|------|------|
+| 1. **编译** | armcc(C) armasm(汇编) | `main.c`, `*.s` | `*.o` | 源文件→机器码,含调试信息 |
+| 2. **链接** | armlink | `*.o`, 库文件 | `*.axf/.elf` | 合并所有 .o,分配地址,解析符号 |
+| 3. **格式转换** | fromelf | `*.axf` | `*.bin/.hex` | 去掉调试信息,生成烧录用文件 |
+| 4. **下载** | ST-Link Utility | `*.bin/.hex` | Flash | 烧录到芯片 |
+
+### 程序在存储器中的分布
+
+```
+Flash (ROM)                 RAM (SRAM)
+┌──────────────────┐      ┌──────────────────┐
+│  .text (Code)    │      │  .data (RW-data) │  已初始化全局变量
+│  程序代码         │      │  (从Flash复制过来) │
+├──────────────────┤      ├──────────────────┤
+│  .rodata         │      │  .bss (ZI-data)  │  零初始化全局变量
+│  只读数据(const)  │      │  (全为0)          │
+├──────────────────┤      ├──────────────────┤
+│  .data (初值)     │──────→│  堆(Heap)        │
+│  已初始化全局变量  │ 复   │  malloc分配区域   │
+│  的初始值         │ 刻   ├──────────────────┤
+│                  │      │  栈(Stack) ⬇⬆    │
+│                  │      │  局部变量/函数调用 │
+└──────────────────┘      └──────────────────┘
+```
+
+**编译输出信息解读**:
+
+| 段 | 含义 | 存储位置 | 生成时的内容 |
+|----|------|---------|-------------|
+| **Code** | 程序代码(指令) | Flash | 函数体、指令序列 |
+| **RO-data** | 只读数据 | Flash | `const` 变量、字符串常量 |
+| **RW-data** | 可读写数据 | Flash 存初值→RAM 运行 | 已初始化的全局/静态变量 |
+| **ZI-data** | 零初始化数据 | RAM | 未初始化/初始化为 0 的全局变量 |
+
+**编译输出示例**:
+```
+Program Size: Code=1234 RO-data=56 RW-data=12 ZI-data=200
+Flash 占用 = Code + RO-data + RW-data = 1234 + 56 + 12 = 1302 字节
+RAM   占用 = RW-data + ZI-data = 12 + 200 = 212 字节
+```
+
+---
+
+## Keil MDK 调试技巧
+
+### 寄存器窗口的使用
+
+在 Debug 模式下,通过 Peripherals 菜单打开外设寄存器窗口:
+```
+Peripherals → General Purpose I/O → GPIOA
+```
+可以直接查看和修改寄存器值,非常适合调试硬件配置是否正确。
+
+### 断点设置
+
+| 断点类型 | 说明 |
+|---------|------|
+| 普通断点 | F2 或点击行号左侧 |
+| 条件断点 | 右键断点 → Condition(如 `count > 10`) |
+| 日志断点 | 右键断点 → Action → "printf x"(不停止,只打印) |
+| 访问断点 | 数据地址读写触发(**Data Breakpoint**) |
+
+### 观察变量
+
+- **Watch 窗口**:可查看全局/局部变量
+- **Live Watch**:实时更新(调试时自动刷新)
+- **System Viewer**:查看外设寄存器状态(Debug → System Viewer)
+
+---
+
+## 核心速查表(HAL 库常用函数)
+
+| 外设 | 初始化 | 轮询收发 | 中断收发 | DMA 收发 |
+|------|--------|---------|---------|---------|
+| GPIO | `HAL_GPIO_Init()` | `ReadPin/WritePin` | `EXTI_Callback` | — |
+| UART | `HAL_UART_Init()` | `Transmit/Receive` | `_IT` | `_DMA` |
+| SPI | `HAL_SPI_Init()` | `Transmit/Receive` | `_IT` | `_DMA` |
+| I2C | `HAL_I2C_Init()` | `Master_Transmit/Receive` | `_IT` | `_DMA` |
+| ADC | `HAL_ADC_Init()` | `Start/Stop` | `_IT` | `_DMA` |
+| TIM | `HAL_TIM_Init()` | — | `PWM/OC/IC_Start_IT` | — |
+
+## 常见问题与避坑
+
+1. **HAL_Delay 不准确** → HAL 时基默认使用 SysTick,若有更高优先级中断长时间阻塞 SysTick 中断会导致延时不准
+2. **CubeMX 生成代码后编译报错** → 检查芯片包版本、HAL 库版本是否匹配
+3. **Keil 下载后不运行** → 检查 BOOT0/BOOT1 跳线、Flash 编程算法是否正确(Options → Debug → Settings → Flash Download)
+4. **编译器 V5 vs V6** → V5(ARMCC)兼容老旧代码,V6(ARMCLANG)代码密度更好,但需适配
+5. **RAM 不够用** → 减小 ZI-data(减少全局数组)、增加堆栈大小配置(启动文件中的 Stack/Heap Size)

+ 16 - 0
X-Knowledge-Base/wiki/index.md

@@ -31,3 +31,19 @@ graph RL
   B --> F[freertos-ipc]
   B --> G[freertos-interrupt-time]
 ```
+
+## STM32 嵌入式开发(尚硅谷系列)
+
+> 来源:[[source-stm32-basic|尚硅谷基础篇]] · [[source-stm32-medium|尚硅谷进阶篇]]
+
+原始笔记(寄存器 + HAL 双版,代码源自配套项目):
+`raw/Joplin/嵌入式+Linux/STM32学习笔记/`
+
+21 篇笔记覆盖内容:
+
+| 层级 | 笔记编号 | 主题 |
+|------|---------|------|
+| **基础篇** | 01~06 | 概述/GPIO/LED/中断/时钟/USART |
+| **进阶篇** | 07~12 | I2C/TIM/PWM/DMA/ADC/SPI+FSMC |
+| **高级篇** | 13~17 | CAN/ETH/WiFi/BLE/LoRa |
+| **扩展篇** | 18~21 | 低功耗/RTC/看门狗/附录 |

+ 20 - 0
X-Knowledge-Base/wiki/log.md

@@ -62,6 +62,26 @@ type: log
 
     更新 `wiki/log.md`
 
+- `ingest`: 导入尚硅谷 STM32 全套笔记(全部完成)
+
+   原始资料归档:
+   - `raw/Joplin/嵌入式+Linux/STM32学习笔记/01~21` 共 21 篇笔记
+   - `raw/Joplin/嵌入式+Linux/STM32学习笔记/assets/` 精选图片
+
+   笔记结构:
+   - 01~06: 基础篇(概述/GPIO/中断/时钟/USART)
+   - 07~12: 进阶篇(I2C/定时器/PWM/DMA/ADC/SPI+FSMC)
+   - 13~17: 高级篇(CAN/以太网/WiFi/BLE/LoRa)
+   - 18~21: 扩展篇(低功耗/RTC/看门狗/附录)
+
+   每篇特色:前端 freeRTOS 风格 + 原 DOCX 章节结构 + 源码复制逐行注释 + 寄存器/HAL 双版 + 参考手册/原理图引用
+
+   新建 Wiki 页面:
+   - `wiki/source-stm32-basic.md`(基础篇资料索引)
+   - `wiki/source-stm32-medium.md`(进阶篇资料索引)
+
+   更新 `wiki/index.md`、`wiki/log.md`
+
 - `ingest`: 导入尚硅谷 STM32 进阶篇笔记
 
    原始资料归档:

+ 0 - 74
docs/superpowers/specs/2026-07-08-video-note-extractor-design.md

@@ -1,74 +0,0 @@
-# Video Note Extractor — Design Spec
-
-## Purpose
-
-Extract timed text transcripts and single-frame screenshots from tutorial video
-directories, for use in AI-assisted markdown note writing. Targeted at STM32 /
-embedded tutorial collections, but intentionally collection-agnostic.
-
-## Commands
-
-### `parse` — Extract timed text from subtitle files
-
-```
-python tools/extract.py parse <input> [--outdir <dir>]
-```
-
-- **input**: path to a single `.mp4` file, or a **directory** containing `.mp4`
-  files (scans recursively).
-- **outdir**: output root. Default `./transcripts/`.
-
-**Output structure** (mirrors input directory layout):
-
-```
-<outdir>/
-└── transcripts/
-    └── <collection-name>/
-        └── <video-name>.txt
-```
-
-Each `.txt` file contains lines of:
-
-```
-[HH:MM:SS.mmm] <subtitle text>
-```
-
-### `frame` — Extract a single frame as JPEG
-
-```
-python tools/extract.py frame <video> <timestamp> --outpath <path>
-```
-
-- **video**: path to `.mp4` file.
-- **timestamp**: `HH:MM:SS` or `SSS` (seconds).
-- **outpath**: where to write the JPEG image (parent directory created
-  automatically).
-
-## Subtitle resolution priority
-
-For each `.mp4`, find the best-matching subtitle file:
-
-1.  Same basename + `.srt`, excluding `_英文.srt` / `_English.srt`.
-2.  Same basename + `.ass` (parse `[Events]` → `Dialogue` lines, strip ASS
-    override tags like `{\\fn...}`).
-3.  Neither → print warning, skip.
-
-## Dependencies
-
-- Python ≥ 3.10 (stdlib only for `parse`)
-- `ffmpeg` / `ffprobe` on PATH (only for `frame`)
-
-## Anti-goals (explicitly out of scope)
-
-- No VAD / Silero / ONNX integration.
-- No ASR / API calls.
-- No batch screenshot extraction.
-- No markdown generation — the output `.txt` files are consumed by a human or
-  AI note-writer.
-
-## Usage flow (intended)
-
-1.  `python tools/extract.py parse <collection-dir> --outdir ./transcripts`
-2.  Read `.txt` files → decide which timestamps need screenshots.
-3.  `python tools/extract.py frame <video> <ts> --outpath <notes>/assets/<name>.jpg`
-4.  Write markdown notes, embedding relevant screenshots.