瀏覽代碼

fix: 修复审计发现的7处代码错误——LED_1→LED1, KEY_Init→Key_Init, ARR 99→9999, CR1 =→|=, I2C_Ack/NAck顺序, DMA CPAR/CMAR反了, PWM公式移除+1

OpenCode 1 天之前
父節點
當前提交
246485257a
共有 17 個文件被更改,包括 4581 次插入1648 次删除
  1. 13 13
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/03-GPIO输出:LED控制.md
  2. 1 1
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/04-GPIO输入与NVIC中断系统.md
  3. 3 3
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/06-串口USART通信.md
  4. 313 25
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/07-I2C通信与EEPROM 24C02.md
  5. 145 16
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/08-SysTick与通用定时器TIM.md
  6. 463 9
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/09-TIM高级应用:PWM与输入捕获.md
  7. 210 7
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/10-DMA数据传输.md
  8. 406 140
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/11-ADC模数转换.md
  9. 1130 255
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/12-SPI通信与FSMC总线.md
  10. 303 4
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/13-CAN通信协议与bxCAN外设.md
  11. 371 0
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/14-以太网通信与W5500.md
  12. 184 148
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/15-WiFi通信与ESP32-C3.md
  13. 133 102
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/16-BLE低功耗蓝牙.md
  14. 191 221
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/17-LoRa远距离通信.md
  15. 319 226
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/18-电源管理与低功耗模式.md
  16. 267 283
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/19-BKP备份寄存器与RTC实时时钟.md
  17. 129 195
      X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/20-看门狗IWDG与WWDG.md

+ 13 - 13
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/03-GPIO输出:LED控制.md

@@ -163,12 +163,12 @@ void Delay_s(uint16_t s)
 #include "stm32f10x.h"
 
 // 宏定义:三个LED对应的ODR位
-// LED_1 = GPIO_ODR_ODR0 = 0x0001 = 第0位,对应PA0
-// LED_2 = GPIO_ODR_ODR1 = 0x0002 = 第1位,对应PA1
-// LED_3 = GPIO_ODR_ODR8 = 0x0100 = 第8位,对应PA8
-#define LED_1 GPIO_ODR_ODR0
-#define LED_2 GPIO_ODR_ODR1
-#define LED_3 GPIO_ODR_ODR8
+// LED1 = GPIO_ODR_ODR0 = 0x0001 = 第0位,对应PA0
+// LED2 = GPIO_ODR_ODR1 = 0x0002 = 第1位,对应PA1
+// LED3 = GPIO_ODR_ODR8 = 0x0100 = 第8位,对应PA8
+#define LED1 GPIO_ODR_ODR0
+#define LED2 GPIO_ODR_ODR1
+#define LED3 GPIO_ODR_ODR8
 
 void LED_Init(void);                      // 初始化LED(时钟+GPIO配置)
 void LED_On(uint16_t led);                // 点亮指定LED
@@ -206,9 +206,9 @@ void LED_Init(void)
     GPIOA->CRH &= ~GPIO_CRH_CNF8;
 
     // 3. 初始状态:所有灯熄灭
-    LED_Off(LED_1);
-    LED_Off(LED_2);
-    LED_Off(LED_3);
+    LED_Off(LED1);
+    LED_Off(LED2);
+    LED_Off(LED3);
 }
 
 // 点亮LED:ODR对应位清0(低电平点亮)
@@ -331,7 +331,7 @@ int main(void)
 {
     LED_Init();  // 时钟+GPIO+初始状态 一站式初始化
 
-    uint16_t leds[] = {LED_1, LED_2, LED_3};
+    uint16_t leds[] = {LED1, LED2, LED3};
     uint8_t n = 3;
 
     while (1)
@@ -438,9 +438,9 @@ void LED_OffAll(LED leds[], uint8_t size)
 | 操作 | 代码 | 说明 |
 |------|------|------|
 | LED初始化 | `LED_Init()` | 开启时钟 + 配置推挽输出 + 初始熄灭 |
-| 点亮LED | `LED_On(LED_1)` | `GPIOA->ODR &= ~led_bit` |
-| 熄灭LED | `LED_Off(LED_1)` | `GPIOA->ODR |= led_bit` |
-| 翻转LED | `LED_Toggle(LED_1)` | 读取IDR判断当前状态,取反 |
+| 点亮LED | `LED_On(LED1)` | `GPIOA->ODR &= ~led_bit` |
+| 熄灭LED | `LED_Off(LED1)` | `GPIOA->ODR |= led_bit` |
+| 翻转LED | `LED_Toggle(LED1)` | 读取IDR判断当前状态,取反 |
 | 延时 | `Delay_ms(500)` | 基于SysTick查询方式 |
 
 ### HAL库版

+ 1 - 1
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/04-GPIO输入与NVIC中断系统.md

@@ -182,7 +182,7 @@ void EXTI15_10_IRQHandler(void)
     if ((GPIOF->IDR & GPIO_IDR_IDR10) != 0) // &测试: IDR10位是否为1
     {
         // 翻转LED1状态
-        LED_Toggle(LED_1);
+        LED_Toggle(LED1);
     }
 }
 ```

+ 3 - 3
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/STM32学习笔记/06-串口USART通信.md

@@ -136,9 +136,9 @@ USART1 使用 PA9(TX)和 PA10(RX):
 
 ### 软件设计(寄存器版 — 轮询方式)
 
-**项目路径**:`stm32/07_usart_polling_register`
+**项目路径**:`stm32/07_usart_register`
 
-**文件:`stm32/07_usart_polling_register/Hardware/USART/usart.h`**
+**文件:`stm32/07_usart_register/Hardware/USART/usart.h`**
 
 ```c
 #ifndef __USART_H
@@ -155,7 +155,7 @@ void USART_ReceiveString(uint8_t buffer[], uint8_t *size);
 #endif
 ```
 
-**文件:`stm32/07_usart_polling_register/Hardware/USART/usart.c`**
+**文件:`stm32/07_usart_register/Hardware/USART/usart.c`**
 
 ```c
 #include "usart.h"

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

@@ -231,8 +231,8 @@ void I2C_Stop(void)
 
 void I2C_Ack(void)
 {
-    SDA_HIGH;
     SCL_LOW;
+    SDA_HIGH;
     I2C_DELAY;
     SDA_LOW;
     I2C_DELAY;
@@ -244,10 +244,10 @@ void I2C_Ack(void)
     I2C_DELAY;
 }
 
-void I2C_NAck(void)
+void I2C_Nack(void)
 {
-    SDA_HIGH;
     SCL_LOW;
+    SDA_HIGH;
     I2C_DELAY;
     SCL_HIGH;
     I2C_DELAY;
@@ -349,38 +349,326 @@ int main(void)
 
 ---
 
-## 软件设计(HAL 库版 — 硬件 I2C)
+## 实验:硬件 I2C 读写 24C02
+
+### 软件设计(寄存器版 — 硬件 I2C)
+
+**项目路径**:`stm32/14_i2c_hardware_register`
+
+**文件:`stm32/14_i2c_hardware_register/Hardware/I2C/i2c.h`**
+
+```c
+#ifndef __I2C_H
+#define __I2C_H
+
+#include "stm32f10x.h"
+#include "delay.h"
+
+#define OK 0
+#define FAIL 1
+
+void I2C_Init(void);
+uint8_t I2C_Start(void);
+void I2C_Stop(void);
+void I2C_Ack(void);
+void I2C_Nack(void);
+uint8_t I2C_SendAddr(uint8_t addr);
+uint8_t I2C_SendByte(uint8_t byte);
+uint8_t I2C_ReadByte(void);
+
+#endif
+```
+
+**文件:`stm32/14_i2c_hardware_register/Hardware/I2C/i2c.c`**
+
+```c
+#include "i2c.h"
+
+void I2C_Init(void)
+{
+    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
+    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
+
+    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11 |
+                   GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
+
+    I2C2->CR1 &= ~I2C_CR1_SMBUS;
+    I2C2->CCR &= ~I2C_CCR_FS;
+
+    I2C2->CR2 |= 36;
+
+    I2C2->CCR |= 180;
+
+    I2C2->TRISE |= 37;
+
+    I2C2->CR1 |= I2C_CR1_PE;
+}
+
+uint8_t I2C_Start(void)
+{
+    I2C2->CR1 |= I2C_CR1_START;
+
+    uint16_t timeout = 0xffff;
+
+    while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout)
+    {
+        timeout--;
+    }
+    return timeout ? OK : FAIL;
+}
+
+void I2C_Stop(void)
+{
+    I2C2->CR1 |= I2C_CR1_STOP;
+}
+
+void I2C_Ack(void)
+{
+    I2C2->CR1 |= I2C_CR1_ACK;
+}
+
+void I2C_Nack(void)
+{
+    I2C2->CR1 &= ~I2C_CR1_ACK;
+}
+
+uint8_t I2C_SendAddr(uint8_t addr)
+{
+    I2C2->DR = addr;
+
+    uint16_t timeout = 0xffff;
+    while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout)
+    {
+        timeout--;
+    }
+    if (timeout > 0)
+    {
+        I2C2->SR2;
+    }
+
+    return timeout ? OK : FAIL;
+}
+
+uint8_t I2C_SendByte(uint8_t byte)
+{
+    uint16_t timeout = 0xffff;
+    while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout)
+    {
+        timeout--;
+    }
+
+    I2C2->DR = byte;
+
+    timeout = 0xffff;
+    while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout)
+    {
+        timeout--;
+    }
+    return timeout ? OK : FAIL;
+}
+
+uint8_t I2C_ReadByte(void)
+{
+    uint16_t timeout = 0xffff;
+    while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout)
+    {
+        timeout--;
+    }
+
+    return timeout ? I2C2->DR : FAIL;
+}
+```
+
+**文件:`stm32/14_i2c_hardware_register/User/main.c`**
+
+```c
+#include "usart.h"
+#include "m24c02.h"
+#include <string.h>
+
+int main(void)
+{
+    USART_Init();
+    M24C02_Init();
+
+    printf("尚硅谷 I2C 硬件寄存器实验...\n");
+
+    M24C02_WriteByte(0x00, 'a');
+    M24C02_WriteByte(0x01, 'b');
+    M24C02_WriteByte(0x02, 'c');
+
+    uint8_t byte1 = M24C02_ReadByte(0x00);
+    uint8_t byte2 = M24C02_ReadByte(0x01);
+    uint8_t byte3 = M24C02_ReadByte(0x02);
+
+    printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3);
+
+    M24C02_WriteBytes(0x00, "123456", 6);
+
+    uint8_t buffer[100] = {0};
+    M24C02_ReadBytes(0x00, buffer, 6);
+
+    printf("buffer = %s\n", buffer);
+
+    memset(buffer, 0, sizeof(buffer));
+
+    M24C02_WriteBytes(0x00, "1234567890abcdefghijk", 21);
+    M24C02_ReadBytes(0x00, buffer, 21);
+    printf("buffer = %s\n", buffer);
+
+    while (1)
+    {
+    }
+}
+```
+
+---
+
+## 实验:HAL 库硬件 I2C 读写 24C02
+
+### 软件设计(HAL 库版 — 硬件 I2C)
+
+
+**项目路径**:`stm32/15_i2c_hardware_hal`
 
-HAL 库使用结构体 `I2C_HandleTypeDef` 管理 I2C 外设,CubeMX 图形化配置后调用函数读写 EEPROM。
+**文件:`stm32/15_i2c_hardware_hal/Core/Src/i2c.c`** (CubeMX generated I2C init)
 
 ```c
-// HAL 库 I2C 写 24C02(Mem_Write 自动处理 START+地址+数据+STOP)
-uint8_t data = 0x55;
-HAL_I2C_Mem_Write(&hi2c1, 0xA0, 0x00, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
-// 参数: hi2c句柄, 设备地址(7位+写), 存储器地址, 地址宽度, 数据指针, 数据长度, 超时ms
+#include "i2c.h"
+
+I2C_HandleTypeDef hi2c2;
 
-// 等待页写入完成(超时重试或轮询 ACK)
-HAL_Delay(10);
+void MX_I2C2_Init(void)
+{
+    hi2c2.Instance = I2C2;
+    hi2c2.Init.ClockSpeed = 100000;
+    hi2c2.Init.DutyCycle = I2C_DUTYCYCLE_2;
+    hi2c2.Init.OwnAddress1 = 0;
+    hi2c2.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
+    hi2c2.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
+    hi2c2.Init.OwnAddress2 = 0;
+    hi2c2.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
+    hi2c2.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
+    if (HAL_I2C_Init(&hi2c2) != HAL_OK)
+    {
+        Error_Handler();
+    }
+}
 
-// HAL 库 I2C 读 24C02
-uint8_t read_val;
-HAL_I2C_Mem_Read(&hi2c1, 0xA0, 0x00, I2C_MEMADD_SIZE_8BIT, &read_val, 1, 100);
-// 自动处理: START+写地址+寄存器地址+RESTART+读地址+数据+NACK+STOP
+void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if(i2cHandle->Instance==I2C2)
+    {
+        __HAL_RCC_GPIOB_CLK_ENABLE();
+
+        GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11;
+        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
+        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
+
+        __HAL_RCC_I2C2_CLK_ENABLE();
+    }
+}
+```
+
+**文件:`stm32/15_i2c_hardware_hal/Core/Src/m24c02.c`**
+
+```c
+#include "m24c02.h"
 
-printf("HAL I2C Read: 0x%02X\r\n", read_val);
+void M24C02_Init(void)
+{
+    MX_I2C2_Init();
+}
+
+void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte)
+{
+    HAL_I2C_Mem_Write(&hi2c2, W_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, &byte, 1, 1000);
+    HAL_Delay(5);
+}
+
+uint8_t M24C02_ReadByte(uint8_t innerAddr)
+{
+    uint8_t byte;
+    HAL_I2C_Mem_Read(&hi2c2, R_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, &byte, 1, 1000);
+    return byte;
+}
+
+void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size)
+{
+    HAL_I2C_Mem_Write(&hi2c2, W_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, bytes, size, 1000);
+    HAL_Delay(5);
+}
+
+void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size)
+{
+    HAL_I2C_Mem_Read(&hi2c2, R_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, buffer, size, 1000);
+}
+```
+
+**文件:`stm32/15_i2c_hardware_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "i2c.h"
+#include "usart.h"
+#include "gpio.h"
+#include "m24c02.h"
+#include <string.h>
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_I2C2_Init();
+    MX_USART1_UART_Init();
+
+    printf("尚硅谷 I2C HAL 库实验...\n");
+
+    M24C02_WriteByte(0x00, 'a');
+    M24C02_WriteByte(0x01, 'b');
+    M24C02_WriteByte(0x02, 'c');
+
+    uint8_t byte1 = M24C02_ReadByte(0x00);
+    uint8_t byte2 = M24C02_ReadByte(0x01);
+    uint8_t byte3 = M24C02_ReadByte(0x02);
+
+    printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3);
+
+    M24C02_WriteBytes(0x00, "123456", 6);
+
+    uint8_t buffer[100] = {0};
+    M24C02_ReadBytes(0x00, buffer, 6);
+
+    printf("buffer = %s\n", buffer);
+
+    memset(buffer, 0, sizeof(buffer));
+
+    M24C02_WriteBytes(0x00, "1234567890abcdefghijk", 21);
+    M24C02_ReadBytes(0x00, buffer, 21);
+    printf("buffer = %s\n", buffer);
+
+    while (1)
+    {
+    }
+}
 ```
 
 ## 核心函数速查表
 
-| I2C 操作 | 软件模拟 | HAL 库 |
-|----------|---------|--------|
-| 初始化 | `I2C_Init()` | `HAL_I2C_Init()` |
-| 写 EEPROM | `I2C_Start/SendByte/Stop` 组合 | `HAL_I2C_Mem_Write(addr, mem_addr, data, len, timeout)` |
-| 读 EEPROM | `Start+写地址+寄存器地址+RESTART+读地址+数据+NACK+Stop` | `HAL_I2C_Mem_Read(addr, mem_addr, data, len, timeout)` |
-| 发送字节 | `I2C_SendByte(byte)` | —(底层封装在 HAL 中) |
-| 读取字节 | `I2C_ReadByte()` | —(底层封装在 HAL 中) |
-| 等待应答 | `I2C_Wait4Ack()` | —(自动处理) |
-| 发应答/非应答 | `I2C_Ack()` / `I2C_NAck()` | —(自动处理) |
+| I2C 操作 | 软件模拟 | 硬件 I2C(寄存器) | 硬件 I2C(HAL) |
+|----------|---------|-------------------|----------------|
+| 初始化 | `I2C_Init()` | `I2C_Init()` | `HAL_I2C_Init()` / `MX_I2C2_Init()` |
+| 写 EEPROM | `I2C_Start/SendByte/Stop` 组合 | `I2C_SendAddr/SendByte/Stop` 组合 | `HAL_I2C_Mem_Write(addr, mem_addr, data, len, timeout)` |
+| 读 EEPROM | `Start+写地址+寄存器地址+RESTART+读地址+数据+NACK+Stop` | `I2C_SendAddr/SendByte/Restart/ReadByte` 组合 | `HAL_I2C_Mem_Read(addr, mem_addr, data, len, timeout)` |
+| 发送字节 | `I2C_SendByte(byte)` | `I2C_SendByte(byte)` | —(底层封装在 HAL 中) |
+| 读取字节 | `I2C_ReadByte()` | `I2C_ReadByte()` | —(底层封装在 HAL 中) |
+| 等待应答 | `I2C_Wait4Ack()` | —(硬件自动处理) | —(硬件自动处理) |
+| 发应答/非应答 | `I2C_Ack()` / `I2C_NAck()` | `I2C_Ack()` / `I2C_Nack()` | —(硬件自动处理) |
+| 启动传输 | `I2C_Start()` | `I2C_Start()` | —(封装在 Mem_Write/Read 中) |
+| 停止传输 | `I2C_Stop()` | `I2C_Stop()` | —(封装在 Mem_Write/Read 中) |
 
 ## 常见问题与避坑
 

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

@@ -218,12 +218,69 @@ void SysTick_Handler(void)
     // 计数到 1000 次中断,即是1s,翻转LED1
     if (count == 1000)
     {
-        LED_Toggle(LED_1);
+        LED_Toggle(LED1);
         count = 0;
     }
 }
 ```
 
+### 软件设计(SysTick HAL 库版)
+
+**项目路径**:`stm32/17_led_twinkle_systick_hal`
+
+SysTick HAL 版不需要额外配置 SysTick 寄存器,CubeMX 生成的 `HAL_Init()` 会自动配置 SysTick 为 1ms 中断,直接利用 `HAL_IncTick()` 和 `uwTick` 全局变量计数即可。
+
+**文件:`stm32/17_led_twinkle_systick_hal/Core/Src/stm32f1xx_it.c`**(SysTick_Handler 部分)
+
+```c
+void SysTick_Handler(void)
+{
+  HAL_IncTick();
+  if (uwTick % 1000 == 0)
+  {
+    HAL_GPIO_TogglePin(GPIOA, LED1_Pin);
+  }
+}
+```
+
+**文件:`stm32/17_led_twinkle_systick_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+
+  while (1)
+  {
+  }
+}
+```
+
+**文件:`stm32/17_led_twinkle_systick_hal/Core/Src/gpio.c`**(MX_GPIO_Init 部分)
+
+```c
+void MX_GPIO_Init(void)
+{
+  GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+  __HAL_RCC_GPIOC_CLK_ENABLE();
+  __HAL_RCC_GPIOA_CLK_ENABLE();
+
+  HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET);
+
+  GPIO_InitStruct.Pin = LED1_Pin;
+  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
+  GPIO_InitStruct.Pull = GPIO_NOPULL;
+  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+  HAL_GPIO_Init(LED1_GPIO_Port, &GPIO_InitStruct);
+}
+```
+
 ### 软件设计(TIM 中断版)
 
 **项目路径**:`stm32/18_tim_led_twinkle_register`
@@ -256,7 +313,7 @@ void TIM6_Init(void)
     TIM6->PSC = 7199;
 
     // 3. 设置自动装载值 99,表示 100 次计数得到 1s
-    TIM6->ARR = 99;
+    TIM6->ARR = 9999;
 
     // 4. 打开更新中断使能
     TIM6->DIER |= TIM_DIER_UIE;
@@ -267,7 +324,7 @@ void TIM6_Init(void)
     NVIC_EnableIRQ(TIM6_IRQn);
 
     // 6. 使能计数器
-    TIM6->CR1 = TIM_CR1_CEN;
+    TIM6->CR1 |= TIM_CR1_CEN;
 }
 
 // 中断服务函数
@@ -277,7 +334,7 @@ void TIM6_IRQHandler(void)
     TIM6->SR &= ~TIM_SR_UIF;
 
     // 每1s翻转一次LED2
-    LED_Toggle(LED_2);
+    LED_Toggle(LED2);
 }
 ```
 
@@ -304,6 +361,79 @@ int main(void)
 }
 ```
 
+### 软件设计(TIM HAL 库版)
+
+**项目路径**:`stm32/19_led_twinkle_tim_hal`
+
+**文件:`stm32/19_led_twinkle_tim_hal/Core/Src/tim.c`**(MX_TIM6_Init)
+
+```c
+TIM_HandleTypeDef htim6;
+
+void MX_TIM6_Init(void)
+{
+  htim6.Instance = TIM6;
+  htim6.Init.Prescaler = 7199;
+  htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim6.Init.Period = 9999;
+  htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+  if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
+  {
+    Error_Handler();
+  }
+}
+
+void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
+{
+  if(tim_baseHandle->Instance==TIM6)
+  {
+    __HAL_RCC_TIM6_CLK_ENABLE();
+    HAL_NVIC_SetPriority(TIM6_IRQn, 2, 0);
+    HAL_NVIC_EnableIRQ(TIM6_IRQn);
+  }
+}
+```
+
+**文件:`stm32/19_led_twinkle_tim_hal/Core/Src/stm32f1xx_it.c`**(中断和回调)
+
+```c
+// TIM6 中断入口
+void TIM6_IRQHandler(void)
+{
+  HAL_TIM_IRQHandler(&htim6);
+}
+
+// 用户回调
+void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
+{
+  if (htim->Instance == TIM6)
+  {
+    HAL_GPIO_TogglePin(GPIOA, LED2_Pin);
+  }
+}
+```
+
+**文件:`stm32/19_led_twinkle_tim_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "tim.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_TIM6_Init();
+  HAL_TIM_Base_Start_IT(&htim6);
+
+  while (1)
+  {
+  }
+}
+```
+
 ---
 
 ## HAL 库版定时器
@@ -312,27 +442,26 @@ HAL 库使用 `TIM_HandleTypeDef` 管理定时器,CubeMX 生成初始化代码
 
 ```c
 // CubeMX 生成: tim.c 中的 MX_TIMx_Init()
-TIM_HandleTypeDef htim5;
+TIM_HandleTypeDef htim6;
 
-void MX_TIM5_Init(void)
+void MX_TIM6_Init(void)
 {
-    htim5.Instance = TIM5;
-    htim5.Init.Prescaler = 7200 - 1;          // PSC
-    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
-    htim5.Init.Period = 10000 - 1;             // ARR
-    htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
-    htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
-    HAL_TIM_Base_Init(&htim5);
+    htim6.Instance = TIM6;
+    htim6.Init.Prescaler = 7199;          // PSC
+    htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
+    htim6.Init.Period = 9999;             // ARR → (PSC+1)*(ARR+1)/72M = 7200*10000/72M = 1s
+    htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+    HAL_TIM_Base_Init(&htim6);
 }
 
 // HAL 库启动定时器中断
-HAL_TIM_Base_Start_IT(&htim5);
-// 不使用中断: HAL_TIM_Base_Start(&htim5);
+HAL_TIM_Base_Start_IT(&htim6);
+// 不使用中断: HAL_TIM_Base_Start(&htim6);
 
 // HAL 库定时器中断回调(用户重写此函数)
 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
 {
-    if (htim->Instance == TIM5)
+    if (htim->Instance == TIM6)
     {
         static uint16_t tick = 0;
         if (++tick >= 1000) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); tick = 0; }

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

@@ -235,6 +235,90 @@ int main(void)
 
 ---
 
+## 实验:呼吸灯(HAL 库版)
+
+### 软件设计(HAL 库版)
+
+**项目路径**:`stm32/21_led_breathe_hal`
+
+**文件:`stm32/21_led_breathe_hal/Core/Src/tim.c`**(MX_TIM5_Init + TIM5_SetDutyCycle)
+
+```c
+TIM_HandleTypeDef htim5;
+
+void MX_TIM5_Init(void)
+{
+  htim5.Instance = TIM5;
+  htim5.Init.Prescaler = 7199;
+  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim5.Init.Period = 99;
+  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+  htim5.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+  HAL_TIM_Base_Init(&htim5);
+  HAL_TIM_PWM_Init(&htim5);
+
+  TIM_OC_InitTypeDef sConfigOC = {0};
+  sConfigOC.OCMode = TIM_OCMODE_PWM1;
+  sConfigOC.Pulse = 0;
+  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
+  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
+  HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
+}
+
+void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef* timHandle)
+{
+  GPIO_InitTypeDef GPIO_InitStruct = {0};
+  if(timHandle->Instance==TIM5)
+  {
+    __HAL_RCC_TIM5_CLK_ENABLE();
+    __HAL_RCC_GPIOA_CLK_ENABLE();
+    GPIO_InitStruct.Pin = GPIO_PIN_1;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
+    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+  }
+}
+
+void TIM5_SetDutyCycle(uint8_t dutyCycle)
+{
+  __HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, dutyCycle);
+}
+```
+
+**文件:`stm32/21_led_breathe_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "tim.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_TIM5_Init();
+
+  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
+
+  uint8_t dutyCycle = 1;
+  int8_t step = -1;
+
+  while (1)
+  {
+    if (dutyCycle <= 1 || dutyCycle >= 99)
+    {
+      step = -step;
+    }
+    dutyCycle += step;
+    TIM5_SetDutyCycle(dutyCycle);
+    HAL_Delay(20);
+  }
+}
+```
+
+---
+
 ## 实验:PWM 周期/占空比独立控制
 
 **项目路径**:`stm32/22_tim_pwm_cycle_register`(周期捕获)
@@ -307,6 +391,190 @@ int main(void)
 }
 ```
 
+
+
+### 软件设计(HAL 库版 — 周期捕获)
+
+**项目路径**:`stm32/23_pwm_cycle_hal`
+
+**文件:`stm32/23_pwm_cycle_hal/Core/Src/tim.c`**(MX_TIM4_Init + 回调)
+
+```c
+TIM_HandleTypeDef htim4;
+TIM_HandleTypeDef htim5;
+
+void MX_TIM4_Init(void)
+{
+  htim4.Instance = TIM4;
+  htim4.Init.Prescaler = 71;
+  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim4.Init.Period = 65535;
+  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+  HAL_TIM_Base_Init(&htim4);
+  HAL_TIM_IC_Init(&htim4);
+
+  TIM_IC_InitTypeDef sConfigIC = {0};
+  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
+  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
+  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
+  sConfigIC.ICFilter = 0;
+  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_1);
+}
+
+void MX_TIM5_Init(void)
+{
+  htim5.Instance = TIM5;
+  htim5.Init.Prescaler = 7199;
+  htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim5.Init.Period = 99;
+  htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+  HAL_TIM_Base_Init(&htim5);
+  HAL_TIM_PWM_Init(&htim5);
+
+  TIM_OC_InitTypeDef sConfigOC = {0};
+  sConfigOC.OCMode = TIM_OCMODE_PWM1;
+  sConfigOC.Pulse = 60;
+  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
+  HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
+}
+
+double TIM4_GetPWMCycle(void)
+{
+  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
+}
+double TIM4_GetPWMFreq(void)
+{
+  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
+}
+```
+
+**文件:`stm32/23_pwm_cycle_hal/Core/Src/stm32f1xx_it.c`**(输入捕获回调)
+
+```c
+void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
+{
+  if (htim->Instance == TIM4)
+  {
+    __HAL_TIM_SetCounter(&htim4, 0);
+  }
+}
+```
+
+**文件:`stm32/23_pwm_cycle_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "tim.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_TIM4_Init();
+  MX_TIM5_Init();
+  MX_USART1_UART_Init();
+
+  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
+  HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1);
+
+  while (1)
+  {
+    printf("T = %.2f ms, f = %.2f Hz\n", TIM4_GetPWMCycle(), TIM4_GetPWMFreq());
+    HAL_Delay(1000);
+  }
+}
+```
+
+### 软件设计(HAL 库版 — 周期+占空比捕获)
+
+**项目路径**:`stm32/25_pwm_duty_hal`
+
+**文件:`stm32/25_pwm_duty_hal/Core/Src/tim.c`**(MX_TIM4_Init 双通道 + 从模式)
+
+```c
+TIM_HandleTypeDef htim4;
+TIM_HandleTypeDef htim5;
+
+void MX_TIM4_Init(void)
+{
+  htim4.Instance = TIM4;
+  htim4.Init.Prescaler = 71;
+  htim4.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim4.Init.Period = 65535;
+  htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+  htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+  HAL_TIM_Base_Init(&htim4);
+  HAL_TIM_IC_Init(&htim4);
+
+  TIM_SlaveConfigTypeDef sSlaveConfig = {0};
+  sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
+  sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
+  sSlaveConfig.TriggerPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
+  sSlaveConfig.TriggerFilter = 0;
+  HAL_TIM_SlaveConfigSynchro(&htim4, &sSlaveConfig);
+
+  TIM_IC_InitTypeDef sConfigIC = {0};
+  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
+  sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
+  sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
+  sConfigIC.ICFilter = 0;
+  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_1);
+
+  sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_FALLING;
+  sConfigIC.ICSelection = TIM_ICSELECTION_INDIRECTTI;
+  HAL_TIM_IC_ConfigChannel(&htim4, &sConfigIC, TIM_CHANNEL_2);
+}
+
+double TIM4_GetPWMCycle(void)
+{
+  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1) / 1000.0;
+}
+double TIM4_GetPWMFreq(void)
+{
+  return 1000000.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
+}
+double TIM4_GetPWMDutyCycle(void)
+{
+  return __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_2) * 1.0 / __HAL_TIM_GetCompare(&htim4, TIM_CHANNEL_1);
+}
+```
+
+**文件:`stm32/25_pwm_duty_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "tim.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_TIM4_Init();
+  MX_TIM5_Init();
+  MX_USART1_UART_Init();
+
+  printf("Hello, world!\n");
+
+  HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
+  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_1);
+  HAL_TIM_IC_Start(&htim4, TIM_CHANNEL_2);
+
+  while (1)
+  {
+    printf("T = %.2f ms, f = %.2f Hz, duty = %.2f %%\n",
+        TIM4_GetPWMCycle(), TIM4_GetPWMFreq(), TIM4_GetPWMDutyCycle() * 100);
+    HAL_Delay(1000);
+  }
+}
+```
+
 ---
 
 ## 实验:输入捕获测频率
@@ -470,7 +738,7 @@ double TIM4_GetPWMCycle(void);
 double TIM4_GetPWMFreq(void);
 
 // 获取PWM占空比
-double TIM4_GetPWMDuty(void);
+double TIM4_GetPWMDutyCycle(void);
 
 #endif
 ```
@@ -515,13 +783,183 @@ void TIM4_Init(void)
 }
 
 // 获取PWM占空比
-double TIM4_GetPWMDuty(void)
+double TIM4_GetPWMDutyCycle(void)
 {
     return (TIM4->CCR2 + 1) * 1.0 / (TIM4->CCR1 + 1);
 }
 ```
 
-> **原理**:CH1(TI1 -> IC1)捕获上升沿,此时 CNT 值存入 CCR1(周期);CH2(TI1 -> IC2)捕获下降沿,CNT 值存入 CCR2(高电平宽度)。从模式复位在 TI1FP1 上升沿自动清零 CNT,无需中断干预。占空比 = (CCR2 + 1) / (CCR1 + 1)。
+> **原理**:CH1(TI1 -> IC1)捕获上升沿,此时 CNT 值存入 CCR1(周期);CH2(TI1 -> IC2)捕获下降沿,CNT 值存入 CCR2(高电平宽度)。从模式复位在 TI1FP1 上升沿自动清零 CNT,无需中断干预。占空比 = CCR2 / CCR1。
+
+
+
+## 实验:指定脉冲数 PWM(N 个脉冲后自动停止)
+
+### 软件设计(寄存器版)
+
+**项目路径**:`stm32/26_pwm_n_register`
+
+使用 TIM1 高级定时器 + TIM4 输入捕获 + TIM5 PWM 输出 + TIM6 定时。利用 TIM1 的重复计数器(RCR)实现输出 N 个脉冲后停止。
+
+**文件:`stm32/26_pwm_n_register/Hardware/TIM/tim1.h`**
+
+```c
+#ifndef __TIM1_H
+#define __TIM1_H
+
+#include "stm32f10x.h"
+#include <stdio.h>
+
+void TIM1_Init(void);
+void TIM1_Start(void);
+void TIM1_Stop(void);
+
+#endif
+```
+
+**文件:`stm32/26_pwm_n_register/Hardware/TIM/tim1.c`**
+
+```c
+#include "tim1.h"
+
+void TIM1_Init(void)
+{
+    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
+    RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
+
+    GPIOA->CRH |= GPIO_CRH_MODE8;
+    GPIOA->CRH |= GPIO_CRH_CNF8_1;
+    GPIOA->CRH &= ~GPIO_CRH_CNF8_0;
+
+    TIM1->PSC = 7199;
+    TIM1->ARR = 4999;
+    TIM1->CR1 &= ~TIM_CR1_DIR;
+    TIM1->RCR = 4;
+
+    TIM1->CCMR1 &= ~TIM_CCMR1_CC1S;
+    TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
+    TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
+    TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;
+
+    TIM1->CCR1 = 2500;
+    TIM1->CCER &= ~TIM_CCER_CC1P;
+    TIM1->CR1 |= TIM_CR1_URS;
+    TIM1->EGR |= TIM_EGR_UG;
+    TIM1->CCER |= TIM_CCER_CC1E;
+    TIM1->BDTR |= TIM_BDTR_MOE;
+
+    TIM1->DIER |= TIM_DIER_UIE;
+
+    NVIC_SetPriorityGrouping(3);
+    NVIC_SetPriority(TIM1_UP_IRQn, 3);
+    NVIC_EnableIRQ(TIM1_UP_IRQn);
+}
+
+void TIM1_Start(void)
+{
+    TIM1->CR1 |= TIM_CR1_CEN;
+}
+
+void TIM1_Stop(void)
+{
+    TIM1->CR1 &= ~TIM_CR1_CEN;
+}
+
+void TIM1_UP_IRQHandler(void)
+{
+    printf("into interrupt...\n");
+    TIM1->SR &= ~TIM_SR_UIF;
+    TIM1_Stop();
+}
+```
+
+**文件:`stm32/26_pwm_n_register/User/main.c`**
+
+```c
+#include "usart.h"
+#include "tim1.h"
+#include "delay.h"
+
+int main(void)
+{
+    USART_Init();
+    TIM1_Init();
+
+    printf("Hello, world!\n");
+
+    TIM1_Start();
+
+    while (1)
+    {
+    }
+}
+```
+
+### 软件设计(HAL 库版)
+
+**项目路径**:`stm32/27_pwm_n_hal`
+
+**文件:`stm32/27_pwm_n_hal/Core/Src/tim.c`**(MX_TIM1_Init)
+
+```c
+TIM_HandleTypeDef htim1;
+
+void MX_TIM1_Init(void)
+{
+  htim1.Instance = TIM1;
+  htim1.Init.Prescaler = 7199;
+  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
+  htim1.Init.Period = 4999;
+  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
+  htim1.Init.RepetitionCounter = 4;
+  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
+  HAL_TIM_Base_Init(&htim1);
+  HAL_TIM_PWM_Init(&htim1);
+
+  TIM_OC_InitTypeDef sConfigOC = {0};
+  sConfigOC.OCMode = TIM_OCMODE_PWM1;
+  sConfigOC.Pulse = 2500;
+  sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;
+  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
+  HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1);
+}
+```
+
+**文件:`stm32/27_pwm_n_hal/Core/Src/stm32f1xx_it.c`**(TIM1 中断)
+
+```c
+void TIM1_UP_IRQHandler(void)
+{
+  HAL_TIM_IRQHandler(&htim1);
+  HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1);
+}
+```
+
+**文件:`stm32/27_pwm_n_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "tim.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_TIM1_Init();
+  MX_USART1_UART_Init();
+
+  __HAL_TIM_CLEAR_FLAG(&htim1, TIM_FLAG_UPDATE);
+  __HAL_TIM_ENABLE_IT(&htim1, TIM_IT_UPDATE);
+  HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
+
+  while (1)
+  {
+  }
+}
+```
 
 ---
 
@@ -530,25 +968,41 @@ double TIM4_GetPWMDuty(void)
 HAL 库使用 `HAL_TIM_PWM_Start()` 启动 PWM,用宏 `__HAL_TIM_SET_COMPARE()` 修改占空比。
 
 ```c
