参考手册:RM0090,第12章
适用芯片:STM32F405xx / F407xx / F415xx / F417xx / F42xxx / F43xxx

目录

  1. NVIC — 嵌套向量中断控制器
  2. SysTick 校准值
  3. 中断向量表
  4. EXTI — 外部中断/事件控制器
  5. EXTI 寄存器详解
  6. 完整 C++ 示例代码

1. NVIC — 嵌套向量中断控制器

1.1 什么是 NVIC?

想象你是一个公司老板,同时有多个下属来向你汇报事情(中断)。你需要一套规则来决定:

  • 谁的事情更紧急(优先级)
  • 如果你正在处理 A 的事,B 的事能不能打断你(嵌套)
  • 哪些人的汇报你暂时不想听(屏蔽)
    NVIC 就是 CPU 的这套"秘书系统",负责管理所有中断请求。

1.2 NVIC 核心特性


特性 STM32F405/07/15/17xx STM32F42/43xxx
可屏蔽中断通道数 82 个 最多 91 个
优先级级别数 16 级(使用 4 位) 16 级(使用 4 位)
延迟特性 低延迟 低延迟

注意:上表中的中断数量不包含 Cortex-M4 内核自带的 16 条异常线。

1.3 优先级如何理解?

STM32 用 4 位来表示优先级,这 4 位又分成两部分:
4位优先级 = 抢占优先级(高位) ⏟ 决定能否打断别人 + 子优先级(低位) ⏟ 同级抢占时谁先执行 \text{4位优先级} = \underbrace{\text{抢占优先级(高位)}}_{\text{决定能否打断别人}} + \underbrace{\text{子优先级(低位)}}_{\text{同级抢占时谁先执行}} 4位优先级=决定能否打断别人 抢占优先级(高位)+同级抢占时谁先执行 子优先级(低位)

  • 抢占优先级:数字越小,优先级越高。高优先级中断可以打断正在执行的低优先级中断(这就是"嵌套")。
  • 子优先级:在抢占优先级相同时,数字越小越先响应,但不能相互打断。
    共有 16 个可编程优先级( 2 4 = 16 2^4 = 16 24=16)。

1.4 NVIC 与处理器核心的关系

┌─────────────────────────────────────────────┐
│              Cortex-M4 处理器核              │
│                                             │
│   ┌──────────┐      ┌─────────────────┐    │
│   │   CPU    │◄────►│  NVIC 中断控制器 │    │
│   │ 执行代码 │      │  (紧密耦合接口)  │    │
│   └──────────┘      └────────┬────────┘    │
└────────────────────────────── │ ────────────┘
                                │ 接收中断请求
              ┌─────────────────┼──────────────┐
              │                 │              │
         外设中断           系统异常        软件中断
       (EXTI/UART等)      (HardFault等)   (SVC等)

NVIC 和处理器核心紧密耦合,这使得:

  1. 中断响应延迟极低(通常只需几个时钟周期)
  2. 即使中断来得很晚(CPU 正要进入中断时另一个来了),也能高效处理

2. SysTick 校准值

SysTick 是一个系统滴答定时器,常用于产生操作系统的时基(比如 FreeRTOS 的 1ms 心跳)。
校准值固定为 18750,含义如下:
参考时基 = 校准值 SysTick时钟频率 = 18750 18.75 × 10 6  Hz = 1  ms \text{参考时基} = \frac{\text{校准值}}{\text{SysTick时钟频率}} = \frac{18750}{18.75 \times 10^6 \text{ Hz}} = 1 \text{ ms} 参考时基=SysTick时钟频率校准值=18.75×106 Hz18750=1 ms
SysTick 时钟来源:
f SysTick = H C L K 8 = 150  MHz 8 = 18.75  MHz f_{\text{SysTick}} = \frac{HCLK}{8} = \frac{150 \text{ MHz}}{8} = 18.75 \text{ MHz} fSysTick=8HCLK=8150 MHz=18.75 MHz
所以当 HCLK = 150 MHz 时,SysTick 每计数 18750 次,恰好过去 1 毫秒。

3. 向量表

3.1 什么是向量表?

当中断发生时,CPU 需要知道"跳到哪里去执行处理函数"。向量表就是一张地址列表,存放在 Flash 的起始位置(默认从 0x0000 0000 开始),每个条目是一个 32 位的函数指针。

内存地址         内容
0x0000 0000  →  [MSP 初始值]         ← 堆栈顶指针初始值
0x0000 0004  →  [Reset_Handler 地址]  ← 复位后跳这里
0x0000 0008  →  [NMI_Handler 地址]    ← 不可屏蔽中断
0x0000 000C  →  [HardFault 地址]      ← 硬件错误
...
0x0000 0040  →  [WWDG_IRQHandler]     ← 位置0,第一个外设中断
0x0000 0044  →  [PVD_IRQHandler]      ← 位置1
...

3.2 异常类型说明

向量表中有两种优先级类型:

类型 含义 例子
fixed(固定) 优先级由硬件写死,不可更改 Reset(-3)、NMI(-2)、HardFault(-1)
settable(可设置) 可通过软件配置优先级 所有外设中断

3.3 核心异常(两款芯片相同)


优先级 缩写 描述 地址
-3(fixed) Reset 复位,上电或手动复位后执行 0x0000 0004
-2(fixed) NMI 不可屏蔽中断,连接到时钟安全系统CSS 0x0000 0008
-1(fixed) HardFault 所有类型的硬件错误 0x0000 000C
0(settable) MemManage 内存管理错误(MPU违规) 0x0000 0010
1(settable) BusFault 总线错误(预取失败、内存访问失败) 0x0000 0014
2(settable) UsageFault 未定义指令或非法状态 0x0000 0018
3(settable) SVCall 通过 SWI 指令的系统服务调用 0x0000 002C
4(settable) Debug Monitor 调试监视器 0x0000 0030
5(settable) PendSV 可挂起的系统服务请求 0x0000 0038
6(settable) SysTick 系统滴答定时器 0x0000 003C

3.4 STM32F405/07/15/17xx 外设中断(部分重要项)


位置 优先级编号 缩写 描述 地址
0 7 WWDG 窗口看门狗中断 0x0000 0040
1 8 PVD 通过EXTI线的PVD检测中断 0x0000 0044
5 12 RCC RCC全局中断 0x0000 0054
6 13 EXTI0 EXTI Line0 中断 0x0000 0058
7 14 EXTI1 EXTI Line1 中断 0x0000 005C
10 17 EXTI4 EXTI Line4 中断 0x0000 0068
23 30 EXTI9_5 EXTI Line[9:5] 中断 0x0000 009C
35 42 SPI1 SPI1 全局中断 0x0000 00CC
37 44 USART1 USART1 全局中断 0x0000 00D4
40 47 EXTI15_10 EXTI Line[15:10] 中断 0x0000 00E0
81 88 FPU FPU 全局中断 0x0000 0184

3.5 STM32F42/43xxx 新增的外设中断(相比F405系列)


位置 优先级编号 缩写 描述 地址
82 89 UART7 UART7 全局中断 0x0000 0188
83 90 UART8 UART8 全局中断 0x0000 018C
84 91 SPI4 SPI4 全局中断 0x0000 0190
85 92 SPI5 SPI5 全局中断 0x0000 0194
86 93 SPI6 SPI6 全局中断 0x0000 0198
87 94 SAI1 SAI1 全局中断 0x0000 019C
88 95 LCD-TFT LTDC 全局中断 0x0000 01A0
89 96 LCD-TFT LTDC 全局错误中断 0x0000 01A4
90 97 DMA2D DMA2D 全局中断 0x0000 01A8

4. EXTI — 外部中断/事件控制器

4.1 EXTI 是什么?为什么需要它?

假设你有一个按键接在 GPIO 引脚上,你希望按下按键时 CPU 立刻响应,而不是让 CPU 不停地循环检测(轮询,浪费算力)。EXTI 就是解决这个问题的:它监视外部引脚的电平变化,一旦检测到边沿(上升沿或下降沿),就自动触发中断或事件通知 CPU。

4.2 EXTI 主要特性

  • 23 条独立的中断/事件线,每条都可以独立配置
  • 每条线可选触发类型:上升沿、下降沿,或两者都触发
  • 每条线可独立屏蔽(不想响应就屏蔽掉)
  • 有专用的 Pending(待处理)状态位,软件可查询和清除
  • 支持软件触发(不需要外部信号,程序可以主动模拟一次中断)
  • 可检测宽度小于 APB2 时钟周期的脉冲(极短脉冲也能捕捉)

4.3 EXTI 工作原理框图

外部引脚信号
      │
      ▼
┌─────────────┐        ┌──────────────────┐
│ 边沿检测电路 │        │ 上升沿选择寄存器  │ EXTI_RTSR
│ Edge Detect  │◄───────│ 下降沿选择寄存器  │ EXTI_FTSR
└──────┬───────┘        └──────────────────┘
       │ 检测到边沿
       ▼
  ┌────┴────┐
  │         │
  ▼         ▼
[中断路径]  [事件路径]
  │         │
  │  ┌──────┴──────┐     ┌────────────┐
  │  │ 事件屏蔽寄存 │     │ 脉冲发生器  │──► 唤醒WFE休眠
  │  │ 器 EXTI_EMR │     └────────────┘
  │  └─────────────┘
  │
  ▼
┌──────────────────┐
│ 中断屏蔽寄存器    │ EXTI_IMR
│ (屏蔽 = 不响应) │
└────────┬─────────┘
         │ 未屏蔽才通过
         ▼
┌──────────────────┐
│  Pending 寄存器   │ EXTI_PR(标记"有中断未处理")
│                  │ 软件写1清除
└────────┬─────────┘
         │
         ▼
     发送给 NVIC → CPU 执行中断处理函数
另外:EXTI_SWIER(软件中断事件寄存器)
可以不依赖外部信号,由软件直接触发

4.4 中断 vs 事件,区别是什么?

这是 STM32 中一个容易混淆的概念:

特性 中断(Interrupt) 事件(Event)
目的 让 CPU 执行中断处理函数 唤醒处于低功耗状态的 CPU
Pending位 会被置位,软件必须清除 不会被置位
走的路径 IMR → Pending → NVIC → CPU EMR → 脉冲发生器 → 唤醒逻辑
配置寄存器 EXTI_IMR EXTI_EMR
典型场景 按键触发执行某段代码 按键唤醒睡眠中的MCU

简单记忆:中断让CPU做事,事件让CPU醒来

4.5 唤醒事件管理(WFE)

当 MCU 执行 WFE(Wait For Event)指令进入低功耗待机状态后,有两种方式可以唤醒它:
方式一:通过 SEVONPEND 位

  1. 在外设控制寄存器中使能中断,但不在 NVIC 中使能
  2. 设置 Cortex-M4 系统控制寄存器中的 SEVONPEND 位为 1
  3. 外设触发后 CPU 从 WFE 唤醒
  4. 唤醒后必须手动清除外设中断 pending 位和 NVIC IRQ pending 位
    方式二:通过 EXTI 事件模式(推荐)
  5. 将 EXTI 线配置为事件模式(写 EXTI_EMR)
  6. 外部信号到来后 CPU 自动唤醒
  7. 不需要清除 pending 位(事件模式下 pending 位不会被置位)

4.6 GPIO 与 EXTI 线的映射

STM32 的 GPIO 非常多(F405 有 140 个,F42xxx 有 168 个),但 EXTI 只有 16 条 GPIO 线(EXTI0~EXTI15)。映射规则如下:

所有端口的同号引脚共用同一条 EXTI 线:
PA0 ──┐
PB0 ──┤
PC0 ──┤──► EXTI0  (同时只能选其中一个端口)
PD0 ──┤
PE0 ──┤
...   ┘
PA1 ──┐
PB1 ──┤
PC1 ──┤──► EXTI1
...   ┘
PA15 ──┐
PB15 ──┤
PC15 ──┤──► EXTI15
...    ┘

选择哪个端口通过 SYSCFG_EXTICRx 寄存器配置(4位选择器)。
例如,要让 PC13 触发 EXTI13,就在 SYSCFG_EXTICR4 的 EXTI13[3:0] 字段写入 0010(代表端口C)。

4.7 EXTI 16~22 的特殊用途(非 GPIO)


EXTI 线号 连接到的信号
EXTI16 PVD(电源电压检测)输出
EXTI17 RTC 闹钟事件
EXTI18 USB OTG FS 唤醒事件
EXTI19 以太网唤醒事件
EXTI20 USB OTG HS(FS模式)唤醒事件
EXTI21 RTC Tamper 和 TimeStamp 事件
EXTI22 RTC 唤醒事件

4.8 配置 EXTI 的步骤

配置为硬件中断源:
步骤1:配置 SYSCFG_EXTICRx → 选择哪个端口(PA/PB/...)
步骤2:配置 EXTI_RTSR / EXTI_FTSR → 选择触发边沿
步骤3:配置 EXTI_IMR → 取消屏蔽(使能对应中断线)
步骤4:配置 NVIC → 使能对应 IRQ 通道,设置优先级
配置为硬件事件源:
步骤1:配置 SYSCFG_EXTICRx → 选择端口
步骤2:配置 EXTI_RTSR / EXTI_FTSR → 选择触发边沿
步骤3:配置 EXTI_EMR → 取消屏蔽事件线(不需要配置NVIC)
软件触发中断/事件:
步骤1:配置 EXTI_IMR / EXTI_EMR → 使能对应线
步骤2:写 EXTI_SWIER 对应位为 1 → 软件模拟触发

5. EXTI 寄存器详解

EXTI 控制器共有 6 个寄存器,基地址参考 Memory Map。每个寄存器都是 32 位,但只有低 23 位(bit[22:0])有效,对应 23 条 EXTI 线。

5.1 中断屏蔽寄存器 EXTI_IMR(偏移 0x00)

复位值:0x0000 0000(全部屏蔽)

位[31:23]  保留,必须保持复位值不变
位[22:0]   MRx:对应第 x 条中断线的屏蔽位
             0 = 屏蔽该线的中断请求(默认)
             1 = 不屏蔽(使能该线的中断)

5.2 事件屏蔽寄存器 EXTI_EMR(偏移 0x04)

复位值:0x0000 0000

位[31:23]  保留
位[22:0]   MRx:对应第 x 条事件线的屏蔽位
             0 = 屏蔽该线的事件请求
             1 = 不屏蔽(使能该线的事件)

结构与 IMR 完全相同,只是控制的是"事件"路径而非"中断"路径。

5.3 上升沿触发选择寄存器 EXTI_RTSR(偏移 0x08)

复位值:0x0000 0000

位[31:23]  保留
位[22:0]   TRx:第 x 线的上升沿触发配置
             0 = 禁用上升沿触发
             1 = 使能上升沿触发

注意:外部唤醒线是边沿触发的,不能有毛刺(glitch)。如果在写 EXTI_RTSR 的同时恰好有上升沿到来,Pending 位会被意外置位。

5.4 下降沿触发选择寄存器 EXTI_FTSR(偏移 0x0C)

复位值:0x0000 0000

位[31:23]  保留
位[22:0]   TRx:第 x 线的下降沿触发配置
             0 = 禁用下降沿触发
             1 = 使能下降沿触发

注意:与 RTSR 不同,写 EXTI_FTSR 时发生下降沿,Pending 位不会被置位。
RTSR 和 FTSR 可以同时对同一条线置 1,实现双边沿触发(上升沿和下降沿都响应)。

5.5 软件中断事件寄存器 EXTI_SWIER(偏移 0x10)

复位值:0x0000 0000

位[31:23]  保留
位[22:0]   SWIERx:软件触发第 x 条中断/事件线

工作逻辑

如果 EXTI_IMR 中对应位已置 1(已使能中断):
  对 SWIERx 写 1(从0变1时)→ 自动置位 EXTI_PR 中对应的 Pending 位
                              → 触发中断请求
清除方式:
  清除 EXTI_PR 中对应位(写1清零)→ SWIERx 自动被清除

5.6 Pending 寄存器 EXTI_PR(偏移 0x14)

复位值:0x0000 0000

位[31:23]  保留
位[22:0]   PRx:第 x 线的 Pending(待处理)标志
             0 = 没有触发请求发生
             1 = 检测到了所选边沿,有中断待处理
清除方式:向对应位写 1(rc_w1 类型,写1清零,写0无效)

极其重要:在中断处理函数(ISR)中,必须先清除 EXTI_PR 中对应的 pending 位,否则 ISR 执行完后会立刻再次触发,形成无限循环!

5.7 寄存器汇总表


偏移 寄存器 作用 复位值
0x00 EXTI_IMR 中断屏蔽(1=使能) 0x0000 0000
0x04 EXTI_EMR 事件屏蔽(1=使能) 0x0000 0000
0x08 EXTI_RTSR 上升沿触发选择 0x0000 0000
0x0C EXTI_FTSR 下降沿触发选择 0x0000 0000
0x10 EXTI_SWIER 软件触发(写1触发) 0x0000 0000
0x14 EXTI_PR Pending状态(写1清除) 0x0000 0000

6. 完整示例代码

6.1 场景说明

用 PC13 引脚(常见于 STM32 蓝色小板的用户按键)配置外部中断,下降沿触发(按键按下时从高变低),在 ISR 中翻转 PA5(LED)。

6.2 完整 C++ 代码

// 文件:main_exti_demo.cpp
// 目标芯片:STM32F407VGT6(或类似 F4 系列)
// 功能:PC13 下降沿触发外部中断,ISR 中翻转 PA5(LED)
//
// 说明:本代码直接操作寄存器,不依赖 HAL/LL 库,
//       便于理解底层原理。
//
// 头文件说明:
//   stm32f4xx.h  — CMSIS 设备头文件,定义所有外设寄存器结构体
//                  以及 IRQn_Type 枚举(中断号)
//   core_cm4.h   — 由 stm32f4xx.h 自动包含,提供 NVIC 内联函数
#include "stm32f4xx.h"   // 包含 STM32F4 寄存器定义(CMSIS)
                         // 不使用万能头文件,只引用必要定义
// ─────────────────────────────────────────────
// 函数声明
// ─────────────────────────────────────────────
static void clock_init();       // 使能必要的时钟
static void gpio_init();        // 配置 GPIO
static void syscfg_exti_init(); // 配置 SYSCFG 和 EXTI
static void nvic_init();        // 配置 NVIC
// ─────────────────────────────────────────────
// 主函数
// ─────────────────────────────────────────────
int main()
{
    clock_init();        // 第1步:打开时钟
    gpio_init();         // 第2步:初始化 GPIO
    syscfg_exti_init();  // 第3步:配置 EXTI 线
    nvic_init();         // 第4步:使能 NVIC 中断通道
    // 主循环:什么都不做,等待中断
    // 实际项目中可以在这里执行其他任务
    while (true)
    {
        // 可选:让 CPU 进入低功耗等待(中断唤醒)
        // __WFI();  // Wait For Interrupt,省电
    }
    return 0;
}
// ─────────────────────────────────────────────
// 时钟初始化
// ─────────────────────────────────────────────
static void clock_init()
{
    // RCC->AHB1ENR:AHB1 外设时钟使能寄存器
    // bit0 = GPIOAEN(GPIOA 时钟)
    // bit2 = GPIOCEN(GPIOC 时钟)
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN   // 使能 GPIOA 时钟(PA5 LED)
                  | RCC_AHB1ENR_GPIOCEN;  // 使能 GPIOC 时钟(PC13 按键)
    // RCC->APB2ENR:APB2 外设时钟使能寄存器
    // bit14 = SYSCFGEN(SYSCFG 时钟)
    // SYSCFG 用于配置 EXTI 线与 GPIO 端口的映射
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN; // 使能 SYSCFG 时钟
}
// ─────────────────────────────────────────────
// GPIO 初始化
// ─────────────────────────────────────────────
static void gpio_init()
{
    // ── PA5:LED 输出引脚 ──────────────────────
    // GPIOA->MODER:模式寄存器,每引脚占2位
    // PA5 在 bit[11:10],写 01 = 通用推挽输出
    GPIOA->MODER &= ~(0x3u << (5 * 2));   // 先清零 bit[11:10]
    GPIOA->MODER |=  (0x1u << (5 * 2));   // 写 01 = 输出模式
    // GPIOA->OSPEEDR:输出速度,PA5 bit[11:10] 写 00 = 低速(2MHz)
    GPIOA->OSPEEDR &= ~(0x3u << (5 * 2)); // 低速足够驱动 LED
    // GPIOA->OTYPER:输出类型,bit5 = 0 = 推挽输出
    GPIOA->OTYPER &= ~(0x1u << 5);        // 推挽(默认)
    // GPIOA->PUPDR:上下拉,PA5 bit[11:10] 写 00 = 无上下拉
    GPIOA->PUPDR &= ~(0x3u << (5 * 2));   // 无上下拉
    // ── PC13:按键输入引脚 ────────────────────
    // GPIOC->MODER bit[27:26] 写 00 = 输入模式(复位默认值)
    GPIOC->MODER &= ~(0x3u << (13 * 2));  // 清零 = 输入模式
    // GPIOC->PUPDR bit[27:26] 写 01 = 上拉
    // 按键未按下时保持高电平,按下接地变低 → 下降沿
    GPIOC->PUPDR &= ~(0x3u << (13 * 2));  // 先清零
    GPIOC->PUPDR |=  (0x1u << (13 * 2));  // 写 01 = 上拉
}
// ─────────────────────────────────────────────
// SYSCFG + EXTI 初始化
// ─────────────────────────────────────────────
static void syscfg_exti_init()
{
    // ── 步骤1:SYSCFG 配置 EXTI13 使用 GPIOC ────
    //
    // PC13 对应 EXTI13,由 SYSCFG_EXTICR4 寄存器控制。
    //
    // SYSCFG->EXTICR[3] 即 SYSCFG_EXTICR4(下标从0计)
    // EXTI13[3:0] 位于 EXTICR4 的 bit[7:4]
    //
    // 端口编码:0000=PA, 0001=PB, 0010=PC, 0011=PD, ...
    // PC → 写 0b0010 = 0x2
    SYSCFG->EXTICR[3] &= ~(0xFu << 4);   // 清除 EXTI13[3:0](bit[7:4])
    SYSCFG->EXTICR[3] |=  (0x2u << 4);   // 写 0010 = 选择 GPIOC(PC13)
    // ── 步骤2:配置 EXTI_FTSR,使能 Line13 下降沿触发 ──
    //
    // EXTI->FTSR bit13 = 1 → Line13 检测下降沿
    EXTI->FTSR |= (0x1u << 13);
    // 确保没有意外使能上升沿触发(可选清除)
    EXTI->RTSR &= ~(0x1u << 13);         // 清除上升沿触发
    // ── 步骤3:配置 EXTI_IMR,取消屏蔽 Line13 ──
    //
    // EXTI->IMR bit13 = 1 → Line13 的中断请求不被屏蔽
    EXTI->IMR |= (0x1u << 13);
    // (此时 EXTI 已准备好,等待 NVIC 使能后即可响应中断)
}
// ─────────────────────────────────────────────
// NVIC 初始化
// ─────────────────────────────────────────────
static void nvic_init()
{
    // EXTI13 对应的 IRQ 通道是 EXTI15_10_IRQn
    // (EXTI Line10~15 共用一个中断向量)
    // 设置优先级:抢占优先级 = 1,子优先级 = 0
    // CMSIS 函数:NVIC_SetPriority(IRQn, priority)
    // 注意:priority 是编码后的值,使用 NVIC_EncodePriority 更准确
    // 这里简单设置为 1
    NVIC_SetPriority(EXTI15_10_IRQn, 1);
    // 使能 NVIC 中的 EXTI15_10 通道
    NVIC_EnableIRQ(EXTI15_10_IRQn);
}
// ─────────────────────────────────────────────
// 中断服务程序(ISR)
// ─────────────────────────────────────────────
// 函数名必须与启动文件(startup_stm32f407xx.s)中的弱定义一致
// EXTI Line10~15 共用此 ISR,需在内部区分是哪条线触发的
extern "C" void EXTI15_10_IRQHandler()
{
    // 检查是否是 EXTI Line13 触发的中断
    // EXTI->PR bit13 = 1 表示 Line13 有 pending 中断
    if (EXTI->PR & (0x1u << 13))
    {
        // ── 重要:先清除 Pending 位!──────────────
        // EXTI_PR 是 rc_w1 类型:向对应位写 1 来清除
        // 写 0 无效,不要用 &= ~(...) 的方式清除!
        EXTI->PR = (0x1u << 13);   // 写1清除 bit13
        // ── 翻转 PA5(LED)──────────────────────
        // GPIOA->ODR bit5 控制 PA5 输出
        // 使用 XOR 操作翻转:0变1,1变0
        GPIOA->ODR ^= (0x1u << 5);
        // 或者使用原子操作(推荐,避免读-改-写被中断打断):
        // if (GPIOA->ODR & (0x1u << 5))
        //     GPIOA->BSRR = (0x1u << (5 + 16));  // 高16位 = 复位
        // else
        //     GPIOA->BSRR = (0x1u << 5);          // 低16位 = 置位
    }
}

