STM32多通道ADC+DMA同步采集原理与实现

1. 多通道ADC采集与DMA读取的工程实现原理

在嵌入式系统中,当需要对多个模拟信号源进行周期性、高精度、低CPU开销的数据采集时,单通道ADC配置已无法满足实际需求。典型的工业传感器阵列、多路环境监测节点或电机控制系统中,往往需同步采集温度、电压、电流、压力等多个物理量。此时,必须采用扫描模式(Scan Mode)配合DMA(Direct Memory Access)控制器构建高效的数据通路。本实验以STM32F103系列MCU为平台,基于ADC1外设,通过PA0–PA5六个GPIO引脚接入六路模拟输入,完整实现多通道连续采集与DMA自动搬运的工程闭环。其核心价值不在于功能罗列,而在于揭示ADC硬件状态机、DMA传输协议与CPU资源调度三者间的协同逻辑。

1.1 扫描模式的本质:硬件级通道轮询调度器

扫描模式并非软件循环调用的简化替代,而是ADC内部状态机的固有工作方式。当 ADC_InitTypeDef.ADC_ScanConvMode 设置为 ENABLE 时,ADC硬件将依据规则组(Regular Group)中预设的通道序列,自动完成通道切换、采样保持、逐次逼近转换(SAR)及结果寄存器更新的全流程。该过程完全脱离CPU干预,仅需一次启动指令( HAL_ADC_Start() ),后续所有通道的转换均按序自动触发。关键参数 ADC_NbrOfChannel 定义了该序列长度——本实验中设为6,意味着每次ADC启动后,硬件将严格按顺序执行CH0→CH1→CH2→CH3→CH4→CH5共六次转换,形成一个完整的扫描周期(Scan Cycle)。此机制消除了软件延时引入的通道间时间抖动,保证了多路信号在时间轴上的严格对齐性,是实现同步采样的物理基础。

1.2 DMA传输的触发与数据流绑定

DMA在此架构中承担“数据搬运工”角色,其存在意义在于解除CPU对海量采样数据的搬运负担。ADC在每次转换完成(EOC, End of Conversion)时,会向DMA控制器发出一个硬件请求(Request Signal)。DMA接收到该请求后,立即执行一次内存写操作,将 ADC_DR (Data Register)中的16位转换结果搬移至用户指定的缓冲区地址。本实验中,DMA通道(DMA1_Channel1)被配置为外设到内存(Peripheral to Memory)传输方向,且工作于循环模式(Circular Mode)。循环模式确保DMA在填满整个缓冲区后,自动重置地址指针,持续覆盖写入,避免因缓冲区溢出导致的数据丢失。这种“ADC硬件触发→DMA硬件搬运→内存自动填充”的三级流水线,将CPU从每毫秒数百次的数据读取中断中彻底解放,使其可专注于数据处理、通信协议栈或人机交互等更高价值任务。

1.3 时钟树与采样时间的协同约束

ADC的精度与速度直接受制于其时钟源( ADCCLK )与各通道采样时间( ADC_SampleTime )的精确配置。STM32F103的ADCCLK由APB2总线时钟( PCLK2 )经预分频器( RCC_CFGR ADCPRE 位)分频得到,最大允许频率为14MHz。本实验中,若 PCLK2 =72MHz,则 ADCPRE=01 (二分频)可得 ADCCLK=36MHz ,但此值超限,故必须选择 ADCPRE=11 (八分频),使 ADCCLK=9MHz ,满足ADC规格书要求。采样时间则决定了模拟输入信号在采样电容上建立稳定电压所需的时间。 ADC_SampleTime_239.5Cycles (239.5个ADC时钟周期)是本实验选用的最大采样时间,对应约26.6μs(239.5/9MHz)。该长采样时间虽牺牲部分吞吐率,却极大提升了对高源阻抗信号(如电位器分压输出)的采集精度,有效抑制了采样电容充电不足引起的量化误差。总转换时间( Tconv )由此计算: Tconv = 采样时间 + 12.5个ADC时钟周期(SAR转换固定开销) ≈ 26.6μs + 1.39μs ≈ 28μs。单次扫描周期(6通道)理论耗时约168μs,远低于常见应用所需的毫秒级采样间隔,为系统留出了充足的处理裕量。

2. 工程化代码实现与关键配置解析

本节将基于HAL库框架,详细剖析多通道ADC+DMA方案的核心驱动代码。所有配置均围绕硬件原理展开,杜绝无意义的参数堆砌,每一行代码均有其明确的工程目的与物理意义。

