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);
	//配置ADCCLK
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	//配置GPIO
	GPIO_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,控制上报频率为2Hz
  Delay_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通道1
  DMA_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_Channel5
  DMA_Cmd(DMA1_Channel1, ENABLE);
}

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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