STM32-ADC+DMA

本内容基于江协科技STM32视频学习之后整理而得。

文章目录

  • [1. ADC模拟-数字转换器](#1. ADC模拟-数字转换器)
    • [1.1 ADC模拟-数字转换器](#1.1 ADC模拟-数字转换器)
    • [1.2 逐次逼近型ADC](#1.2 逐次逼近型ADC)
    • [1.3 ADC框图](#1.3 ADC框图)
    • [1.4 ADC基本结构](#1.4 ADC基本结构)
    • [1.5 输入通道](#1.5 输入通道)
    • [1.6 规则组的转换模式](#1.6 规则组的转换模式)
      • [1.6.1 单次转换,非扫描模式](#1.6.1 单次转换,非扫描模式)
      • [1.6.2 连续转换,非扫描模式](#1.6.2 连续转换,非扫描模式)
      • [1.6.3 单次转换,扫描模式](#1.6.3 单次转换,扫描模式)
      • [1.6.4 连续转换,扫描模式](#1.6.4 连续转换,扫描模式)
    • [1.7 触发控制](#1.7 触发控制)
    • [1.8 数据对齐](#1.8 数据对齐)
    • [1.9 转换时间](#1.9 转换时间)
    • [1.10 校准](#1.10 校准)
    • [1.11 硬件电路](#1.11 硬件电路)
  • [2. AD库函数及代码](#2. AD库函数及代码)
    • [2.1 AD库函数](#2.1 AD库函数)
    • [2.2 7-1AD单通道代码](#2.2 7-1AD单通道代码)
      • [2.2.1 硬件电路](#2.2.1 硬件电路)
      • [2.2.2 代码流程](#2.2.2 代码流程)
      • [2.2.3 代码](#2.2.3 代码)
    • [2.3 7-2AD多通道代码](#2.3 7-2AD多通道代码)
      • [2.3.1 硬件电路](#2.3.1 硬件电路)
      • [2.3.2 硬件运行结果](#2.3.2 硬件运行结果)
      • [2.3.3 代码流程](#2.3.3 代码流程)
      • [2.3.4 代码](#2.3.4 代码)
  • [3. DMA直接存储器存取](#3. DMA直接存储器存取)
    • [3.1 DMA](#3.1 DMA)
    • [3.2 存储器映像](#3.2 存储器映像)
    • [3.3 DMA框图](#3.3 DMA框图)
    • [3.4 DMA基本结构](#3.4 DMA基本结构)
    • [3.5 DMA请求(触发)](#3.5 DMA请求(触发))
    • [3.6 数据宽度与对齐](#3.6 数据宽度与对齐)
    • [3.7 数据转运+DMA](#3.7 数据转运+DMA)
    • [3.8 ADC扫描模式+DMA](#3.8 ADC扫描模式+DMA)
  • [4. DMA库函数及代码](#4. DMA库函数及代码)
    • [4.1 DMA库函数](#4.1 DMA库函数)
    • [4.2 8-1DMA数据转运](#4.2 8-1DMA数据转运)
      • [4.2.1 硬件电路](#4.2.1 硬件电路)
      • [4.2.2 代码流程](#4.2.2 代码流程)
      • [4.2.3 代码](#4.2.3 代码)
    • [4.3 8-2DMA+AD多通道](#4.3 8-2DMA+AD多通道)
      • [4.3.1 硬件电路](#4.3.1 硬件电路)
      • [4.3.2 代码流程](#4.3.2 代码流程)
      • [4.3.3 代码](#4.3.3 代码)

1. ADC模拟-数字转换器

1.1 ADC模拟-数字转换器

  • ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁;(DAC数字模拟转换器,PWM是数字到模拟的转换,使用PWM来控制LED的亮度、电机的速度,这就是DAC的功能,同时PWM只有完全导通和完全断开两种状态,在这两种状态上都没有功率损耗,所以在直流电机调速这种大功率的应用场景中,使用PWM来等效模拟量,是比DAC更好的选择,并且PWM电路更加简单,更加常用,所以可以看出PWM还是挤占了DAC的很多应用空间,目前DAC的应用主要是在波形生成这些领域,比如信号发生器、音频解码芯片。)
  • 12位逐次逼近型ADC,1us转换时间。(12位表示分辨率,范围0-2^12-1=0~4095,位数越高,量化结果就越精细,对应分辨率就越高。转换时间即转换频率,转换需要时间,1us表示AD从转换开始到产生结果,需要花1us的时间,对应的AD转换频率就是1MHz)
  • 输入电压范围:0-3.3V,转换结果范围:0~4095
  • 18个输入通道,可测量16个外部和2个内部信号源
  • 规则组和注入组两个转换单元
  • 模拟看门狗自动监测输入电压范围(ADC一般可以用于测量光线强度、温度这些值,如果光线高于某个阈值,低于某个阈值,或者温度高于某个阈值,低于某个阈值时,执行一些操作,低于某个阈值、高于某个阈值的判断就可以用模拟看门狗来自动执行,模拟看门狗可以监测指定的某些通道,当AD值高于它设定的上阈值或低于下阈值时,它就会申请中断,就可以在中断函数里执行相应的操作,这样就不用不断地手动读值,再用if进行判断了。)
  • STM32103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道(就是最多只能测量10个外部引脚的模拟信号)

1.2 逐次逼近型ADC

  • ADC0809:独立8位逐次逼近型ADC芯片
  • IN0~IN7:8路输入通道,通过通道选择开关,选中一路,输入到比较器进行转换。
  • 地址锁存和译码:想选中哪个通道,就把通道号放在ADDA~ADDC上,然后给一个锁存信号,对应的通路开关就可以自动拨好。通路选择开关相当于一个可以通过模拟信号的数据选择器。
  • 比较器:电压比较器,可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小。其输入:一个是通道选择开关输出的待测电压,另一个是DAC的电压输出端。
  • DAC是数模转换器,给DAC一个数据,就能输出对应的电压值;内部是使用加权电阻网络实现的转换。
  • 现在有了一个外部通道输入的未知编码的电压,和一个DAC输出的已知编码的电压,它俩同时输入到电压比较器,进行大小判断。如果DAC输出的电压比较大,就调小DAC数据;反之,输出电压比较小,就增大DAC数据。直到DAC输出的电压和外部通道输入的电压近似相等。这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理,该电压调节过程是逐次逼近寄存器SAR完成,为了最快找到未知电压的编码,采用二分法,0~255,每次对半分,128、64、32这些数据,正好是二进制每一位的位权。该判断过程,相当于对二进制从高位到低位依次判断是1还是0的过程。对于8位的ADC,从高位到低位依次判断8次就能找到未知电压的编码。AD转换结束后,DAC的输入数据,就是未知电压的编码,通过8位三态锁存缓冲器输出。
  • EOC是End of Convert,转换结束信号;
  • START是开始转换,给一个输入脉冲,开始转换;
  • CLOCK是ADC时钟,因ADC内部是一步一步进行判断的,因此需要时钟来推动这个过程。
  • VREF+和VREF-是DAC的参考电压,该参考电压也决定了ADC的输入范围,所以也是ADC参考电压。

1.3 ADC框图

  • 对于普通的ADC,多路开关一般都是只选中一个的,就是选中某一个通道、开始转换、等待转换完成、取出结果。
  • 但是在这里可以选中多个,而且在转换的时候,还分成了两个组,规则通道组和注入通道组,其中规则组可以一次性最多选中16个通道,注入组最多可以选中4个通道,但是规则组只有一个数据寄存器,而注入组有4个数据寄存器,用规则组需要使用DMA配合转运数据。
  • 规则组和注入组的触发源主要来自定时器,有定时器的各个通道,还有TRGO定时器主模式的输出,可以选择TIM3定一个1ms的时间,并且把TIM3的更新事件选择为TRGO输出,然后在ADC里,选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换了。也可以选择外部中断引脚来触发转换。
  • VREF+和VREF-是ADC的参考电压,VDDA和VSSA是ADC的供电引脚,一般VREF+要接VDDA,VREF-要接VSSA,
  • ADCCLK是ADC的时钟,用于驱动内部逐次比较的时钟,是来自ADC预分频器,这个ADC预分频器是来源于RCC的。

1.4 ADC基本结构

  • 规则组最多可以选中16个通道;注入组最多可以选择4个通道;
  • 转换结果存放在AD数据寄存器里,规则组只有1个数据寄存器,注入组有4个;
  • 触发控制,提供了开始转换START信号;可以选择软件触发和硬件触发。
  • 硬件触发主要来自于定时器,也可以选择外部中断的引脚。
    来自于RCC的ADC时钟CLOCK,ADC逐次比较的过程就是由这个时钟推动的;
  • 可以布置一个模拟看门狗用于检测转换结果的范围,若超出设定的阈值,就通过中断输出控制,向NVIC申请中断;
  • 规则组和注入组转换完成后会有个EOC信号,会置一个标志位,也可以通向NVIC。
  • 开关控制:在库函数中,就是ADC_Cmd函数,用于给ADC上电的。

1.5 输入通道

1.6 规则组的转换模式

在ADC初始化的结构体里,会有两个参数:参1是选择单次转换还是连续转换,参2是选择扫描模式还是非扫描模式。

1.6.1 单次转换,非扫描模式

  • 在非扫描模式下,该菜单只有第一个序列1的位置有效。菜单同时选中一组的方式就退化为简单地选中一个的方式。
  • 在序列1可以指定要转换的通道,之后就可以触发转换,ADC就会对这个通道2进行模数转换。
  • 转换完成后,结果存放在数据寄存器里,同时给EOC标志位置1,转换结束。
  • 判断转换结束后,就可以在数据寄存器里读取结果。若想再启动一次转换,就需要再触发一次,转换结束,置EOC标志位,读结果。
  • 若想换一个通道转换,则在转换之前,把第一个位置的通道2改为其他通道,然后再启动转换。
  • 流程:触发转换-->判断转换结束(置EOC标志位)-->获取转换值

1.6.2 连续转换,非扫描模式

它在一次转换结束后,不会停止,而是立刻开始下一轮的转换,然后一直持续下去。因此只需最开始触发一次,之后就可以一直转换。优点是开始转换之后不需要等待一段时间,想要读AD值的时候,直接从数据寄存器取就是了。

1.6.3 单次转换,扫描模式

每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。初始化结构体中还会有个参数:通道数目,若为7,就是在每次触发之后,依次对前7个位置进行AD转换,转换结果都放在数据寄存器里。为了防止数据被覆盖,就需要用DMA及时将数据挪走。7个通道转换完成之后,产生EOC信号,转换结束。然后再触发下一次,就又开始新一轮的转换。

1.6.4 连续转换,扫描模式

一次转换完成后,立刻开始下一次的转换。

在扫描模式的情况下,还有一种模式:间断模式,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。

1.7 触发控制

类型:外部引脚/来自片上定时器的内部信号,具体是引脚还是定时器,需要用AFIO重映射来确定。

软件控制位:软件触发

触发信号的选择可以通过设置右边的寄存器来完成。也可以使用库函数实现。

1.8 数据对齐

ADC是12位的,其转换结果就是一个12位的数据。但数据寄存器是16位的,所以存在数据对齐的问题

1.9 转换时间

  • 量化编码即逐次比较
  • 采样保持:是因为量化编码是需要一小段时间的,如果在这一小段时间里,输入的电压不断变化,则无法定位输入电压的位置,所以在量化编码之前,需要设置一个采样开关,先打开采样开关,收集一下外部的电压,之后断开采样开关,再进行AD转换,这样在量化编码的期间,电压始终保持不变,这样才能精确地定位未知电压的位置。
  • 采样时间就是采样保持的时间,采样时间越大,越能避免一些毛刺信号的干扰。但转换时间也会相应延长。
  • 12.5个ADC周期是量化编码花费的时间,因为是12位的ADC,所以需要花费12个周期,0.5个周期是做其他用的。ADC周期就是从RCC分频过来的ADCCLK,ADCCLK最大是14MHz。

1.10 校准

  • ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
  • 建议在每次上电后执行一次校准
  • 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期

1.11 硬件电路

  • 第一个:电位器可调电压的电路,就是接一个电位器,当滑动端往上滑时,电压增大,往下滑时,电压减小。
  • 第二个:传感器输出电压的电路,如光敏电阻、热敏电阻、红外接收管、麦克风等都可以等效为一个可变电阻。传感器阻值变小时,下拉作用变强,输出端电压就下降;传感器阻值变大时,下拉作用变弱,输出端受上拉电阻的作用,电压就会升高。
  • 第三个:电压转换电路,若想测一个0-5V的VIN电压,但ADC只能接收0-3.3V的电压,就可以搭一个转换电路,上面阻值17K,下面阻值33K,总共50K,根据分压公式,中间的电压就是VIN/50K*33K,得到的电压范围就是0~3.3V,就可以进入ADC转换了。

2. AD库函数及代码

2.1 AD库函数

c 复制代码
// 配置ADCCLK分频器
void RCC_ADCCLKConfig(uint32_t RCC_PCLK2);

// DeInit恢复缺省配置、Init初始化、StructInit结构体初始化
void ADC_DeInit(ADC_TypeDef* ADCx);
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);

// 给ADC上电的,即开关控制
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// 开启DMA输出信号
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// 中断输出控制,用于控制某个中断,能不能通往NVIC
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);

// 复位校准、获取复位校准状态
void ADC_ResetCalibration(ADC_TypeDef* ADCx);
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);

// 开始校准、获取开始校准状态
void ADC_StartCalibration(ADC_TypeDef* ADCx);
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);

// ADC软件开始转换控制,用于软件触发的函数
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC获取软件开始转换状态
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);

// 判断转换是否结束。获取标志位状态,参数给EOC的标志位,判断EOC标志位是不是置1了
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);

// 配置间断模式,函数1:每隔几个通道间断一次;函数2:是不是启用间断模式
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC规则组通道配置,给序列的每个位置填写指定的通道
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);

// ADC 外部触发转换控制,就是是否允许外部触发转换
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

// ADC换取转换值。就是获取AD转换的数据寄存器,读取转换结果使用该函数
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

// ADC获取双模式转换值,是ADC模式读取转换结果的函数
uint32_t ADC_GetDualModeConversionValue(void);

// 对ADC注入组进行配置
void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv);
void ADC_ExternalTrigInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
void ADC_SoftwareStartInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
FlagStatus ADC_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef* ADCx);
void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);
void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length);
void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset);
uint16_t ADC_GetInjectedConversionValue(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel);

// 对模拟看门狗进行配置,函数1:是否启动看门狗;函数2:配置高低阈值;函数3:配置看门的通道
void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog);
void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold);
void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel);

// ADC温度传感器、内部参考电压控制,用于开启内部的两个通道
void ADC_TempSensorVrefintCmd(FunctionalState NewState);

// 获取中断状态、清除中断挂起位
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
ITStatus ADC_GetITStatus(ADC_TypeDef* ADCx, uint16_t ADC_IT);
void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);

2.2 7-1AD单通道代码

2.2.1 硬件电路

实现功能:接一个电位器,即滑动变阻器,用该电位器产生一个0~3.3V连续变化的模拟电压信号,接到STM32的PA0口上,之后用STM32内部的ADC读取电压数据,显示在屏幕上。屏幕上第一行显示的是AD转换后的原始数据,第二行是经过处理后实际的电压值。往左拧,AD值减小,电压值也减小,AD值最小为0,对应的电压就是0V,往右拧,AD值变大,对应电压值也变大。STM32的ADC是12位的,所以AD结果最大值是4095(2^12 - 1),对应的电压是3.3V。

2.2.2 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

2.2.3 代码

AD.c代码:

c 复制代码
#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频=12MHz
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 采样时间是55.5个ADCCLK的周期
	// 在规则组序列1的位置写入通道0,
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	
	ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 触发源:软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;// 单次转换
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;   // 非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
}

uint16_t AD_GetValue(void)
{
	// 软件触发
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	// 判断转换结束
	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); // 55.5 + 12.5 = 68个周期,ADCCLK = 12MHz,1/12M*68 = 5.6us,等待5.6us
	// ADC获取转换值
	return ADC_GetConversionValue(ADC1);
}

main.c代码:

c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;
float Voltage;

int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Voltage:0.00V");
	
	while(1)
	{
		ADValue = AD_GetValue();
		Voltage = (float)ADValue / 4095 * 3.3;
		OLED_ShowNum(1, 9, ADValue, 4);
		OLED_ShowNum(2, 9, Voltage, 1);// 显示整数部分
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);
		
		Delay_ms(100);
	}
}

2.3 7-2AD多通道代码

2.3.1 硬件电路

接了三个传感器:光敏电阻、热敏电阻、反射式红外传感器,把它们的AO(模拟电压输出端)分别接在了A1、A2、A3引脚,加上原来的电位器,总共四个输出通道,然后测出来的4个AD数据分别显示在屏幕上,AD0:电位器,往左拧,减小,往右拧增大;AD1:光敏电阻,遮挡时电阻变大,下拉作用变弱,输出电压变大,AD值增大;AD2:热敏电阻,用手热一下,温度升高,阻值变小,输出电压变小,AD值减小;AD3:反射式红外传感器,手靠近,有反光,AD值减小。

2.3.2 硬件运行结果

2.3.3 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

2.3.4 代码

  1. AD.c代码:
c 复制代码
#include "stm32f10x.h"                  // Device header

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 触发源:软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;// 单次转换
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;   // 非扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 1;  // 1个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
}

uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	/*
	调用该函数时,只要先指定通道,
	返回值就是我们指定通道的结果.
	通道为 0、1、2、3
	*/
	
	// 采样时间是55.5个ADCCLK的周期
	// 通道为参数指定的通道
	ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
	// 软件触发
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
	// 判断转换结束
	while (ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC) == RESET); // 等待5.6us
	// ADC换取转换值
	return ADC_GetConversionValue(ADC1);
}
  1. main.c
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0, AD1, AD2, AD3;
float Voltage;

int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	
	while(1)
	{
		AD0 = AD_GetValue(ADC_Channel_0);
		AD1 = AD_GetValue(ADC_Channel_1);
		AD2 = AD_GetValue(ADC_Channel_2);
		AD3 = AD_GetValue(ADC_Channel_3);
		
		
		OLED_ShowNum(1, 5, AD0, 4);
		OLED_ShowNum(2, 5, AD1, 4);
		OLED_ShowNum(3, 5, AD2, 4);
		OLED_ShowNum(4, 5, AD3, 4);
		
		Delay_ms(100);
	}
}

3. DMA直接存储器存取

3.1 DMA

  • DMA可以提取外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。(外设指外设的寄存器,一般是外设的数据寄存器DR,如ADC的数据寄存器、串口的数据寄存器;存储器是指运行内存SRAM和程序存储器Flash,是存储变量数组和程序代码的地方)
  • 12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)
  • 每个通道都支持软件触发和特定的硬件触发(如果DMA进行的是存储器到存储器的数据转运,比如想把Flash里的一批数据转运到SRAM里去,就需要软件触发。使用软件触发之后,DMA就会把这批数据以最快的速度全部转运完成。如果DMA进行的是外设到存储器的数据转运,由于外设的数据转运需要一定的时机,就需要硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次。所以存储器到存储器的转运一般用软件触发,外设到存储器的转运一般用硬件触发。采用特定的硬件触发意思是每个DMA的通道的硬件触发源是不一样的,要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。)
  • STM32F103C8T6 DMA资源:DMA1(7个通道)

3.2 存储器映像

类型 起始地址 存储器 用途
ROM 0x0800 0000 程序存储器Flash 存储C语言编译后的程序代码和常量·
0x1FFF F000 系统存储器 存储BootLoader,用于串口下载
0x1FFF F800 选项字节 存储一些独立于程序代码的配置参数
RAM 0x2000 0000 运行内存SRAM 存储运行过程中的临时变量
0x4000 0000 外设寄存器 存储各个外设的配置参数
0xE000 0000 内核外设寄存器 存储内核各个外设的配置参数
  • 计算器的5大组成部分:运算器、控制器、存储器、输入设备和输出设备;
  • 运算器和控制器合在一起,称为CPU;
    因此,计算机的核心关键部分就是CPU和存储器
  • 存储器又包括:存储器的内容、存储器的地址
  • ROM是只读存储器,是一种非易失性、掉电不丢失的存储器。
  • RAM是随机存储器,是一种易失性、掉电丢失的存储器。
  • 程序存储器Flash:存储C语言编译后的程序代码,即下载程序的位置。
  • 运行程序,一般也是从主闪存里面开始运行的。
  • 系统存储器和选项字节:实际存储介质也是Flash。
  • 选项字节:存的主要是Flash的读保护、写保护。
  • 运行内存SRAM:存储运行过程中的临时变量,就是在程序中定义变量、数组、结构体的地方。

3.3 DMA框图

主要包括:CPU和存储器

  • Flash:主闪存,是ROM只读存储器的一种。如果通过总线直接访问的话,无论是CPU还是DMA都是只读的,只能读取数据,而不能写入。如果DMA的目的地址,填了Flash的区域,转运时就会出错。可以配置Flash接口控制器对Flash进行写入。
  • SRAM是运行内存,可以任意读写。
  • 数据寄存器可以正常读写。
  • 各种外设,都可以看成是寄存器,也是一种SRAM存储器。
  • 寄存器 :一种特殊的存储器。一方面,CPU可以对寄存器进行读写,就像读写运行内存一样。另一方面,寄存器的每一位背后,都连接了一根导线。这些导线可以用于控制外设电路的状态,如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当做计数器、数据寄存器。因此,寄存器是连接软件和硬件的桥梁。软件读写寄存器,就相当于在控制硬件的执行。
    因此,外设就是寄存器,寄存器就是存储器。因此,DMA转运就可以归结为一类问题,就是从某个地址取内容,再放到另一个地址去。
  • 总线矩阵的左端是主动单元,也就是拥有存储器的访问权;右端是被动单元,它们的存储器只能被左边的主动单元读写。主动单元里,内核有DCode和系统总线,可以访问右边的存储器。DCode总线是专门访问Flash的,系统总线是访问其他东西的。另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。
  • DMA1 有一条DMA总线,DMA2 有一条DMA总线;DMA1有7个通道,DMA2有5个通道。各个通道可以分别设置它们转运数据的源地址和目的地址,可以各自独立地工作。虽然多个通道可以独立转运数据,但DMA总线只有一条,所以所有的通道都只能分时复用一条DMA总线,如果产生了冲突,那就会由仲裁器 ,根据通道的优先级,来决定谁先用,谁后用。在总线矩阵这边也会有个仲裁器,如果DMA和CPU都要访问同一个目标,则DMA就会暂停CPU的访问,以防止冲突。但总线仲裁器,仍会保证CPU得到一半的总线带宽,使CPU也能正常的工作。
  • AHB从设备:即DMA自身的寄存器。DMA作为一个外设,其也会有相应的配置寄存器,连接在了总线右边的AHB总线上。因此,DMA即是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。
  • DMA请求:即是触发的意思。其右边的触发源是各个外设。所以DMA请求就是DMA的硬件触发源,如ADC转换完成、串口接收到数据。需要触发DMA转运数据的时候,通过DMA请求线路向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作。
  • DMA的工作:用于访问各个存储器的DMA总线;内部的多个通道可以进行独立的数据转运;仲裁器用于调度各个通道,防止产生冲突;AHB从设备用于配置DMA参数;DMA请求用于硬件触发DMA的数据转运。

3.4 DMA基本结构

  • 左边的外设寄存器与右边的寄存器(Flash+SRAM)是数据转运的两大站点。
  • DMA的数据转运:向右的外设到存储器;向左的存储器到外设,可以通过方向参数控制。另一种转运方式:存储器到存储器,如Flash到SRAM或SRAM到SRAM。
  • 转运方法 :外设和存储器都包括三个参数,起始地址决定了数据从哪里来到哪里去;数据宽度指定一次转运要按多大的数据宽度来进行,可以选择字节Byte、半字HalfWord和字Word。字节Byte是8位,一次转运uint8_t;半字HalfWord是16位,一次转运uint16_t;字Word是32位,一次转运uint32_t。地址是否自增:指定一次转运完成后,下一次转运,是不是要把地址移动到下一个位置去。
    若要进行存储器到存储器的转运,那就需要把其中一个存储器的地址,放在外设的这个站点。
  • 传输计数器:用来指定总共需要转运几次的。是自减计数器。
  • 自动重装器:当传输计数器减到0之后,是否要自动恢复到最初的值。比如最初传输计数器给5,如果不使用自动重装器,那转运5次后,DMA就结束了。如果使用自动重装器,那转运5次后,计数器减到0后,就会立即重装到初始值5。
  • 触发 :就是决定DMA需要在什么时机进行转运。触发源有硬件触发和软件触发,具体选择由M2M(存储器到存储器)参数决定,M2M为1,选择软件触发,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。软件触发和自动重装器不能同时使用。软件触发是想把传输计数器清零,循环模式是清零后自动重装。软件触发适用于存储器到存储器的转运,因为存储器到存储器的转运是软件启动、不需要时机,并且想尽快完成的任务。
    M2M为0,就是硬件触发,硬件触发源可以选择ADC、串口、定时器,使用硬件触发的转运,一般都是与外设有关的转运。转运需要一定的时机,如ADC转换完成、串口收到数据、定时时间到等,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运。
  • 开关控制(EN位):DMA_Cmd函数。当给DMA使能后,DMA就准备就绪,可以进行转运了。,
  • DMA转运条件:(1)开关控制,DMA_Cmd必须使能;(2)传输计数器必须大于0;(3)触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次。当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA。再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,继续工作。
  • 注意:写传输计数器时,必须先关闭DMA,再进行。

3.5 DMA请求(触发)

默认优先级是通道号越小,优先级越高。也可以在程序中配置优先级。

3.6 数据宽度与对齐

  • 如果数据宽度都一样,就是正常的一个一个转运。当源端和目标都是8位时,转运第一步,在源端的0位置,读数据B0,在目标的0位置,写数据B0,就是把这个B0,从左边挪到右边。之后的步骤就是把B1从左边挪到右边,之后B2、B3。
  • 当源端是8位,目标是16位,其操作是,在源端读B0,在目标写00B0,之后读B1,写00B1。就是如果目标宽度比源端数据宽度大,那就在目标数据前面多出来的空位补0。
  • 当目标数据宽度,比源端数据宽度小时,像从16位转运到8位时,就是读B1B0,只写入B0,读B3B2,只写入B2,也就是把多出来的高位舍弃掉。
  • 总结:如果把小的数据转到大的里面去,高位就会补0;如果把大的数据转到小的里面去,高位就会舍弃掉;如果数据宽度一样,那就没事。

3.7 数据转运+DMA

  • 外设地址:DataA数组的首地址;存储器地址:DataB数组的首地址
  • 起始地址:DataA[0]和DataB[0]
  • 数据宽度:8位
  • 地址是否自增:是,两边地址都要自增。
  • 方向参数:外设站点 ->存储器站点
  • 传输计数器:7,不需要自动重装
  • 触发:软件触发

3.8 ADC扫描模式+DMA

  • 触发一次后,7个通道依次进行AD转换,转换结果都放到ADC_DR数据寄存器里面。需要做的是:在每次转换完成后进行一个DMA数据转运,并且目的地址进行自增。
  • DMA配置:(1)外设地址写入ADC_DR这个寄存器的地址,存储器的地址可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当作存储器的地址;(2)数据宽度:因为ADC_DR和SRAM数组都是uint16_t,因此数据宽度都是16位的半字传输;(3)地址是否自增:外设地址不自增,存储器地址自增;
  • 传输方向:外设站点 -> 存储器站点
  • 传输计数器:7个。计数器是否自动重装,可以看ADC的配置。若ADC是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止。若ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。
  • 触发选择:ADC_DR的值是在ADC单个通道转换完成后才会生效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发。
  • ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不好判断,某个通道转换完成的时机是什么时候,但应该会产生DMA请求,去触发DMA转运。

4. DMA库函数及代码

4.1 DMA库函数

c 复制代码
// ADC1_BASE是ADC1的基地址,基地址就是起始地址,即4001 2400
#define ADC1                ((ADC_TypeDef *) ADC1_BASE)
#define ADC1_BASE             (APB2PERIPH_BASE + 0x2400)
#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)
#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */

typedef struct
{
  __IO uint32_t SR;
  __IO uint32_t CR1;
  __IO uint32_t CR2;
  __IO uint32_t SMPR1;
  __IO uint32_t SMPR2;
  __IO uint32_t JOFR1;
  __IO uint32_t JOFR2;
  __IO uint32_t JOFR3;
  __IO uint32_t JOFR4;
  __IO uint32_t HTR;
  __IO uint32_t LTR;
  __IO uint32_t SQR1;
  __IO uint32_t SQR2;
  __IO uint32_t SQR3;
  __IO uint32_t JSQR;
  __IO uint32_t JDR1;
  __IO uint32_t JDR2;
  __IO uint32_t JDR3;
  __IO uint32_t JDR4;
  __IO uint32_t DR;
} ADC_TypeDef;

ADC1->DR 
// ADC1是结构体指针,指向的是ADC1外设的起始地址,
// 访问结构体成员,相当于是加一个地址偏移。
// 起始地址 + 偏移,就是指定的寄存器
c 复制代码
// 恢复缺省配置
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);
// 初始化
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
// 结构体初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);
// 使能
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
// 中断输出使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
// DMA设置当前数据寄存器,给传输计数器写数据的
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 
// DMA获取当前数据寄存器,返回传输计数器的值
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
// 获取标志位状态
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
// 清除标志位
void DMA_ClearFlag(uint32_t DMAy_FLAG);
// 获取中断状态
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);
// 清除中断挂起位
void DMA_ClearITPendingBit(uint32_t DMAy_IT);

4.2 8-1DMA数据转运

4.2.1 硬件电路

使用DMA进行存储器到存储器的数据转运,把一个数组(源数组)的数据转运到另一个数组(目的数组)中。采用软件触发。

在OLED中显示源数组DataA 、地址及数据,目的数组DataB、地址及数据。

4.2.2 代码流程

  1. RCC开启DMA的时钟(AHB时钟)
  2. 调用DMA_Init,初始化参数:外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源
  3. 开关控制,DMA_Cmd
  4. 如果是硬件触发,在对应的外设调用一下xxx_DMACmd,开启一下触发信号的输出
  5. 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,写中断函数
  6. 如果转运完成,传输计数器清0了,若再想给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能。

4.2.3 代码

  1. MyDMA.c代码:
c 复制代码
#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint32_t Size)
{
/*
1. RCC开启DMA的时钟
2. 调用DMA_Init,初始化参数:外设和存储器站点的
   起始地址、数据宽度、地址是否自增、方向、
   传输计数器、是否需要自动重装、选择触发源
3. 开关控制,DMA_Cmd
4. 如果是硬件触发,在对应的外设调用一下xxx_DMACmd,
   开启一下触发信号的输出
5. 如果需要DMA的中断,调用DMA_ITConfig,开启中断输出,
   再在NVIC里,配置相应的中断通道,写中断函数
6. 如果转运完成,传输计数器清0了,若再想给传输计数器赋值的话,
   就DMA失能、写传输计数器、DMA使能。
*/
	// DMA是AHB总线的设备,需要开启AHB时钟
	
	MyDMA_Size = Size;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; // 外设站点的起始地址 
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 外设站点的数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable ; // 外设站点的是否自增
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; // 存储器站点的起始地址 
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 存储器站点的数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器站点的是否自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向
	DMA_InitStructure.DMA_BufferSize = Size; // 缓存区大小,即传输计数器
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 传输模式,就是是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 选择是否是存储器到存储器,即选择软件触发还是硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	/*
	DMA转运有三个条件:
	1. 传输计数器大于0
	2. 触发源有触发信号
	3. DMA使能
	*/
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

// DMA传输函数,调用依次这个函数,就再次启动一次DMA转运
void MyDMA_Transfer(void)
{
	// 重新给传输计数器赋值,先使DMA失能
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	// 等待转运完成
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);
}
  1. main.c
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"


/*
uint8_t bb = 0x66; // 存储在SRAM中
const uint8_t aa = 0x66; 
存储在Flash中,const定义的变量是常量,值不能更改
Flash是只读不能写入,因此const与Flash对应
*/

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};// 源端数组
uint8_t DataB[] = {0, 0, 0, 0};// 目标数组



int main(void)
{
	OLED_Init();
	
	// 把DataA数组的数据转运到DataB里
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
	
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);// 显示DataA的地址
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
	
	OLED_ShowHexNum(2, 1, DataA[0], 2);
	OLED_ShowHexNum(2, 4, DataA[1], 2);
	OLED_ShowHexNum(2, 7, DataA[2], 2);
	OLED_ShowHexNum(2, 10, DataA[3], 2);
	OLED_ShowHexNum(4, 1, DataB[0], 2);
	OLED_ShowHexNum(4, 4, DataB[1], 2);
	OLED_ShowHexNum(4, 7, DataB[2], 2);
	OLED_ShowHexNum(4, 10, DataB[3], 2);
	

	
	while(1)
	{
		DataA[0] ++;
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		OLED_ShowHexNum(2, 1, DataA[0], 2);
	    OLED_ShowHexNum(2, 4, DataA[1], 2);
	    OLED_ShowHexNum(2, 7, DataA[2], 2);
	    OLED_ShowHexNum(2, 10, DataA[3], 2);
	    OLED_ShowHexNum(4, 1, DataB[0], 2);
	    OLED_ShowHexNum(4, 4, DataB[1], 2);
	    OLED_ShowHexNum(4, 7, DataB[2], 2);
	    OLED_ShowHexNum(4, 10, DataB[3], 2);
		
		Delay_ms(1000);
		
		MyDMA_Transfer();
		
		OLED_ShowHexNum(2, 1, DataA[0], 2);
	    OLED_ShowHexNum(2, 4, DataA[1], 2);
	    OLED_ShowHexNum(2, 7, DataA[2], 2);
	    OLED_ShowHexNum(2, 10, DataA[3], 2);
	    OLED_ShowHexNum(4, 1, DataB[0], 2);
	    OLED_ShowHexNum(4, 4, DataB[1], 2);
	    OLED_ShowHexNum(4, 7, DataB[2], 2);
	    OLED_ShowHexNum(4, 10, DataB[3], 2);
		
		Delay_ms(1000);
	}
}

4.3 8-2DMA+AD多通道

4.3.1 硬件电路

使用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运,AD转换的数据就会自动到定义的数组里,然后用OLED显示一下。

4.3.2 代码流程

  1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
  2. 配置GPIO,配置为模拟输入模式
  3. 配置多路开关,把左边的通道接入到右边的规则组列表里
  4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
  5. 开关控制,调用ADC_Cmd函数,开启ADC
  6. 想要软件触发转换,有函数可以触发

4.3.3 代码

  1. AD.c代码:
c 复制代码
#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	/*
	1. 开启RCC时钟,包括ADC和GPIO时钟,ADCCLK的分频器
	2. 配置GPIO,配置为模拟输入模式
	3. 配置多路开关,把左边的通道接入到右边的规则组列表里
	4. 配置ADC转换器,采用库函数结构体:包括AD转换器和AD数据寄存器
	5. 开关控制,调用ADC_Cmd函数,开启ADC
	6. 想要软件触发转换,有函数可以触发
	
	*/
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 6分频
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;// 模拟输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 4个通道
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
	
	ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐:右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 触发源:软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;// 单次转换,循环
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;   // 扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 4;  // 4个通道
	ADC_Init(ADC1,&ADC_InitStructure);
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 外设站点的起始地址 ADC
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设站点的数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable ; // 外设站点的是否自增,否
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // 存储器站点的起始地址 SRAM
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 存储器站点的数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器站点的是否自增,是
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向
	DMA_InitStructure.DMA_BufferSize = 4; // 缓存区大小,即传输计数器
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 传输模式,就是是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 选择是否是存储器到存储器,即选择硬件触发还是软件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);// 开启DMA触发信号
	ADC_Cmd(ADC1, ENABLE);
	
	// 复位校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
    // 开始校准、获取开始校准状态
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);
}

/*
void AD_GetValue(void)
{
	
	调用该函数,ADC开始转换,DMA也同步进行转运,
	AD转换结果,依次放在这上面的AD_Value数组里,
	
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,4);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);
}
*/
  1. main.c代码:
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"



int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	
	while(1)
	{
		
		// AD_GetValue();
		
		OLED_ShowNum(1, 5, AD_Value[0], 4);
		OLED_ShowNum(2, 5, AD_Value[1], 4);
		OLED_ShowNum(3, 5, AD_Value[2], 4);
		OLED_ShowNum(4, 5, AD_Value[3], 4);
		
		Delay_ms(100);
	}
}
相关推荐
yutian06061 小时前
Keil MDK下载程序后MCU自动重启设置
单片机·嵌入式硬件·keil
析木不会编程4 小时前
【小白51单片机专用教程】protues仿真独立按键控制LED
单片机·嵌入式硬件·51单片机
枯无穷肉8 小时前
stm32制作CAN适配器4--WinUsb的使用
stm32·单片机·嵌入式硬件
不过四级不改名6778 小时前
基于HAL库的stm32的can收发实验
stm32·单片机·嵌入式硬件
嵌入式科普9 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
嵌入式大圣9 小时前
单片机UDP数据透传
单片机·嵌入式硬件·udp
云山工作室9 小时前
基于单片机的视力保护及身姿矫正器设计(论文+源码)
stm32·单片机·嵌入式硬件·毕业设计·毕设
嵌入式-老费9 小时前
基于海思soc的智能产品开发(mcu读保护的设置)
单片机·嵌入式硬件
qq_3975623111 小时前
MPU6050 , 设置内部低通滤波器,对于输出数据的影响。(简单实验)
单片机
liyinuo201711 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范