-// CubeMX 生成: MX_TIM5_Init() 配置 PSC/ARR 和 CH1 输出模式
+// CubeMX 生成: MX_TIM5_Init() 配置 PSC/ARR 和 CH2 输出模式
 TIM_HandleTypeDef htim5;
 
+void MX_TIM5_Init(void)
+{
+    htim5.Instance = TIM5;
+    htim5.Init.Prescaler = 7199;          // 72MHz / 7200 = 10KHz (0.1ms)
+    htim5.Init.CounterMode = TIM_COUNTERMODE_UP;
+    htim5.Init.Period = 99;               // ARR → 10KHz / 100 = 100Hz (10ms)
+    HAL_TIM_Base_Init(&htim5);
+    HAL_TIM_PWM_Init(&htim5);
+
+    TIM_OC_InitTypeDef sConfigOC = {0};
+    sConfigOC.OCMode = TIM_OCMODE_PWM1;
+    sConfigOC.Pulse = 0;                  // 初始占空比 0%
+    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
+    HAL_TIM_PWM_ConfigChannel(&htim5, &sConfigOC, TIM_CHANNEL_2);
+}
+
 // 启动 PWM 输出
-HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_1);
-// HAL_TIMEx_PWMN_Start(&htim5, TIM_CHANNEL_1); // 互补输出(TIM1 高级定时器)
+HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
 
-// 修改占空比(HAL 宏,比直接写寄存器更可移植)
-__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, duty);  // duty = 0~ARR
+// 修改占空比
+__HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, duty);  // duty = 0~ARR
 
 // HAL 库呼吸灯示例
 uint8_t duty = 0, dir = 0;
 while (1) {
     if (!dir) { duty++;  if (duty >= 99) dir = 1; }
     else      { duty--;  if (duty <= 0)  dir = 0; }
-    __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, duty * 10);
+    __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_2, duty);
     HAL_Delay(10);
 }
 ```
+```
 
 ## 核心速查表
 

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

@@ -174,11 +174,11 @@ void DMA1_Init(void)
 // DMA1通道1传输数据,需要指定源地址、目的地址、数据长度
 void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
 {
-    // 1. 将源地址写入外设地址寄存器
-    DMA1_Channel1->CPAR = srcAddr;
+    // 1. 将目的地址写入外设地址寄存器(MEM2MEM 模式下 CPAR=目的)
+    DMA1_Channel1->CPAR = destAddr;
 
-    // 2. 将目的地址写入存储器地址寄存器
-    DMA1_Channel1->CMAR = destAddr;
+    // 2. 将源地址写入存储器地址寄存器(MEM2MEM 模式下 CMAR=源)
+    DMA1_Channel1->CMAR = srcAddr;
 
     // 3. 将数据长度写入 CNDTR
     DMA1_Channel1->CNDTR = dataLen;
@@ -251,6 +251,95 @@ int main(void)
 }
 ```
 
+### 软件设计(HAL 库版)
+
+**项目路径**:`stm32/29_dma_mem2mem_hal`
+
+**文件:`stm32/29_dma_mem2mem_hal/Core/Src/dma.c`**(MX_DMA_Init)
+
+```c
+#include "dma.h"
+
+DMA_HandleTypeDef hdma_memtomem_dma1_channel1;
+
+void MX_DMA_Init(void)
+{
+  __HAL_RCC_DMA1_CLK_ENABLE();
+
+  hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1;
+  hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY;
+  hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE;
+  hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE;
+  hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
+  hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
+  hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL;
+  hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW;
+  if (HAL_DMA_Init(&hdma_memtomem_dma1_channel1) != HAL_OK)
+  {
+    Error_Handler();
+  }
+
+  HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
+  HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);
+}
+```
+
+**文件:`stm32/29_dma_mem2mem_hal/Core/Src/stm32f1xx_it.c`**(DMA 中断)
+
+```c
+void DMA1_Channel1_IRQHandler(void)
+{
+  HAL_DMA_IRQHandler(&hdma_memtomem_dma1_channel1);
+}
+```
+
+**文件:`stm32/29_dma_mem2mem_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "dma.h"
+#include "usart.h"
+#include "gpio.h"
+
+const uint8_t src[] = {10,20,30,40};
+uint8_t dest[4] = {0};
+uint8_t isFinished = 0;
+
+void finishedCallback(DMA_HandleTypeDef *_hdma)
+{
+  printf("call back...\n");
+  HAL_DMA_Abort_IT(&hdma_memtomem_dma1_channel1);
+  isFinished = 1;
+}
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_DMA_Init();
+  MX_USART1_UART_Init();
+
+  printf("Hello, world!\n");
+  printf("src = %p, dest = %p\n", src, dest);
+
+  HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, finishedCallback);
+  HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src, (uint32_t)dest, 4);
+
+  while (1)
+  {
+    if (isFinished)
+    {
+      for (uint8_t i = 0; i < 4; i++)
+      {
+        printf("%d\t", dest[i]);
+      }
+      isFinished = 0;
+    }
+  }
+}
+```
+
 ---
 
 ## 实验:DMA + USART 自动发送
@@ -376,22 +465,136 @@ int main(void)
 }
 ```
 
+### 软件设计(HAL 库版)
+
+**项目路径**:`stm32/31_dma_mem2uart_hal`
+
+**文件:`stm32/31_dma_mem2uart_hal/Core/Src/dma.c`**(MX_DMA_Init)
+
+```c
+#include "dma.h"
+
+void MX_DMA_Init(void)
+{
+  __HAL_RCC_DMA1_CLK_ENABLE();
+  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
+  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
+}
+```
+
+**文件:`stm32/31_dma_mem2uart_hal/Core/Src/usart.c`**(HAL_UART_MspInit 中包含 DMA 初始化)
+
+```c
+void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
+{
+  if(uartHandle->Instance==USART1)
+  {
+    __HAL_RCC_USART1_CLK_ENABLE();
+    __HAL_RCC_GPIOA_CLK_ENABLE();
+
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    GPIO_InitStruct.Pin = GPIO_PIN_9;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+    GPIO_InitStruct.Pin = GPIO_PIN_10;
+    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
+    GPIO_InitStruct.Pull = GPIO_NOPULL;
+    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+    // USART1_TX DMA Init
+    DMA_HandleTypeDef hdma_usart1_tx;
+    hdma_usart1_tx.Instance = DMA1_Channel4;
+    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
+    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
+    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
+    hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
+    hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
+    hdma_usart1_tx.Init.Mode = DMA_CIRCULAR;
+    hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
+    HAL_DMA_Init(&hdma_usart1_tx);
+
+    __HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx);
+  }
+}
+```
+
+**文件:`stm32/31_dma_mem2uart_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "dma.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
+  MX_GPIO_Init();
+  MX_DMA_Init();
+  MX_USART1_UART_Init();
+
+  uint8_t src[] = {'a','b','c','d','e'};
+
+  HAL_UART_Transmit_DMA(&huart1, src, 5);
+
+  while (1)
+  {
+  }
+}
+```
+
+**文件:`stm32/31_dma_mem2uart_hal/Core/Src/stm32f1xx_it.c`**(DMA 中断)
+
+```c
+void DMA1_Channel4_IRQHandler(void)
+{
+  HAL_DMA_IRQHandler(&hdma_usart1_tx);
+}
+```
+
 ---
 
 ## HAL 库版 DMA 传输
 
-HAL 库提供了 `HAL_UART_Transmit_DMA()` 函数,封装了上述所有寄存器操作:
+HAL 库提供了简化的 DMA API,使用 `HAL_DMA_Start_IT()` 和 `HAL_UART_Transmit_DMA()` 函数封装寄存器操作:
 
 ```c
-// HAL 库 DMA 发送
+// HAL 库 DMA 内存到内存传输(通过注册完成回调)
+DMA_HandleTypeDef hdma_memtomem_dma1_channel1;
+
+void MX_DMA_Init(void)
+{
+    __HAL_RCC_DMA1_CLK_ENABLE();
+
+    hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1;
+    hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY;
+    hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE;
+    hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE;
+    hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
+    hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
+    hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL;
+    hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW;
+    HAL_DMA_Init(&hdma_memtomem_dma1_channel1);
+}
+
+// 注册传输完成回调 + 启动传输
+HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, MyCallback);
+HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src, (uint32_t)dest, 4);
+
+// HAL 库 DMA + USART 发送(一步到位)
 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 + USART 需在 HAL_UART_MspInit 中配置 DMA 通道
+// 关键步骤: __HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx);
 ```
 
 ---

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

@@ -133,8 +133,8 @@ ADCPRE 分频系数:
 | 寄存器 | 地址偏移 | 功能 |
 |--------|---------|------|
 | **SR** | 0x00 | 状态寄存器(EOC=转换结束,STRT=开始) |
-| **CR1** | 0x04 | 控制1(SCAN=扫描,JAUTO=自动注入,RES=分辨率) |
-| **CR2** | 0x08 | 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐) |
+| **CR1** | 0x04 | 控制1(SCAN=扫描,JAUTO=自动注入) |
+| **CR2** | 0x08 | 控制2(ADON=开启,CONT=连续,SWSTART=软触发,EXTTRIG=外部触发,ALIGN=对齐,DMA=DMA 使能) |
 | **SMPR1** | 0x0C | 采样时间寄存器1(通道10~17) |
 | **SMPR2** | 0x10 | 采样时间寄存器2(通道0~9) |
 | **SQR1** | 0x2C | 规则序列寄存器1(L=序列长度,SQ13~16) |
@@ -154,7 +154,6 @@ ADC 上电后需要校准以提高精度:
 // 校准流程
 ADC1->CR2 |= ADC_CR2_ADON;           // 开启 ADC
 // 等待稳定(约 2 个 ADC 时钟)
-Delay_us(1);
 
 ADC1->CR2 |= ADC_CR2_CAL;             // 开始校准
 while (ADC1->CR2 & ADC_CR2_CAL)       // 等待校准完成(CAL 位硬件自动清 0)
@@ -163,91 +162,118 @@ while (ADC1->CR2 & ADC_CR2_CAL)       // 等待校准完成(CAL 位硬件自
 // 校准完成,ADC 准备就绪
 ```
 
+HAL 库版本(CubeMX 自动在 `MX_ADC1_Init()` 后由用户手动调用):
+
+```c
+HAL_ADCEx_Calibration_Start(&hadc1);
+```
+
 > 校准原理:ADC 内部测量一次 VREFINT,根据误差调整内部电容阵列。
 
 ---
 
-## 实验:单通道 ADC 采集
+## 实验对比总览
+
+本笔记包含 4 个实验项目,覆盖单/双通道 × 寄存器/HAL 的全部组合:
+
+| 项目 | 通道数 | 实现方式 | DMA | 关键文件 |
+|------|--------|---------|-----|---------|
+| 32_adc_single_register | 单通道(PC0/CH10) | 寄存器 | 无 | `Hardware/ADC/adc.c/.h`, `User/main.c` |
+| 33_adc_single_hal | 单通道(PC0/CH10) | HAL | 无 | `Core/Src/adc.c`, `Core/Src/main.c` |
+| 34_adc_double_register | 双通道(PC0/CH10, PC2/CH12) | 寄存器 | DMA1 CH1 | `Hardware/ADC/adc.c/.h`, `User/main.c` |
+| 35_adc_double_hal | 双通道(PC0/CH10, PC2/CH12) | HAL | DMA1 CH1 | `Core/Src/adc.c`, `Core/Src/dma.c`, `Core/Src/main.c` |
+
+---
 
-### 软件设计(寄存器版 — 单次采集)
+## 实验一:单通道 ADC 采集(寄存器版
 
 **项目路径**:`stm32/32_adc_single_register`
 
 **需求**:使用 ADC1 通道 10(PC0)采集模拟电压,通过串口打印原始值和电压值。
 
-**文件:`stm32/32_adc_single_register/Hardware/ADC/adc.c`**
+**配置摘要**:
+- 单通道、连续转换、非扫描模式
+- ADC 时钟 = APB2/6 = 12MHz
+- 采样时间 = 7.5 周期
+
+**文件:`32_adc_single_register/Hardware/ADC/adc.h`**
+
+```c
+#ifndef __ADC_H
+#define __ADC_H
+
+#include "stm32f10x.h"
+
+void ADC1_Init(void);
+void ADC1_StartConvert(void);
+double ADC1_ReadV(void);
+
+#endif
+```
+
+**文件:`32_adc_single_register/Hardware/ADC/adc.c`**
 
 ```c
 #include "adc.h"
 
-// 初始化
 void ADC1_Init(void)
 {
     // 1. 时钟配置
     RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
-    // ADC的时钟频率不能超过14MHz,需预分频 10 - 6分频
-    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;
+    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
+    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
 
-    // RCC->APB2ENR | RCC_APB2ENR_IOPCEN;
-
-    // 2. GPIO配置模式:PC0 - 模拟输入,CNF = 00, MODE = 00
+    // 2. GPIO配置:PC0 - 模拟输入
     GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
 
     // 3. ADC配置
-    // 3.1 关闭扫描模式,只有1个转换通道
+    // 3.1 关闭扫描模式
     ADC1->CR1 &= ~ADC_CR1_SCAN;
 
-    // 3.2 开启连续转换模式
+    // 3.2 连续转换模式
     ADC1->CR2 |= ADC_CR2_CONT;
 
-    // 3.3 数据对齐方式:0 - 右对齐
+    // 3.3 右对齐
     ADC1->CR2 &= ~ADC_CR2_ALIGN;
 
-    // 3.4 配置采样时间,010 - 13.5个ADC时钟周期
+    // 3.4 采样时间:001 - 7.5周期
     ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
-    ADC1->SMPR1 |= ADC_SMPR1_SMP10_1;
+    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
 
-    // 3.5 配置通道序列
-    // 3.5.1 设置规则组通道数目L = 0000,1个通道
+    // 3.5 通道序列
+    // 3.5.1 L = 0,1个通道
     ADC1->SQR1 &= ~ADC_SQR1_L;
 
-    // 3.5.2 将通道号10写入 SQ1(在SQR3里)
+    // 3.5.2 SQ1 = 通道10
     ADC1->SQR3 &= ~ADC_SQR3_SQ1;
-    ADC1->SQR3 |= 10 << 0;      // 低5位,第一个通道号,设为10
-
-    // 3.6 选择外部触发方式(暂时注释)
-    // ADC1->CR2 |= ADC_CR2_EXTTRIG;
-    // ADC1->CR2 |= ADC_CR2_EXTSEL;
+    ADC1->SQR3 |= 10 << 0;
 }
 
-// 启动ADC开始转换
 void ADC1_StartConvert(void)
 {
-    // 1. 上电,将ADC从断电模式唤醒
+    // 1. 上电
     ADC1->CR2 |= ADC_CR2_ADON;
 
-    // 2. 执行校准,等待结束
+    // 2. 校准
     ADC1->CR2 |= ADC_CR2_CAL;
     while (ADC1->CR2 & ADC_CR2_CAL)
     {}
 
-    // 3. 连续方式启动AD转换
-    // ADC1->CR2 |= ADC_CR2_SWSTART;
+    // 3. 启动转换(连续模式)
     ADC1->CR2 |= ADC_CR2_ADON;
-    
-    // 4. 等待转换结束
+
+    // 4. 等待首次转换结束
     while ((ADC1->SR & ADC_SR_EOC) == 0)
     {}
 }
 
-// 获取转换后的电压值
 double ADC1_ReadV(void)
 {
     return ADC1->DR * 3.3 / 4095;
 }
 ```
 
-**文件:`stm32/32_adc_single_register/User/main.c`**
+**文件:`32_adc_single_register/User/main.c`**
 
 ```c
 #include "usart.h"
@@ -256,33 +282,166 @@ double ADC1_ReadV(void)
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	ADC1_Init();
+    USART_Init();
+    ADC1_Init();
+
+    printf("Hello, world!\n");
+
+    ADC1_StartConvert();
+
+    while (1)
+    {
+        printf("V = %.2f\n", ADC1_ReadV());
+        Delay_ms(1000);
+    }
+}
+```
+
+**说明**:连续转换模式下,只需在 `ADC1_StartConvert()` 中等待首次 EOC,后续硬件自动持续转换,`ADC1_ReadV()` 直接读取 DR 寄存器获得最新结果。
+
+---
+
+## 实验二:单通道 ADC 采集(HAL 版)
+
+**项目路径**:`stm32/33_adc_single_hal`
+
+**需求**:与实验一相同,使用 HAL 库实现 ADC1 通道 10(PC0)单通道连续采集。
+
+**文件:`33_adc_single_hal/Core/Src/adc.c`**
+
+```c
+#include "adc.h"
+
+ADC_HandleTypeDef hadc1;
+
+void MX_ADC1_Init(void)
+{
+    ADC_ChannelConfTypeDef sConfig = {0};
+
+    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;
+    if (HAL_ADC_Init(&hadc1) != HAL_OK)
+    {
+        Error_Handler();
+    }
+
+    sConfig.Channel = ADC_CHANNEL_10;
+    sConfig.Rank = ADC_REGULAR_RANK_1;
+    sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
+    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
+    {
+        Error_Handler();
+    }
+}
+
+void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if (adcHandle->Instance == ADC1)
+    {
+        __HAL_RCC_ADC1_CLK_ENABLE();
+        __HAL_RCC_GPIOC_CLK_ENABLE();
+
+        GPIO_InitStruct.Pin = GPIO_PIN_0;
+        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
+        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
+    }
+}
+
+void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
+{
+    if (adcHandle->Instance == ADC1)
+    {
+        __HAL_RCC_ADC1_CLK_DISABLE();
+        HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0);
+    }
+}
+```
+
+**文件:`33_adc_single_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "adc.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_ADC1_Init();
+    MX_USART1_UART_Init();
+
+    printf("Hello, world\n");
 
-	printf("Hello world!\n");
+    HAL_ADCEx_Calibration_Start(&hadc1);
 
-	// 启动AD转换
-	ADC1_StartConvert();
+    HAL_ADC_Start(&hadc1);
 
-	while (1)
-	{
-		printf("V = %.2f\n", ADC1_ReadV());
+    while (1)
+    {
+        double v = HAL_ADC_GetValue(&hadc1) * 3.3 / 4095;
+        printf("V = %.2f\n", v);
+        HAL_Delay(1000);
+    }
+}
 
-		Delay_ms(1000);
-	}
+void SystemClock_Config(void)
+{
+    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
+    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
+    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
+
+    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
+    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
+    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
+    RCC_OscInitStruct.HSIState = RCC_HSI_ON;
+    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
+    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
+    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
+    HAL_RCC_OscConfig(&RCC_OscInitStruct);
+
+    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
+                                | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
+    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
+    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
+    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
+    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
+    HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
+
+    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
+    PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
+    HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);
 }
 ```
 
+**说明**:
+- CubeMX 生成 `adc.c`(`MX_ADC1_Init` + MSP 回调)和 `gpio.c`(`MX_GPIO_Init`,使能 GPIOC 时钟)
+- 时钟配置在 `SystemClock_Config()` 中用 `RCC_ADCPCLK2_DIV6` 设置 ADC 预分频
+- 校准需用户手动调用 `HAL_ADCEx_Calibration_Start`(CubeMX 不自动执行)
+- 阻塞式读取:`HAL_ADC_Start` → 循环中 `HAL_ADC_GetValue`(连续模式下自动更新 DR)
+
 ---
 
-## 实验:双通道 ADC + DMA 采集
+## 实验:双通道 ADC + DMA 采集(寄存器版)
 
 **项目路径**:`stm32/34_adc_double_register`
 
-多通道 + DMA 是 ADC 的典型高效用法。实际项目中使用 ADC1 的**通道10(PC0)**和**通道12(PC2)**,通过 DMA1 通道1自动搬运转换结果:
+**需求**:使用 ADC1 通道 10(PC0)和通道 12(PC2)采集两路模拟电压,通过 DMA1 通道 1 自动搬运转换结果到内存数组。
+
+**配置摘要**:
+- 双通道、连续转换、**扫描模式**
+- DMA1 通道 1:外设→内存,16 位,内存地址递增,循环模式
+- 规则序列长度 L=1(2 个通道),SQ1=CH10, SQ2=CH12
 
-**文件:`stm32/34_adc_double_register/Hardware/ADC/adc.h`**
+**文件:`34_adc_double_register/Hardware/ADC/adc.h`**
 
 ```c
 #ifndef __ADC_H
@@ -290,212 +449,319 @@ int main(void)
 
 #include "stm32f10x.h"
 
-// 初始化
 void ADC1_Init(void);
-
-// DMA相关的初始化
 void ADC1_DMA_Init(void);
-
-// 启动ADC开始转换,DMA配置完成(源地址固定为ADC1->DR)
-void ADC1_StartConvert_DMA(uint32_t destAddr, uint16_t len);
+void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len);
 
 #endif
 ```
 
-**文件:`stm32/34_adc_double_register/Hardware/ADC/adc.c`**
+**文件:`34_adc_double_register/Hardware/ADC/adc.c`**
 
 ```c
 #include "adc.h"
 
-// 初始化
 void ADC1_Init(void)
 {
     // 1. 时钟配置
     RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
-    // ADC的时钟频率不能超过14MHz,需预分频 10 - 6分频
-    RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;
+    RCC->CFGR |= RCC_CFGR_ADCPRE_1;
+    RCC->CFGR &= ~RCC_CFGR_ADCPRE_0;
 
-    // RCC->APB2ENR | RCC_APB2ENR_IOPCEN;
+    RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
 
-    // 2. GPIO配置模式:PC0 - 模拟输入,CNF = 00, MODE = 00
+    // 2. GPIO配置:PC0、PC2 - 模拟输入
     GPIOC->CRL &= ~(GPIO_CRL_MODE0 | GPIO_CRL_CNF0);
-
-    // PC2 - 模拟输入,CNF = 00, MODE = 00
     GPIOC->CRL &= ~(GPIO_CRL_MODE2 | GPIO_CRL_CNF2);
 
     // 3. ADC配置
     // 3.1 开启扫描模式
     ADC1->CR1 |= ADC_CR1_SCAN;
 
-    // 3.2 开启连续转换模式
+    // 3.2 连续转换模式
     ADC1->CR2 |= ADC_CR2_CONT;
 
-    // 3.3 数据对齐方式:0 - 右对齐
+    // 3.3 右对齐
     ADC1->CR2 &= ~ADC_CR2_ALIGN;
 
-    // 3.4 配置采样时间,010 - 13.5个ADC时钟周期
+    // 3.4 采样时间:001 - 7.5周期(通道10 和 通道12)
     ADC1->SMPR1 &= ~ADC_SMPR1_SMP10;
-    ADC1->SMPR1 |= ADC_SMPR1_SMP10_1;
+    ADC1->SMPR1 |= ADC_SMPR1_SMP10_0;
     ADC1->SMPR1 &= ~ADC_SMPR1_SMP12;
+    ADC1->SMPR1 |= ADC_SMPR1_SMP12_0;
 
-    // 3.5 配置通道序列
-    // 3.5.1 设置规则组通道数目L = 0001,2个通道
+    // 3.5 通道序列
+    // 3.5.1 L = 1,2个通道
     ADC1->SQR1 &= ~ADC_SQR1_L;
     ADC1->SQR1 |= ADC_SQR1_L_0;
 
-    // 3.5.2 将通道号10写入 SQ1(在SQR3里)
+    // 3.5.2 SQ1 = 通道10, SQ2 = 通道12
     ADC1->SQR3 &= ~ADC_SQR3_SQ1;
-    ADC1->SQR3 |= 10 << 0;      // 低5位,第一个通道号,设为10
-
-    // 将通道号12写入 SQ2(在SQR3里)
+    ADC1->SQR3 |= 10 << 0;
     ADC1->SQR3 &= ~ADC_SQR3_SQ2;
-    ADC1->SQR3 |= 12 << 5;      // 中间5位,第二个通道号,设为12
-
-    // 3.6 选择外部触发方式(暂时注释)
-    // ADC1->CR2 |= ADC_CR2_EXTTRIG;
-    // ADC1->CR2 |= ADC_CR2_EXTSEL;
+    ADC1->SQR3 |= 12 << 5;
 }
 
-// DMA相关的初始化
 void ADC1_DMA_Init(void)
 {
-    // 1. 开启时钟
+    // 1. DMA时钟
     RCC->AHBENR |= RCC_AHBENR_DMA1EN;
 
-    // 2. DMA传输方向DIR = 0,从外设读取
+    // 2. 方向:从外设读取
     DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;
 
-    // 3. 数据宽度:01 - 16位
+    // 3. 数据宽度:16位
     DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE_1;
     DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0;
-
     DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE_1;
     DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0;
 
-    // 4. 地址是否增量:外设不增、存储器增
+    // 4. 地址增量:外设不增、存储器增
     DMA1_Channel1->CCR &= ~DMA_CCR1_PINC;
     DMA1_Channel1->CCR |= DMA_CCR1_MINC;
 
-    // 5. 开启循环模式
+    // 5. 循环模式
     DMA1_Channel1->CCR |= DMA_CCR1_CIRC;
 
-    // 6. ADC1使能DMA请求
+    // 6. ADC使能DMA请求
     ADC1->CR2 |= ADC_CR2_DMA;
 }
 
-// 启动ADC开始转换,DMA配置完成(源地址固定为ADC1->DR)
-void ADC1_StartConvert_DMA(uint32_t destAddr, uint16_t len)
+void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len)
 {
-    // 0. DMA配置参数:通道1
+    // 0. DMA参数配置
     DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR);
     DMA1_Channel1->CMAR = destAddr;
     DMA1_Channel1->CNDTR = len;
-
     DMA1_Channel1->CCR |= DMA_CCR1_EN;
 
-    // 1. 上电,将ADC从断电模式唤醒
+    // 1. 上电
     ADC1->CR2 |= ADC_CR2_ADON;
 
-    // 2. 执行校准,等待结束
+    // 2. 校准
     ADC1->CR2 |= ADC_CR2_CAL;
     while (ADC1->CR2 & ADC_CR2_CAL)
     {}
 
-    // 3. 连续方式启动AD转换
-    // ADC1->CR2 |= ADC_CR2_SWSTART;
+    // 3. 启动转换
     ADC1->CR2 |= ADC_CR2_ADON;
-    
-    // 4. 等待转换结束
+
+    // 4. 等待首次转换结束
     while ((ADC1->SR & ADC_SR_EOC) == 0)
     {}
 }
 ```
 
-**文件:`stm32/34_adc_double_register/User/main.c`**
+**文件:`34_adc_double_register/User/main.c`**
 
 ```c
 #include "usart.h"
 #include "adc.h"
 #include "delay.h"
 
-// 定义一个大小为2的数组,保存转换结果
-uint16_t data[2];
+uint16_t data[2] = {0};
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	ADC1_Init();
-	ADC1_DMA_Init();
-
-	printf("Hello world!\n");
+    USART_Init();
+    ADC1_Init();
+    ADC1_DMA_Init();
 
-	// 启动AD转换
-	ADC1_StartConvert_DMA((uint32_t)data, 2);
+    printf("Hello, world!\n");
 
-	while (1)
-	{
-		double v1 = data[0] * 3.3 / 4095;
-		double v2 = data[1] * 3.3 / 4095;
-		printf("V_PC0 = %.2f, V_PC2 = %.2f\n", v1, v2);
+    ADC1_DMA_StartConvert((uint32_t)data, 2);
 
-		Delay_ms(1000);
-	}
+    while (1)
+    {
+        printf("V_PC0 = %.2f, V_PC2 = %.2f\n",
+               data[0] * 3.3 / 4095, data[1] * 3.3 / 4095);
+        Delay_ms(1000);
+    }
 }
 ```
 
+**关键要点**:
+1. **扫描模式**(`CR1_SCAN`)必须开启,否则只能转换第一个通道
+2. **序列长度 L** = 通道数 - 1,即 `ADC_SQR1_L_0`(=1)表示 2 个通道
+3. DMA 的 **外设地址不增**(CPAR 固定指向 `ADC1->DR`)、**内存地址递增**(CMAR 指向数组,自动偏移)
+4. **循环模式**(CIRC)使 DMA 持续搬运,与 ADC 连续转换匹配
+5. `CNDTR` 初始值为 2,每完成一次 DMA 传输自动减 1,循环模式下到达 0 时自动重载
+
 ---
 
-## HAL 库版 ADC
+## 实验四:双通道 ADC + DMA 采集(HAL 版)
+
+**项目路径**:`stm32/35_adc_double_hal`
+
+**需求**:与实验三相同,使用 HAL 库 + CubeMX 实现双通道 DMA 采集。
+
+**文件:`35_adc_double_hal/Core/Src/adc.c`**
 
 ```c
-// HAL 库 ADC 初始化(CubeMX 生成)
+#include "adc.h"
+
 ADC_HandleTypeDef hadc1;
+DMA_HandleTypeDef hdma_adc1;
 
 void MX_ADC1_Init(void)
 {
+    ADC_ChannelConfTypeDef sConfig = {0};
+
     hadc1.Instance = ADC1;
-    hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;     // 非扫描
-    hadc1.Init.ContinuousConvMode = ENABLE;          // 连续转换
+    hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
+    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);
+    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
+    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
+    hadc1.Init.NbrOfConversion = 2;
+    if (HAL_ADC_Init(&hadc1) != HAL_OK)
+    {
+        Error_Handler();
+    }
+
+    sConfig.Channel = ADC_CHANNEL_10;
+    sConfig.Rank = ADC_REGULAR_RANK_1;
+    sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
+    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
+    {
+        Error_Handler();
+    }
+
+    sConfig.Channel = ADC_CHANNEL_12;
+    sConfig.Rank = ADC_REGULAR_RANK_2;
+    if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
+    {
+        Error_Handler();
+    }
+}
+
+void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if (adcHandle->Instance == ADC1)
+    {
+        __HAL_RCC_ADC1_CLK_ENABLE();
+        __HAL_RCC_GPIOC_CLK_ENABLE();
+
+        GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_2;
+        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
+        HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
+
+        // ADC1 DMA Init
+        hdma_adc1.Instance = DMA1_Channel1;
+        hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
+        hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
+        hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
+        hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
+        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
+        hdma_adc1.Init.Mode = DMA_CIRCULAR;
+        hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
+        HAL_DMA_Init(&hdma_adc1);
+
+        __HAL_LINKDMA(adcHandle, DMA_Handle, hdma_adc1);
+    }
+}
+
+void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
+{
+    if (adcHandle->Instance == ADC1)
+    {
+        __HAL_RCC_ADC1_CLK_DISABLE();
+        HAL_GPIO_DeInit(GPIOC, GPIO_PIN_0 | GPIO_PIN_2);
+        HAL_DMA_DeInit(adcHandle->DMA_Handle);
+    }
 }
