江科大 STM32 — DMA直接存储器存取代码总结第八章来源B站 江协科技/江科大 STM32入门教程平台STM32F103C8T6标准外设库StdPeriph Library涵盖8-1 DMA数据转运存储器→存储器→ 8-2 DMAAD多通道外设→存储器一、DMA 存储器→存储器 数据转运8-1任务描述将 SRAM 中的数组DataA通过 DMA 搬运到数组DataB使用软件触发搬运一次即停止。文件清单Hardware/MyDMA.c— DMA 驱动包含MyDMA_Init()和MyDMA_Transfer()Hardware/MyDMA.h— 头文件User/main.c— 主程序验证数据从 DataA 搬运到 DataB关键参数总结参数取值含义DMA外设DMA1_Channel1存储器→存储器可任选通道此处选通道1外设地址数组 DataA 的地址数据来源“外设站点”存储器地址数组 DataB 的地址数据目的地“存储器站点”数据宽度Byte8位以一个字节为单位搬运传输方向DMA_DIR_PeripheralSRC从外设→存储器外设地址自增ENABLE每次搬运后源地址1依次取 DataA[0], DataA[1]…存储器地址自增ENABLE每次搬运后目标地址1依次写 DataB[0], DataB[1]…传输次数Size如4搬运 4 次后停止模式DMA_Mode_Normal正常模式传输计数器减到0后自动停止触发方式DMA_M2M_Enable软件触发存储器→存储器专用优先级DMA_Priority_Medium中等优先级MyDMA_Init() 代码逐段解析uint16_tMyDMA_Size;// 全局变量记录转运次数供 Transfer 函数重复写入计数器/** * brief 初始化 DMA将数据从 AddrA 搬运到 AddrB * param AddrA 源地址外设站点 * param AddrB 目标地址存储器站点 * param Size 转运次数 / 传输数据量 */voidMyDMA_Init(uint32_tAddrA,uint32_tAddrB,uint16_tSize){/*保存 Size 到全局变量便于后续 MyDMA_Transfer() 重复写入传输计数器*/MyDMA_SizeSize;// 第一步开启 RCC 时钟 // DMA 是 AHB 总线设备注意与 APB 总线的区别RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);// 第二步初始化 DMA 结构体参数 DMA_InitTypeDef DMA_InitStructure;/* 外设站点配置 */DMA_InitStructure.DMA_PeripheralBaseAddrAddrA;// 源地址DataA 数组首地址DMA_InitStructure.DMA_PeripheralDataSizeDMA_PeripheralDataSize_Byte;// 源端数据宽度 1字节DMA_InitStructure.DMA_PeripheralIncDMA_PeripheralInc_Enable;// 源地址自增每搬一个字节地址1依次DataA[0]→DataA[1]→…/* 存储器站点配置 */DMA_InitStructure.DMA_MemoryBaseAddrAddrB;// 目标地址DataB 数组首地址DMA_InitStructure.DMA_MemoryDataSizeDMA_MemoryDataSize_Byte;// 目标端数据宽度 1字节DMA_InitStructure.DMA_MemoryIncDMA_MemoryInc_Enable;// 目标地址自增每个字节依次填入 DataB[0], DataB[1]…/* 传输整体配置 */DMA_InitStructure.DMA_DIRDMA_DIR_PeripheralSRC;// 传输方向外设→存储器DMA_InitStructure.DMA_BufferSizeSize;// 传输计数器搬运多少次即数组大小DMA_InitStructure.DMA_ModeDMA_Mode_Normal;// 正常模式计数器减到0后自动停止不循环DMA_InitStructure.DMA_M2MDMA_M2M_Enable;// ★ 软件触发存储器→存储器模式DMA 以最快速度连续搬运直到计数器清零DMA_InitStructure.DMA_PriorityDMA_Priority_Medium;// 优先级多个 DMA 通道竞争 AHBP 时的仲裁优先级此处选中等DMA_Init(DMA1_Channel1,DMA_InitStructure);// 第三步初始状态不使能等待用户手动调用 MyDMA_Transfer() DMA_Cmd(DMA1_Channel1,DISABLE);}MyDMA_Transfer() 代码逐段解析每次要重新搬运数据时调用此函数一次即可。voidMyDMA_Transfer(void){// ★ 步骤1必须先失能 DMA// 手册规定——写传输计数器前必须关闭 DMA 通道DMA_Cmd(DMA1_Channel1,DISABLE);// ★ 步骤2重新写入传输计数器// 上一次转运完成后计数器已减到0必须重新写入才能再次搬运DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);// ★ 步骤3重新使能 DMA// 使能后在软件触发模式下 DMA 即刻开始全速搬运DMA_Cmd(DMA1_Channel1,ENABLE);// ★ 步骤4等待转运完成// TC1(Transfer Complete) 标志位计数器减到0时硬件自动置1while(DMA_GetFlagStatus(DMA1_FLAG_TC1)RESET);// ★ 步骤5清除标志位// 为下一次转运做准备清除本次完成的标志DMA_ClearFlag(DMA1_FLAG_TC1);}重要细节写传输计数器的三步关键顺序不能颠倒——失能 → 写计数器 → 使能。如果直接写计数器而不先失能属于对寄存器非法操作。main.c 调用逻辑#includestm32f10x.h#includeOLED.h#includeMyDMA.huint8_tDataA[]{0x01,0x02,0x03,0x04};// 源数组uint8_tDataB[]{0x00,0x00,0x00,0x00};// 目标数组初始全是0intmain(void){OLED_Init();// 初始化 DMA配置从 DataA 搬运 4 个字节到 DataBMyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);OLED_ShowString(1,1,DataA:);OLED_ShowString(3,1,DataB:);while(1){// 改变源数据模拟数据更新DataA[0];DataA[1];DataA[2];DataA[3];// 显示当前 DataAOLED_ShowHexNum(1,8,DataA[0],2);OLED_ShowHexNum(1,11,DataA[1],2);OLED_ShowHexNum(1,14,DataA[2],2);OLED_ShowHexNum(1,17,DataA[3],2);// 显示当前 DataB转运前应该是旧值或0OLED_ShowHexNum(3,8,DataB[0],2);OLED_ShowHexNum(3,11,DataB[1],2);OLED_ShowHexNum(3,14,DataB[2],2);OLED_ShowHexNum(3,17,DataB[3],2);Delay_ms(1000);// ★ 调用一次 DMA 转运MyDMA_Transfer();// 转运后再显示 DataB此时 DataB 已等于 DataAOLED_ShowHexNum(3,8,DataB[0],2);OLED_ShowHexNum(3,11,DataB[1],2);OLED_ShowHexNum(3,14,DataB[2],2);OLED_ShowHexNum(3,17,DataB[3],2);Delay_ms(1000);}}运行现象OLED 屏幕上 DataA 每次循环自增1调用 Transfer 后 DataB 立即同步为 DataA 的值。二、DMA AD多通道8-2任务描述ADC 扫描模式采集 4 个通道PA0~PA3DMA硬件触发将 ADC1 数据寄存器DR的值自动搬运到 SRAM 数组AD_Value[]中全程无需 CPU 干预。文件清单Hardware/AD.c— ADC 驱动内含 ADC DMA 联合初始化Hardware/AD.h— 头文件声明AD_Init()和AD_Value[]外部变量User/main.c— 主程序实时读取四个通道的 ADC 值并显示内存→内存 与 外设→内存 参数对比参数8-1 数据转运M→M8-2 ADCDMAP→M外设地址DataA 数组地址(uint32_t)ADC1-DRADC数据寄存器外设地址自增EnableDisable始终读取同一个 DR 寄存器存储器地址DataB 数组地址AD_Value数组地址存储器地址自增EnableEnable数据宽度Byte8位HalfWord16位ADC是12位精度传输方向PeripheralSRCPeripheralSRC传输次数44与扫描通道数一致模式Normal单次Circular循环配合连续转换触发方式M2M_Enable软件触发M2M_Disable硬件触发ADC_EOC通道选择DMA1_Channel1任意DMA1_Channel1ADC1 固定映射为什么 DMA 必须选择 Channel1STM32F103 中DMA 每个通道与特定外设的硬件触发信号固定绑定DMA1 通道绑定的外设Channel1ADC1、TIM2_CH3、TIM4_CH1Channel2SPI1_RX、USART1_TX、TIM2_UPChannel3SPI1_TX、USART1_RX、TIM3_CH3Channel4USART1_TX、I2C1_RX、TIM4_CH2Channel5USART2_RX、I2C1_TX、TIM2_CH1Channel6USART2_TX、TIM1_CH1、TIM3_CH1Channel7SPI2_RX、USART3_RX、TIM2_CH2AD_Init()含 DMA代码逐段解析// 全局数组存放4个通道的AD转换结果由DMA自动填充uint16_tAD_Value[4];voidAD_Init(void){// 第一步开启所有用到的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);// ADC1 在 APB2RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// GPIOA 在 APB2RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);// DMA1 在 AHB ★// 第二步设置 ADC 时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6);// 72MHz / 6 12MHz上限14MHz// 第三步GPIO 初始化PA0~PA3 为模拟输入 GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_ModeGPIO_Mode_AIN;GPIO_InitStructure.GPIO_PinGPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,GPIO_InitStructure);// 第四步配置规则组通道序列 // 扫描模式下规则组有4个序列分别对应0~3号通道ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);// 通道0 → 序列第1个转换ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);// 通道1 → 序列第2个转换ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);// 通道2 → 序列第3个转换ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);// 通道3 → 序列第4个转换// 采样时间55.5周期 ≈ 4.6μs 12MHz// 第五步ADC 工作模式初始化 ADC_InitTypeDef ADC_InitStructure;ADC_InitStructure.ADC_ModeADC_Mode_Independent;// 独立模式不与其他ADC同步ADC_InitStructure.ADC_DataAlignADC_DataAlign_Right;// 右对齐12位数据存放在 [0:11] 位ADC_InitStructure.ADC_ExternalTrigConvADC_ExternalTrigConv_None;// 软件触发ADC_SoftwareStartConvCmd 来启动ADC_InitStructure.ADC_ContinuousConvModeENABLE;// ★ 连续转换模式一次触发后不断循环扫描ADC_InitStructure.ADC_ScanConvModeENABLE;// ★ 扫描模式按序列依次转换多个通道ADC_InitStructure.ADC_NbrOfChannel4;// 扫描通道数量4个ADC_Init(ADC1,ADC_InitStructure);// 第六步DMA 初始化硬件触发 循环模式 DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr(uint32_t)ADC1-DR;// ★ 外设地址 ADC1 数据寄存器地址所有通道共用这同一个寄存器DMA_InitStructure.DMA_PeripheralDataSizeDMA_PeripheralDataSize_HalfWord;// ★ 16位宽度ADC 精度12位需半字DMA_InitStructure.DMA_PeripheralIncDMA_PeripheralInc_Disable;// ★ 外设地址不自增始终从 DR 寄存器读取DMA_InitStructure.DMA_MemoryBaseAddr(uint32_t)AD_Value;// 存储器地址 AD_Value 数组首地址DMA_InitStructure.DMA_MemoryDataSizeDMA_MemoryDataSize_HalfWord;// 存储器端也是16位DMA_InitStructure.DMA_MemoryIncDMA_MemoryInc_Enable;// ★ 存储器地址自增依次填充 AD_Value[0], AD_Value[1]…DMA_InitStructure.DMA_DIRDMA_DIR_PeripheralSRC;// 方向外设ADC_DR→ 存储器AD_Value 数组DMA_InitStructure.DMA_BufferSize4;// ★ 传输计数器 4与通道数量一致DMA_InitStructure.DMA_ModeDMA_Mode_Circular;// ★ 循环模式计数器减到0后自动重装实现持续搬运DMA_InitStructure.DMA_M2MDMA_M2M_Disable;// ★ 硬件触发由 ADC 的 EOC 信号触发每次搬运DMA_InitStructure.DMA_PriorityDMA_Priority_Medium;DMA_Init(DMA1_Channel1,DMA_InitStructure);// 第七步使能顺序DMA → ADC_DMACmd → ADC_Cmd → 校准 → 触发DMA_Cmd(DMA1_Channel1,ENABLE);// ① DMA 先使能等待触发信号ADC_DMACmd(ADC1,ENABLE);// ② ★ 关键一步开启 ADC → DMA 的触发信号通路// 不写这一句ADC 转换完成后不会触发 DMAADC_Cmd(ADC1,ENABLE);// ③ 使能 ADC上电工作// 第八步ADC 校准 ADC_ResetCalibration(ADC1);while(ADC_GetResetCalibrationStatus(ADC1)SET);// 复位校准ADC_StartCalibration(ADC1);while(ADC_GetCalibrationStatus(ADC1)SET);// 执行校准// 第九步软件触发开始连续转换 ADC_SoftwareStartConvCmd(ADC1,ENABLE);// ★ 触发后 ADC 不断扫描4个通道 → 每次EOC触发DMA搬运一次// DMA将ADC_DR中的16位数据依次写入 AD_Value[0]→AD_Value[1]→…// 4次写满后 DMA计数器重装回到 AD_Value[0] 继续循环}使能顺序详解为什么这个顺序不能乱顺序操作原因①DMA_Cmd(DMA1_Channel1, ENABLE)DMA 必须先准备好才能响应 ADC 的硬件触发信号②ADC_DMACmd(ADC1, ENABLE)打开 ADC 内部到 DMA 的触发信号开关此线默认关闭③ADC_Cmd(ADC1, ENABLE)ADC 上电后需稳定一段时间才能开始转换④校准每次上电后必须执行消除内部电容误差⑤ADC_SoftwareStartConvCmd(ADC1, ENABLE)软件触发ADC 开始连续扫描转换核心要点如果 DMA 在 ADC 启动转换后才使能可能丢失前面的数据。ADC_DMACmd 是 ADC 内部的开关不调用则 ADC_EOC 信号不会输出到 DMA 通道。main.c 调用逻辑#includestm32f10x.h#includeOLED.h#includeAD.hexternuint16_tAD_Value[4];// 声明外部数组AD.c 中定义intmain(void){OLED_Init();AD_Init();// 初始化 ADC DMA启动连续扫描转换OLED_ShowString(1,1,CH0:);OLED_ShowString(2,1,CH1:);OLED_ShowString(3,1,CH2:);OLED_ShowString(4,1,CH3:);while(1){// ★ 无需手动触发任何操作// DMA 在后台持续将 ADC_DR 数据搬运到 AD_Value[] 数组中// 只需直接读取数组即可获得最新 AD 值OLED_ShowNum(1,5,AD_Value[0],4);// PA0 通道值0~4095OLED_ShowNum(2,5,AD_Value[1],4);// PA1 通道值OLED_ShowNum(3,5,AD_Value[2],4);// PA2 通道值OLED_ShowNum(4,5,AD_Value[3],4);// PA3 通道值Delay_ms(100);// 100ms 刷新一次即可}}运行现象OLED 屏幕上四个通道的 ADC 值实时更新。滑动 PA0~PA3 上的电位器对应通道值同步变化且 CPU 完全不参与数据搬运。三、DMA核心知识点补充1. DMA 转运的三个必要条件DMA 开始传输以下三个条件必须同时满足条件说明① 传输计数器 0DMA_BufferSize 0否则无数据传输②DMA_Cmd(ENABLE)通道必须使能③ 触发信号到来软件触发M2M1→ 使能后立即连续触发硬件触发M2M0→ 由外设事件触发2. Normal正常模式 vs Circular循环模式模式行为适用场景Normal计数器减到 0 后停止需手动重写计数器再使能单次搬运如 8-1 数据转运Circular计数器减到 0 后自动重装持续循环持续采集如 8-2 ADCDMA3. 软件触发M2M1 vs 硬件触发M2M0触发方式M2M适用方向触发行为软件触发Enable存储器→存储器使能后DMA全速连续搬运直到计数器清零硬件触发Disable外设→存储器 / 存储器→外设每次外设事件触发一次搬运计数器减1注意软件触发M2M1不能与循环模式Circular同时使用如果同时开启DMA 会无限循环搬运无法停止。4. 数据宽度与对齐规则DMA 搬运时源端宽度和目标端宽度可以不一致情况行为示例源宽 目标宽原样搬运16位→16位不变源宽 目标宽取源数据的低字节16位→8位取低8位高8位丢弃源宽 目标宽目标高位补08位→16位高8位补05. DMA 命名注意点——“外设和存储器”在存储器→存储器的转运中DMA_PeripheralBaseAddr只是一个站点A不一定是真正的外设DMA_MemoryBaseAddr只是一个站点B搬运方向由DMA_DIR决定PeripheralSRC 表示从 A 到 B当 M2M1 时两个站点都在存储器中名称只是约定的区分手段四、文件结构工程目录 ├── User/ │ └── main.c # 主程序 ├── Hardware/ │ ├── MyDMA.c # 8-1DMA 数据转运驱动 │ ├── MyDMA.h │ ├── AD.c # 8-2ADC DMA 联合驱动 │ ├── AD.h │ ├── OLED.c / OLED.h # OLED 显示驱动 │ └── Delay.c / Delay.h # 延时函数 ├── Libraries/ │ ├── stm32f10x_adc.h # ADC 标准外设库 │ ├── stm32f10x_dma.h # DMA 标准外设库 │ └── ... └── System/ └── Delay.c第八章的核心是理解 DMA 自动搬运数据的能力将 CPU 从逐字节拷贝数据的机械劳动中解放出来。8-1 展示了 DMA 的基本工作流程初始化→使能→搬运→等待完成8-2 则展示了 DMA 最经典的应用场景——配合 ADC 实现多通道数据自动采集实现真正的CPU 零干预。