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

2023-12-10 07:30

本文主要是介绍普冉(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 文件中去就好了。

/** ----------------------------------------------------------------------------
* @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 函数)。

/*** -----------------------------------------------------------------------* @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通道1HdmaCh1.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 下去,就能定位到上面的这两个函数。

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 的参数说明,在“踩坑记”里。

/*** 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;            // 转换分辨率12bithadcdma.Init.DataAlign             = ADC_DATAALIGN_RIGHT;           // 右对齐hadcdma.Init.ScanConvMode          = ADC_SCAN_DIRECTION_FORWARD;    // 扫描序列方向:0-12hadcdma.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 中调用

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 的初始化参数中,以下设置是多路采样可以正确运行的唯一组合:
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 的好处。本例每半秒钟读取一次 DMA 的更新结果,而采样时间只有

                Tsample = (239.5+12.5)/24 = 10.5 us

DMA 搬运三个 uint32_t 的数更是不在话下。运行了近半个小时,没有遇到读不出来的情况。回头再试试把采样前的 if 改为 while 看看会等待多长时间。

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

这篇关于普冉(PUYA)单片机开发笔记(8): ADC-DMA多路采样的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/476420

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