+```
+
+**文件:`35_adc_double_hal/Core/Src/dma.c`**
+
+```c
+#include "dma.h"
+
+void MX_DMA_Init(void)
+{
+    __HAL_RCC_DMA1_CLK_ENABLE();
+}
+```
+
+**文件:`35_adc_double_hal/Core/Src/gpio.c`**
+
+```c
+#include "gpio.h"
+
+void MX_GPIO_Init(void)
+{
+    __HAL_RCC_GPIOC_CLK_ENABLE();
+    __HAL_RCC_GPIOA_CLK_ENABLE();
+}
+```
 
-// 阻塞式读取
-HAL_ADC_Start(&hadc1);
-HAL_ADC_PollForConversion(&hadc1, 100);
-uint16_t val = HAL_ADC_GetValue(&hadc1);
+**文件:`35_adc_double_hal/Core/Src/main.c`**
 
-// DMA 方式
-HAL_ADC_Start_DMA(&hadc1, buffer, size);
-// 完成回调: HAL_ADC_ConvCpltCallback(&hadc1)
+```c
+#include "main.h"
+#include "adc.h"
+#include "dma.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_DMA_Init();
+    MX_ADC1_Init();
+    MX_USART1_UART_Init();
+
+    printf("Hello, world\n");
+
+    uint16_t data[2] = {0};
+
+    HAL_ADCEx_Calibration_Start(&hadc1);
+
+    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)data, 2);
+
+    while (1)
+    {
+        printf("V_PC0 = %.2f, V_PC2 = %.2f\n",
+               data[0] * 3.3 / 4095, data[1] * 3.3 / 4095);
+        HAL_Delay(1000);
+    }
+}
 ```
 
+**关键要点**:
+1. **初始化顺序很重要**:`MX_GPIO_Init()` → `MX_DMA_Init()` → `MX_ADC1_Init()`。DMA 必须在 ADC 之前初始化,因为 `HAL_ADC_Init()` 内部会调用 `HAL_ADC_MspInit()`,而 MSP 中配置了 DMA(`__HAL_LINKDMA`)。
+2. `MX_DMA_Init()` 仅使能 DMA1 时钟,实际的 DMA 通道配置在 `HAL_ADC_MspInit()` 中完成。
+3. CubeMX 生成代码时,采样时间可能为 `ADC_SAMPLETIME_1CYCLE_5`(默认),实际项目中需根据信号源阻抗调整。
+4. DMA 完成回调 `HAL_ADC_ConvCpltCallback` 可通过重载实现每轮转换完成后的处理。
+
 ---
 
 ## 核心函数速查表
 
-| 操作 | 寄存器版 | 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()` |
+| 操作 | 寄存器版(单通道) | HAL 版(单通道) | 寄存器版(双通道+DMA) | HAL 版(双通道+DMA) |
+|------|-------------------|-----------------|----------------------|-------------------|
+| 开启 ADC | `CR2 |= ADON` | `HAL_ADC_Init()` | `CR2 |= ADON` | `HAL_ADC_Init()` |
+| 校准 | `CR2 |= CAL; while(CR2 & CAL)` | `HAL_ADCEx_Calibration_Start()` | `CR2 |= CAL; while(CR2 & CAL)` | `HAL_ADCEx_Calibration_Start()` |
+| 配置通道 | `SQR3.SQ1 = ch` | `ADC_ChannelConfTypeDef + HAL_ADC_ConfigChannel` | `SQR3.SQ1/SQ2 = ch` | 同上(多通道重复调用) |
+| 启动转换 | `CR2 |= ADON` | `HAL_ADC_Start()` | `CR2 |= ADON`(含 DMA 参数) | `HAL_ADC_Start_DMA()` |
+| 查询完成 | `while(!(SR & EOC))` | `HAL_ADC_PollForConversion()` | `while(!(SR & EOC))`(首次等待) | 回调 `HAL_ADC_ConvCpltCallback` |
+| 读取结果 | `val = DR` | `HAL_ADC_GetValue()` | DMA 自动搬运到 `data[]` | DMA 自动搬运到 `data[]` |
+| DMA 配置 | N/A | N/A | 手动配置 DMA1 寄存器 | `MX_DMA_Init()` + MSP 中 `HAL_DMA_Init()` |
+| 电压换算 | `val × 3.3 / 4095` | 同上 | `data[n] × 3.3 / 4095` | 同上 |
+| 停止 ADC | `CR2 &= ~ADON` | `HAL_ADC_Stop()` | `CR2 &= ~ADON` + `CCR &= ~EN` | `HAL_ADC_Stop_DMA()` |
+
+---
 
 ## 常见问题与避坑
 
 1. **ADC 值始终为 0** → 检查 GPIO 模式(必须是**模拟输入**,不是浮空输入)、ADC 是否校准
 2. **ADC 值始终为 4095** → 输入引脚悬空(感应噪声)、输入电压超过 VREF+
 3. **ADC 值跳动很大** → 采样时间太短(增加采样周期数)、电源去耦不良、PCB 布线噪声
-4. **多通道值错乱** → SQR 序列配置错误、DMA 缓冲区大小与通道数不匹配
+4. **多通道值错乱** → SQR 序列配置错误、DMA 缓冲区大小与通道数不匹配、未开启扫描模式
 5. **ADC 时钟 >14MHz** → 转换结果不准确,必须确保 ADCPRE 分频后 ADC_CLK ≤ 14MHz
 6. **电压换算不准** → VREF+ 实际电压可能是 3.28V 而非 3.30V,需用万用表测量后修正
 7. **DMA 传输不停止** → CIRC(循环模式)导致持续传输,停止时需要先清 DMA EN 再清 CIRC
+8. **HAL 双通道 DMA 数据错位** → 确认 `MX_DMA_Init()` 在 `MX_ADC1_Init()` **之前**调用,否则 MSP 中的 `__HAL_LINKDMA` 不会生效

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

@@ -95,13 +95,15 @@ MISO 逐位输入 ← 移位寄存器(8位) ← RX 缓冲区 → 读 DR
 
 ---
 
-## 软件 SPI 实现
+## 项目 36:软件 SPI(寄存器版)
 
-### 软件设计(寄存器版 — 软件模拟 SPI)
+> 使用 GPIO 位操作模拟 SPI 时序,纯软件实现,不依赖硬件 SPI 外设。
 
-**项目路径**:`stm32/36_spi_software_register`
+**项目路径**:`上部-基础篇\03_代码\stm32\36_spi_software_register`
 
-**文件:`stm32/36_spi_software_register/Hardware/SPI/spi.h`**
+### spi.h
+
+**文件**:`Hardware/SPI/spi.h`
 
 ```c
 #ifndef __SPI_H
@@ -110,44 +112,40 @@ MISO 逐位输入 ← 移位寄存器(8位) ← RX 缓冲区 → 读 DR
 #include "stm32f10x.h"
 #include "delay.h"
 
-// 宏定义:SPI总线的操作
+// 宏定义:控制各信号线高低电平
 // CS - PC13
 #define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
-#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
+#define CS_LOW  (GPIOC->ODR &= ~GPIO_ODR_ODR13)
 
 // SCK - PA5
 #define SCK_HIGH (GPIOA->ODR |= GPIO_ODR_ODR5)
-#define SCK_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR5)
+#define SCK_LOW  (GPIOA->ODR &= ~GPIO_ODR_ODR5)
 
 // MOSI - PA7
 #define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)
-#define MOSI_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR7)
+#define MOSI_LOW  (GPIOA->ODR &= ~GPIO_ODR_ODR7)
 
 // MISO - PA6,读取输入
-#define MISO_READ ( GPIOA->IDR & GPIO_IDR_IDR6 )
+#define MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)
 
 // 产生标准的延迟时间
 #define SPI_DELAY Delay_us(5)
 
-// 初始化
 void SPI_Init(void);
-
-// SPI通信的开启和关闭
 void SPI_Start(void);
 void SPI_Stop(void);
-
-// 一个时钟周期内,主机交换一个字节数据
 uint8_t SPI_SwapByte(uint8_t byte);
 
 #endif
 ```
 
-**文件:`stm32/36_spi_software_register/Hardware/SPI/spi.c`**
+### spi.c
+
+**文件**:`Hardware/SPI/spi.c`
 
 ```c
 #include "spi.h"
 
-// 初始化
 void SPI_Init(void)
 {
     // 1. 开启时钟
@@ -155,19 +153,19 @@ void SPI_Init(void)
     RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
 
     // 2. GPIO配置模式
-    // 2.1 CS - PC13,通用推挽输出,CNF = 00,MODE = 11
+    // CS - PC13,通用推挽输出,CNF = 00,MODE = 11
     GPIOC->CRH |= GPIO_CRH_MODE13;
     GPIOC->CRH &= ~GPIO_CRH_CNF13;
 
-    // 2.2 SCK - PA5,通用推挽输出,CNF = 00,MODE = 11
+    // SCK - PA5,通用推挽输出,CNF = 00,MODE = 11
     GPIOA->CRL |= GPIO_CRL_MODE5;
     GPIOA->CRL &= ~GPIO_CRL_CNF5;
 
-    // 2.3 MOSI - PA7,通用推挽输出,CNF = 00,MODE = 11
+    // MOSI - PA7,通用推挽输出,CNF = 00,MODE = 11
     GPIOA->CRL |= GPIO_CRL_MODE7;
     GPIOA->CRL &= ~GPIO_CRL_CNF7;
 
-    // 2.4 MISO - PA6,浮空输入,CNF = 01,MODE = 00
+    // MISO - PA6,浮空输入,CNF = 01,MODE = 00
     GPIOA->CRL &= ~GPIO_CRL_MODE6;
     GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
     GPIOA->CRL |= GPIO_CRL_CNF6_0;
@@ -182,108 +180,89 @@ void SPI_Init(void)
     SPI_DELAY;
 }
 
-// SPI通信的开启和关闭(通过片选信号控制)
-void SPI_Start(void)
-{
-    CS_LOW;
-}
-void SPI_Stop(void)
-{
-    CS_HIGH;
-}
+void SPI_Start(void) { CS_LOW; }
+void SPI_Stop(void)  { CS_HIGH; }
 
-// 一个时钟周期内,主机交换一个字节数据
 uint8_t SPI_SwapByte(uint8_t byte)
 {
-    // 交换字节并返回接收数据
     uint8_t rByte = 0x00;
 
-    // 通过循环每次读写一位
     for (uint8_t i = 0; i < 8; i++)
     {
         // 1. 判断当前最高位,向MOSI输出相应电平
-        if ( byte & 0x80 )
-        {
-            MOSI_HIGH;
-        }
-        else
-        {
-            MOSI_LOW;
-        }
+        if (byte & 0x80) MOSI_HIGH;
+        else             MOSI_LOW;
 
-        // 2. 移位
         byte <<= 1;
-        
-        // 3. 输出时钟,在第一个时钟沿产生上升沿
+
+        // 2. 输出时钟上升沿
         SCK_HIGH;
         SPI_DELAY;
 
-        // 4. 移位,腾出最低位用来接收数据
+        // 3. 移位,腾出最低位用来接收
         rByte <<= 1;
+        if (MISO_READ) rByte |= 0x01;
 
-        // 5. 根据 MISO 电平来接收
-        if ( MISO_READ )
-        {
-            rByte |= 0x01;  // 高电平则最低位置1
-        }
-
-        // 6. 下降沿,为下次传输准备
+        // 4. 下降沿,为下次传输准备
         SCK_LOW;
         SPI_DELAY;
     }
-    
+
     return rByte;
 }
 ```
 
-**文件:`stm32/36_spi_software_register/User/main.c`**
+### main.c
+
+**文件**:`User/main.c`
 
 ```c
 #include "usart.h"
 #include "w25q32.h"
+#include <string.h>
 
 int main(void)
 {
-	// 1. 初始化
-	USART_Init();
-	W25Q32_Init();
-
-	printf("中国芯SPI通信模块实验...\n");
+    USART_Init();
+    W25Q32_Init();
 
-	// 2. 获取JEDEC ID
-	uint8_t mID = 0;
-	uint16_t dID = 0;
+    printf("中国芯SPI通信模块实验开始...\n");
 
-	W25Q32_ReadID(&mID, &dID);
-	printf("mid = %#x, did = %#x\n", mID, dID);
+    // 读取ID进行检验
+    uint8_t mid = 0;
+    uint16_t did = 0;
+    W25Q32_ReadID(&mid, &did);
+    printf("mid = %#x, did = %#x\n", mid, did);
 
-	// 3. 写数据前需要先擦除一个扇区
-	W25Q32_EraseSector(0, 0);
+    // 扇区擦除
+    W25Q32_EraseSector(0, 0);
 
-	// 4. 页写入一页数据
-	W25Q32_WritePage(0, 0, 0, "12345678", 8);
+    // 页写入
+    W25Q32_PageWrite(0, 0, 0, "12345678", 8);
 
-	// 5. 读取
-	uint8_t buff[10] = {0};
-	W25Q32_Read(0x0, buff, 8);
+    // 读取
+    uint8_t buffer[10] = {0};
+    W25Q32_Read(0, 0, 0, 2, buffer, 6);
 
-	printf("buff: %s\n", buff);
+    printf("buffer = %s\n", buffer);
 
-	while (1)
-	{
-	}
+    while (1) {}
 }
 ```
 
-> **注意**:W25Q32 是 4MB(32Mbit)Flash,Page=256B,Sector=4KB,Block=64KB。与 W25Q64(8MB)指令集完全兼容,仅容量减半
+> **软件 SPI vs 硬件 SPI**:软件 SPI 不依赖片内外设,任意 GPIO 均可模拟,但速度受限(约 2~4MHz)。适合低速设备或硬件 SPI 引脚被占用的场景
 
 ---
 
-## 硬件 SPI(stm32/37_spi_hardware_register)
+## 项目 37:硬件 SPI(寄存器版)
+
+> 使用 STM32 片内 SPI 外设,硬件自动移位,效率更高。CS 片选仍用 GPIO(PC13)独立控制,SCK/MOSI 配置为复用推挽输出。
+
+**项目路径**:`上部-基础篇\03_代码\stm32\37_spi_hardware_register`
 
-硬件 SPI 使用 STM32 片内外设,更高效(硬件自动移位)。实际项目中 CS 片选仍用 GPIO(PC13)独立控制,SCK/MOSI 配置为复用推挽输出:
+### spi.h
 
-**文件:`stm32/37_spi_hardware_register/Hardware/SPI/spi.h`**
+**文件**:`Hardware/SPI/spi.h`
 
 ```c
 #ifndef __SPI_H
@@ -291,30 +270,25 @@ int main(void)
 
 #include "stm32f10x.h"
 
-// 宏定义:SPI总线的操作
-// CS - PC13
+// CS - PC13(片选仍用GPIO独立控制)
 #define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
-#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
+#define CS_LOW  (GPIOC->ODR &= ~GPIO_ODR_ODR13)
 
-// 初始化
 void SPI_Init(void);
-
-// SPI通信的开启和关闭
 void SPI_Start(void);
 void SPI_Stop(void);
-
-// 一个时钟周期内,主机交换一个字节数据
 uint8_t SPI_SwapByte(uint8_t byte);
 
 #endif
 ```
 
-**文件:`stm32/37_spi_hardware_register/Hardware/SPI/spi.c`**
+### spi.c
+
+**文件**:`Hardware/SPI/spi.c`
 
 ```c
 #include "spi.h"
 
-// 初始化
 void SPI_Init(void)
 {
     // 1. 开启时钟
@@ -323,95 +297,235 @@ void SPI_Init(void)
     RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
 
     // 2. GPIO配置模式
-    // 2.1 CS - PC13,通用推挽输出,CNF = 00,MODE = 11
+    // PC13:通用推挽输出(CS)
     GPIOC->CRH |= GPIO_CRH_MODE13;
     GPIOC->CRH &= ~GPIO_CRH_CNF13;
 
-    // 2.2 SCK - PA5,复用推挽输出,CNF = 10,MODE = 11
+    // PA5(SCK)、PA7(MOSI):复用推挽输出,CNF = 10
     GPIOA->CRL |= GPIO_CRL_MODE5;
     GPIOA->CRL |= GPIO_CRL_CNF5_1;
     GPIOA->CRL &= ~GPIO_CRL_CNF5_0;
 
-    // 2.3 MOSI - PA7,复用推挽输出,CNF = 10,MODE = 11
     GPIOA->CRL |= GPIO_CRL_MODE7;
     GPIOA->CRL |= GPIO_CRL_CNF7_1;
     GPIOA->CRL &= ~GPIO_CRL_CNF7_0;
 
-    // 2.4 MISO - PA6,浮空输入,CNF = 01,MODE = 00
+    // PA6(MISO):浮空输入
     GPIOA->CRL &= ~GPIO_CRL_MODE6;
     GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
     GPIOA->CRL |= GPIO_CRL_CNF6_0;
 
     // 3. SPI模块参数配置
-    // 3.1 通信模式配置
-    // 3.1.1 设为主机模式
+    // 3.1 设为主机模式
     SPI1->CR1 |= SPI_CR1_MSTR;
 
-    // 3.1.2 选择软件控制片选,NSS电平为高电平
+    // 3.2 软件控制片选,NSS电平为高
     SPI1->CR1 |= SPI_CR1_SSM;
     SPI1->CR1 |= SPI_CR1_SSI;
 
-    // 3.1.2 SPI通信模式设为模式0:CPOL = 0,CPHA = 0
-    SPI1->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA);
+    // 3.3 模式0:CPOL = 0,CPHA = 0
+    SPI1->CR1 &= ~SPI_CR1_CPOL;
+    SPI1->CR1 &= ~SPI_CR1_CPHA;
 
-    // 3.2 设置波特率,时钟分频系数:001 - 4分频,18MHz
+    // 3.4 波特率:4分频(18MHz,当APB2=72MHz时)
     SPI1->CR1 &= ~SPI_CR1_BR;
     SPI1->CR1 |= SPI_CR1_BR_0;
 
-    // 3.3 帧格式配置
-    // 3.3.1 帧长度:0 - 8位
+    // 3.5 帧格式:8位,MSB在前
     SPI1->CR1 &= ~SPI_CR1_DFF;
-
-    // 3.3.2 数据传输顺序:0 - MSB在前
     SPI1->CR1 &= ~SPI_CR1_LSBFIRST;
 
-    // 3.4 使能SPI
+    // 3.6 使能SPI
     SPI1->CR1 |= SPI_CR1_SPE;
 }
 
-// SPI通信的开启和关闭(通过片选信号控制)
+void SPI_Start(void) { CS_LOW; }
+void SPI_Stop(void)  { CS_HIGH; }
+
+uint8_t SPI_SwapByte(uint8_t byte)
+{
+    // 1. 等待发送缓冲区为空(TXE = 1)
+    while ((SPI1->SR & SPI_SR_TXE) == 0) {}
+
+    // 2. 将数据写入DR
+    SPI1->DR = byte;
+
+    // 3. 等待接收缓冲区非空(RXNE = 1)
+    while ((SPI1->SR & SPI_SR_RXNE) == 0) {}
+
+    // 4. 从DR读取接收到的数据
+    return (uint8_t)(SPI1->DR & 0xff);
+}
+```
+
+> **关键差异**:SCK/MOSI 配置为 `复用推挽输出(CNF=10)`,由 SPI 外设自动控制电平,CPU 只需读写 DR 寄存器。CS 仍为通用推挽输出(GPIO 控制)。
+
+### main.c
+
+与项目 36 的 main.c 完全相同(`W25Q32_*` 函数适配 `SPI_SwapByte` 接口即可)。
+
+---
+
+## 项目 38:硬件 SPI(HAL 库版)
+
+> 使用 STM32CubeMX 生成初始化代码,HAL 库封装 SPI 外设。CS 片选仍由 GPIO 独立控制,CubeMX 中配置 PC13 为普通输出。
+
+**项目路径**:`上部-基础篇\03_代码\stm32\38_spi_hardware_hal`
+
+### CubeMX 生成:spi.c
+
+**文件**:`Core/Src/spi.c`
+
+```c
+SPI_HandleTypeDef hspi1;
+
+void MX_SPI1_Init(void)
+{
+    hspi1.Instance = SPI1;
+    hspi1.Init.Mode = SPI_MODE_MASTER;
+    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
+    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
+    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
+    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
+    hspi1.Init.NSS = SPI_NSS_SOFT;
+    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
+    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
+    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
+    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
+    hspi1.Init.CRCPolynomial = 10;
+    if (HAL_SPI_Init(&hspi1) != HAL_OK)
+    {
+        Error_Handler();
+    }
+}
+
+void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if (spiHandle->Instance == SPI1)
+    {
+        __HAL_RCC_SPI1_CLK_ENABLE();
+        __HAL_RCC_GPIOA_CLK_ENABLE();
+
+        // PA5(SCK), PA7(MOSI) — 复用推挽输出
+        GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;
+        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+
+        // PA6(MISO) — 浮空输入
+        GPIO_InitStruct.Pin = GPIO_PIN_6;
+        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
+        GPIO_InitStruct.Pull = GPIO_NOPULL;
+        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
+    }
+}
+```
+
+### CS 引脚定义(CubeMX 生成)
+
+**文件**:`Core/Inc/main.h`
+
+```c
+#define CS_Pin       GPIO_PIN_13
+#define CS_GPIO_Port GPIOC
+```
+
+**文件**:`Core/Src/gpio.c`
+
+```c
+void MX_GPIO_Init(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+
+    __HAL_RCC_GPIOC_CLK_ENABLE();
+    __HAL_RCC_GPIOA_CLK_ENABLE();
+
+    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
+
+    GPIO_InitStruct.Pin = CS_Pin;
+    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
+    GPIO_InitStruct.Pull = GPIO_NOPULL;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(CS_GPIO_Port, &GPIO_InitStruct);
+}
+```
+
+### 用户添加代码(spi.c USER CODE 段)
+
+```c
 void SPI_Start(void)
 {
-    CS_LOW;
+    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
 }
+
 void SPI_Stop(void)
 {
-    CS_HIGH;
+    HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
 }
 
-// 一个时钟周期内,主机交换一个字节数据
 uint8_t SPI_SwapByte(uint8_t byte)
 {
-    // 1. 将要发送的数据写入发送缓冲区
-    // 1.1 等待发送缓冲区为空(TXE = 1)
-    while ((SPI1->SR & SPI_SR_TXE) == 0 )
-    {}
-    
-    // 1.2 将数据写入DR
-    SPI1->DR = byte;
+    uint8_t rByte;
+    HAL_SPI_TransmitReceive(&hspi1, &byte, &rByte, 1, 1000);
+    return rByte;
+}
+```
 
-    // 2. 获取接收到的数据并返回
-    // 2.1 等待接收缓冲区非空(RXNE = 1)
-    while ( (SPI1->SR & SPI_SR_RXNE) == 0 )
-    {}
-    
-    // 2.2 将DR中的数据返回
-    return (uint8_t)(SPI1->DR & 0xff);
+### main.c
+
+**文件**:`Core/Src/main.c`
+
+```c
+#include "main.h"
+#include "spi.h"
+#include "usart.h"
+#include "gpio.h"
+#include "w25q32.h"
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+
+    MX_GPIO_Init();
+    MX_SPI1_Init();
+    MX_USART1_UART_Init();
+
+    printf("中国芯SPI通信模块实验开始...\n");
+
+    uint8_t mid = 0;
+    uint16_t did = 0;
+    W25Q32_ReadID(&mid, &did);
+    printf("mid = %#x, did = %#x\n", mid, did);
+
+    W25Q32_EraseSector(0, 0);
+    W25Q32_PageWrite(0, 0, 0, "12345678", 8);
+
+    uint8_t buffer[10] = {0};
+    W25Q32_Read(0, 0, 0, 2, buffer, 6);
+    printf("buffer = %s\n", buffer);
+
+    while (1) {}
 }
 ```
 
+> HAL 库优势:初始化代码由 CubeMX 生成,跨芯片移植方便;`HAL_SPI_TransmitReceive` 封装了 TXE/RXNE 轮询逻辑。但仍有轮询等待开销,大批量数据建议使用 DMA 方式。
+
 ---
 
-## W25Q64 Flash 驱动
+## W25Q64 / W25Q32 Flash 驱动
+
+| 参数 | W25Q64 | W25Q32 |
+|------|--------|--------|
+| 容量 | 8MB (64Mbit) | 4MB (32Mbit) |
+| 页(Page) | 256 字节 | 256 字节 |
+| 扇区(Sector) | 4KB (16 页) | 4KB (16 页) |
+| 块(Block) | 64KB (16 扇区) | 64KB (16 扇区) |
+| 擦除时间(扇区) | 典型 45ms | 典型 45ms |
+| 写寿命 | 100,000 次 | 100,000 次 |
 
-| 参数 | 值 |
-|------|-----|
-| 容量 | 8MB (64Mbit) |
-| 页(Page) | 256 字节 |
-| 扇区(Sector) | 4KB (16 页) |
-| 块(Block) | 64KB (16 扇区) |
-| 擦除时间(扇区) | 典型 45ms |
-| 写寿命 | 100,000 次 |
+两者指令集完全兼容,容量减半。以下驱动使用 `SPI_SwapByte` 接口,三个项目(36/37/38)均可直接复用。
 
 ### 常用指令集
 
@@ -429,18 +543,10 @@ uint8_t SPI_SwapByte(uint8_t byte)
 | CHIP_ERASE | 0xC7 | 全片擦除 | — |
 | RDID | 0x9F | 读芯片 ID | 3字节返回 |
 
-### W25Q64/W25Q32 擦写(读写前必须擦除)
-
-> **注意**:实际项目中使用 **W25Q32**(4MB/32Mbit),与 W25Q64 指令集完全兼容,容量减半。以下代码使用 `SPI_SwapByte` 函数(软件/硬件 SPI 驱动均为此函数名)。
+### 核心驱动逻辑
 
 ```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;
@@ -458,26 +564,25 @@ void W25Q64_WaitBusy(void)
 
 void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len)
 {
-    W25Q64_WaitBusy();          // 等上次完成
+    W25Q64_WaitBusy();
     CS_LOW;
-    SPI_SwapByte(WREN);         // 写使能
+    SPI_SwapByte(WREN);          // 写使能
     CS_HIGH;
 
     CS_LOW;
-    SPI_SwapByte(PAGE_PROG);    // 页编程指令
-    SPI_SwapByte(addr >> 16);   // 地址高8位
-    SPI_SwapByte(addr >> 8);    // 地址中8位
-    SPI_SwapByte(addr);         // 地址低8位
+    SPI_SwapByte(PAGE_PROG);     // 页编程指令
+    SPI_SwapByte(addr >> 16);    // 地址高8位
+    SPI_SwapByte(addr >> 8);     // 地址中8位
+    SPI_SwapByte(addr);          // 地址低8位
     for (uint16_t i = 0; i < len; i++) {
-        SPI_SwapByte(data[i]);  // 发送数据
+        SPI_SwapByte(data[i]);
     }
     CS_HIGH;
-    W25Q64_WaitBusy();          // 等写入完成
+    W25Q64_WaitBusy();
 }
 ```
 
-> **注意**:W25Q64/W25Q32 不支持"写覆盖"——必须先擦除再写。最小擦除单位是扇区(4KB)。
-> 页编程不能跨页(256 字节边界)。如果数据跨页,需要分多次写入。
+> **注意**:Flash 不能写覆盖——必须先擦除再写。最小擦除单位是扇区(4KB)。页编程不能跨页(256 字节边界)。
 
 ---
 
@@ -500,8 +605,8 @@ Bank1 分为 4 个子区(片选 NE1~NE4),各 64MB:
 |------|---------|---------|
 | NE1 | FSMC_NE1 | 0x60000000~0x63FFFFFF(最常用) |
 | NE2 | FSMC_NE2 | 0x64000000~0x67FFFFFF |
-| NE3 | FSMC_NE3 | 0x68000000~0x6BFFFFFF |
-| NE4 | FSMC_NE4 | 0x6C000000~0x6FFFFFFF |
+| NE3 | FSMC_NE3 | 0x68000000~0x6BFFFFFF(SRAM 实验用) |
+| NE4 | FSMC_NE4 | 0x6C000000~0x6FFFFFFF(LCD 实验用) |
 
 ### FSMC 信号线
 
@@ -537,13 +642,17 @@ Bank1 分为 4 个子区(片选 NE1~NE4),各 64MB:
 | 8:15 | DATAST | 数据保持时间(1~255个HCLK周期) |
 | 16:19 | BUSTURN | 总线周转时间 |
 
-### 实验:FSMC 扩展 SRAM(寄存器版)
+---
+
+## 项目 39:FSMC 扩展 SRAM(寄存器版)
 
-**项目路径**:`stm32/39_fsmc_sram_register`
+> 使用 FSMC Bank1 子区 3(NE3,片选 PG10),地址范围 0x68000000~0x6BFFFFFF,16 位数据总线连接外部 SRAM。
 
-使用 FSMC 的 Bank1 子区3(NE3,地址范围 0x68000000~0x6BFFFFFF),16 位数据总线连接外部 SRAM。
+**项目路径**:`上部-基础篇\03_代码\stm32\39_fsmc_sram_register`
 
-**文件:`stm32/39_fsmc_sram_register/Hardware/FSMC/fsmc.h`**
+### fsmc.h
+
+**文件**:`Hardware/FSMC/fsmc.h`
 
 ```c
 #ifndef __FSMC_H
@@ -551,66 +660,55 @@ Bank1 分为 4 个子区(片选 NE1~NE4),各 64MB:
 
 #include "stm32f10x.h"
 
-// 初始化
 void FSMC_Init(void);
 
 #endif
 ```
 
-**文件:`stm32/39_fsmc_sram_register/Hardware/FSMC/fsmc.c`**
+### fsmc.c
+
+**文件**:`Hardware/FSMC/fsmc.c`
 
 ```c
 #include "fsmc.h"
 
 void FSMC_GPIO_Init(void);
 
-// 初始化
 void FSMC_Init(void)
 {
     // 1. 开启时钟
     RCC->AHBENR |= RCC_AHBENR_FSMCEN;
-
     RCC->APB2ENR |= (RCC_APB2ENR_IOPDEN | RCC_APB2ENR_IOPEEN |
                      RCC_APB2ENR_IOPFEN | RCC_APB2ENR_IOPGEN);
 
     // 2. GPIO模式配置
     FSMC_GPIO_Init();
 
-    // 3. FSMC寄存器配置
-    // 3.1 BCR3 - BTCR[4]
-    // 3.1.1 存储区使能
+    // 3. FSMC寄存器配置 — BCR3(BTCR[4])
+    // 存储区使能
     FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MBKEN;
-
-    // 3.1.2 设置存储器类型:MTYP = 00,SRAM/ROM
+    // 存储器类型:SRAM(MTYP = 00)
     FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MTYP;
-
-    // 3.1.3 禁止Flash访问
+    // 禁止Flash访问
     FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_FACCEN;
-
-    // 3.1.4 地址数据复用功能,不使能
+    // 禁止地址数据复用
     FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MUXEN;
-
-    // 3.1.5 设置总线宽度:MWID = 01,16位
+    // 总线宽度:16位(MWID = 01)
     FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MWID_1;
     FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MWID_0;
-
-    // 3.1.6 使能写操作
+    // 使能写操作
     FSMC_Bank1->BTCR[4] |= FSMC_BCR3_WREN;
 
-    // 3.2 BTR - BTCR[5]
-    // 3.2.1 地址建立时间 ADDSET
+    // 4. 时序配置 — BTR3(BTCR[5])
     FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_ADDSET;
-
-    // 3.2.2 数据建立时间 DATAST
     FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_DATAST;
-    FSMC_Bank1->BTCR[5] |= (71 << 8);
+    FSMC_Bank1->BTCR[5] |= (71 << 8);  // DATAST = 71
 }
 
-// 配置GPIO引脚,均为复用推挽输出(CNF = 10,MODE = 11)
 void FSMC_GPIO_Init(void)
 {
-    // 1. 地址线 A0 ~ A18
-    // MODE = 11
+    // 地址线 A0~A18:PF0~PF5, PF12~PF15, PG0~PG5, PD11~PD13
+    // MODE = 11(50MHz),CNF = 10(复用推挽输出)
     GPIOF->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1 | GPIO_CRL_MODE2 |
                    GPIO_CRL_MODE3 | GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
     GPIOF->CRH |= (GPIO_CRH_MODE12 | GPIO_CRH_MODE13 |
@@ -619,14 +717,13 @@ void FSMC_GPIO_Init(void)
                    GPIO_CRL_MODE3 | GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
     GPIOD->CRH |= (GPIO_CRH_MODE11 | GPIO_CRH_MODE12 | GPIO_CRH_MODE13);
 
-    // CNF = 10(复用推挽输出)
     GPIOF->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1 | GPIO_CRL_CNF2_1 |
                    GPIO_CRL_CNF3_1 | GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
     GPIOF->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0 | GPIO_CRL_CNF2_0 |
                     GPIO_CRL_CNF3_0 | GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
-    // ...(其余地址线引脚配置类似)
+    // ...(其余地址线引脚配置类似,CNF=10,MODE=11
 
-    // 2. 数据线 D0 ~ D15(MODE = 11,CNF = 10)
+    // 数据线 D0~D15:PD0~PD1, PD8~PD10, PD14~PD15, PE7~PE15
     GPIOD->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
     GPIOD->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
                    GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
@@ -639,7 +736,7 @@ void FSMC_GPIO_Init(void)
     GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
     // ...
 
-    // 3. 控制线
+    // 控制线
     // PD4 - NOE(读使能),PD5 - NWE(写使能)
     GPIOD->CRL |= (GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
     GPIOD->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
@@ -650,125 +747,903 @@ void FSMC_GPIO_Init(void)
     GPIOG->CRH |= GPIO_CRH_CNF10_1;
     GPIOG->CRH &= ~GPIO_CRH_CNF10_0;
 
-    // PE0, PE1 - NBL(字节掩码,16位SRAM的高低位使能
+    // PE0, PE1 - NBL0, NBL1(字节掩码)
     GPIOE->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
     GPIOE->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
     GPIOE->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
 }
-
-// 配置完成后,FSMC 区域3的内存映射地址为 0x68000000
-// 写: *(uint16_t *)0x68000000 = data;
-// 读: data = *(uint16_t *)0x68000000;
 ```
 
-**文件:`stm32/39_fsmc_sram_register/User/main.c`**
+### main.c
+
+**文件**:`User/main.c`
 
 ```c
 #include "usart.h"
 #include "fsmc.h"
 
-// 方法1:使用关键字attribute指定全局变量的地址
+// 方法1:使用 __attribute__((at())) 指定全局变量的地址
 uint8_t v1 __attribute__((at(0x68000000)));
 uint8_t v2 __attribute__((at(0x68000004)));
-
 uint16_t v3 = 30;
 
 int main(void)
 {
-	// 1. 初始化
-	USART_Init();
-	FSMC_Init();
+    USART_Init();
+    FSMC_Init();
+
+    printf("中国芯FSMC扩展SRAM实验...\n");
+
+    v1 = 10;
+    v2 = 20;
+
+    uint8_t v4 __attribute__((at(0x68000008)));
+    v4 = 40;
+    uint8_t v5 = 50;
+
+    printf("v1 = %d, @%p\n", v1, &v1);
+    printf("v2 = %d, @%p\n", v2, &v2);
+    printf("v3 = %d, @%p\n", v3, &v3);
+    printf("v4 = %d, @%p\n", v4, &v4);
+    printf("v5 = %d, @%p\n", v5, &v5);
+
+    // 方法2:指针直接访问
+    uint8_t *p = (uint8_t *)0x68000001;
+    *p = 100;
+    printf("*p = %d, @%p\n", *p, p);
+
+    while (1) {}
+}
+```
+
+> **配置完成后,FSMC 区域3 的地址为 0x68000000。写:`*(uint16_t *)0x68000000 = data;` 读:`data = *(uint16_t *)0x68000000;`**
+
+---
 
