上一个实验完成了基于轮询的多路 ADC 采样,现在尝试跑一下使用 DMA 的 ADC 多路采样。厂家例程中有使用 DMA 完成单路采样的,根据这个例程提供的模板,再加上在 STM32 开发同样功能的基础,摸索着尝试。
经过多次修改和测试,最终完成了在开发板上使用 DMA 的 三路 ADC 采样的功能,和各位码神分享。
利用 Keil 实现一个功能,无怪乎就是 xxx_init 进行初始化,然后在主循环中用 xxx_start, xxx_stop, xxx_action 这一类的函数实现预定功能。老套路,在 main.h 中增加
的声明,至于如何实现的,main.h 中不用考虑,也不用做更多的 #define 和全局变量定义,代码解耦不是么,把这些函数用得到的(即使是全局变量)都放到各自的 .c 文件中去就好了。
/** ----------------------------------------------------------------------------
* @name : void ADC_DMA_Init(void)
* @brief : 使用 DMA 进行 ADC 的初始化
* @param : [in] None
* @retval : [out] void
* @remark :
*** ----------------------------------------------------------------------------
*/
void ADC_DMA_Init(void);
/** ----------------------------------------------------------------------------
* @name : HAL_StatusTypeDef ADC_DMA_Sample(char * sampleResult)
* @brief : 从 DMA 获取 ADC 的采样结果,结果存放在 sampleResult 字符串中
* @param : [in] None
* @retval : [out] HAL_HandleTypeDef. 操作成功返回 HAL_OK, 错误返回错误码。
* @remark : sampleResult 是格式化的字符串,需要解析
*** ----------------------------------------------------------------------------
*/
HAL_StatusTypeDef ADC_DMA_Sample(char* sampleResult);
/** ----------------------------------------------------------------------------
* @name : HAL_StatusTypeDef ADC_DMA_Start(void);
* @brief : 启动 ADC DMA 采样
* @param : [in] None
* @retval : [out] HAL_HandleTypeDef. 操作成功返回 HAL_OK, 错误返回错误码。
* @remark :
*** ----------------------------------------------------------------------------
*/
HAL_StatusTypeDef ADC_DMA_Start(void);
重写(类似于 C++ 的 override)HAL_ADC_MspInit 函数。如果要用到定时器,中断,定时器,比较器等,都要在这个文件中加入(或者修改 HAL_xxx_MspInit 函数)。
/**
* -----------------------------------------------------------------------
* @name : void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
* @brief : 初始化 ADC 相关 MSP
* @param : [in] *hadc, ADC handler pointer
* @retval : void
* @remark :
* -----------------------------------------------------------------------
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance != ADC1) return;
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_SYSCFG_CLK_ENABLE(); // SYSCFG 时钟使能
__HAL_RCC_DMA_CLK_ENABLE(); // DMA 时钟使能
__HAL_RCC_GPIOA_CLK_ENABLE(); // GPIOA 时钟使能
__HAL_RCC_ADC_CLK_ENABLE(); // ADC 时钟使能
/* ----------------
ADC通道配置PA0/1/4
---------------- */
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_SYSCFG_DMA_Req(0); // DMA1_MAP 选择为 ADC
/* ------------
DMA配置
------------ */
HdmaCh1.Instance = DMA1_Channel1; // 选择DMA通道1
HdmaCh1.Init.Direction = DMA_PERIPH_TO_MEMORY; // 方向为从外设到存储器
HdmaCh1.Init.PeriphInc = DMA_PINC_DISABLE; // 禁止外设地址增量
HdmaCh1.Init.MemInc = DMA_MINC_ENABLE; // 使能存储器地址增量
HdmaCh1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设数据宽度为16位
HdmaCh1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 存储器数据宽度位16位
HdmaCh1.Init.Mode = DMA_CIRCULAR; // 循环模式
HdmaCh1.Init.Priority = DMA_PRIORITY_MEDIUM; // 通道优先级为很高
HAL_DMA_DeInit(&HdmaCh1); // DMA 清除初始化
HAL_DMA_Init(&HdmaCh1); // 初始化 DMA 通道1
__HAL_LINKDMA(hadc, DMA_Handle, HdmaCh1); // 连接 DMA 句柄
}
以上代码中,
在 DMA1_Channel1_IRQHandler 中直接调用 HAL_DMA_IRQHandler;在 ADC_COMP_IRQHandler 中直接调用 HAL_ADC_IRQHandler。为什么要这么做呢?这里简短节说:顺着 HAL_Init,HAL_ADC_Init 和 HAL_ADC_Start_DMA 几个函数一路嵌套地 F12 下去,就能定位到上面的这两个函数。
void DMA1_Channel1_IRQHandler(void)
{
HAL_DMA_IRQHandler(hadcdma.DMA_Handle);
}
void DMA1_Channel2_3_IRQHandler(void)
{
}
void ADC_COMP_IRQHandler(void)
{
HAL_ADC_IRQHandler(&hadcdma);
}
对 ADCDMA handler 的参数说明,在“踩坑记”里。
/**
* Variables for ADC loop sample with DMA
*/
#define DMA_SAMP_COUNT 3
ADC_HandleTypeDef hadcdma;
uint32_t adc_dma_value[DMA_SAMP_COUNT] = {0};
void ADC_DMA_Init(void)
{
ADC_ChannelConfTypeDef adConfig = {0};
__HAL_RCC_ADC_FORCE_RESET();
__HAL_RCC_ADC_RELEASE_RESET();
__HAL_RCC_ADC_CLK_ENABLE(); //ADC时钟使能
hadcdma.Instance = ADC1;
if (HAL_ADCEx_Calibration_Start(&hadcdma) != HAL_OK) // ADC 校准
Error_Handler();
hadcdma.Instance = ADC1;
hadcdma.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; // 模拟ADC时钟源为PCLK,无分频
hadcdma.Init.Resolution = ADC_RESOLUTION_12B; // 转换分辨率12bit
hadcdma.Init.DataAlign = ADC_DATAALIGN_RIGHT; // 右对齐
hadcdma.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD; // 扫描序列方向:0-12
hadcdma.Init.LowPowerAutoWait = ENABLE; // 等待转换模式开启
hadcdma.Init.ContinuousConvMode = ENABLE; // 连续转换
hadcdma.Init.DiscontinuousConvMode = DISABLE; // 使能连续模式
hadcdma.Init.ExternalTrigConv = ADC_SOFTWARE_START; // ADC 无外部事件
hadcdma.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; // 无硬件驱动检测
hadcdma.Init.DMAContinuousRequests = ENABLE; // DMA 连续传输
hadcdma.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; // 当过载发生时,ADC_DR 新值覆盖
hadcdma.Init.SamplingTimeCommon = ADC_SAMPLETIME_239CYCLES_5; // 采样时间
if (HAL_ADC_Init(&hadcdma) != HAL_OK) // ADC初始化
Error_Handler();
adConfig.Channel = ADC_CHANNEL_0; /* 配置 ADC 通道0 */
adConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
if (HAL_ADC_ConfigChannel(&hadcdma, &adConfig) != HAL_OK) Error_Handler();
adConfig.Channel = ADC_CHANNEL_1; /* 配置 ADC 通道1 */
adConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
if (HAL_ADC_ConfigChannel(&hadcdma, &adConfig) != HAL_OK) Error_Handler();
adConfig.Channel = ADC_CHANNEL_4; /* 配置 ADC 通道4 */
adConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
if (HAL_ADC_ConfigChannel(&hadcdma, &adConfig) != HAL_OK) Error_Handler();
ADC_DMA_Start();
}
HAL_StatusTypeDef ADC_DMA_Start(void)
{
for( uint8_t i = 0; i < DMA_SAMP_COUNT; i++)
adc_dma_value[i] = 0;
if (HAL_ADC_Start_DMA(&hadcdma, &(adc_dma_value[0]), DMA_SAMP_COUNT) != HAL_OK)
Error_Handler();
return HAL_OK;
}
HAL_StatusTypeDef ADC_DMA_Sample(char* sampleResult)
{
uint8_t i = 0;
char res_part[20]={0};
if(__HAL_DMA_GET_FLAG(DMA1->ISR, DMA_ISR_TCIF1))
{
sprintf(sampleResult, "[");
for(i = 0; i< DMA_SAMP_COUNT; i++)
{
if(i > 0) strcat(sampleResult, ",");
sprintf(res_part, "{\"C\":%d,\"D\":%4u}", i, adc_dma_value[i]);
strcat(sampleResult, res_part);
}
strcat(sampleResult, "]");
__HAL_DMA_CLEAR_FLAG(DMA1->ISR, DMA_IFCR_CTCIF1);
}
return HAL_OK;
}
int main(void)
{
HAL_Init(); // systick初始化
SystemClock_Config(); // 配置系统时钟
GPIO_Config();
if(USART_Config() != HAL_OK) Error_Handler();
printf("[SYS_INIT] Debug port initilaized.\r\n");
ADC_DMA_Init();
printf("[SYS_INIT] ADC DMA initilaized.\r\n");
printf("\r\n+---------------------------------------+"
"\r\n| PY32F003 MCU is ready. |"
"\r\n+---------------------------------------+"
"\r\n 10 digits sent to you! "
"\r\n+---------------------------------------+"
"\r\n");
if (DBG_UART_Start() != HAL_OK) Error_Handler();
char sres[64]={0};
uint8_t sIndex = 0;
while (1)
{
BSP_LED_Toggle(LED3);
if(sIndex % 2 == 0)
{
if(ADC_DMA_Sample(sres) == HAL_OK)
{
printf("%s\r\n", sres);
}
}
sIndex ++;
HAL_Delay(500);
}
}
main() 函数中,只需要准备好一个足够长的字符串(也不能太长,要知道 RAM 总共就 8K字节)容纳采样结果就行了。本次实验中使用 JSON 串表示采样结果,64个字节了。MCU 编程中往往需要根据预期结果仔细地分配形参的尺寸,只要考虑完整,够用就行。本例中组装的 JSON 串,最大长度是 52 个字符,那么分配 53 个字节就行了(别忘记了末尾的那个 '\0')。
用杜邦线吧 PA0 和 PA1 都接 3.3V,PA4 悬空。编译烧录程序,在 XCOM 上观察打印的信息,截图如下:
运行结果符合设计预期,但明显有一个缺点就是上一轮运行“剩下”的部分字符串会遗留下来,采样结果字符串的第一行是无法用 JSON 解析的。(我先放自己一马 ;)
根据运行结果,得到 PA0/1 采样 VCC(3.3V)的电压平均值为
4085 * 3.3 /4096 = 3.291V
PA4 悬空,实测电压平均值为
17 * 3.3 / 4096 = 0.014V = 14 mV
这个精度对于毫伏级测量还是不够的,实用中还是要加电压跟随器才好。
厂家例程完成单路采样,不少参数都需要修改,要不的话,结果不是错误,就是颠倒,甚至莫名其妙。
hadcdma.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD;
hadcdma.Init.LowPowerAutoWait = ENABLE;
hadcdma.Init.ContinuousConvMode = ENABLE;
hadcdma.Init.DiscontinuousConvMode = DISABLE;
改好以后,main.c 的主循环只管在需要的时候,判断是否转换完成。在转换完成时直接读取 adc_mda_value[i] (i=0,1,2)就可以了,这就是利用 DMA 的好处。本例每1秒钟读取一次 DMA 的更新结果,而采样时间只有
Tsample = (239.5+12.5)/24 = 10.5 us
DMA 搬运三个 uint32_t 的数更是不在话下。运行了近半个小时,没有遇到读不出来的情况。回头再试试把采样前的 if 改为 while 看看会等待多长时间。
初次试用,谬误之处,欢迎评论,指正。