STM32CubeMX系列05——ADC(轮询、中断、DMA)
文章目录
1. 所用硬件
正点原子Mini板,主控 STM32F103RCT6.
用到的外设:
- 串口1(PA9、PA10)
- 任意几个GPIO口(这里用PA1、PA2、PA3,对应ADC通道1、2、3)。
2. 生成工程
2.1. 创建工程选择主控
2.2. 系统配置
配置时钟源
配置debug模式(如果需要ST-Link下载及调试可以勾选)
配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
注意最后的ADC时钟,时钟频率最大14MHZ,因此这里设置6分频,刚好小于14。
2.3. 配置工程目录
2.4. 配置用到的外设
串口1配置(用于输出结果)
3. ADC配置(四选一)
有如下情况:
- 单通道轮询
- 单通道中断
- 多通道轮询
- DMA模式(单通道、多通道都能用)
设置说明:
Right alignment
:转换结果数据右对齐,一般我们选择右对齐模式。Enabled 开启扫描模式
。如果是多通道 AD 转换使用 ENABLE。Disabled 单次转换
。转换一次后停止需要手动控制才重新启动转换。Disabled 禁止间断模式
。这个在需要考虑功耗问题的产品中很有必要,也就是在某个事件触发下,开启转换。Enable Regular Conversions
是否使能规则转换。Number Of Conversion
ADC转换通道数目,有几个写几个就行。External Trigger Conversion Source
外部触发选择。这个有多个选择,一般采用软件触发方式。Channel
ADC 转换通道Sampling Time
采样周期选择,采样周期越短,ADC 转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC 转换数据输出周期就越长同时数据精度越高。Enable Injected Conversions 是否使能注入转换。注入通道只有在规则通道存在时才会出现。
3.1. 单通道轮询
第一步:配置ADC
第二步:点击生成代码
第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用
// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
x = x;
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
第四步:编写 main.c 代码 其他的什么都不用改
while (1)
{
// 开启ADC
HAL_ADC_Start(&hadc1);
// 开始轮询转换
HAL_ADC_PollForConversion(&hadc1,100);
// 存储转换的值
float value = 0;
// 查询ADC状态
uint32_t state = HAL_ADC_GetState(&hadc1);
if (( state & HAL_ADC_STATE_REG_EOC) == HAL_ADC_STATE_REG_EOC)
{
// 获取ADC转换结果
value = HAL_ADC_GetValue(&hadc1);
printf("adc value:%f \r\n",value/4096.0*3.3);
}
else
{
printf("adc state %d \r\n",state);
}
// 关闭ADC
HAL_ADC_Stop(&hadc1);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
效果验证
单通道轮询 在转换时会阻塞直到转换完成。
3.2. 单通道中断
第一步:配置上:在“单通道轮询”实现配置基础上再打开ADC全局中断。
第二步:点击生成代码
第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用
// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
x = x;
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
第四步:编写 main.c 代码
生成后查看代码,在stm32f1xx_it.c
文件中有 ADC1通道2的中断函数ADC1_2_IRQHandler
,这个中断函数又调用了HAL_ADC_IRQHandler(&hadc1);
/**
* @brief This function handles ADC1 and ADC2 global interrupts.
*/
void ADC1_2_IRQHandler(void)
{
/* USER CODE BEGIN ADC1_2_IRQn 0 */
/* USER CODE END ADC1_2_IRQn 0 */
HAL_ADC_IRQHandler(&hadc1);
/* USER CODE BEGIN ADC1_2_IRQn 1 */
/* USER CODE END ADC1_2_IRQn 1 */
}
在HAL_ADC_IRQHandler(&hadc1);
函数在stm32f1xx_hal_adc.c
中,这个函数考虑了很多情况,其中调用了HAL_ADC_ConvCpltCallback(hadc);
,还是在同一个文件中,这是一个弱函数。根据翻译,很好理解,我们直接重新定义这个方法即可。
/**
* @brief Conversion complete callback in non blocking mode
* @param hadc: ADC handle
* @retval None
*/
__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(hadc);
/* NOTE : This function should not be modified. When the callback is needed,
function HAL_ADC_ConvCpltCallback must be implemented in the user file.
*/
}
main.c
/* USER CODE BEGIN PFP */
// 重定义ADC转换完成回调函数
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc == &hadc1)
{
uint16_t adc_value = HAL_ADC_GetValue(hadc);
printf("refresh adc value:%f \r\n", adc_value/4096.0*3.3);
// 重新开启ADC中断
HAL_ADC_Start_IT(&hadc1);
}
}
/* USER CODE END PFP */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN WHILE */
// 开启ADC中断
HAL_ADC_Start_IT(&hadc1);
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
效果和轮询的一样,不过这个会一直执行,而且很快
实际上,这里设置的是单次转换,所以中断只会触发一次,需要再次使用HAL_ADC_Start_IT开启中断。如果需要实时的转换,可以将转换设为连续模式,这样的话ADC转换器便会实时的持续的进行转换,那将是非常消耗CPU的,以至于main将不能正常执行(采样时间太短的话)。
开启中断后,一般需要实现HAL_ADC_ConvCpltCallback函数,在callback中GetValue,也可以在程序其他地方像轮询那样先判断ADC状态,再GetValue。
3.3. 多通道轮询
第一步:ADC配置
多通道时扫描模式会自动打开。要开启“Discontinuous Conversion Mode”。
第二步:点击生成代码
第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用
// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
x = x;
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
第四步:编写 main.c 代码
HAL_ADCEx_Calibration_Start(&hadc1);
const uint8_t kNbrOfPin = 3;
while (1)
{
for(int i = 0; i < kNbrOfPin; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
float value = 0;
uint32_t state = HAL_ADC_GetState(&hadc1);
if (( state & HAL_ADC_STATE_REG_EOC) == HAL_ADC_STATE_REG_EOC)
{
value = HAL_ADC_GetValue(&hadc1);
printf("adc value [%d]:%f\r\n", i,value/4096.0*3.3);
}
else
{
printf("adc state[%d]:%d\r\n", i, state);
}
}
HAL_ADC_Stop(&hadc1);
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
效果验证
3.4. DMA模式
第一步:ADC配置
第二步:点击生成代码
第三步:串口重定向,在usart.c中添加如下代码。具体的参考上一篇文章串口使用
// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
x = x;
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
第四步:编写 main.c 代码
/* USER CODE BEGIN PFP */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc == &hadc1)
{
// 使用DMA其实也会运行到这里,也可以将结果在这里输出。
// 当然此函数也可以不写。
}
}
/* USER CODE END PFP */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
uint16_t adc_value[3] = {0};
/* USER CODE END 1 */
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN WHILE */
HAL_ADCEx_Calibration_Start(&hadc1);
// enable DMA通道
// 参数:ADC1、目标缓冲区地址、从ADC外围设备传输到内存的数据长度
/*
* 此处有个大坑,经过测试,DMA中断非常容易进(具体的不知道)
*
* 如果ADC采样周期短的话,一直在执行中断,
* 导致无法执行主程序,因此会卡死在这个函数里面出不去。
*
* 因此,ADC的采用周期需要长一点。
*/
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_value, 3);
while (1)
{
printf("-------------------- \r\n");
printf("adc value[0]:%f \r\n", adc_value[0]/4096.0*3.3);
printf("adc value[1]:%f \r\n", adc_value[1]/4096.0*3.3);
printf("adc value[2]:%f \r\n", adc_value[2]/4096.0*3.3);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
注意
用到 DMA 的外设,MX_DMA_Init(); 一定要在外设初始化前面,比如这里的 MX_ADC1_Init(); 。
效果验证
多通道DMA和单通道DMA配置基本相同,只需注意存储AD转换结果的数组,如果有两个通道,数组长度为2,则每个通道的值分别对应数组的每一位;如果数组长度为2的整数倍,如10,则数组内[0] [2] [4] [6] [8]的值对应其中一个通道的AD值,即存储了连续采集5次的AD值,这样可以用多个AD值求平均值。