6.3 执行流程图(Mermaid)

上电复位
Reset

clock_init
使能 GPIOA/C/SYSCFG 时钟

gpio_init
PA5 输出 / PC13 输入上拉

syscfg_exti_init
EXTICR4 选 GPIOC
FTSR 下降沿
IMR 使能 Line13

nvic_init
设置优先级
使能 EXTI15_10_IRQn

主循环 while true
等待中断

用户按下按键
PC13 高→低

EXTI 边沿检测电路
检测到下降沿

EXTI_PR bit13
置 1 = Pending

通知 NVIC
EXTI15_10_IRQn

CPU 暂停主循环
跳转到 EXTI15_10_IRQHandler

检查 PR bit13
是 Line13 吗?

写 1 清除
EXTI_PR bit13

翻转 PA5
LED 状态改变

ISR 返回
继续主循环

6.4 寄存器操作位运算速查

理解代码中大量的位操作,核心就三种:
置位(Set): reg ∣ = ( 1 ≪ n ) \text{置位(Set):} \quad \text{reg} \mathrel{|}= (1 \ll n) 置位(Set):reg=(1n)
清位(Clear): reg & = ∼ ( 1 ≪ n ) \text{清位(Clear):} \quad \text{reg} \mathrel{\&}= \sim(1 \ll n) 清位(Clear):reg&=∼(1n)
翻转(Toggle): reg ⊕ = ( 1 ≪ n ) \text{翻转(Toggle):} \quad \text{reg} \mathrel{\oplus}= (1 \ll n) 翻转(Toggle):reg=(1n)
其中 n n n 是目标位的编号(从0开始), ≪ \ll 表示左移操作。

附录:EXTI 线快速查找表


EXTI 线 GPIO 引脚 对应 IRQ 向量 备注
EXTI0 Px0 EXTI0_IRQn 独立向量
EXTI1 Px1 EXTI1_IRQn 独立向量
EXTI2 Px2 EXTI2_IRQn 独立向量
EXTI3 Px3 EXTI3_IRQn 独立向量
EXTI4 Px4 EXTI4_IRQn 独立向量
EXTI5~9 Px5~9 EXTI9_5_IRQn 共用向量
EXTI10~15 Px10~15 EXTI15_10_IRQn 共用向量
EXTI16 PVD_IRQn 非GPIO
EXTI17 RTC_Alarm_IRQn 非GPIO
EXTI22 RTC_WKUP_IRQn 非GPIO

x = A/B/C/D/E/F/G/H/I(F405系列)或增加 J/K(F42x系列)

STM32F4xx ADC 模数转换器 — 从零开始完全理解

参考手册:RM0090,第13章
适用芯片:STM32F405xx / F407xx / F415xx / F417xx / F42xxx / F43xxx

目录

  1. ADC 是什么?基本概念
  2. ADC 主要特性总览
  3. ADC 功能描述
  4. 转换模式详解
  5. 数据对齐
  6. 采样时间与转换时间
  7. 外部触发
  8. 数据管理(DMA)
  9. 多ADC模式
  10. 温度传感器与VBAT
  11. ADC 中断
  12. 寄存器详解
  13. 完整 C++ 示例代码

1. ADC 是什么?

1.1 从生活出发理解 ADC

现实世界里的信号(温度、声音、电压)都是模拟量,也就是连续变化的值。但数字电路只认识 0 和 1。ADC(Analog-to-Digital Converter,模数转换器)的作用就是把连续的模拟电压转换成数字数值,让 CPU 能够处理。
举个例子:

模拟世界                       数字世界
────────────────────────────────────────
电位器拧到一半 → 1.65V   →  ADC  →  2048(12位满量程4095)
温度传感器输出 → 0.76V   →  ADC  →   952
电池电压       → 3.3V    →  ADC  →  4095(满量程)

1.2 STM32F4 ADC 的基本参数

  • 类型:逐次逼近型(SAR,Successive Approximation Register)
  • 分辨率:最高 12 位(也可选 10/8/6 位)
  • 通道数:最多 19 个(16 个外部 GPIO 通道 + 2 个内部 + 1 个 VBAT)
  • 结果寄存器:16 位(实际有效位取决于分辨率设置)
    12 位分辨率意味着可以将输入电压范围分成 2 12 = 4096 2^{12} = 4096 212=4096 个等级:
    数字输出值 = V I N − V R E F − V R E F + − V R E F − × ( 2 12 − 1 ) 数字输出值 = \frac{V_{IN} - V_{REF-}}{V_{REF+} - V_{REF-}} \times (2^{12} - 1) 数字输出值=VREF+VREFVINVREF×(2121)
    例如, V R E F + = 3.3 V V_{REF+} = 3.3\text{V} VREF+=3.3V V R E F − = 0 V V_{REF-} = 0\text{V} VREF=0V,输入 V I N = 1.65 V V_{IN} = 1.65\text{V} VIN=1.65V
    输出 = 1.65 3.3 × 4095 = 2047 输出 = \frac{1.65}{3.3} \times 4095 = 2047 输出=3.31.65×4095=2047

2. 主要特性


特性 说明
分辨率 12 / 10 / 8 / 6 位可配置
转换模式 单次、连续、扫描、不连续
通道组 规则组(最多16路)+ 注入组(最多4路)
触发方式 软件触发、定时器触发、EXTI外部触发
数据对齐 左对齐或右对齐
模拟看门狗 电压超限自动报警
DMA支持 支持,避免数据丢失
多ADC模式 双ADC/三ADC同步/交错/交替模式
供电电压 2.4V3.6V(全速);1.8V3.6V(降速)
输入范围 V R E F − ≤ V I N ≤ V R E F + V_{REF-} \leq V_{IN} \leq V_{REF+} VREFVINVREF+

3. ADC 功能描述

3.1 ADC 引脚说明


引脚名 类型 说明
VREF+ 模拟参考正端输入 ADC 正参考电压, 1.8 V ≤ V R E F + ≤ V D D A 1.8\text{V} \leq V_{REF+} \leq V_{DDA} 1.8VVREF+VDDA
VDDA 模拟电源输入 等于 VDD,全速时 2.4 V ≤ V D D A ≤ 3.6 V 2.4\text{V} \leq V_{DDA} \leq 3.6\text{V} 2.4VVDDA3.6V
VREF- 模拟参考负端输入 必须连接到 VSSA
VSSA 模拟电源地 等于 VSS
ADCx_IN[15:0] 模拟信号输入 16 路外部模拟输入通道

3.2 ADC 开关控制

ADC 的开关由 ADC_CR2 寄存器的 ADON 位控制:

ADON = 0  →  ADC 关闭(功耗极低,仅几微安)
ADON = 1  →  ADC 上电,从掉电模式唤醒
注意:第一次写 ADON=1 是唤醒过程,不会立即开始转换。
启动转换需要额外设置 SWSTART(规则组)或 JSWSTART(注入组)。

3.3 ADC 时钟

ADC 有两套时钟:

                  APB2 时钟(PCLK2)
                        │
            ┌───────────┴────────────┐
            │                        │
   ┌────────▼────────┐    ┌──────────▼──────────┐
   │  ADC 预分频器   │    │  数字接口时钟         │
   │  /2 /4 /6 /8    │    │  (寄存器读写用)     │
   └────────┬────────┘    └─────────────────────┘
            │
         ADCCLK
   (模拟电路使用,所有ADC共用)
  • ADCCLK:供模拟转换电路使用,通过 ADC_CCR.ADCPRE 配置分频比。查阅数据手册确认 ADCCLK 最大值(通常不超过 36 MHz)。
  • 数字接口时钟:等于 APB2 频率,每个 ADC 可通过 RCC_APB2ENR 单独使能/禁用。

3.4 通道选择:规则组与注入组

STM32F4 的 ADC 把转换通道分成两类,就像餐厅的"正餐"和"插队加菜":

规则组(Regular Group)— 最多 16 个通道
  就像正常点的菜,按顺序一道道上,最终结果放进 ADC_DR 寄存器
  通道顺序用 ADC_SQR1/2/3 寄存器配置
  总通道数写在 ADC_SQR1 的 L[3:0] 字段
注入组(Injected Group)— 最多 4 个通道
  就像紧急插队的菜,可以打断规则组,优先被转换
  结果分别放进 ADC_JDR1~4 寄存器(4个专用)
  通道顺序用 ADC_JSQR 寄存器配置
  总通道数写在 ADC_JSQR 的 JL[1:0] 字段

转换序列可以完全自由组合,例如:IN3 → IN8 → IN2 → IN2 → IN0 → IN2 → IN15,同一通道可以重复出现。

3.5 内部特殊通道


通道 F40x/F41x F42x/F43x 说明
温度传感器 ADC1_IN16 ADC1_IN18(与VBAT共用) 测量芯片结温
VREFINT ADC1_IN17 ADC1_IN17 内部参考电压
VBAT ADC1_IN18(与温度传感器共用) 电池电压监测

重要:这三个内部通道只在 ADC1 上可用,ADC2/ADC3 无法访问。
使用前必须设置 ADC_CCR.TSVREFE = 1 来唤醒温度传感器和 VREFINT。

4. 转换模式

4.1 转换时序图(通用)

ADCCLK  ___│‾│_│‾│_│‾│_│‾│_│‾│_│‾│_ ...
ADON    _____________│‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
          ← tSTAB →
SWSTART ___________________│‾‾│________________
ADC转换  ___________________│      转换中      │___
EOC     _________________________________│‾‾‾│___
         ← 稳定时间 →← 15个ADCCLK周期(12位)→

关键点:

  • 必须等待 ADC 稳定时间 t S T A B t_{STAB} tSTAB 后才能开始转换
  • 12 位分辨率时,从启动到 EOC 标志置位需要 15 个 ADCCLK 周期

4.2 单次转换模式(Single Conversion)

每次触发只转换一个通道,完成后 ADC 停止。

CONT 位 = 0  (关键!)
触发方式:
  规则通道 → 写 SWSTART=1,或外部触发
  注入通道 → 写 JSWSTART=1,或外部触发
转换完成后:
  规则通道 → 结果写入 ADC_DR,EOC=1,若 EOCIE=1 则产生中断
  注入通道 → 结果写入 ADC_JDR1,JEOC=1,若 JEOCIE=1 则产生中断
然后 ADC 停止,等待下次触发。

4.3 连续转换模式(Continuous Conversion)

转换完成后立即开始下一次,不停歇,直到软件停止。

CONT 位 = 1  (关键!)
启动方式:外部触发,或写 SWSTART=1(仅规则通道)
每次转换完成后:
  结果写入 ADC_DR(新值覆盖旧值)
  EOC=1,若 EOCIE=1 则产生中断
  立刻开始下一次转换,形成循环
注意:注入通道不能直接用连续模式,除非通过 JAUTO 自动注入配合。

4.4 扫描模式(Scan Mode)

用于自动扫描一组通道,按配置好的序列依次转换每个通道,不需要每次都手动触发。

SCAN 位 = 1
工作流程:
  触发一次 → 自动依次转换 ADC_SQRx 中配置的所有通道
  每个通道转换完成 → 自动切换到下一个
  最后一个通道转换完成 → EOC 置位(若 EOCS=0)
                         或每个通道完成都置位(若 EOCS=1)
配合 CONT=1 可实现"持续循环扫描所有通道"
配合 DMA 可把每个通道的结果自动搬运到内存数组

4.5 不连续模式(Discontinuous Mode)

把一个长转换序列分批执行,每次外部触发只转换其中 n n n 个( n ≤ 8 n \leq 8 n8)。
规则组不连续模式示例( n = 3 n = 3 n=3,总序列 = CH0, CH1, CH2, CH3, CH6, CH7, CH9, CH10):

第1次触发 → 转换 CH0, CH1, CH2 → 每个完成都产生 EOC
第2次触发 → 转换 CH3, CH6, CH7 → 每个完成都产生 EOC
第3次触发 → 转换 CH9, CH10     → 每个完成都产生 EOC
第4次触发 → 回到开头,转换 CH0, CH1, CH2  ← 循环

注意:

  • 通过 ADC_CR1.DISCEN = 1 使能(规则组)
  • ADC_CR1.DISCNUM[2:0] 设置每批转换的通道数 n n n(000=1个,…,111=8个)
  • 不连续模式不能与自动注入模式同时使用
  • 规则组和注入组不能同时使用不连续模式
    注入组不连续模式(每次触发只转换序列中的下一个通道):
序列配置:CH1, CH2, CH3
第1次触发 → 转换 CH1
第2次触发 → 转换 CH2
第3次触发 → 转换 CH3 → 产生 JEOC
第4次触发 → 回到 CH1  ← 循环

4.6 注入通道管理

注入通道有两种触发方式:
触发注入(Triggered Injection)

  1. 规则组正在转换时,若注入触发发生
  2. 当前规则通道转换被立即中断复位
  3. 切换到注入序列(扫描一次模式)全部转换完
  4. 规则组从被中断的位置恢复继续
时序图:
规则组  ──[R0]──[R1]──[R2]──!──────────────[R2恢复]──[R3]──
                              ↑ 注入触发
注入组                        ──[J0]──[J1]──[J2]──[J3]──

注意:触发注入时,两次触发之间的间隔必须大于注入序列总时长。
自动注入(Auto-Injection,JAUTO=1)

  • 规则组转换完成后,自动紧接着转换注入组
  • 不需要额外触发,最多实现 16+4=20 路连续转换
  • 此时必须禁用注入组的外部触发
  • 不能与不连续模式同时使用

5. 数据对齐

ADC 转换结果是 12 位(或更低),但数据寄存器是 16 位,所以有两种对齐方式:

5.1 右对齐(ALIGN=0,默认)

数据靠右放,高位补零:

16位寄存器布局(规则组,12位结果):
 bit15  bit14  bit13  bit12  bit11  bit10  ...  bit1   bit0
┌──────┬──────┬──────┬──────┬──────┬──────┬───┬──────┬──────┐
│  0   │  0   │  0   │  0   │ D11  │ D10  │...│  D1  │  D0  │
└──────┴──────┴──────┴──────┴──────┴──────┴───┴──────┴──────┘
  高4位补零                    有效数据 D11~D0(12位)
读取方式:直接读 ADC_DR,值就是 0~4095(12位满量程)

注入组的右对齐还会减去偏移量(ADC_JOFRx),结果可能为负数,用 SEXT 符号位扩展。

5.2 左对齐(ALIGN=1)

数据靠左放,低位补零:

16位寄存器布局(规则组,12位结果):
 bit15  bit14  bit13  bit12  bit11  ...  bit4   bit3   bit2   bit1   bit0
┌──────┬──────┬──────┬──────┬──────┬───┬──────┬──────┬──────┬──────┬──────┐
│ D11  │ D10  │  D9  │  D8  │  D7  │...│  D0  │  0   │  0   │  0   │  0   │
└──────┴──────┴──────┴──────┴──────┴───┴──────┴──────┴──────┴──────┴──────┘
  有效数据 D11~D0(12位)              低4位补零
读取方式:需右移4位:value = ADC_DR >> 4
好处:精度要求不高时,直接取高字节即为8位结果

特殊情况:6位分辨率左对齐时,数据对齐到字节边界(而非半字),低位补0。

6. 采样时间与转换时间

6.1 采样时间

每个通道可以独立设置采样时间(通过 ADC_SMPR1/ADC_SMPR2SMP[2:0] 字段):

SMP[2:0] 编码 采样时间
000 3 个 ADCCLK 周期
001 15 个 ADCCLK 周期
010 28 个 ADCCLK 周期
011 56 个 ADCCLK 周期
100 84 个 ADCCLK 周期
101 112 个 ADCCLK 周期
110 144 个 ADCCLK 周期
111 480 个 ADCCLK 周期

采样时间越长,结果越稳定(适合高阻抗源),但转换越慢。

6.2 总转换时间计算