-	printf("中国芯FSMC扩展SRAM实验...\n");
+## 项目 40:FSMC 扩展 SRAM(HAL 库版)
 
-	v1 = 10;
-	v2 = 20;
+> 使用 CubeMX 生成 FSMC 初始化代码,HAL 库管理 SRAM 配置。同样使用 Bank3(NE3),16 位模式。
 
-	// 测试局部变量指定地址
-	uint8_t v4 __attribute__((at(0x68000008)));
-	v4 = 40;
-	uint8_t v5 = 50;
+**项目路径**:`上部-基础篇\03_代码\stm32\40_fsmc_sram_hal`
 
-	// 打印地址验证
-	printf("v1 = %d, @%p\n", v1, &v1);
-	printf("v2 = %d, @%p\n", v2, &v2);
-	printf("v3 = %d, @%p\n", v3, &v3);
-	printf("v4 = %d, @%p\n", v4, &v4);
-	printf("v5 = %d, @%p\n", v5, &v5);
+### CubeMX 生成:fsmc.c
 
-	// 方法2:指针直接访问
-	uint8_t *p = (uint8_t *)0x68000FFF;
-	*p = 100;
-	printf("*p = %d, @%p\n", *p, p);
+**文件**:`Core/Src/fsmc.c`
 
-	while (1)
-	{
-	}
+```c
+SRAM_HandleTypeDef hsram1;
+
+void MX_FSMC_Init(void)
+{
+    FSMC_NORSRAM_TimingTypeDef Timing = {0};
+
+    hsram1.Instance = FSMC_NORSRAM_DEVICE;
+    hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
+    hsram1.Init.NSBank = FSMC_NORSRAM_BANK3;
+    hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
+    hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
+    hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
+    hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
+    hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
+    hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
+    hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
+    hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
+    hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
+    hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
+    hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
+    hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;
+
+    Timing.AddressSetupTime = 15;
+    Timing.AddressHoldTime = 15;
+    Timing.DataSetupTime = 71;
+    Timing.BusTurnAroundDuration = 15;
+    Timing.CLKDivision = 16;
+    Timing.DataLatency = 17;
+    Timing.AccessMode = FSMC_ACCESS_MODE_A;
+
+    if (HAL_SRAM_Init(&hsram1, &Timing, NULL) != HAL_OK)
+    {
+        Error_Handler();
+    }
+
+    __HAL_AFIO_FSMCNADV_DISCONNECTED();
 }
 ```
 
-### FSMC + LCD 应用
+### HAL_FSMC_MspInit(GPIO 配置,由 CubeMX 生成)
 
-将 LCD 连接到 FSMC,利用地址线 A0 区分命令和数据:
+```c
+static void HAL_FSMC_MspInit(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if (FSMC_Initialized) return;
+    FSMC_Initialized = 1;
+
+    __HAL_RCC_FSMC_CLK_ENABLE();
+
+    // PF0~PF5, PF12~PF15 → FSMC_A0~A9
+    GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
+                          |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_12|GPIO_PIN_13
+                          |GPIO_PIN_14|GPIO_PIN_15;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);
+
+    // PG0~PG5, PG10 → FSMC_A10~A15, NE3
+    GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3
+                          |GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_10;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
+
+    // PE0~PE1(NBL), PE7~PE15(D4~D12)
+    GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10
+                          |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14
+                          |GPIO_PIN_15|GPIO_PIN_0|GPIO_PIN_1;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
+
+    // PD0~PD1(D2~D3), PD4(NOE), PD5(NWE), PD8~PD10(D13~D15),
+    // PD11~PD13(A16~A18), PD14~PD15(D0~D1)
+    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11
+                          |GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14|GPIO_PIN_15
+                          |GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_4|GPIO_PIN_5;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
+}
 ```
-写 (uint16_t *)0x60000000 = cmd   → RS=0 → 写命令
-写 (uint16_t *)0x60020000 = data  → RS=1(A0=1) → 写数据
+
+### main.c
+
+**文件**:`Core/Src/main.c`
+
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
+#include "fsmc.h"
+
+// 方法1:__attribute__((at())) 指定全局变量地址
+uint8_t v1 __attribute__((at(0x68000000)));
+uint8_t v2 __attribute__((at(0x68000004)));
+uint16_t v3 = 30;
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+
+    MX_GPIO_Init();
+    MX_FSMC_Init();
+    MX_USART1_UART_Init();
+
+    printf("中国芯FSMC实验...\n");
+
+    v1 = 10;
+    v2 = 20;
+
+    uint8_t v4 __attribute__((at(0x68000008)));
+    v4 = 40;
+    uint8_t v5 = 50;
+
+    printf("v1 = %d, @%p\n", v1, &v1);
+    printf("v2 = %d, @%p\n", v2, &v2);
+    printf("v3 = %d, @%p\n", v3, &v3);
+    printf("v4 = %d, @%p\n", v4, &v4);
+    printf("v5 = %d, @%p\n", v5, &v5);
+
+    uint8_t *p = (uint8_t *)0x68000001;
+    *p = 100;
+    printf("*p = %d, @%p\n", *p, p);
+
+    while (1) {}
+}
 ```
 
 ---
 
+## 项目 41:FSMC + LCD(寄存器版)
+
+> 将 LCD 连接到 FSMC,利用地址线 A10 区分命令和数据:
+> - `*LCD_ADDR_CMD = cmd`  → A10=0 → RS=0 → 写命令
+> - `*LCD_ADDR_DATA = data` → A10=1 → RS=1 → 写数据
+>
+> 使用 Bank1 子区 4(NE4,片选 PG12),地址基址 0x6C000000。
+
+**项目路径**:`上部-基础篇\03_代码\stm32\41_lcd_register`
+
+> 该项目的 FSMC 驱动在 `Hardware/FSMC/`,LCD 驱动在 `Interface/LCD/`(不同于其他项目的 `Hardware/` 目录结构)。
+
+### fsmc.c(LCD 专用 FSMC 配置)
+
+**文件**:`Hardware/FSMC/fsmc.c`(区别于 SRAM 版本,使用 BCR4/BTCR[6] 和 NE4)
+
+```c
+#include "fsmc.h"
+
+void FSMC_GPIO_Init(void);
+
+void FSMC_Init(void)
+{
+    // 1. 开启时钟(增加 GPIOB 用于背光)
+    RCC->AHBENR |= RCC_AHBENR_FSMCEN;
+    RCC->APB2ENR |= (RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPDEN |
+                     RCC_APB2ENR_IOPEEN | RCC_APB2ENR_IOPFEN |
+                     RCC_APB2ENR_IOPGEN);
+
+    FSMC_GPIO_Init();
+
+    // 3. FSMC BCR4 — BTCR[6]
+    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MBKEN;
+    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MTYP;      // SRAM类型
+    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_FACCEN;     // 禁止Flash访问
+    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MWID_1;
+    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MWID_0;      // 16位
+    FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MUXEN;      // 非复用
+    FSMC_Bank1->BTCR[6] |= FSMC_BCR4_WREN;        // 写使能
+
+    // 4. FSMC BTR4 — BTCR[7]
+    FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_ADDSET;
+    FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_DATAST;
+    FSMC_Bank1->BTCR[7] |= (71 << 8);             // DATAST = 71
+}
+
+void FSMC_GPIO_Init(void)
+{
+    // 地址线:只用了 A10(PG0)
+    GPIOG->CRL |= GPIO_CRL_MODE0;
+    GPIOG->CRL |= GPIO_CRL_CNF0_1;
+    GPIOG->CRL &= ~GPIO_CRL_CNF0_0;
+
+    // 数据线 D0~D15:PD0~PD1, PD8~PD10, PD14~PD15, PE7~PE15
+    GPIOD->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
+    GPIOD->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
+                   GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
+    GPIOE->CRL |= GPIO_CRL_MODE7;
+    GPIOE->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
+                   GPIO_CRH_MODE11 | GPIO_CRH_MODE12 | GPIO_CRH_MODE13 |
+                   GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
+    // CNF = 10
+    GPIOD->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
+    GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
+    // ...
+
+    // 控制线:PD4(NOE), PD5(NWE)
+    GPIOD->CRL |= (GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
+    GPIOD->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
+    GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
+
+    // PG12 — NE4(片选)
+    GPIOG->CRH |= GPIO_CRH_MODE12;
+    GPIOG->CRH |= GPIO_CRH_CNF12_1;
+    GPIOG->CRH &= ~GPIO_CRH_CNF12_0;
+
+    // PG15 — LCD复位(通用推挽输出)
+    GPIOG->CRH |= GPIO_CRH_MODE15;
+    GPIOG->CRH &= ~GPIO_CRH_CNF15;
+
+    // PB0 — 背光控制(通用推挽输出)
+    GPIOB->CRL |= GPIO_CRL_MODE0;
+    GPIOB->CRL &= ~GPIO_CRL_CNF0;
+}
+```
+
+### lcd.h
+
+**文件**:`Interface/LCD/lcd.h`
+
+```c
+#ifndef __LCD_H
+#define __LCD_H
+
+#include "fsmc.h"
+#include "delay.h"
+#include <math.h>
+
+// 命令/数据地址(A10 区分)
+#define SRAM_BANK1_4   0x6C000000
+#define LCD_ADDR_CMD   (uint16_t *)SRAM_BANK1_4
+#define LCD_ADDR_DATA  (uint16_t *)(SRAM_BANK1_4 + (1 << 11))
+// A10=0 → 命令,A10=1 → 数据(地址偏移 0x800)
+
+#define LCD_W  320
+#define LCD_H  480
+
+/* 常用颜色 */
+#define WHITE   0xFFFF
+#define BLACK   0x0000
+#define BLUE    0x001F
+#define RED     0xF800
+#define GREEN   0x07E0
+#define YELLOW  0xFFE0
+#define GRAY    0x8430
+// ... 其他颜色
+
+// 基础操作
+void LCD_Init(void);
+void LCD_Reset(void);
+void LCD_BGOn(void);
+void LCD_BGOff(void);
+void LCD_RegConfig(void);
+void LCD_WriteCmd(uint16_t cmd);
+void LCD_WriteData(uint16_t data);
+uint16_t LCD_ReadData(void);
+
+// 绘图功能
+uint32_t LCD_ReadID(void);
+void LCD_ClearAll(uint16_t color);
+void LCD_SetArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
+void LCD_WriteAsciiChar(uint16_t x, uint16_t y, uint16_t height,
+                        uint8_t c, uint16_t fColor, uint16_t bColor);
+void LCD_WriteAsciiString(uint16_t x, uint16_t y, uint16_t height,
+                          uint8_t *str, uint16_t fColor, uint16_t bColor);
+void LCD_WriteChineseChar(uint16_t x, uint16_t y, uint16_t height,
+                          uint8_t index, uint16_t fColor, uint16_t bColor);
+void LCD_DisplayAtguiguLogo(uint16_t x, uint16_t y);
+void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t w, uint16_t color);
+void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
+                  uint16_t w, uint16_t color);
+void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
+                       uint16_t w, uint16_t color);
+void LCD_DrawCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                    uint16_t w, uint16_t color);
+void LCD_DrawCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                        uint16_t w, uint16_t color);
+void LCD_DrawFilledCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                          uint16_t w, uint16_t bColor, uint16_t fColor);
+void LCD_DrawFilledCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                              uint16_t w, uint16_t bColor, uint16_t fColor);
+
+#endif
+```
+
+### lcd.c(核心函数)
+
+**文件**:`Interface/LCD/lcd.c`
+
+```c
+#include "lcd.h"
+#include "lcd_font.h"
+
+// 初始化调用 FSMC_Init,然后复位、亮背光、写寄存器
+void LCD_Init(void)
+{
+    FSMC_Init();
+    LCD_Reset();
+    LCD_BGOn();
+    LCD_RegConfig();
+}
+
+// 复位:PG15 拉低100ms再拉高
+void LCD_Reset(void)
+{
+    GPIOG->ODR &= ~GPIO_ODR_ODR15;
+    Delay_ms(100);
+    GPIOG->ODR |= GPIO_ODR_ODR15;
+    Delay_ms(100);
+}
+
+// 背光:PB0
+void LCD_BGOn(void)  { GPIOB->ODR |= GPIO_ODR_ODR0; }
+void LCD_BGOff(void) { GPIOB->ODR &= ~GPIO_ODR_ODR0; }
+
+// 寄存器初始化序列(ILI9341 兼容)
+void LCD_RegConfig(void)
+{
+    /* 1. 正极伽马校正 */
+    LCD_WriteCmd(0xE0);
+    LCD_WriteData(0x00); LCD_WriteData(0x07); LCD_WriteData(0x10);
+    LCD_WriteData(0x09); LCD_WriteData(0x17); LCD_WriteData(0x0B);
+    LCD_WriteData(0x41); LCD_WriteData(0x89); LCD_WriteData(0x4B);
+    LCD_WriteData(0x0A); LCD_WriteData(0x0C); LCD_WriteData(0x0E);
+    LCD_WriteData(0x18); LCD_WriteData(0x1B); LCD_WriteData(0x0F);
+
+    /* 2. 负极伽马校正 */
+    LCD_WriteCmd(0XE1);
+    LCD_WriteData(0x00); LCD_WriteData(0x17); LCD_WriteData(0x1A);
+    LCD_WriteData(0x04); LCD_WriteData(0x0E); LCD_WriteData(0x06);
+    LCD_WriteData(0x2F); LCD_WriteData(0x45); LCD_WriteData(0x43);
+    LCD_WriteData(0x02); LCD_WriteData(0x0A); LCD_WriteData(0x09);
+    LCD_WriteData(0x32); LCD_WriteData(0x36); LCD_WriteData(0x0F);
+
+    /* 4. 电源控制1 */
+    LCD_WriteCmd(0xC0);
+    LCD_WriteData(0x11); LCD_WriteData(0x09);
+
+    /* 5. 电源控制2 */
+    LCD_WriteCmd(0xC1);
+    LCD_WriteData(0x02); LCD_WriteData(0x03);
+
+    /* 6. VCOM控制 */
+    LCD_WriteCmd(0XC5);
+    LCD_WriteData(0x00); LCD_WriteData(0x0A); LCD_WriteData(0x80);
+
+    /* 7. 帧率控制 */
+    LCD_WriteCmd(0xB1);
+    LCD_WriteData(0xB0); LCD_WriteData(0x11);
+
+    /* 12. 像素格式:16位 */
+    LCD_WriteCmd(0x3A);
+    LCD_WriteData(0x55);
+
+    /* 13. 退出睡眠 */
+    LCD_WriteCmd(0x11);
+    Delay_ms(120);
+
+    /* 14. 显示开启 */
+    LCD_WriteCmd(0x29);
+}
+
+// 命令/数据访问
+void LCD_WriteCmd(uint16_t cmd)  { *LCD_ADDR_CMD = cmd; }
+void LCD_WriteData(uint16_t data) { *LCD_ADDR_DATA = data; }
+uint16_t LCD_ReadData(void)      { return *LCD_ADDR_DATA; }
+
+// 读 LCD 控制器 ID
+uint32_t LCD_ReadID(void)
+{
+    LCD_WriteCmd(0x04);
+    LCD_ReadData();  // 丢弃第一个无效字节
+    uint32_t id = 0;
+    id |= (LCD_ReadData() & 0xff) << 16;
+    id |= (LCD_ReadData() & 0xff) << 8;
+    id |= (LCD_ReadData() & 0xff);
+    return id;
+}
+
+// 设置读写窗口
+void LCD_SetArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
+{
+    LCD_WriteCmd(0x2a);
+    LCD_WriteData(x >> 8 & 0xff);  LCD_WriteData(x & 0xff);
+    LCD_WriteData((x + w - 1) >> 8 & 0xff);
+    LCD_WriteData((x + w - 1) & 0xff);
+
+    LCD_WriteCmd(0x2b);
+    LCD_WriteData(y >> 8 & 0xff);  LCD_WriteData(y & 0xff);
+    LCD_WriteData((y + h - 1) >> 8 & 0xff);
+    LCD_WriteData((y + h - 1) & 0xff);
+}
+
+// 全屏清空为指定颜色
+void LCD_ClearAll(uint16_t color)
+{
+    LCD_SetArea(0, 0, LCD_W, LCD_H);
+    LCD_WriteCmd(0x2c);
+    for (uint32_t i = 0; i < LCD_W * LCD_H; i++) {
+        LCD_WriteData(color);
+    }
+}
+
+// ASCII 字符显示(支持 12/16/24/32 高度)
+void LCD_WriteAsciiChar(uint16_t x, uint16_t y, uint16_t height,
+                        uint8_t c, uint16_t fColor, uint16_t bColor)
+{
+    LCD_SetArea(x, y, height / 2, height);
+    LCD_WriteCmd(0x2C);
+
+    uint8_t index = c - ' ';
+
+    if (height == 16 || height == 12) {
+        for (uint8_t i = 0; i < height; i++) {
+            uint8_t tempByte = (height == 16)
+                ? ascii_1608[index][i] : ascii_1206[index][i];
+            for (uint8_t j = 0; j < height / 2; j++) {
+                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
+                tempByte >>= 1;
+            }
+        }
+    } else if (height == 24) {
+        for (uint8_t i = 0; i < height * 2; i++) {
+            uint8_t tempByte = ascii_2412[index][i];
+            uint8_t jCount = (i % 2) ? 4 : 8;
+            for (uint8_t j = 0; j < jCount; j++) {
+                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
+                tempByte >>= 1;
+            }
+        }
+    } else if (height == 32) {
+        for (uint8_t i = 0; i < height * 2; i++) {
+            uint8_t tempByte = ascii_3216[index][i];
+            for (uint8_t j = 0; j < 8; j++) {
+                LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
+                tempByte >>= 1;
+            }
+        }
+    }
+}
+
+// 字符串(支持 \n 换行和自动换行)
+void LCD_WriteAsciiString(uint16_t x, uint16_t y, uint16_t height,
+                          uint8_t *str, uint16_t fColor, uint16_t bColor)
+{
+    uint8_t i = 0;
+    while (str[i] != '\0') {
+        if (str[i] != '\n') {
+            if (x + height / 2 > LCD_W) { x = 0; y += height; }
+            LCD_WriteAsciiChar(x, y, height, str[i], fColor, bColor);
+            x += height / 2;
+        } else {
+            x = 0; y += height;
+        }
+        i++;
+    }
+}
+
+// 显示汉字(使用 chinese[][128] 字库)
+void LCD_WriteChineseChar(uint16_t x, uint16_t y, uint16_t height,
+                          uint8_t index, uint16_t fColor, uint16_t bColor)
+{
+    LCD_SetArea(x, y, height, height);
+    LCD_WriteCmd(0x2C);
+    for (uint8_t i = 0; i < 128; i++) {
+        uint8_t tempByte = chinese[index][i];
+        for (uint8_t j = 0; j < 8; j++) {
+            LCD_WriteData((tempByte & 0x01) ? fColor : bColor);
+            tempByte >>= 1;
+        }
+    }
+}
+
+// 显示 Logo(使用 gImage_logo 数组)
+void LCD_DisplayAtguiguLogo(uint16_t x, uint16_t y)
+{
+    LCD_SetArea(x, y, 227, 68);
+    LCD_WriteCmd(0x2C);
+    uint16_t len = sizeof(gImage_logo);
+    for (uint16_t i = 0; i < len; i += 2) {
+        uint16_t p = gImage_logo[i] + (gImage_logo[i + 1] << 8);
+        LCD_WriteData(p);
+    }
+}
+
+// 画点
+void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t w, uint16_t color)
+{
+    LCD_SetArea(x, y, w, w);
+    LCD_WriteCmd(0x2C);
+    for (uint16_t i = 0; i < w * w; i++) LCD_WriteData(color);
+}
+
+// 画线(Bresenham 近似:y = kx + b)
+void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
+                  uint16_t w, uint16_t color)
+{
+    if (x1 == x2) {
+        for (uint16_t y = y1; y <= y2; y++) LCD_DrawPoint(x1, y, w, color);
+        return;
+    }
+    double k = 1.0 * (y1 - y2) / (x1 - x2);
+    double b = y1 - k * x1;
+    for (uint16_t x = x1; x <= x2; x++) {
+        uint16_t y = (uint16_t)(k * x + b);
+        LCD_DrawPoint(x, y, w, color);
+    }
+}
+
+// 矩形
+void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
+                       uint16_t w, uint16_t color)
+{
+    LCD_DrawLine(x1, y1, x2, y1, w, color);
+    LCD_DrawLine(x2, y1, x2, y2, w, color);
+    LCD_DrawLine(x1, y1, x1, y2, w, color);
+    LCD_DrawLine(x1, y2, x2, y2, w, color);
+}
+
+// 画圆(参数方程,逐点)
+void LCD_DrawCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                    uint16_t w, uint16_t color)
+{
+    for (uint16_t theta = 0; theta < 360; theta++) {
+        uint16_t x = xCenter + r * cos(3.14 * theta / 180);
+        uint16_t y = yCenter + r * sin(3.14 * theta / 180);
+        LCD_DrawPoint(x, y, w, color);
+    }
+}
+
+// 画圆优化版(同时画 4 象限)
+void LCD_DrawCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                        uint16_t w, uint16_t color)
+{
+    for (uint16_t theta = 0; theta <= 90; theta++) {
+        uint16_t dx = r * cos(3.14 * theta / 180);
+        uint16_t dy = r * sin(3.14 * theta / 180);
+        LCD_DrawPoint(xCenter + dx, yCenter + dy, w, color);
+        LCD_DrawPoint(xCenter - dx, yCenter + dy, w, color);
+        LCD_DrawPoint(xCenter - dx, yCenter - dy, w, color);
+        LCD_DrawPoint(xCenter + dx, yCenter - dy, w, color);
+    }
+}
+
+// 实心圆
+void LCD_DrawFilledCircle(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                          uint16_t w, uint16_t bColor, uint16_t fColor)
+{
+    for (uint16_t i = 0; i <= r; i++) {
+        for (uint16_t theta = 0; theta < 360; theta++) {
+            uint16_t x = xCenter + i * cos(3.14 * theta / 180);
+            uint16_t y = yCenter + i * sin(3.14 * theta / 180);
+            LCD_DrawPoint(x, y, w, (i == r) ? fColor : bColor);
+        }
+    }
+}
+
+// 实心圆优化版(画填充直线代替逐点扫描)
+void LCD_DrawFilledCircle_Pro(uint16_t xCenter, uint16_t yCenter, uint16_t r,
+                              uint16_t w, uint16_t bColor, uint16_t fColor)
+{
+    for (uint16_t theta = 0; theta <= 90; theta++) {
+        uint16_t dx = r * cos(3.14 * theta / 180);
+        uint16_t dy = r * sin(3.14 * theta / 180);
+
+        uint16_t x1 = xCenter + dx, y1 = yCenter + dy;
+        uint16_t x2 = xCenter - dx, y2 = yCenter + dy;
+        LCD_DrawPoint(x1, y1, w, fColor);
+        LCD_DrawPoint(x2, y2, w, fColor);
+        LCD_DrawLine(x2 + w, y2, x1 - w, y1, w, bColor);
+
+        x2 = xCenter - dx; y2 = yCenter - dy;
+        x1 = xCenter + dx; y1 = yCenter - dy;
+        LCD_DrawPoint(x2, y2, w, fColor);
+        LCD_DrawPoint(x1, y1, w, fColor);
+        LCD_DrawLine(x2 + w, y2, x1 - w, y1, w, bColor);
+    }
+}
+```
+
+### main.c
+
+**文件**:`User/main.c`
+
+```c
+#include "usart.h"
+#include "lcd.h"
+
+int main(void)
+{
+    USART_Init();
+    LCD_Init();
+
+    printf("中国芯LCD实验开始...\n");
+
+    uint32_t id = LCD_ReadID();
+    printf("id = %#x\n", id);
+
+    // 全屏白色
+    LCD_ClearAll(WHITE);
+
+    // 显示 ASCII 字符
+    LCD_WriteAsciiChar(10, 10, 16, 'A', WHITE, RED);
+    LCD_WriteAsciiChar(10, 30, 24, 'A', WHITE, RED);
+    LCD_WriteAsciiChar(20, 60, 32, 'A', BLUE, WHITE);
+    LCD_WriteAsciiChar(20, 100, 12, 'B', BLUE, YELLOW);
+
+    // 显示字符串
+    LCD_WriteAsciiString(200, 200, 24,
+        "Hello\natguigu!\nHello, world!at\nguigu", BLACK, WHITE);
+
+    // 显示汉字
+    LCD_WriteChineseChar(20, 330, 32, 0, RED, BLUE);
+    LCD_WriteChineseChar(20, 362, 32, 1, BLUE, RED);
+    LCD_WriteChineseChar(20, 394, 32, 2, GRAY, RED);
+
+    // 显示 Logo
+    LCD_DisplayAtguiguLogo(57, 100);
+
+    // 几何图形
+    LCD_DrawPoint(300, 300, 5, RED);
+    LCD_DrawLine(10, 10, 10, 300, 5, RED);
+    LCD_DrawLine(10, 10, 300, 180, 3, BLUE);
+    LCD_DrawRectangle(20, 20, 300, 300, 5, RED);
+    LCD_DrawCircle_Pro(160, 240, 100, 5, BLUE);
+    LCD_DrawFilledCircle_Pro(240, 400, 50, 3, BLUE, RED);
+
+    while (1) {}
+}
+```
+
+> **LCD 地址映射原理**:NE4 基址 = 0x6C000000,A10 连接 LCD 的 RS 引脚。
+> 当 CPU 访问 `0x6C000000` 时,A10=0 → RS=0 → 命令模式。
+> 当 CPU 访问 `0x6C000000 + 0x800`(即 A10=1)时 → RS=1 → 数据模式。
+
 ---
 
-## HAL 库版 SPI
+## 项目 42:FSMC + LCD(HAL 库版)
 
-HAL 库使用 `SPI_HandleTypeDef` 管理 SPI,提供 `Transmit/Receive` 函数。
+> CubeMX 生成 FSMC 初始化代码,使用 Bank4(NE4),LCD 驱动逻辑与寄存器版相同。
+
+**项目路径**:`上部-基础篇\03_代码\stm32\42_lcd_hal`
+
+### CubeMX 生成:fsmc.c
+
+**文件**:`Core/Src/fsmc.c`
 
 ```c
-// CubeMX 生成: MX_SPI1_Init()
-SPI_HandleTypeDef hspi1;
+SRAM_HandleTypeDef hsram1;
+
+void MX_FSMC_Init(void)
+{
+    FSMC_NORSRAM_TimingTypeDef Timing = {0};
+
+    hsram1.Instance = FSMC_NORSRAM_DEVICE;
+    hsram1.Extended = FSMC_NORSRAM_EXTENDED_DEVICE;
+    hsram1.Init.NSBank = FSMC_NORSRAM_BANK4;       // NE4
+    hsram1.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE;
+    hsram1.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM;
+    hsram1.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16;
+    hsram1.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE;
+    hsram1.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW;
+    hsram1.Init.WrapMode = FSMC_WRAP_MODE_DISABLE;
+    hsram1.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS;
+    hsram1.Init.WriteOperation = FSMC_WRITE_OPERATION_ENABLE;
+    hsram1.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE;
+    hsram1.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE;
+    hsram1.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE;
+    hsram1.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE;
+
+    Timing.AddressSetupTime = 15;
+    Timing.AddressHoldTime = 15;
+    Timing.DataSetupTime = 71;
+    Timing.BusTurnAroundDuration = 15;
+    Timing.CLKDivision = 16;
+    Timing.DataLatency = 17;
+    Timing.AccessMode = FSMC_ACCESS_MODE_A;
+
+    if (HAL_SRAM_Init(&hsram1, &Timing, NULL) != HAL_OK)
+        Error_Handler();
+
+    __HAL_AFIO_FSMCNADV_DISCONNECTED();
+}
+```
 
-// HAL 库发送接收
-uint8_t tx_data = 0x06;           // WREN 指令
-uint8_t rx_data;
+### HAL_FSMC_MspInit(LCD 版,NE4)
 
-HAL_SPI_Transmit(&hspi1, &tx_data, 1, 100);      // 阻塞发送 1 字节
-HAL_SPI_Receive(&hspi1, &rx_data, 1, 100);        // 阻塞接收 1 字节
-HAL_SPI_TransmitReceive(&hspi1, &tx_data, &rx_data, 1, 100);  // 同时收发
+```c
+static void HAL_FSMC_MspInit(void)
+{
+    GPIO_InitTypeDef GPIO_InitStruct = {0};
+    if (FSMC_Initialized) return;
+    FSMC_Initialized = 1;
+
+    __HAL_RCC_FSMC_CLK_ENABLE();
+
+    // PG0(A10), PG12(NE4)
+    GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_12;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
+
+    // PE7~PE15(D4~D12)
+    GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10
+                          |GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13|GPIO_PIN_14
+                          |GPIO_PIN_15;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOE, &GPIO_InitStruct);
+
+    // PD0(D2), PD1(D3), PD4(NOE), PD5(NWE), PD8~PD10(D13~D15),
+    // PD14(D0), PD15(D1)
+    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_14
+                          |GPIO_PIN_15|GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_4
+                          |GPIO_PIN_5;
+    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
+    HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
+}
+```
+
+### lcd.h 与 lcd.c
+
+与项目 41 的 LCD 驱动代码**几乎相同**,关键差异:
+- `LCD_Init()` 调用 `MX_FSMC_Init()` 而非 `FSMC_Init()`
+- `LCD_Reset()` 使用 `HAL_Delay()` 而非 `Delay_ms()`
+- `LCD_RegConfig()` 使用 `HAL_Delay(120)` 而非 `Delay_ms(120)`
 
-// HAL 库中断方式
-HAL_SPI_Transmit_IT(&hspi1, buffer, size);         // 中断发送
-HAL_SPI_Receive_DMA(&hspi1, buffer, size);         // DMA 接收
+### main.c
 
-// HAL 库写 W25Q64 示例
-uint8_t cmd[] = {0x06};              // WREN
-HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);
-HAL_SPI_Transmit(&hspi1, cmd, 1, 100);
-HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);
+**文件**:`Core/Src/main.c`
+
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
+#include "fsmc.h"
+#include "lcd.h"
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+
+    MX_GPIO_Init();
+    MX_FSMC_Init();
+    MX_USART1_UART_Init();
+
+    LCD_Init();
+
+    printf("中国芯LCD实验开始...\n");
+
+    uint32_t id = LCD_ReadID();
+    printf("id = %#x\n", id);
+
+    LCD_ClearAll(WHITE);
+
+    LCD_WriteAsciiChar(10, 10, 16, 'A', WHITE, RED);
+    LCD_WriteAsciiChar(10, 30, 24, 'B', RED, WHITE);
+    LCD_WriteAsciiChar(10, 60, 32, 'R', BLUE, YELLOW);
+
+    LCD_WriteAsciiString(200, 200, 24,
+        "Hello, Atguigu! Hello, hello! At\nguigu! Hello, wolrd!",
+        BLACK, WHITE);
+
+    LCD_WriteChineseChar(20, 330, 32, 0, RED, BLUE);
+    LCD_WriteChineseChar(20, 362, 32, 1, BLUE, RED);
+    LCD_WriteChineseChar(20, 394, 32, 2, GRAY, RED);
+
+    LCD_DisplayAtguiguLogo(50, 100);
+
+    LCD_DrawPoint(300, 300, 5, RED);
+    LCD_DrawLine(10, 10, 10, 300, 5, RED);
+    LCD_DrawLine(10, 20, 300, 100, 3, BLUE);
+    LCD_DrawRectangle(15, 15, 280, 55, 3, RED);
+    LCD_DrawCircle(150, 400, 50, 3, BLUE);
+    LCD_DrawFilledCircle_Pro(260, 400, 50, 3, BLUE, RED);
+
+    while (1) {}
+}
 ```
 
+---
+
 ## 核心速查表
 
-| SPI 操作 | 软件模拟 | 硬件寄存器 | HAL 库 |
-|---------|---------|-----------|--------|
-| 初始化 | GPIO 开漏/推挽 | `CR1 |= MSTR + SPE` + GPIO 复用 | `MX_SPI1_Init()` |
-| 发 1 字节 | 8 次 GPIO 位操作 | `DR = byte; while(!RXNE); val = DR` | `HAL_SPI_Transmit()` |
-| 收 1 字节 | 同发送(发 0xFF 占位) | 同发送(发占位字节) | `HAL_SPI_Receive()` |
-| 同时收发 | 循环 8 次读写 | `DR = tx; val = DR` | `HAL_SPI_TransmitReceive()` |
-| 中断方式 | — | CR2.TXEIE/RXNEIE | `_IT()` 后缀 |
-| DMA 方式 | — | CR2.TXDMAEN/RXDMAEN | `_DMA()` 后缀 |
-| 片选 | GPIO 位操作 | GPIO 位操作 | `HAL_GPIO_WritePin()` |
-
-| FSMC 操作 | 代码 |
-|----------|------|
-| 写 SRAM | `*(uint16_t *)0x60000000 = data` |
-| 读 SRAM | `data = *(uint16_t *)0x60000000` |
-| 写 LCD 命令 | `*(uint16_t *)0x60000000 = cmd` |
-| 写 LCD 数据 | `*(uint16_t *)0x60020000 = data` |
+| SPI 操作 | 软件模拟(36) | 硬件寄存器(37) | HAL 库(38) |
+|---------|--------------|----------------|------------|
+| SCK/MOSI 配置 | 通用推挽输出 | 复用推挽输出 | `GPIO_MODE_AF_PP` |
+| MISO 配置 | 浮空输入 | 浮空输入 | `GPIO_MODE_INPUT` |
+| 初始化 | GPIO 位操作 + 延时 | `CR1 \|= MSTR+SSM+SSI+SPE` | `MX_SPI1_Init()` |
+| 发 1 字节 | 8 次 GPIO 位操作 | `DR=byte; while(!RXNE); val=DR` | `HAL_SPI_Transmit()` |
+| 同时收发 | 循环 8 次读写 | `DR=byte; val=DR` | `HAL_SPI_TransmitReceive()` |
+| 片选 CS | GPIO 位操作 | GPIO 位操作 | `HAL_GPIO_WritePin()` |
+| 适用场景 | 任意引脚/低速 | 固定引脚/高速 | 跨平台/快速开发 |
+
+| FSMC 操作 | 寄存器版(39/41) | HAL 库版(40/42) |
+|----------|----------------|-----------------|
+| SRAM 片选 | Bank3 (NE3, PG10) | `FSMC_NORSRAM_BANK3` |
+| LCD 片选 | Bank4 (NE4, PG12) | `FSMC_NORSRAM_BANK4` |
+| 命令/数据区分 | A10 地址线:`偏移 0x800` | 同左 |
+| 写 SRAM | `*(uint16_t *)0x68000000 = data` | 同左 |
+| 读 SRAM | `data = *(uint16_t *)0x68000000` | 同左 |
+| 写 LCD 命令 | `*LCD_ADDR_CMD = cmd` | 同左 |
+| 写 LCD 数据 | `*LCD_ADDR_DATA = data` | 同左 |
+| 初始化 | 手动配置寄存器 | `MX_FSMC_Init()` + `HAL_SRAM_Init()` |
+
+### 各项目引脚分配
+
+| 信号 | 36/37/38 (SPI) | 39/40 (SRAM) | 41/42 (LCD) |
+|------|---------------|-------------|------------|
+| SPI_SCK | PA5 | — | — |
+| SPI_MOSI | PA7 | — | — |
+| SPI_MISO | PA6 | — | — |
+| SPI_CS | PC13 | — | — |
+| FSMC_NE | — | PG10 (NE3) | PG12 (NE4) |
+| FSMC_A10 | — | — | PG0 (RS) |
+| LCD_RST | — | — | PG15 |
+| LCD_BL | — | — | PB0 |
+| FSMC_NOE | — | PD4 | PD4 |
+| FSMC_NWE | — | PD5 | PD5 |
+
+---
 
 ## 常见问题与避坑
 
 1. **SPI 接收数据为 0xFF** → MISO 连接断开、从机未选中(CS 拉低后才有输出)、从机忙于内部操作
 2. **W25Q64 写入失败** → 每次写操作前必须发 WREN(0x06);扇区必须事先擦除(Flash 不能写覆盖)
