STM32F4系列Reference manual学习:STM32F4xx 中断与事件系统
参考手册:RM0090,第12章
适用芯片:STM32F405xx / F407xx / F415xx / F417xx / F42xxx / F43xxx
目录
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 和处理器核心紧密耦合,这使得:
- 中断响应延迟极低(通常只需几个时钟周期)
- 即使中断来得很晚(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 位
- 在外设控制寄存器中使能中断,但不在 NVIC 中使能
- 设置 Cortex-M4 系统控制寄存器中的
SEVONPEND位为 1 - 外设触发后 CPU 从 WFE 唤醒
- 唤醒后必须手动清除外设中断 pending 位和 NVIC IRQ pending 位
方式二:通过 EXTI 事件模式(推荐) - 将 EXTI 线配置为事件模式(写 EXTI_EMR)
- 外部信号到来后 CPU 自动唤醒
- 不需要清除 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)
6.4 寄存器操作位运算速查
理解代码中大量的位操作,核心就三种:
置位(Set): reg ∣ = ( 1 ≪ n ) \text{置位(Set):} \quad \text{reg} \mathrel{|}= (1 \ll n) 置位(Set):reg∣=(1≪n)
清位(Clear): reg & = ∼ ( 1 ≪ n ) \text{清位(Clear):} \quad \text{reg} \mathrel{\&}= \sim(1 \ll n) 清位(Clear):reg&=∼(1≪n)
翻转(Toggle): reg ⊕ = ( 1 ≪ n ) \text{翻转(Toggle):} \quad \text{reg} \mathrel{\oplus}= (1 \ll n) 翻转(Toggle):reg⊕=(1≪n)
其中 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
目录
- ADC 是什么?基本概念
- ADC 主要特性总览
- ADC 功能描述
- 转换模式详解
- 数据对齐
- 采样时间与转换时间
- 外部触发
- 数据管理(DMA)
- 多ADC模式
- 温度传感器与VBAT
- ADC 中断
- 寄存器详解
- 完整 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+−VREF−VIN−VREF−×(212−1)
例如, 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+} VREF−≤VIN≤VREF+ |
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.8V≤VREF+≤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.4V≤VDDA≤3.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 n≤8)。
规则组不连续模式示例( 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):
- 规则组正在转换时,若注入触发发生
- 当前规则通道转换被立即中断复位
- 切换到注入序列(扫描一次模式)全部转换完
- 规则组从被中断的位置恢复继续
时序图:
规则组 ──[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_SMPR2 的 SMP[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周期12位10位8位6位
最快转换时间(最小采样时间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 周期12位10位8位6位
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:
ADC_SR.OVR = 1(置位)- DMA 请求被禁止(防止继续搬运无效数据)
- 如果
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。
使用步骤:
- 选择对应 ADC 通道(F40x: IN16;F42x: IN18)
- 设置采样时间大于数据手册规定的最小值
- 置位
ADC_CCR.TSVREFE = 1(唤醒传感器) - 设置
SWSTART = 1开始转换 - 读取
ADC_DR得到 V S E N S E V_{SENSE} VSENSE(数字值) - 用以下公式换算温度:
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_SlopeVSENSE−V25+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)。
启动时序优化:建议同时设置ADON和TSVREFE,以减少总启动等待时间。
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):规则组转换结果,只读,读取后自动清除 EOCADC_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)
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+⎩ ⎨ ⎧12108612位10位8位6位(单位: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=2n−1ADC_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_SlopeVSENSE−V25+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 清除 DMAUDRx 标志
- 清除 DMA 流的 DMAEN 位
- 重新初始化 DMA 和 DAC
- 降低触发频率或减轻 DMA 负载
- 重新使能 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)
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. 整体数据流
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. 中断系统
中断寄存器三层结构:
原始状态 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_Width−1
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_Height−1
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+HBP−1
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+VBP−1
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_Width−1
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_Height−1
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+HFP−1
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+VFP−1
为什么是累积值? 这样硬件内部只需要一个计数器和几个比较器,逻辑更简单。
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=8−1=7=0x7,VSH=4−1=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+7−1=14=0xE,AVBP=4+2−1=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+640−1=654=0x28E,AAH=4+2+480−1=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+6−1=660=0x294,TOTALH=4+2+480+2−1=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=0x02940000∣0x000001E7=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 255240≈0.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+(1−0.94)×48=120.32+2.88≈123
六、像素格式——内存里的数据怎么存?
帧缓冲区可以用多种格式存储像素,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 总线发生错误 |
九、编程步骤流程图
十、完整 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+Width−1
误区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_INT−1=1MHz168MHz−1=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)
更新事件是定时器的一个重要信号,触发时会:
- 预分频缓冲值 → 生效
- ARR 预装值 → 生效
- RCR 重载
- 置位 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为例):
- CC1S = 01(TIMx_CCMR1),把通道1映射到 TI1 输入
- 设置 IC1F(滤波,消抖)
- 设置 CC1P/CC1NP(检测哪种边沿)
- 设置 IC1PS(输入预分频)
- CC1E = 1(使能捕获)
- 可选: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)检测到时钟失效
刹车发生时的行为:
- MOE 位异步清零(即使没有时钟也有效!)
- 所有输出立刻进入 OISx/OISxN 设定的安全状态
- BIF 标志位置位,可触发中断
- 如果 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
十六、常见误区和注意事项
误区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 完全相同。
更多推荐

所有评论(0)