# 用 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)。掌握这一技巧,你将能构建更高效、更灵活的嵌入式系统。