STM32CubeMX+HAL TIM触发单通道ADC电压采样+DMA传输

本文采用的是野火霸天虎V2 STM32 F407ZGT6系列单片机。

这是我的第一篇文章,因为这是我学习和复习了整整6天的ADC、DMA、TIM的知识,翻了无数的网页,仔细看了无数遍手册后,(应该是)第一次自主建构出整个工程并最终正确运行,幸甚至哉,特此记录,如果形式不太成熟,还请各位看官见谅。

工程架构总体思路:

配置时钟树,使得APB1总线频率为42MHz,通过一个输入为APB1的倍频器传给定时器TIM3的时钟频率为84MHz。

设置TIM分频系数Prescaler=84-1,自动重装载值ARR=1000-1,使能TIM3中断,使得TIM3发生溢出中断TIM Period Elapsed Interruption的频率为1kHz,即每1ms触发一次定时器溢出中断。

如上图蓝色箭头,该中断信号通过APB1总线传给MPU,MPU保存并暂停当前main函数任务,跳转执行该中断类型对应的中断服务程序,此处将会进入<stm32f4xx_it.c>中的TIM3_IRQHandler()函数,进而进入HAL_TIM_PeriodElapsedCallback(&htim3)函数,这是一个弱定义函数。

如上图绿色箭头,重定义HAL_TIM_PeriodElapsedCallback(&htim3)函数,当TIM3溢出中断事件发生时,启动DMA模式ADC,即执行HAL_ADC_Start_DMA(&hadc1)函数,然后由ADC进行4次连续电压采样。

注意,这里ADC的几个关键参数配置及其原因如下:

1、Scan Conversion Mode:DISABLE,因为这里只进行是单通道ADC转换;

2、External Trigger Conversion Source:By Software,也就是需要在程序中自己去启动DMA模式ADC工作,例如此工程中在TIM3发生溢出中断时,通过重定义TIM3_PeriodElapsedCallBack()函数,从软件层面启动ADC工作;当然也可以选择Timer3 Capture Compare Channel1、Trigger Detection on the Rising Edge,这样在捕获到TIM3 Channel1 PWM 上升沿时,启动DMA模式ADC,程序设计思路大同小异。

3、Continous Conversion Mode:ENABLE。

4、DMA Continous Request:DISABLE。

5、DMA mode:Normal模式,不选择Circular模式。

其实我也没太搞懂这三个参数具体含义的差别,在另一篇相似的文章中定时器(PWM输出)触发ADC采样(DMA)——STM32CubeMX,作者第3、5个参数和我的恰好相反,但结果是我们的程序最终都能正常运行。我的大概理解如下:

DMA模式下单通道ADC连续转换模式,配合DMA正常模式,是什么意思?就是在DMA模式下ADC被触发后,执行一轮采样、ADC转换、ADC转换完成事件去触发DMA非循环模式传输,也就是这一次传输完成后传输任务完成,该事件又触发ADC在DMA模式下开始新的一轮同样的工作,直到DMA传输的数据量达到在HAL_ADC_Start_DMA()中设定的传输长度,此处为4,ADC转换任务结束,进入ADC_IRQHandler()中断服务函数;

查阅了书籍《Mastering STM32 - 2nd Edition (Carmine Noviello) 》对这一部分的配置讲解,其对DMA、ADC初始化配置与我的方案一致:

关于DMA模式ADC工作方式的官方介绍:STM32 官方技术文档:Getting started with ADC

4次 “采样随后ADC转换,随后DMA将结果数据从ADC外设寄存器传输到SRAM” 的任务全部完成后,进入ADC_IRQHandler()中断服务函数,进而进入HAL_ADC_ConvCpltCallback(&hadc1)函数,在中断服务函数中:关闭ADC、TIM,,进行数据处理,并将处理结果通过串口发送到上位机。

把上述工程设计转化为实际工程

STM32CubeMX的配置:

(1)RCC配置

(2)时钟树配置

(3)ADC配置

(4)TIM定时器与PWM通道配置

此处的配置选择:

(5)USART串口配置

(6)配置工程,然后Generate Code

Keil MDK用户层Application/User/Core代码:

我的第一版可以正确运行的代码如下:

main.c:

启用中断模式TIM3、启动TIM3 Channel1 PWM输出

/* <main.c> */

/* USER CODE BEGIN PV */
uint16_t ADC_Values[4] = {0};
/* USER CODE END PV */