-3. **FSMC 读写时序不对** → 查 SRAM/LCD 数据手册的时序参数(ADDSET 和 DATAST),STM32 的 HCLK 对应关系
+3. **FSMC 读写时序不对** → 查 SRAM/LCD 数据手册的时序参数(ADDSET 和 DATAST),STM32 的 HCLK 对应关系。DATAST 的计算:`DATAST = (T_access / T_HCLK) - ADDSET`。LCD 通常比 SRAM 慢,可适当增大 DATAST
 4. **软件 SPI 速度太慢** → 软件 SPI 受限于 GPIO 翻转速度(约 2~4MHz),大批量数据建议用硬件 SPI
 5. **NSS 软件管理模式** → 多从机时必须用 SSM=1 软件管理 NSS,否则硬件自动管理可能产生冲突
+6. **LCD 显示异常(花屏/白屏)** → 检查 FSMC 时序(LCD 控制器通常需要更长的 DATAST);检查复位引脚 PG15 和背光引脚 PB0 的 GPIO 配置;确认 LCD_ADDR_DATA 的地址偏移量(A10 连接 RS,偏移量 = 1<<10 = 0x400,但实际使用 1<<11 = 0x800,需核对硬件连接)
+7. **HAL 库 FSMC 初始化失败** → 确认 CubeMX 中 FSMC 的 Bank 选择与硬件一致(SRAM 选 NE3,LCD 选 NE4);NADV 必须断开(`__HAL_AFIO_FSMCNADV_DISCONNECTED()`)

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

@@ -183,9 +183,9 @@ bxCAN = Basic Extended CAN,支持 CAN 2.0A(标准帧)和 2.0B Active(标
 
 ## 软件设计(寄存器版 — 环回静默测试)
 
-**项目路径**:`stm32/stm32/01_can_test_register`
+**项目路径**:`stm32/01_can_test_register`
 
-**文件:`stm32/stm32/01_can_test_register/Hardware/CAN/can.h`**
+**文件:`stm32/01_can_test_register/Hardware/CAN/can.h`**
 
 ```c
 /*
@@ -220,7 +220,7 @@ void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount);
 #endif
 ```
 
-**文件:`stm32/stm32/01_can_test_register/Hardware/CAN/can.c`**
+**文件:`stm32/01_can_test_register/Hardware/CAN/can.c`**
 
 ```c
 /*
@@ -425,7 +425,7 @@ void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount)
 }
 ```
 
-**文件:`stm32/stm32/01_can_test_register/User/main.c`**
+**文件:`stm32/01_can_test_register/User/main.c`**
 
 ```c
 /*
@@ -484,6 +484,305 @@ int main(void)
 
 ---
 
+## 软件设计(寄存器版 — 双机收发通信)
+
+**项目路径**:`stm32/03_can_tx_register`(发送节点)、`stm32/03_can_rx_register`(接收节点)
+
+与 01 项目相比,关键差别:
+- **不设置环回静默模式**(SILM=0, LBKM=0 → 正常模式)
+- **过滤器掩码 FR2=0x00**(接受所有报文,而非只匹配 0x06e)
+
+**can.c 差异(vs 01)**:
+```c
+// 03 项目注释掉了环回静默配置(can.c:52-54)
+// CAN1->BTR |= CAN_BTR_SILM;
+// CAN1->BTR |= CAN_BTR_LBKM;
+
+// 03 项目过滤器掩码设为 0(接受所有 ID)
+CAN1->sFilterRegister[0].FR2 = 0x00;   // 01 项目为 0x7f1 << 21
+```
+
+**文件:`stm32/03_can_tx_register/User/main.c`** — 发送节点,每 1s 发一帧
+
+```c
+#include "delay.h"
+#include "usart.h"
+#include "can.h"
+#include <string.h>
+
+int main(void)
+{
+    USART_Init();
+    CAN_Init();
+
+    printf("尚硅谷CAN通讯实验:双机收发测试 - 发送节点,寄存器版...\n");
+
+    uint16_t stdID = 0x066;
+    uint8_t * data = "abc";
+    uint8_t buffer[10];
+    uint32_t i = 0;
+
+    while (1)
+    {
+        sprintf((char *)buffer, "%s %d", data, ++i);
+        CAN_SendMsg(stdID, buffer, strlen((char *)buffer));
+        printf("报文发送完毕...\n");
+        Delay_ms(1000);
+    }
+}
+```
+
+**文件:`stm32/03_can_rx_register/User/main.c`** — 接收节点,持续轮询
+
+```c
+#include "usart.h"
+#include "can.h"
+#include <string.h>
+
+int main(void)
+{
+    USART_Init();
+    CAN_Init();
+
+    printf("尚硅谷CAN通讯实验:双机收发测试 - 接收节点,寄存器版...\n");
+
+    RxMsg rxMsg[3];
+    uint8_t msgCount;
+
+    while (1)
+    {
+        CAN_ReceiveMsg(rxMsg, &msgCount);
+        for (uint8_t i = 0; i < msgCount; i++)
+        {
+            printf("stdID = %#X, len = %d, data = %.*s\n",
+                   rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
+        }
+    }
+}
+```
+
+---
+
+## 软件设计(HAL 库版 — 环回静默测试)
+
+**项目路径**:`stm32/02_can_test_hal`
+
+CubeMX 生成 MX_CAN_Init,用户在 can.c 的 `USER CODE` 区添加过滤器和收发函数。
+
+**文件:`stm32/02_can_test_hal/Core/Src/can.c`** — HAL 初始化与收发
+
+```c
+CAN_HandleTypeDef hcan;
+
+void MX_CAN_Init(void)
+{
+    hcan.Instance = CAN1;
+    hcan.Init.Prescaler = 36;                      // 与寄存器版 BRP=35 对应
+    hcan.Init.Mode = CAN_MODE_SILENT_LOOPBACK;     // 环回静默模式
+    hcan.Init.SyncJumpWidth = CAN_SJW_2TQ;
+    hcan.Init.TimeSeg1 = CAN_BS1_3TQ;
+    hcan.Init.TimeSeg2 = CAN_BS2_6TQ;
+    hcan.Init.TimeTriggeredMode = DISABLE;
+    hcan.Init.AutoBusOff = ENABLE;
+    hcan.Init.AutoWakeUp = ENABLE;
+    hcan.Init.AutoRetransmission = DISABLE;
+    hcan.Init.ReceiveFifoLocked = DISABLE;
+    hcan.Init.TransmitFifoPriority = DISABLE;
+    if (HAL_CAN_Init(&hcan) != HAL_OK)
+        Error_Handler();
+}
+
+// CAN 过滤器配置
+void CAN_FilterConfig(void)
+{
+    CAN_FilterTypeDef filterConfig;
+    filterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;
+    filterConfig.FilterBank = 0;
+    filterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
+    filterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
+    filterConfig.FilterIdHigh = 0x0000;
+    filterConfig.FilterIdLow = 0x0000;
+    filterConfig.FilterMaskIdHigh = 0x0000;
+    filterConfig.FilterMaskIdLow = 0x0000;
+    filterConfig.FilterActivation = ENABLE;
+    HAL_CAN_ConfigFilter(&hcan, &filterConfig);
+}
+
+// 发送报文
+void CAN_SendMsg(uint16_t stdID, uint8_t * data, uint8_t len)
+{
+    while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0) {}
+
+    CAN_TxHeaderTypeDef txHeader;
+    txHeader.StdId = stdID;
+    txHeader.IDE = CAN_ID_STD;
+    txHeader.RTR = CAN_RTR_DATA;
+    txHeader.DLC = len;
+
+    uint32_t txMailBox;
+    HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailBox);
+
+    while (__HAL_CAN_GET_FLAG(&hcan, CAN_FLAG_TXOK0) == 0) {}
+}
+
+// 接收报文
+void CAN_ReceiveMsg(RxMsg rxMsg[], uint8_t * msgCount)
+{
+    *msgCount = HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0);
+
+    CAN_RxHeaderTypeDef rxHeader;
+    for (uint8_t i = 0; i < *msgCount; i++)
+    {
+        HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rxHeader, rxMsg[i].data);
+        rxMsg[i].stdID = rxHeader.StdId;
+        rxMsg[i].len = rxHeader.DLC;
+    }
+}
+```
+
+> HAL 库的 `CAN_TxHeaderTypeDef` / `CAN_RxHeaderTypeDef` 等效于寄存器版的手动拼装 TIR/TDTR/TDLR/TDHR。`CAN_FilterTypeDef` 等效于操作 sFilterRegister[].FR1/FR2。
+
+**文件:`stm32/02_can_test_hal/Core/Src/main.c`** — 环回测试逻辑(与 01 寄存器版效果相同)
+
+```c
+#include "main.h"
+#include "can.h"
+#include "usart.h"
+#include "gpio.h"
+#include <string.h>
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_CAN_Init();
+    MX_USART1_UART_Init();
+
+    HAL_CAN_Start(&hcan);          // 启动 CAN 模块
+    CAN_FilterConfig();            // 配置过滤器
+
+    printf("尚硅谷CAN通讯实验:环回静默模式测试,HAL版...\n");
+
+    // 发送三帧
+    uint16_t stdID = 0x066;
+    uint8_t *data = "abcdefg";
+    CAN_SendMsg(stdID, data, strlen((char *)data));
+
+    stdID = 0x068;
+    data = "123";
+    CAN_SendMsg(stdID, data, strlen((char *)data));
+
+    stdID = 0x067;
+    data = "xyz";
+    CAN_SendMsg(stdID, data, strlen((char *)data));
+
+    // 接收
+    RxMsg rxMsg[3];
+    uint8_t msgCount;
+    CAN_ReceiveMsg(rxMsg, &msgCount);
+
+    printf("报文接收完毕,count = %d\n", msgCount);
+    for (uint8_t i = 0; i < msgCount; i++) {
+        printf("stdID = %#X, len = %d, data = %.*s\n",
+               rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
+    }
+
+    while (1) {}
+}
+```
+
+---
+
+## 软件设计(HAL 库版 — 双机收发通信)
+
+**项目路径**:`stm32/04_can_tx_hal`(发送节点)、`stm32/04_can_rx_hal`(接收节点)
+
+与 02 项目的唯一区别是 `MX_CAN_Init` 中 `Mode = CAN_MODE_NORMAL`(而非 `SILENT_LOOPBACK`),其余 can.c 完全一致。
+
+**can.c 差异(vs 02)**:
+```c
+hcan.Init.Mode = CAN_MODE_NORMAL;    // 02 项目为 CAN_MODE_SILENT_LOOPBACK
+```
+
+**文件:`stm32/04_can_tx_hal/Core/Src/main.c`** — HAL 发送节点
+
+```c
+#include "main.h"
+#include "can.h"
+#include "usart.h"
+#include "gpio.h"
+#include <string.h>
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_CAN_Init();
+    MX_USART1_UART_Init();
+
+    HAL_CAN_Start(&hcan);
+    CAN_FilterConfig();
+
+    printf("尚硅谷CAN通讯实验:双机收发测试 - 发送节点,HAL版...\n");
+
+    uint16_t stdID = 0x066;
+    uint8_t * data = "abc";
+    uint8_t buffer[10];
+    uint32_t i = 0;
+
+    while (1)
+    {
+        sprintf((char *)buffer, "%s %d", data, ++i);
+        CAN_SendMsg(stdID, buffer, strlen((char *)buffer));
+        printf("报文发送完毕...\n");
+        HAL_Delay(1000);
+    }
+}
+```
+
+**文件:`stm32/04_can_rx_hal/Core/Src/main.c`** — HAL 接收节点
+
+```c
+#include "main.h"
+#include "can.h"
+#include "usart.h"
+#include "gpio.h"
+#include <string.h>
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_CAN_Init();
+    MX_USART1_UART_Init();
+
+    HAL_CAN_Start(&hcan);
+    CAN_FilterConfig();
+
+    printf("尚硅谷CAN通讯实验:双机收发测试 - 接收节点,HAL版...\n");
+
+    RxMsg rxMsg[3];
+    uint8_t msgCount;
+
+    while (1)
+    {
+        CAN_ReceiveMsg(rxMsg, &msgCount);
+        for (uint8_t i = 0; i < msgCount; i++)
+        {
+            printf("stdID = %#X, len = %d, data = %.*s\n",
+                   rxMsg[i].stdID, rxMsg[i].len, rxMsg[i].len, rxMsg[i].data);
+        }
+    }
+}
+```
+
+> 双机实验需两块开发板,一块烧 TX(04_can_tx_hal),一块烧 RX(04_can_rx_hal),通过 CAN_H/CAN_L 对接并各接 120Ω 终端电阻。
+
+---
+
 ## HAL 库版 CAN
 
 HAL 库使用 `CAN_HandleTypeDef` 管理 CAN 外设,CubeMX 图形化配置波特率/过滤器/工作模式。

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

@@ -522,6 +522,377 @@ static void do_led_action(uint8_t action)
 
 ---
 
+## 实验项目详解
+
+所有项目共享 `Hardware/SPI/spi.h/.c`(SPI 驱动,已在上面展示)和 `Interface/Ethernet/eth.h/.c`(W5500 网络初始化)。
+
+### 05 — 基础以太网通信测试(寄存器版)
+
+**项目路径**:`stm32/05_ethernet_test_register`
+
+仅验证 SPI + W5500 初始化是否成功,不做任何网络通信。
+
+**文件:`stm32/05_ethernet_test_register/User/main.c`**
+
+```c
+#include "usart.h"
+#include "eth.h"
+
+int main(void)
+{
+    USART_Init();
+    printf("尚硅谷以太网实验:初始化硬件建立\n");
+
+    ETH_Init();
+    printf("\n以太网初始化完成!\n");
+
+    while (1) {}
+}
+```
+
+`eth.c` 执行:SPI 初始化 → 注册 W5500 回调 → 复位 W5500 → 配置 MAC/IP/掩码/网关。与 `09_ethernet_webserver_register` 中的 `eth.c` 完全一致。
+
+---
+
+### 06 — TCP Server(寄存器版)
+
+**项目路径**:`stm32/06_ethernet_tcp_server_register`
+
+W5500 作为 TCP 服务器,监听端口 8080,接收客户端数据并原样返回(Echo Server)。
+
+**文件:`stm32/06_ethernet_tcp_server_register/App/TCP/tcp.h`**
+
+```c
+#ifndef __TCP_H
+#define __TCP_H
+
+#include "eth.h"
+#include "socket.h"
+
+#define SN 0
+#define CLIENT 0
+#define SERVER 1
+#define ROLE SERVER
+
+void TCP_ServerStart(void);
+void TCP_RecvData(uint8_t buff[], uint16_t *len);
+void TCP_SendData(uint8_t data[], uint16_t len);
+
+#endif
+```
+
+**文件:`stm32/06_ethernet_tcp_server_register/App/TCP/tcp.c`** — 状态机驱动
+
+```c
+#include "tcp.h"
+
+uint8_t clientIP[4];
+uint16_t clientPort;
+
+void TCP_ServerStart(void)
+{
+    uint8_t status = getSn_SR(SN);
+
+    if (status == SOCK_CLOSED) {
+        // 关闭状态 → 创建 TCP socket,端口 8080
+        int8_t n = socket(SN, Sn_MR_TCP, 8080, SF_TCP_NODELAY);
+        if (n == SN) printf("socket %d 打开成功!\n", SN);
+        else         printf("socket %d 开失败,返回码:%d\n", SN, n);
+    }
+    else if (status == SOCK_INIT) {
+        // INIT 状态 → 开始监听
+        int8_t res = listen(SN);
+        if (res == SOCK_OK) printf("socket %d 监听成功!\n", SN);
+        else                printf("socket %d 监听失败,返回码:%d\n", SN, res);
+    }
+    else if (status == SOCK_ESTABLISHED) {
+        // 已建立连接 → 提取客户端 IP/端口
+        if (getSn_IR(SN) & Sn_IR_CON) {
+            getSn_DIPR(SN, clientIP);
+            clientPort = getSn_DPORT(SN);
+            printf("客户端连接成功!IP: %d.%d.%d.%d, Port: %d\n",
+                clientIP[0], clientIP[1], clientIP[2], clientIP[3], clientPort);
+            setSn_IR(SN, Sn_IR_CON);
+        }
+    }
+    else if (status == SOCK_CLOSE_WAIT) {
+        // 客户端断开 → 关闭 socket 等待重连
+        printf("失去客户端的连接,准备关闭socket重新打开...\n");
+        close(SN);
+    }
+}
+
+void TCP_RecvData(uint8_t buff[], uint16_t *len)
+{
+    uint8_t status = getSn_SR(SN);
+    if (status == SOCK_ESTABLISHED && (getSn_IR(SN) & Sn_IR_RECV)) {
+        setSn_IR(SN, Sn_IR_RECV);
+        *len = getSn_RX_RSR(SN);
+        recv(SN, buff, *len);
+    }
+}
+
+void TCP_SendData(uint8_t data[], uint16_t len)
+{
+    uint8_t status = getSn_SR(SN);
+    if (status == SOCK_ESTABLISHED) {
+        send(SN, data, len);
+    }
+}
+```
+
+> W5500 Socket API 是状态机模式。`socket()` → `listen()` → 等待 `SOCK_ESTABLISHED` → `recv()`/`send()`。每个函数执行一步状态迁移,主循环中反复调用。
+
+**文件:`stm32/06_ethernet_tcp_server_register/User/main.c`** — 主循环
+
+```c
+#include "usart.h"
+#include "eth.h"
+#include "tcp.h"
+
+uint8_t rxBuff[1024];
+uint16_t rxLen;
+
+int main(void)
+{
+    USART_Init();
+    printf("尚硅谷以太网实验:TCP Server\n");
+
+    ETH_Init();
+
+    while (1) {
+        TCP_ServerStart();
+        TCP_RecvData(rxBuff, &rxLen);
+
+        if (rxLen > 0) {
+            printf("收到数据:%.*s\n", rxLen, rxBuff);
+            TCP_SendData(rxBuff, rxLen);   // 原样返回
+            rxLen = 0;
+        }
+    }
+}
+```
+
+---
+
+### 07 — TCP Client(寄存器版)
+
+**项目路径**:`stm32/07_ethernet_tcp_client_register`
+
+W5500 作为 TCP 客户端,主动连接服务器(192.168.44.53:8888),发送初始消息后收发数据。
+
+**文件:`stm32/07_ethernet_tcp_client_register/App/TCP/tcp.h`** — 新增 `TCP_ClientStart` 声明
+
+```c
+#define CLIENT 0
+#define SERVER 1
+#define ROLE CLIENT          // 角色切换为客户端
+
+void TCP_ServerStart(void);
+void TCP_ClientStart(void);  // 新增
+void TCP_RecvData(uint8_t buff[], uint16_t *len);
+void TCP_SendData(uint8_t data[], uint16_t len);
+```
+
+**文件:`stm32/07_ethernet_tcp_client_register/App/TCP/tcp.c`** — 新增 `TCP_ClientStart`
+
+```c
+// 目标服务器 IP 和端口
+uint8_t serverIP[4] = {192, 168, 44, 53};
+uint16_t serverPort = 8888;
+
+void TCP_ClientStart(void)
+{
+    uint8_t status = getSn_SR(SN);
+
+    if (status == SOCK_CLOSED) {
+        // 创建 TCP socket,本地端口 9999
+        int8_t n = socket(SN, Sn_MR_TCP, 9999, SF_TCP_NODELAY);
+        if (n == SN) printf("socket %d 打开成功!\n", SN);
+        else         printf("socket %d 开失败,返回码:%d\n", SN, n);
+    }
+    else if (status == SOCK_INIT) {
+        // INIT 状态 → 连接服务器
+        int8_t res = connect(SN, serverIP, serverPort);
+        if (res == SOCK_OK) {
+            printf("客户端连接服务器成功!\n");
+            TCP_SendData("Hello, this is STM32 TCP Client!", 32);
+        } else {
+            printf("客户端连接服务器失败,返回码: %d\n", res);
+        }
+    }
+    else if (status == SOCK_CLOSE_WAIT) {
+        printf("失去服务端的连接,准备关闭socket重新打开...\n");
+        close(SN);
+    }
+}
+```
+
+> 客户端状态机:`socket()` → `connect(serverIP, serverPort)` → 等待 `SOCK_ESTABLISHED`。`TCP_RecvData()`/`TCP_SendData()` 与服务器版完全相同。
+
+**文件:`stm32/07_ethernet_tcp_client_register/User/main.c`** — 主循环调用 `TCP_ClientStart`
+
+```c
+#include "usart.h"
+#include "eth.h"
+#include "tcp.h"
+
+uint8_t rxBuff[1024];
+uint16_t rxLen;
+
+int main(void)
+{
+    USART_Init();
+    printf("尚硅谷以太网实验:TCP Client\n");
+
+    ETH_Init();
+
+    while (1) {
+        TCP_ClientStart();               // 调用 Client 而非 Server
+        TCP_RecvData(rxBuff, &rxLen);
+
+        if (rxLen > 0) {
+            printf("收到数据:%.*s\n", rxLen, rxBuff);
+            TCP_SendData(rxBuff, rxLen);
+            rxLen = 0;
+        }
+    }
+}
+```
+
+---
+
+### 08 — UDP 通信(寄存器版)
+
+**项目路径**:`stm32/08_ethernet_udp_register`
+
+UDP 无连接通信,W5500 Socket 0 绑定端口 9999,收发数据时不维护连接状态。
+
+**文件:`stm32/08_ethernet_udp_register/App/UDP/udp.h`**
+
+```c
+#ifndef __UDP_H
+#define __UDP_H
+
+#include "eth.h"
+#include "socket.h"
+
+#define SN 0
+
+void UDP_Start(void);
+void UDP_RecvData(uint8_t buff[], uint16_t *len, uint8_t *srcIP, uint16_t *srcPort);
+void UDP_SendData(uint8_t data[], uint16_t len, uint8_t *dstIP, uint16_t dstPort);
+
+#endif
+```
+
+**文件:`stm32/08_ethernet_udp_register/App/UDP/udp.c`**
+
+```c
+#include "udp.h"
+
+void UDP_Start(void)
+{
+    uint8_t status = getSn_SR(SN);
+    if (status == SOCK_CLOSED) {
+        // 创建 UDP socket,协议 Sn_MR_UDP
+        int8_t n = socket(SN, Sn_MR_UDP, 9999, 0);
+        if (n == SN) printf("socket %d 打开成功!\n", SN);
+        else         printf("socket %d 开失败,返回码:%d\n", SN, n);
+    }
+}
+
+void UDP_RecvData(uint8_t buff[], uint16_t *len, uint8_t *srcIP, uint16_t *srcPort)
+{
+    uint8_t status = getSn_SR(SN);
+    if (status == SOCK_UDP && (getSn_IR(SN) & Sn_IR_RECV)) {
+        setSn_IR(SN, Sn_IR_RECV);
+
+        uint16_t tmp = getSn_RX_RSR(SN);
+        if (tmp > 8) {                     // > 8 字节说明有实际数据(前 8 字节为 UDP 头)
+            *len = tmp - 8;
+            recvfrom(SN, buff, *len, srcIP, srcPort);  // 获取数据 + 来源 IP/端口
+        }
+    }
+}
+
+void UDP_SendData(uint8_t data[], uint16_t len, uint8_t *dstIP, uint16_t dstPort)
+{
+    uint8_t status = getSn_SR(SN);
+    if (status == SOCK_UDP) {
+        sendto(SN, data, len, dstIP, dstPort);          // 指定目标 IP/端口发送
+        printf("发送完毕,数据: %.*s\n", len, data);
+    }
+}
+```
+
+> UDP 与 TCP 的关键差异:`recvfrom()` 会返回发送者的 IP 和端口(用于应答);`sendto()` 需指定目标 IP/端口;协议常量用 `Sn_MR_UDP`。
+
+**文件:`stm32/08_ethernet_udp_register/User/main.c`**
+
+```c
+#include "usart.h"
+#include "eth.h"
+#include "udp.h"
+
+uint8_t rxBuff[1024];
+uint16_t rxLen;
+uint8_t srcIP[4];
+uint16_t srcPort;
+
+int main(void)
+{
+    USART_Init();
+    printf("尚硅谷以太网实验:UDP通信\n");
+
+    ETH_Init();
+
+    while (1) {
+        UDP_Start();
+        UDP_RecvData(rxBuff, &rxLen, srcIP, &srcPort);
+
+        if (rxLen > 0) {
+            printf("收到数据:%.*s\n", rxLen, rxBuff);
+            UDP_SendData(rxBuff, rxLen, srcIP, srcPort);
+            rxLen = 0;
+        }
+    }
+}
+```
+
+---
+
+### 09 — HTTP Web Server(寄存器版)
+
+**项目路径**:`stm32/09_ethernet_webserver_register`
+
+Web Server 代码(`App/Web/web_server.h/.c`、`Interface/Ethernet/eth.h/.c`)已在之前的"实验:W5500 TCP Server"小节完整展示。此处仅展示主函数。
+
+**文件:`stm32/09_ethernet_webserver_register/User/main.c`**
+
+```c
+#include "usart.h"
+#include "eth.h"
+#include "web_server.h"
+
+int main(void)
+{
+    USART_Init();
+    printf("尚硅谷以太网实验:Web Server\n");
+
+    ETH_Init();
+    WebServer_Init();          // 注册 HTML 页面 + LED 初始化
+
+    while (1) {
+        WebServer_Start();     // 轮询所有 8 个 socket 的 HTTP 请求
+    }
+}
+```
+
+> `WebServer_Init()` 中注册的 HTML 页面包含"开灯""关灯""翻转"三个按钮,通过 URL 参数 `?action=1/2/3` 控制开发板上的 LED。这是典型的嵌入式 Web 控制面板模式。
+
+---
+
 ## 核心函数速查表(W5500 Socket API)
 
 | 函数 | 说明 |

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — WiFi章节 + 配套代码10~11"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -38,9 +38,9 @@ ESP32-C3 默认出厂固件为**AT 指令模式**——芯片从 Flash 启动后
 
 **通信方式**:
 ```
-STM32(USART) ── TX ──→ RX(ESP32-C3)
-             ── RX ←── TX(ESP32-C3)
-             ── GND ── GND
+STM32(USART2) ── TX ──→ RX(ESP32-C3)
+              ── RX ←── TX(ESP32-C3)
+              ── GND ── GND
 ```
 
 **响应格式**:每条指令执行后返回 `\r\nOK\r\n` 或 `\r\nERROR\r\n`
@@ -84,6 +84,8 @@ STM32(USART) ── TX ──→ RX(ESP32-C3)
 | `AT+CIPCLOSE\r\n` | 关闭当前连接 | `CLOSED` |
 | `AT+CIPMUX=0\r\n` | 单连接模式 | `OK` |
 | `AT+CIPMUX=1\r\n` | 多连接模式(最多 5 个) | `OK` |
+| `AT+CIPSERVER=1,port\r\n` | 创建 TCP Server | `OK` |
+| `AT+CIPDINFO=1\r\n` | IPD 显示格式含对端信息 | `OK` |
 
 ### 透传模式详解
 
@@ -113,228 +115,255 @@ STM32(USART) ── TX ──→ RX(ESP32-C3)
 
 **项目路径**:`stm32/10_wifi_test_hal`、`stm32/11_wifi_tcp_server_hal`
 
-### 软件设计(HAL 库版)
+### 实验 1:基础 AT 通信测试
 
-**基础 AT 通信测试 — 文件:`stm32/10_wifi_test_hal/Core/Src/main.c`**
+**文件:`stm32/10_wifi_test_hal/Core/Src/main.c`**
+
+两个实验共享同一个 ESP32 底层驱动(`esp32.c`),通过 USART2 与 ESP32-C3 通信。实验 1 仅发送基础 AT 测试指令:
 
 ```c
-/* Includes ------------------------------------------------------------------*/
 #include "main.h"
 #include "usart.h"
 #include "gpio.h"
 
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
 #include "esp32.h"
-/* USER CODE END Includes */
-
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
-
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
-
-/* USER CODE END 0 */
 
 int main(void)
 {
-
-  /* USER CODE BEGIN 1 */
-
-  /* USER CODE END 1 */
-
-  /* MCU Configuration--------------------------------------------------------*/
-
-  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
   HAL_Init();
-
-  /* USER CODE BEGIN Init */
-
-  /* USER CODE END Init */
-
-  /* Configure the system clock */
   SystemClock_Config();
-
-  /* USER CODE BEGIN SysInit */
-
-  /* USER CODE END SysInit */
-
-  /* Initialize all configured peripherals */
   MX_GPIO_Init();
   MX_USART1_UART_Init();
   // MX_USART2_UART_Init();
-  /* USER CODE BEGIN 2 */
 
   printf("尚硅谷Wi-Fi通信实验:AT指令...\n");
 
-  // 初始化ESP32
   ESP32_Init();
   printf("ESP32 初始化完成!\n");
 
-  // 1. 发送AT测试
   uint8_t *cmd = "AT\r\n";
   ESP32_SendCmd(cmd, strlen((char *)cmd));
 
-  // 2. 查看固件版本信息
   cmd = "AT+GMR\r\n";
   ESP32_SendCmd(cmd, strlen((char *)cmd));
 
-  /* USER CODE END 2 */
-
-  /* Infinite loop */
-  /* USER CODE BEGIN WHILE */
-  while (1)
-  {
-    /* USER CODE END WHILE */
-
-    /* USER CODE BEGIN 3 */
-  }
-  /* USER CODE END 3 */
+  while (1) { }
 }
 ```
 
-**TCP 服务器 — 文件:`stm32/11_wifi_tcp_server_hal/Core/Src/main.c`**
+**文件:`stm32/10_wifi_test_hal/Interface/ESP32/esp32.c`**(底层驱动,所有真实 AT 指令在此):
 
 ```c
-/* Includes ------------------------------------------------------------------*/
-#include "main.h"
-#include "usart.h"
-#include "gpio.h"
+#include "esp32.h"
 
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
-#include "wifi.h"
-/* USER CODE END Includes */
+uint8_t respBuff[1024];
+uint16_t respLen;
 
-/* Private variables ---------------------------------------------------------*/
+void ESP32_Init(void)
+{
+    MX_USART2_UART_Init();
 
-/* USER CODE BEGIN PV */
+    // AT+RST=0 复位 ESP32(此处的 =0 为特定固件版本的复位参数)
+    uint8_t *cmd = "AT+RST=0\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
 
-// 全局变量定义,数据缓冲区及其长度、连接id、对端IP和端口号
-uint8_t rxBuff[1024];
-uint16_t rxLen;
-uint8_t id;
-uint8_t ip[16];
-uint16_t port;
+    HAL_Delay(2000);
+}
 
-/* USER CODE END PV */
+void ESP32_SendCmd(uint8_t *cmd, uint16_t cmdLen)
+{
+    memset(respBuff, 0, 1024);
 
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
+    HAL_UART_Transmit(&huart2, cmd, cmdLen, 1000);
 
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
+    do
+    {
+        ESP32_ReadResp(respBuff, &respLen);
+    } while (strstr((char *)respBuff, "OK") == NULL);
 
-/* USER CODE END 0 */
+    printf("%.*s\n", respLen, respBuff);
+}
 
-int main(void)
+void ESP32_ReadResp(uint8_t buff[], uint16_t *len)
 {
+    HAL_UARTEx_ReceiveToIdle(&huart2, buff, 1024, len, 1000);
+}
+```
 
-  /* USER CODE BEGIN 1 */
+**文件:`stm32/10_wifi_test_hal/Interface/ESP32/esp32.h`**
 
-  /* USER CODE END 1 */
+```c
+#ifndef __ESP32_H
+#define __ESP32_H
 
-  /* MCU Configuration--------------------------------------------------------*/
+#include "usart.h"
+#include <string.h>
 
-  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
-  HAL_Init();
+void ESP32_Init(void);
+void ESP32_SendCmd(uint8_t *cmd, uint16_t cmdLen);
+void ESP32_ReadResp(uint8_t buff[], uint16_t *len);
 
