当前位置: 首页 > news >正文

STM32F103C8T6学习——直接存储器访问(DMA)标准库实战3(ADC数据采集+DMA回传)

1. 项目概述

本项目成功地在STM32F103C8T6微控制器上,使用标准外设库(SPL),构建了一个自动化、高效率的双通道模拟信号数据采集与上报系统。该系统能够在无需CPU持续干预的情况下,连续监测两个模拟输入端口(如电位器电压)这里我们采集的一个是光敏传感器,一个是红外避障传感器,并将采集到的数据格式化后,通过串口实时发送给上位机。

这个项目是嵌入式系统设计中一个典型的“生产者-消费者”模型,完美展示了如何利用DMA解放CPU,实现硬件间的并行工作,是从入门到进阶的标志性实践。

2. 核心技术栈
  • 硬件平台: STM32F103C8T6 核心板

  • 软件库: STM32 标准外设库 (Standard Peripheral Library, SPL)

  • 关键外设:

    • ADC1 (模数转换器): 用于将模拟信号转换为数字值。

    • DMA1 (直接内存访问): 使用两个独立通道,是实现系统自动化的核心。

    • USART1 (通用同步异步收发器): 用于与PC进行串行通信。

3. 系统架构与数据流

本项目的精髓在于构建了一条高效的数据流水线 (Data Pipeline),数据在其中自动流动,CPU仅在必要时介入:

  1. 信号输入: 两个电位器产生的模拟电压信号 (0-3.3V) 分别输入到MCU的PA0PA1引脚。

  2. ADC自动采样: ADC1被配置为多通道扫描连续转换模式。启动后,它会像一个永不停歇的“扫描仪”,自动依次对PA0PA1进行A/D转换,并将结果暂存至其内部的数据寄存器(ADC1->DR)。

  3. DMA无缝搬运 (采集环节):

    • DMA1通道1 被配置为专门服务于ADC1。

    • 它持续“监听”ADC1->DR寄存器。每当ADC完成一次转换,DMA1通道1便被硬件自动触发,将12位的转换结果(以16位HalfWord形式)从ADC1->DR中取出,并存入内存中的volatile uint16_t adc_values[2]数组。

    • 此过程工作在循环模式下,当存满adc_values[1]后,下一次数据会自动存回adc_values[0],实现了对内存缓冲区的循环写入。

  4. CPU轻量处理:

    • CPU的主循环while(1)以一个固定的频率醒来一次。

    • 它的任务非常简单:读取volatile adc_values数组中由DMA随时更新的最新数据,并使用sprintf函数将这两个数字格式化成一个人类可读的字符串(例如 "ADC CH0=1024, CH1=2048\r\n"),存入uart_tx_buffer

  5. DMA异步上报 (上报环节):

    • CPU准备好数据后,启动DMA1通道4

    • DMA1通道4从uart_tx_buffer中读取字符串内容,并逐字节地送入USART1->DR寄存器,通过串口发送出去。

    • 此过程工作在普通模式下,发送完指定长度的数据后便自动停止。CPU在启动它之后,无需等待,可以立刻进入下一次延时。

4. 关键实现细节
  • ADC配置: ADC_ScanConvModeADC_ContinuousConvMode的使能是实现自动连续采样的关键。同时,ADC上电后的校准流程是保证数据精度的必要步骤。

  • ADC的DMA配置: 数据宽度必须设置为HalfWord (16位) 来匹配ADC数据寄存器。循环模式 (DMA_Mode_Circular) 与ADC的连续转换模式完美配合。

  • 共享数据 volatile: 存储ADC结果的adc_values数组必须用volatile关键字修饰,以防止编译器优化,确保CPU每次都能从内存中读取到最新的、由DMA更新的值。

  • UART的DMA发送:重用了项目二中经过千锤百炼的健壮发送逻辑,即在每次启动发送前,先关闭通道,再用DMA_SetCurrDataCounter设置长度,并用DMA_ClearFlag清除上一次的“传输完成”标志,最后再启动通道。

