STM32G4 DMA多路复用器实战:任意GPIO触发ADC注入组转换的配置技巧
👁 3 阅读 · 2026-08-27 · 嵌入式
STM32G4系列内置的DMA多路复用器(DMAMUX)为外设触发管理带来了革命性变化,允许开发者将任意GPIO事件映射为ADC注入组的转换触发源,从而摆脱固定触发通道的限制。本文深入剖析DMAMUX的触发路由原理,详细讲解从GPIO配置到ADC注入组设置的完整流程,并给出可直接落地的代码示例与常见陷阱规避指南,助力嵌入式开发者灵活构建事件驱动的数据采集系统。
# 用 DMA 多路复用器实现 STM32G4 任意 GPIO 触发 ADC 注入组转换的配置技巧
## 引言
在嵌入式系统中,ADC 转换的触发方式往往决定了系统的实时性与灵活性。传统 STM32 的 ADC 触发源固定(如定时器、EXTI 等),而 STM32G4 系列引入的 DMA 多路复用器(DMAMUX)打破了这一限制,它允许将任意 GPIO 的外部中断请求(EXTI)映射为 ADC 注入组的触发信号。这意味着你可以用按键、传感器数字输出甚至自定义逻辑信号直接启动高优先级注入转换,无需占用定时器资源。本文将带你从原理到实践,掌握这一高级配置技巧。
## 原理剖析:DMAMUX 的触发路由机制
DMAMUX 的核心功能是作为 DMA 请求的“路由器”,但它还内置了一个触发生成器(Trigger Generator)。该生成器可以监听多个输入源(包括 EXTI 线、定时器事件、LPTIM 等),并在检测到指定边沿时产生一个 DMA 请求信号。
对于 ADC 注入组,其触发源可以配置为 DMA 触发(通过 `ADC_CFGR2` 的 `JEXTEN` 和 `JEXTSEL` 字段),而 DMA 触发信号正是由 DMAMUX 的触发生成器提供的。因此,链路为:
- GPIO 输入边沿 → EXTI 事件 → DMAMUX 触发生成器 → DMA 请求 → ADC 注入组转换启动。
关键点在于:DMAMUX 的触发生成器支持选择 EXTI 线(EXTI0~EXTI15),并可配置上升沿、下降沿或双边沿触发。这比直接使用 EXTI 中断更高效,因为无需 CPU 介入,转换完全由硬件自动完成。
## 硬件与软件准备
- 硬件:任何 STM32G4 系列开发板(如 NUCLEO-G474RE)
- 软件:STM32CubeIDE(或 Keil)+ STM32CubeMX 初始化代码
- 外设:一个 GPIO 输入引脚(如 PA0),一个 ADC 通道(如 ADC1_IN1)
## 配置步骤
### 1. GPIO 配置
首先,将 PA0 配置为输入模式,并使能其 EXTI 线。注意,这里不需要使能 EXTI 中断,因为 DMAMUX 直接监听事件(而非中断)。
```c
// 使能 GPIOA 时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 或上升沿/双边沿
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能 EXTI 线 0 的事件(注意:不是中断)
HAL_NVIC_EnableIRQ(EXTI0_IRQn); // 实际上不需要,但为了保险可开启,但中断处理函数留空
```
### 2. DMAMUX 触发生成器配置
在 STM32CubeMX 中,找到 DMAMUX 配置,启用 Trigger Generator,并选择输入源为 EXTI0,触发边沿为下降沿。若使用代码,则需操作 DMAMUX 寄存器。
```c
// 使能 DMAMUX 时钟
__HAL_RCC_DMAMUX1_CLK_ENABLE();
// 配置 DMAMUX 触发生成器 0(假设使用 Trigger Gen 0)
DMAMUX_Channel_TypeDef *dmamux_ch = DMAMUX1_Channel0; // 实际使用哪个通道取决于 DMA 映射
// 这里我们使用 DMAMUX1_Channel0 作为触发源,但更常见的是使用专用触发寄存器
// 实际上,DMAMUX 的触发生成器通过 DMAMUX1_TriggerGen 寄存器配置
DMAMUX1_TriggerGen0->CR = DMAMUX_TGCR_TRIGEN | DMAMUX_TGCR_TRIGSEL_EXTI0 | DMAMUX_TGCR_TRIGPOL_FALLING;
```
更简洁的方式是使用 HAL 库函数:
```c
DMAMUX_TriggerGenerator_TypeDef *tg = DMAMUX1_TriggerGen0;
tg->CR = DMAMUX_TGCR_TRIGEN | DMAMUX_TGCR_TRIGSEL_EXTI0 | DMAMUX_TGCR_TRIGPOL_FALLING;
```
### 3. DMA 配置(用于触发 ADC)
虽然我们不需要 DMA 搬运数据,但 ADC 的触发信号必须经过 DMA 请求线。因此,需要配置一个 DMA 通道,但将其设置为循环模式且数据长度为半字,并禁用内存地址递增,这样它只会产生触发信号而不会实际搬运有效数据(或者我们也可以利用 DMA 将 ADC 数据搬到内存,但这里聚焦触发)。
```c
DMA_HandleTypeDef hdma_adc1;
hdma_adc1.Instance = DMA1_Channel1; // 选择 DMA1 通道1
hdma_adc1.Init.Request = DMA_REQUEST_TRIG; // 关键:请求源为触发生成器
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_DISABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式,持续响应触发
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
// 关联 DMA 到 ADC 的触发请求
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
```
注意:`DMA_REQUEST_TRIG` 是 STM32G4 特有的,表示 DMA 请求由 DMAMUX 触发生成器产生。
### 4. ADC 注入组配置
配置 ADC 的注入组,使其由 DMA 触发启动。
```c
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 注入组通常单通道
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
hadc1.Init.LowPowerAutoWait = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE; // 非连续,由外部触发
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T8_TRGO; // 此处会被覆盖,但需占位
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 由 DMA 触发,边沿由 DMAMUX 决定
hadc1.Init.DMAContinuousRequests = ENABLE; // 使能 DMA 请求
HAL_ADC_Init(&hadc1);
// 配置注入组通道
ADC_InjectionConfTypeDef sConfigInjected = {0};
sConfigInjected.InjectedChannel = ADC_CHANNEL_1; // PA0 对应的 ADC 通道?注意:PA0 是 ADC1_IN1,但这里我们用另一个引脚如 PC0 作为模拟输入,PA0 作为触发输入
sConfigInjected.InjectedRank = 1;
sConfigInjected.InjectedNbrOfConversion = 1;
sConfigInjected.InjectedSamplingTime = ADC_SAMPLINGTIME_COMMON_1;
sConfigInjected.ExternalTrigInjecConv = ADC_EXTERNALTRIGINJECCONV_T8_TRGO; // 占位,实际由 DMA 触发
sConfigInjected.AutoInjectedConv = DISABLE;
sConfigInjected.InjectedDiscontinuousConvMode = DISABLE;
sConfigInjected.InjectedOffset = 0;
HAL_ADCEx_InjectedConfigChannel(&hadc1, &sConfigInjected, ADC_INJECTED_SOFTWARE_START); // 注意:这里不能用软件启动,但函数要求参数,我们传软件启动,之后会修改触发源
```
关键:在 `HAL_ADCEx_InjectedConfigChannel` 之后,我们需要手动修改 ADC 的触发源为 DMA 触发。直接操作寄存器:
```c
ADC1->CFGR2 |= ADC_CFGR2_JEXTEN_0; // 使能注入组外部触发(上升沿有效,但实际由 DMA 信号驱动)
ADC1->CFGR2 &= ~ADC_CFGR2_JEXTSEL; // 清除选择位
ADC1->CFGR2 |= (0b011 << ADC_CFGR2_JEXTSEL_Pos); // 选择 DMA 触发(具体值参考参考手册,通常为 011)
```
### 5. 启动与中断处理
启用 ADC 注入组转换完成中断,以便读取结果。
```c
HAL_ADCEx_InjectedStart_IT(&hadc1); // 启动注入组,但注意触发方式为外部,所以不会立即转换
```
在中断回调中读取数据:
```c
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
uint32_t value = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
// 处理数据
}
```
## 完整代码示例
以下是一个精简的初始化函数,整合了上述步骤:
```c
void GPIO_Trigger_ADC_Init(void)
{
// 1. GPIO 触发引脚 PA0
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 2. DMAMUX 触发生成器
__HAL_RCC_DMAMUX1_CLK_ENABLE();
DMAMUX1_TriggerGen0->CR = DMAMUX_TGCR_TRIGEN | DMAMUX_TGCR_TRIGSEL_EXTI0 | DMAMUX_TGCR_TRIGPOL_FALLING;
// 3. DMA 配置
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Request = DMA_REQUEST_TRIG;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc1.Init.MemInc = DMA_MINC_DISABLE;
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
// 4. ADC 配置(略,见上文)
// ... 初始化 hadc1 和注入通道,然后修改 CFGR2
// 5. 启动注入组中断
HAL_ADCEx_InjectedStart_IT(&hadc1);
}
```
## 注意事项与常见陷阱
- **触发源选择**:`JEXTSEL` 的值必须与参考手册中的 DMA 触发对应,不同系列可能不同,务必查阅 STM32G4 参考手册(如 RM0440)的 ADC 章节。
- **DMA 通道占用**:虽然我们只用 DMA 产生触发,但该 DMA 通道会被占用,无法用于数据传输。如果同时需要 ADC 数据搬运,可考虑使用 DMA 的循环模式将数据存入缓冲区,但需注意内存递增设置。
- **GPIO 与 ADC 通道冲突**:PA0 通常也是 ADC 输入,如果用作触发,则不能同时作为模拟输入。建议使用不同引脚。
- **边沿极性**:DMAMUX 触发生成器的边沿与 GPIO 的 EXTI 边沿设置无关,需独立配置。确保两者一致,否则可能无触发。
- **中断优先级**:如果使用了 EXTI 中断(虽然不必要),注意与 ADC 中断的优先级关系,避免死锁。
- **调试技巧**:使用逻辑分析仪观察 GPIO 边沿和 ADC 转换完成标志,确认触发链路是否畅通。
## 结语
通过 DMAMUX 的触发生成器,STM32G4 实现了 GPIO 到 ADC 注入组的灵活映射,极大简化了事件驱动采样设计。本文的配置方法不仅适用于 ADC,还可扩展到其他外设(如定时器、DAC)。掌握这一技巧,你将能构建更高效、更灵活的嵌入式系统。