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仅在必要时介入:
信号输入: 两个电位器产生的模拟电压信号 (0-3.3V) 分别输入到MCU的
PA0
和PA1
引脚。ADC自动采样: ADC1被配置为多通道扫描和连续转换模式。启动后,它会像一个永不停歇的“扫描仪”,自动依次对
PA0
和PA1
进行A/D转换,并将结果暂存至其内部的数据寄存器(ADC1->DR
)。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]
,实现了对内存缓冲区的循环写入。
CPU轻量处理:
CPU的主循环
while(1)
以一个固定的频率醒来一次。它的任务非常简单:读取
volatile adc_values
数组中由DMA随时更新的最新数据,并使用sprintf
函数将这两个数字格式化成一个人类可读的字符串(例如"ADC CH0=1024, CH1=2048\r\n"
),存入uart_tx_buffer
。
DMA异步上报 (上报环节):
CPU准备好数据后,启动DMA1通道4。
DMA1通道4从
uart_tx_buffer
中读取字符串内容,并逐字节地送入USART1->DR
寄存器,通过串口发送出去。此过程工作在普通模式下,发送完指定长度的数据后便自动停止。CPU在启动它之后,无需等待,可以立刻进入下一次延时。
4. 关键实现细节
ADC配置:
ADC_ScanConvMode
和ADC_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);
}
完整代码在本页顶部位置处,有需要的铁子可以自取,细细体会一番