5. 项目价值与意义
  • 解放CPU,提升系统效率: 整个数据采集过程几乎由ADC和DMA硬件在后台自动完成,CPU占用率极低,可以去执行更复杂的任务(如算法、控制、UI刷新等)。

  • 硬件并行,实时性强: 数据的采集和上报都在并行发生,系统能够非常及时地响应模拟信号的变化,具有很强的实时性。

  • 掌握核心设计模式: 深刻理解了DMA作为片上“数据总线”的核心作用,掌握了外设之间通过DMA联动的设计模式,这是构建复杂嵌入式系统的基础。

  • 代码健壮性: 通过处理DMA状态标志位等细节,学习了如何编写稳定、可靠、可重复工作的嵌入式应用程序。

DMA的相关代码就不再给出了,参考之前的项目一和项目二很快就能想到,重点在ADC的采集上,可认真参阅下下面的代码

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include "Delay.h"
#include "Serial.h"
// ADC相关定义
#define ADC_CHANNELS 2
// volatile关键字是必须的!因为它会被DMA在后台修改,而主程序会读取它
// 这可以防止编译器因优化而导致主程序读到的是旧数据
volatile uint16_t adc_values[ADC_CHANNELS];
void AD_Init(void)
{//配置时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);RCC_APB2PeriphClockCmd(GPIOA,ENABLE);//配置ADCCLKRCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置GPIOGPIO_InitTypeDef GPIO_Initstruct;GPIO_Initstruct.GPIO_Mode=GPIO_Mode_AIN;GPIO_Initstruct.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_1;GPIO_Initstruct.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_Initstruct);// 定义一个ADC_InitTypeDef类型的结构体变量,用于存储ADC的所有配置参数ADC_InitTypeDef ADC_InitStructure;// 设置ADC工作在独立模式。这是单个ADC工作时的标准设置。ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// 使能扫描模式。当ADC配置为转换多个通道时,此模式必须使能。ADC_InitStructure.ADC_ScanConvMode = ENABLE;// 使能连续转换模式。ADC完成一次扫描序列后,会自动从第一个通道开始,无缝地进行下一次扫描。ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 禁止外部触发转换。ADC的转换将由软件命令(ADC_SoftwareStartConvCmd)在配置完成后立即启动。ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;// 设置ADC转换结果的数据对齐方式为右对齐。12位的转换结果将存储在16位数据寄存器的低12位。ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;// 指定在规则转换组中要转换的通道数量。这里我们设置为2(通道0和通道1)。ADC_InitStructure.ADC_NbrOfChannel = 2;// 根据上面在ADC_InitStructure结构体中设置好的所有参数,正式初始化ADC1外设。ADC_Init(ADC1, &ADC_InitStructure);// --- 配置ADC的常规通道扫描序列 ---// Rank参数指定了通道在扫描序列中的顺序。// ADC_SampleTime_55Cycles5 设置了通道的采样时间,采样时间越长,精度越高,但速度越慢。// 配置ADC1的通道0(对应PA0),作为扫描序列的第1个转换通道。ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);// 配置ADC1的通道1(对应PA1),作为扫描序列的第2个转换通道。ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);// 使能ADC1外设。这是ADC开始工作前的总开关。ADC_Cmd(ADC1, ENABLE);// --- ADC校准 (关键步骤,用于消除偏移误差,显著提高转换精度) ---// 1. 复位ADC的校准寄存器。ADC_ResetCalibration(ADC1);// 2. 等待复位校准完成。while(ADC_GetResetCalibrationStatus(ADC1));// 3. 开始ADC校准。ADC_StartCalibration(ADC1);// 4. 等待校准完成。while(ADC_GetCalibrationStatus(ADC1));// 通过软件命令启动ADC转换。// 因为我们配置的是连续转换模式,所以这个命令只需要在初始化时执行一次。// ADC就会从此开始,永不停歇地进行多通道扫描和数据转换。ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
void AD_Scan(void)
{uint16_t len;// 1. 格式化ADC采集到的数据// ADC是12位的,所以最大值是4095// %04d 格式表示打印4位数,不足的前面补0,使显示更整齐sprintf((char*)TxBuffer, "ADC CH0=%04d, CH1=%04d\r\n", adc_values[0], adc_values[1]);// 2. 获取字符串长度len = strlen((const char*)TxBuffer);// 3. 使用完善的DMA发送逻辑来启动一次数据上报DMA_Cmd(DMA1_Channel4, DISABLE);                 // 先关闭通道,确保可以修改配置DMA_SetCurrDataCounter(DMA1_Channel4, len);      // 设置本次要发送的长度DMA_ClearFlag(DMA1_FLAG_TC4);                    // 清除上次的“传输完成”标志DMA_Cmd(DMA1_Channel4, ENABLE);                  // 启动发送!  // 4. 延时500ms,控制上报频率为2HzDelay_ms(100);
}

ADC数据转运通道的初始化

void ADCDMA_init()
{RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);DMA_InitTypeDef DMA_InitStructure;// --- 配置DMA1_Channel5 (USART1_RX) ---DMA_DeInit(DMA1_Channel1); // 复位DMA通道1DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设地址:USART1数据寄存器DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values; // 内存地址:接收缓冲区DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 方向:外设(串口)到内存DMA_InitStructure.DMA_BufferSize = 2; // 缓冲区大小DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不自增DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // ADC数据是16位DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 数据宽度:字节DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 模式:循环模式DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级:高DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式DMA_Init(DMA1_Channel1, &DMA_InitStructure);// 使能ADC1外设发出DMA请求。当一次转换完成时,ADC会向DMA控制器请求数据转运。ADC_DMACmd(ADC1, ENABLE);// 使能DMA1_Channel5DMA_Cmd(DMA1_Channel1, ENABLE);
}

