STM32 HAL库 ADC+TIM+DMA 3路 1S采样一次电压

一、引言

在很多嵌入式系统应用中,需要对多路模拟信号进行周期性采样,例如在工业控制、环境监测等领域。STM32F407 是一款高性能的微控制器,其丰富的外设资源可以方便地实现这样的功能。通过结合 ADC(模拟 - 数字转换器)、TIM(定时器)和 DMA(直接内存访问),可以高效、稳定地完成多路模拟信号的周期性采样。

二、硬件连接

在使用 STM32F407 进行 ADC 采样时,需要将外部模拟信号连接到对应的 ADC 通道引脚。以下是一个常见的硬件连接示例:

信号STM32F407 引脚ADC 通道
模拟信号 1PA0ADC1 通道 0
模拟信号 2PA1ADC1 通道 1
模拟信号 3PA2ADC1 通道 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)对程序进行调试。可以在主循环中添加打印语句,将采样数据通过串口输出到电脑上,观察采样结果是否正确。

七、注意事项

  1. 时钟配置:确保系统时钟和外设时钟配置正确,否则定时器和 ADC 可能无法正常工作。
  2. DMA 配置:DMA 的配置需要与 ADC 的配置相匹配,例如数据对齐方式、传输方向等。
  3. 电源和接地:确保模拟信号的电源和接地良好,避免引入噪声干扰。

八、总结

通过结合 STM32F407 的 ADC、TIM 和 DMA 外设,我们可以高效、稳定地实现 3 路模拟信号的周期性采样。定时器用于控制采样频率,DMA 用于数据传输,提高了系统的效率。在实际应用中,可以根据需要对采样频率、通道数量等参数进行调整。

实现。 以下是实现步骤: 1. 定义ADC采样通道和DMA缓存区。 ``` #define ADC_CHANNEL ADC_CHANNEL_0 #define ADC_BUFFER_SIZE 256 uint16_t ADC_Buffer[ADC_BUFFER_SIZE]; ``` 2. 配置ADCDMA,使其能够进行采样和存储。 ``` ADC_HandleTypeDef hadc; DMA_HandleTypeDef hdma_adc; void MX_ADC_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) */ hadc.Instance = ADC1; hadc.Init.ScanConvMode = DISABLE; hadc.Init.ContinuousConvMode = ENABLE; hadc.Init.DiscontinuousConvMode = DISABLE; hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc.Init.NbrOfConversion = 1; if (HAL_ADC_Init(&hadc) != HAL_OK) { Error_Handler(); } /** Configure Regular Channel */ sConfig.Channel = ADC_CHANNEL; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) { Error_Handler(); } } void MX_DMA_Init(void) { /* DMA controller clock enable */ __HAL_RCC_DMA1_CLK_ENABLE(); /* DMA interrupt init */ /* DMA1_Channel1_IRQn interrupt configuration */ HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); /* DMA1_Channel1 init */ hdma_adc.Instance = DMA1_Channel1; hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc.Init.MemInc = DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode = DMA_CIRCULAR; hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; if (HAL_DMA_Init(&hdma_adc) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(&hadc, DMA_Handle, hdma_adc); } ``` 3. 配置定时器,使其能够触发ADC采样。 ``` TIM_HandleTypeDef htim; void MX_TIM_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim.Instance = TIM2; htim.Init.Prescaler = 72-1; // 1 MHz htim.Init.CounterMode = TIM_COUNTERMODE_UP; htim.Init.Period = 100-1; // 10 kHz htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; if (HAL_TIM_Base_Init(&htim) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim, &sMasterConfig) != HAL_OK) { Error_Handler(); } } ``` 4. 在定时器中断中开启ADC采样DMA传输,并在DMA传输完成中断中进行FFT计算。 ``` void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { HAL_ADC_Start_DMA(&hadc, (uint32_t*)ADC_Buffer, ADC_BUFFER_SIZE); } } void HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma) { if (hdma->Instance == DMA1_Channel1) { if (hdma->State == HAL_DMA_STATE_READY) { arm_cfft_radix4_instance_f32 S; uint32_t ifftFlag = 0; uint32_t doBitReverse = 1; float32_t maxValue; arm_rfft_fast_f32(&S, (float32_t*)ADC_Buffer, (float32_t*)ADC_Buffer, ifftFlag); arm_cmplx_mag_f32((float32_t*)ADC_Buffer, (float32_t*)ADC_Buffer, ADC_BUFFER_SIZE/2); arm_max_f32((float32_t*)ADC_Buffer, ADC_BUFFER_SIZE/2, &maxValue, NULL); arm_scale_f32((float32_t*)ADC_Buffer, 1.0f/maxValue, (float32_t*)ADC_Buffer, ADC_BUFFER_SIZE/2); } } } ``` 5. 在主函数中初始化ADCDMA和定时器,并启动定时器。 ``` int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC_Init(); MX_DMA_Init(); MX_TIM_Init(); HAL_TIM_Base_Start_IT(&htim); while (1) { } } ``` 通过以上步骤,我们可以实现定时器控制ADC采样并用DMA存储,然后用FFT算法计算低频周期性变化直流信号的频率和幅值的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值