T c o n v = T s a m p l i n g + T r e s o l u t i o n T_{conv} = T_{sampling} + T_{resolution} Tconv=Tsampling+Tresolution
其中 T r e s o l u t i o n T_{resolution} Tresolution 取决于分辨率:
T r e s o l u t i o n = { 12  个ADCCLK周期 12位 10  个ADCCLK周期 10位 8  个ADCCLK周期 8位 6  个ADCCLK周期 6位 T_{resolution} = \begin{cases} 12 \text{ 个ADCCLK周期} & \text{12位} \\ 10 \text{ 个ADCCLK周期} & \text{10位} \\ 8 \text{ 个ADCCLK周期} & \text{8位} \\ 6 \text{ 个ADCCLK周期} & \text{6位} \end{cases} Tresolution= 12 ADCCLK周期10 ADCCLK周期8 ADCCLK周期6 ADCCLK周期121086
最快转换时间(最小采样时间3周期):
T c o n v _ m i n = 3 + 12 = 15  个ADCCLK周期(12位) T_{conv\_min} = 3 + 12 = 15 \text{ 个ADCCLK周期(12位)} Tconv_min=3+12=15 ADCCLK周期(12位)
实际例子:ADCCLK = 30 MHz,采样时间 = 3 周期:
T c o n v = 3 + 12 = 15  周期 = 15 30 × 10 6 = 0.5  μs T_{conv} = 3 + 12 = 15 \text{ 周期} = \frac{15}{30 \times 10^6} = 0.5 \text{ μs} Tconv=3+12=15 周期=30×10615=0.5 μs
各分辨率最快转换时间汇总:
T m i n = { 15  周期 12位 13  周期 10位 11  周期 8位 9  周期 6位 T_{min} = \begin{cases} 15 \text{ 周期} & \text{12位} \\ 13 \text{ 周期} & \text{10位} \\ 11 \text{ 周期} & \text{8位} \\ 9 \text{ 周期} & \text{6位} \end{cases} Tmin= 15 周期13 周期11 周期9 周期121086

7. 外部触发

7.1 触发极性配置

EXTEN[1:0](规则组)或 JEXTEN[1:0](注入组):

编码 含义
00 禁用外部触发(只能软件触发)
01 上升沿触发
10 下降沿触发
11 上升沿和下降沿都触发

触发极性可以在运行时动态修改(on the fly)。

7.2 规则组可用触发源(EXTSEL[3:0])


EXTSEL 触发源
0000 TIM1_CH1
0001 TIM1_CH2
0010 TIM1_CH3
0011 TIM2_CH2
0100 TIM2_CH3
0101 TIM2_CH4
0110 TIM2_TRGO
0111 TIM3_CH1
1000 TIM3_TRGO
1001 TIM4_CH4
1010 TIM5_CH1
1011 TIM5_CH2
1100 TIM5_CH3
1101 TIM8_CH1
1110 TIM8_TRGO
1111 EXTI line11

7.3 注入组可用触发源(JEXTSEL[3:0])


JEXTSEL 触发源
0000 TIM1_CH4
0001 TIM1_TRGO
0010 TIM2_CH1
0011 TIM2_TRGO
0100 TIM3_CH2
0101 TIM3_CH4
0110 TIM4_CH1
0111 TIM4_CH2
1000 TIM4_CH3
1001 TIM4_TRGO
1010 TIM5_CH4
1011 TIM5_TRGO
1100 TIM8_CH2
1101 TIM8_CH3
1110 TIM8_CH4
1111 EXTI line15

更改触发选择时,会有 1 个 APB 时钟周期的触发检测禁用窗口,防止切换瞬间的误触发。

8. 数据管理

8.1 为什么需要 DMA?

规则组所有通道转换完成后,结果都写入同一个 ADC_DR 寄存器(16位)。如果 CPU 不及时读取,下一个通道的数据就会覆盖上一个——这叫数据溢出(Overrun)
DMA(直接存储器访问)能在转换完成后自动把数据搬到内存数组,不需要 CPU 干预。

8.2 使用 DMA 的工作流程

ADC 完成一次规则通道转换
    │
    ▼
产生 DMA 请求
    │
    ▼
DMA 自动把 ADC_DR 的值搬到 SRAM 目标地址
    │
    ▼
ADC 继续下一次转换

相关控制位:

  • ADC_CR2.DMA = 1:使能 DMA 模式
  • ADC_CR2.DDS = 1:DMA 循环模式(请求持续发出,支持双缓冲)
  • ADC_CR2.DDS = 0:DMA 单次模式(最后一次传输完成后停止)

8.3 Overrun(溢出)处理

当 DMA 来不及搬运数据(速度跟不上 ADC 转换速度),发生 Overrun:

  1. ADC_SR.OVR = 1(置位)
  2. DMA 请求被禁止(防止继续搬运无效数据)
  3. 如果 OVRIE = 1 则产生中断
    OVR 恢复步骤(DMA 模式):
步骤1:重新初始化 DMA(重设目标地址和 NDTR 计数器)
步骤2:清除 ADC_SR.OVR 位(写 0 清除)
步骤3:重新触发 ADC 开始转换

8.4 不用 DMA 的软件管理(EOCS 模式)

如果转换频率较低,可以不用 DMA,由软件轮询:

  • 设置 ADC_CR2.EOCS = 1:每个通道转换完都置位 EOC(而非整个序列结束才置位)
  • 每次 EOC 置位后,软件读取 ADC_DR
  • 自动使能 Overrun 检测

8.5 模拟看门狗(Analog Watchdog,AWD)时的无 DMA 模式

条件:DMA=0, EOCS=0
效果:EOC 仅在整个序列结束时置位,Overrun 检测禁用
适用场景:只需要看门狗监视,不需要读取每次转换值

9. 多ADC模式

STM32F4 最多有 3 个 ADC,可以协同工作,实现更高的采样率或同步采样。

9.1 多 ADC 架构

ADC1(主,Master)
  ├─ 控制触发信号(分发给ADC2/ADC3)
  ├─ 有温度传感器/VREFINT/VBAT接入
  └─ 通过 ADC_CDR 共享数据寄存器
ADC2(从,Slave)
  └─ 接收 ADC1 的同步触发
ADC3(从,Slave,三ADC模式)
  └─ 接收 ADC1 的同步触发

通过 ADC_CCR.MULTI[4:0] 选择工作模式。

9.2 注入同步模式(Injected Simultaneous)

ADC1 触发后,同时触发 ADC2(和 ADC3),所有 ADC 同时对各自的注入通道进行转换。
适用场景:同时采集多路不同信号(比如三相电流同步采样)。
双 ADC 注入同步时序

触发信号  ────│‾│──────────────────────────────────────────
ADC1    ──────│ CH0  │ CH1  │ CH2  │ CH3  │──────────────
ADC2    ──────│ CH15 │ CH14 │ CH13 │ CH12 │──────────────
              ← 采样 →← 转换 →
              同时开始,同时结束(序列等长时)

9.3 规则同步模式(Regular Simultaneous)

与注入同步类似,但针对规则组,所有 ADC 同时转换各自的规则通道序列。

9.4 交错模式(Interleaved Mode)

同一个通道由多个 ADC 交替采样,大幅提高有效采样率
双 ADC 交错模式示意:

触发信号  ────│‾│──────────────────────────────────────────
ADC1    ──────│ CH0 │           │ CH0 │
                    ←8个ADCCLK→
ADC2    ──────────────│ CH0 │           │ CH0 │
每个 ADC 采样率 = 30 MHz / 15 = 2 MSPS
两个交错后总采样率 ≈ 4 MSPS(两倍提升)

延迟由 ADC_CCR.DELAY[3:0] 配置(5~20 个 ADCCLK 周期)。

9.5 交替触发模式(Alternate Trigger)

针对注入组,每次触发轮流在 ADC1/ADC2/ADC3 之间切换:

第1次触发 → ADC1 注入组转换
第2次触发 → ADC2 注入组转换
第3次触发 → ADC3 注入组转换(三ADC模式)
第4次触发 → 回到 ADC1

9.6 多 ADC 模式下的 DMA

DMA 模式通过 ADC_CCR.DMA[1:0] 配置(不是各 ADC 自己的 DMA 位):

DMA[1:0] 模式 每次请求传输量 适用场景
01 DMA 模式1 1个半字(16位) 三ADC规则同步
10 DMA 模式2 1个字(32位=2个半字) 双ADC交错/同步;三ADC交错
11 DMA 模式3 1个半字(8位×2) 6/8位分辨率双/三ADC交错

DMA 模式2 双ADC交错示例(ADC_CDR 32位寄存器):

第1次DMA请求:ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0]
                               高16位=ADC2    低16位=ADC1
第2次DMA请求:ADC_CDR[31:0] = ADC2_DR[15:0] | ADC1_DR[15:0]
...(持续)

10. 温度传感器与 VBAT

10.1 温度传感器

芯片内部集成了温度传感器,通过 ADC 读取结温 T J T_J TJ
使用步骤

  1. 选择对应 ADC 通道(F40x: IN16;F42x: IN18)
  2. 设置采样时间大于数据手册规定的最小值
  3. 置位 ADC_CCR.TSVREFE = 1(唤醒传感器)
  4. 设置 SWSTART = 1 开始转换
  5. 读取 ADC_DR 得到 V S E N S E V_{SENSE} VSENSE(数字值)
  6. 用以下公式换算温度:
    T ° C = V S E N S E − V 25 A v g _ S l o p e + 25 T_{°C} = \frac{V_{SENSE} - V_{25}}{Avg\_Slope} + 25 T°C=Avg_SlopeVSENSEV25+25
    其中:
  • V 25 V_{25} V25:温度 25°C 时的 V S E N S E V_{SENSE} VSENSE 电压(查数据手册,单位 mV)
  • A v g _ S l o p e Avg\_Slope Avg_Slope:温度-电压曲线斜率(查数据手册,单位 mV/°C)

注意:内部温度传感器适合检测温度变化,不适合测量绝对温度(芯片间误差最大 ±45°C)。精确测温请用外部传感器(如 DS18B20)。
启动时序优化:建议同时设置 ADONTSVREFE,以减少总启动等待时间。

10.2 VBAT 电池监测

ADC_CCR.VBATE = 1 使能 VBAT 通道。为防止 VBAT 超过 VDDA,内部有分压桥:
V A D C _ I N = { V B A T / 2 F40x/F41x 设备 V B A T / 4 F42x/F43x 设备 V_{ADC\_IN} = \begin{cases} V_{BAT} / 2 & \text{F40x/F41x 设备} \\ V_{BAT} / 4 & \text{F42x/F43x 设备} \end{cases} VADC_IN={VBAT/2VBAT/4F40x/F41x 设备F42x/F43x 设备
所以读取到 ADC 值后,需要乘回分压比才能得到真实电池电压:
V B A T = A D C _ v a l u e × V R E F + 4096 × 分压比 V_{BAT} = ADC\_value \times \frac{V_{REF+}}{4096} \times 分压比 VBAT=ADC_value×4096VREF+×分压比

在 F42x/F43x 上,VBAT 和温度传感器共用 IN18,不能同时转换,且 VBAT 优先级更高(若同时使能,只执行 VBAT 转换)。

11. ADC 中断


中断事件 事件标志 使能控制位
规则组转换结束(EOC) ADC_SR.EOC ADC_CR1.EOCIE
注入组转换结束(JEOC) ADC_SR.JEOC ADC_CR1.JEOCIE
模拟看门狗事件(AWD) ADC_SR.AWD ADC_CR1.AWDIE
数据溢出(OVR) ADC_SR.OVR ADC_CR1.OVRIE

另有两个仅标志位(无中断):

  • ADC_SR.STRT:规则组开始转换
  • ADC_SR.JSTRT:注入组开始转换

12. 寄存器详解

12.1 ADC 状态寄存器 ADC_SR(偏移 0x00)

位[5]  OVR    溢出标志:1=发生了数据覆盖(仅DMA=1或EOCS=1时检测)
位[4]  STRT   规则组启动标志:1=规则组已开始转换(软件写0清除)
位[3]  JSTRT  注入组启动标志:1=注入组已开始转换(软件写0清除)
位[2]  JEOC   注入组转换完成标志(软件写0清除)
位[1]  EOC    规则组转换完成标志(软件写0清除,或读ADC_DR自动清除)
位[0]  AWD    模拟看门狗标志:1=电压越限(软件写0清除)
全部为 rc_w0 类型:硬件置1,软件写0清除(写1无效)

12.2 ADC 控制寄存器1 ADC_CR1(偏移 0x04)

主要位域:

位[26]     OVRIE     溢出中断使能
位[25:24]  RES[1:0]  分辨率:00=12位,01=10位,10=8位,11=6位
位[23]     AWDEN     规则通道模拟看门狗使能
位[22]     JAWDEN    注入通道模拟看门狗使能
位[15:13]  DISCNUM   不连续模式每批通道数(000=1个...111=8个)
位[12]     JDISCEN   注入通道不连续模式使能
位[11]     DISCEN    规则通道不连续模式使能
位[10]     JAUTO     自动注入使能(规则组完成后自动转注入组)
位[9]      AWDSGL    单通道看门狗使能(0=监视所有,1=只监视AWDCH)
位[8]      SCAN      扫描模式使能
位[7]      JEOCIE    注入组EOC中断使能
位[6]      AWDIE     模拟看门狗中断使能
位[5]      EOCIE     规则组EOC中断使能
位[4:0]    AWDCH     模拟看门狗监视的通道号(0~18)

12.3 ADC 控制寄存器2 ADC_CR2(偏移 0x08)

主要位域:

位[30]     SWSTART      软件启动规则组转换(写1触发,硬件自动清0)
位[29:28]  EXTEN        规则组外部触发极性(00=禁用,01=上升,10=下降,11=双边沿)
位[27:24]  EXTSEL       规则组外部触发源选择
位[22]     JSWSTART     软件启动注入组转换(写1触发,硬件自动清0)
位[21:20]  JEXTEN       注入组外部触发极性
位[19:16]  JEXTSEL      注入组外部触发源选择
位[11]     ALIGN        数据对齐(0=右对齐,1=左对齐)
位[10]     EOCS         EOC位置时机(0=序列结束,1=每次转换结束)
位[9]      DDS          DMA持续请求(0=结束后停止,1=持续发请求)
位[8]      DMA          DMA使能
位[1]      CONT         连续转换模式(0=单次,1=连续)
位[0]      ADON         ADC使能(0=关闭,1=打开)

12.4 采样时间寄存器 ADC_SMPR1/2(偏移 0x0C / 0x10)

  • SMPR1 管理通道 10~18(每通道 3 位 SMP)
  • SMPR2 管理通道 0~9(每通道 3 位 SMP)

12.5 规则序列寄存器 ADC_SQR1/2/3(偏移 0x2C/0x30/0x34)


寄存器 配置的通道位置
ADC_SQR3 第16个转换(SQ1SQ6)
ADC_SQR2 第712个转换(SQ7SQ12)
ADC_SQR1 第1316个转换(SQ13SQ16)+ L[3:0]总长度

每个 SQx 字段写通道号(0~18)。L[3:0] 写总转换数减1(0000=1次,1111=16次)。

12.6 注入序列寄存器 ADC_JSQR(偏移 0x38)

JL[1:0] 写注入序列总长度减1,JSQ1~JSQ4 写通道号。
重要的对齐陷阱:注入序列是从末尾对齐的!

JL=0(1个通道):只执行 JSQ4
JL=1(2个通道):执行 JSQ3 → JSQ4
JL=2(3个通道):执行 JSQ2 → JSQ3 → JSQ4
JL=3(4个通道):执行 JSQ1 → JSQ2 → JSQ3 → JSQ4

所以不管配置几个通道,最后一个通道始终放在 JSQ4

12.7 注入偏移寄存器 ADC_JOFRx(偏移 0x140x20,x=14)

注入通道转换完成后,结果会自动减去对应的偏移量 JOFFSETx[11:0],结果存入 ADC_JDRx(带符号)。用于补偿传感器零点偏移。

12.8 模拟看门狗阈值寄存器

  • ADC_HTR(偏移 0x24):高阈值 HT[11:0],复位值 = 0xFFF(最大值)
  • ADC_LTR(偏移 0x28):低阈值 LT[11:0],复位值 = 0x000
    注意:阈值与数据对齐设置无关,始终与转换前的原始 12 位值比较。

12.9 数据寄存器

  • ADC_DR(偏移 0x4C):规则组转换结果,只读,读取后自动清除 EOC
  • ADC_JDR1~4(偏移 0x3C~0x48):4个注入组各自独立的数据寄存器,只读

12.10 公共寄存器(基址 = ADC1基址 + 0x300)

  • ADC_CSR(+0x00):所有 ADC 状态位的镜像,只读(需在各自 ADC_SR 中清除)
  • ADC_CCR(+0x04):公共控制(TSVREFE、VBATE、ADCPRE、MULTI、DELAY、DMA)
  • ADC_CDR(+0x08):双/三ADC模式的公共数据寄存器(32位,高16位=第2路,低16位=第1路)

12.11 寄存器地址总览


偏移 寄存器 主要功能
0x000~0x04C ADC1 寄存器组 ADC1 控制与数据
0x100~0x14C ADC2 寄存器组 ADC2 控制与数据
0x200~0x24C ADC3 寄存器组 ADC3 控制与数据
ADC1+0x300 ADC_CSR 所有ADC状态镜像
ADC1+0x304 ADC_CCR 公共控制
ADC1+0x308 ADC_CDR 公共数据(多ADC模式)

13. 完整示例代码

13.1 场景说明

  • 使用 ADC1,读取 PA0(ADC1_IN0)上的电压
  • 单次转换模式,软件触发,轮询 EOC
  • 12 位右对齐,采样时间 56 周期
  • 同时演示如何读取内部温度传感器

13.2 完整 C++ 代码

// 文件:main_adc_demo.cpp
// 目标芯片:STM32F407VGT6(或类似 F4 系列)
// 功能1:单次读取 PA0(ADC1_IN0)电压
// 功能2:读取内部温度传感器(ADC1_IN16,F40x)
//
// 头文件:
//   stm32f4xx.h  — CMSIS 设备头文件,定义所有外设寄存器结构体
#include "stm32f4xx.h"   // 包含 STM32F4 全部寄存器定义
#include <cstdint>        // uint16_t, uint32_t 等类型
// ─────────────────────────────────────────────
// 常量定义(从数据手册获取,F407典型值)
// ─────────────────────────────────────────────
// 温度传感器校准值(单位:ADC 原始计数值)
// 实际项目应从芯片内部校准数据地址读取:
//   F40x: TS_CAL1 @ 0x1FFF7A2C (30°C), TS_CAL2 @ 0x1FFF7A2E (110°C)
// 这里用典型值近似
constexpr float V25_MV        = 760.0f;   // 25°C 时典型 VSENSE 电压(mV)
constexpr float AVG_SLOPE_UV  = 2500.0f;  // 平均斜率 2.5 mV/°C = 2500 μV/°C
constexpr float VREF_MV       = 3300.0f;  // VREF+ = 3.3V
// ─────────────────────────────────────────────
// 函数声明
// ─────────────────────────────────────────────
static void    clock_init();
static void    gpio_init();
static void    adc_init();
static uint16_t adc_read_channel(uint8_t channel);
static float   adc_to_voltage(uint16_t raw);
static float   adc_to_temperature(uint16_t raw);
// ─────────────────────────────────────────────
// 主函数
// ─────────────────────────────────────────────
int main()
{
    clock_init();   // 使能时钟
    gpio_init();    // 配置 PA0 为模拟输入
    adc_init();     // 初始化 ADC1
    while (true)
    {
        // ── 读取 PA0 外部电压 ────────────────────
        uint16_t raw_pa0 = adc_read_channel(0);     // ADC1_IN0 = PA0
        float    volt    = adc_to_voltage(raw_pa0); // 转换为实际电压(V)
        // ── 读取内部温度传感器 ───────────────────
        // F40x/F41x: 温度传感器在 ADC1_IN16
        uint16_t raw_temp = adc_read_channel(16);
        float    temp     = adc_to_temperature(raw_temp);
        // raw_pa0, volt, temp 已计算完成
        // 实际项目中:通过 UART 打印、LCD 显示等输出结果
        // 简单延迟(实际项目用 SysTick 或 HAL_Delay)
        for (volatile uint32_t i = 0; i < 1000000u; ++i) {}
    }
    return 0;
}
// ─────────────────────────────────────────────
// 时钟初始化
// ─────────────────────────────────────────────
static void clock_init()
{
    // 使能 GPIOA 时钟(PA0 作为模拟输入)
    // RCC->AHB1ENR bit0 = GPIOAEN
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    // 使能 ADC1 时钟
    // RCC->APB2ENR bit8 = ADC1EN
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;
}
// ─────────────────────────────────────────────
// GPIO 初始化
// ─────────────────────────────────────────────
static void gpio_init()
{
    // PA0 配置为模拟输入(MODER = 11,Analog模式)
    // 模拟模式下,GPIO 内部数字输入电路断开,减少噪声
    //
    // GPIOA->MODER bit[1:0](PA0)写 11 = 模拟模式
    GPIOA->MODER |= (0x3u << (0 * 2));   // bit[1:0] = 11
    // 模拟模式不需要配置 OSPEEDR、OTYPER、PUPDR
    // 默认值(无上下拉)即可
    // 注意:不需要配置内部温度传感器的引脚,
    // 温度传感器直接内部连接到 ADC1,没有外部引脚
}
// ─────────────────────────────────────────────
// ADC 初始化
// ─────────────────────────────────────────────
static void adc_init()
{
    // ── 步骤1:配置 ADC 公共控制寄存器 ADC_CCR ──
    //
    // ADC_CCR 地址 = ADC1基址 + 0x300 + 0x04
    // 通过 ADC123_COMMON->CCR 访问(CMSIS 定义)
    // ADCPRE[1:0] = 00 → PCLK2 / 2
    // 假设 PCLK2 = 84 MHz,则 ADCCLK = 42 MHz
    // 注意:F4 的 ADC 最大 ADCCLK 通常为 36 MHz,建议至少 /4
    // 这里设置 /4(ADCPRE = 01)
    ADC123_COMMON->CCR &= ~ADC_CCR_ADCPRE;            // 清除 ADCPRE 字段
    ADC123_COMMON->CCR |=  ADC_CCR_ADCPRE_0;          // ADCPRE = 01 → /4
    // 若 PCLK2 = 84 MHz,ADCCLK = 84/4 = 21 MHz(符合要求)
    // 使能温度传感器和 VREFINT(TSVREFE = 1)
    // 不使用时可以不设置(节省功耗)
    ADC123_COMMON->CCR |= ADC_CCR_TSVREFE;
    // ── 步骤2:配置 ADC1 分辨率和对齐 ──────────
    // ADC_CR1:RES[1:0] = 00 → 12位分辨率
    // SCAN = 0 → 不使用扫描模式(单通道)
    ADC1->CR1 = 0x00000000u;   // 全清,使用默认12位分辨率,右对齐
    // ADC_CR2:
    //   ALIGN = 0 → 右对齐(数据在 bit[11:0],高4位为0)
    //   EOCS  = 1 → 每次单个通道转换完成后置位 EOC
    //   CONT  = 0 → 单次转换模式
    //   DMA   = 0 → 不使用 DMA(本示例软件轮询)
    ADC1->CR2 = ADC_CR2_EOCS;  // 只置位 EOCS,其余为0
    // ── 步骤3:配置采样时间 ──────────────────────
    //
    // ADC_SMPR2 管理通道 0~9,每通道3位
    // 通道0(IN0):位[2:0],写 011 = 56 个周期
    // 56周期提供足够的采样时间,适合中等阻抗源(≤10kΩ)
    ADC1->SMPR2 &= ~(0x7u << (0 * 3));   // 清除 SMP0[2:0]
    ADC1->SMPR2 |=  (0x3u << (0 * 3));   // 写 011 = 56 周期
    // ADC_SMPR1 管理通道 10~18,每通道3位
    // 通道16(温度传感器):在 SMPR1 中
    // bit[20:18] 对应 CH16(= SMP16,偏移 = (16-10)*3 = 18 位)
    // 设置最长采样时间 480 周期(温度传感器需要较长采样时间)
    ADC1->SMPR1 &= ~(0x7u << ((16 - 10) * 3));  // 清除 SMP16[2:0]
    ADC1->SMPR1 |=  (0x7u << ((16 - 10) * 3));  // 写 111 = 480 周期
    // ── 步骤4:上电 ADC ─────────────────────────
    //
    // 设置 ADON = 1,ADC 从掉电模式唤醒
    // 注意:第一次设置 ADON,ADC 需要稳定时间(tSTAB)
    // 不会立即开始转换
    ADC1->CR2 |= ADC_CR2_ADON;
    // 等待 ADC 稳定(简单延迟,实际应参考数据手册的 tSTAB 值)
    for (volatile uint32_t i = 0; i < 10000u; ++i) {}
}
// ─────────────────────────────────────────────
// 读取指定通道的 ADC 值(单次转换,软件触发)
// 参数:channel — 通道号(0~18)
// 返回:12位原始 ADC 值(0~4095)
// ─────────────────────────────────────────────
static uint16_t adc_read_channel(uint8_t channel)
{
    // ── 步骤1:配置规则序列(只转换一个通道)───
    //
    // ADC_SQR1.L[3:0] = 0000 → 序列长度为1(只转换1个通道)
    ADC1->SQR1 &= ~ADC_SQR1_L;   // 清除 L[3:0],= 0000 表示1次转换
    // ADC_SQR3.SQ1[4:0] = channel(第1个转换的通道号)
    ADC1->SQR3 = (channel & 0x1Fu);  // SQ1 = channel
    // ── 步骤2:软件启动规则组转换 ────────────
    //
    // 写 SWSTART = 1,ADC 开始采样和转换
    // 硬件会在转换开始后自动将 SWSTART 清0
    ADC1->CR2 |= ADC_CR2_SWSTART;
    // ── 步骤3:等待 EOC 置位 ──────────────────
    //
    // EOC = 1 表示转换完成,结果已写入 ADC_DR
    // 轮询等待(实际项目建议用中断或DMA,不要死等)
    while (!(ADC1->SR & ADC_SR_EOC))
    {
        // 等待...(生产代码应加超时保护)
    }
    // ── 步骤4:读取结果 ────────────────────────
    //
    // 读取 ADC_DR 会自动清除 EOC 标志
    // DATA[15:0] 即为转换结果,右对齐时有效位在 [11:0]
    uint16_t result = static_cast<uint16_t>(ADC1->DR & 0xFFFFu);
    return result;
}
// ─────────────────────────────────────────────
// 将原始 ADC 值转换为实际电压(单位:V)
// ─────────────────────────────────────────────
static float adc_to_voltage(uint16_t raw)
{
    // V = raw_value / (2^12 - 1) × VREF+
    // 12位满量程 = 4095
    return (static_cast<float>(raw) / 4095.0f) * (VREF_MV / 1000.0f);
}
// ─────────────────────────────────────────────
// 将温度传感器 ADC 原始值转换为温度(单位:°C)
// 公式:T = (VSENSE - V25) / Avg_Slope + 25
// ─────────────────────────────────────────────
static float adc_to_temperature(uint16_t raw)
{
    // 先将原始值转为电压(mV)
    float vsense_mv = (static_cast<float>(raw) / 4095.0f) * VREF_MV;
    // 代入公式计算温度
    // AVG_SLOPE 单位 μV/°C = 0.001 mV/°C
    float temp_c = ((vsense_mv - V25_MV) / (AVG_SLOPE_UV / 1000.0f)) + 25.0f;
    return temp_c;
}

13.3 ADC 转换流程图(Mermaid)

系统上电

clock_init
使能 GPIOA 和 ADC1 时钟

gpio_init
PA0 配置为模拟模式

adc_init
配置分辨率/对齐/采样时间
ADON=1 上电

主循环

adc_read_channel 0
SQR1.L=0 SQR3.SQ1=0

SWSTART=1
启动单次转换

ADC 采样 56周期
然后逐次逼近 12周期

EOC == 1?

读取 ADC_DR
返回 raw 值 0-4095

adc_to_voltage
raw / 4095 × 3.3V

adc_read_channel 16
温度传感器

adc_to_temperature
计算结温

输出 volt 和 temp

13.4 数据对齐直观示意

输入电压 2.0V(VREF+=3.3V):
  原始值 = 2.0 / 3.3 × 4095 ≈ 2482(0x9B2)
右对齐(ALIGN=0,默认):
  ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
  │ 0  │ 0  │ 0  │ 0  │ 1  │ 0  │ 0  │ 1  │ 1  │ 0  │ 1  │ 1  │ 0  │ 0  │ 1  │ 0  │
  └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
  bit15                                                                          bit0
  直接读取 = 2482,对应 2.0V
左对齐(ALIGN=1):
  ┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
  │ 1  │ 0  │ 0  │ 1  │ 1  │ 0  │ 1  │ 1  │ 0  │ 0  │ 1  │ 0  │ 0  │ 0  │ 0  │ 0  │
  └────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
  bit15                                                                          bit0
  读取值 = 39712,需右移4位:39712 >> 4 = 2482
  取高字节(bit15:8)= 0x9B ≈ 155(近似8位结果)

附录:关键配置速查

转换时间公式

T c o n v = T s a m p l i n g + { 12 12 位 10 10 位 8 8 位 6 6 位 (单位:ADCCLK周期) T_{conv} = T_{sampling} + \begin{cases} 12 & 12\text{位} \\ 10 & 10\text{位} \\ 8 & 8\text{位} \\ 6 & 6\text{位} \end{cases} \quad \text{(单位:ADCCLK周期)} Tconv=Tsampling+ 121086121086(单位:ADCCLK周期)

ADCCLK 频率

f A D C C L K = f P C L K 2 p r e s c a l e r , p r e s c a l e r ∈ { 2 , 4 , 6 , 8 } f_{ADCCLK} = \frac{f_{PCLK2}}{prescaler}, \quad prescaler \in \{2, 4, 6, 8\} fADCCLK=prescalerfPCLK2,prescaler{2,4,6,8}

电压反推公式

V I N = A D C _ r a w 2 n − 1 × V R E F + V_{IN} = \frac{ADC\_raw}{2^{n} - 1} \times V_{REF+} VIN=2n1ADC_raw×VREF+
其中 n n n 为配置的分辨率位数(6/8/10/12)。

温度计算公式

T ° C = V S E N S E − V 25 A v g _ S l o p e + 25 T_{°C} = \frac{V_{SENSE} - V_{25}}{Avg\_Slope} + 25 T°C=Avg_SlopeVSENSEV25+25

模拟看门狗使能组合


AWDSGL AWDEN JAWDEN 监视范围
x 0 0 不监视任何通道
0 0 1 所有注入通道
0 1 0 所有规则通道
0 1 1 所有规则+注入通道
1 0 1 单个注入通道(AWDCH指定)
1 1 0 单个规则通道(AWDCH指定)
1 1 1 单个规则或注入通道

STM32F4xx DAC(数模转换器)从零理解详解

1. DAC 是什么?

DAC(Digital-to-Analog Converter,数模转换器) 的作用是把单片机内部的数字信号(0和1组成的数字)转换成外部世界能用的模拟电压。
举个生活中的例子:

数字信号(0~4095 的整数)
        |
        | DAC 转换
        v
模拟电压(0V ~ VREF+ 之间的某个连续电压)

STM32F4 的 DAC 是 12位分辨率,也就是说它能表示 2 12 = 4096 2^{12} = 4096 212=4096 个不同的电压级别。

2. 输出电压公式

DAC 的输出电压由以下公式决定:
V o u t = V R E F + × D O R 4096 V_{out} = V_{REF+} \times \frac{DOR}{4096} Vout=VREF+×4096DOR
其中:

  • V R E F + V_{REF+} VREF+:参考电压(最高不超过 VDDA,最低 1.8V)
  • D O R DOR DOR:数据输出寄存器中的值(0 ~ 4095)
  • 4096 = 2 12 4096 = 2^{12} 4096=212:12位分辨率的总级数
    例子:若 V R E F + = 3.3 V V_{REF+} = 3.3V VREF+=3.3V,想输出 1.65 V 1.65V 1.65V,则:
    D O R = 1.65 3.3 × 4096 = 2048 DOR = \frac{1.65}{3.3} \times 4096 = 2048 DOR=3.31.65×4096=2048

3. 硬件结构总览

           +------------------+
  VREF+ -->|                  |
  VDDA  -->|   DAC 模块       |--> DAC_OUT1 (PA4 引脚)
  VSSA  -->|   (两个通道)     |--> DAC_OUT2 (PA5 引脚)
           |                  |
           +------------------+
                 ^
                 |
           CPU / DMA 写数据

STM32F4 的 DAC 有 两个独立通道

  • 通道1 输出在 PA4 引脚
  • 通道2 输出在 PA5 引脚

注意:使用 DAC 之前,必须先把 PA4/PA5 配置为模拟输入模式(AIN),否则会有寄生电流消耗。

4. 主要功能一览


功能 说明
12位/8位模式 可选精度
左对齐/右对齐 数据写入方式灵活
触发转换 定时器/外部中断/软件触发
DMA支持 不占用CPU,自动搬运数据
噪声波形生成 内置LFSR伪随机噪声
三角波生成 内置三角波发生器
双通道同步 两路同时输出
输出缓冲器 直接驱动外部负载

5. 数据格式详解

写数据到 DAC 有三种方式,对应三种寄存器格式:

5.1 单通道模式(以通道1为例)

寄存器位编号:  31      15  14  13  12  11  10  9   8   7   6   5   4   3   2   1   0
               |        |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
8位右对齐:
               [  保留(全0)  ][ 保留 ][ 保留 ][D7][D6][D5][D4][D3][D2][D1][D0]
               写入 DAC_DHR8R1,数据在 [7:0]
12位左对齐:
               [  保留      ][D11][D10][D9][D8][D7][D6][D5][D4][D3][D2][D1][D0][ 保留 ]
               写入 DAC_DHR12L1,数据在 [15:4]
12位右对齐:
               [  保留                         ][D11][D10][D9][D8][D7][D6][D5][D4][D3][D2][D1][D0]
               写入 DAC_DHR12R1,数据在 [11:0]

最常用的是 12位右对齐,直接把 0~4095 的值写入即可。

6. 数据流转过程

数据从CPU写入,到最终输出电压,经历以下步骤:

CPU 写值
   |
   v
DHR 寄存器(数据保持寄存器,不可直接读写 DOR)
   |
   | (触发条件满足后转移)
   v
DOR 寄存器(数据输出寄存器)
   |
   | (经过建立时间 tSETTLING)
   v
DAC_OUTx 引脚输出电压

转移时机取决于是否启用触发(TENx 位):

TENx 触发方式 转移延迟
0 (关闭触发) 写入 DHR 后自动转移 1个 APB1 时钟周期后
1 (启用触发) 等待外部/软件触发 触发后 3个 APB1 时钟周期
1 + 软件触发 置位 SWTRIG 位 1个 APB1 时钟周期后

时序图(TEN=0,无触发):

APB1_CLK  _|‾|_|‾|_|‾|_|‾|_|‾|_
DHR        ___XXXXXXX____________
                写入 0x1AC
DOR        ________XXXXXXX_______
                  延迟1周期后更新
输出电压    ____________~~~~~~~~~~~
                       建立时间后稳定

7. 触发源选择

当 TENx=1 时,通过 TSELx[2:0] 选择触发源:

TSELx[2:0] 触发来源
000 定时器6 TRGO
001 定时器8 TRGO
010 定时器7 TRGO
011 定时器5 TRGO
100 定时器2 TRGO
101 定时器4 TRGO
110 外部中断线9
111 软件触发 SWTRIG

重要:ENx 置位(通道使能)后,TSELx 不能再修改!

8. 波形生成功能

8.1 噪声波形(LFSR 伪随机噪声)

设置 WAVEx[1:0] = 01 开启。
原理:使用一个 12 位的线性反馈移位寄存器(LFSR),每次触发后按特定算法更新,产生伪随机序列,加到 DHRx 上输出。

  • 初始值:0xAAA
  • 若 LFSR 变为 0x000,自动注入 1(防锁死机制)
  • MAMPx[3:0] 控制"掩码",决定噪声幅度(屏蔽高位 = 幅度小)
输出 = DHRx 基础值 + (LFSR & 掩码)

8.2 三角波

设置 WAVEx[1:0] = 1x 开启。

电压
  ^
  |    /\      /\      /\
  |   /  \    /  \    /  \
  |  /    \  /    \  /    \
  | /      \/      \/      \
  +----------------------------> 时间
  基础值 = DAC_DHRx
  峰值   = DAC_DHRx + MAMPx 对应的最大幅度

MAMPx[3:0] 与三角波幅度对应:

MAMPx 三角波幅度
0000 1
0001 3
0010 7
0011 15
0100 31
0101 63
0110 127
0111 255
1000 511
1001 1023
1010 2047
>=1011 4095

注意:MAMPx 必须在 DAC 使能(ENx=1)之前配置!

9. DMA 支持

DAC 支持 DMA(直接内存访问),不用 CPU 干预就能持续输出波形数据。
DMA 下溢错误(DMA Underrun)

正常情况:
  触发1 -> DMA 搬数据 -> 确认 -> 触发2 -> DMA 搬数据 -> ...
下溢情况(触发太快,DMA 来不及):
  触发1 -> DMA 搬数据(还没确认)
               触发2 来了!-> 忽略!设置 DMAUDRx 标志

发生下溢后,处理步骤:

  1. 软件写 1 清除 DMAUDRx 标志
  2. 清除 DMA 流的 DMAEN 位
  3. 重新初始化 DMA 和 DAC
  4. 降低触发频率或减轻 DMA 负载
  5. 重新使能 DMA 和触发

10. 双通道模式

两个通道可以共用一个寄存器写操作,节省总线带宽:

寄存器 地址偏移 说明
DAC_DHR12RD 0x20 双通道12位右对齐
DAC_DHR12LD 0x24 双通道12位左对齐
DAC_DHR8RD 0x28 双通道8位右对齐

双通道共有 11种工作模式,分为两大类:

双通道模式
├── 独立触发
│   ├── 无波形
│   ├── 相同 LFSR 掩码
│   ├── 不同 LFSR 掩码
│   ├── 相同三角波幅度
│   └── 不同三角波幅度
└── 同步触发
    ├── 无波形
    ├── 相同 LFSR 掩码
    ├── 不同 LFSR 掩码
    ├── 相同三角波幅度
    ├── 不同三角波幅度
    └── 软件同时启动

11. 关键寄存器速查

DAC_CR(控制寄存器,偏移 0x00)

位31~16 控制通道2,位15~0 控制通道1,结构完全对称。
以通道1为例(低16位):
位  15:14  保留
位  13     DMAUDRIE1 - DMA下溢中断使能
位  12     DMAEN1    - DMA使能
位  11:8   MAMP1[3:0]- 掩码/幅度选择
位  7:6    WAVE1[1:0]- 波形类型(00=无,01=噪声,1x=三角)
位  5:3    TSEL1[2:0]- 触发源选择
位  2      TEN1      - 触发使能
位  1      BOFF1     - 输出缓冲器禁用(1=禁用)
位  0      EN1       - 通道1使能

DAC_SR(状态寄存器,偏移 0x34)

位 29  DMAUDR2 - 通道2 DMA下溢标志(硬件置位,软件写1清除)
位 13  DMAUDR1 - 通道1 DMA下溢标志(硬件置位,软件写1清除)

12. 完整 C++ 示例代码

以下代码演示:使用定时器6触发,DMA方式,通过 DAC 通道1 输出一段正弦波。

/*
 * STM32F4 DAC + TIM6 + DMA 输出正弦波示例
 * 目标:通过 DAC 通道1(PA4)输出正弦波形
 * 编译环境:STM32CubeF4 HAL 库
 */
#include <stdint.h>
#include <math.h>
// ===== 寄存器基地址定义(也可使用 HAL/CMSIS 头文件)=====
#define RCC_BASE    0x40023800UL
#define GPIOA_BASE  0x40020000UL
#define DAC_BASE    0x40007400UL
#define DMA1_BASE   0x40026000UL
#define TIM6_BASE   0x40001000UL
// RCC 寄存器
#define RCC_AHB1ENR  (*(volatile uint32_t*)(RCC_BASE  + 0x30))
#define RCC_APB1ENR  (*(volatile uint32_t*)(RCC_BASE  + 0x40))
// GPIOA 寄存器
#define GPIOA_MODER  (*(volatile uint32_t*)(GPIOA_BASE + 0x00))
// DAC 寄存器
#define DAC_CR       (*(volatile uint32_t*)(DAC_BASE + 0x00))
#define DAC_SWTRIGR  (*(volatile uint32_t*)(DAC_BASE + 0x04))
#define DAC_DHR12R1  (*(volatile uint32_t*)(DAC_BASE + 0x08))  // 通道1 12位右对齐数据保持寄存器
#define DAC_DOR1     (*(volatile uint32_t*)(DAC_BASE + 0x2C))  // 通道1 输出寄存器(只读)
// TIM6 寄存器
#define TIM6_CR1     (*(volatile uint32_t*)(TIM6_BASE + 0x00))
#define TIM6_CR2     (*(volatile uint32_t*)(TIM6_BASE + 0x04))
#define TIM6_DIER    (*(volatile uint32_t*)(TIM6_BASE + 0x0C))
#define TIM6_PSC     (*(volatile uint32_t*)(TIM6_BASE + 0x28))
#define TIM6_ARR     (*(volatile uint32_t*)(TIM6_BASE + 0x2C))
// DMA1 Stream5 寄存器(DAC CH1 使用 DMA1 Stream5 Channel7)
#define DMA1_S5CR    (*(volatile uint32_t*)(DMA1_BASE + 0x088))
#define DMA1_S5NDTR  (*(volatile uint32_t*)(DMA1_BASE + 0x08C))
#define DMA1_S5PAR   (*(volatile uint32_t*)(DMA1_BASE + 0x090))
#define DMA1_S5M0AR  (*(volatile uint32_t*)(DMA1_BASE + 0x094))
// ===== 正弦波查找表(256点,12位分辨率)=====
// 值范围 0~4095,中心在 2048
#define SINE_TABLE_SIZE 256
static uint16_t sine_table[SINE_TABLE_SIZE];
/*
 * 生成正弦波查找表
 * 公式:val = 2048 + 2047 * sin(2π * i / N)
 * 其中 N = SINE_TABLE_SIZE
 */
void generate_sine_table(void) {
    for (int i = 0; i < SINE_TABLE_SIZE; i++) {
        // 计算正弦值,范围 -1.0 ~ +1.0
        double angle = 2.0 * 3.14159265358979 * i / SINE_TABLE_SIZE;
        // 映射到 0 ~ 4095(12位DAC满量程)
        sine_table[i] = (uint16_t)(2048.0 + 2047.0 * sin(angle));
    }
}
/*
 * 配置 PA4 为模拟模式(DAC_OUT1 复用)
 * GPIOA_MODER 的 [9:8] 位控制 PA4
 * 11 = 模拟模式
 */
void dac_gpio_init(void) {
    RCC_AHB1ENR |= (1 << 0);              // 使能 GPIOA 时钟(AHB1 第0位)
    // PA4 设置为模拟模式:MODER[9:8] = 11
    GPIOA_MODER |= (3 << 8);              // 写入 11,设为模拟模式
}
/*
 * 配置 TIM6 产生 TRGO 触发信号,用于触发 DAC 转换
 * 目标:产生频率 = APB1_CLK / ((PSC+1) * (ARR+1)) 的触发信号
 * 例:APB1=42MHz, PSC=0, ARR=163 => 触发频率 ≈ 256kHz(256点波形时输出1kHz正弦波)
 */
void tim6_init(void) {
    RCC_APB1ENR |= (1 << 4);              // 使能 TIM6 时钟(APB1 第4位)
    TIM6_CR1  = 0;                         // 先清零
    TIM6_PSC  = 0;                         // 预分频器 = 0(不分频)
    TIM6_ARR  = 163;                       // 自动重载值,决定触发频率
    // CR2[6:4] = MMS[2:0],设为 010 = Update 事件作为 TRGO 输出
    TIM6_CR2  = (2 << 4);
    TIM6_CR1 |= (1 << 0);                  // CEN=1,启动定时器
}
/*
 * 配置 DMA1 Stream5,将正弦波表循环传输到 DAC_DHR12R1
 * 参数含义:
 *   外设地址(PAR)= DAC_DHR12R1 地址
 *   内存地址(M0AR)= sine_table 数组地址
 *   传输数量(NDTR)= SINE_TABLE_SIZE
 *   方向:内存 -> 外设
 *   循环模式:开启(循环播放正弦波)
 *   内存地址自增:开启
 *   外设地址固定不变
 *   数据宽度:半字(16位)
 */
void dma_init(void) {
    RCC_AHB1ENR |= (1 << 21);             // 使能 DMA1 时钟(AHB1 第21位)
    DMA1_S5CR = 0;                         // 先禁用 Stream5
    // 外设地址 = DAC_DHR12R1 寄存器
    DMA1_S5PAR  = (uint32_t)(&DAC_DHR12R1);
    // 内存地址 = 正弦波查找表首地址
    DMA1_S5M0AR = (uint32_t)(sine_table);
    // 传输数量 = 表的大小
    DMA1_S5NDTR = SINE_TABLE_SIZE;
    /*
     * 配置 DMA Stream5 控制寄存器:
     * 位 [27:25] CHSEL = 111 (Channel 7,DAC CH1 使用)
     * 位 [16]    MINC  = 1   (内存地址自增)
     * 位 [15:13] MSIZE = 01  (内存数据宽度 16位)
     * 位 [12:11] PSIZE = 01  (外设数据宽度 16位)
     * 位 [10]    CIRC  = 1   (循环模式)
     * 位 [6]     DIR   = 01  (内存 -> 外设方向)
     */
    DMA1_S5CR = (7    << 25) |   // Channel 7
                (1    << 16) |   // 内存地址自增
                (0x01 << 13) |   // 内存数据 16位
                (0x01 << 11) |   // 外设数据 16位
                (1    << 10) |   // 循环模式
                (0x01 << 6);     // 方向:内存->外设
    DMA1_S5CR |= (1 << 0);       // EN=1,使能 DMA Stream5
}
/*
 * 配置 DAC 通道1
 * 使能触发(TEN1=1),触发源选 TIM6 TRGO(TSEL1=000)
 * 使能 DMA(DMAEN1=1)
 * 使能通道(EN1=1)
 */
void dac_init(void) {
    RCC_APB1ENR |= (1 << 29);    // 使能 DAC 时钟(APB1 第29位)
    /*
     * DAC_CR 配置:
     * DMAEN1 [12] = 1   使能 DMA
     * TSEL1  [5:3] = 000  触发源 = TIM6 TRGO
     * TEN1   [2]  = 1   使能触发
     * BOFF1  [1]  = 0   使能输出缓冲器(降低输出阻抗)
     * EN1    [0]  = 1   使能 DAC 通道1
     */
    DAC_CR = (1 << 12) |     // DMAEN1
             (0 << 3)  |     // TSEL1 = 000(Timer6 TRGO)
             (1 << 2)  |     // TEN1
             (0 << 1)  |     // BOFF1=0,启用输出缓冲
             (1 << 0);       // EN1
}
/*
 * 主函数
 */
int main(void) {
    // 1. 先生成正弦波查找表
    generate_sine_table();
    // 2. 配置 GPIO(PA4 设为模拟模式)
    dac_gpio_init();
    // 3. 配置 DMA(准备好数据搬运通道)
    dma_init();
    // 4. 配置 DAC(使能通道1,绑定触发和DMA)
    dac_init();
    // 5. 配置并启动 TIM6(产生触发信号,驱动DAC转换)
    tim6_init();
    // 到这里,DAC 就在 PA4 引脚上持续输出正弦波了
    // 频率 = TIM6触发频率 / 正弦表点数
    //       = 256000Hz / 256 = 1000Hz = 1kHz
    while (1) {
        // CPU 可以做其他事,DAC 由 DMA+定时器自动驱动
    }
    return 0;
}

13. 数据流向总图(Mermaid)

触发条件满足
(无触发:1周期
有触发:3周期)

经过建立时间
tSETTLING

触发信号

叠加到数据上

CPU / DMA 写入数据

DHR 数据保持寄存器
(DAC_DHR12R1 等)

DOR 数据输出寄存器
(DAC_DOR1)

DAC_OUT 引脚
(PA4/PA5)输出模拟电压

定时器 TIM6 TRGO

LFSR 噪声发生器
或三角波计数器

14. 寄存器地址速查表


寄存器 偏移地址 复位值 说明
DAC_CR 0x00 0x00000000 控制寄存器
DAC_SWTRIGR 0x04 0x00000000 软件触发寄存器
DAC_DHR12R1 0x08 0x00000000 通道1 12位右对齐
DAC_DHR12L1 0x0C 0x00000000 通道1 12位左对齐
DAC_DHR8R1 0x10 0x00000000 通道1 8位右对齐
DAC_DHR12R2 0x14 0x00000000 通道2 12位右对齐
DAC_DHR12L2 0x18 0x00000000 通道2 12位左对齐
DAC_DHR8R2 0x1C 0x00000000 通道2 8位右对齐
DAC_DHR12RD 0x20 0x00000000 双通道12位右对齐
DAC_DHR12LD 0x24 0x00000000 双通道12位左对齐
DAC_DHR8RD 0x28 0x00000000 双通道8位右对齐
DAC_DOR1 0x2C 0x00000000 通道1 输出(只读)
DAC_DOR2 0x30 0x00000000 通道2 输出(只读)
DAC_SR 0x34 0x00000000 状态寄存器

15. 常见使用步骤总结

初始化 DAC 通道1 基本步骤(不用触发,不用DMA):
1. 使能 GPIOA 时钟,配置 PA4 为模拟模式(MODER[9:8] = 11)
2. 使能 DAC 时钟(RCC APB1ENR DAC bit)
3. 配置 DAC_CR:
   - EN1 = 1          (使能通道1)
   - BOFF1 = 0        (启用输出缓冲器)
   - TEN1 = 0         (不使用触发,写入即生效)
4. 写值到 DAC_DHR12R1(范围 0~4095)
5. 等待 tSETTLING 后,PA4 输出对应电压
输出电压 = VREF+ × (写入值 / 4096)

STM32F4xx DCMI(数字摄像头接口)从零理解详解

1. DCMI 是什么?

DCMI(Digital Camera Interface,数字摄像头接口) 是 STM32F4 内置的一个专用硬件外设,它的作用是从外部摄像头模块高速接收图像数据,存入内存,供 CPU 或显示模块使用。
生活类比:

外部摄像头模块
   (一直在拍照,每秒产生大量像素数据)
           |
           | 并行数据线(8~14根)
           v
        DCMI 接口
   (负责接收、打包、送给DMA)
           |
           | DMA 自动搬运
           v
        内存(SRAM/SDRAM)
   (存好的图像帧,供后续处理)

关键特点:

  • 最高支持 54 MB/s 的数据传输速率
  • 支持 8/10/12/14 位并行数据宽度
  • 支持 YCbCr、RGB565、JPEG 等格式
  • 图像最大尺寸 2048×2048 像素(JPEG无限制)

2. DCMI 引脚说明

摄像头模块                STM32F4 DCMI
   DATA[0:13] ---------> D[0:13]   (数据线,8/10/12/14根)
   HSYNC      ---------> HSYNC     (行同步,表示一行开始/结束)
   VSYNC      ---------> VSYNC     (帧同步,表示一帧开始/结束)
   PIXCLK     ---------> PIXCLK    (像素时钟,每个时钟锁存一次数据)

信号名 方向 说明
D[0:13] 输入 像素数据线(根据位宽使用不同数量)
HSYNC 输入 行同步信号,标识一行数据的有效区间
VSYNC 输入 帧同步信号,标识一帧图像的有效区间
PIXCLK 输入 像素时钟,数据在其边沿锁存

3. 时钟关系

DCMI 使用两个时钟域:

PIXCLK(来自摄像头)    HCLK(STM32内部AHB时钟)
PIXCLK 周期 > 2.5 × HCLK 周期
原因:PIXCLK 产生的数据需要在 HCLK 域中被稳定采样,
      必须留出足够的建立时间。

换句话说:摄像头时钟不能比 MCU 总线时钟快太多,否则数据还没稳定就被采走了。

4. 整体数据流

摄像头模块
PIXCLK/HSYNC/VSYNC/D[0:13]

同步器
(硬件同步 or 嵌入式同步码)

4字 FIFO
数据格式化

DMA 接口
每凑满32位触发一次

内存
(图像帧缓冲区)

CPU 处理 / 显示

5. 数据位宽与打包方式

DCMI 总是把数据打包成 32位字 再交给 DMA。不同位宽的打包方式不同:

5.1 8位模式(EDM[1:0] = 00)

每个像素时钟采集 8 位,4个像素 组成一个 32 位字。

像素时钟:   CLK1      CLK2      CLK3      CLK4
采集数据:   Dn[7:0]   Dn+1[7:0] Dn+2[7:0] Dn+3[7:0]
32位字布局(地址 0):
  位[31:24]  位[23:16]  位[15:8]   位[7:0]
   Dn+3       Dn+2       Dn+1       Dn      <- 先收到的放低位

5.2 10位模式(EDM[1:0] = 01)

每个像素时钟采集 10 位,存入 16 位槽(高 6 位补 0),2个像素 组成一个 32 位字。

32位字布局(地址 0):
  位[31:26]  位[25:16]   位[15:10]  位[9:0]
    0(补零)   Dn+1[9:0]   0(补零)   Dn[9:0]

5.3 12位模式(EDM[1:0] = 10)

每个像素时钟采集 12 位,存入 16 位槽(高 4 位补 0),2个像素 组成一个 32 位字。

32位字布局(地址 0):
  位[31:28]  位[27:16]    位[15:12]  位[11:0]
    0(补零)   Dn+1[11:0]   0(补零)   Dn[11:0]

5.4 14位模式(EDM[1:0] = 11)

每个像素时钟采集 14 位,存入 16 位槽(高 2 位补 0),2个像素 组成一个 32 位字。

32位字布局(地址 0):
  位[31:30]  位[29:16]    位[15:14]  位[13:0]
    0(补零)   Dn+1[13:0]   0(补零)   Dn[13:0]

6. 同步方式

DCMI 支持两种同步方式,决定了如何知道"一行开始了"、“一帧结束了”:

6.1 硬件同步模式(ESS = 0,默认)

使用独立的 HSYNC 和 VSYNC 引脚:

VSYNC  ‾‾‾‾‾‾‾|___________________________|‾‾‾‾‾
               ^帧开始                     ^帧结束
HSYNC  ‾|__|‾‾‾‾|__|‾‾‾‾|__|‾‾‾‾|__|‾‾‾‾
         行1    行2     行3    行4...
PIXCLK 在HSYNC有效期间持续提供,每个边沿采一个像素
  • VSYNC 有效期间的数据 = 一帧有效图像
  • HSYNC 有效期间的数据 = 一行有效像素
  • 同步期间收到的数据自动忽略

6.2 嵌入式同步模式(ESS = 1,仅8位模式)

不用 HSYNC/VSYNC 引脚,改用数据流中插入特殊同步码
同步码格式固定为:0xFF 0x00 0x00 XY,其中 XY 由寄存器配置:

事件 寄存器字段 说明
帧开始 (FS) FSC 0xFF0000FSC
帧结束 (FE) FEC 0xFF0000FEC
行开始 (LS) LSC 0xFF0000LSC
行结束 (LE) LEC 0xFF0000LEC

注意:嵌入式同步模式下,数据中禁止出现 0x00 和 0xFF,这两个值只用于同步码!

7. 捕获模式

7.1 快照模式(CM = 1)

只拍一张照片:

设置 CAPTURE = 1
        |
        v
等待帧开始(VSYNC/FS)
        |
        v
接收完整一帧数据 → DMA 搬到内存
        |
        v
CAPTURE 位自动清零(硬件清除)
触发 IT_FRAME 中断

适用场景:按需拍照、节省内存。

7.2 连续抓取模式(CM = 0)

持续不停地拍:

设置 CAPTURE = 1
        |
        v
连续接收帧数据
帧1 → DMA → 内存缓冲区
帧2 → DMA → 内存缓冲区(或循环覆盖)
帧3 → ...
        |
        v
直到软件主动清除 CAPTURE 位
(清零后当前帧接收完才停止)

还可以通过 FCRC[1:0] 降低帧率:

FCRC 捕获策略 带宽节省
00 每帧都捕获 0%
01 每隔一帧捕获 50%
10 每4帧捕获1帧 75%
11 保留 -

8. 裁剪功能(Crop)

可以只采集图像中的一个矩形子区域,不需要整帧:

完整图像(例如 640×480):
+------------------------------------------+
|                                          |
|    (HOFFCNT, VST)                        |
|         +-----------+                   |
|         |  裁剪区域  | VLINE 行          |
|         |  CAPCNT列 |                   |
|         +-----------+                   |
|                                          |
+------------------------------------------+

参数 寄存器字段 说明
起始列偏移(像素时钟数) HOFFCNT 从行开始跳过多少个时钟
起始行号 VST 从第几行开始(0=第1行)
捕获宽度(像素时钟数) CAPCNT 捕获多少个像素时钟(必须是4的倍数)
捕获行数 VLINE 捕获多少行(0=1行)

CAPCNT 必须是4的倍数,原因是 DMA 以32位对齐传输。

9. JPEG 模式

JPEG 数据不是规则的行列格式,需要特殊处理:

普通图像模式:
  VSYNC = 帧边界,HSYNC = 行边界
JPEG 模式(JPEG = 1):
  VSYNC = 整张JPEG图像的开始/结束标志
  HSYNC = 数据有效信号(Data Enable)

JPEG 特殊规则:

  • 不能使用裁剪功能(CROP=1 无效)
  • 不能使用嵌入式同步(ESS=1 无效)
  • 帧末尾不足32位时,自动用0补齐,然后触发DMA

10. FIFO 与 DMA

摄像头数据                     内存
(PIXCLK边沿)                 (最终目的地)
每个像素 → 数据格式化器 → [FIFO] → DMA → RAM
                            4个32位字深度
                            满32位 = 触发一次DMA请求

FIFO 作用:在 DMA 还没来得及响应时,临时缓冲数据,避免丢失。但 FIFO 只有 4 个字深,若 DMA 长时间不响应:

FIFO 溢出(Overrun)
    → FIFO 被复位清空
    → 等待下一个帧开始
    → 触发 IT_OVR 中断

11. 中断系统

IT_LINE
行接收完成

IT_DCMI
(所有中断 OR)

IT_FRAME
帧接收完成

IT_OVR
数据溢出

IT_VSYNC
VSYNC 边沿

IT_ERR
嵌入式同步错误

NVIC
中断控制器

中断寄存器三层结构:

原始状态 DCMI_RIS(硬件置位)
          &
中断使能  DCMI_IER(软件配置)
          ↓
屏蔽后状态 DCMI_MIS = RIS & IER(实际触发中断)
          ↓
清除中断   写1到 DCMI_ICR 对应位

12. 数据格式详解

12.1 灰度图(Monochrome)

  • 每像素 8 位
  • 4个像素组成一个32位字
  • 按光栅扫描顺序存储(从左到右,从上到下)

12.2 RGB565

  • 每像素 16 位:红5位 + 绿6位 + 蓝5位
  • 2个像素组成一个32位字
32位字结构:
  [31:27] R(n+1)  [26:21] G(n+1)  [20:16] B(n+1)
  [15:11] R(n)    [10:5]  G(n)    [4:0]   B(n)

12.3 YCbCr 4:2:2

  • Y = 亮度(每像素一个),Cb/Cr = 色度(两像素共用一组)
  • 存储顺序:Cb Y Cr Y Cb Y Cr Y …(交错存储)
32位字结构(地址0):
  [31:24] Y(n+1)  [23:16] Cr(n)  [15:8] Y(n)  [7:0] Cb(n)

13. 关键寄存器速查

DCMI_CR(控制寄存器,偏移 0x00)

位 14      ENABLE   - DCMI 总使能(先配好其他位再置1)
位 11:10   EDM[1:0] - 数据位宽(00=8位, 01=10位, 10=12位, 11=14位)
位 9:8     FCRC[1:0]- 帧率控制(仅连续模式有效)
位 7       VSPOL    - VSYNC 极性(0=低有效, 1=高有效)
位 6       HSPOL    - HSYNC 极性(0=低有效, 1=高有效)
位 5       PCKPOL   - 像素时钟极性(0=下降沿, 1=上升沿采数据)
位 4       ESS      - 同步方式(0=硬件HSYNC/VSYNC, 1=嵌入式同步码)
位 3       JPEG     - JPEG模式使能
位 2       CROP     - 裁剪功能使能
位 1       CM       - 捕获模式(0=连续, 1=快照)
位 0       CAPTURE  - 开始捕获

14. 完整 C++ 示例代码

以下代码演示:硬件同步模式 + 连续抓取 + DMA,接收 RGB565 格式,320×240 分辨率

/*
 * STM32F4 DCMI + DMA 接收摄像头图像示例
 * 格式:RGB565,320×240,硬件同步
 * 编译环境:STM32CubeF4 HAL 库 + CMSIS
 */
#include <stdint.h>
#include <string.h>
// ===== 寄存器基地址 =====
#define RCC_BASE     0x40023800UL
#define DCMI_BASE    0x50050000UL
#define DMA2_BASE    0x40026400UL  // DCMI 使用 DMA2 Stream1 Channel1
// RCC 寄存器
#define RCC_AHB1ENR  (*(volatile uint32_t*)(RCC_BASE + 0x30))
#define RCC_AHB2ENR  (*(volatile uint32_t*)(RCC_BASE + 0x34))
// DCMI 寄存器
#define DCMI_CR      (*(volatile uint32_t*)(DCMI_BASE + 0x00))  // 控制寄存器
#define DCMI_SR      (*(volatile uint32_t*)(DCMI_BASE + 0x04))  // 状态寄存器
#define DCMI_RIS     (*(volatile uint32_t*)(DCMI_BASE + 0x08))  // 原始中断状态
#define DCMI_IER     (*(volatile uint32_t*)(DCMI_BASE + 0x0C))  // 中断使能
#define DCMI_MIS     (*(volatile uint32_t*)(DCMI_BASE + 0x10))  // 屏蔽后中断状态
#define DCMI_ICR     (*(volatile uint32_t*)(DCMI_BASE + 0x14))  // 中断清除(写1清除)
#define DCMI_ESCR    (*(volatile uint32_t*)(DCMI_BASE + 0x18))  // 嵌入式同步码寄存器
#define DCMI_ESUR    (*(volatile uint32_t*)(DCMI_BASE + 0x1C))  // 嵌入式同步掩码
#define DCMI_CWSTRT  (*(volatile uint32_t*)(DCMI_BASE + 0x20))  // 裁剪窗口起始
#define DCMI_CWSIZE  (*(volatile uint32_t*)(DCMI_BASE + 0x24))  // 裁剪窗口大小
#define DCMI_DR      (*(volatile uint32_t*)(DCMI_BASE + 0x28))  // 数据寄存器(只读)
// DMA2 Stream1 寄存器(DCMI 使用 DMA2 Stream1 Channel1)
#define DMA2_S1CR    (*(volatile uint32_t*)(DMA2_BASE + 0x028))
#define DMA2_S1NDTR  (*(volatile uint32_t*)(DMA2_BASE + 0x02C))
#define DMA2_S1PAR   (*(volatile uint32_t*)(DMA2_BASE + 0x030))
#define DMA2_S1M0AR  (*(volatile uint32_t*)(DMA2_BASE + 0x034))
#define DMA2_S1M1AR  (*(volatile uint32_t*)(DMA2_BASE + 0x038))  // 双缓冲区缓冲1
#define DMA2_LIFCR   (*(volatile uint32_t*)(DMA2_BASE + 0x008))  // 低流中断标志清除
// ===== 图像参数 =====
#define IMG_WIDTH    320
#define IMG_HEIGHT   240
// RGB565: 每像素2字节,2像素一个32位字
// 每帧总字数 = 320 * 240 * 2 / 4 = 38400 个32位字
#define IMG_WORDS    (IMG_WIDTH * IMG_HEIGHT * 2 / 4)
// ===== 帧缓冲区(双缓冲,一个接收时另一个供CPU读)=====
// __attribute__((aligned(4))) 保证4字节对齐,DMA要求
static uint32_t frame_buf0[IMG_WORDS] __attribute__((aligned(4)));
static uint32_t frame_buf1[IMG_WORDS] __attribute__((aligned(4)));
// 当前供CPU使用的缓冲区索引(0或1)
volatile uint8_t current_frame = 0;
// 新帧就绪标志,CPU轮询或在中断里处理
volatile uint8_t frame_ready = 0;
/*
 * 配置 DCMI 相关的 GPIO 引脚为复用功能(AF13 = DCMI)
 * 实际项目中需要根据具体硬件连接配置所有用到的引脚
 * 这里仅示意配置流程,以 PIXCLK(PA6) 为例
 */
void dcmi_gpio_init(void) {
    // 使能 GPIOA/B/C/E 时钟(根据实际引脚分布)
    RCC_AHB1ENR |= (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4);
    /*
     * 典型 DCMI 引脚分配(STM32F407):
     * PIXCLK -> PA6  (AF13)
     * HSYNC  -> PA4  (AF13)
     * VSYNC  -> PB7  (AF13)
     * D0     -> PC6  (AF13)
     * D1     -> PC7  (AF13)
     * D2     -> PE0  (AF13)
     * D3     -> PE1  (AF13)
     * D4     -> PE4  (AF13)
     * D5     -> PB6  (AF13)
     * D6     -> PE5  (AF13)
     * D7     -> PE6  (AF13)
     *
     * 每个引脚都需要:
     * 1. MODER[1:0] = 10(复用功能模式)
     * 2. OSPEEDR = 11(最高速)
     * 3. AFRx 对应字段 = 13(AF13)
     *
     * 此处省略每个引脚的具体配置代码,
     * 实际项目中用 HAL_GPIO_Init 或按手册逐位配置
     */
}
/*
 * 配置 DMA2 Stream1(DCMI 专用)
 * 功能:将 DCMI_DR 中的32位数据搬到帧缓冲区
 * 使用双缓冲模式(DBM),自动在两块缓冲区间切换
 */
void dcmi_dma_init(void) {
    // 使能 DMA2 时钟(AHB1 第22位)
    RCC_AHB1ENR |= (1 << 22);
    // 先禁用 Stream,等待完全停止
    DMA2_S1CR = 0;
    while (DMA2_S1CR & 1) {}  // 等待 EN 位清零
    // 清除 Stream1 的所有中断标志(LIFCR 低流,Stream1 对应位 [11:6])
    DMA2_LIFCR = (0x3F << 6);
    // 外设地址 = DCMI_DR(只读数据寄存器)
    DMA2_S1PAR  = (uint32_t)(&DCMI_DR);
    // 两个内存缓冲区地址(双缓冲模式)
    DMA2_S1M0AR = (uint32_t)(frame_buf0);  // 缓冲区0
    DMA2_S1M1AR = (uint32_t)(frame_buf1);  // 缓冲区1
    // 传输数量 = 一帧图像的32位字数
    DMA2_S1NDTR = IMG_WORDS;
    /*
     * 配置 DMA2 Stream1 控制寄存器:
     * 位 [27:25] CHSEL = 001  Channel 1(DCMI对应DMA2 Stream1 CH1)
     * 位 [18]    DBM   = 1    双缓冲模式(自动在M0AR/M1AR间切换)
     * 位 [16]    MINC  = 1    内存地址自增
     * 位 [15:13] MSIZE = 10   内存数据宽度 32位
     * 位 [12:11] PSIZE = 10   外设数据宽度 32位
     * 位 [10]    CIRC  = 1    循环模式(每帧后自动重置)
     * 位 [9:8]   PL    = 11   优先级最高
     * 位 [6]     DIR   = 00   外设 -> 内存方向
     * 位 [4]     TCIE  = 1    传输完成中断使能(每帧完成触发)
     */
    DMA2_S1CR = (1    << 25) |  // CHSEL = 1
                (1    << 18) |  // DBM 双缓冲
                (1    << 16) |  // MINC 内存地址自增
                (0x02 << 13) |  // MSIZE 32位
                (0x02 << 11) |  // PSIZE 32位
                (1    << 10) |  // CIRC 循环模式
                (0x03 << 8)  |  // 最高优先级
                (0x00 << 6)  |  // 方向:外设->内存
                (1    << 4);    // 传输完成中断
    // 使能 Stream1
    DMA2_S1CR |= (1 << 0);
}
/*
 * 配置并使能 DCMI
 * 模式:硬件同步,连续抓取,8位数据(RGB565每像素用2个时钟),
 *       VSYNC高有效,HSYNC低有效,PIXCLK上升沿采样
 */
void dcmi_init(void) {
    // 使能 DCMI 时钟(AHB2 第0位)
    RCC_AHB2ENR |= (1 << 0);
    /*
     * 配置 DCMI_CR:
     * ENABLE [14] = 0     先不使能,配置完成后再开
     * EDM    [11:10] = 00 8位数据模式
     * FCRC   [9:8]  = 00  每帧都捕获
     * VSPOL  [7]    = 1   VSYNC 高电平有效
     * HSPOL  [6]    = 0   HSYNC 低电平有效
     * PCKPOL [5]    = 1   上升沿采样像素数据
     * ESS    [4]    = 0   硬件同步模式(使用HSYNC/VSYNC引脚)
     * JPEG   [3]    = 0   非JPEG模式
     * CROP   [2]    = 0   不裁剪,接收完整帧
     * CM     [1]    = 0   连续抓取模式
     * CAPTURE[0]    = 0   先不开始捕获
     */
    DCMI_CR = (0 << 14) |  // ENABLE = 0(稍后置1)
              (0 << 10) |  // EDM = 00(8位)
              (0 << 8)  |  // FCRC = 00(全帧)
              (1 << 7)  |  // VSPOL = 1(高有效)
              (0 << 6)  |  // HSPOL = 0(低有效)
              (1 << 5)  |  // PCKPOL = 1(上升沿)
              (0 << 4)  |  // ESS = 0(硬件同步)
              (0 << 3)  |  // JPEG = 0
              (0 << 2)  |  // CROP = 0
              (0 << 1)  |  // CM = 0(连续)
              (0 << 0);    // CAPTURE = 0
    // 使能帧完成中断和溢出中断
    DCMI_IER = (1 << 0) |  // FRAME_IE:帧完成中断
               (1 << 1);   // OVR_IE:溢出中断
    // 使能 DCMI
    DCMI_CR |= (1 << 14);  // ENABLE = 1
    // 开始捕获
    DCMI_CR |= (1 << 0);   // CAPTURE = 1
}
/*
 * DCMI 中断服务函数
 * 每当一帧图像接收完成,DMA双缓冲自动切换,
 * 这里通知 CPU 新帧已就绪
 */
void DCMI_IRQHandler(void) {
    // 检查帧完成中断(FRAME_MIS,位0)
    if (DCMI_MIS & (1 << 0)) {
        // 清除中断标志(写1到 DCMI_ICR 的 FRAME_ISC 位)
        DCMI_ICR = (1 << 0);
        /*
         * DMA 双缓冲模式:
         * DMA 刚刚写完的缓冲区是 M0AR 还是 M1AR?
         * 检查 DMA2 S1CR 的 CT 位(Current Target):
         *   CT = 0 意味着 DMA 现在正在写 M0AR,
         *        所以刚写完的是 M1AR(缓冲区1)
         *   CT = 1 意味着 DMA 现在正在写 M1AR,
         *        所以刚写完的是 M0AR(缓冲区0)
         */
        if (DMA2_S1CR & (1 << 19)) {  // CT = 1,DMA当前目标是M1
            current_frame = 0;          // 刚写完的是缓冲区0,供CPU读
        } else {
            current_frame = 1;          // 刚写完的是缓冲区1,供CPU读
        }
        frame_ready = 1;  // 通知主循环有新帧可以处理
    }
    // 检查溢出中断(OVR_MIS,位1)
    if (DCMI_MIS & (1 << 1)) {
        DCMI_ICR = (1 << 1);  // 清除溢出中断标志
        // 溢出时 FIFO 已经被硬件自动复位
        // 等待下一帧开始即可自动恢复
        // 实际项目中可在此计溢出次数,评估性能瓶颈
    }
}
/*
 * 获取当前有效帧缓冲区的指针
 * 返回刚刚接收完成的那块缓冲区的地址
 */
const uint32_t* dcmi_get_frame(void) {
    if (current_frame == 0) {
        return frame_buf0;
    } else {
        return frame_buf1;
    }
}
/*
 * 主函数
 */
int main(void) {
    // 1. 配置 GPIO(所有 DCMI 相关引脚设为 AF13)
    dcmi_gpio_init();
    // 2. 配置并启动 DMA(先于 DCMI 配置)
    dcmi_dma_init();
    // 3. 配置并使能 DCMI,开始连续抓帧
    dcmi_init();
    // 4. 使能 DCMI 全局中断(在 NVIC 中设置,此处示意)
    // NVIC_EnableIRQ(DCMI_IRQn);  // 使用 CMSIS 头文件时调用此函数
    // 主循环:等待新帧,处理图像
    while (1) {
        if (frame_ready) {
            frame_ready = 0;
            // 获取刚接收完的帧数据指针
            const uint32_t* p_frame = dcmi_get_frame();
            /*
             * 此处可对 p_frame 指向的图像数据做处理,例如:
             * - 显示到 LCD
             * - 做颜色识别
             * - 传给图像压缩算法
             * 
             * 注意:操作期间 DMA 已经在向另一块缓冲区写下一帧,
             * 所以处理必须在下一帧到来之前完成,否则又会丢帧。
             */
            (void)p_frame;  // 防止编译器 unused 警告
        }
    }
    return 0;
}

15. 裁剪功能使用示例

假设摄像头输出 640×480,只想要中心 320×240 区域:

左上角坐标:
  水平偏移 HOFFCNT = (640 - 320) / 2 = 160 个像素时钟
  垂直偏移 VST     = (480 - 240) / 2 = 120 行(从0计,写119)
窗口大小:
  CAPCNT = 320 - 1 = 319(像素时钟数,实际捕获320个,写319)
            注意:必须是4的倍数,319不是,需向上取到 319→应用中 CAPCNT=(320*2/4-1)
            对于RGB565,每像素2字节,320像素=640字节=160个32位字
            CAPCNT 指像素时钟数(即640),写 639
  VLINE  = 240 - 1 = 239(行数)

代码配置:

// 开启裁剪之前,先关闭 CAPTURE
// DCMI_CWSTRT:高16位是 VST,低16位是 HOFFCNT
DCMI_CWSTRT = (119 << 16) | 160;   // 从第120行、第160个像素时钟开始
// DCMI_CWSIZE:高16位是 VLINE,低16位是 CAPCNT
// CAPCNT 必须是4的整数倍(因为DMA 32位对齐)
// RGB565 模式:320像素 × 2字节/像素 = 640字节 = 160个32位字
// CAPCNT 表示像素时钟数,RGB565下一像素需2个时钟,故 CAPCNT = 320*2 - 1 = 639
// 但CAPCNT[1:0]强制为0,即 639 & ~3 = 636
DCMI_CWSIZE = (239 << 16) | 636;
// 在 DCMI_CR 中置位 CROP = 1
DCMI_CR |= (1 << 2);

16. 同步信号时序示意(ASCII)

硬件同步模式下,一帧图像的信号时序:
VSYNC  高|         低(有效帧期间)         |高
         |                                   |
HSYNC  __|‾‾行1有效‾‾|__|‾‾行2有效‾‾|__|‾‾行3..
         :                                   :
PIXCLK   ‾|_|‾|_|‾|_|‾ ...每个上升沿锁一个像素
         :                                   :
DATA   --<像素1><像素2><像素3>...          ---
        ^VSYNC下降沿             ^VSYNC上升沿
         帧开始                   帧结束

17. 寄存器速查表


寄存器 偏移 访问 说明
DCMI_CR 0x00 读写 控制寄存器
DCMI_SR 0x04 只读 状态(FNE/VSYNC/HSYNC)
DCMI_RIS 0x08 只读 原始中断状态
DCMI_IER 0x0C 读写 中断使能
DCMI_MIS 0x10 只读 屏蔽后中断状态
DCMI_ICR 0x14 只写 中断清除(写1清除)
DCMI_ESCR 0x18 读写 嵌入式同步码
DCMI_ESUR 0x1C 读写 嵌入式同步掩码
DCMI_CWSTRT 0x20 读写 裁剪窗口起点
DCMI_CWSIZE 0x24 读写 裁剪窗口大小
DCMI_DR 0x28 只读 32位数据输出

18. 初始化顺序总结

正确的初始化顺序(顺序错误会导致数据丢失或死锁):
步骤1:配置所有 DCMI 相关 GPIO 为 AF13 模拟输入
步骤2:配置 DMA2 Stream1(但先不使能 EN)
步骤3:配置 DCMI_CR(但 ENABLE=0,CAPTURE=0)
步骤4:(可选)配置裁剪寄存器 DCMI_CWSTRT / DCMI_CWSIZE
步骤5:使能 NVIC 中对 DCMI 的中断响应
步骤6:使能 DMA(EN=1)
步骤7:置 DCMI_CR 的 ENABLE=1
步骤8:置 DCMI_CR 的 CAPTURE=1(开始等待第一个 VSYNC)
步骤9:主循环中等待 frame_ready 标志

STM32F429 LTDC 液晶显示控制器 从零理解

参考手册:RM0090,第16章
适用芯片:STM32F429xx / STM32F439xx

一、LTDC 是什么?为什么需要它?

想象你要在一块液晶屏上显示一张图片。
CPU 直接控制每个像素?太慢了,而且 CPU 会被完全占用。
LTDC(LCD-TFT Controller) 就是专门负责"把内存里的图像数据,自动、连续地搬运到液晶屏上显示"的硬件模块。
CPU 只需要把图像数据放进内存(帧缓冲区),剩下的事全交给 LTDC:它会按照液晶屏的时序要求,一帧一帧地把像素数据推送给屏幕。

CPU 把图像写入内存 (SDRAM)
         |
         v
    [帧缓冲区 Frame Buffer]
         |
         v
      [LTDC]  <-- 自动、持续地搬数据
         |
         v
    [液晶屏 TFT Panel]
    显示出图像

二、LTDC 整体结构鸟瞰

                      STM32F429 内部
+-----------------------------------------------------------+
|                                                           |
|  内存(SDRAM)                                              |
|    [帧缓冲区 Layer1]                                      |
|    [帧缓冲区 Layer2]                                      |
|         |                                                 |
|         | AHB总线 (HCLK) -- 搬运像素数据                  |
|         v                                                 |
|  +------+-------+                                         |
|  | Layer1 FIFO  | --> [像素格式转换 PFC] --+              |
|  +--------------+                          |              |
|  | Layer2 FIFO  | --> [像素格式转换 PFC] --+--> [混合器]  |
|  +--------------+                          |    Blending  |
|                                            |       |      |
|  APB2总线 (PCLK2) -- 配置寄存器            |       v      |
|  [全局配置寄存器]                           |   [抖动器]   |
|  [中断寄存器]                              |   Dithering  |
|                                            |       |      |
|  像素时钟域 (LCD_CLK) -- 时序生成           |       v      |
|  [同步时序生成器]  <-----------------------+  [时序生成器] |
|                                                    |      |
+----------------------------------------------------+------+
                                                     |
                              LCD_CLK  LCD_HSYNC  LCD_VSYNC
                              LCD_DE   LCD_R[7:0] LCD_G[7:0]
                              LCD_B[7:0]
                                                     |
                                                     v
                                              [液晶屏 TFT Panel]

关键模块解释:

  • 帧缓冲区(Frame Buffer):内存里的一块区域,存放一整帧的像素数据
  • FIFO:每层有一个 64×32bit 的缓冲队列,防止数据断流
  • PFC(像素格式转换器):把 RGB565、RGB888 等格式统一转成内部 ARGB8888 格式
  • 混合器(Blending Unit):把 Layer1、Layer2 和背景色按 alpha 值叠加
  • 抖动器(Dithering Unit):用于 18bit 屏幕显示 24bit 数据时的伪随机抖动
  • 时序生成器(Timing Generator):产生 HSYNC、VSYNC、DE、CLK 等控制信号

三、三个时钟域——为什么有三个?

LTDC 内部有三套不同速度的时钟,各管各的事:

+------------------+---------------------------+----------------------------------+
| 时钟域            | 时钟来源                   | 负责的事                          |
+------------------+---------------------------+----------------------------------+
| AHB 时钟 (HCLK)  | 系统主时钟,最高168MHz     | 从内存把像素数据搬到 FIFO          |
+------------------+---------------------------+----------------------------------+
| APB2 时钟(PCLK2) | APB2总线时钟               | 读写配置寄存器、中断寄存器          |
+------------------+---------------------------+----------------------------------+
| 像素时钟(LCD_CLK) | PLLSAI 专用PLL生成         | 驱动液晶屏时序信号、像素数据输出     |
+------------------+---------------------------+----------------------------------+

注意:访问 LTDC 寄存器时,APB2 总线会"卡顿"(stall)若干个时钟周期,这是跨时钟域同步造成的,属于正常现象。

四、液晶屏时序——最重要的概念

理解 LTDC 配置的关键,就是理解液晶屏的扫描时序。

4.1 液晶屏是怎么"画"一帧图像的?

液晶屏不是一次性刷新整个画面,而是像扫描仪一样,一行一行、从左到右、从上到下地扫描:

屏幕左上角 (0,0)
+----------------------------------------> X 方向(水平)
|  第1行像素:左 --> 右
|  第2行像素:左 --> 右
|  第3行像素:左 --> 右
|  ...
|  第N行像素:左 --> 右
v
Y 方向(垂直)
屏幕左下角

每扫完一行,要有"水平消隐"时间(回到行首);
每扫完一帧,要有"垂直消隐"时间(回到帧首)。

4.2 一行的时序分解

时间轴 ----------------------------------------------------------------->
HSYNC:  |_____|
        ^ 同步脉冲
              |<-- HBP -->|<------ 有效像素 ------>|<-- HFP -->|
              后廊(等待)    (Active Width,如640像素)   前廊(等待)
完整一行 = HSYNC宽度 + HBP + 有效宽度 + HFP
  • HSYNC(水平同步):告诉屏幕"新的一行要开始了"
  • HBP(Horizontal Back Porch,水平后廊):HSYNC 结束后,屏幕内部需要一段准备时间
  • 有效像素区(Active Width):真正的像素数据在这里输出
  • HFP(Horizontal Front Porch,水平前廊):一行结束后、下一个 HSYNC 前的等待时间

4.3 一帧的时序分解

时间轴(垂直方向)----------------------------->
VSYNC:  |___|
        ^ 垂直同步脉冲
             |<- VBP ->|<------- 有效行 ------->|<- VFP ->|
             垂直后廊    (Active Height,如480行)   垂直前廊
完整一帧 = VSYNC高度 + VBP + 有效高度 + VFP

原理与水平方向完全类似,只是单位从"像素"变成了"行"。

4.4 完整的时序图(ASCII 版)

        <----------- 一帧总宽度 TOTALW ------------>
        HSYNC  HBP     Active Width      HFP
        +----+------+-------------------+------+
VSYNC   |////|      |                   |      |  <- VSYNC 期间
        +----+------+-------------------+------+
VBP     |    |      |                   |      |  <- 垂直后廊
        +----+------+-------------------+------+
Active  |    |      |  *** 真正显示 ***  |      |  <- 有效行(如480行)
Height  |    |      |  *** 的区域 ***   |      |
        +----+------+-------------------+------+
VFP     |    |      |                   |      |  <- 垂直前廊
        +----+------+-------------------+------+

4.5 寄存器配置公式——"累积值"写法

ST 的 LTDC 寄存器使用累积值,不是直接填各段的宽度:
L T D C _ S S C R . H S W = H S Y N C _ W i d t h − 1 LTDC\_SSCR.HSW = HSYNC\_Width - 1 LTDC_SSCR.HSW=HSYNC_Width1
L T D C _ S S C R . V S H = V S Y N C _ H e i g h t − 1 LTDC\_SSCR.VSH = VSYNC\_Height - 1 LTDC_SSCR.VSH=VSYNC_Height1
L T D C _ B P C R . A H B P = H S Y N C _ W i d t h + H B P − 1 LTDC\_BPCR.AHBP = HSYNC\_Width + HBP - 1 LTDC_BPCR.AHBP=HSYNC_Width+HBP1
L T D C _ B P C R . A V B P = V S Y N C _ H e i g h t + V B P − 1 LTDC\_BPCR.AVBP = VSYNC\_Height + VBP - 1 LTDC_BPCR.AVBP=VSYNC_Height+VBP1
L T D C _ A W C R . A A W = H S Y N C _ W i d t h + H B P + A c t i v e _ W i d t h − 1 LTDC\_AWCR.AAW = HSYNC\_Width + HBP + Active\_Width - 1 LTDC_AWCR.AAW=HSYNC_Width+HBP+Active_Width1
L T D C _ A W C R . A A H = V S Y N C _ H e i g h t + V B P + A c t i v e _ H e i g h t − 1 LTDC\_AWCR.AAH = VSYNC\_Height + VBP + Active\_Height - 1 LTDC_AWCR.AAH=VSYNC_Height+VBP+Active_Height1
L T D C _ T W C R . T O T A L W = H S Y N C _ W i d t h + H B P + A c t i v e _ W i d t h + H F P − 1 LTDC\_TWCR.TOTALW = HSYNC\_Width + HBP + Active\_Width + HFP - 1 LTDC_TWCR.TOTALW=HSYNC_Width+HBP+Active_Width+HFP1
L T D C _ T W C R . T O T A L H = V S Y N C _ H e i g h t + V B P + A c t i v e _ H e i g h t + V F P − 1 LTDC\_TWCR.TOTALH = VSYNC\_Height + VBP + Active\_Height + VFP - 1 LTDC_TWCR.TOTALH=VSYNC_Height+VBP+Active_Height+VFP1
为什么是累积值? 这样硬件内部只需要一个计数器和几个比较器,逻辑更简单。

4.6 配置举例(640x480 分辨率屏幕)

面板数据手册给出的参数:

参数
HSYNC 宽度 8 像素
HBP 7 像素
有效宽度 640 像素(0x280)
HFP 6 像素
VSYNC 高度 4 行
VBP 2 行
有效高度 480 行(0x1E0)
VFP 2 行

计算寄存器值:
S S C R : H S W = 8 − 1 = 7 = 0 x 7 , V S H = 4 − 1 = 3 = 0 x 3 SSCR: HSW = 8 - 1 = 7 = 0x7,\quad VSH = 4 - 1 = 3 = 0x3 SSCR:HSW=81=7=0x7,VSH=41=3=0x3
⇒ L T D C _ S S C R = 0 x 00070003 \Rightarrow LTDC\_SSCR = 0x00070003 LTDC_SSCR=0x00070003
B P C R : A H B P = 8 + 7 − 1 = 14 = 0 x E , A V B P = 4 + 2 − 1 = 5 = 0 x 5 BPCR: AHBP = 8 + 7 - 1 = 14 = 0xE,\quad AVBP = 4 + 2 - 1 = 5 = 0x5 BPCR:AHBP=8+71=14=0xE,AVBP=4+21=5=0x5
⇒ L T D C _ B P C R = 0 x 000 E 0005 \Rightarrow LTDC\_BPCR = 0x000E0005 LTDC_BPCR=0x000E0005
A W C R : A A W = 8 + 7 + 640 − 1 = 654 = 0 x 28 E , A A H = 4 + 2 + 480 − 1 = 485 = 0 x 1 E 5 AWCR: AAW = 8 + 7 + 640 - 1 = 654 = 0x28E,\quad AAH = 4 + 2 + 480 - 1 = 485 = 0x1E5 AWCR:AAW=8+7+6401=654=0x28E,AAH=4+2+4801=485=0x1E5
⇒ L T D C _ A W C R = 0 x 028 E 01 E 5 \Rightarrow LTDC\_AWCR = 0x028E01E5 LTDC_AWCR=0x028E01E5
T W C R : T O T A L W = 8 + 7 + 640 + 6 − 1 = 660 = 0 x 294 , T O T A L H = 4 + 2 + 480 + 2 − 1 = 487 = 0 x 1 E 7 TWCR: TOTALW = 8 + 7 + 640 + 6 - 1 = 660 = 0x294,\quad TOTALH = 4 + 2 + 480 + 2 - 1 = 487 = 0x1E7 TWCR:TOTALW=8+7+640+61=660=0x294,TOTALH=4+2+480+21=487=0x1E7
⇒ L T D C _ T W C R = 0 x 02940000 ∣ 0 x 000001 E 7 = 0 x 029401 E 7 \Rightarrow LTDC\_TWCR = 0x02940000 \mid 0x000001E7 = 0x029401E7 LTDC_TWCR=0x029400000x000001E7=0x029401E7

五、两个显示层(Layer)——硬件叠加

LTDC 支持两个独立的图层,就像 Photoshop 里的图层概念:

+---------------------------------------------------+
|  Layer 2(顶层,如UI界面)     半透明              |
+---------------------------------------------------+
        |  Alpha 混合(叠加)
+---------------------------------------------------+
|  Layer 1(底层,如背景图片)                       |
+---------------------------------------------------+
        |  Alpha 混合(叠加)
+---------------------------------------------------+
|  Background Color(纯色背景)                      |
+---------------------------------------------------+
        |
        v
    最终输出到屏幕的颜色

混合公式:
B C = B F 1 × C + B F 2 × C s BC = BF1 \times C + BF2 \times Cs BC=BF1×C+BF2×Cs
其中:

  • B C BC BC = 混合后的颜色(Blended Color)
  • C C C = 当前层的颜色
  • C s Cs Cs = 下方层(已混合)的颜色
  • B F 1 BF1 BF1 = 混合因子1(可选:常数Alpha 或 像素Alpha × 常数Alpha)
  • B F 2 BF2 BF2 = 混合因子2(可选:$1 - $ 常数Alpha 或 $1 - $ 像素Alpha × 常数Alpha)
    举例:Layer1 的常数 Alpha = 240( 240 255 ≈ 0.94 \frac{240}{255} \approx 0.94 2552400.94),Layer1 颜色 = 128,背景色 = 48
    B C = 0.94 × 128 + ( 1 − 0.94 ) × 48 = 120.32 + 2.88 ≈ 123 BC = 0.94 \times 128 + (1 - 0.94) \times 48 = 120.32 + 2.88 \approx 123 BC=0.94×128+(10.94)×48=120.32+2.88123

六、像素格式——内存里的数据怎么存?

帧缓冲区可以用多种格式存储像素,LTDC 支持8种:

PF[2:0] 格式 每像素字节数 说明
000 ARGB8888 4 字节 最高质量,带 alpha
001 RGB888 3 字节 无 alpha,24bit 颜色
010 RGB565 2 字节 最常用,节省内存
011 ARGB1555 2 字节 1bit alpha
100 ARGB4444 2 字节 4bit alpha
101 L8 1 字节 256色索引
110 AL44 1 字节 4bit alpha + 4bit 亮度
111 AL88 2 字节 8bit alpha + 8bit 亮度

帧缓冲区大小计算
帧缓冲区大小 = 宽度 × 高度 × 每像素字节数 帧缓冲区大小 = 宽度 \times 高度 \times 每像素字节数 帧缓冲区大小=宽度×高度×每像素字节数
例:640×480,RGB565 格式:
640 × 480 × 2 = 614400  字节 ≈ 600   K B 640 \times 480 \times 2 = 614400\ 字节 \approx 600\ KB 640×480×2=614400 字节600 KB

七、关键寄存器速查

LTDC_GCR(全局控制寄存器,偏移 0x18)


名称 说明
31 HSPOL HSYNC 极性(0=低有效,1=高有效)
30 VSPOL VSYNC 极性
29 DEPOL DE 极性
28 PCPOL 像素时钟极性(0=正常,1=反相)
16 DEN 抖动使能
0 LTDCEN LTDC 使能(最后才打开!)

LTDC_SRCR(影子寄存器重载,偏移 0x24)

Layer 层的配置寄存器是"影子寄存器"——写入后不立刻生效,需要通过 SRCR 触发重载:

名称 说明
1 VBR 在下一个垂直消隐期重载(平滑切换,不撕裂)
0 IMR 立即重载(马上生效)

重要规则:写完所有层配置后,才触发 IMR/VBR,否则可能出现中间状态!

LTDC_LxCFBLR(帧缓冲行长度,偏移 0xB0 + 0x80×(x-1))


位域 名称 说明
28:16 CFBP Pitch(行间距,字节),即相邻两行起始地址的差
12:0 CFBLL 行长度(字节)= 有效宽度 × 每像素字节数 + 3

行长度公式:
C F B L L = A c t i v e _ W i d t h × b y t e s _ p e r _ p i x e l + 3 CFBLL = Active\_Width \times bytes\_per\_pixel + 3 CFBLL=Active_Width×bytes_per_pixel+3

为什么加3?这是 AHB 总线 burst 传输对齐的补偿,硬件规定。

八、LTDC 中断

LTDC 有 4 个中断源,汇聚到 2 个 NVIC 中断向量:

中断源              中断向量
-----------         ----------
Line           ---> LTDC 全局中断
Register Reload--->
FIFO Underrun  ---> LTDC 错误中断
Transfer Error --->

中断 标志位 使能位 触发条件
行中断 LIF LIE 扫描到指定行号(LIPCR 设置)
寄存器重载 RRIF RRIEN 垂直消隐期完成了 VBR 重载
FIFO 下溢 FUIF FUIE FIFO 空了但还要读数据(数据跟不上)
传输错误 TERRIF TERRIE AHB 总线发生错误

九、编程步骤流程图

开始

使能 RCC 时钟
APB2: LTDC
AHB1: GPIO
AHB3: FMC/SDRAM

配置 PLLSAI
生成 LCD_CLK 像素时钟
匹配面板数据手册要求

配置 GPIO 引脚
所有 LCD 信号脚设为复用功能 AF14

配置同步时序
写 LTDC_SSCR / BPCR
AWCR / TWCR

配置信号极性
写 LTDC_GCR
HSPOL VSPOL DEPOL PCPOL

配置背景色
写 LTDC_BCCR
可选

配置 Layer1 参数
窗口位置 / 像素格式
帧缓冲地址 / 行长度 / 行数

需要 Layer2?

配置 Layer2 参数
同 Layer1

使能 Layer
写 LTDC_LxCR
LEN = 1

重载影子寄存器
写 LTDC_SRCR
IMR = 1

使能 LTDC
写 LTDC_GCR
LTDCEN = 1

屏幕开始显示

十、完整 C++ 代码示例

下面的代码演示如何初始化 STM32F429 LTDC,驱动一块 640×480 的 RGB565 格式 TFT 屏幕,Layer1 全屏显示,使用直接寄存器操作(不依赖 HAL 库)。

/*
 * STM32F429 LTDC 初始化示例
 * 目标屏幕:640×480,RGB565 格式
 * LTDC 基地址:0x40016800
 *
 * 假设条件:
 *   - 系统时钟 168MHz 已配置完毕
 *   - SDRAM 已初始化,帧缓冲区存放在 SDRAM 中
 *   - GPIO 已配置为 AF14(LTDC 功能)
 *   - PLLSAI 已配置,LCD_CLK 约为 6.25MHz(适合此面板)
 *
 * 编译:arm-none-eabi-g++ -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16
 *       -mfloat-abi=hard -O2 -std=c++14
 */
#include <cstdint>   // uint32_t, uint16_t 等标准整数类型
#include <cstring>   // memset
// ==============================================================
// 1. 寄存器基地址定义
// ==============================================================
// LTDC 寄存器基地址(见 RM0090 存储器映射)
#define LTDC_BASE       0x40016800UL
// RCC 基地址(用于使能时钟)
#define RCC_BASE        0x40023800UL
// ==============================================================
// 2. LTDC 寄存器结构体
//    所有寄存器均为 volatile,防止编译器优化掉硬件访问
// ==============================================================
struct LTDC_TypeDef
{
    uint32_t RESERVED0[2];  // 0x00, 0x04 保留
    volatile uint32_t SSCR; // 0x08 同步尺寸配置(HSYNC/VSYNC 宽度)
    volatile uint32_t BPCR; // 0x0C 后廊配置(HBP/VBP 累积值)
    volatile uint32_t AWCR; // 0x10 有效宽高配置(AAW/AAH 累积值)
    volatile uint32_t TWCR; // 0x14 总宽高配置(TOTALW/TOTALH 累积值)
    volatile uint32_t GCR;  // 0x18 全局控制(使能、极性、抖动)
    uint32_t RESERVED1[2];  // 0x1C, 0x20 保留
    volatile uint32_t SRCR; // 0x24 影子寄存器重载控制
    uint32_t RESERVED2;     // 0x28 保留
    volatile uint32_t BCCR; // 0x2C 背景色配置(RGB888)
    uint32_t RESERVED3;     // 0x30 保留
    volatile uint32_t IER;  // 0x34 中断使能寄存器
    volatile uint32_t ISR;  // 0x38 中断状态寄存器(只读)
    volatile uint32_t ICR;  // 0x3C 中断清除寄存器(写1清零)
    volatile uint32_t LIPCR;// 0x40 行中断位置配置
    volatile uint32_t CPSR; // 0x44 当前位置状态(只读)
    volatile uint32_t CDSR; // 0x48 当前显示状态(只读)
};
// ==============================================================
// 3. LTDC Layer 寄存器结构体
//    Layer1 偏移 0x84,Layer2 偏移 0x104(= 0x84 + 0x80)
// ==============================================================
struct LTDC_Layer_TypeDef
{
    volatile uint32_t CR;     // +0x00 层控制(使能、CLUT、颜色键)
    volatile uint32_t WHPCR;  // +0x04 水平位置(起止像素)
    volatile uint32_t WVPCR;  // +0x08 垂直位置(起止行)
    volatile uint32_t CKCR;   // +0x0C 颜色键值(RGB)
    volatile uint32_t PFCR;   // +0x10 像素格式选择
    volatile uint32_t CACR;   // +0x14 常数 Alpha 值
    volatile uint32_t DCCR;   // +0x18 默认颜色(层外/禁用时)
    volatile uint32_t BFCR;   // +0x1C 混合因子配置
    uint32_t RESERVED[2];     // +0x20, 0x24 保留
    volatile uint32_t CFBAR;  // +0x28 帧缓冲起始地址
    volatile uint32_t CFBLR;  // +0x2C 帧缓冲行长度和 Pitch
    volatile uint32_t CFBLNR; // +0x30 帧缓冲行数
    uint32_t RESERVED2[3];    // 保留
    volatile uint32_t CLUTWR; // +0x40 CLUT 写寄存器
};
// ==============================================================
// 4. 外设指针(直接映射到硬件地址)
// ==============================================================
// LTDC 全局寄存器
#define LTDC   (reinterpret_cast<LTDC_TypeDef*>(LTDC_BASE))
// Layer1 基地址 = LTDC_BASE + 0x84
#define LTDC_L1 (reinterpret_cast<LTDC_Layer_TypeDef*>(LTDC_BASE + 0x84))
// Layer2 基地址 = LTDC_BASE + 0x84 + 0x80 = LTDC_BASE + 0x104
#define LTDC_L2 (reinterpret_cast<LTDC_Layer_TypeDef*>(LTDC_BASE + 0x104))
// RCC APB2 使能寄存器(LTDC 时钟使能 bit 26)
#define RCC_APB2ENR  (*reinterpret_cast<volatile uint32_t*>(RCC_BASE + 0x44))
// ==============================================================
// 5. 屏幕参数(从面板数据手册读取,以 640×480 为例)
// ==============================================================
// 面板原始参数
constexpr uint32_t LCD_HSYNC  = 8;    // 水平同步脉冲宽度(像素)
constexpr uint32_t LCD_HBP    = 7;    // 水平后廊(像素)
constexpr uint32_t LCD_WIDTH  = 640;  // 有效像素宽度
constexpr uint32_t LCD_HFP    = 6;    // 水平前廊(像素)
constexpr uint32_t LCD_VSYNC  = 4;    // 垂直同步脉冲高度(行)
constexpr uint32_t LCD_VBP    = 2;    // 垂直后廊(行)
constexpr uint32_t LCD_HEIGHT = 480;  // 有效行数
constexpr uint32_t LCD_VFP    = 2;    // 垂直前廊(行)
// 帧缓冲区地址(假设 SDRAM 起始于 0xD0000000)
// 640×480×2 字节(RGB565)= 614400 字节 ≈ 600KB
constexpr uint32_t FRAMEBUF_ADDR = 0xD0000000UL;
// 每像素字节数(RGB565 = 2 字节)
constexpr uint32_t BYTES_PER_PIXEL = 2;
// ==============================================================
// 6. 帧缓冲区(声明为全局数组,也可以放在 SDRAM 映射地址)
//    这里用指针指向 SDRAM 地址
// ==============================================================
// 直接操作 SDRAM 中的帧缓冲区(确保 SDRAM 已初始化)
static uint16_t* const framebuf = reinterpret_cast<uint16_t*>(FRAMEBUF_ADDR);
// ==============================================================
// 7. 辅助函数:将 RGB888 转换为 RGB565
// ==============================================================
/**
 * @brief 将 24bit RGB 颜色转换为 16bit RGB565 格式
 * @param r  红色分量 (0~255)
 * @param g  绿色分量 (0~255)
 * @param b  蓝色分量 (0~255)
 * @return   RGB565 格式的 16bit 颜色值
 *
 * RGB565 布局:
 *   bit[15:11] = R[7:3]  (5位红色,取高5位)
 *   bit[10:5]  = G[7:2]  (6位绿色,取高6位)
 *   bit[4:0]   = B[7:3]  (5位蓝色,取高5位)
 */
inline uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b)
{
    return static_cast<uint16_t>(
        ((r & 0xF8u) << 8) |   // 红:取高5位,放到bit[15:11]
        ((g & 0xFCu) << 3) |   // 绿:取高6位,放到bit[10:5]
        ((b & 0xF8u) >> 3)     // 蓝:取高5位,放到bit[4:0]
    );
}
// ==============================================================
// 8. LTDC 初始化函数
// ==============================================================
/**
 * @brief 初始化 LTDC 控制器
 *
 * 调用此函数前需确保:
 *   1. PLLSAI 已配置并产生合适的 LCD_CLK
 *   2. 相关 GPIO 引脚已配置为 AF14(LTDC 复用功能)
 *   3. SDRAM 已初始化(帧缓冲区可用)
 */
void LTDC_Init(void)
{
    // ----------------------------------------------------------
    // 步骤 1:使能 LTDC 外设时钟
    // RCC_APB2ENR bit26 = LTDCEN
    // ----------------------------------------------------------
    RCC_APB2ENR |= (1UL << 26);
    // ----------------------------------------------------------
    // 步骤 2:配置同步时序(写"累积值",不是直接写各段宽度)
    //
    // SSCR 寄存器:
    //   HSW[27:16] = HSYNC_Width - 1 = 8 - 1 = 7
    //   VSH[10:0]  = VSYNC_Height - 1 = 4 - 1 = 3
    // ----------------------------------------------------------
    LTDC->SSCR = ((LCD_HSYNC - 1) << 16)   // HSW 写到高16位
               | ((LCD_VSYNC - 1) << 0);   // VSH 写到低11位
    // BPCR 寄存器(后廊累积值):
    //   AHBP = HSYNC + HBP - 1 = 8 + 7 - 1 = 14 = 0xE
    //   AVBP = VSYNC + VBP - 1 = 4 + 2 - 1 = 5
    LTDC->BPCR = ((LCD_HSYNC + LCD_HBP - 1) << 16)
               | ((LCD_VSYNC + LCD_VBP - 1) << 0);
    // AWCR 寄存器(有效区域累积值):
    //   AAW = HSYNC + HBP + Width - 1 = 8 + 7 + 640 - 1 = 654 = 0x28E
    //   AAH = VSYNC + VBP + Height - 1 = 4 + 2 + 480 - 1 = 485 = 0x1E5
    LTDC->AWCR = ((LCD_HSYNC + LCD_HBP + LCD_WIDTH  - 1) << 16)
               | ((LCD_VSYNC + LCD_VBP + LCD_HEIGHT - 1) << 0);
    // TWCR 寄存器(总尺寸累积值):
    //   TOTALW = HSYNC + HBP + Width + HFP - 1 = 8+7+640+6-1 = 660 = 0x294
    //   TOTALH = VSYNC + VBP + Height + VFP - 1 = 4+2+480+2-1 = 487 = 0x1E7
    LTDC->TWCR = ((LCD_HSYNC + LCD_HBP + LCD_WIDTH  + LCD_HFP - 1) << 16)
               | ((LCD_VSYNC + LCD_VBP + LCD_HEIGHT + LCD_VFP - 1) << 0);
    // ----------------------------------------------------------
    // 步骤 3:配置全局控制寄存器 GCR
    //   HSPOL = 0  :HSYNC 低电平有效(大多数屏幕默认)
    //   VSPOL = 0  :VSYNC 低电平有效
    //   DEPOL = 0  :DE 低电平有效(Not Data Enable,低=数据有效)
    //   PCPOL = 0  :像素时钟不反相(上升沿采样)
    //   DEN   = 0  :暂不使能抖动
    //   LTDCEN = 0 :最后才使能,这里先不开
    // ----------------------------------------------------------
    // 读-改-写,只修改极性位,其余保持不变
    LTDC->GCR &= ~((1UL << 31) |  // 清 HSPOL
                   (1UL << 30) |  // 清 VSPOL
                   (1UL << 29) |  // 清 DEPOL
                   (1UL << 28));  // 清 PCPOL
    // 如果某个极性需要高有效,在这里 OR 进去,例如:
    // LTDC->GCR |= (1UL << 31);  // HSPOL 高有效
    // ----------------------------------------------------------
    // 步骤 4:配置背景色(纯黑色,RGB888 = 0x000000)
    // BCCR[23:0] = {R[7:0], G[7:0], B[7:0]}
    // ----------------------------------------------------------
    LTDC->BCCR = 0x00000000UL;  // 黑色背景
    // ----------------------------------------------------------
    // 步骤 5:配置 Layer1
    // ----------------------------------------------------------
    // 5.1 Layer1 窗口水平位置
    //   WHSTPOS(起始像素)= AHBP + 1 = 14 + 1 = 15
    //   WHSPPOS(终止像素)= AAW = HSYNC + HBP + Width - 1 = 654
    //
    //   这里配置 Layer1 铺满整个有效区域(不偏移)
    uint32_t h_start = LCD_HSYNC + LCD_HBP;            // = 15
    uint32_t h_stop  = LCD_HSYNC + LCD_HBP + LCD_WIDTH - 1;  // = 654
    // WHPCR[27:16] = WHSPPOS(终止),WHPCR[11:0] = WHSTPOS(起始)
    LTDC_L1->WHPCR = (h_stop  << 16) | (h_start << 0);
    // 5.2 Layer1 窗口垂直位置
    //   WVSTPOS(起始行)= AVBP + 1 = 5 + 1 = 6
    //   WVSPPOS(终止行)= AAH = VSYNC + VBP + Height - 1 = 485
    uint32_t v_start = LCD_VSYNC + LCD_VBP;             // = 6
    uint32_t v_stop  = LCD_VSYNC + LCD_VBP + LCD_HEIGHT - 1; // = 485
    // WVPCR[26:16] = WVSPPOS(终止),WVPCR[10:0] = WVSTPOS(起始)
    LTDC_L1->WVPCR = (v_stop  << 16) | (v_start << 0);
    // 5.3 像素格式:RGB565(PF[2:0] = 010 = 2)
    LTDC_L1->PFCR = 0x02UL;
    // 5.4 常数 Alpha = 0xFF(完全不透明,255/255 = 1.0)
    LTDC_L1->CACR = 0xFFUL;
    // 5.5 默认颜色(层窗口外或层禁用时显示的颜色),设为透明黑
    //   DCALPHA=0x00(完全透明),DCRED=DCGREEN=DCBLUE=0
    LTDC_L1->DCCR = 0x00000000UL;
    // 5.6 混合因子配置
    //   BF1[10:8] = 110 = 6:使用"像素Alpha × 常数Alpha"
    //   BF2[2:0]  = 111 = 7:使用"1 - (像素Alpha × 常数Alpha)"
    //   对于不透明显示(无 alpha 通道),也可以直接用:
    //   BF1 = 100(常数Alpha),BF2 = 101(1 - 常数Alpha)
    //   默认复位值就是 0x0607,这里保持默认即可
    LTDC_L1->BFCR = 0x00000607UL; // BF1=6, BF2=7(复位默认值)
    // 5.7 帧缓冲起始地址(Layer1 图像数据在内存中的起始位置)
    LTDC_L1->CFBAR = FRAMEBUF_ADDR;
    // 5.8 帧缓冲行长度和 Pitch
    //   CFBLL(行长度)= 有效宽度 × 每像素字节数 + 3
    //                  = 640 × 2 + 3 = 1283 = 0x503
    //   CFBP(Pitch)  = 相邻两行起始地址差(字节)
    //                  = 与 CFBLL 相同(紧凑存储,无填充)= 640 × 2 = 1280 = 0x500
    //
    //   注意:CFBLL 写"字节数+3",CFBP 写"真实行字节数"
    uint32_t line_len = LCD_WIDTH * BYTES_PER_PIXEL + 3;  // = 1283
    uint32_t pitch    = LCD_WIDTH * BYTES_PER_PIXEL;      // = 1280
    // CFBLR[28:16] = CFBP,CFBLR[12:0] = CFBLL
    LTDC_L1->CFBLR = (pitch << 16) | (line_len << 0);
    // 5.9 帧缓冲行数(= 有效高度)
    LTDC_L1->CFBLNR = LCD_HEIGHT;  // = 480
    // 5.10 使能 Layer1(LEN bit0 = 1)
    //   CLUTEN(bit4)= 0:不使用 CLUT(我们用 RGB565,不是索引色)
    //   COLKEN(bit1)= 0:不使用颜色键(不做透明色)
    LTDC_L1->CR = 0x01UL;  // LEN = 1
    // ----------------------------------------------------------
    // 步骤 6:重载影子寄存器
    //   所有 Layer 寄存器都是"影子寄存器",写入后不立即生效
    //   必须触发一次重载,才会把影子寄存器的值同步到实际工作寄存器
    //
    //   IMR(bit0)= 1:立即重载
    // ----------------------------------------------------------
    LTDC->SRCR = 0x01UL;  // IMR = 1,立即重载
    // ----------------------------------------------------------
    // 步骤 7:使能 LTDC
    //   LTDCEN(bit0)= 1
    //   一旦使能,LTDC 就会开始输出时序信号和像素数据
    // ----------------------------------------------------------
    LTDC->GCR |= (1UL << 0);  // LTDCEN = 1
}
// ==============================================================
// 9. 绘图工具函数
// ==============================================================
/**
 * @brief 用指定颜色填充整个屏幕
 * @param color RGB565 格式的颜色值
 */
void LTDC_FillScreen(uint16_t color)
{
    // LCD_WIDTH * LCD_HEIGHT 个像素,逐一写入帧缓冲区
    for (uint32_t i = 0; i < LCD_WIDTH * LCD_HEIGHT; ++i)
    {
        framebuf[i] = color;
    }
}
/**
 * @brief 在指定位置画一个像素
 * @param x     水平坐标(0 ~ LCD_WIDTH-1)
 * @param y     垂直坐标(0 ~ LCD_HEIGHT-1)
 * @param color RGB565 格式的颜色值
 */
void LTDC_DrawPixel(uint32_t x, uint32_t y, uint16_t color)
{
    if (x >= LCD_WIDTH || y >= LCD_HEIGHT) return;  // 越界保护
    // 像素在帧缓冲区的索引 = y * 行宽 + x
    framebuf[y * LCD_WIDTH + x] = color;
}
/**
 * @brief 画一条水平线
 * @param x0    起始 X 坐标
 * @param y     Y 坐标(固定行)
 * @param len   线的长度(像素数)
 * @param color RGB565 格式颜色
 */
void LTDC_DrawHLine(uint32_t x0, uint32_t y, uint32_t len, uint16_t color)
{
    for (uint32_t x = x0; x < x0 + len && x < LCD_WIDTH; ++x)
    {
        LTDC_DrawPixel(x, y, color);
    }
}
/**
 * @brief 画一个填充矩形
 * @param x0    左上角 X 坐标
 * @param y0    左上角 Y 坐标
 * @param w     宽度(像素)
 * @param h     高度(像素)
 * @param color RGB565 格式颜色
 */
void LTDC_FillRect(uint32_t x0, uint32_t y0,
                   uint32_t w,  uint32_t h,
                   uint16_t color)
{
    for (uint32_t y = y0; y < y0 + h && y < LCD_HEIGHT; ++y)
    {
        for (uint32_t x = x0; x < x0 + w && x < LCD_WIDTH; ++x)
        {
            framebuf[y * LCD_WIDTH + x] = color;
        }
    }
}
// ==============================================================
// 10. 动态修改 Layer 参数(利用影子寄存器和 VBR 无撕裂更新)
// ==============================================================
/**
 * @brief 在垂直消隐期平滑切换帧缓冲区地址(双缓冲)
 * @param new_addr 新的帧缓冲区地址
 *
 * 使用场景:动画/视频,需要在显示当前帧的同时,在另一块内存中画下一帧
 * 切换完成后不会出现"撕裂"(画面撕裂 = 屏幕刷新到一半,内存已切换)
 */
void LTDC_SwitchFrameBuffer(uint32_t new_addr)
{
    // 修改 Layer1 帧缓冲地址
    LTDC_L1->CFBAR = new_addr;
    // 在下一个垂直消隐期触发重载(VBR = 1)
    // 硬件会在当前帧扫描完成后自动重载,然后硬件自动清零 VBR
    LTDC->SRCR = 0x02UL;  // VBR = 1
    // 如果需要等待重载完成,可以查询 SRCR.VBR 是否变为 0
    // while (LTDC->SRCR & 0x02UL) {}  // 等待硬件清零 VBR
}
// ==============================================================
// 11. 中断服务函数示例
// ==============================================================
/**
 * @brief LTDC 全局中断处理(Line interrupt + Register Reload interrupt)
 *
 * 需要在启动文件或向量表中正确绑定此函数名
 */
extern "C" void LTDC_IRQHandler(void)
{
    uint32_t isr = LTDC->ISR;  // 读取中断状态
    // 检查是否是"行中断"(扫描到指定行)
    if (isr & (1UL << 0))  // LIF = bit0
    {
        // 在这里可以做一些与行同步相关的操作
        // 例如:在特定行切换调色板等
        LTDC->ICR = (1UL << 0);  // 写 CLIF 清零 LIF
    }
    // 检查是否是"寄存器重载中断"(VBR 重载完成)
    if (isr & (1UL << 3))  // RRIF = bit3
    {
        // 影子寄存器已经成功同步到工作寄存器
        // 在这里可以通知主循环:可以开始准备下一帧了
        LTDC->ICR = (1UL << 3);  // 写 CRRIF 清零 RRIF
    }
}
/**
 * @brief LTDC 错误中断处理(FIFO 下溢 + 传输错误)
 */
extern "C" void LTDC_ER_IRQHandler(void)
{
    uint32_t isr = LTDC->ISR;
    // FIFO 下溢:帧缓冲数据跟不上像素时钟(SDRAM 太慢?)
    if (isr & (1UL << 1))  // FUIF = bit1
    {
        // 错误处理:降低像素时钟 或 提高 SDRAM 速度
        LTDC->ICR = (1UL << 1);  // 清 CFUIF
    }
    // AHB 总线传输错误
    if (isr & (1UL << 2))  // TERRIF = bit2
    {
        // 错误处理:检查帧缓冲地址是否合法
        LTDC->ICR = (1UL << 2);  // 清 CTERRIF
    }
}
// ==============================================================
// 12. 主函数演示
// ==============================================================
int main(void)
{
    // 前提:此时系统时钟、PLLSAI(LCD_CLK)、GPIO、SDRAM 均已初始化
    // 初始化 LTDC
    LTDC_Init();
    // 用蓝色填充屏幕(R=0, G=0, B=255 → RGB565 = 0x001F)
    LTDC_FillScreen(RGB565(0, 0, 255));
    // 在屏幕中央画一个 100×100 的红色矩形
    // 中心坐标 (320, 240),矩形从 (270, 190) 开始
    LTDC_FillRect(270, 190, 100, 100, RGB565(255, 0, 0));
    // 在矩形中间画一条绿色水平线
    LTDC_DrawHLine(270, 240, 100, RGB565(0, 255, 0));
    // 主循环:LTDC 硬件在后台自动刷屏,CPU 可以做其他事
    while (true)
    {
        // 在这里可以更新帧缓冲区内容
        // LTDC 会在下一帧自动显示新内容(无需手动触发)
    }
    return 0;
}

十一、常见误区与注意事项

误区1:直接写寄存器就生效了

Layer 的所有寄存器(除 CLUT 写寄存器)都是"影子寄存器"。写入后必须触发 LTDC_SRCR.IMR = 1(立即)或 VBR = 1(垂直消隐期)才会真正生效。
误区2:CFBLL 直接填字节数
规定:CFBLL = 有效宽度 × 每像素字节数 + 3,多加 3 是硬件要求,不能省略。
误区3:帧缓冲地址指向 Flash 或 SRAM
640×480 的 RGB565 帧缓冲需要 600KB,STM32F429 内部 SRAM 只有约 256KB,必须使用外部 SDRAM。
误区4:层窗口坐标填错
WHSTPOS 不是从 0 开始的!它必须大于等于 AHBP + 1(即 BPCR 里的累积后廊值加1)。全屏显示时:

W H S T P O S = H S Y N C + H B P WHSTPOS = HSYNC + HBP WHSTPOS=HSYNC+HBP

W H S P P O S = H S Y N C + H B P + W i d t h − 1 WHSPPOS = HSYNC + HBP + Width - 1 WHSPPOS=HSYNC+HBP+Width1
误区5:CLUT 在显示过程中修改
CLUT 写寄存器(LxCLUTWR)只能在消隐期层禁用时修改,否则会出现花屏。
误区6:使能顺序错误
正确顺序:配置所有寄存器 → 触发影子寄存器重载(SRCR)→ 最后才使能 LTDC(GCR.LTDCEN = 1)。

十二、寄存器地址速查表

LTDC 基地址:0x40016800

偏移 寄存器 说明
0x08 LTDC_SSCR HSYNC/VSYNC 宽度(累积值减1)
0x0C LTDC_BPCR 后廊累积值
0x10 LTDC_AWCR 有效区域累积值
0x14 LTDC_TWCR 总尺寸累积值
0x18 LTDC_GCR 全局控制(极性、使能、抖动)
0x24 LTDC_SRCR 影子寄存器重载触发
0x2C LTDC_BCCR 背景色(RGB888)
0x34 LTDC_IER 中断使能
0x38 LTDC_ISR 中断状态(只读)
0x3C LTDC_ICR 中断清除(写1清零)
0x40 LTDC_LIPCR 行中断触发行号
0x44 LTDC_CPSR 当前扫描位置(只读)
0x48 LTDC_CDSR 当前显示状态(只读)

Layer1 基地址:0x40016884,Layer2 基地址:0x40016904

偏移 寄存器 说明
+0x00 LxCR 层控制(使能/CLUT/颜色键)
+0x04 LxWHPCR 水平窗口位置
+0x08 LxWVPCR 垂直窗口位置
+0x0C LxCKCR 颜色键值
+0x10 LxPFCR 像素格式选择
+0x14 LxCACR 常数 Alpha
+0x18 LxDCCR 默认颜色(ARGB)
+0x1C LxBFCR 混合因子
+0x28 LxCFBAR 帧缓冲起始地址
+0x2C LxCFBLR 行长度和 Pitch
+0x30 LxCFBLNR 帧缓冲行数
+0x40 LxCLUTWR CLUT 写入(仅消隐期操作)

STM32F4 高级控制定时器 TIM1 & TIM8 从零理解

参考手册:RM0090,第17章
适用芯片:STM32F4xx 全系列

一、定时器是什么?为什么需要它?

想象你要让一个 LED 每隔 1 秒闪烁一次。如果用 CPU 死循环等待,CPU 就什么都做不了了。定时器就是专门帮你"计时"的硬件模块,CPU 把计时任务交给它,自己去做别的事,时间到了定时器会"拍你一下"(产生中断)。
TIM1 和 TIM8 是 STM32F4 中功能最强的两个定时器,叫做高级控制定时器(Advanced-control Timer)

二、整体结构鸟瞰

外部引脚/内部时钟
       |
       v
  [时钟选择器]  ← 可以选内部时钟 or 外部引脚 or 另一个定时器
       |
       v
  [预分频器 PSC]  ← 把时钟频率除以 1~65536
       |
       v
  [16位计数器 CNT]  ← 核心:不断计数
       |
       v
  [自动重载寄存器 ARR]  ← 决定计到多少重置
       |
   溢出/下溢 → [重复计数器 RCR] → 产生更新事件(UEV)
       |
  [捕获/比较通道 x4]  ← 可以测量输入脉冲 or 产生输出波形
       |
  [互补输出 + 死区时间插入]  ← 高级功能,用于电机驱动
       |
  [刹车电路 BRK]  ← 安全保护

三、时基单元(Time-Base Unit)—— 定时器的心脏

3.1 核心寄存器


寄存器 名称 作用
TIMx_CNT 计数器 存放当前计数值,每个时钟脉冲 +1 或 -1
TIMx_PSC 预分频器 对输入时钟进行分频,范围 0~65535(实际分频 = PSC+1)
TIMx_ARR 自动重载寄存器 计数的"终点",到了就重新开始
TIMx_RCR 重复计数器 溢出多少次才产生一次更新事件

3.2 预分频器工作原理

假设系统时钟 f C K _ I N T = 168   MHz f_{CK\_INT} = 168\,\text{MHz} fCK_INT=168MHz,我想让计数器以 1   MHz 1\,\text{MHz} 1MHz 计数:
P S C = f C K _ I N T f C K _ C N T − 1 = 168   MHz 1   MHz − 1 = 167 PSC = \frac{f_{CK\_INT}}{f_{CK\_CNT}} - 1 = \frac{168\,\text{MHz}}{1\,\text{MHz}} - 1 = 167 PSC=fCK_CNTfCK_INT1=1MHz168MHz1=167
计数器时钟频率公式:
f C K _ C N T = f C K _ P S C P S C + 1 f_{CK\_CNT} = \frac{f_{CK\_PSC}}{PSC + 1} fCK_CNT=PSC+1fCK_PSC
重要:PSC 寄存器有"缓冲"机制,写入新值不会立刻生效,而是在下一次**更新事件(UEV)**后才生效。

3.3 更新事件(UEV)

更新事件是定时器的一个重要信号,触发时会:

  1. 预分频缓冲值 → 生效
  2. ARR 预装值 → 生效
  3. RCR 重载
  4. 置位 UIF 标志位(可触发中断/DMA)
    触发更新事件的条件:
  • 计数器上溢(向上计数到 ARR)
  • 计数器下溢(向下计数到 0)
  • 软件写 TIMx_EGR 的 UG 位
  • 从模式控制器触发

四、三种计数模式

4.1 向上计数模式(Upcounting)

计数值
  |
ARR|----/----/----/----
  |   /    /    /
  |  /    /    /
0 |_/____/____/______> 时间
    ^    ^    ^
    UEV  UEV  UEV(溢出时产生)
  • 从 0 计数到 ARR
  • 到达 ARR 后归零,产生"上溢事件"
  • 周期公式:
    T = ( A R R + 1 ) × 1 f C K _ C N T = ( A R R + 1 ) × ( P S C + 1 ) × 1 f C K _ I N T T = (ARR + 1) \times \frac{1}{f_{CK\_CNT}} = (ARR + 1) \times (PSC + 1) \times \frac{1}{f_{CK\_INT}} T=(ARR+1)×fCK_CNT1=(ARR+1)×(PSC+1)×fCK_INT1
    举例:想要 1ms 中断, f C K _ I N T = 168   MHz f_{CK\_INT} = 168\,\text{MHz} fCK_INT=168MHz
    P S C = 167 PSC = 167 PSC=167,则 f C K _ C N T = 1   MHz f_{CK\_CNT} = 1\,\text{MHz} fCK_CNT=1MHz
    A R R = 999 ARR = 999 ARR=999,则 T = 1000 × 1   μ s = 1   ms T = 1000 \times 1\,\mu s = 1\,\text{ms} T=1000×1μs=1ms

4.2 向下计数模式(Downcounting)

计数值
  |
ARR|\    \    \
  | \    \    \
  |  \    \    \
0 |   \____\____\___> 时间
        ^    ^
        UEV(下溢时产生)
  • 从 ARR 计数到 0
  • 到达 0 后重载 ARR,产生"下溢事件"

4.3 中央对齐模式(Center-aligned,上下计数)

计数值
  |
ARR|  /\    /\    /\
  | /  \  /  \  /  \
  |/    \/    \/    \
0 |__________________> 时间
    ^  ^ ^  ^ ^
    溢出 下溢(两处都可产生UEV)
  • 先从 0 数到 ARR-1(上溢)
  • 再从 ARR 数回 1(下溢)
  • 一个完整周期 = 两倍的单向计数
    CMS 位控制哪种情况触发输出比较中断:

CMS[1:0] 含义
00 边沿对齐(普通向上/向下)
01 中央模式1:仅向下计数时触发
10 中央模式2:仅向上计数时触发
11 中央模式3:上下都触发

五、重复计数器(RCR)

普通定时器每次溢出都产生更新事件,但在 PWM 驱动电机时,我们可能只想每 N 次周期才更新一次参数。
RCR 的作用:把"溢出 N+1 次"才产生一次 UEV

RCR = 0:每次溢出产生UEV
RCR = 1:每2次溢出产生UEV
RCR = 2:每3次溢出产生UEV
...
RCR = N:每(N+1)次溢出产生UEV

数据(预装值)从预载寄存器 → 影子寄存器的搬运,也在 UEV 时发生。

六、时钟源选择

四种时钟来源
       |
       +-- 1. 内部时钟 CK_INT(最常用)
       |
       +-- 2. 外部时钟模式1(TIx引脚)
       |       SMS = 111,从外部引脚边沿计数
       |
       +-- 3. 外部时钟模式2(ETR引脚)
       |       ECE = 1,从ETR引脚边沿计数
       |
       +-- 4. 内部触发(ITRx)
               用另一个定时器的TRGO作为时钟
               可以实现"定时器级联"

内部时钟(最常用)

当 SMS = 000 时,使用内部时钟 CK_INT。设置 CEN = 1 就开始计数。

七、捕获/比较通道(4个)

每个通道可以独立工作在两种模式下:

                    +--> 输入捕获模式(测量外部信号)
TIMx_CH1/2/3/4 ----+
                    +--> 输出比较/PWM模式(产生波形)

7.1 输入捕获模式

用途:测量外部脉冲的宽度或频率
工作原理
当检测到指定的边沿(上升沿/下降沿),自动把当前计数器的值存入 CCRx 寄存器,同时置位 CCxIF 标志。
测量脉冲宽度的方法(PWM输入模式):

TI1 引脚上的PWM信号:
        ____        ____
_______|    |______|    |___
通道1(上升沿捕获)→ CCR1 存储周期(T)
通道2(下降沿捕获)→ CCR2 存储高电平时间(t_high)
占空比 = CCR2 / CCR1 × 100%

配置步骤(以通道1为例):

  1. CC1S = 01(TIMx_CCMR1),把通道1映射到 TI1 输入
  2. 设置 IC1F(滤波,消抖)
  3. 设置 CC1P/CC1NP(检测哪种边沿)
  4. 设置 IC1PS(输入预分频)
  5. CC1E = 1(使能捕获)
  6. 可选:CC1IE = 1(使能中断)

7.2 输出比较模式

用途:在特定时刻改变引脚电平(计时到点干某事)
工作原理:当 CNT == CCRx 时,根据 OCxM 的设置改变输出引脚。
OCxM 输出模式:

OCxM[2:0] 模式 说明
000 冻结 匹配时不改变输出
001 置高 匹配时输出置高
010 置低 匹配时输出置低
011 翻转 匹配时输出翻转
100 强制低 无论如何都低
101 强制高 无论如何都高
110 PWM模式1 向上计数:CNT < CCRx 时高
111 PWM模式2 向上计数:CNT < CCRx 时低

7.3 PWM 模式(最常用)

PWM = 脉冲宽度调制,通过改变高电平占总周期的比例(占空比)来控制功率。
关键公式
PWM频率 = f C K _ C N T A R R + 1 \text{PWM频率} = \frac{f_{CK\_CNT}}{ARR + 1} PWM频率=ARR+1fCK_CNT
占空比 = C C R x A R R + 1 × 100 % \text{占空比} = \frac{CCRx}{ARR + 1} \times 100\% 占空比=ARR+1CCRx×100%
边沿对齐 PWM(向上计数,PWM模式1)示意

ARR = 8, CCRx = 4 时:
计数值:0  1  2  3  4  5  6  7  8  0  1  2 ...
OCxREF:高  高  高  高  低  低  低  低  低  高  高  高 ...
         |<--- 4 个周期高 --->|<--- 5 个周期低 --->|
         占空比 = 4/9 ≈ 44%(ARR+1=9)
ARR = 8, CCRx = 8 时:
OCxREF:高  高  高  高  高  高  高  高  高(到ARR才低)
         100% 占空比
ARR = 8, CCRx = 0 时:
OCxREF:低  低  低  低  低  低  低  低  低
         0% 占空比

中央对齐 PWM 对称性更好,适合电机控制,谐波更少:

ARR = 8, CCRx = 4 时:
计数方向:  上          下
计数值:0 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 0 ...
OCxREF:高 高 高 高 低 低 低 低 低 低 低 低 高 高 高 高 高...

八、互补输出与死区时间插入

这是 TIM1/TIM8 特有的高级功能,主要用于驱动 H 桥(电机驱动)。

8.1 为什么需要互补输出?

驱动电机的 H 桥需要两个互相相反的控制信号:

  VCC
   |
  [Q1 上管]  ← OCx  (高边)
   |
  Motor
   |
  [Q2 下管]  ← OCxN (低边,互补)
   |
  GND

Q1 和 Q2 不能同时导通!否则会直通短路,烧毁器件。

8.2 死区时间(Dead-time)

功率管从导通到关断有一定延迟,为了防止两管短暂同时导通,在切换时插入一段两管都关断的时间,这就是死区时间。

OCxREF:  ____        ____
         |    |      |    |
_________|    |______|    |_______
OCx:       ____        ____
      delay|    |      |    |
___________|    |______|    |_____
           |<-->|
           死区延迟(上升沿延迟)
OCxN:  ___        ____        ___
       |   |delay |    |delay |
_______|   |_____|    |______|___
               |<-->|
               死区延迟(OCx下降沿后,OCxN才上升)

死区时间计算(DTG[7:5] 决定范围,DTG[4:0] 决定精度):
DTG[7:5]=0xx: D T = D T G [ 7 : 0 ] × t D T S \text{DTG[7:5]=0xx:} \quad DT = DTG[7:0] \times t_{DTS} DTG[7:5]=0xx:DT=DTG[7:0]×tDTS
DTG[7:5]=10x: D T = ( 64 + D T G [ 5 : 0 ] ) × 2 × t D T S \text{DTG[7:5]=10x:} \quad DT = (64 + DTG[5:0]) \times 2 \times t_{DTS} DTG[7:5]=10x:DT=(64+DTG[5:0])×2×tDTS
DTG[7:5]=110: D T = ( 32 + D T G [ 4 : 0 ] ) × 8 × t D T S \text{DTG[7:5]=110:} \quad DT = (32 + DTG[4:0]) \times 8 \times t_{DTS} DTG[7:5]=110:DT=(32+DTG[4:0])×8×tDTS
DTG[7:5]=111: D T = ( 32 + D T G [ 4 : 0 ] ) × 16 × t D T S \text{DTG[7:5]=111:} \quad DT = (32 + DTG[4:0]) \times 16 \times t_{DTS} DTG[7:5]=111:DT=(32+DTG[4:0])×16×tDTS
其中 t D T S t_{DTS} tDTS 由 TIMx_CR1 的 CKD 位决定。

九、刹车功能(Break)

当检测到故障(过流、过温等),需要立即关闭所有输出引脚,保护电路。
触发来源

  • BRK 引脚输入(外部硬件故障信号)
  • 时钟安全系统(CSS)检测到时钟失效
    刹车发生时的行为
  1. MOE 位异步清零(即使没有时钟也有效!)
  2. 所有输出立刻进入 OISx/OISxN 设定的安全状态
  3. BIF 标志位置位,可触发中断
  4. 如果 AOE = 1,下一个 UEV 时 MOE 自动恢复

十、从模式(Slave Mode)—— 定时器同步

多个定时器可以互相联动,一个做"主",另一个做"从"。

10.1 复位模式(SMS = 100)

外部信号来了,计数器清零重新开始

TI1信号:     ___|‾‾‾|___
计数器:...31 32 33 00 01 02 03...
                    ^
                    TI1上升沿 → 计数器清零

10.2 门控模式(SMS = 101)

输入信号高时计数,低时停止(但不清零)

TI1信号:  ‾‾‾‾‾‾‾‾|___|‾‾‾‾‾‾‾
计数器:   30 31 32 32 32 33 34 35...
                    ^^^
                    TI1低时停止计数

10.3 触发模式(SMS = 110)

输入信号上升沿时,计数器开始计数

TI2信号:  __|‾‾‾‾‾‾‾‾‾‾‾
计数器:   00 00 34 35 36 37...
              ^
              TI2上升沿 → 启动计数

十一、编码器接口模式

用于读取正交编码器(电机轴上的位置传感器),可以直接测量转速和方向。
原理:编码器有两路信号 A(TI1)和 B(TI2),相位差 90°,通过相位关系判断正转/反转。

正转:
TI1:  ‾|_|‾|_|‾
TI2:   ‾|_|‾|_|
计数:  +1+1+1+1(向上)
反转:
TI1:  ‾|_|‾|_|‾
TI2:  |_|‾|_|‾|
计数:  -1-1-1-1(向下)

SMS 设置:

SMS 含义
001 仅 TI1 边沿计数
010 仅 TI2 边沿计数
011 TI1 和 TI2 都计数(精度最高)

十二、单脉冲模式(OPM)

收到触发信号后,输出一个脉冲然后停止。

TI2(触发):_____|‾‾‾‾‾‾‾‾‾‾
                  |
计数器:    0     0→开始计数→ARR→停止
OC1输出:       __|‾‾‾‾‾‾‾|__
               tDELAY  tPULSE

其中:

  • t D E L A Y t_{DELAY} tDELAY = CCR1(比较值)对应的时间
  • t P U L S E t_{PULSE} tPULSE = ARR - CCR1 对应的时间

十三、关键寄存器速查表

TIMx_CR1(控制寄存器1)


名称 说明
0 CEN 计数器使能(1=启动)
1 UDIS 更新禁止(1=关闭UEV)
2 URS 更新请求源选择
3 OPM 单脉冲模式
4 DIR 方向(0=向上,1=向下)
6:5 CMS 中央对齐模式选择
7 ARPE ARR 预装载使能
9:8 CKD 时钟分频(用于死区和滤波采样)

TIMx_BDTR(刹车和死区寄存器)


名称 说明
7:0 DTG 死区时间设置
9:8 LOCK 写保护级别(写一次,永久生效直到复位)
10 OSSI 空闲模式下的输出状态
11 OSSR 运行模式下非活动输出状态
12 BKE 刹车使能
13 BKP 刹车极性
14 AOE 自动输出使能
15 MOE 主输出使能

十四、完整 C++ 代码示例

示例1:产生 1kHz PWM,占空比 50%

/*
 * STM32F4 TIM1 产生 PWM 示例
 * 系统时钟:168 MHz
 * 目标:在 TIM1_CH1(PA8)输出 1kHz、50% 占空比 PWM
 *
 * 使用标准外设库(SPL)风格,但只使用直接寄存器操作
 * 可直接在 STM32CubeIDE 或 Keil 中使用
 */
#include <cstdint>
// 定义 STM32F4 寄存器基地址(简化版,实际项目请用官方头文件)
#define RCC_BASE    0x40023800UL
#define GPIOA_BASE  0x40020000UL
#define TIM1_BASE   0x40010000UL
// RCC 寄存器
#define RCC_AHB1ENR  (*reinterpret_cast<volatile uint32_t*>(RCC_BASE + 0x30))
#define RCC_APB2ENR  (*reinterpret_cast<volatile uint32_t*>(RCC_BASE + 0x44))
// GPIOA 寄存器
#define GPIOA_MODER   (*reinterpret_cast<volatile uint32_t*>(GPIOA_BASE + 0x00))
#define GPIOA_OSPEEDR (*reinterpret_cast<volatile uint32_t*>(GPIOA_BASE + 0x08))
#define GPIOA_AFRH    (*reinterpret_cast<volatile uint32_t*>(GPIOA_BASE + 0x24))
// TIM1 寄存器(使用结构体方式)
struct TIM_TypeDef {
    volatile uint32_t CR1;    // 0x00 控制寄存器1
    volatile uint32_t CR2;    // 0x04 控制寄存器2
    volatile uint32_t SMCR;   // 0x08 从模式控制
    volatile uint32_t DIER;   // 0x0C 中断/DMA使能
    volatile uint32_t SR;     // 0x10 状态寄存器
    volatile uint32_t EGR;    // 0x14 事件产生
    volatile uint32_t CCMR1;  // 0x18 捕获/比较模式1
    volatile uint32_t CCMR2;  // 0x1C 捕获/比较模式2
    volatile uint32_t CCER;   // 0x20 捕获/比较使能
    volatile uint32_t CNT;    // 0x24 计数器
    volatile uint32_t PSC;    // 0x28 预分频器
    volatile uint32_t ARR;    // 0x2C 自动重载值
    volatile uint32_t RCR;    // 0x30 重复计数器
    volatile uint32_t CCR1;   // 0x34 捕获/比较寄存器1
    volatile uint32_t CCR2;   // 0x38 捕获/比较寄存器2
    volatile uint32_t CCR3;   // 0x3C 捕获/比较寄存器3
    volatile uint32_t CCR4;   // 0x40 捕获/比较寄存器4
    volatile uint32_t BDTR;   // 0x44 刹车和死区寄存器
    volatile uint32_t DCR;    // 0x48 DMA控制
    volatile uint32_t DMAR;   // 0x4C DMA全传输地址
};
#define TIM1  (reinterpret_cast<TIM_TypeDef*>(TIM1_BASE))
// ================================================================
// 配置 PA8 为 TIM1_CH1 的 PWM 输出引脚
// ================================================================
void GPIO_Init_PWM()
{
    // 1. 使能 GPIOA 时钟(AHB1 总线)
    //    Bit 0 = GPIOAEN
    RCC_AHB1ENR |= (1U << 0);
    // 2. 配置 PA8 为复用功能模式(MODER = 10)
    //    PA8 对应 bit[17:16]
    GPIOA_MODER &= ~(0x3U << 16);  // 先清零
    GPIOA_MODER |=  (0x2U << 16);  // 设为复用模式 (AF mode)
    // 3. 配置 PA8 输出速度为高速
    //    PA8 对应 OSPEEDR bit[17:16] = 11(高速)
    GPIOA_OSPEEDR |= (0x3U << 16);
    // 4. 配置 PA8 的复用功能为 AF1(TIM1/TIM2)
    //    AFRH 控制 PA8~PA15,PA8 对应 AFRH[3:0]
    //    AF1 = 0x1
    GPIOA_AFRH &= ~(0xFU << 0);  // 清零 PA8 的 AF 位
    GPIOA_AFRH |=  (0x1U << 0);  // AF1 = TIM1_CH1
}
// ================================================================
// 配置 TIM1 输出 PWM
// 系统时钟 = 168MHz,TIM1 挂载在 APB2,APB2 分频器=2
// 所以 TIM1 时钟 = 168MHz(当APB2预分频不为1时,定时器时钟x2)
//
// 目标:1kHz PWM,50% 占空比
// 方案:PSC=167,ARR=999
//   CK_CNT = 168MHz / (167+1) = 1MHz
//   PWM频率 = 1MHz / (999+1) = 1kHz
//   CCR1 = 500 → 占空比 = 500/1000 = 50%
// ================================================================
void TIM1_PWM_Init()
{
    // 1. 使能 TIM1 时钟(APB2 总线)
    //    Bit 0 = TIM1EN
    RCC_APB2ENR |= (1U << 0);
    // 2. 关闭计数器,进行配置
    TIM1->CR1 &= ~(1U << 0);  // CEN = 0,停止计数
    // 3. 设置预分频器
    //    PSC = 167,实际分频 = 168
    //    CK_CNT = 168MHz / 168 = 1MHz
    TIM1->PSC = 167;
    // 4. 设置自动重载值
    //    ARR = 999,PWM周期 = (999+1) / 1MHz = 1ms = 1kHz
    TIM1->ARR = 999;
    // 5. 重复计数器设为0(每次溢出都产生UEV)
    TIM1->RCR = 0;
    // 6. 配置通道1为 PWM 模式1(边沿对齐,向上计数)
    //    CCMR1 的 OC1M[2:0] = 110(PWM模式1)
    //    同时使能预装载:OC1PE = 1
    TIM1->CCMR1 &= ~(0x7U << 4);   // 清除 OC1M[2:0](bit 6:4)
    TIM1->CCMR1 |=  (0x6U << 4);   // OC1M = 110,PWM模式1
    TIM1->CCMR1 |=  (1U   << 3);   // OC1PE = 1,使能预装载
    // 7. 设置 CCR1 = 500,占空比 50%
    TIM1->CCR1 = 500;
    // 8. 使能通道1输出,极性为高(CC1P = 0 → 高有效)
    TIM1->CCER &= ~(1U << 1);  // CC1P = 0(高极性)
    TIM1->CCER |=  (1U << 0);  // CC1E = 1,使能CH1输出
    // 9. 使能主输出(TIM1 特有,通用定时器没有此步骤!)
    //    MOE = 1,打开主输出使能
    TIM1->BDTR |= (1U << 15);  // MOE = 1
    // 10. 使能自动重载预装载
    //     ARPE = 1(bit 7)
    TIM1->CR1 |= (1U << 7);   // ARPE = 1
    // 11. 产生一次软件更新事件,使预装载值立刻生效
    //     设置 UG = 1(TIMx_EGR bit 0),硬件自动清零
    TIM1->EGR |= (1U << 0);   // UG = 1
    // 12. 启动计数器!
    //     向上计数模式(DIR = 0,默认)
    //     CEN = 1
    TIM1->CR1 |= (1U << 0);   // CEN = 1
}
// ================================================================
// 运行时修改占空比(0~100%)
// ================================================================
void TIM1_SetDutyCycle(uint8_t duty_percent)
{
    if (duty_percent > 100) duty_percent = 100;
    // 占空比 = CCR1 / (ARR + 1)
    // CCR1 = duty_percent% × (ARR + 1)
    uint32_t arr = TIM1->ARR;
    TIM1->CCR1 = (static_cast<uint32_t>(duty_percent) * (arr + 1)) / 100;
}
// ================================================================
// 主函数
// ================================================================
int main()
{
    GPIO_Init_PWM();   // 配置 PA8 为 TIM1_CH1 复用引脚
    TIM1_PWM_Init();   // 初始化 TIM1,输出 1kHz 50% 占空比 PWM
    // 动态改变占空比示例(实际项目中可在主循环中调用)
    // TIM1_SetDutyCycle(25);  // 改为 25% 占空比
    // TIM1_SetDutyCycle(75);  // 改为 75% 占空比
    while (true)
    {
        // 主循环(实际项目中做其他事)
    }
    return 0;
}

示例2:互补输出 + 死区时间(电机驱动)

/*
 * TIM1 互补 PWM + 死区时间示例
 * 在 PA8(TIM1_CH1)和 PB13(TIM1_CH1N)输出互补 PWM
 * 死区时间约 1us(假设 tDTS = 125ns @ 8MHz CKD分频后)
 */
#include <cstdint>
// (寄存器定义同上,省略)
// 假设 TIM1 结构体已定义
// ================================================================
// 配置互补 PWM 和死区
// ================================================================
void TIM1_ComplementaryPWM_Init()
{
    // ---- 时基配置(同上)----
    // PSC = 167,ARR = 999 → 1kHz
    // (省略重复代码,参考上面示例)
    // ---- 通道1:PWM模式1 + 预装载 ----
    TIM1->CCMR1 &= ~(0x7U << 4);
    TIM1->CCMR1 |=  (0x6U << 4);  // PWM模式1
    TIM1->CCMR1 |=  (1U   << 3);  // OC1PE 预装载
    // CCR1 = 500 → 50% 占空比
    TIM1->CCR1 = 500;
    // ---- 使能 CH1(主)和 CH1N(互补)----
    TIM1->CCER &= ~(1U << 1);   // CC1P = 0,主输出高有效
    TIM1->CCER &= ~(1U << 3);   // CC1NP = 0,互补输出高有效
    TIM1->CCER |=  (1U << 0);   // CC1E = 1,使能主输出
    TIM1->CCER |=  (1U << 2);   // CC1NE = 1,使能互补输出
    // ---- 配置死区时间 ----
    // 假设 tDTS = 125ns(CKD=00,DTS=CK_INT=8MHz)
    // 目标死区 = 1us = 8 × 125ns
    // DTG[7:5]=000 → DT = DTG[7:0] × 125ns
    // DTG = 8 → DT = 8 × 125ns = 1us
    //
    // BDTR 低8位为 DTG
    TIM1->BDTR &= ~(0xFFU << 0);   // 清除 DTG[7:0]
    TIM1->BDTR |=  (8U    << 0);   // DTG = 8 → 死区 1us
    // ---- 使能主输出 ----
    TIM1->BDTR |= (1U << 15);  // MOE = 1
    // ---- ARPE + 产生UEV + 启动 ----
    TIM1->CR1 |= (1U << 7);    // ARPE = 1
    TIM1->EGR |= (1U << 0);    // UG = 1,立即更新
    TIM1->CR1 |= (1U << 0);    // CEN = 1,启动
}

十五、完整流程图:初始化 TIM1 PWM

开始

使能 RCC 时钟
AHB1: GPIOA
APB2: TIM1

配置 GPIO 引脚
复用模式 AF1

设置 PSC 预分频器
确定计数时钟频率

设置 ARR 自动重载
确定 PWM 频率

设置 CCRx
确定占空比

配置 CCMRx
选择 PWM 模式1 或 2
使能预装载 OC1PE=1

配置 CCER
使能输出通道 CC1E=1

是否用互补输出?

使能 CC1NE=1
设置死区 DTG
配置 OIS idle状态

使能主输出 MOE=1
仅TIM1/TIM8需要

设置 ARPE=1
使能自动重载预装载

写 UG=1
产生软件更新事件
使预装载立即生效

设置 CEN=1
启动计数器

PWM 输出开始

十六、常见误区和注意事项

误区1:忘记使能主输出(MOE)

TIM1/TIM8 有刹车功能,必须设置 TIM1->BDTR |= (1<<15) 才有输出,通用定时器 TIM2~5 不需要此步骤。
误区2:修改 ARR 后立刻生效导致毛刺
应先使能 ARPE(auto-reload preload enable),这样新值在下一个 UEV 才生效,避免波形异常。
误区3:忘记产生 UEV 初始化
配置完成后要写 TIM1->EGR = 1(UG位),强制把预装载值同步到影子寄存器,否则第一个 PWM 周期可能不正确。
误区4:中央对齐模式下修改 DIR 位
中央对齐模式下 DIR 由硬件自动更新,软件不得修改,否则行为不可预测。
误区5:LOCK 位一旦写入就锁定
TIMx_BDTR 的 LOCK[1:0] 只能在复位后写一次,之后就无法修改被锁定的位,直到下次复位。

十七、寄存器地址速查

TIM1 基地址:0x40010000

偏移 寄存器 说明
0x00 TIMx_CR1 控制寄存器1
0x04 TIMx_CR2 控制寄存器2
0x08 TIMx_SMCR 从模式控制
0x0C TIMx_DIER 中断/DMA使能
0x10 TIMx_SR 状态寄存器
0x14 TIMx_EGR 事件产生
0x18 TIMx_CCMR1 捕获/比较模式1
0x1C TIMx_CCMR2 捕获/比较模式2
0x20 TIMx_CCER 捕获/比较使能
0x24 TIMx_CNT 计数器值
0x28 TIMx_PSC 预分频器
0x2C TIMx_ARR 自动重载值
0x30 TIMx_RCR 重复计数器
0x34 TIMx_CCR1 捕获/比较寄存器1
0x38 TIMx_CCR2 捕获/比较寄存器2
0x3C TIMx_CCR3 捕获/比较寄存器3
0x40 TIMx_CCR4 捕获/比较寄存器4
0x44 TIMx_BDTR 刹车和死区
0x48 TIMx_DCR DMA控制
0x4C TIMx_DMAR DMA全传输

TIM8 基地址:0x40010400,寄存器布局与 TIM1 完全相同。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