普冉(PUYA)单片机开发笔记(8): ADC-DMA多路采样

概述

上一个实验完成了基于轮询的多路 ADC 采样,现在尝试跑一下使用 DMA 的 ADC 多路采样。厂家例程中有使用 DMA 完成单路采样的,根据这个例程提供的模板,再加上在 STM32 开发同样功能的基础,摸索着尝试。

经过多次修改和测试,最终完成了在开发板上使用 DMA 的 三路 ADC 采样的功能,和各位码神分享。

实现代码

在 main.h 中增加和 ADC_DMA 相关的函数声明

利用 Keil 实现一个功能,无怪乎就是 xxx_init 进行初始化,然后在主循环中用 xxx_start, xxx_stop, xxx_action 这一类的函数实现预定功能。老套路,在 main.h 中增加

  • ADC_DMA_Init(void); // 初始化利用 DMA 的 ADC
  • ADC_DMA_Start(void); // 开始采样和搬运
  • ADC_DM_Sample(char *sampleResult); // 获取采样的当前值

的声明,至于如何实现的,main.h 中不用考虑,也不用做更多的 #define 和全局变量定义,代码解耦不是么,把这些函数用得到的(即使是全局变量)都放到各自的 .c 文件中去就好了。

cpp 复制代码
/** ----------------------------------------------------------------------------
* @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);

修改 py32_f0xx_hal_msp.c

重写(类似于 C++ 的 override)HAL_ADC_MspInit 函数。如果要用到定时器,中断,定时器,比较器等,都要在这个文件中加入(或者修改 HAL_xxx_MspInit 函数)。

cpp 复制代码
/**
 * -----------------------------------------------------------------------
 * @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 句柄
}

以上代码中,

  1. 首先使能相关的时钟,DMA_CLK 不说了;用到了 ADC,那肯定要使能 ADC_CLK 的啦;还要用到模拟信号的输入管脚,ADC1 的 CH0/1/5 分别是 PA0/1/4,那么 GPIOA_CLK 也要使能。
  2. 然后把 PA0/1/4 初始化成模拟输入管脚,内部拉低。拉高和拉低对采样结果的影响还是蛮大的,"踩坑记"中有说明。
  3. 使用 HAL_SYSCFG_DMA_Req(0) 把 DMA1 映射成"为 ADC 做数据搬运"。这是厂家例程里的一条语句,但是这句话什么意思,厂家 HAL 库中没找到完整的说明,暂且不动它。
  4. 再下一步就是对 DMA1 的通道1进行初始化,代码先放到这里,"踩坑记"中对这些参数有说明的。
  5. 最后一步是使用 __HAL_LINKDMA() 将 ADC1 和 DMA1 的通道1关联起来。这句话挺特别的,挺底层的(两个下划线开始的函数呢)。

修改 py32_f0xx_hal_it.c

在 DMA1_Channel1_IRQHandler 中直接调用 HAL_DMA_IRQHandler;在 ADC_COMP_IRQHandler 中直接调用 HAL_ADC_IRQHandler。为什么要这么做呢?这里简短节说:顺着 HAL_Init,HAL_ADC_Init 和 HAL_ADC_Start_DMA 几个函数一路嵌套地 F12 下去,就能定位到上面的这两个函数。

cpp 复制代码
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);
}

在 app_adc.c 中实现相关函数

  1. 首先在文件的开头部分,增加几个全局变量
  2. 然后实现 ADC_DMA_Init() 函数、ADC_DMA_Start() 函数和 ADC_DMA_Sample() 函数。

对 ADCDMA handler 的参数说明,在"踩坑记"里。

cpp 复制代码
/**
 * Variables for ADC loop sample with DMA
*/
#define DMA_SAMP_COUNT 3
ADC_HandleTypeDef hadcdma;
uint32_t adc_dma_value[DMA_SAMP_COUNT] = {0};
cpp 复制代码
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;
}

在 main.c 中调用

cpp 复制代码
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

这个精度对于毫伏级测量还是不够的,实用中还是要加电压跟随器才好。

