STM32 DMA实战:ADC连续采样与UART发送的完整配置指南

📅 2026/8/4 11:59:39
STM32 DMA实战:ADC连续采样与UART发送的完整配置指南
最近在嵌入式开发中经常需要处理大量数据在内存与外设如ADC、DAC、UART之间的搬运。如果全靠CPU搬运会严重消耗其算力导致系统响应变慢。DMA直接存储器访问技术就是为了解决这个痛点而生的它允许外设与内存之间直接进行数据传输无需CPU干预从而解放CPU去处理更复杂的任务。本文将以STM32系列微控制器为例深入讲解DMA的工作原理、配置流程并提供一个从零开始的完整实战案例使用DMA实现ADC连续采样并通过串口DMA发送数据。无论你是刚接触STM32的新手还是希望优化现有项目性能的开发者都能通过本文掌握DMA的核心配置技巧和避坑指南。1. DMA核心概念与工作原理1.1 什么是DMADMA全称Direct Memory Access即直接存储器访问。它是一种允许特定硬件子系统外设独立于中央处理器CPU直接读写系统内存的技术。你可以把它想象成一个高效的“数据搬运工”。在没有DMA的情况下当ADC转换完成一个数据CPU需要执行一系列指令从ADC数据寄存器读取值 - 将这个值存入某个内存变量 - 判断是否发送 - 将变量值写入UART数据寄存器。这个过程每一步都占用CPU周期。而启用DMA后你只需要告诉DMA控制器“请把ADC数据寄存器源地址的数据自动搬运到内存中的数组目标地址每搬运完一个就告诉我一声”。之后ADC每转换一次DMA就自动完成一次搬运CPU完全不用管可以继续执行主循环或其他中断服务程序。1.2 DMA在STM32中的架构与通道在STM32中DMA控制器是一个独立的外设。以STM32F1系列为例它通常有1-2个DMA控制器DMA1 DMA2每个控制器有多个通道Channel。通道Channel每个通道关联到一个或多个特定的外设如ADC1、USART1_TX、TIM2_CH1等。通道是DMA传输的执行单元一次传输只能使用一个通道。仲裁器Arbiter当多个通道同时请求DMA传输时仲裁器根据优先级软件可配置来决定哪个通道先执行。数据流Stream在更高级的系列如STM32F4/F7/H7中引入了“数据流Stream”的概念一个数据流可以映射到多个通道提供了更灵活的配置。一次DMA传输涉及三个关键地址外设地址数据来源或去向的外设数据寄存器地址如ADC1-DR。存储器地址数据在内存中的地址如一个数组adc_buffer的地址。传输数量NDTR需要传输的数据项数量。1.3 DMA传输模式与方向DMA传输主要有以下几种模式理解它们对正确配置至关重要传输方向外设到存储器例如ADC采样数据存入内存数组。存储器到外设例如从内存数组发送数据到UART。存储器到存储器例如在两个内存区域如数组间快速拷贝数据。注意不是所有型号的DMA都支持此模式。传输模式单次传输SingleDMA在传输完指定数量的数据后自动停止需要重新使能才能进行下一次传输。循环传输CircularDMA在传输完指定数量的数据后自动重置传输计数器并重新开始形成一个“乒乓”缓冲区非常适合连续数据流如音频采集、持续ADC采样。数据宽度与对齐需要配置外设和存储器的数据宽度8位、16位、32位两者可以不同DMA会自动处理数据打包/解包但需注意地址对齐问题。增量模式决定每次传输后外设地址和/或存储器地址是否自动增加。例如从ADC读数据外设地址ADC数据寄存器地址通常固定而写入内存数组时存储器地址需要递增。2. 开发环境与硬件准备2.1 硬件平台与软件工具主控芯片本文以STM32F103C8T6蓝桥杯常用核心板为例其DMA1有7个通道。原理同样适用于其他STM32系列但寄存器或HAL库函数名可能略有不同。开发环境IDEKeil MDK-ARM (uVision5) 或 STM32CubeIDE。开发库本文采用STM32CubeMX图形化配置工具结合HAL库进行开发这种方式直观且可移植性强。调试工具ST-LINK V2 或 J-Link。外设使用ADC1用于模拟信号采样使用通道0PA0。USART1用于打印采样数据TX引脚为PA9RX引脚为PA10。DMA1使用通道1服务于ADC1通道4服务于USART1_TX。2.2 使用STM32CubeMX创建工程新建项目打开STM32CubeMX选择MCU型号STM32F103C8Tx。配置时钟在RCC配置中将高速外部时钟HSE设置为Crystal/Ceramic Resonator。然后在Clock Configuration标签页将系统时钟SYSCLK配置为72MHz这是F103的最高主频。配置ADC在Analog下拉菜单中找到ADC1。启用IN0通道对应PA0。在右侧参数设置中Scan Conversion Mode:Enabled(扫描模式为多通道预留)。Continuous Conversion Mode:Enabled(连续转换模式)。DMA Continuous Requests:Enabled(使能DMA连续请求)。End of Conversion Selection:EOC flag at the end of single conversion。在Rank中设置采样时间例如Cycles 55.5以获得较好精度。配置DMA for ADC点击DMA Settings-Add。选择ADC1。配置模式Mode:Circular(循环模式实现连续采样)。Direction:Peripheral To Memory。Increment Address:Memory选Yes(内存地址递增)Peripheral选No(外设地址固定)。Data Width:Peripheral和Memory都选Word(32位因为ADC是12位HAL库用32位变量存储)。配置USART在Connectivity中选择USART1。模式选择Asynchronous(异步通信)。参数默认波特率115200 8位数据无校验1停止位。配置DMA for USART_TX再次进入DMA Settings-Add。选择USART1_TX。配置模式Mode:Normal(单次模式发送完指定数量数据后停止)。Direction:Memory To Peripheral。Increment Address:Memory选YesPeripheral选No。Data Width:Byte(UART通常按字节发送)。生成代码在Project Manager标签页设置项目名称、路径、IDEMDK-ARM V5。在Code Generator中选择“为每个外设生成独立的.c/.h文件”。点击GENERATE CODE生成工程。3. HAL库DMA关键函数与配置解析生成的代码基于HAL库它封装了底层寄存器操作。以下是几个核心的DMA相关HAL函数3.1 启动DMA传输的函数// 启动ADC的DMA传输 // hadc: ADC句柄指针 // pData: 目标内存缓冲区地址 // Length: 传输数据长度单位取决于数据宽度 HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length); // 启动UART的DMA发送 // huart: UART句柄指针 // pData: 源内存缓冲区地址 // Size: 发送数据长度字节 HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef* huart, uint8_t* pData, uint16_t Size);3.2 DMA传输回调函数DMA传输完成、半传输完成或发生错误时会触发中断并调用相应的回调函数。我们需要在用户代码中重写Weak这些函数以添加自己的处理逻辑。// DMA传输完成回调在stm32f1xx_hal_adc.c中已定义为__weak void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc); void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc); // 半传输完成 // UART DMA发送完成回调在stm32f1xx_hal_uart.c中 void HAL_UART_TxCpltCallback(UART_HandleTypeDef* huart);循环模式下的缓冲区管理当ADC DMA配置为循环模式时HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback会交替被调用。这是一种高效的“乒乓缓冲区”策略。例如你定义一个大小为N的数组adc_buf[N]。当DMA填满前半部分adc_buf[0]到adc_buf[N/2-1]时触发半传输完成中断你可以安全地处理这前半部分数据。当DMA继续填满后半部分adc_buf[N/2]到adc_buf[N-1]时触发传输完成中断你可以处理后半部分数据。与此同时DMA会自动回到数组开头重新开始填充如此循环。这保证了数据处理和数据采集的并发进行没有数据丢失。4. 完整实战ADC DMA采样与UART DMA发送接下来我们实现一个经典场景使用DMA循环采集ADC数据并在主循环中定时通过DMA将一批数据发送到串口助手显示。4.1 定义全局变量与缓冲区在main.c文件开始处用户代码区域添加以下定义/* USER CODE BEGIN PV */ #define ADC_BUFFER_SIZE 100 // ADC DMA缓冲区大小 uint32_t adc_dma_buffer[ADC_BUFFER_SIZE]; // ADC DMA目标缓冲区 uint8_t uart_tx_buffer[ADC_BUFFER_SIZE * 5]; // UART发送缓冲区需要将数值转换为字符串 volatile uint8_t adc_data_ready 0; // 数据就绪标志 uint32_t adc_data_index 0; // 用于标记已处理的数据位置 /* USER CODE END PV */4.2 启动ADC DMA采集在main函数的/* USER CODE BEGIN 2 */区域启动ADC的DMA采集/* USER CODE BEGIN 2 */ // 启动ADC并指定DMA传输的目标地址和长度 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFFER_SIZE) ! HAL_OK) { Error_Handler(); // 启动失败进入错误处理 } /* USER CODE END 2 */4.3 实现ADC DMA传输完成回调在main.c文件中找到/* USER CODE BEGIN 4 */区域添加回调函数。这里我们使用传输完成回调来设置标志位。/* USER CODE BEGIN 4 */ /** * brief ADC转换完成回调函数DMA模式 * param hadc: ADC句柄 * retval None */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { adc_data_ready 1; // 设置数据就绪标志 // 注意在循环模式下这个回调会在缓冲区被完全填满时调用。 // 此时DMA已经回到缓冲区开头继续填充所以我们需要尽快处理数据或拷贝数据。 } } /* USER CODE END 4 */4.4 主循环处理与UART DMA发送在main函数的while (1)循环中我们检查标志位处理数据并发送。/* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if(adc_data_ready) { adc_data_ready 0; // 清除标志 // 1. 将ADC原始值0-4095转换为电压值字符串假设Vref3.3V // 为了演示我们只处理前10个数据并发送 int offset 0; for(int i 0; i 10 i ADC_BUFFER_SIZE; i) { float voltage (adc_dma_buffer[i] * 3.3f) / 4095.0f; offset sprintf((char*)uart_tx_buffer[offset], ADC[%d]: %.3fV\r\n, i, voltage); } offset sprintf((char*)uart_tx_buffer[offset], ----\r\n); // 2. 通过DMA发送字符串到UART // 注意HAL_UART_Transmit_DMA是非阻塞的函数调用后立即返回。 // 在上一次DMA发送完成前不能再次调用此函数发送同一个huart实例否则会导致错误。 // 这里我们简单处理在实际项目中需要更严谨的状态管理。 if(HAL_UART_GetState(huart1) HAL_UART_STATE_READY) { HAL_UART_Transmit_DMA(huart1, uart_tx_buffer, offset); } // 3. 添加一个简单延时避免发送过于频繁 HAL_Delay(1000); // 延时1秒 } // 其他任务... } /* USER CODE END 3 */4.5 编译、下载与验证编译工程点击IDE中的编译按钮确保0错误0警告。连接硬件将STM32核心板的PA0ADC输入连接到一个可调电位器的中间引脚电位器两端接3.3V和GND。将PA9UART_TX连接到USB转TTL模块的RX共地。下载程序使用ST-LINK将程序下载到芯片。观察结果打开串口助手如XCOM Putty设置波特率115200。旋转电位器你应该能看到串口每秒打印一次10个ADC通道的电压值。5. 常见问题与排查思路DMA配置相对复杂容易遇到问题。下表列出了常见现象、原因及解决方法问题现象可能原因排查思路与解决方案ADC DMA不启动数据不变1. DMA或ADC时钟未使能。2. DMA通道映射错误。3.HAL_ADC_Start_DMA调用失败未检查。4. 缓冲区地址或长度参数错误。1. 在CubeMX中检查RCC配置确保DMA和对应外设时钟已开启。2. 核对数据手册确认ADC1是否对应DMA1 Channel1F1系列。3. 检查函数返回值并进入Error_Handler调试。4. 确保缓冲区是全局变量或静态变量地址有效。UART DMA发送一次后卡死1. 在上一次DMA发送完成前再次调用了HAL_UART_Transmit_DMA。2. UART或DMA发生错误未清除。1. 在发送前检查UART状态if(HAL_UART_GetState(huart1) HAL_UART_STATE_READY)。2. 实现HAL_UART_ErrorCallback回调函数打印或处理错误。考虑使用发送完成标志位或队列管理发送请求。ADC采样数据错乱或偏移1. 数据宽度配置不一致。2. 内存地址增量模式错误。3. 缓冲区大小太小DMA覆盖了未处理的数据。1. 在CubeMX中确认ADC侧和Memory侧的数据宽度通常都是Word。2. 确认ADC的DMA配置Memory Increment Yes, Peripheral Increment No。3. 增大缓冲区或使用“乒乓缓冲区”策略半传输中断确保处理速度大于采集速度。DMA传输完成中断不触发1. DMA传输完成中断未使能。2. 中断优先级配置过低被其他中断阻塞。3. 回调函数未重写名字拼写错误。1. 在CubeMX的DMA配置中确认Transfer Complete Interrupt已勾选。2. 在NVIC配置中调整DMA通道中断的优先级。3. 确保回调函数名完全正确且放在USER CODE BEGIN 4区域。存储器到存储器DMA失败1. 芯片不支持此模式如某些F1型号。2. 源或目标地址不是内存地址如外设寄存器。3. 数据缓存Cache一致性问题在带Cache的M7等内核上。1. 查阅芯片参考手册的DMA章节。2. 确保地址是SRAM地址。3. 对于M7使用SCB_CleanDCache_by_Addr等函数维护Cache一致性。6. 最佳实践与工程建议掌握了基础操作后以下建议能帮助你在实际项目中更稳健、高效地使用DMA。精心设计缓冲区大小缓冲区大小需权衡。太小会导致溢出或频繁中断消耗CPU太大会增加内存占用和数据处理延迟。根据采样率、处理速度和系统内存综合决定。对齐对于高性能MCU如STM32H7考虑将DMA缓冲区进行字节对齐如使用__attribute__((aligned(32)))这有助于发挥DMA或Cache的最佳性能。多缓冲区对于连续高速数据流强烈建议使用双缓冲区甚至多缓冲区乒乓缓冲区配合半传输和全传输中断实现无锁、无丢失的数据生产和消费。妥善管理DMA状态避免重入对于Normal模式的DMA传输如UART发送一定要在本次传输完成回调函数被调用后才能启动下一次传输。使用状态标志位或队列来管理发送请求。错误处理务必实现DMA和对应外设的错误回调函数如HAL_UART_ErrorCallback并在其中进行错误标志清除、日志记录或系统复位提高系统健壮性。优化中断与优先级DMA中断频率可能很高。如果数据处理逻辑复杂不要在DMA传输完成中断回调中执行耗时操作应仅设置标志位在主循环或更低优先级的任务中处理数据。合理配置NVIC中断优先级。确保关键DMA通道如用于高速ADC或DAC的中断优先级高于其他非实时性任务的中断。注意数据一致性在启用数据缓存D-Cache的Cortex-M7等内核上如果CPU和DMA会访问同一块内存区域必须手动维护缓存一致性。DMA写入后CPU读取前需无效化Invalidate对应缓存行CPU写入后DMA读取前需清理Clean对应缓存行。混合使用查询与中断不是所有场景都需要DMA中断。对于低速、非连续的数据可以使用查询方式检查DMA传输是否完成通过HAL_DMA_GetState或__HAL_DMA_GET_FLAG减少中断开销。调试技巧在复杂DMA应用中使用调试器的“实时变量”窗口观察DMA缓冲区的数据变化非常直观。可以利用GPIO引脚在DMA中断开始和结束时拉高/拉低用示波器测量中断处理时间评估系统负载。DMA是提升嵌入式系统性能的利器但也引入了额外的复杂性。从简单的单外设DMA传输开始逐步理解其机制再应用到多外设、高并发的场景中。本文的ADCUART案例是一个经典的起点你可以在此基础上尝试更复杂的组合如DAC波形生成、SPI全双工通信、SD卡读写等。