STM32 HAL 进阶:DMA + ADC 多通道连续采集
目标
使用 STM32F407 的 ADC1 + DMA 实现 4 通道连续采样(无需 CPU 参与传输),配合 TIM2 触发实现精确采样率。
硬件配置(CubeMX)
- MCU:STM32F407VGT6
- ADC1:4 通道(PA0=CH0, PA1=CH1, PA2=CH2, PA3=CH3)
- DMA2 Stream0:外设到内存,循环模式,半字(16bit)
- TIM2:PWM 输出触发 ADC(10kHz 采样率)
- USART2:115200bps,DMA 发送
完整代码
#include "main.h"
#define ADC_CHANNELS 4
#define ADC_BUFFER_SIZE (ADC_CHANNELS * 256) /* 每通道 256 个样本 */
#define SAMPLE_RATE_HZ 10000
static __attribute__((aligned(4))) uint16_t adc_buffer[ADC_BUFFER_SIZE];
static volatile uint8_t adc_half_complete = 0;
static volatile uint8_t adc_full_complete = 0;
static float channel_voltage[ADC_CHANNELS];
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
adc_half_complete = 1;
}
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
adc_full_complete = 1;
}
}
static void process_adc_data(uint16_t *buf, uint32_t len) {
float sum[ADC_CHANNELS] = {0};
uint32_t count_per_channel = len / ADC_CHANNELS;
for (uint32_t ch = 0; ch < ADC_CHANNELS; ch++) {
for (uint32_t i = 0; i < count_per_channel; i++) {
sum[ch] += buf[i * ADC_CHANNELS + ch];
}
float avg_raw = sum[ch] / count_per_channel;
channel_voltage[ch] = (avg_raw * 3300.0f) / 4095.0f;
}
}
static void print_results(void) {
char msg[256];
int len = snprintf(msg, sizeof(msg),
"CH0=%.1fmV | CH1=%.1fmV | CH2=%.1fmV | CH3=%.1fmV | Fs=%dHz\r\n",
channel_voltage[0], channel_voltage[1],
channel_voltage[2], channel_voltage[3],
SAMPLE_RATE_HZ);
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)msg, len);
}
static void adc_dma_init(void) {
HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
HAL_ADC_Start_DMA(&hadc1,
(uint32_t *)adc_buffer,
ADC_BUFFER_SIZE);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
printf("ADC + DMA 初始化完成 | %d 通道 @ %d Hz | 缓冲区 %d 样本\r\n",
ADC_CHANNELS, SAMPLE_RATE_HZ, ADC_BUFFER_SIZE);
}
int main(void) {
HAL_Init();
SystemClock_Config(); /* CubeMX */
MX_GPIO_Init(); /* CubeMX */
MX_DMA_Init(); /* CubeMX */
MX_ADC1_Init(); /* CubeMX */
MX_TIM2_Init(); /* CubeMX — TIM2 CH1 → ADC Trigger */
MX_USART2_UART_Init(); /* CubeMX */
adc_dma_init();
while (1) {
if (adc_half_complete) {
adc_half_complete = 0;
process_adc_data(adc_buffer, ADC_BUFFER_SIZE / 2);
print_results();
}
if (adc_full_complete) {
adc_full_complete = 0;
process_adc_data(
adc_buffer + ADC_BUFFER_SIZE / 2,
ADC_BUFFER_SIZE / 2);
print_results();
}
}
}
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc) {
if (hadc == &hadc1) {
uint32_t err = HAL_ADC_GetError(hadc);
printf("ADC 错误: 0x%08lx\r\n", err);
HAL_ADC_Stop_DMA(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE);
}
}
关键 CubeMX 设置
| 外设 |
配置 |
| ADC1 |
Scan Conversion Mode: Enabled, Continuous Conversion: Disabled, DMA Continuous Requests: Enabled, Number of Conversions: 4 |
| ADC1 Rank 1-4 |
Channel 0-3, Sampling Time: 480 Cycles |
| TIM2 |
Period: 8399 (84MHz / 8400 = 10kHz), CH1: PWM Mode 1, Pulse: 4200 |
| ADC Trigger |
Timer 2 Capture Compare 1 event |
| DMA2 Stream0 |
Direction: Peripheral to Memory, Circular, Half Word, Priority: High |
预期行为
- CPU 零参与 ADC 数据搬运(全部 DMA 自动完成)
- 串口以约 20Hz 频率输出 4 通道的电压平均值
- 双缓冲机制:处理前半时 DMA 继续填充后半,零数据丢失
关键点
- 双缓冲(Half/Full callback)是 DMA 连续采集的关键模式——处理 & 采集并行
- TIM 触发 ADC 确保精确等间隔采样(采样抖动 < 1 个时钟周期)
HAL_ADC_Start_DMA 启动后无需 CPU 干预,直到回调触发
__attribute__((aligned(4))) 确保缓冲区对齐满足 DMA 要求
STM32 HAL:GPIO 流水灯与按键控制
目标
基于 STM32CubeMX 生成的 HAL 工程,实现 3 个 LED 流水灯和按键切换流水方向。
硬件配置(CubeMX)
- MCU:STM32F407VGT6(Nucleo-F407ZG,其他 F4/F1/H7 类似)
- LED1-3:PD12、PD13、PD14(Nucleo 板载 LED 为 LD4=PD12, LD5=PD13, LD6=PD14)
- 按键:PC13(Nucleo 蓝色用户按键),上拉输入,按下为低电平
- 时钟:HSE 8MHz → PLL 168MHz(F407 最大)
完整代码
#include "main.h"
extern TIM_HandleTypeDef htim2; /* 用于非阻塞定时 */
#define LED_COUNT 3
const uint16_t LED_PINS[LED_COUNT] = {
GPIO_PIN_12,
GPIO_PIN_13,
GPIO_PIN_14
};
GPIO_TypeDef *const LED_PORT = GPIOD;
#define KEY_PORT GPIOC
#define KEY_PIN GPIO_PIN_13
typedef enum { FORWARD, REVERSE } Direction;
static Direction dir = FORWARD;
static uint8_t active_led = 0;
static uint32_t last_toggle_tick = 0;
static uint32_t last_key_check = 0;
void led_init(void) {
for (int i = 0; i < LED_COUNT; i++) {
HAL_GPIO_WritePin(LED_PORT, LED_PINS[i], GPIO_PIN_RESET);
}
HAL_TIM_Base_Start(&htim2);
}
uint8_t key_pressed(void) {
static uint8_t last_state = 1;
uint8_t now = HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN);
if (last_state == 1 && now == 0) { // 下降沿
HAL_Delay(20); // 消抖
if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == 0) {
last_state = now;
return 1; // 有效按下
}
}
last_state = now;
return 0;
}
void led_shift(void) {
for (int i = 0; i < LED_COUNT; i++) {
HAL_GPIO_WritePin(LED_PORT, LED_PINS[i], GPIO_PIN_RESET);
}
HAL_GPIO_WritePin(LED_PORT, LED_PINS[active_led], GPIO_PIN_SET);
if (dir == FORWARD) {
active_led = (active_led + 1) % LED_COUNT;
} else {
active_led = (active_led == 0) ? (LED_COUNT - 1) : (active_led - 1);
}
}
int main(void) {
HAL_Init();
SystemClock_Config(); // CubeMX 生成
MX_GPIO_Init(); // CubeMX 生成
MX_TIM2_Init(); // CubeMX 生成,TIM2 用于基准计时
led_init();
uint32_t now;
while (1) {
now = HAL_GetTick();
if (now - last_toggle_tick >= 200) {
last_toggle_tick = now;
led_shift();
}
if (now - last_key_check >= 10) {
last_key_check = now;
if (key_pressed()) {
dir = (dir == FORWARD) ? REVERSE : FORWARD;
for (int i = 0; i < 2; i++) {
HAL_GPIO_WritePin(LED_PORT, LED_PINS[1], GPIO_PIN_SET);
HAL_Delay(50);
HAL_GPIO_WritePin(LED_PORT, LED_PINS[1], GPIO_PIN_RESET);
HAL_Delay(50);
}
}
}
}
}
运行步骤
- STM32CubeMX 打开
.ioc → 引脚配置 PD12-14 为 Output,PC13 为 Input(上拉)
- TIM2 使能(用作
HAL_GetTick() 的时基),生成代码
- 将上述
main.c 替换 CubeMX 生成的 main 函数体
- 编译下载:
arm-none-eabi-gcc 或 CubeIDE → Run
- 观察 LED 流水效果,按蓝色用户按键切换方向
预期行为
- 3 个 LED 以 200ms 间隔依次点亮形成流水效果
- 按一次按键,方向反转(逆流水)
- 切换时中间 LED 快闪两次确认
关键点
HAL_GetTick() 由 SysTick 中断驱动(默认 1ms),用作非阻塞定时基准
- 按键消抖用硬件延时 20ms + 二次确认
- 避免在中断或主循环中使用
HAL_Delay 做长延时(阻塞型)