DMA 是被动搬运工:以 CH32X035 的 TIM1_UP + 通道 5 驱动 WS2812 为例
结论速览
- DMA 不会自己动:通道 EN + 请求使能 + 事件真的发生,三个条件缺一不可。
- 请求源不是”默认”的,是硬件焊死的映射:TIM1 的更新事件
TIM1_UP只连在 DMA1 通道 5 上。 - DMA 需要的不是”消耗方”而是请求方:目的寄存器
CH1CVR只是搬运地址,它不会主动来取数据。 - 唯一的例外是存储器到存储器模式(
MEM2MEM=1),只有那种模式才”开了 EN 就自己连搬”。 - 只处理
TC不处理TE,一次传输错误就会让忙标志永远停在”忙”,整层静默假死。
一、先补齐概念:DMA 靠什么动起来
1.1 一次搬运的三个条件
以”TIM1 更新事件把数据搬进 TIM1->CH1CVR“为例:
| 条件 | 对应代码 | 缺了会怎样 |
|---|---|---|
| ① 通道使能 | DMA_Cmd(ch, ENABLE) |
通道关闭,请求来了也搬不了 |
| ② 请求源已打开 | TIM_DMACmd(TIM1, TIM_DMA_Update, ENABLE) |
即使事件发生了,也不会转成 DMA 请求 |
| ③ 事件真的发生 | TIM_Cmd(TIM1, ENABLE) 计数器在跑 |
没有更新事件 → 没有请求 → 一个半字都不搬 |
DMA_Cmd(ENABLE) 只是让通道待命:CNTR 保持满值,源/目的地址一动不动。所以启动顺序必须是「先备好 DMA,再开 TIM1」。
1.2 请求线是硬件固定的:请求映射表
每个外设事件的请求线在芯片内部是硬连线,软件改不了。CH32X035 手册 9.2.3「DMA 请求映射」里 TIM1 一行:
| 请求源 | 硬件连到的通道 |
|---|---|
TIM1_CH1(比较/捕获 1) |
通道 2 |
TIM1_CH2 |
通道 3 |
TIM1_CH4 / TIM1_TRIG / TIM1_COM |
通道 4 |
TIM1_UP(更新事件) |
通道 5 |
TIM1_CH3 |
通道 6 |
有两点必须记住:
- “用哪个通道”由”要哪个事件”决定,不是自由挑的。选中
TIM1_UP,通道就只能是 5。 - 定时器通道号和 DMA 通道号没有对应关系:定时器的 CH2 事件落在 DMA 通道 3,而 DMA 通道 2 接的却是定时器 CH1。三处共用通道 4 也一样反直觉。
1.3 术语速查:UP / TRIG / COM / CCx / CCR
| 缩写 | 英文全称 | 中文 | 说明 |
|---|---|---|---|
UP |
Update event | 更新事件 | 计数器上溢/下溢(或软件置 UG)重装寄存器时产生,手册里写作 UEV |
TRIG |
Trigger event | 触发事件 | 软件置 TG 位产生,或从模式控制器识别到触发输入时产生 |
COM |
Commutation event | 换相事件 | 高级定时器专有,软件置 COMG 产生;用于批量更新 CCxE/CCxNE/OCxM,BLDC 六步换相靠它 |
CC1~`CC4` |
Capture/Compare channel 1~4 | 捕获比较通道 | 定时器的 4 个通道,各自方向独立(输入捕获或输出比较) |
CCR |
Capture/Compare Register | 捕获/比较寄存器 | 本芯片写作 TIM1->CH1CVR,STM32 里叫 TIMx->CCR1,同一个东西 |
CCR 是一个寄存器、两种身份,由通道方向决定:
- 输出比较 / PWM:
CNT数到 CCR 就翻转输出 → 决定占空比; - 输入捕获:捕获到边沿时,硬件把当时的
CNT复制进 CCR → 当时间戳用(测脉宽/周期)。
ARR 与 CCR 的分工是:ARR 决定周期/频率,CCR 决定高电平长度。以本工程 CK_CNT = 8MHz、ARR = 9 为例:
| 值 | 计数 | 时间 | 含义 |
|---|---|---|---|
ARR = 9 |
10 | 1.25µs | 位周期 |
CODE_1 = 7 |
7 | 0.875µs | 位 1 的高电平 |
CODE_0 = 3 |
3 | 0.375µs | 位 0 的高电平 |
CCR 还是双缓冲的:写入先落到预装载寄存器,到下一个更新事件才搬进活动寄存器生效(TIM_OCPreload_Enable 就是打开这个行为)。WS2812 正需要这一点——DMA 可以随时写,而新值统一在周期边界生效,不会把高电平从中间截断。
1.4 谁在什么时候”喊”:各事件的触发时机
“喊”的是硬件事件,不是代码。软件只做两件事,之后每次事件自动搬一次:
1 | TIM_DMACmd(TIM1, TIM_DMA_Update, ENABLE); /* ① 打开“哪个事件”的请求使能 */ |
| 请求源 | DMA 通道 | 何时产生 | 本工程时基下(8MHz / ARR=9) | 使能 API |
|---|---|---|---|---|
TIM1_UP |
5 | 每次计数器溢出/重装 | 800kHz(每 1.25µs) | TIM_DMA_Update |
TIM1_CH1(CC1) |
2 | CNT == CCR1(比较匹配)或捕获到边沿 |
每周期最多 1 次,相位由 CCR1 决定 | TIM_DMA_CC1 |
TIM1_CH2(CC2) |
3 | 同上,用 CCR2 | 同上 | TIM_DMA_CC2 |
TIM1_CH3(CC3) |
6 | 同上,用 CCR3 | 同上 | TIM_DMA_CC3 |
TIM1_CH4(CC4) |
4 | 同上,用 CCR4 | 同上 | TIM_DMA_CC4 |
TIM1_TRIG |
4 | 软件置 TG / 触发输入 |
偶发 | 与 CC4 共用 |
TIM1_COM |
4 | 软件置 COMG / 换相 |
偶发 | 与 CC4 共用 |
两个坑:
- DMA 通道 4 被 CC4 / TRIG / COM 三个源共用,同时只能用一个,否则请求混在一起分不清是谁发的。
- 一次请求只搬一个数据单元(这里是 1 个半字),不是一整帧;一帧 156 个数据要 156 次请求。
1.5 中断事件:TC / TE / HT / GL
DMA 一共四种事件:
| 缩写 | 全称 | 含义 |
|---|---|---|
TC |
Transfer Complete | 传输完成,设定次数搬完了 |
TE |
Transfer Error | 传输错误,访问了保留/非法地址等 |
HT |
Half Transfer | 半传输,搬完一半,用于双缓冲 |
GL |
Global | 全局,上面几种的汇总位 |
它们有两套位,很容易看混:
1 | /* CFGR 里的“中断使能位”——哪种事件能触发中断,给 DMA_ITConfig 用 */ |
所以「开中断」和「查标志」是两步:
1 | DMA_ITConfig(WS2812_DMA_CH, DMA_IT_TC | DMA_IT_TE, ENABLE); /* 让 TC/TE 都能进中断 */ |
状态读 DMA_INTFR,清标志写 DMA_INTFCR(写 1 清除)。
1.6 传输错误会自己把通道关掉
这一点决定了”只处理 TC”是危险的。手册原文:发生传输错误时 TEIFx 由硬件置位,同时硬件自动清 0 该通道 CCRx 的 EN 位,通道被关闭;只有置位了 TEIE 才会产生中断。
推论:TE 一旦发生,通道就停了,永远不会再有 TC。如果代码只清 TC 标志、只处理 TC 中断,那么”忙”标志就再也回不到空闲。
二、把机制套到 WS2812 上
2.1 为什么是 TIM1_UP + 通道 5
WS2812 每一位占 1.25µs,需要每个位周期更新一次比较值。TIM1 的时基被设成:
1 | PSC = 5 -> CK_CNT = 48MHz / 6 = 8MHz |
计数器每 1.25µs 溢出一次,正好产生一次更新事件——和位周期天然对齐。请求源选 TIM1_UP,按 1.2 的映射表,通道就是 DMA1 通道 5。
配置三件套:
1 | BspTim1BaseInit(); /* PSC/ARR/启动计数器 */ |
缓冲区长度 = 灯珠数 × 24 + 复位槽:
1 |
60 个复位槽输出常低,60 × 1.25µs = 75µs,满足 WS2812 要求的 >50µs 复位间隔。
2.2 每 1.25µs 搬一个:运行时闭环
1 | TIM1 计数器溢出 -> 置更新事件 UEV -> (UDE=1)向 DMA 通道 5 发一次请求 |
一趟 156 次搬运就是送完一帧。启动函数要做的收尾:
1 | DMA_SetCurrDataCounter(WS2812_DMA_CH, WS2812_SLOT_NUM); /* Normal 模式发完即停,每次都要重装 */ |
顺序不能反:反过来的话计数器已经在跑,第一个更新事件可能早于 DMA 就绪,前面几个槽位会漏掉。
2.3 为什么用更新事件,而不是 CH1 比较事件
TIM_DMA_CC1(通道 2)也能触发搬运,但两个方案的生效时机不同。这里用的是更新事件 + TIM_OCPreload_Enable:DMA 写进 CH1CVR 的值先落到预装载寄存器,到下一个更新事件才生效,节拍严格按位周期走。
而用 CC1 事件的话,比较匹配发生在 CNT == CH1CVR 的那一刻——CH1CVR 正是 DMA 搬进去的数据,数据一变匹配点就跟着移,位宽会随 0/1 序列抖动,WS2812 会认错位。
代价是复位后第一个 1.25µs 周期输出的还是 compare=0(低电平),第一个数据位从第 2 个周期才出现——等于帧前多了一段低电平,对 WS2812 只有好处(复位时间更长),没有坏处。
三、工程里的三个易错点
3.1 只开 DMA 不开 TIM1,一个字节都不会搬
除了 MEM2MEM=1 的存储器到存储器传输,DMA 一律需要外设请求。DMA_M2M_Disable 下开 EN 只是待命,CNTR 保持满值。
对照实验:如果初始化里把 DMA_Cmd(ENABLE) 和 TIM_Cmd(TIM1, ENABLE) 都做了,上电就会自动搬一帧(缓冲区全 0,输出一路低电平)——这也是为什么启动动作应该统一收进 Show(),而不是留在初始化末尾。
3.2 只处理 TC 不处理 TE,忙标志会卡死
1 | /* 收尾动作对 TC/TE 是一样的 */ |
漏掉 TE 的表现很隐蔽:灯突然不亮了,Show() 一直返回 E_BUSY,看起来像”卡在发送中”,其实是通道已被硬件关掉、TC 永远不会来。
顺带一个取舍:收尾时**只关本通道 DMA,不要 TIM_Cmd(TIM1, DISABLE)**。DMA 停了、CH1CVR=0,输出自然保持低电平;而关整个 TIM1 会连坐同一时基下将来可能启用的 CH2/CH3。
3.3 宏名别叫 TIM_DMA_CH1_CH
这个通道绑的是 TIM1_UP(更新事件),不是 TIM1_CH1。名字写成 TIM_DMA_CH1_CH 会让人以为它对应比较捕获通道 1,排查问题时按”CH1 的请求在通道 2”去找,方向就偏了。叫 WS2812_DMA_CH 或 TIM1_UP_DMA_CH 更准确。