int main(void)
{
  HAL_Init();

  SystemClock_Config();

  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_ADC1_Init();
  MX_TIM3_Init();

  /* USER CODE BEGIN 2 */
	
	/* 启用中断模式 TIM3 */
	HAL_TIM_Base_Start_IT(&htim3);
	
	/* TIM3 Channel1 开始输出 PWM */
	HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
	
	/**
		HAL_ADC_Start_DMA()函数的封装太高层次了、太抽象了,其具体功能只能概括为:使能 DMA + 启用 DMA 模式下的 ADC;
		注意"启用"不等于"采样", 具体可查看 ADC 工作原理;
		提示:此时已配置 ADC 为 软件触发 ADC 采样.
	*/
	printf("**********By xy Liao**********  \r\n");

  /* USER CODE END 2 */

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}
 stm32f4xx_it.c:

 完成中断回调函数重定义

/* <stm32f4xx_it.c> */

/* USER CODE BEGIN PV */
extern uint16_t ADC_Values[4];
/* USER CODE END PV */

/* USER CODE BEGIN 1 */

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim)
{
    /**
        总数据长度为 16bit * 4, 配置的 DMA 传输数据宽度为 HALF_WORD(32 * 0.5 bit);
		故HAL_ADC_Start_DMA 第三个参数————DMA传输长度设置为 4.
    */
	if(htim == &htim3)
	{
		HAL_ADC_Start_DMA(&hadc1, (uint32_t*)ADC_Values, 4);
	}
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc == &hadc1)
	{
		HAL_ADC_Stop_DMA(&hadc1);
		HAL_TIM_Base_Stop_IT(&htim3);
		float temp = (ADC_Values[0]+ADC_Values[1]+ADC_Values[2]+ADC_Values[3])/4;
		
		printf("ADC Average Result: %.4f  \r\n", temp);
		printf("Actual Voltage: %.4f  \r\n", temp * 3.3 / 4096.0);
		HAL_TIM_Base_Start_IT(&htim3);
	}
}

/* USER CODE END 1 */
 stm32f4xx_hal_msp.c:

完成串口重定向

/* <stm32f4xx_hal_msp.c> */

/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
extern UART_HandleTypeDef huart1;
/* USER CODE END PV */

/* USER CODE BEGIN 1 */

/**
  * @brief: 重定向c库函数printf到DEBUG_USARTx
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}
 
/**
  * @brief: 重定向c库函数getchar,scanf到DEBUG_USARTx
  */
int fgetc(FILE *f)
{
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  return ch;
}

/* USER CODE END 1 */

运行效果:

一个BUG:由于115200波特率的printf函数调用的时间量级为0.1-1ms量级,因此在TIM3控制1ms采样周期的条件下,会出现串口调试平台接收一段时间的数据后,发生"串口调试平台 停止响应",因此建议是不要在中断回调函数中使用任何printf、Delay函数,这样非常消耗MCU资源。

参考了以下文章:

STM32CubeMX | HAL库的ADC多通道数据采集(轮训、DMA、DMA+TIM)、读取内部传感器温度

一个严谨的STM32串口DMA发送&接收机制(1.5Mbps波特率)

目前对bug尝试的修复方案:将在中断回调函数中的操作转移到main函数while循环中,将使用printf函数的地方转为sprintf+串口DMA发送,并在发送完成后执行HAL_Delay(50)。或许双缓冲DMA可以实现不用延迟也能使串口调试平台不卡死的效果?等我晚些学习一下双缓冲DMA之后再来这里更新一下它的修复效果。

/* stm32f4xx_it.c */
extern int ADC_ConvEnd;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc == &hadc1)
		ADC_ConvEnd = 1;
}

/* main.c */

/* USER CODE BEGIN WHILE */
int ADC_ConvEnd = 0;
int main(void)
{
  /* USER CODE BEGIN 1 */
	char buffer[80];
	uint8_t j = 0;
  /* USER CODE END 1 */

  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  MX_ADC1_Init();
  MX_TIM3_Init();

  /* USER CODE BEGIN 2 */
	
  HAL_TIM_Base_Start_IT(&htim3);
  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
		if(ADC_ConvEnd)
		{
			HAL_ADC_Stop_DMA(&hadc1);
			HAL_TIM_Base_Stop_IT(&htim3);
			float temp = (ADC_Values[0]+ADC_Values[1]+ADC_Values[2]+ADC_Values[3])/4;
			
			j = sprintf(buffer, "ADC Average Result: %.4f  \r\n", temp);
			j += sprintf(buffer + j, "Actual Voltage: %.4f  \r\n", temp * 3.3 / 4096.0);
			
			HAL_UART_Transmit_DMA(&huart1, (uint8_t*)buffer, j);
			HAL_TIM_Base_Start_IT(&htim3);
			ADC_ConvEnd = 0;
			HAL_Delay(50);
		}
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

修复效果:串口调试平台没有再出现卡死现象

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值