2.1 GPIO与ADC外设时钟使能

// 使能GPIOA与ADC1时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_ADC1_CLK_ENABLE();

// 配置PA0-PA5为模拟输入模式
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | 
                       GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

原理阐释 :GPIO引脚必须显式配置为 GPIO_MODE_ANALOG ,这是ADC正常工作的前提。该模式禁用施密特触发器与上/下拉电阻,使引脚呈现高阻抗状态,最大限度减少对模拟信号源的负载效应。若误配为浮空输入( GPIO_MODE_INPUT )或带上拉/下拉,将导致ADC参考电压被拉偏,引入系统性测量误差。时钟使能顺序亦不可颠倒——必须先使能GPIO时钟再初始化GPIO,否则寄存器写入无效。

2.2 ADC初始化:扫描模式与通道序列配置

ADC_HandleTypeDef hadc1;
ADC_ChannelConfTypeDef sConfig = {0};

// ADC基本参数配置
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ENABLE;           // 关键:启用扫描模式
hadc1.Init.ContinuousConvMode = ENABLE;     // 连续模式,扫描周期自动重复
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 6;            // 规则组包含6个通道
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
    Error_Handler(); // 初始化失败处理
}

// 通道0配置(CH0 -> PA0)
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;                            // 在规则组中排第1位
sConfig.SamplingTime = ADC_SAMPLETIME_239CYCLES_5; // 最大采样时间
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
}

// 通道1配置(CH1 -> PA1)
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;                            // 在规则组中排第2位
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
}

// 依此类推,配置CH2(CH2), CH3(CH3), CH4(CH4), CH5(CH5)
// Rank值依次为3,4,5,6,构成完整的6通道扫描序列

原理阐释 NbrOfConversion=6 与六个 HAL_ADC_ConfigChannel() 调用共同定义了扫描序列的拓扑结构。 Rank 参数决定了硬件执行转换的严格顺序——Rank=1的通道最先转换,Rank=6的最后转换。此顺序直接映射到后续DMA搬运的数据在缓冲区中的存储位置。例如,若 Rank=1 对应CH0,则DMA缓冲区索引 [0] [6] [12] …处的数据即为CH0的连续采样值。该设计为后续按通道分离数据提供了确定性内存布局,是实现通道级数据处理的基础。

2.3 DMA初始化与ADC-DMA绑定

DMA_HandleTypeDef hdma_adc1;

// DMA基本参数配置
hdma_adc1.Instance = DMA1_Channel1;
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;      // 外设地址固定(ADC_DR)
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;          // 内存地址递增
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;               // 关键:循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK) {
    Error_Handler();
}

// 将DMA句柄关联到ADC句柄
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

// 启用ADC的DMA请求
__HAL_ADC_ENABLE_DMA(&hadc1, ADC_DMA_CONTINUOUS_REQUEST);

原理阐释 PeriphInc=DISABLE 确保DMA始终从 ADC_DR 这一固定地址读取数据; MemInc=ENABLE 则使DMA在每次传输后自动递增内存地址,将6个通道的转换结果依次存入缓冲区。 DMA_CIRCULAR 模式是实现持续采集的关键——当DMA填满整个缓冲区后,自动重置内存地址指针,开始新一轮覆盖写入,避免了因缓冲区满而触发的中断或传输停止。 __HAL_ADC_ENABLE_DMA() 宏最终将ADC的EOC事件与DMA通道1的请求信号在硬件层面连接,完成了“ADC转换完成→DMA自动搬运”的硬连线,此过程无需任何CPU参与。

2.4 数据缓冲区设计与DMA内存布局

// 定义DMA缓冲区:6通道 × 50次采样 = 300个16位数据
#define ADC_CHANNELS 6
#define SAMPLES_PER_CHANNEL 50
#define BUFFER_SIZE (ADC_CHANNELS * SAMPLES_PER_CHANNEL)

uint16_t adc_dma_buffer[BUFFER_SIZE]; // 全局缓冲区,供DMA直接写入

// 启动ADC与DMA
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, BUFFER_SIZE,
                 ADC_ALIGN_RIGHT, DMA_CIRCULAR);

