一、引言
在很多嵌入式系统应用中,需要对多路模拟信号进行周期性采样,例如在工业控制、环境监测等领域。STM32F407 是一款高性能的微控制器,其丰富的外设资源可以方便地实现这样的功能。通过结合 ADC(模拟 - 数字转换器)、TIM(定时器)和 DMA(直接内存访问),可以高效、稳定地完成多路模拟信号的周期性采样。
二、硬件连接
在使用 STM32F407 进行 ADC 采样时,需要将外部模拟信号连接到对应的 ADC 通道引脚。以下是一个常见的硬件连接示例:
信号 | STM32F407 引脚 | ADC 通道 |
---|---|---|
模拟信号 1 | PA0 | ADC1 通道 0 |
模拟信号 2 | PA1 | ADC1 通道 1 |
模拟信号 3 | PA2 | ADC1 通道 2 |
三、原理分析
1. ADC(模拟 - 数字转换器)
ADC 的作用是将模拟信号转换为数字信号。STM32F407 的 ADC 具有 12 位分辨率,可对输入的模拟电压进行转换,转换结果范围为 0 - 4095(2^12 - 1)。在本应用中,我们使用 ADC1 来对 3 路模拟信号进行采样。
2. TIM(定时器)
定时器用于产生周期性的触发信号,控制 ADC 的采样频率。在本应用中,我们希望每 1S 采样一次,因此需要配置定时器的周期为 1S。
3. DMA(直接内存访问)
DMA 可以在不占用 CPU 的情况下,将 ADC 转换结果直接传输到内存中。这样可以提高系统的效率,避免 CPU 频繁参与数据传输。
四、代码实现
1. 初始化代码
#include "stm32f4xx_hal.h"
// 定义全局变量
// hadc1 用于存储 ADC1 的句柄,通过这个句柄可以操作 ADC1 外设
ADC_HandleTypeDef hadc1;
// hdma_adc1 用于存储 DMA2 流 0 的句柄,用于控制 ADC1 数据的 DMA 传输
DMA_HandleTypeDef hdma_adc1;
// htim2 用于存储定时器 2 的句柄,用于产生 ADC 转换的触发信号
TIM_HandleTypeDef htim2;
// 采样数据数组,用于存储 ADC 转换后的 3 路数据
uint16_t adc_values[3];
// 定时器初始化函数,用于配置定时器 2 产生 1S 的定时信号
void TIM2_Init(void)
{
// 定义定时器时钟源配置结构体,用于配置定时器的时钟源
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
// 定义定时器主模式配置结构体,用于配置定时器的主模式输出触发等
TIM_MasterConfigTypeDef sMasterConfig = {0};
// 指定定时器 2 作为操作对象
htim2.Instance = TIM2;
// 定时器预分频器,将定时器时钟频率进行分频
// 8400 - 1 表示将定时器时钟频率分频为原来的 1/8400
htim2.Init.Prescaler = 8400 - 1;
// 定时器计数模式设置为向上计数,即从 0 开始递增计数
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
// 定时器周期,当计数器计数到这个值时产生更新事件
// 10000 - 1 结合预分频器实现 1S 的定时
htim2.Init.Period = 10000 - 1;
// 定时器时钟分频,这里设置为不分频
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
// 自动重载预装载功能禁用
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
// 调用 HAL 库函数初始化定时器 2,如果初始化失败则调用错误处理函数
if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
{
Error_Handler();
}
// 配置定时器的时钟源为内部时钟
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
// 应用时钟源配置,如果配置失败则调用错误处理函数
if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
// 配置定时器的主模式输出触发为更新事件,即定时器更新时产生触发信号
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
// 禁用主从模式
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
// 应用主模式配置,如果配置失败则调用错误处理函数
if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
// DMA 初始化函数,用于配置 DMA2 流 0 进行 ADC 数据的传输
void DMA_Init(void)
{
// 使能 DMA2 时钟,为 DMA2 外设提供时钟信号
__HAL_RCC_DMA2_CLK_ENABLE();
// 指定 DMA2 流 0 作为操作对象
hdma_adc1.Instance = DMA2_Stream0;
// 指定 DMA 通道为通道 0
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
// 设置 DMA 传输方向为从外设(ADC)到内存
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
// 外设地址不递增,因为 ADC 数据寄存器地址固定
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
// 内存地址递增,因为要将数据依次存储到数组中
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
// 外设数据对齐方式为半字(16 位)
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
// 内存数据对齐方式为半字(16 位)
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
// 设置 DMA 工作模式为循环模式,数据传输完成后自动重新开始
hdma_adc1.Init.Mode = DMA_CIRCULAR;
// 设置 DMA 传输优先级为低
hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
// 禁用 DMA FIFO 模式
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
// 调用 HAL 库函数初始化 DMA,如果初始化失败则调用错误处理函数
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
{
Error_Handler();
}
// 将 DMA 句柄与 ADC 句柄关联起来,使 DMA 可以为 ADC 服务
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);
}
// ADC 初始化函数,用于配置 ADC1 进行 3 路模拟信号的转换
void ADC1_Init(void)
{
// 定义 ADC 通道配置结构体,用于配置 ADC 通道的参数
ADC_ChannelConfTypeDef sConfig = {0};
// 使能 ADC1 时钟,为 ADC1 外设提供时钟信号
__HAL_RCC_ADC1_CLK_ENABLE();
// 指定 ADC1 作为操作对象
hadc1.Instance = ADC1;
// ADC 时钟预分频,将 ADC 时钟频率进行分频
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
// 设置 ADC 分辨率为 12 位
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
// 使能 ADC 扫描模式,用于多通道转换
hadc1.Init.ScanConvMode = ENABLE;
// 禁用连续转换模式,每次转换完成后等待下一次触发
hadc1.Init.ContinuousConvMode = DISABLE;
// 禁用不连续转换模式
hadc1.Init.DiscontinuousConvMode = DISABLE;
// 设置外部触发转换的边沿为上升沿
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
// 设置外部触发源为定时器 2 的 TRGO 信号
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
// 设置 ADC 数据对齐方式为右对齐
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
// 设置要转换的通道数量为 3
hadc1.Init.NbrOfConversion = 3;
// 使能 DMA 连续请求模式,确保 DMA 可以持续传输数据
hadc1.Init.DMAContinuousRequests = ENABLE;
// 设置 EOC 选择为序列转换结束标志
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
// 调用 HAL 库函数初始化 ADC1,如果初始化失败则调用错误处理函数
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
// 配置 ADC 通道 0
sConfig.Channel = ADC_CHANNEL_0;
// 设置通道 0 的转换顺序为第 1 个
sConfig.Rank = 1;
// 设置通道 0 的采样时间为 3 个 ADC 时钟周期
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
// 应用通道 0 的配置,如果配置失败则调用错误处理函数
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
// 配置 ADC 通道 1
sConfig.Channel = ADC_CHANNEL_1;
// 设置通道 1 的转换顺序为第 2 个
sConfig.Rank = 2;
// 应用通道 1 的配置,如果配置失败则调用错误处理函数
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
// 配置 ADC 通道 2
sConfig.Channel = ADC_CHANNEL_2;
// 设置通道 2 的转换顺序为第 3 个
sConfig.Rank = 3;
// 应用通道 2 的配置,如果配置失败则调用错误处理函数
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}
// GPIO 初始化函数,用于配置 GPIO 引脚作为 ADC 的模拟输入引脚
void GPIO_Init(void)
{
// 使能 GPIOA 时钟,为 GPIOA 外设提供时钟信号
__HAL_RCC_GPIOA_CLK_ENABLE();
// 定义 GPIO 初始化结构体,用于配置 GPIO 引脚的参数
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 指定要配置的引脚为 PA0、PA1 和 PA2
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
// 设置引脚模式为模拟输入模式
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
// 不使用上拉或下拉电阻
GPIO_InitStruct.Pull = GPIO_NOPULL;
// 应用 GPIO 引脚配置
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
// 系统初始化函数,用于初始化整个系统的外设
void System_Init(void)
{
// 初始化 HAL 库
HAL_Init();
// 初始化 GPIO 引脚
GPIO_Init();
// 初始化定时器 2
TIM2_Init();
// 初始化 DMA2 流 0
DMA_Init();
// 初始化 ADC1
ADC1_Init();
}
2. 主函数
int main(void)
{
System_Init();
// 启动定时器
HAL_TIM_Base_Start(&htim2);
// 启动ADC DMA传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, 3);
while (1)
{
// 可以在这里处理采样数据
// 例如将采样数据通过串口发送出去
}
}
3. 错误处理函数
void Error_Handler(void)
{
while (1)
{
// 可以在这里添加错误处理代码,例如点亮LED灯提示错误
}
}
五、代码解释
1. 定时器初始化
在TIM2_Init
函数中,我们配置了定时器 2 的预分频器和周期,使其产生 1S 的定时信号。定时器的更新事件(TRGO)将作为 ADC 的外部触发信号。
2. DMA 初始化
在DMA_Init
函数中,我们配置了 DMA2 的 Stream0,使其将 ADC 的转换结果从外设(ADC)传输到内存(adc_values
数组)。使用循环模式可以实现连续的数据传输。
3. ADC 初始化
在ADC1_Init
函数中,我们配置了 ADC1 的时钟、分辨率、扫描模式等参数。使用外部触发模式,由定时器 2 的 TRGO 信号触发 ADC 转换。配置了 3 个转换通道,分别对应 PA0、PA1 和 PA2 引脚。
4. GPIO 初始化
在GPIO_Init
函数中,我们将 PA0、PA1 和 PA2 引脚配置为模拟输入模式,用于连接外部模拟信号。
5. 主函数
在main
函数中,我们首先调用System_Init
函数进行系统初始化。然后启动定时器和 ADC 的 DMA 传输。在主循环中,可以对采样数据进行处理,例如将数据通过串口发送出去。
六、调试与测试
在完成代码编写后,可以使用调试工具(如 ST-Link)对程序进行调试。可以在主循环中添加打印语句,将采样数据通过串口输出到电脑上,观察采样结果是否正确。
七、注意事项
- 时钟配置:确保系统时钟和外设时钟配置正确,否则定时器和 ADC 可能无法正常工作。
- DMA 配置:DMA 的配置需要与 ADC 的配置相匹配,例如数据对齐方式、传输方向等。
- 电源和接地:确保模拟信号的电源和接地良好,避免引入噪声干扰。
八、总结
通过结合 STM32F407 的 ADC、TIM 和 DMA 外设,我们可以高效、稳定地实现 3 路模拟信号的周期性采样。定时器用于控制采样频率,DMA 用于数据传输,提高了系统的效率。在实际应用中,可以根据需要对采样频率、通道数量等参数进行调整。