-  /* USER CODE BEGIN Init */
+#endif
+```
 
-  /* USER CODE END Init */
+### 实验 2:WiFi TCP 服务器
 
-  /* Configure the system clock */
-  SystemClock_Config();
+**文件:`stm32/11_wifi_tcp_server_hal/Core/Src/main.c`**
 
-  /* USER CODE BEGIN SysInit */
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
 
-  /* USER CODE END SysInit */
+#include "wifi.h"
 
-  /* Initialize all configured peripherals */
+uint8_t rxBuff[1024];
+uint16_t rxLen;
+uint8_t id;
+uint8_t ip[16];
+uint16_t port;
+
+int main(void)
+{
+  HAL_Init();
+  SystemClock_Config();
   MX_GPIO_Init();
   MX_USART1_UART_Init();
-  // MX_USART2_UART_Init();
-  /* USER CODE BEGIN 2 */
 
   printf("尚硅谷Wi-Fi通信实验:TCP服务器...\n");
 
-  // 1. WiFi初始化
   WIFI_Init(AP);
-
-  // 2. 启动TCP服务器
   WIFI_TCP_ServerStart();
 
-  /* USER CODE END 2 */
-
-  /* Infinite loop */
-  /* USER CODE BEGIN WHILE */
   while (1)
   {
-    // 3. 从套接字读取
     WIFI_TCP_ReadData(rxBuff, &rxLen, &id, ip, &port);
 
-    // 4. 如果收到数据,原样发送回去
     if (rxLen > 0)
     {
       printf("收到数据:连接ID = %d, 对端IP:端口号 = %s:%d, 数据长度 = %d, 内容 = %.*s\n",
         id, ip, port, rxLen, rxLen, rxBuff);
 
       WIFI_TCP_SendData(id, rxBuff, rxLen);
-
-      // 清空长度0
       rxLen = 0;
     }
-    
-    /* USER CODE END WHILE */
-
-    /* USER CODE BEGIN 3 */
   }
-  /* USER CODE END 3 */
 }
+```
+
+**文件:`stm32/11_wifi_tcp_server_hal/App/WIFI/wifi.h`**
+
+```c
+#ifndef __WIFI_H
+#define __WIFI_H
 
-void SystemClock_Config(void)
+#include "esp32.h"
+
+typedef enum
 {
-  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
-  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
-
-  /** Initializes the RCC Oscillators according to the specified parameters
-   * in the RCC_OscInitTypeDef structure.
-   */
-  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
-  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
-  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
-  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
-  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
-  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
-  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
-  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
-  {
-    Error_Handler();
-  }
+    STA = 1,
+    AP = 2
+} WIFI_MODE;
 
-  /** Initializes the CPU, AHB and APB buses clocks
-   */
-  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
-  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
-  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
-  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
-  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
+void WIFI_Init(WIFI_MODE mode);
+void WIFI_TCP_ServerStart(void);
+void WIFI_TCP_SendData(uint8_t id, uint8_t *data, uint16_t len);
+void WIFI_TCP_ReadData(uint8_t rxBuff[], uint16_t *rxLen, uint8_t *id, uint8_t *ip, uint16_t *port);
 
-  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
-  {
-    Error_Handler();
-  }
+#endif
+```
+
+**文件:`stm32/11_wifi_tcp_server_hal/App/WIFI/wifi.c`**(包含所有真实 WiFi AT 指令和 TCP 通信):
+
+```c
+#include "wifi.h"
+
+static void WIFI_STA_Mode(void);
+static void WIFI_AP_Mode(void);
+
+void WIFI_Init(WIFI_MODE mode)
+{
+    ESP32_Init();
+
+    if (mode == STA)
+        WIFI_STA_Mode();
+    else if (mode == AP)
+        WIFI_AP_Mode();
+
+    printf("Wi-Fi 初始化完成!\n");
 }
 
-void Error_Handler(void)
+static void WIFI_STA_Mode(void)
 {
-  /* USER CODE BEGIN Error_Handler_Debug */
-  /* User can add his own implementation to report the HAL error return state */
-  __disable_irq();
-  while (1)
-  {
-  }
-  /* USER CODE END Error_Handler_Debug */
+    printf("设置为STA模式...\n");
+    uint8_t *cmd = "AT+CWMODE=1\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("连接AP...\n");
+    cmd = "AT+CWJAP=\"wsr\",\"1234abcd\"\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("查询IP地址为:\n");
+    cmd = "AT+CIPSTA?\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+}
+
+static void WIFI_AP_Mode(void)
+{
+    printf("设置为AP模式...\n");
+    uint8_t *cmd = "AT+CWMODE=2\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("设置AP...\n");
+    cmd = "AT+CWSAP=\"atguigu-esp32\",\"12345678\",5,3\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("设置本地IP地址...\n");
+    cmd = "AT+CIPAP=\"192.168.8.1\"\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+}
+
+void WIFI_TCP_ServerStart(void)
+{
+    printf("使能多连接...\n");
+    uint8_t *cmd = "AT+CIPMUX=1\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("启动TCP服务器...\n");
+    cmd = "AT+CIPSERVER=1,8080\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    printf("设置IPD数据格式...\n");
+    cmd = "AT+CIPDINFO=1\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+}
+
+void WIFI_TCP_SendData(uint8_t id, uint8_t *data, uint16_t len)
+{
+    printf("准备发送数据...\n");
+
+    uint8_t sendCmd[50] = {0};
+    sprintf((char *)sendCmd, "AT+CIPSEND=%d,%d\r\n", id, len);
+    ESP32_SendCmd(sendCmd, strlen((char *)sendCmd));
+
+    HAL_UART_Transmit(&huart2, data, len, 1000);
+}
+
+uint8_t tempBuff[1024];
+uint16_t tempLen;
+
+void WIFI_TCP_ReadData(uint8_t rxBuff[], uint16_t *rxLen, uint8_t *id, uint8_t *ip, uint16_t *port)
+{
+    HAL_UARTEx_ReceiveToIdle(&huart2, tempBuff, 1024, &tempLen, 1000);
+
+    if ( strstr((char *)tempBuff, "+IPD") != NULL )
+    {
+        sscanf((char *)tempBuff, "%*[\r\n]+IPD,%hhu,%hu,\"%[^\"]\",%hu",
+             id, rxLen, ip, port);
+
+        char * pData = strstr((char *)tempBuff, ":") + 1;
+        memcpy(rxBuff, pData, *rxLen);
+    }
 }
 ```
 
@@ -345,12 +374,19 @@ void Error_Handler(void)
 | 操作 | AT 指令 | 等待响应 | 超时 |
 |------|---------|---------|------|
 | 测试连接 | `AT` | `OK` | 2s |
+| 复位模块 | `AT+RST=0` | `ready` | 2s |
 | 设置 Station | `AT+CWMODE=1` | `OK` | 1s |
+| 设置 AP | `AT+CWMODE=2` | `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 |
+| 查询 IP | `AT+CIFSR` / `AT+CIPSTA?` | `OK` | 2s |
+| 设置 AP 参数 | `AT+CWSAP="ssid","pwd",ch,enc` | `OK` | 2s |
+| 设置本机 IP | `AT+CIPAP="192.168.x.x"` | `OK` | 2s |
+| 多连接使能 | `AT+CIPMUX=1` | `OK` | 2s |
+| 启动 TCP Server | `AT+CIPSERVER=1,port` | `OK` | 2s |
+| IPD 含对端信息 | `AT+CIPDINFO=1` | `OK` | 2s |
+| TCP 连接(客户端) | `AT+CIPSTART="TCP",ip,port` | `CONNECT` | 10s |
+| 发送数据 | `AT+CIPSEND=id,len` → 数据 | `SEND OK` | 5s |
 | 进入透传 | `AT+CIPMODE=1` → `AT+CIPSEND` | `>` | 3s |
 | 退出透传 | `+++`(无换行,20ms 间隔) | `OK` | — |
 | 关闭连接 | `AT+CIPCLOSE` | `CLOSED` | 5s |
@@ -362,4 +398,4 @@ void Error_Handler(void)
 3. **TCP 连接失败** → 检查目标 IP 可达性、端口是否开放、防火墙规则
 4. **透传模式数据回显** → `ATE0` 关闭回显;否则发送的数据会同时从 ESP 返回
 5. **+++ 退不出透传** → 要求前后 20ms 无数据,保证足够间隔
-6. **数据长度错** → 非透传下,AT+CIPSEND=len 的 len 必须与后续发送的数据字节数严格一致
+6. **数据长度错** → 非透传下,AT+CIPSEND=id,len 的 len 必须与后续发送的数据字节数严格一致

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — BLE章节 + 配套代码12"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -87,147 +87,170 @@ Service (服务)         如 "心率服务" UUID: 0x180D
 
 ## 实验:STM32 + BLE 模块通信
 
-STM32 通过串口连接 BLE 透传模块(支持 AT 指令),实现 BLE 数据收发。
-
 **项目路径**:`stm32/12_ble_server_hal`
 
+ESP32-C3 同时支持 WiFi 和 BLE。BLE 实验使用 ESP32-C3 的 BLE 功能作为 GATT Server(从机),通过串口 AT 指令控制。`ble.c` 中包含了完整的 BLE 初始化流程和 SPP 透传模式切换。
+
 ### 软件设计
 
 **文件:`stm32/12_ble_server_hal/Core/Src/main.c`**
 
 ```c
-/* Includes ------------------------------------------------------------------*/
 #include "main.h"
 #include "usart.h"
 #include "gpio.h"
 
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
 #include "ble.h"
-/* USER CODE END Includes */
-
-/* Private variables ---------------------------------------------------------*/
-
-/* USER CODE BEGIN PV */
 
-// 全局变量定义,数据缓冲区及其长度
 uint8_t rxBuff[1024];
 uint16_t rxLen;
 
-/* USER CODE END PV */
-
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
-
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
-
-/* USER CODE END 0 */
-
 int main(void)
 {
-
-  /* USER CODE BEGIN 1 */
-
-  /* USER CODE END 1 */
-
-  /* MCU Configuration--------------------------------------------------------*/
-
-  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
   HAL_Init();
-
-  /* USER CODE BEGIN Init */
-
-  /* USER CODE END Init */
-
-  /* Configure the system clock */
   SystemClock_Config();
-
-  /* USER CODE BEGIN SysInit */
-
-  /* USER CODE END SysInit */
-
-  /* Initialize all configured peripherals */
   MX_GPIO_Init();
   MX_USART1_UART_Init();
-  // MX_USART2_UART_Init();
-  /* USER CODE BEGIN 2 */
 
   printf("尚硅谷BLE实验...\n");
 
-  // BLE初始化
   BLE_Init();
 
-  /* USER CODE END 2 */
-
-  /* Infinite loop */
-  /* USER CODE BEGIN WHILE */
   while (1)
   {
-    // 读取数据
     BLE_ReadData(rxBuff, &rxLen);
 
-    // 如果收到数据,原样发送回去
     if (rxLen > 0)
     {
       printf("收到数据:数据长度 = %d, 内容 = %.*s\n", rxLen, rxLen, rxBuff);
 
       BLE_SendData(rxBuff, rxLen);
-
-      // 清空长度0
       rxLen = 0;
     }
-    
-    /* USER CODE END WHILE */
-
-    /* USER CODE BEGIN 3 */
   }
-  /* USER CODE END 3 */
+}
+```
+
+**文件:`stm32/12_ble_server_hal/App/BLE/ble.h`**
+
+```c
+#ifndef __BLE_H
+#define __BLE_H
+
+#include "esp32.h"
+
+void BLE_Init(void);
+void BLE_SendData(uint8_t txBuff[], uint16_t txLen);
+void BLE_ReadData(uint8_t rxBuff[], uint16_t *rxLen);
+
+#endif
+```
+
+**文件:`stm32/12_ble_server_hal/App/BLE/ble.c`**(包含完整的 BLE AT 指令序列和 SPP 透传处理):
+
+```c
+#include "ble.h"
+
+void BLE_Init(void)
+{
+    ESP32_Init();
+
+    // 1. 设置蓝牙角色为 Peripheral(服务器)
+    printf("设置蓝牙角色为服务器...\n");
+    uint8_t *cmd = "AT+BLEINIT=2\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 2. 创建 GATT 服务
+    printf("创建GATT服务...\n");
+    cmd = "AT+BLEGATTSSRVCRE\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 3. 启动 GATT 服务
+    printf("启动GATT服务...\n");
+    cmd = "AT+BLEGATTSSRVSTART\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 4. 获取本机 MAC 地址
+    printf("获取本机MAC地址...\n");
+    cmd = "AT+BLEADDR?\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 5. 配置广播参数
+    //    参数含义:最小间隔,最大间隔,广播类型,过滤策略,信道,广播策略
+    //    最小/最大间隔单位 0.625ms,50 = 31.25ms
+    printf("配置广播参数...\n");
+    cmd = "AT+BLEADVPARAM=50,50,0,0,7,0\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 6. 设置广播数据
+    //    参数:设备名,自定义UUID,厂商数据,长度类型
+    printf("设置广播数据...\n");
+    cmd = "AT+BLEADVDATAEX=\"atguigu-ble\",\"A123\",\"0102030405\",1\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 7. 开始广播
+    printf("开始广播...\n");
+    cmd = "AT+BLEADVSTART\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 8. 配置 BLE SPP 透传参数
+    //    参数含义:SPP使能,最大连接数,MTU大小,是否使能通知,分段大小
+    printf("配置 BLE SPP 参数...\n");
+    cmd = "AT+BLESPPCFG=1,1,7,1,5\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
+
+    // 9. 设置系统消息指示
+    //    4 = 只输出连接/断开事件通知
+    printf("设置系统提示信息...\n");
+    cmd = "AT+SYSMSG=4\r\n";
+    ESP32_SendCmd(cmd, strlen((char *)cmd));
 }
 
-void SystemClock_Config(void)
+void BLE_SendData(uint8_t txBuff[], uint16_t txLen)
 {
-  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
-  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
-
-  /** Initializes the RCC Oscillators according to the specified parameters
-   * in the RCC_OscInitTypeDef structure.
-   */
-  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
-  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
-  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
-  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
-  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
-  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
-  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
-  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
-  {
-    Error_Handler();
-  }
+    HAL_UART_Transmit(&huart2, txBuff, txLen, 1000);
+}
 
-  /** Initializes the CPU, AHB and APB buses clocks
-   */
-  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
-  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
-  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
-  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
-  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
+static uint8_t BLE_IsConnChanged(uint8_t msg[]);
 
-  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
-  {
-    Error_Handler();
-  }
+void BLE_ReadData(uint8_t rxBuff[], uint16_t *rxLen)
+{
+    HAL_UARTEx_ReceiveToIdle(&huart2, rxBuff, 1024, rxLen, 1000);
+
+    if (*rxLen == 0)
+        return;
+
+    if ( BLE_IsConnChanged(rxBuff) )
+        *rxLen = 0;
 }
 
-void Error_Handler(void)
+static uint8_t BLE_IsConnChanged(uint8_t msg[])
 {
-  /* USER CODE BEGIN Error_Handler_Debug */
-  /* User can add his own implementation to report the HAL error return state */
-  __disable_irq();
-  while (1)
-  {
-  }
-  /* USER CODE END Error_Handler_Debug */
+    // 收到 +BLECONN: 连接建立 → 进入 SPP 透传
+    if (strstr((char *)msg, "+BLECONN:") != NULL)
+    {
+        printf("有BLE客户端连接,准备进入SPP模式...\n");
+        uint8_t * cmd = "AT+BLESPP\r\n";
+        ESP32_SendCmd(cmd, strlen((char *)cmd));
+        return 1;
+    }
+    // 收到 +BLEDISCONN: 断开连接 → 退出 SPP 透传
+    else if (strstr((char *)msg, "+BLEDISCONN:") != NULL)
+    {
+        printf("BLE客户端断开连接,准备退出SPP模式...\n");
+        HAL_UART_Transmit(&huart2, "+++", 3, 1000);
+        HAL_Delay(2000);
+        return 1;
+    }
+    // 过滤 WiFi 网络事件(ESP32-C3 为 WiFi+BLE 双模芯片)
+    else if(strstr((char *)msg, "WIFI CONNECTED") != NULL
+        || strstr((char *)msg, "WIFI GOT IP") != NULL
+        || strstr((char *)msg, "+DIST_STA_IP:") != NULL)
+    {
+        return 1;
+    }
+
+    return 0;
 }
 ```
 
@@ -237,22 +260,30 @@ void Error_Handler(void)
 
 | BLE 操作 | AT 指令 | 说明 |
 |---------|---------|------|
-| 初始化(Peripheral) | `AT+BLEINIT=2` | 2=从机模式 |
-| 初始化(Central) | `AT+BLEINIT=1` | 1=主机模式 |
+| GATT Server 初始化(从机) | `AT+BLEINIT=2` | 2=从机模式 |
+| GATT Client 初始化(主机) | `AT+BLEINIT=1` | 1=主机模式 |
+| 创建 GATT 服务 | `AT+BLEGATTSSRVCRE` | 创建标准服务 |
+| 启动 GATT 服务 | `AT+BLEGATTSSRVSTART` | 服务上线 |
+| 获取 MAC 地址 | `AT+BLEADDR?` | 查询本机地址 |
 | 设置名称 | `AT+BLENAME="name"` | 最长 29 字节 |
-| 设置广播间隔 | `AT+BADVINT=N` | N×0.625ms,默认 100 (62.5ms) |
-| 设置广播数据 | `AT+BLEADVDATA="hex"` | 16 进制字符串 |
+| 设置广播间隔 | `AT+BLEADVPARAM=min,max,...` | 单位 0.625ms |
+| 设置广播数据 | `AT+BLEADVDATAEX="name","uuid","data",1` | 16 进制字符串 |
 | 开始广播 | `AT+BLEADVSTART` | Peripheral 模式 |
 | 停止广播 | `AT+BLEADVSTOP` | — |
-| 发送数据 | `AT+BLESEND=conn,len` → 数据 | — |
+| 发送数据(非透传) | `AT+BLESEND=conn,len` → 数据 | — |
+| SPP 透传配置 | `AT+BLESPPCFG=1,1,7,1,5` | 使能透传 |
+| 进入 SPP 透传 | `AT+BLESPP` | 连接之后调用 |
+| 退出透传 | `+++`(无换行,20ms 间隔) | — |
 | 查询连接状态 | `AT+BLECONN?` | 返回当前连接 |
 | 断开连接 | `AT+BLEDISCONN=n` | n=连接 ID |
 | 扫描设备 | `AT+BLESCAN=0` | 0=被动扫描 |
+| 连接事件通知 | `AT+SYSMSG=4` | 仅输出连接/断开 |
 
 ## 常见问题与避坑
 
 1. **手机扫描不到设备** → 检查广播是否启动、名称是否设置、广播功率是否太低
-2. **连接后数据发不出去** → 确保 GATT Server 已启动、通知/写属性已配置、MTU 大小(默认 23 字节)
+2. **连接后数据发不出去** → 确保 GATT Server 已启动、SPP 使能配置正确、MTU 大小
 3. **功耗高** → 广播间隔增大(100ms+)、连接间隔增大(30ms+)、不用时停止广播
 4. **断开连接后需要显式重连** → BLE 不会自动重连,Peripheral 需重新调用 `AT+BLEADVSTART`
-5. **连接间隔与功耗的关系**:间隔越短→延迟越低→功耗越高;传感器场景推荐 30~50ms
+5. **SPP 透传意外退出** → 数据中不能出现连续 `+++`,否则会被误判为退出命令
+6. **WiFi+BLE 同芯片干扰** → ESP32-C3 是单射频双模,BLE 通信时 WiFi 性能会下降

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(高级篇)V2.0.1 — LoRa章节 + 配套代码13~14"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -61,7 +61,7 @@ CAD 的意义:普通 LoRa 接收模式下,接收机须持续打开(功耗
 
 LoRa 网络采用**星型拓扑**:
 ```
-         ┌─→ 节点1 (SF12, 上报温度)
+         ┌─→ 节点1 (SF9, 上报温度)
 ┌──────┐ ├─→ 节点2 (SF9, 上报湿度)     ┌──────────┐
 │ 网关  │←┼─→ 节点3 (SF7, 上报位移) ───→│ 云服务器 │
@@ -79,88 +79,40 @@ LoRa 网络采用**星型拓扑**:
 
 ## 实验:LoRa 节点与网关通信
 
-### 软件设计(SPI 驱动 API 模式 — LLCC68 库)
-
 **项目路径**:`stm32/13_lora_node_hal`(节点)、`stm32/14_lora_gateway_hal`(网关)
 
-实际代码使用 LLCC68 芯片 SPI 驱动 API,通过 SPI 配置 LoRa 射频参数并收发数据,而非 AT 指令。
+实际代码使用 LLCC68 芯片 SPI 驱动 API(`lora.c`),通过 SPI 接口配置 LoRa 射频参数并收发数据。两项目共享相同的 `lora.h/.c` 驱动,默认配置为 SF9 / BW125 / CR4/5 / 480MHz。
+
+### 实验 1:LoRa 节点(按键触发发送)
 
-**文件:`stm32/13_lora_node_hal/Core/Src/main.c`(节点端)**
+**文件:`stm32/13_lora_node_hal/Core/Src/main.c`**
 
 ```c
-/* Includes ------------------------------------------------------------------*/
 #include "main.h"
 #include "spi.h"
 #include "usart.h"
 #include "gpio.h"
 
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
 #include "lora.h"
-/* USER CODE END Includes */
-
-/* Private variables ---------------------------------------------------------*/
 
-/* USER CODE BEGIN PV */
-
-// 定义全局变量,按键标志位,以及计数值
 uint8_t isKeyed;
 uint32_t count;
 
-/* USER CODE END PV */
-
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
-
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
-
-/* USER CODE END 0 */
-
 int main(void)
 {
-
-  /* USER CODE BEGIN 1 */
-
-  /* USER CODE END 1 */
-
-  /* MCU Configuration--------------------------------------------------------*/
-
-  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
   HAL_Init();
-
-  /* USER CODE BEGIN Init */
-
-  /* USER CODE END Init */
-
-  /* Configure the system clock */
   SystemClock_Config();
-
-  /* USER CODE BEGIN SysInit */
-
-  /* USER CODE END SysInit */
-
-  /* Initialize all configured peripherals */
   MX_GPIO_Init();
-  // MX_SPI1_Init();
   MX_USART1_UART_Init();
-  /* USER CODE BEGIN 2 */
 
   printf("尚硅谷LoRa通讯实验:普通节点...\n");
 
   LoRa_Init();
-
-  // 进入接收模式
   LoRa_EnterRxMode();
 
-  // 定义接收缓冲区和长度
   uint8_t rxBuff[256];
   uint16_t rxLen;
 
-  /* USER CODE END 2 */
-
-  /* Infinite loop */
-  /* USER CODE BEGIN WHILE */
   while (1)
   {
     LoRa_RecvData(rxBuff, &rxLen);
@@ -168,163 +120,60 @@ int main(void)
     if (rxLen > 0)
     {
       printf("收到数据:data: %.*s\n", rxLen, rxBuff);
-
-      // 清空长度
       rxLen = 0;
     }
     if (isKeyed)
     {
       printf("按键按下!\n");
 
-      // 准备发送数据,拼接上按键次数
       uint8_t msg[100] = {0};
       sprintf((char *)msg, "一个普通LoRa节点,准备开始发送数据... %d", ++count);
 
       LoRa_SendData(msg, strlen((char *)msg));
-
-      isKeyed = 0;  // 清除标志位
-
-      // 再次进入接收模式
+      isKeyed = 0;
       LoRa_EnterRxMode();
     }
-    /* USER CODE END WHILE */
-
-    /* USER CODE BEGIN 3 */
   }
-  /* USER CODE END 3 */
 }
 
-void SystemClock_Config(void)
-{
-  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
-  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
-
-  /** Initializes the RCC Oscillators according to the specified parameters
-  * in the RCC_OscInitTypeDef structure.
-  */
-  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
-  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
-  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
-  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
-  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
-  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
-  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
-  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
-  {
-    Error_Handler();
-  }
-
-  /** Initializes the CPU, AHB and APB buses clocks
-  */
-  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
-                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
-  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
-  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
-  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
-  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
-
-  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
-  {
-    Error_Handler();
-  }
-}
-
-/* USER CODE BEGIN 4 */
 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
 {
   if (GPIO_Pin == KEY_Pin)
   {
-    // 延时消抖
     HAL_Delay(100);
-
     if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin))
-    {
       isKeyed = 1;
-    }
-    
   }
-  
-}
-/* USER CODE END 4 */
-
-void Error_Handler(void)
-{
-  /* USER CODE BEGIN Error_Handler_Debug */
-  /* User can add his own implementation to report the HAL error return state */
-  __disable_irq();
-  while (1)
-  {
-  }
-  /* USER CODE END Error_Handler_Debug */
 }
 ```
 
-**文件:`stm32/14_lora_gateway_hal/Core/Src/main.c`(网关端)**
+### 实验 2:LoRa 网关(自动回复)
+
+**文件:`stm32/14_lora_gateway_hal/Core/Src/main.c`**
 
 ```c
-/* Includes ------------------------------------------------------------------*/
 #include "main.h"
 #include "spi.h"
 #include "usart.h"
 #include "gpio.h"
 
-/* Private includes ----------------------------------------------------------*/
-/* USER CODE BEGIN Includes */
 #include "lora.h"
-/* USER CODE END Includes */
-
-/* Private function prototypes -----------------------------------------------*/
-void SystemClock_Config(void);
-
-/* Private user code ---------------------------------------------------------*/
-/* USER CODE BEGIN 0 */
-
-/* USER CODE END 0 */
 
 int main(void)
 {
-
-  /* USER CODE BEGIN 1 */
-
-  /* USER CODE END 1 */
-
-  /* MCU Configuration--------------------------------------------------------*/
-
-  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
   HAL_Init();
-
-  /* USER CODE BEGIN Init */
-
-  /* USER CODE END Init */
-
-  /* Configure the system clock */
   SystemClock_Config();
-
-  /* USER CODE BEGIN SysInit */
-
-  /* USER CODE END SysInit */
-
-  /* Initialize all configured peripherals */
   MX_GPIO_Init();
-  // MX_SPI1_Init();
   MX_USART1_UART_Init();
-  /* USER CODE BEGIN 2 */
 
   printf("尚硅谷LoRa通讯实验:网关节点...\n");
 
   LoRa_Init();
-
-  // 进入接收模式
   LoRa_EnterRxMode();
 
-  // 定义接收缓冲区和长度
   uint8_t rxBuff[256];
   uint16_t rxLen;
 
-  /* USER CODE END 2 */
-
-  /* Infinite loop */
-  /* USER CODE BEGIN WHILE */
   while (1)
   {
     LoRa_RecvData(rxBuff, &rxLen);
@@ -332,69 +181,191 @@ int main(void)
     if (rxLen > 0)
     {
       printf("收到数据:data: %.*s\n", rxLen, rxBuff);
-
-      // 清空长度
       rxLen = 0;
 
-      // 发送回复消息
       uint8_t *msg = "网关已经收到你的数据,收到请回复...";
       LoRa_SendData(msg, strlen((char *)msg));
-
-      // 再次进入接收模式
       LoRa_EnterRxMode();
     }
-    
-    /* USER CODE END WHILE */
-
-    /* USER CODE BEGIN 3 */
   }
-  /* USER CODE END 3 */
+}
+```
+
+### LoRa 驱动(节点与网关共享)
+
+**文件:`stm32/13_lora_node_hal/Interface/LoRa/lora.h`**(默认射频参数:SF9/BW125/CR4/5/480MHz/+17dBm)
+
+```c
+#ifndef __LORA_H
+#define __LORA_H
+
+#include "driver_llcc68_interface.h"
+
+#define LLCC68_LORA_DEFAULT_STOP_TIMER_ON_PREAMBLE      LLCC68_BOOL_FALSE
+#define LLCC68_LORA_DEFAULT_REGULATOR_MODE              LLCC68_REGULATOR_MODE_DC_DC_LDO
+#define LLCC68_LORA_DEFAULT_PA_CONFIG_DUTY_CYCLE        0x02
+#define LLCC68_LORA_DEFAULT_PA_CONFIG_HP_MAX            0x03
+#define LLCC68_LORA_DEFAULT_TX_DBM                      17
+#define LLCC68_LORA_DEFAULT_RAMP_TIME                   LLCC68_RAMP_TIME_10US
+#define LLCC68_LORA_DEFAULT_SF                          LLCC68_LORA_SF_9
+#define LLCC68_LORA_DEFAULT_BANDWIDTH                   LLCC68_LORA_BANDWIDTH_125_KHZ
+#define LLCC68_LORA_DEFAULT_CR                          LLCC68_LORA_CR_4_5
+#define LLCC68_LORA_DEFAULT_LOW_DATA_RATE_OPTIMIZE      LLCC68_BOOL_FALSE
+#define LLCC68_LORA_DEFAULT_RF_FREQUENCY                480000000U
+#define LLCC68_LORA_DEFAULT_SYMB_NUM_TIMEOUT            0
+#define LLCC68_LORA_DEFAULT_SYNC_WORD                   0x3444U
+#define LLCC68_LORA_DEFAULT_RX_GAIN                     0x94
+#define LLCC68_LORA_DEFAULT_OCP                         0x38
+#define LLCC68_LORA_DEFAULT_PREAMBLE_LENGTH             12
+#define LLCC68_LORA_DEFAULT_HEADER                      LLCC68_LORA_HEADER_EXPLICIT
+#define LLCC68_LORA_DEFAULT_BUFFER_SIZE                 255
+#define LLCC68_LORA_DEFAULT_CRC_TYPE                    LLCC68_LORA_CRC_TYPE_ON
+#define LLCC68_LORA_DEFAULT_INVERT_IQ                   LLCC68_BOOL_FALSE
+#define LLCC68_LORA_DEFAULT_CAD_SYMBOL_NUM              LLCC68_LORA_CAD_SYMBOL_NUM_2
+#define LLCC68_LORA_DEFAULT_CAD_DET_PEAK                24
+#define LLCC68_LORA_DEFAULT_CAD_DET_MIN                 10
+#define LLCC68_LORA_DEFAULT_START_MODE                  LLCC68_START_MODE_WARM
+#define LLCC68_LORA_DEFAULT_RTC_WAKE_UP                 LLCC68_BOOL_TRUE
+
+uint8_t LoRa_Init(void);
+uint8_t LoRa_SendData(uint8_t data[], uint16_t len);
+uint8_t LoRa_EnterRxMode(void);
+void LoRa_RecvData(uint8_t rxBuff[], uint16_t *rxLen);
+
+#endif
+```
+
+**文件:`stm32/13_lora_node_hal/Interface/LoRa/lora.c`**(LLCC68 SPI 驱动封装)
+
+```c
+#include "lora.h"
+
+static llcc68_handle_t gs_handle;
+
+uint8_t LoRa_Init(void)
+{
+    printf("LoRa开始初始化...\n");
+    uint8_t res;
+    uint32_t reg;
+    uint8_t modulation, config;
+
+    DRIVER_LLCC68_LINK_INIT(&gs_handle, llcc68_handle_t);
+    DRIVER_LLCC68_LINK_SPI_INIT(&gs_handle, llcc68_interface_spi_init);
+    DRIVER_LLCC68_LINK_SPI_DEINIT(&gs_handle, llcc68_interface_spi_deinit);
+    DRIVER_LLCC68_LINK_SPI_WRITE_READ(&gs_handle, llcc68_interface_spi_write_read);
+    DRIVER_LLCC68_LINK_RESET_GPIO_INIT(&gs_handle, llcc68_interface_reset_gpio_init);
+    DRIVER_LLCC68_LINK_RESET_GPIO_DEINIT(&gs_handle, llcc68_interface_reset_gpio_deinit);
+    DRIVER_LLCC68_LINK_RESET_GPIO_WRITE(&gs_handle, llcc68_interface_reset_gpio_write);
+    DRIVER_LLCC68_LINK_BUSY_GPIO_INIT(&gs_handle, llcc68_interface_busy_gpio_init);
+    DRIVER_LLCC68_LINK_BUSY_GPIO_DEINIT(&gs_handle, llcc68_interface_busy_gpio_deinit);
+    DRIVER_LLCC68_LINK_BUSY_GPIO_READ(&gs_handle, llcc68_interface_busy_gpio_read);
+    DRIVER_LLCC68_LINK_DELAY_MS(&gs_handle, llcc68_interface_delay_ms);
+    DRIVER_LLCC68_LINK_DEBUG_PRINT(&gs_handle, llcc68_interface_debug_print);
+    DRIVER_LLCC68_LINK_RECEIVE_CALLBACK(&gs_handle, llcc68_interface_receive_callback);
+
+    res = llcc68_init(&gs_handle);
+    if (res != 0) { return 1; }
+
+    // 进入待机模式 → 配置寄存器链
+    llcc68_set_standby(&gs_handle, LLCC68_CLOCK_SOURCE_XTAL_32MHZ);
+    llcc68_set_stop_timer_on_preamble(&gs_handle, LLCC68_LORA_DEFAULT_STOP_TIMER_ON_PREAMBLE);
+    llcc68_set_regulator_mode(&gs_handle, LLCC68_LORA_DEFAULT_REGULATOR_MODE);
+    llcc68_set_pa_config(&gs_handle, LLCC68_LORA_DEFAULT_PA_CONFIG_DUTY_CYCLE,
+                         LLCC68_LORA_DEFAULT_PA_CONFIG_HP_MAX);
+    llcc68_set_rx_tx_fallback_mode(&gs_handle, LLCC68_RX_TX_FALLBACK_MODE_STDBY_XOSC);
+    llcc68_set_dio_irq_params(&gs_handle, 0x03FF, 0x03FF, 0x0000, 0x0000);
+    llcc68_clear_irq_status(&gs_handle, 0x03FF);
+    llcc68_set_packet_type(&gs_handle, LLCC68_PACKET_TYPE_LORA);
+    llcc68_set_tx_params(&gs_handle, LLCC68_LORA_DEFAULT_TX_DBM, LLCC68_LORA_DEFAULT_RAMP_TIME);
+
+    // 设置 LoRa 调制参数:SF9 / BW125 / CR4/5
+    llcc68_set_lora_modulation_params(&gs_handle,
+        LLCC68_LORA_DEFAULT_SF, LLCC68_LORA_DEFAULT_BANDWIDTH,
+        LLCC68_LORA_DEFAULT_CR, LLCC68_LORA_DEFAULT_LOW_DATA_RATE_OPTIMIZE);
+
+    // 设置射频频率:480MHz
+    llcc68_frequency_convert_to_register(&gs_handle, LLCC68_LORA_DEFAULT_RF_FREQUENCY, &reg);
+    llcc68_set_rf_frequency(&gs_handle, reg);
+
+    llcc68_set_buffer_base_address(&gs_handle, 0x00, 0x00);
+    llcc68_set_lora_symb_num_timeout(&gs_handle, LLCC68_LORA_DEFAULT_SYMB_NUM_TIMEOUT);
+    llcc68_reset_stats(&gs_handle, 0x0000, 0x0000, 0x0000);
+    llcc68_clear_device_errors(&gs_handle);
+    llcc68_set_lora_sync_word(&gs_handle, LLCC68_LORA_DEFAULT_SYNC_WORD);
+
+    // 配置发射调制 + 接收增益 + 过流保护
+    llcc68_get_tx_modulation(&gs_handle, &modulation);
+    modulation |= 0x04;
+    llcc68_set_tx_modulation(&gs_handle, modulation);
+    llcc68_set_rx_gain(&gs_handle, LLCC68_LORA_DEFAULT_RX_GAIN);
+    llcc68_set_ocp(&gs_handle, LLCC68_LORA_DEFAULT_OCP);
+
+    llcc68_get_tx_clamp_config(&gs_handle, &config);
+    config |= 0x1E;
+    llcc68_set_tx_clamp_config(&gs_handle, config);
+
+    printf("LoRa初始化完成!\n");
+    return 0;
 }
 
-void SystemClock_Config(void)
+uint8_t LoRa_SendData(uint8_t data[], uint16_t len)
 {
-  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
-  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
-
-  /** Initializes the RCC Oscillators according to the specified parameters
-  * in the RCC_OscInitTypeDef structure.
-  */
-  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
-  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
-  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
-  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
-  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
-  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
-  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
-  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
-  {
-    Error_Handler();
-  }
+    TXEN_HIGH; RXEN_LOW;
 
-  /** Initializes the CPU, AHB and APB buses clocks
-  */
-  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
-                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
-  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
-  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
-  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
-  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
+    llcc68_set_dio_irq_params(&gs_handle,
+        LLCC68_IRQ_TX_DONE | LLCC68_IRQ_TIMEOUT | LLCC68_IRQ_CAD_DONE | LLCC68_IRQ_CAD_DETECTED,
+        LLCC68_IRQ_TX_DONE | LLCC68_IRQ_TIMEOUT | LLCC68_IRQ_CAD_DONE | LLCC68_IRQ_CAD_DETECTED,
+        0x0000, 0x0000);
+    llcc68_clear_irq_status(&gs_handle, 0x03FFU);
 
-  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
-  {
-    Error_Handler();
-  }
+    if (llcc68_lora_transmit(&gs_handle, LLCC68_CLOCK_SOURCE_XTAL_32MHZ,
+        LLCC68_LORA_DEFAULT_PREAMBLE_LENGTH, LLCC68_LORA_DEFAULT_HEADER,
+        LLCC68_LORA_DEFAULT_CRC_TYPE, LLCC68_LORA_DEFAULT_INVERT_IQ,
+        data, len, 0) != 0)
+        return 1;
+
+    return 0;
 }
 
-void Error_Handler(void)
+uint8_t LoRa_EnterRxMode(void)
 {
-  /* USER CODE BEGIN Error_Handler_Debug */
-  /* User can add his own implementation to report the HAL error return state */
-  __disable_irq();
-  while (1)
-  {
-  }
-  /* USER CODE END Error_Handler_Debug */
+    TXEN_LOW; RXEN_HIGH;
+
+    llcc68_set_dio_irq_params(&gs_handle,
+        LLCC68_IRQ_RX_DONE | LLCC68_IRQ_TIMEOUT | LLCC68_IRQ_CRC_ERR |
+        LLCC68_IRQ_CAD_DONE | LLCC68_IRQ_CAD_DETECTED,
+        LLCC68_IRQ_RX_DONE | LLCC68_IRQ_TIMEOUT | LLCC68_IRQ_CRC_ERR |
+        LLCC68_IRQ_CAD_DONE | LLCC68_IRQ_CAD_DETECTED,
+        0x0000, 0x0000);
+    llcc68_clear_irq_status(&gs_handle, 0x03FFU);
+    llcc68_set_lora_packet_params(&gs_handle,
+        LLCC68_LORA_DEFAULT_PREAMBLE_LENGTH, LLCC68_LORA_DEFAULT_HEADER,
+        LLCC68_LORA_DEFAULT_BUFFER_SIZE, LLCC68_LORA_DEFAULT_CRC_TYPE,
+        LLCC68_LORA_DEFAULT_INVERT_IQ);
+
+    uint8_t setup;
+    llcc68_get_iq_polarity(&gs_handle, &setup);
+#if LLCC68_LORA_DEFAULT_INVERT_IQ == LLCC68_BOOL_FALSE
+    setup |= 1 << 2;
+#else
+    setup &= ~(1 << 2);
+#endif
+    llcc68_set_iq_polarity(&gs_handle, setup);
+
+    llcc68_continuous_receive(&gs_handle);
+    return 0;
+}
+
+void LoRa_RecvData(uint8_t rxBuff[], uint16_t *rxLen)
+{
+    llcc68_irq_handler(&gs_handle);
+
+    if (gs_handle.receive_len > 0)
+    {
+        *rxLen = gs_handle.receive_len;
+        memcpy(rxBuff, gs_handle.receive_buf, *rxLen);
+        gs_handle.receive_len = 0;
+    }
 }
 ```
 