原理阐释 :缓冲区大小 BUFFER_SIZE=300 的设计蕴含了工程权衡。 SAMPLES_PER_CHANNEL=50 并非随意设定,而是为后续软件滤波(如滑动平均)提供足够样本点。DMA以循环模式向此缓冲区写入,其内存布局遵循严格的通道顺序: [0] ~ [49] 为CH0的50次采样, [50] ~ [99] 为CH1的50次, [100] ~ [149] 为CH2,依此类推。这种线性、可预测的布局,使得在数据处理阶段可通过简单的整数除法( index / SAMPLES_PER_CHANNEL )快速定位通道号,通过取模运算( index % SAMPLES_PER_CHANNEL )获取该通道内的采样序号,极大简化了数据索引逻辑。

3. 应用层数据处理与显示逻辑

DMA完成数据搬运后,CPU需在适当时机读取缓冲区内容,进行数值计算与用户界面更新。本节重点解析如何高效、准确地从循环缓冲区中提取有效数据,并转化为用户可理解的物理量。

3.1 DMA传输完成中断与数据就绪标志

// 在stm32f1xx_it.c中定义DMA传输完成中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_adc1);
}

// 在HAL_DMA_IRQHandler回调中,当DMA完成一次完整缓冲区填充时触发
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1) {
        // 标记DMA数据已就绪,供主循环检查
        dma_transfer_complete_flag = 1;
    }
}

原理阐释 HAL_ADC_ConvCpltCallback() 是HAL库提供的标准回调函数,当DMA完成对整个 BUFFER_SIZE (300个字)的搬运后自动调用。此处设置全局标志 dma_transfer_complete_flag=1 ,是一种轻量级的同步机制。主循环( while(1) )通过轮询此标志即可获知新一批50×6个采样数据已准备就绪,可安全读取。此方法避免了在中断服务函数内执行耗时的计算与显示操作,保障了中断响应的实时性。

3.2 通道级数据提取与滑动平均滤波

// 主循环中处理数据
if(dma_transfer_complete_flag) {
    dma_transfer_complete_flag = 0; // 清除标志

    // 对每个通道(0-5)计算50次采样的平均值
    for(uint8_t ch = 0; ch < ADC_CHANNELS; ch++) {
        uint32_t sum = 0;
        // 累加该通道的50个采样值
        for(uint8_t i = 0; i < SAMPLES_PER_CHANNEL; i++) {
            uint32_t index = ch * SAMPLES_PER_CHANNEL + i;
            sum += adc_dma_buffer[index];
        }
        // 计算平均值(整数除法)
        uint16_t avg_value = sum / SAMPLES_PER_CHANNEL;

        // 存储平均结果(例如存入ch_avg_values[ch]数组)
        ch_avg_values[ch] = avg_value;
    }
}

原理阐释 :滑动平均(Moving Average)是嵌入式系统中最常用、最有效的软件滤波技术之一,能显著抑制随机噪声。本实现中,对每个通道独立累加其在缓冲区中对应的50个连续采样值,再求平均。此过程利用了DMA循环写入的特性:当 dma_transfer_complete_flag 被置位时,缓冲区中 ch * 50 ch * 50 + 49 这50个位置的数据,必然是该通道最近连续50次转换的结果。这种确定性的数据时空关系,是滤波算法正确性的根本保障。平均值计算采用整数运算,规避了浮点运算的性能开销与精度损失。

3.3 ADC数值到物理电压的精确换算

// 假设VREF+ = 3.3V,ADC为12位分辨率(4096级)
#define VREF_MV 3300
#define ADC_RESOLUTION 4096

// 将12位ADC平均值转换为毫伏(mV)
uint32_t voltage_mv = (uint32_t)ch_avg_values[ch] * VREF_MV / ADC_RESOLUTION;

// 分离整数与小数部分用于显示(例如显示"3.299V")
uint16_t voltage_int = voltage_mv / 1000;         // 整数部分(3)
uint16_t voltage_dec = voltage_mv % 1000;         // 小数部分(299)

原理阐释 :换算公式 Voltage = (ADC_Value / 4096) * VREF 是ADC的基本原理。 VREF_MV=3300 代表参考电压为3.3V, ADC_RESOLUTION=4096 是12位ADC的满量程计数值。为避免浮点运算并保证显示精度,全程采用整数运算:先将ADC值乘以3300(单位为毫伏),再除以4096,得到结果单位为毫伏。随后通过整数除法与取模,将毫伏值分解为整数伏特( voltage_int )和三位小数毫伏( voltage_dec ),为LCD或串口显示提供格式化数据。此方法消除了浮点舍入误差,确保了显示值与理论值的严格一致性。

4. 硬件连接与实测验证要点