踩坑记

厂家例程完成单路采样,不少参数都需要修改,要不的话,结果不是错误,就是颠倒,甚至莫名其妙。

  • GPIO_InitStructure.Pull 属性设置为 PULLDOWN 更符合实用场景,悬空时为一个接近于 0 的采样值,这一点在上一个实验中也说明了。
  • 多路采样时,adc_dma_value(在 app_adc.c 中定义的全局变量) 要设置为和采样通道数相同维数的数组,uint32_t 类型的。
  • HdmaCh1.Init.MemInc (在 HAL_ADC_MspInit 函数中)设置成了 DMA_MINC_DISABLE,在多路采样时,要改为 DMA_MINC_ENABLE。如果不 ENABLE,第二次的采样值将总会把 adc_dma_value[0] 的值覆盖掉,而 adc_dma_value[1] 和 adc_dma_value[2] 中没有值。
  • 和 adc_dma_value 的字长对应,HdmaCh1.Init.PeriphDataAlignment(在 HAL_ADC_MspInit 函数中)要设置成 DMA_PDATAALIGN_WORD,即外设数据宽度为32位,酱紫的设置在采样程序中不需要对 adc_dma_value 进行 16 位的分割。同样地,HdmaCh1.Init.MemDataAlignment 也要设置成 DMA_MDATAALIGN_WORD。厂家例程是 HALFWORD,如果用在多路采样当中,每一个 adc_dma_value[x] 中高16位和地16位分别存放一个通道的采样值,需要用"位与"操作和 16 移位操作把这两个数取出来。
  • 在 ADC_DMA_Init 函数中完成的 hadcdma 的初始化参数中,以下设置是多路采样可以正确运行的唯一组合:
cpp 复制代码
hadcdma.Init.ScanConvMode          = ADC_SCAN_DIRECTION_FORWARD;
hadcdma.Init.LowPowerAutoWait      = ENABLE;
hadcdma.Init.ContinuousConvMode    = ENABLE;
hadcdma.Init.DiscontinuousConvMode = DISABLE;
  1. hadcdma.Init.ScanConvMode 设置成 FORWARD 才能保证采样的数据搬运顺序和 adc_dma_value 数组的下标顺序相同,要是设置成 BACKWORD,adc_dma_value 的存放顺序就是 2,1,0了。
  2. ContinuousConvMode 必须 ENABLE,DiscontinuousCovMode 必须 DISABLE。
  • 必须按照本文的描述修改 py32f0xx_hal_it.c,不改不行,少改也不行,放错了地方更不行。如果不照文件写,程序不会卡死,但无法得到正确的结果------我在这个地方白白耗费了 N 多的时间。

改好以后,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 看看会等待多长时间。

初次试用,谬误之处,欢迎评论,指正。

相关推荐
岑梓铭1 天前
《考研408数据结构》第四章(串和串的算法)复习笔记
数据结构·笔记·考研·算法
点灯小铭1 天前
基于单片机的Boost升压斩波电源电路
单片机·嵌入式硬件·毕业设计·课程设计
清风6666661 天前
基于单片机的蓝牙可调PWM波形发生器设计
单片机·嵌入式硬件·mongodb·毕业设计·课程设计
小莞尔1 天前
【51单片机】【protues仿真】基于51单片机汽车智能灯光控制系统
c语言·单片机·嵌入式硬件·汽车·51单片机
冬夜戏雪1 天前
记录下C盘清理步骤(有效)
经验分享·笔记
我登哥MVP1 天前
Apache Tomcat 详解
java·笔记·tomcat
电子科技圈1 天前
芯科科技第三代无线SoC现已全面供货
嵌入式硬件·mcu·物联网·网络安全·智能家居·智能硬件·iot
泽虞1 天前
《Qt应用开发》笔记
linux·开发语言·c++·笔记·qt
报错小能手1 天前
linux学习笔记(21)线程同步——互斥锁
linux·笔记·学习
zm1 天前
数据结构整理
单片机·嵌入式硬件