@@ -402,17 +373,16 @@ void Error_Handler(void)
 
 ## 核心参数速查表
 
-| 参数 | 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=开(低功耗监听) |
+| 参数 | lora.h 宏定义 | 说明 |
+|------|--------------|------|
+| 扩频因子 | `LLCC68_LORA_DEFAULT_SF` | SF9(默认),可改为 SF7~SF12 |
+| 带宽 | `LLCC68_LORA_DEFAULT_BANDWIDTH` | 125 KHz(默认) |
+| 编码率 | `LLCC68_LORA_DEFAULT_CR` | CR4/5(默认) |
+| 发射功率 | `LLCC68_LORA_DEFAULT_TX_DBM` | +17dBm(默认) |
+| 中心频率 | `LLCC68_LORA_DEFAULT_RF_FREQUENCY` | 480000000 Hz |
+| 同步字 | `LLCC68_LORA_DEFAULT_SYNC_WORD` | 0x3444(公开网络) |
+| 前导码长度 | `LLCC68_LORA_DEFAULT_PREAMBLE_LENGTH` | 12 |
+| CRC | `LLCC68_LORA_DEFAULT_CRC_TYPE` | CRC_ON |
 
 ## 常见问题与避坑
 
@@ -420,5 +390,5 @@ void Error_Handler(void)
 2. **丢包率高** → 降低数据速率(高 SF + 低 BW)、增加重传机制、避免信噪比过低的链路
 3. **节点功耗高** → 开启 CAD(低功耗监听模式)、降低发射功率(10dBm 比 20dBm 省一半)、延长发送间隔
 4. **多节点冲突** → 不同节点错开发送时间(TDMA)、使用不同的扩频因子(CDMA 效果,互不干扰)
-5. **LoRa 模块不响应** → 检查波特率匹配、模块供电(峰值 > 120mA)、天线是否接好(长时间无天线可能烧 PA)
+5. **LoRa 模块不响应** → 检查 SPI 接线(NSS/SCK/MISO/MOSI)、检查模块供电(峰值 > 120mA)、天线是否接好
 6. **SF/BW/CR 参数必须一致** → 节点和网关的射频参数完全一致才能通信

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — 第1章:电源控制 + 配套代码15~20"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -84,7 +84,6 @@ void PVD_IRQHandler(void)
 {
     if (EXTI->PR & EXTI_PR_PR16)
     {
-        // 电压低于 2.8V → 紧急保存数据到备份寄存器
         SaveEmergencyData();
         EXTI->PR |= EXTI_PR_PR16;
     }
@@ -111,107 +110,156 @@ 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: 进中断→退出中断时自动睡眠
+// SLEEP-NOW: 执行 WFI 立即进睡眠
 
 SCB->SCR |= SCB_SCR_SLEEPONEXIT_Msk;    // SLEEPONEXIT=1
-//   SLEEP-ON-EXIT: 执行 WFI → 最低优先级 ISR 退出后进睡眠
+// SLEEP-ON-EXIT: 执行 WFI → 最低优先级 ISR 退出后进睡眠
 ```
 
 ### 停止模式(Stop)
 
 CPU + 所有外设时钟都停止,但 **SRAM 和寄存器内容保留**。电压调节器切到低功耗模式。
 
-**配置与进入**:
 ```c
-// 进入停止模式的函数
 void enter_stop_mode(void)
 {
-	// 1. 设置深度睡眠模式
-	SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    SCB->SCR |= SCB_SCR_SLEEPDEEP;    // 深度睡眠
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+    PWR->CR &= ~PWR_CR_PDDS;          // PDDS=0 表示停止模式
+    PWR->CR |= PWR_CR_LPDS;           // 电压调节器低功耗模式
+    __WFI();
+}
+```
 
-	// 2. 开启PWR模块时钟
-	RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+**唤醒后**:触发 EXTI 中断后,系统从停止模式恢复,执行 ISR。
 
-	// 3. 设置PDDS = 0,表示停止模式
-	PWR->CR &= ~PWR_CR_PDDS;
+### 待机模式(Standby)
 
-	// 4. 设置电压调节器的工作模式:低功耗模式
-	PWR->CR |= PWR_CR_LPDS;
+最省电模式。1.8V 内核域完全断电(SRAM 和寄存器内容丢失),仅备份域(VBAT)和待机电路保持。
 
-	// 5. 使用WFI指令,进入停止模式
-	__WFI();
+```c
+void enter_standby_mode(void)
+{
+    SCB->SCR |= SCB_SCR_SLEEPDEEP;    // 深度睡眠
+    PWR->CR |= PWR_CR_PDDS;           // PDDS=1 表示待机模式
+    PWR->CSR |= PWR_CSR_EWUP;         // 使能 PA0 唤醒功能
+    __WFI();
 }
+// 唤醒后相当于系统复位!从 startup 重新执行。
 ```
 
-**唤醒后**:触发 EXTI 中断后,系统从停止模式恢复,执行 ISR,恢复运行主程序。
+---
 
-### 待机模式(Standby)
+## 实验:睡眠/停止/待机模式
 
-最省电模式。1.8V 内核域完全断电(SRAM 和寄存器内容丢失),仅备份域(VBAT)和待机电路保持。
+所有 6 个项目均来自 `stm32/15~20_lowpower_xxx`,覆盖寄存器版和 HAL 库版三种低功耗模式。
+
+### 睡眠模式(Sleep)
+
+**项目 `15_lowpower_sleep_register`(寄存器版)**
+
+**文件:`stm32/15_lowpower_sleep_register/User/main.c`**
 
-**配置与进入**:
 ```c
-// 进入待机模式的函数
-void enter_standby_mode(void)
+#include "usart.h"
+#include "delay.h"
+#include "led.h"
+
+void enter_sleep_mode(void);
+
+int main(void)
 {
-	// 1. 设置深度睡眠模式
-	SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    USART_Init();
+    LED_Init();
 
-	// 2. 设置PDDS = 1,表示待机模式
-	PWR->CR |= PWR_CR_PDDS;
+    printf("尚硅谷低功耗实验:睡眠模式...\n");
 
-	// 3. 使能 PA0 的唤醒功能
-	PWR->CSR |= PWR_CSR_EWUP;
+    LED_On(LED1);
+    Delay_s(2);
 
-	// 4. 使用WFI指令,进入待机模式
-	__WFI();
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入睡眠模式...\n");
+        Delay_s(3);
+        printf("进入睡眠模式");
+        enter_sleep_mode();
+
+        printf("从睡眠模式唤醒...\n");
+        Delay_s(2);
+    }
 }
 
-// 唤醒后相当于系统复位!从 startup 重新执行。
+void enter_sleep_mode(void)
+{
+    // SLEEP-NOW: SLEEPDEEP=0, 执行 WFI
+    SCB->SCR &= ~SCB_SCR_SLEEPDEEP;
+    __WFI();
+}
 ```
 
-**唤醒后检测**:
+**项目 `16_lowpower_sleep_hal`(HAL 库版)**
+
+**文件:`stm32/16_lowpower_sleep_hal/Core/Src/main.c`**
+
 ```c
-// 开启PWR模块时钟
-RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
 
-// 判断标志位来判断是否从待机模式唤醒
-if (PWR->CSR & PWR_CSR_SBF)
+uint8_t ch;
+
+int main(void)
 {
-    printf("从待机模式唤醒:\n");
-    PWR->CR |= PWR_CR_CSBF;
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
+
+    printf("尚硅谷低功耗实验:睡眠模式...\n");
+
+    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
+    HAL_Delay(2000);
+
+    HAL_UART_Receive_IT(&huart1, &ch, 1);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入睡眠模式...\n");
+        HAL_Delay(3000);
+        printf("进入睡眠模式\n");
+
+        HAL_SuspendTick();                             // 暂停 Systick
+        HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON,
+                               PWR_SLEEPENTRY_WFI);   // 进睡眠
+        HAL_ResumeTick();                              // 恢复 Systick
+
+        printf("从睡眠模式唤醒...\n");
+        HAL_Delay(2000);
+    }
 }
-if (PWR->CSR & PWR_CSR_WUF)
+
+void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
 {
-    printf("产生了唤醒事件:\n");
-    PWR->CR |= PWR_CR_CWUF;
+    if (huart->Instance == USART1)
+    {
+        printf("%c", ch);
+        HAL_UART_Receive_IT(&huart1, &ch, 1);
+    }
 }
 ```
 
----
-
-## 实验:睡眠/停止/待机模式
-
-### 项目路径
+### 停止模式(Stop)
 
-| 实验 | 寄存器版 | 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` |
+**项目 `17_lowpower_stop_register`(寄存器版)**
 
-**文件:`stm32/17_lowpower_stop_register/User/main.c`**(停止模式,进入停止模式后通过 KEY 唤醒)
+**文件:`stm32/17_lowpower_stop_register/User/main.c`**
 
 ```c
 #include "usart.h"
@@ -220,147 +268,140 @@ if (PWR->CSR & PWR_CSR_WUF)
 #include "key.h"
 
 void enter_stop_mode(void);
-void get_clock_freq(uint32_t *sys_clock, uint32_t *AHB_clock, uint32_t *APB2_clock, uint32_t *APB1_clock);
+void get_clock_freq(uint32_t *sys_clock, uint32_t *AHB_clock,
+                    uint32_t *APB2_clock, uint32_t *APB1_clock);
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	LED_Init();
-	KEY_Init();
-
-	printf("尚硅谷低功耗实验:停止模式...\n");
+    USART_Init();
+    LED_Init();
+    KEY_Init();
 
-	// 1. 点亮LED灯,延时2s,模拟正常运行程序
-	LED_On(LED_2);
-	Delay_s(2);
+    printf("尚硅谷低功耗实验:停止模式...\n");
 
-	while (1)
-	{
-		// 2. 进入停止模式
-		printf("正常运行执行完毕,3s后进入停止模式...\n");
-		Delay_s(3);
-		printf("进入停止模式...\n");
-		Delay_ms(1);	// 稍作延时确保串口信息成功发送
-		enter_stop_mode();
+    LED_On(LED_2);
+    Delay_s(2);
 
-		uint32_t sys_clock, ahb_clock, apb1_clock, apb2_clock;
-		get_clock_freq(&sys_clock, &ahb_clock, &apb2_clock, &apb1_clock);
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入停止模式...\n");
+        Delay_s(3);
+        printf("进入停止模式...\n");
+        Delay_ms(1);
+        enter_stop_mode();
 
-		// 3. 重新进行系统初始化,配置系统时钟
-		SystemInit();
+        uint32_t sys_clock, ahb_clock, apb1_clock, apb2_clock;
+        get_clock_freq(&sys_clock, &ahb_clock, &apb2_clock, &apb1_clock);
 
-		printf("唤醒前的时钟频率:\nsys_clock = %d, ahb_clock = %d, apb1_clock = %d, apb2_clock = %d\n\n",
-				sys_clock, ahb_clock, apb1_clock, apb2_clock);
+        // 停止模式唤醒后 HSE/PLL 已关闭,需重新配置时钟
+        SystemInit();
 
-		get_clock_freq(&sys_clock, &ahb_clock, &apb2_clock, &apb1_clock);
+        printf("唤醒前的时钟频率:\nsys_clock = %d, ahb_clock = %d, ...\n\n",
+                sys_clock, ahb_clock, apb1_clock, apb2_clock);
 
-		printf("唤醒后的时钟频率:\nsys_clock = %d, ahb_clock = %d, apb1_clock = %d, apb2_clock = %d\n\n",
-				sys_clock, ahb_clock, apb1_clock, apb2_clock);
+        get_clock_freq(&sys_clock, &ahb_clock, &apb2_clock, &apb1_clock);
+        printf("唤醒后的时钟频率:\nsys_clock = %d, ahb_clock = %d, ...\n\n",
+                sys_clock, ahb_clock, apb1_clock, apb2_clock);
 
-		// 4. 以下代码只有在唤醒之后才会执行
-		printf("从停止模式唤醒...\n");
-		Delay_s(2);
-	}
+        printf("从停止模式唤醒...\n");
+        Delay_s(2);
+    }
 }
 
-// 进入深度睡眠模式的函数
 void enter_stop_mode(void)
 {
-	// 1. 设置深度睡眠模式
-	SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+    PWR->CR &= ~PWR_CR_PDDS;       // 停止模式
+    PWR->CR |= PWR_CR_LPDS;        // 低功耗调节器
+    __WFI();
+}
 
-	// 2. 开启PWR模块时钟
-	RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+void get_clock_freq(uint32_t *sys_clock, uint32_t *AHB_clock,
+                    uint32_t *APB2_clock, uint32_t *APB1_clock)
+{
+    uint32_t clock_src = RCC->CFGR & RCC_CFGR_SWS;
+    if (clock_src == RCC_CFGR_SWS_HSE)
+        *sys_clock = HSE_VALUE;
+    else if (clock_src == RCC_CFGR_SWS_HSI)
+        *sys_clock = HSI_VALUE;
+    else if (clock_src == RCC_CFGR_SWS_PLL)
+    {
+        uint32_t mul = ((RCC->CFGR & RCC_CFGR_PLLMULL) >> 18) + 2;
+        *sys_clock = HSE_VALUE * mul;
+    }
 
-	// 3. 设置PDDS = 0,表示停止模式
-	PWR->CR &= ~PWR_CR_PDDS;
+    uint32_t hpre = (RCC->CFGR & RCC_CFGR_HPRE) >> 4;
+    if (hpre & 0x8)
+    {
+        uint32_t n = hpre & 0x7;
+        *AHB_clock = (n < 4) ? (*sys_clock >> (n+1)) : (*sys_clock >> (n+2));
+    }
+    else *AHB_clock = *sys_clock;
 
-	// 4. 设置电压调节器的工作模式:低功耗模式
-	PWR->CR |= PWR_CR_LPDS;
+    uint32_t ppre1 = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8;
+    *APB1_clock = (ppre1 & 0x4) ? (*AHB_clock >> ((ppre1 & 0x3) + 1)) : *AHB_clock;
 
-	// 5. 使用WFI指令,进入停止模式
-	__WFI();
+    uint32_t ppre2 = (RCC->CFGR & RCC_CFGR_PPRE2) >> 11;
+    *APB2_clock = (ppre2 & 0x4) ? (*AHB_clock >> ((ppre2 & 0x3) + 1)) : *AHB_clock;
 }
+```
 
-// 自定义函数,查询各时钟频率(系统时钟sysclock、AHB、APB2、APB1)
-void get_clock_freq(uint32_t *sys_clock, uint32_t *AHB_clock, uint32_t *APB2_clock, uint32_t *APB1_clock)
+**项目 `18_lowpower_stop_hal`(HAL 库版)**
+
+**文件:`stm32/18_lowpower_stop_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
+
+uint8_t ch;
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
+
+    printf("尚硅谷低功耗实验:停止模式...\n");
+
+    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
+    HAL_Delay(2000);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入停止模式...\n");
+        HAL_Delay(3000);
+        printf("进入停止模式...\n");
+
+        // HAL 停止模式:低功耗调节器 + WFI 进入
+        HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI);
+
+        // 唤醒后需重新配置系统时钟
+        SystemClock_Config();
+
+        printf("从停止模式唤醒...\n");
+        HAL_Delay(2000);
+    }
+}
+
+void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
 {
-	// 1. 获取时钟源
-	uint32_t clock_src = RCC->CFGR & RCC_CFGR_SWS;
-
-	// 2. 根据时钟源获取系统时钟频率
-	if (clock_src == RCC_CFGR_SWS_HSE)
-	{
-		*sys_clock = HSE_VALUE;
-	}
-	else if (clock_src == RCC_CFGR_SWS_HSI)
-	{
-		*sys_clock = HSI_VALUE;
-	}
-	else if (clock_src == RCC_CFGR_SWS_PLL)
-	{
-		// 取倍频系数值
-		uint32_t mul = ((RCC->CFGR & RCC_CFGR_PLLMULL) >> 18) + 2;
-		*sys_clock = HSE_VALUE * mul;
-	}
-
-	// 3. 根据分频系数确定最终时钟频率
-	// 3.1 AHB
-	// 3.1.1 获取分频系数
-	uint32_t hpre = (RCC->CFGR & RCC_CFGR_HPRE) >> 4;
-	// 3.1.2 检查最高位来判断是否有分频
-	if (hpre & 0x8)
-	{
-		// 有分频,取低三位
-		uint32_t n = hpre & 0x7;
-		// 与4比较,选则n+1位或n+2位
-		if (n < 4)
-		{
-			*AHB_clock = *sys_clock >> (n+1);
-		}
-		else
-		{
-			*AHB_clock = *sys_clock >> (n+2);
-		}
-	}
-	else
-	{
-		// 没有分频
-		*AHB_clock = *sys_clock;
-	}
-
-	// 3.2 APB1
-	// 3.2.1 获取分频系数
-	uint32_t ppre1 = (RCC->CFGR & RCC_CFGR_PPRE1) >> 8;
-	// 3.2.2 检查最高位来判断是否有分频
-	if (ppre1 & 0x4)
-	{
-		*APB1_clock = *AHB_clock >> ((ppre1 & 0x3) + 1);
-	}
-	else
-	{
-		// 没有分频
-		*APB1_clock = *AHB_clock;
-	}
-
-	// 3.3 APB2
-	// 3.3.1 获取分频系数
-	uint32_t ppre2 = (RCC->CFGR & RCC_CFGR_PPRE2) >> 11;
-	// 3.3.2 检查最高位来判断是否有分频
-	if (ppre2 & 0x4)
-	{
-		*APB2_clock = *AHB_clock >> ((ppre2 & 0x3) + 1);
-	}
-	else
-	{
-		// 没有分频
-		*APB2_clock = *AHB_clock;
-	}
+    if (huart->Instance == USART1)
+    {
+        printf("%c", ch);
+        HAL_UART_Receive_IT(&huart1, &ch, 1);
+    }
 }
 ```
 
-**文件:`stm32/19_lowpower_standby_register/User/main.c`**(待机模式,通过 PA0 WKUP 引脚唤醒)
+### 待机模式(Standby)
+
+**项目 `19_lowpower_standby_register`(寄存器版)**
+
+**文件:`stm32/19_lowpower_standby_register/User/main.c`**
 
 ```c
 #include "usart.h"
@@ -372,61 +413,109 @@ void enter_standby_mode(void);
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	LED_Init();
-	KEY_Init();
-
-	// 开启PWR模块时钟
-	RCC->APB1ENR |= RCC_APB1ENR_PWREN;
-
-	// 判断标志位来判断是否从待机模式唤醒
-	if (PWR->CSR & PWR_CSR_SBF)
-	{
-		printf("从待机模式唤醒:\n");
-		PWR->CR |= PWR_CR_CSBF;
-	}
-	if (PWR->CSR & PWR_CSR_WUF)
-	{
-		printf("产生了唤醒事件:\n");
-		PWR->CR |= PWR_CR_CWUF;
-	}
-
-	printf("尚硅谷低功耗实验:待机模式...\n");
-
-	// 1. 点亮LED灯,延时2s,模拟正常运行程序
-	LED_On(LED_2);
-	Delay_s(2);
-
-	while (1)
-	{
-		// 2. 进入待机模式
-		printf("正常运行执行完毕,3s后进入待机模式...\n");
-		Delay_s(3);
-		printf("进入待机模式,按下KEY唤醒...\n");
-		Delay_ms(1);	// 稍作延时确保串口信息成功发送
-		enter_standby_mode();
-
-		// 4. 以下代码不会执行
-		printf("从待机模式唤醒...\n");
-		Delay_s(2);
-	}
+    USART_Init();
+    LED_Init();
+    KEY_Init();
+
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
+
+    if (PWR->CSR & PWR_CSR_SBF)
+    {
+        printf("从待机模式唤醒:\n");
+        PWR->CR |= PWR_CR_CSBF;
+    }
+    if (PWR->CSR & PWR_CSR_WUF)
+    {
+        printf("产生了唤醒事件:\n");
+        PWR->CR |= PWR_CR_CWUF;
+    }
+
+    printf("尚硅谷低功耗实验:待机模式...\n");
+
+    LED_On(LED_2);
+    Delay_s(2);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入待机模式...\n");
+        Delay_s(3);
+        printf("进入待机模式,按下KEY唤醒...\n");
+        Delay_ms(1);
+        enter_standby_mode();
+
+        printf("从待机模式唤醒...\n");
+        Delay_s(2);
+    }
 }
 
-// 进入待机模式的函数
 void enter_standby_mode(void)
 {
-	// 1. 设置深度睡眠模式
-	SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    PWR->CR |= PWR_CR_PDDS;           // 待机模式
+    PWR->CSR |= PWR_CSR_EWUP;         // 使能 PA0 唤醒
+    __WFI();
+}
+```
+
+**项目 `20_lowpower_standby_hal`(HAL 库版)**
+
+**文件:`stm32/20_lowpower_standby_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
+
+uint8_t ch;
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
 
-	// 2. 设置PDDS = 1,表示待机模式
-	PWR->CR |= PWR_CR_PDDS;
+    // 检测唤醒标志
+    if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB))
+    {
+        printf("从待机模式唤醒:\n");
+        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
+    }
+    if (__HAL_PWR_GET_FLAG(PWR_FLAG_WU))
+    {
+        printf("产生了唤醒事件:\n");
+        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
+    }
+
+    printf("尚硅谷低功耗实验:待机模式...\n");
+
+    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
+    HAL_Delay(2000);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入待机模式...\n");
+        HAL_Delay(3000);
+        printf("进入待机模式,按下KEY唤醒...\n");
 
-	// 3. 使能 PA0 的唤醒功能
-	PWR->CSR |= PWR_CSR_EWUP;
+        // 使能 PA0 唤醒引脚
+        HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1);
 
-	// 4. 使用WFI指令,进入待机模式
-	__WFI();
+        // 进入待机模式(唤醒后相当于复位)
+        HAL_PWR_EnterSTANDBYMode();
+
+        printf("从待机模式唤醒...\n");
+        HAL_Delay(2000);
+    }
+}
+
+void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
+{
+    if (huart->Instance == USART1)
+    {
+        printf("%c", ch);
+        HAL_UART_Receive_IT(&huart1, &ch, 1);
+    }
 }
 ```
 
@@ -436,12 +525,14 @@ void enter_standby_mode(void)
 
 | 操作 | 寄存器操作 | 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()` |
+| 进入睡眠 | `SCB->SCR &= ~SLEEPDEEP;` + `__WFI()` | `HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI)` |
+| 进入停止 | `SLEEPDEEP=1` + `PDDS=0` + `LPDS=1` + `__WFI()` | `HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_SLEEPENTRY_WFI)` |
+| 进入待机 | `SLEEPDEEP=1` + `PDDS=1` + `EWUP=1` + `__WFI()` | `HAL_PWR_EnterSTANDBYMode()` |
+| 使能 PA0 唤醒 | `PWR->CSR |= PWR_CSR_EWUP` | `HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1)` |
+| 配置 PVD | `PWR->CR` 配置 PLS + PVDE + EXTI16 | `HAL_PWR_ConfigPVD()` |
+| 清除唤醒标志 | `PWR->CR |= PWR_CR_CSBF/CWUF` | `__HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB/WU)` |
+| 检测待机唤醒 | `PWR->CSR & PWR_CSR_SBF` | `__HAL_PWR_GET_FLAG(PWR_FLAG_SB)` |
+| 暂停/恢复 Systick | — | `HAL_SuspendTick()` / `HAL_ResumeTick()` |
 
 ## 常见问题与避坑
 
@@ -450,3 +541,5 @@ void enter_standby_mode(void)
 3. **待机后 GPIO 输出乱** → 待机模式 GPIO 复位为默认浮空输入,恢复后需重新初始化
 4. **待机模式功耗还是很高** → 检查 VBAT 引脚是否接 VDD、WKUP 引脚是否有漏电路径
 5. **多次进出停止模式后异常** → 检查唤醒标志是否清除(PWR.CSR.WUF)
+6. **HAL 睡眠必须暂停 Systick** → `HAL_SuspendTick()` / `HAL_ResumeTick()` 配对使用,否则 SysTick 中断会立即唤醒
+7. **停止模式唤醒后时钟异常** → 唤醒后 HSE/PLL 已关闭,需调用 `SystemInit()` 或 `HAL_RCC_ClockConfig()` 重新配置

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — BKP/RTC章节 + 配套代码21~26"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -45,7 +45,9 @@ VBAT(电池) ────┘
 
 BKP(Backup)寄存器共 **42 个 16 位寄存器**(总容量 84 字节),可在系统复位/待机/主电源断电时保持内容。
 
-### 数据存储(写入 BKP 寄存器)
+### 实验 1:寄存器版 BKP
+
+**项目路径**:`stm32/21_bkp_test_register`
 
 **文件:`stm32/21_bkp_test_register/Hardware/BKP/bkp.h`**
 
@@ -55,7 +57,6 @@ BKP(Backup)寄存器共 **42 个 16 位寄存器**(总容量 84 字节)
 
 #include "stm32f10x.h"
 
-// 初始化
 void BKP_Init(void);
 
 #endif
@@ -66,23 +67,11 @@ void BKP_Init(void);
 ```c
 #include "bkp.h"
 
-// 初始化
 void BKP_Init(void)
 {
-    // 1. 开启PWR时钟
-    RCC->APB1ENR |= RCC_APB1ENR_PWREN;
-
-    // 2. 解锁备份域写保护
-    PWR->CR |= PWR_CR_DBP;
-
-    // 3. 开启BKP时钟
-    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
-
-    // // 4. 复位备份域寄存器
-    // RCC->BDCR |= RCC_BDCR_BDRST;
-
-    // // 5. 结束备份域复位
-    // RCC->BDCR &= ~RCC_BDCR_BDRST;
+    RCC->APB1ENR |= RCC_APB1ENR_PWREN;    // 开启 PWR 时钟
+    PWR->CR |= PWR_CR_DBP;                 // 解锁备份域写保护
+    RCC->APB1ENR |= RCC_APB1ENR_BKPEN;    // 开启 BKP 时钟
 }
 ```
 
@@ -96,19 +85,56 @@ void BKP_Init(void)
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	KEY_Init();
-	BKP_Init();
+    USART_Init();
+    KEY_Init();
+    BKP_Init();
+
+    printf("尚硅谷备份寄存器实验...\n");
+
+    // BKP->DR1 = 9999;   // 向备份寄存器写入数据
+
+    while (1) { }
+}
+```
+
+### 实验 2:HAL 库版 BKP
+
+**项目路径**:`stm32/22_bkp_test_hal`
+
+**文件:`stm32/22_bkp_test_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "rtc.h"
+#include "usart.h"
+#include "gpio.h"
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_RTC_Init();
+    MX_USART1_UART_Init();
 
-	printf("尚硅谷备份寄存器实验...\n");
+    printf("尚硅谷备份寄存器实验...\n");
 
-	// 向备份数据寄存器写入数据值
-	// BKP->DR1 = 9999;
+    // HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 6666);   // HAL 库写 BKP
 
-	while (1)
-	{
-	}
+    while (1) { }
+}
+
+// 按键中断读取 BKP 数据
+void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
+{
+    if (GPIO_Pin == KEY_Pin)
+    {
+        HAL_Delay(100);
+        if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_SET)
+        {
+            printf("DR1 = %d\n", HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1));
+        }
+    }
 }
 ```
 
@@ -117,8 +143,7 @@ int main(void)
 BKP 支持防侵入检测——当 TAMPER 引脚被触发时,自动清除所有 BKP 寄存器内容(防止篡改)。
 
 ```c
-// 配置 TAMPER 引脚(PC13)为防侵入模式
-BKP->CR |= BKP_CR_TPE;        // TPE=1: 使能防侵入
+BKP->CR |= BKP_CR_TPE;    // TPE=1: 使能防侵入
 // 当 PC13 检测到上升沿时 → 自动清除 BKP 寄存器 → 产生中断
 ```
 
@@ -138,8 +163,6 @@ RTC(Real-Time Clock)提供精确的**日历/时钟功能**,具有闹钟中
 
 ### RTC 时钟源选择
 
-RTC 有三个时钟源可选:
-
 | 时钟源 | 频率 | 精度 | 特点 |
 |--------|------|------|------|
 | **LSE** | **32.768KHz** | **高** | **推荐**,需外部晶振 |
@@ -154,7 +177,6 @@ RTC 预分频器由 20 位组成(7 位异步 + 13 位同步),**STM32F1 的
 
 ```c
 // LSE = 32.768KHz 时,PRL = 32767 → 1Hz
-// 配置流程(封装在 RTC_Init 中):
 RTC->PRLH = 0;
 RTC->PRLL = 0x7fff;        // 32767
 ```
@@ -172,7 +194,13 @@ RTC->PRLL = 0x7fff;        // 32767
 
 > **注意**:RTC 寄存器通过 **APB1 接口**访问,但 APB1 频率可能低于 RTC 频率,因此读取前需等待 RSF 标志同步。
 
-### RTC 初始化与配置
+---
+
+## RTC 实验
+
+### 实验 3:寄存器版 RTC 日历
+
+**项目路径**:`stm32/25_rtc_calendar_register`
 
 **文件:`stm32/25_rtc_calendar_register/Hardware/RTC/rtc.h`**
 
@@ -183,7 +211,6 @@ RTC->PRLL = 0x7fff;        // 32767
 #include "stm32f10x.h"
 #include <time.h>
 
-// 自定义日期时间结构体类型
 typedef struct
 {
     uint16_t year;
@@ -194,317 +221,240 @@ typedef struct
     uint8_t second;
 } DateTime;
 
-// 初始化
 void RTC_Init(void);