理论设计的最终价值体现在真实硬件上的可靠运行。本节总结实验板级连接的关键细节与常见问题排查思路,这些经验源于多次硬件调试的实践沉淀。

4.1 正点原子战舰开发板引脚映射确认

在正点原子战舰V3开发板上,ADC1的通道0至通道5严格对应GPIOA的Pin0至Pin5:
- ADC1_IN0 → PA0 :位于P1端子排,标号为 ADC_IN0
- ADC1_IN1 → PA1 :位于P1端子排,标号为 ADC_IN1
- ADC1_IN2 → PA2 :位于P7端子排,标号为 ADC_IN2
- ADC1_IN3 → PA3 :位于P7端子排,标号为 ADC_IN3
- ADC1_IN4 → PA4 :位于P1端子排,标号为 ADC_IN4
- ADC1_IN5 → PA5 :位于P1端子排,标号为 ADC_IN5

实践要点 :务必使用万用表蜂鸣档,在开发板上实测所选端子与对应PAx引脚的连通性。曾遇到某批次P1端子排焊接虚焊,导致PA4通道读数恒为0,此问题仅通过实测导通性得以快速定位。切勿完全依赖原理图或文档描述。

4.2 电源与参考电压的稳定性验证

ADC的精度直接受 VDDA (模拟电源)与 VREF+ (参考电压)质量影响。战舰板默认使用 VDDA=3.3V 作为参考,其稳定性至关重要:
- 使用示波器观察 VDDA 引脚,纹波应小于10mVpp。若纹波过大,可在 VDDA VSSA (模拟地)之间并联一个10μF钽电容与100nF陶瓷电容。
- 测量 PA0 悬空时的ADC读数。理想情况下,由于输入高阻,读数应在0x000–0x00F范围内微小跳变。若读数稳定在某个非零值(如0x100),则表明 VDDA 存在共模噪声或 VSSA 接地不良。

4.3 实测数据解读与异常诊断

在串口终端或LCD上观察到的数据,其行为模式是判断系统健康状态的直观依据:
- 全通道读数恒为0 :检查PAx引脚是否被意外配置为推挽输出或上拉输入;确认 HAL_ADC_Init() HAL_ADC_ConfigChannel() 调用无误;用万用表直流电压档测量PAx对地电压,确认信号源存在。
- 单通道读数恒为满量程(0xFFF) :该通道引脚可能被短接到 VDDA ;或PCB走线存在与 VDDA 的隐性短路;检查 HAL_GPIO_Init() 中是否误将该引脚设为 GPIO_MODE_OUTPUT_PP
- 读数规律性跳变(如0x000, 0xFFF, 0x000, 0xFFF) :典型DMA地址错位症状。检查 HAL_ADC_Start_DMA() 中传入的缓冲区地址是否为 (uint32_t*) 类型强制转换,而非 (uint16_t*) ;确认 DMA_PeriphDataAlignment DMA_MemDataAlignment 均设为 HALFWORD

我在实际项目中曾因 DMA_MemDataAlignment 误设为 BYTE ,导致DMA将16位ADC值拆分为两个字节写入内存,后续读取时高位字节被解释为另一通道数据,引发完全不可预测的乱码。踩过几次坑之后,现在每次配置DMA都会用示波器抓取 DMA1_Channel1 的请求信号与 ADC_DR 读取时序,双重验证数据流的完整性。