完整代码在本页顶部位置处,有需要的铁子可以自取,细细体会一番

http://www.xdnf.cn/news/1301833.html

相关文章:

  • 开始回溯的学习
  • I/O多路复用特性与实现
  • 【学习嵌入式day-25-线程】
  • 扣子(Coze),开源了!Dify 天塌了
  • 无人机智能跟踪模块设计与运行分析
  • Mac Mysql 卸载
  • 【Docker】openEuler 使用docker-compose部署gitlab-ce
  • C++设计模式:类间关系
  • 企业级时序数据库选型指南:从传统架构向智能时序数据管理的转型之路
  • Flinksql bug: Heartbeat of TaskManager with id container_XXX timed out.
  • gitee_流水线搭配 Dockerfile 部署vue项目
  • MetaFox官方版:轻松转换视频,畅享MKV格式的便捷与高效
  • 【Linux基础知识系列】第九十六篇 - 使用history命令管理命令历史
  • std::set_symmetric_difference
  • 4. 图像识别模型与训练策略
  • 解锁AI大模型:Prompt工程全面解析
  • Spring MVC ModelAndView 详解
  • Linux网络基础(一)
  • 【计算机视觉与深度学习实战】01基于直方图优化的图像去雾技术
  • Python入门第3课:Python中的条件判断与循环语句
  • 电商架构测试体系:ZKmall开源商城筑牢高并发场景下的系统防线
  • Dijkstra与Floyd求最短路算法简介
  • 【JAVA高级】实现word转pdf 实现,源码概述。深坑总结
  • Vue3 学习教程,从入门到精通,Axios 在 Vue 3 中的使用指南(37)
  • 在Ubuntu 22.04上安装远程桌面服务
  • 关于C++的#include的超超超详细讲解
  • 为什么 /deep/ 现在不推荐使用?
  • 稳定且高效:GSPO如何革新大型语言模型的强化学习训练?
  • Webpack详解
  • 思考:高速场景的行星轮混动效率如何理解