-
-// 设置闹钟(s秒之后报警)
 void RTC_SetAlarm(uint32_t s);
-
-// 设置当前时间(UNIX时间戳)
 void RTC_SetTimestamp(uint32_t ts);
-
-// 获取日期时间(年月日时分秒)
 void RTC_GetDateTime(DateTime * dateTime);
 
 #endif
 ```
 
-**文件:`stm32/25_rtc_calendar_register/Hardware/RTC/rtc.c`**(RTC 初始化 + 日历功能)
+**文件:`stm32/25_rtc_calendar_register/Hardware/RTC/rtc.c`**
 
 ```c
 #include "rtc.h"
 
-// 初始化
 void RTC_Init(void)
 {
-    // 1. 开启PWR时钟
+    // 1. 开启 PWR 时钟 + 解锁备份域
     RCC->APB1ENR |= RCC_APB1ENR_PWREN;
-
-    // 1.2 解锁备份域写保护
     PWR->CR |= PWR_CR_DBP;
 
-    // // 1.3 复位备份域寄存器
-    // RCC->BDCR |= RCC_BDCR_BDRST;
-    // // 1.4 结束备份域复位
-    // RCC->BDCR &= ~RCC_BDCR_BDRST;
-
-    // 2. 开启RTC时钟源以及使能RTC
-    // 2.1 使能RTC时钟
+    // 2. 使能 LSE → 等待就绪 → 选择 RTC 时钟源
     RCC->BDCR |= RCC_BDCR_RTCEN;
-
-    // 2.2 使能LSE并等待其就绪
     RCC->BDCR |= RCC_BDCR_LSEON;
-    while (!(RCC->BDCR & RCC_BDCR_LSERDY))
-    {
-    }
-
-    // 2.3 选择LSE作为RTC的时钟源
+    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
     RCC->BDCR &= ~RCC_BDCR_RTCSEL;
     RCC->BDCR |= RCC_BDCR_RTCSEL_0;
 
-    // 3. RTC寄存器配置操作
-    // 3.1 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
-
-    // 3.2 进入配置模式
+    // 3. 等待 RTOFF → 进入配置模式 → 设 PRL=32767 → 退出配置
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
     RTC->CRL |= RTC_CRL_CNF;
-
-    // 3.3 设置预分频系数 32767,得到1秒计数
     RTC->PRLH = 0;
     RTC->PRLL = 0x7fff;
-
-    // 3.4 退出配置模式
     RTC->CRL &= ~RTC_CRL_CNF;
-
-    // 3.5 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
 }
 
-// 设置闹钟(s秒之后报警)
 void RTC_SetAlarm(uint32_t s)
 {
-    // 0. 清除闹钟标志
     RTC->CRL &= ~RTC_CRL_ALRF;
-
-    // 1. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
-
-    // 2. 进入配置模式
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
     RTC->CRL |= RTC_CRL_CNF;
-
-    // 3. 设置寄存器
-    // 3.1 设置计数器 CNT = 0
     RTC->CNTH = 0;
     RTC->CNTL = 0;
-
-    // 3.2 设置 ALR = s - 1
     s -= 1;
     RTC->ALRH = (s >> 16) & 0xffff;
     RTC->ALRL = (s >> 0) & 0xffff;
-
-    // 4. 退出配置模式
     RTC->CRL &= ~RTC_CRL_CNF;
-
-    // 5. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
 }
 
-// 设置当前时间(UNIX时间戳)
 void RTC_SetTimestamp(uint32_t ts)
 {
-    // 1. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
-
-    // 2. 进入配置模式
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
     RTC->CRL |= RTC_CRL_CNF;
-
-    // 3. 设置CNT寄存器
     RTC->CNTH = (ts >> 16) & 0xffff;
     RTC->CNTL = (ts >> 0) & 0xffff;
-
-    // 4. 退出配置模式
     RTC->CRL &= ~RTC_CRL_CNF;
-
-    // 5. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {
-    }
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
 }
 
-// 获取日期时间(年月日时分秒)
 void RTC_GetDateTime(DateTime *dateTime)
 {
-    // 1. 等待寄存器同步
-    while ( !(RTC->CRL & RTC_CRL_RSF) )
-    {}
+    while (!(RTC->CRL & RTC_CRL_RSF)) { }
 
-    // 2. 获取当前秒数并组合
     uint32_t second = RTC->CNTH << 16 | RTC->CNTL;
-
-    // 3. 将时间戳转换为tm结构体变量
     struct tm* ptm = localtime(&second);
 
-    // 4. 将tm数据赋给自定义的结构体变量
-    dateTime->year = ptm->tm_year + 1900;
-    dateTime->month = ptm->tm_mon + 1;
-    dateTime->day = ptm->tm_mday;
-    dateTime->hour = ptm->tm_hour;
+    dateTime->year   = ptm->tm_year + 1900;
+    dateTime->month  = ptm->tm_mon + 1;
+    dateTime->day    = ptm->tm_mday;
+    dateTime->hour   = ptm->tm_hour;
     dateTime->minute = ptm->tm_min;
     dateTime->second = ptm->tm_sec;
 }
 ```
 
----
-
-## 实验:BKP 数据存储 + RTC 日历
-
-### BKP 实验
-
-**项目路径**:`stm32/21_bkp_test_register`
-
-**文件:`stm32/21_bkp_test_register/User/main.c`**(向 BKP 寄存器写入数据)
+**文件:`stm32/25_rtc_calendar_register/User/main.c`**
 
 ```c
 #include "usart.h"
 #include "delay.h"
-#include "key.h"
-#include "bkp.h"
+#include "rtc.h"
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	KEY_Init();
-	BKP_Init();
+    USART_Init();
+    RTC_Init();
+
+    printf("尚硅谷RTC实验:RTC实时时钟...\n");
 
-	printf("尚硅谷备份寄存器实验...\n");
+    // RTC_SetTimestamp(1736160789);   // 设置一次初始时间戳
 
-	// 向备份数据寄存器写入数据值
-	// BKP->DR1 = 9999;
+    DateTime dateTime;
 
-	while (1)
-	{
-	}
+    while (1)
+    {
+        RTC_GetDateTime(&dateTime);
+        printf("%04d年%02d月%02d日 %02d:%02d:%02d\n",
+            dateTime.year, dateTime.month, dateTime.day,
+            dateTime.hour, dateTime.minute, dateTime.second);
+        Delay_ms(1000);
+    }
 }
 ```
 
-### RTC 日历实验
+### 实验 4:HAL 库版 RTC 日历
 
-**项目路径**:`stm32/25_rtc_calendar_register`
+**项目路径**:`stm32/26_rtc_calendar_hal`
 
-**文件:`stm32/25_rtc_calendar_register/User/main.c`**
+**文件:`stm32/26_rtc_calendar_hal/Core/Src/main.c`**
 
 ```c
-#include "usart.h"
-#include "delay.h"
+#include "main.h"
 #include "rtc.h"
+#include "usart.h"
+#include "gpio.h"
+
+void read_stored_date(RTC_HandleTypeDef * hrtc);
+void write_stored_date(RTC_HandleTypeDef * hrtc);
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	RTC_Init();
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_RTC_Init();
+    MX_USART1_UART_Init();
 
-	printf("尚硅谷RTC实验:RTC实时时钟...\n");
+    printf("尚硅谷RTC实验:RTC实时时钟...\n");
 
-	// 设定一次当前的时间戳
-	// RTC_SetTimestamp(1736160789);
+    RTC_DateTypeDef date;
+    RTC_TimeTypeDef time;
 
-	DateTime dateTime;
+    while (1)
+    {
+        // 从 BKP 寄存器恢复保存的日期
+        read_stored_date(&hrtc);
+
+        HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
+        HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
 
-	while (1)
-	{
-		// 每隔1s获取当前时间并打印一次
-		RTC_GetDateTime(&dateTime);
+        // 将当前日期写入 BKP 寄存器
+        write_stored_date(&hrtc);
 
-		printf("%04d年%02d月%02d日 %02d:%02d:%02d\n",
-			dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute, dateTime.second);
+        printf("20%02d年%02d月%02d日 %02d:%02d:%02d\n",
+            date.Year, date.Month, date.Date,
+            time.Hours, time.Minutes, time.Seconds);
 
-		Delay_ms(1000);
-	}
+        HAL_Delay(1000);
+    }
+}
+
+void read_stored_date(RTC_HandleTypeDef * hrtc)
+{
+    if (HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1) == 0)
+        return;
+
+    hrtc->DateToUpdate.Year    = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR2);
+    hrtc->DateToUpdate.Month   = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR3);
+    hrtc->DateToUpdate.Date    = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR4);
+    hrtc->DateToUpdate.WeekDay = HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR5);
+}
+
+void write_stored_date(RTC_HandleTypeDef * hrtc)
+{
+    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, 1);
+    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR2, hrtc->DateToUpdate.Year);
+    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR3, hrtc->DateToUpdate.Month);
+    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR4, hrtc->DateToUpdate.Date);
+    HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR5, hrtc->DateToUpdate.WeekDay);
 }
 ```
 
-### RTC 闹钟唤醒待机实验
+### 实验 5:寄存器版 RTC 闹钟唤醒待机
 
 **项目路径**:`stm32/23_rtc_alarm_standby_register`
 
-**文件:`stm32/23_rtc_alarm_standby_register/Hardware/RTC/rtc.c`**(闹钟功能)
+**文件:`stm32/23_rtc_alarm_standby_register/Hardware/RTC/rtc.h`**
+
+```c
+#ifndef __RTC_H
+#define __RTC_H
+
+#include "stm32f10x.h"
+
+void RTC_Init(void);
+void RTC_SetAlarm(uint32_t s);
+
+#endif
+```
+
+**文件:`stm32/23_rtc_alarm_standby_register/Hardware/RTC/rtc.c`**(初始化和闹钟设置)
 
 ```c
 #include "rtc.h"
 
-// 初始化
 void RTC_Init(void)
 {
-    // 1. 开启PWR时钟
     RCC->APB1ENR |= RCC_APB1ENR_PWREN;
-
-    // 1.2 解锁备份域写保护
     PWR->CR |= PWR_CR_DBP;
 
-    // 1.3 复位备份域寄存器
+    // 复位备份域寄存器(与日历版不同:此处主动复位)
     RCC->BDCR |= RCC_BDCR_BDRST;
-
-    // 1.4 结束备份域复位
     RCC->BDCR &= ~RCC_BDCR_BDRST;
 
-    // 2. 开启RTC时钟源以及使能RTC
-    // 2.1 使能RTC时钟
     RCC->BDCR |= RCC_BDCR_RTCEN;
-
-    // 2.2 使能LSE并等待其就绪
     RCC->BDCR |= RCC_BDCR_LSEON;
-    while (!(RCC->BDCR & RCC_BDCR_LSERDY))
-    {}
-
-    // 2.3 选择LSE作为RTC的时钟源
+    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) { }
     RCC->BDCR &= ~RCC_BDCR_RTCSEL;
     RCC->BDCR |= RCC_BDCR_RTCSEL_0;
 
-    // 3. RTC寄存器配置操作
-    // 3.1 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {}
-
-    // 3.2 进入配置模式
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
     RTC->CRL |= RTC_CRL_CNF;
-
-    // 3.3 设置预分频系数 32767,得到1秒计数
     RTC->PRLH = 0;
     RTC->PRLL = 0x7fff;
-
-    // 3.4 退出配置模式
     RTC->CRL &= ~RTC_CRL_CNF;
-
-    // 3.5 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {}
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
 }
 
-// 设置闹钟(s秒之后报警)
 void RTC_SetAlarm(uint32_t s)
 {
-    // 0. 清除闹钟标志
     RTC->CRL &= ~RTC_CRL_ALRF;
-
-    // 1. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {}
-
-    // 2. 进入配置模式
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
     RTC->CRL |= RTC_CRL_CNF;
-
-    // 3. 设置寄存器
-    // 3.1 设置计数器 CNT = 0
     RTC->CNTH = 0;
     RTC->CNTL = 0;
-
-    // 3.2 设置 ALR = s - 1
     s -= 1;
     RTC->ALRH = (s >> 16) & 0xffff;
     RTC->ALRL = (s >> 0) & 0xffff;
-
-    // 4. 退出配置模式
     RTC->CRL &= ~RTC_CRL_CNF;
-
-    // 5. 查询RTOFF位直到为1
-    while (!(RTC->CRL & RTC_CRL_RTOFF))
-    {}
+    while (!(RTC->CRL & RTC_CRL_RTOFF)) { }
 }
 ```
 
-**文件:`stm32/23_rtc_alarm_standby_register/User/main.c`**(闹钟唤醒待机)
+**文件:`stm32/23_rtc_alarm_standby_register/User/main.c`**
 
 ```c
 #include "usart.h"
@@ -516,101 +466,133 @@ void enter_standby_mode(void);
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	LED_Init();
-	RTC_Init();
-
-	// 判断标志位来判断是否从待机唤醒
-	if (PWR->CSR & PWR_CSR_SBF)
-	{
-		printf("从待机模式唤醒:\n");
-		PWR->CR |= PWR_CR_CSBF;
-	}
-	if (PWR->CSR & PWR_CSR_WUF)
-	{
-		printf("产生了唤醒事件:\n");
-		PWR->CR |= PWR_CR_CWUF;
-	}
-
-	printf("尚硅谷RTC实验:闹钟唤醒待机模式...\n");
-
-	// 1. 点亮LED灯,延时2s,模拟正常运行程序
-	LED_On(LED_2);
-	Delay_s(2);
-
-	while (1)
-	{
-		// 2. 进入待机模式
-		printf("正常运行执行完毕,3s后进入待机模式...\n");
-		Delay_s(3);
-		printf("进入待机模式,5s后闹钟唤醒...\n");
-		Delay_ms(1);	// 稍作延时确保串口信息成功发送
-
-		// 3. 在进入待机模式前设置闹钟
-		RTC_SetAlarm(5);
-		enter_standby_mode();
-
-		// 4. 以下代码不会执行
-		printf("从待机模式唤醒...\n");
-		Delay_s(2);
-	}
+    USART_Init();
+    LED_Init();
+    RTC_Init();
+
+    if (PWR->CSR & PWR_CSR_SBF)
+    {
+        printf("从待机模式唤醒:\n");
+        PWR->CR |= PWR_CR_CSBF;
+    }
+    if (PWR->CSR & PWR_CSR_WUF)
+    {
+        printf("产生了唤醒事件:\n");
+        PWR->CR |= PWR_CR_CWUF;
+    }
+
+    printf("尚硅谷RTC实验:闹钟唤醒待机模式...\n");
+
+    LED_On(LED_2);
+    Delay_s(2);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入待机模式...\n");
+        Delay_s(3);
+        printf("进入待机模式,5s后闹钟唤醒...\n");
+        Delay_ms(1);
+
+        RTC_SetAlarm(5);   // 设置 5 秒后闹钟
+        enter_standby_mode();
+
+        printf("从待机模式唤醒...\n");
+        Delay_s(2);
+    }
 }
 
-// 进入待机模式的函数
 void enter_standby_mode(void)
 {
-	// 1. 设置深度睡眠模式
-	SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    SCB->SCR |= SCB_SCR_SLEEPDEEP;
+    PWR->CR |= PWR_CR_PDDS;       // 待机模式
+    __WFI();
+}
+```
 
-	// 2. 设置PDDS = 1,表示待机模式
-	PWR->CR |= PWR_CR_PDDS;
+### 实验 6:HAL 库版 RTC 闹钟唤醒待机
 
-	// 3. 使用WFI指令,进入待机模式
-	__WFI();
+**项目路径**:`stm32/24_rtc_alarm_standby_hal`
+
+**文件:`stm32/24_rtc_alarm_standby_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "usart.h"
+#include "gpio.h"
+#include "rtc.h"
+
+uint8_t ch;
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_USART1_UART_Init();
+    MX_RTC_Init();
+
+    if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB))
+    {
+        printf("从待机模式唤醒:\n");
+        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);
+    }
+    if (__HAL_PWR_GET_FLAG(PWR_FLAG_WU))
+    {
+        printf("产生了唤醒事件:\n");
+        __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);
+    }
+
+    printf("尚硅谷低功耗实验:待机模式...\n");
+
+    HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
+    HAL_Delay(2000);
+
+    while (1)
+    {
+        printf("正常运行执行完毕,3s后进入待机模式...\n");
+        HAL_Delay(3000);
+        printf("进入待机模式,等待5s后RTC闹钟唤醒...\n");
+
+        // 使用自定义 RTC 闹钟函数(与寄存器版共用逻辑)
+        RTC_SetAlarm(5);
+
+        HAL_PWR_EnterSTANDBYMode();
+
+        printf("从待机模式唤醒...\n");
+        HAL_Delay(2000);
+    }
 }
 ```
 
 ---
 
-## HAL 库版 RTC/BKP
-
-HAL 库使用 `RTC_HandleTypeDef` 管理 RTC,提供 `SetTime/GetTime/SetAlarm` 等高级 API。
+## HAL 库版 RTC/BKP 速查
 
 ```c
 // CubeMX 生成 RTC 配置:LSE 时钟源,启用日历
 RTC_HandleTypeDef hrtc;
 
-// HAL 库设置时间(使用 RTC_TimeTypeDef / RTC_DateTypeDef 结构体)
 RTC_TimeTypeDef sTime = {0};
 RTC_DateTypeDef sDate = {0};
 
 sTime.Hours = 12;
 sTime.Minutes = 0;
 sTime.Seconds = 0;
-sTime.TimeFormat = RTC_HOURFORMAT_24;  // 24 小时制
+sTime.TimeFormat = RTC_HOURFORMAT_24;
 sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
 sTime.StoreOperation = RTC_STOREOPERATION_RESET;
-HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);   // 设置时间
+HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
 
-sDate.Year = 26;    // 2026 年
+sDate.Year = 26;
 sDate.Month = 7;
 sDate.Date = 15;
 sDate.WeekDay = RTC_WEEKDAY_WEDNESDAY;
-HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);   // 设置日期
+HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
 
-// HAL 库读取时间
-HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);   // 读时间(必须先读时间再读日期!)
-HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);   // 读日期
+HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);    // 必须先读时间再读日期!
+HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
 
-printf("%04d-%02d-%02d %02d:%02d:%02d\r\n",
-       2000 + sDate.Year, sDate.Month, sDate.Date,
-       sTime.Hours, sTime.Minutes, sTime.Seconds);
-
-// HAL 库读 BKP 寄存器
 uint16_t data = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);
-
-// HAL 库写 BKP 寄存器
 HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x55AA);
 ```
 
@@ -623,6 +605,7 @@ HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x55AA);
 | 读 BKP 寄存器 | `data = BKP->DR[i]` | `HAL_RTCEx_BKUPRead()` |
 | 设置时间 | `RTC_SetTimestamp(UNIX时间戳)` | `HAL_RTC_SetTime()` + `SetDate()` |
 | 读取时间 | `RTC_GetDateTime(&dt)` → DateTime 结构体 | `HAL_RTC_GetTime()` + `GetDate()` |
+| 设置闹钟 | `RTC_SetAlarm(s)` → CNT=0, ALR=s-1 | 自定义 RTC_SetAlarm() |
 | 配置 LSE | `RCC->BDCR` 逐位操作 | CubeMX 自动配置 |
 | 写注意事项 | 需 CNF 模式 + 等待 RTOFF | HAL 库自动处理 |
 
@@ -633,4 +616,5 @@ HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, 0x55AA);
 3. **RTC 读值不对** → 读取前等待 RSF 同步(APB1 比 RTC 慢时)
 4. **VDD 断电 RTC 停走** → 检查 VBAT 引脚是否接电池或 VDD
 5. **写 RTC 后系统挂死** → RTC 写入需等待 RTOFF=1(未完成前不可再次写入)
-6. **Unix 时间戳时区问题** → localtime() 使用系统时区,跨时区应用用 gmtime()
+6. **HAL 读 RTC 顺序** → 必须先读 `GetTime()` 再读 `GetDate()`,否则日期可能不刷新
+7. **闹钟唤醒待机无需使能 EWUP** → RTC 闹钟可以直接从待机模式唤醒,不需要配置 PA0 WKUP

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

@@ -3,7 +3,7 @@ tags: [source-summary]
 type: source
 source: "尚硅谷嵌入式技术之STM32单片机(扩展篇)V1.0.0 — 看门狗章节 + 配套代码27~28"
 author: "尚硅谷研究院"
-date: 2026-07-15
+date: 2026-07-16
 created: 2026-07-15
 ---
 
@@ -28,7 +28,7 @@ IWDG(Independent Watchdog)使用**独立的 LSI 时钟(~40KHz)**,与
 ```
 LSI(~40KHz) → 8位预分频器 → 12位递减计数器(RL[11:0]) → 复位
-                              键寄存器(KRW)写 0xAAAA 重装
+                              键寄存器(KR)写 0xAAAA 重装
 ```
 
 ### IWDG 寄存器
@@ -48,23 +48,25 @@ 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
+| PR[2:0] | 分频系数 | RLR=4095 超时 | 分辨率 |
+|---------|---------|----------------|--------|
+| 000 | 4 | 0.41s | 0.1ms |
+| 001 | 8 | 0.82s | 0.2ms |
+| 010 | 16 | 1.64s | 0.4ms |
+| 011 | 32 | 3.28s | 0.8ms |
+| 100 | 64 | 6.55s | 1.6ms |
+| 101 | 128 | 13.11s | 3.2ms |
+| 110 | 256 | 26.21s | 6.4ms |
+| 111 | 256 | 26.21s | 6.4ms |
+
+**实验配置**:PR=4(分频64), RLR=2499
 ```
-Tout = 1001 × 32 × 4 / 40000 = 3.2s
+Tout = 2500 × 64 × 4 / 40000 = 16s
 ```
 
-### IWDG 配置示例
+### 实验 1:寄存器版 IWDG
+
+**项目路径**:`stm32/27_iwdg_test_register`
 
 **文件:`stm32/27_iwdg_test_register/Hardware/IWDG/iwdg.h`**
 
@@ -74,43 +76,29 @@ Tout = 1001 × 32 × 4 / 40000 = 3.2s
 
 #include "stm32f10x.h"
 
-// 初始化
 void IWDG_Init(void);
-
-// 喂狗:刷新计数器值
-void IWDG_Refresh(void);
+void IWDG_Refresh(void);     // 喂狗
 
 #endif
 ```
 
-**文件:`stm32/27_iwdg_test_register/Hardware/IWDG/iwdg.c`**(实际代码:先 KR=0xCCCC 启动,再 0x5555 配置)
+**文件:`stm32/27_iwdg_test_register/Hardware/IWDG/iwdg.c`**(实际代码:先启动,再配置,最后喂狗
 
 ```c
 #include "iwdg.h"
 
-// 初始化
 void IWDG_Init(void)
 {
-    // 1. 启动看门狗
-    IWDG->KR = 0xCCCC;
-
-    // 2. 解除寄存器保护
-    IWDG->KR = 0x5555;
-
-    // 3. 设置预分频系数 64 - PR = 100
-    IWDG->PR = 4;
-
-    // 4. 设置重装载值:2499
-    IWDG->RLR = 2499;
-
-    // 5. 重新加载计数值:喂一次狗
-    IWDG_Refresh();
+    IWDG->KR = 0xCCCC;       // 启动看门狗
+    IWDG->KR = 0x5555;       // 解除寄存器保护
+    IWDG->PR = 4;            // 预分频 64
+    IWDG->RLR = 2499;        // 重装载值 2499 → 超时约 16s
+    IWDG_Refresh();           // 重新加载计数值
 }
 
-// 喂狗:刷新计数器值
 void IWDG_Refresh(void)
 {
-    IWDG->KR = 0xAAAA;
+    IWDG->KR = 0xAAAA;       // 喂狗
 }
 ```
 
@@ -122,39 +110,85 @@ void IWDG_Refresh(void)
 #include "key.h"
 #include "iwdg.h"
 
-// 全局变量:按键标志位
 uint8_t flag;
 
 int main(void)
 {
-	// 初始化
-	USART_Init();
-	KEY_Init();
-	IWDG_Init();
-
-	printf("尚硅谷独立看门狗实验...\n");
-
-	while (1)
-	{
-		// 1. 模拟正常运行程序
-		printf("正常运行程序...\n");
-		Delay_ms(3000);
-
-		// 按下按键中断,在中断服务中,增加程序的执行时间
-		if (flag)
-		{
-			Delay_ms(3000);
-			flag = 0;
-		}
-
-		// 2. 正常程序运行结束后喂狗
-		IWDG_Refresh();
-		printf("程序运行结束,喂狗成功!\n");
-	}
+    USART_Init();
+    KEY_Init();
+    IWDG_Init();
+
+    printf("尚硅谷独立看门狗实验...\n");
+
+    while (1)
+    {
+        printf("正常执行程序...\n");
+        Delay_ms(3000);         // 模拟主循环 3s 耗时
+
+        if (flag)               // 按键中断中设置的标志位
+        {
+            Delay_ms(3000);     // 模拟额外耗时 3s(累计 6s,超时 16s 以内,不会复位)
+            flag = 0;
+        }
+
+        IWDG_Refresh();         // 喂狗
+        printf("程序执行结束,喂狗成功!\n");
+    }
 }
 ```
 
-> **注意**:实际代码先写 `KR=0xCCCC` **启动**看门狗,再写 `KR=0x5555` 解除保护配置 PR/RLR,最后 `IWDG_Refresh()` 喂狗。IWDG 一旦启动**无法停止**,只能通过系统复位关闭。
+### 实验 2:HAL 库版 IWDG
+
+**项目路径**:`stm32/28_iwdg_test_hal`
+
+**文件:`stm32/28_iwdg_test_hal/Core/Src/main.c`**
+
+```c
+#include "main.h"
+#include "iwdg.h"
+#include "usart.h"
+#include "gpio.h"
+
+uint8_t flag;
+
+int main(void)
+{
+    HAL_Init();
+    SystemClock_Config();
+    MX_GPIO_Init();
+    MX_IWDG_Init();          // CubeMX 生成的 IWDG 初始化
+    MX_USART1_UART_Init();
+
+    printf("尚硅谷独立看门狗实验...\n");
+
+    while (1)
+    {
+        printf("正常执行程序...\n");
+        HAL_Delay(3000);
+
+        if (flag)
+        {
+            HAL_Delay(3000);
+            flag = 0;
+        }
+
+        HAL_IWDG_Refresh(&hiwdg);    // HAL 库喂狗
+        printf("程序执行结束,喂狗成功!\n");
+    }
+}
+
+void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
+{
+    flag = 1;
+    printf("按键按下...\n");
+}
+```
+
+HAL 库 SystemClock_Config 需使能 LSI:
+```c
+RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI | RCC_OSCILLATORTYPE_HSE;
+RCC_OscInitStruct.LSIState = RCC_LSI_ON;
+```
 
 ---
 
@@ -189,150 +223,37 @@ CNT:  127 ← ... ← [W+1] ← ... ← 0x40 ← 0x3F → 复位
 Tout = (CNT - W) × (PSC + 1) / APB1
 ```
 
-WWDG 的最小喂狗间隔和最大喂狗间隔由 PSC 预分频系数和窗口值决定。
-
-### WWDG 配置示例
+### 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);
+    WWDG->CFR |= WWDG_CFR_WDGTB_1 | WWDG_CFR_WDGTB_0;  // WDGTB=11: 分频 8
+    WWDG->CFR &= ~WWDG_CFR_W;
+    WWDG->CFR |= (0x50 << 0);                            // 窗口值 0x50
 
-    // 4. 设置计数器初始值 = 0x7F(最大值)
-    WWDG->CR = 0x7F;
-
-    // 5. 使能窗口看门狗(WDGA=1)
-    WWDG->CR |= WWDG_CR_WDGA;
+    WWDG->CR = 0x7F;                                     // 计数器初始值 127
+    WWDG->CR |= WWDG_CR_WDGA;                            // 使能 WWDG
 }
 
 void WWDG_Feed(void)
 {
-    // 必须在窗口内喂狗(CNT < W + 1)
-    WWDG->CR = 0x7F;      // 重装计数器(必须在主循环写入此值)
+    WWDG->CR = 0x7F;      // 重装计数器
 }
 
 void WWDG_IRQHandler(void)
 {
-    if (WWDG->SR & WWDG_SR_EWIF)         // 早期唤醒中断
+    if (WWDG->SR & WWDG_SR_EWIF)
     {
-        // 距离复位还有 0x40 个计数周期,赶紧处理紧急事务
-        // 注意:在 ISR 中不能喂狗!
-        WWDG->SR = 0x00;                  // 清标志
+        // 距离复位还有 0x40 个计数周期(约 284μs @ 36MHz/8)
+        WWDG->SR = 0x00;
     }
 }
 ```
 
----
-
-## IWDG vs WWDG 对比
-
-| 对比项 | IWDG | WWDG |
-|--------|------|------|
-| **时钟源** | LSI (~40KHz) 独立 | APB1(依赖主时钟) |
-| **计数器** | 12 位递减 | 7 位递减 |
-| **超时范围** | ~0.1ms ~ 26s | ~0.1ms ~ 若干 ms |
-| **喂狗窗口** | 无窗口(任意时间喂即可) | **有窗口:太早太晚都复位** |
-| **可停止** | ❌ 启动后不可停 | ✅ 可通过软件停止 |
-| **中断** | ❌ 无 | ✅ 有 EWI 早期唤醒中断 |
-| **独立供电** | ✅ 完全独立 | ❌ 依赖主电源 |
-| **选型建议** | 需要高可靠性、长时间定时的系统 | 需要精确检测程序执行时序的系统 |
-
----
-
----
-
-## HAL 库版看门狗
-
-### HAL IWDG
-
-**文件:`stm32/28_iwdg_test_hal/Core/Src/main.c`**(HAL 版看门狗,实际项目代码)
-
-```c
-/* Includes ------------------------------------------------------------------*/
-#include "main.h"
-#include "iwdg.h"
-#include "usart.h"
-#include "gpio.h"
-
-uint8_t flag;
-
-int main(void)
-{
-  HAL_Init();
-  SystemClock_Config();
-
-  MX_GPIO_Init();
-  MX_IWDG_Init();
-  MX_USART1_UART_Init();
-
-  printf("尚硅谷独立看门狗实验...\n");
-
-  while (1)
-  {
-    // 1. 模拟正常运行程序
-    printf("正常运行程序...\n");
-    HAL_Delay(3000);
-
-    // 按下按键中断,在中断服务中,增加程序的执行时间
-    if (flag)
-    {
-      HAL_Delay(3000);
-      flag = 0;
-    }
-
-    // 2. 正常程序运行结束后喂狗
-    HAL_IWDG_Refresh(&hiwdg);
-    printf("程序运行结束,喂狗成功!\n");
-  }
-}
-
-void SystemClock_Config(void)
-{
-  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
-  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
-
-  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI | RCC_OSCILLATORTYPE_HSE;
-  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
-  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
-  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
-  RCC_OscInitStruct.LSIState = RCC_LSI_ON;
-  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
-  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
-  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
-  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
-  {
-    Error_Handler();
-  }
-
-  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
-                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
-  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
-  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
-  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
-  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
-
-  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
-  {
-    Error_Handler();
-  }
-}
-
-void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
-{
-  flag = 1;
-  printf("按键按下...\n");
-}
-```
-
-### HAL WWDG
+### HAL 版 WWDG
 
 ```c
 WWDG_HandleTypeDef hwwdg;
@@ -341,26 +262,39 @@ hwwdg.Instance = WWDG;
 hwwdg.Init.Prescaler = WWDG_PRESCALER_8;     // WDGTB=11
 hwwdg.Init.Window = 0x50;                     // 窗口值
 hwwdg.Init.Counter = 0x7F;                    // 计数值初始值
-hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;         // 使能早期唤醒中断
+hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;
 HAL_WWDG_Init(&hwwdg);
 
-// HAL 库喂狗(就在主循环更新 CR 值)
-HAL_WWDG_Refresh(&hwwdg);                     // 相当于 CR=0x7F
+HAL_WWDG_Refresh(&hwwdg);                     // 喂狗
 
-// HAL WWDG 早期唤醒中断回调
 void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
 {
-    // 距离 WWDG 复位还剩 64 个计数周期,执行紧急备份
+    // 距离复位还剩 64 个计数周期
 }
 ```
 
+---
+
+## IWDG vs WWDG 对比
+
+| 对比项 | IWDG | WWDG |
+|--------|------|------|
+| **时钟源** | LSI (~40KHz) 独立 | APB1(依赖主时钟) |
+| **计数器** | 12 位递减 | 7 位递减 |
+| **超时范围** | ~0.1ms ~ 26s | ~0.1ms ~ 若干 ms |
+| **喂狗窗口** | 无窗口(任意时间喂即可) | **有窗口:太早太晚都复位** |
+| **可停止** | ❌ 启动后不可停 | ✅ 可通过软件停止 |
+| **中断** | ❌ 无 | ✅ 有 EWI 早期唤醒中断 |
+| **独立供电** | ✅ 完全独立 | ❌ 依赖主电源 |
+| **选型建议** | 需要高可靠性的系统 | 需要精确检测程序执行时序的系统 |
+
 ## 核心速查表
 
 | IWDG 操作 | 寄存器 | HAL 库 |
 |-----------|--------|--------|
 | 启动 | `KR = 0xCCCC` | `HAL_IWDG_Init()` |
+| 配置 PR/RLR | 先 `KR = 0x5555` | 通过 `Init` 结构体 |
 | 喂狗 | `KR = 0xAAAA` | `HAL_IWDG_Refresh()` |
-| 配置 | 先 `KR = 0x5555` | 通过 `Init` 结构体 |
 
 | WWDG 操作 | 寄存器 | HAL 库 |
 |-----------|--------|--------|
@@ -376,4 +310,4 @@ void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef *hwwdg)
 3. **WWDG 喂狗太早也复位** → 检查窗口值配置(W 值必须小于 CNT 初始值)
 4. **WWDG 应用场景**:适合检测程序的**执行时序是否正常**(太早或太晚说明程序逻辑异常)
 5. **LSI 频率不准** → LSI 约 30~60KHz(典型 40KHz),实际超时时间 ±50%,重要场合需实测
-6. **Keil 调试关闭 IWDG/WWDG**:在 Debug → Settings → Debug 选项卡中勾选 "Reset and Halt" 或配置 DBGMCU 寄存器
+6. **Keil 调试关闭 IWDG**:在 Debug → Settings → Debug 选项卡中配置 DBGMCU 寄存器