摘 要 随着互联网影视产业的迅速发展,电影资源呈指数级增长,类型也愈加多样化,但是用户面对如此庞大的影片库时,会遇到信息过载、筛选效率低下、观影决策成本高这些难题,而传统的电影平台大多只是对基本的分类和热度进行展示,并没有提供个性化的推荐服务,社交互动以及一体化管理的能力也比较薄弱。 该系统使用的是Spring Boot+Vue前后端分离的方式进行开发,设计并开发了一个电影推荐和评论系统。开发系统中存在三个问题,即无法准确地判断出用户的喜好、电影推荐功能上线之初没有用户数据,因此推荐效果不佳、大量用户同时评论打分时系统容易出现不稳定的情况。为了克服上述问题,本文查阅相关资料,不断迭代开发原型,并进行实际测试,完成整个系统的全部过程,即系统需求分析、架构设计、功能开发、系统测试。系统分为普通用户和管理员两种身份,具有注册登录、电影浏览、电影推荐、评分评论、影片收藏、个人中心、后台管理等功能,采用结合用户喜好和电影热度的简单推荐方式,提供热门电影推荐和个性化推荐,很好地解决了推荐功能初始没有数据数据较少的问题。项目用接口来完成前后端的数据交互,使用MySQL数据库存储用户、电影、评论、收藏等各种数据,并且加入权限验证、密码加密等安全措施,保证系统安全稳定并且便于后期扩展。除此之外,系统还增加了电影资讯查看、首页轮播图设置、编辑资讯等功能,使整个电影平台的服务更加全面,使用更加高效。经过全面的功能测试、接口测试和性能测试,系统各个模块运行稳定,推荐接口响应时间小于300ms,主要的互动操作成功率大于98%,可以大大降低用户的选片成本,提高观影决策的速度和社区互动的效果。 本文给出一套轻量级、易部署、可复用的电影推荐类Web应用工程实现方案,相比于传统的单一列表展示型平台,个性化服务、社交互动体验和后台管理效率都有明显的提高,可以给影视文化数字化传播、推荐算法轻量化工程实践
源码直接下载地址: https://pan.quark.cn/s/a4b39357ea24 在 Excel 中进行阳历阴历的相互转换,对于处理集体信息(例如通讯录)统计工作具有显著的实用性。本文将具体阐述如何借助 VBA 编辑器在 Excel 环境下完成阳历阴历的互换转换。首先,需要打开相应的 Excel 文件,通过按 Alt+F11 激活 VBA 编辑器,随后选择插入菜单下的模块选项,将提供的代码片段复制到新模块中,并保存更改后关闭编辑器。完成上述步骤后,即可在指定单元格中调用以下四个函数以达成转换目标。 1. 阴阳历转换功能: 函数 `Lunar(SolarDate[, Part = 0 | 1 | 2 | 3])` 负责将阳历日期转换为对应的阴历日期。参数 `Part` 决定转换内容的详略程度,其值可为 0、1、2 或 3,分别对应完整日期、阴历年、阴历月及阴历日的转换。 函数 `Solar(LunarDate[, LunarMonth = 0 | 1])` 适用于将阴历日期转换为阳历日期。参数 `LunarMonth` 用于指定月份的归属,可为 0 或 1,分别代表转换至阳历月份或保留阴历月份。 2. 生日日期转换功能: 函数 `lunarbirth("1975-5-6")` 能够计算出阴历生日所对应的阳历日期。相对地,函数 `solarbirth("1975-5-6")` 用于推算阳历生日对应的阴历日期。 3. 日期信息计算功能: 函数 `LunarData(q_year)` 提供阴历日期的详细数据,涵盖阴历年、阴历月、阴历日等信息。函数 `ConvDataA` 存储了阴历阳历的日期数据,包括阴历年、阴历月、阴历日、阳历月、阳历日等字段。 4. 应用...
下载代码方式:https://pan.quark.cn/s/a4b39357ea24 在信息技术行业中,特别是在人工智能(AI)的子领域——计算机视觉方面,动作分析是一个至关重要的组成部分。这一议题“Python-PyTorch动作分析模型库”有着紧密的联系,它涵盖了运用Python编程语言和PyTorch框架来构建和应用深度学习模型,旨在解析和判定视频中的行为。接下来,我们将详细研究这一领域的重要概念。 **PyTorch** 是由Facebook开发的一个开源且功能强大的深度学习平台,它具备动态计算图特性,从而让模型构建和调试过程更加便捷。PyTorch的关键在于Tensor类,该类是数值运算的基础,同时支持自动计算梯度,为神经网络的训练提供了便利。 **动作分析** 是计算机视觉中的一个核心任务,其目的是识别视频中的特定行为,例如奔跑、跳跃、招手等。这项任务通常包括从视频材料中提取图像帧,然后对单个帧或帧序列进行特征提取,最终借助已训练的模型进行分类。 在描述中提及的“流行动作分析模型”,或许涵盖了当前研究领域的主流模型,例如**双流卷积网络**,这种模型融合了空间和时间信息,通过分别处理RGB图像和光流图像来提高行为分析的精确度。还可能包括**时间分割网络(TSN)**,它通过跨长时间范围的样本选择来把握行为的整体特征。另外,更前沿的模型如**时间迁移模块(TSM)** 和 **非局部神经网络** 也可能被纳入其中,这些模型通过创新的网络构造来更有效地捕捉时间序列中的动态变化。 **某类数据集** 可能是指像UCF-101或Kinetics这样的标准动作分析数据集,它们包含了大量标注好的视频片段,用于模型的训练和性能评估。这些数据集在动作分析研究中被...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值