江科大STM32 下

这篇具有很好参考价值的文章主要介绍了江科大STM32 下。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

ADC数模转换器

先来看一下本节课程序的现象,本节课共有两个程序,第一个程序是AD单通道,第二个是AD多通道,第一个AD单通道,我接了一个电位器,就是滑动变阻器,用这个电位器产生一个0~3.3V连续变化的模拟电压信号,然后接到STM32的PA0口上之后,用STM32内部的ADC读取电压数据显示在屏幕上,这里屏幕第一行显示的是AD转换后的原始数据,第二行是经过处理后实际的电压值,那我们拧一下这个电位器,往左拧AD值减小,电压值也减小,AD值最小是零,对应的电压就是0V,往右AD值变大,对应电压值也变大,STM32的ADC是12位的,所以AD结果最大值是4095,也就是2^12-1对应的电压是3.3V,这就是第一个程序的现象,那对于GPIO来说,它只能读取引脚的高低电平,而使用了ADC之后呢,我们就可以对这个高电平和低电平之间的任意电压进行量化,最终用一个变量来表示,读取这个变量就可以知道引角的具体电压到底是多少,所以ADC其实就是一个电压表哈,把引脚的电压值测出来,放在一个变量里,这就是ADC的作用。

江科大STM32 下,STM32,stm32

第二个代码是AD多通道,在这里我又接了三个传感器模块,分别是光敏电阻、热敏电阻和反射红外模块,把他们的AO、模拟电压输出端分别接在了A1、A2、A3引脚,加上刚才的电位器,总共四个输出通道,然后测出来的四个AD数据分别显示在屏幕上,我们来试一下第一个电位器,看第一行的AD0,往左拧减小,往右拧增大,和上一个程序现象一样,然后光敏电阻看第二行的AD1,遮挡一下光敏电阻,光线减小,AD值增大,光线增大,AD值减小,然后热敏电阻看第三行的AD2,用手热一下这个热敏电阻温度升高,AD值减小,移开温度降低,AD值增大,最后是反射红外传感器,手靠近有反光,AD值减小,移开没有反光,AD值增大,这就是第二个程序的现象。

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

ADC可以将引脚上连续变化的模拟电压,转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁,刚才通过程序现象我们也看到了,STM32主要是数字电路,数字电路只有高低电平,没有几伏电压的概念,所以如果想读取电压值,就需要借助ADC模数转换器来实现了,ADC读取引脚上的模拟电压转化为一个数据存放在寄存器里,我们再把这个数据读取到变量里来,就可以进行显示判断记录等等操作,ADC可以将模拟信号转换为数字信号,是模拟电路到数字电路的桥梁哈,反过来数字到模拟的桥梁,就是DAC数字模拟转换器,使用DAC就可以将数字变量转化为模拟电压,不过在上一节我们还学到了一个数字的模拟的桥梁,我们使用PWM来控制led的亮度,电机的速度,这就是DAC的功能
同时PWM只有完全导通和完全断开两种状态,在这两种状态上都没有功率损耗,所以在直流电机调速这种大功率的应用场景,使用PWM来等效模拟量是比DAC更好的选择,并且PWM电路更加简单
更加常用,所以可以看出PWM还是挤占了DAC的很多应用空间,目前DAC的应用主要是在波形生成这些领域,比如信号发生器,音频解码芯片等,这些领域PWM还是不好替代的哈,我们本节学习的是ADC,这个型号的STM32没有DAC的外设大家自行了解。

江科大STM32 下,STM32,stm32

逐次逼近型这是这个ADC的工作模式。然后12位和1us的转换时间,这里就涉及到ADC的两个关键参数了,第一个是分辨率,一般用多少位来表示,12位AD值,它的表示范围就是0-2^12-1,就是量化结果的范围是0~4095。位数越高,量化结果就越精细,对应分辨率就越高;第二个是转换时间,就是转换频率,AD转换是需要花一小段时间的,这里1us就表示从AD转换开始到产生结果,需要花1us的时间,对应AD转换的频率就是1MHz,这个就是STM32 ADC的最快转换频率。如果你需要转换一个频率非常高的信号,那就要考虑一下这个转换频率是不是够用,如果你的信号频率比较低,那这个最大1MHz的转换频率也完全够用了。

江科大STM32 下,STM32,stm32

输入电压范围0~ 3.3V ,转换结果范围0~4095,这个ADC的输入电压,一般要求都是要在芯片供电的负极和正极之间变化的啊,最低电压就是负极0伏,最高电压是正极3.3V,经过ADC转换之后,最小值就是零,最大值是4095,0V对应0,3.3V对应4095,中间都是一一对应的线性关系,这个计算起来就非常简单了,直接乘除一个系数就行了。

江科大STM32 下,STM32,stm32

外部信号源就是16个GPIO口,在引脚上直接接模拟信号就行了,不需要任何额外的电路,引脚就直接能测电压。2个内部信号源是内部温度传感器和内部参考电压。温度传感器可以测量CPU的温度,比如你电脑可以显示一个CPU温度,就可以用ADC读取这个温度传感器来测量;内部参考电压是一个1.2V左右的基淮电压,这个基准电压是不随外部供电电压变化而变化的,所以如果你芯片的供电不是标准的3.3V,那测量外部引脚的电压可能就不对,这时就可以读取这个基准电压进行校准,这样就能得到正确的电压值了。

江科大STM32 下,STM32,stm32

规则组和注入组两个转换单元,这个就是STM32 ADC的增强功能了。普通的AD转换流程是,启动一次转换、读一次值,然后再启动、再读值,这样的流程。但是STM32的ADC就比较高级,可以列一个组,一次性启动一个组,连续转换多个值。并且有两个组,一个是用于常规使用的规则组,一个是用于突发事件的注入组。

江科大STM32 下,STM32,stm32

模拟看门狗自动监测输入电压范围,这个ADC,一般可以用于测量光线强度、温度这些值,并且经常会有个需求,就是如果光线高于某个阈值、低于某个阈值或者温度高于某个阈值、低于某个阈值时,执行一些操作。这个高于某个阈值、低于某个阈值的判断,就可以用模拟看门狗来自动执行。模拟看门狗可以监测指定的某些通道,当AD值高于它设定的上阈值或者低于下阈值时,它就会申请中断,你就可以在中断函数里执行相应的操作,这样你就不用不断地手动读值,再用if进行判断了。

江科大STM32 下,STM32,stm32

他的ADC资源有ADC1、ADC2共有两个ADC外设,十个外部输入通道,也就是它最多只能测量十个外部引脚的模拟信号,我们之前这里说的16个外部信号源,这是这个系列,最多有16个外部信号源
但是我们这个芯片引脚比较少,有很多引脚没有引出来,所以就只有十个外部信号源,如果你想要更多的外部通道,可以选择引脚更多的型号,具体有多少通道呢,那还需要再参考一下数据手册。

接下来我们来了解一下这个逐次逼近型ADC到底是怎么测电压的,我们看一下这个图,这就是逐次逼近型ADC的内部结构。了解这个结构对你学习STM32的ADC有很大帮助,因为STM32的ADC原理和这个是一样的,但是STM32只画了一个框表示ADC,并没有描述内部结构,所以我们先介绍一下这个结构,这样再理解STM32的ADC就会简单一些了。

我们来看一下,这个图是ADC0809的内部结构图,它是一个独立的8位逐次逼近型ADC芯片。在以前单片性能不太好的时候,是通过外挂一个ADC芯片才能进行AD转换,这个ADC0809就是一款比较经典的ADC芯片。现在随着单片机的性能和集成度都有很大的提升,很多单片机内部就已经集成了ADC外设。这样就不用外挂芯片,引脚可以直接测电压。

江科大STM32 下,STM32,stm32

输入选择部分:

江科大STM32 下,STM32,stm32

首先左边这里IN0~IN7,是8路输入通道,通过通道选择开关,选中一路,输入到所标点进行转换。
下面这里是地址锁存和译码,就是你想选中哪个通道,就把通道号放在这三个脚(ADDA、ADDB、ADDC)上,然后给一个锁存信号(ALU),上面这里对应的通路开关就可以自动拨好了。这部分就相当于一个可以通过模拟信号的数据选择器。
因为ADC转换是一个很快的过程,你给个开始信号,过几个us就转换完成了。所以说如果你想转换多路信号,那不必设计多个AD转换器,只需要一个AD转换器,然后加一个多路选择开关,想转换哪一路,就先拨一下开关,选中对应通道,然后再开始转换就行了。这就是这个输入通道选择的部分,这个ADC0809只有8个输入通道,我们STM32内部的ADC是有18个输入通道的,所以对应输入电路,就是一个18路输入的多路开关

那然后输入信号选好了,到这里(所标红点)来,怎么才能知道这个电压对应的编码数据是多少呢?这就需要我们用逐次逼近的方法来——比较了

江科大STM32 下,STM32,stm32

首先左边这是一个电压比较器,它可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小。它的两个输入端,一个是待测的电压,另一个是这里DAC的电压输出端,DAC是数模转换器。我们之前说过了,给它一个数据,它就可以输出数据对应的电压,DAC内部是使用加权电阻网络来实现的转换,具体可以江科大51单片机教程里的AD/DA那一节。

那现在,我们有了一个外部通道输入的未知编码的电压,和一个DAC输出的已知编码的电压。它俩同时输入到电压比较器,进行大小判断,如果DAC输出的电压比较大,我就调小DAC数据;如果DAC输出的电压比较小,我就增大DAC数据,直到DAC输出的电压和外部通道输入的电压近似相等 ,这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理。这个电压调节的过程就是这个逐次逼近SAR来完成的。

为了最快找到未知电压的编码,通常我们会使用二分法进行寻找。比如这里是8位的ADC,那编码就是从0~255。第一次比较的时候,我们就给DAC输入255的一半,进行比较,那就是128,然后看看谁大谁小,如果DAC电压大了;第二次比较的时候,再就给128的一半,64,如果还大,第三次比较的时候就给32,如果这次DAC电压小了,那第四次就给32到64中间的值,然后继续,这样依次进行下去,就能最快地找到未知电压的编码。并且这个过程,如果你用二进制来表示的话,你会发现,128、64、32这些数据,正好是二进制每一位的位权,这个判断过程就相当于是,对二进制从高位到低位依次判断是1还是0的过程,这就是逐次逼近型名字的来源。**那对于8位的ADC,从高位到低位依次判断8次就能找到未知电压的编码了,对于12位的ADC,就需要依次判断12次,**这就是逐次逼近的过程。

那然后,AD转换结束后,DAC的输入数据,就是未知电压的编码,通过右边电路进行输出,8位就有8根线,12位就有12根线。

最后上面这里EOC是end of convert,转换结束信号,start是开始转换给一个输入脉冲,clock是ADC时钟,因为ADC内部是一步一步进行判断的,所以需要时钟来推动这个过程。

下面VREF+和VREF-是DAC的参考电压,比如你给一个数据255是对应5伏还是3.3伏呢,就由这个参考电压决定,这个DAC的参考电压,也决定了ADC的输入范围,所以它也是ADC参考电压,最后左边是整个芯片电路的供电,vcc和gnd,通常参考电压的正极和vcc是一样的,会接在一起,参考电压的负极和gnd也是一样的,也接在一起,所以一般情况下adc输入电压的范围就和adc的供电是一样的。

好,到这里,相信你对逐次逼近型ADC就已经了解差不多了,接下来,我们就来看看STM32的逐次逼近型ADC,看看STM32的ADC和这个相比,有什么更高级的变化,那我们看一下STM32的这个ADC框图。

STM(32逐次逼近型)ADC电路图详解
总图:

一般在手册里,每个外设的最前面都有一个整体的结构图,这个结构图还是非常重要的,需要多花点时间看看。

江科大STM32 下,STM32,stm32

核心的大概工作流程:
江科大STM32 下,STM32,stm32

在这里左边是ADC的输入通,包括16个GPIO口,IN0~IN15,和两个内部的通道,一个是内部温度传感器,另一个是VREFINT(V Reference Internal),内部参考电压,总共是18个输入通道,然后到达这里,这是一个模拟多路开关,可以指定我们想要选择的通道,右边是多路开关的输出,进入到模数转换器,这里模数转换器就是刚才讲过的逐次比较的过程,转换结果会直接放在这个数据寄存器里,我们读取寄存器就能知道ADC转换的结果了,然后在这里对于普通的ADC,多路开关一般都是只选中一个的,就是选中某个通道开始转换,等待转换完成取出结果,这是普通的流程,但是这里就比较高级了,它可以同时选中多个,而且在转换的时候还分成了两个组,规则通道组和注入通道组。

注入规则组和规则通道组:

江科大STM32 下,STM32,stm32

其中规则组可以一次性最多选16个通道,注入组最多可以选中4个通道。

比喻解释注入组和规则组:
这有什么作用呢?举个例子,这就像是你去餐厅点菜,普通的ADC是,你指定一个菜,老板给你做,然后做好了送给你;这里就是,你指定一个菜单,这个菜单最多可以填16个菜,然后你直接递个菜单给老板,老板就按照菜单的顺序依次做好,一次性给你端上菜,这样的话就可以大大提高效率。当然,你的菜单也可以只写一个菜,这样这个菜单就简化成了普通的模式了。
那对于这个菜单呢,也有两种,一种是规则组菜单,可以同时上16个菜,但是它有个尴尬的地方。就是这个规则组只有一个数据寄存器,就是这个桌子比较小,最多只能放一个菜,你如果上16个菜,那不好意思,前15个菜都会被挤掉些,你只能得到第16个菜。所以对于规则组转换来说,如果使用这个菜单的话,最好配合DMA来实现。DMA是一个数据转运小帮手,它可以在每上一个菜之后,把这个菜挪到其他地方去,防止被覆盖。这个DMA我们下一节就会讲,现在先大概了解一下,那现在我们就知道了,这个规则组虽然可以同时转换16个通道,但是数据寄存器只能存一个结果,如果不想之前的结果被覆盖,那在转换完成之后,就要尽快把结果拿走。
接着我们看一下注入组,这个组就比较高级了,它相当于是餐厅的VIP座位,在这个座位上,一次性最多可以点4个菜,并且这里数据寄存器有4个,是可以同时上4个菜的。对于注入组而言,就不用担心数据覆盖的问题了,这就是规则组和注入组的介绍。
一般情况下,我们使用规则组就完全足够了,如果要使用规则组的菜单,那就再配合DMA转运数据,这样就不用担心数据覆盖的问题了。所以接下来就只讲规则组的操作,注入组涉及的不多,大家可以看手册自行了解。

那我们接着继续看这个模数转换器外围的一些线路

江科大STM32 下,STM32,stm32

首先,左下角这里是触发转换的部分,也就是这里的START信号,开始转换。那对于STM32的ADC,触发ADC开始转换的信号有两种,一种是软件触发,就是你在程序中手动调用一条代码,就可以启动转换了;另一种是硬件触发,就是这里的这些触发源。上面这些是注入组的触发源,下面这些是规则组的触发源,这些触发源主要是来自于定时器,有定时器的各个通道,还有TRGO定时器主模式的输出,这个之前讲定时器的时候也介绍过。定时器可以通向ADC、 DAC这些外设,用于触发转换。那因为ADC经常需要过一个固定时间段转换一次。比如每隔1ms转换一次,正常的思路就是,用定时器,每隔1ms申请一次中断,在中断里手动开始一次转换,这样也是可以的。但是频繁进中断对我们的程序是有一定影响的,比如你有很多中断都需要频繁进入,那肯定会影响主程序的执行,并且不同中断之间,由于优先级的不同,也会导致某些中断不能及时得到响应。如果触发ADC的中断不能及时响应,那我们ADC的转换频率就肯定会产生影响了。所以对于这种需要频繁进中断,并且在中断里只完成了简单工作的情况,一般都会有硬件的支持。

江科大STM32 下,STM32,stm32

比如这里,就可以给TIM3定个1ms的时间,并且把TIM3的更新事件选择为TRGO输出,然后在ADC这里,选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换了。整个过程不需要进中断,节省了中断资源,这就是这里定时器触发的作用。当然这里还可以选择外部中断引脚来触发转换,都可以在程序中配置。这就是触发转化的部分。

江科大STM32 下,STM32,stm32

然后接着看,左上角这里是VREF+、VREF-、VDDA和VSSA。上面两个是ADC的参考电压,决定了ADC输入电压的范围;下面两个是ADC的供电引脚。一般情况下,VREF+要接VDDA,VREF-要接VSSA,在我们这个芯片上,没有VREF+和VREF-的引脚,它在内部就已经和VDDA和VSSA接在一起了。查看引脚定义,VDDA和VSSA是内部模拟部分的电源(前面讲过分区供电),比如ADC、RC振荡器、锁相环等。在这里VDDA接3.3V, VSSA接GND,所以ADC的输入电压范围就是0~3.3V。

江科大STM32 下,STM32,stm32

然后继续看右边这里是ADCCLK是ADC的时钟,也就是这里的CLOCK,是用于驱动内部逐次比较的时钟。这个ADCCLK是来自ADC预分频器,而ADC预分频器是来源于RCC的。我们找一下:
江科大STM32 下,STM32,stm32

APB2时钟72MHZ,然后通过ADC预分频器进行分频,得到ADCCLK,ADCCLK最大是14MHZ,所以这个预分频器就有点尴尬。它可以选择2、4、6、8分频,如果选择2分频,72M/2=36M,超出允许范围了;4分频之后是18M,也超了,所以对于ADC预分频器只能选择6分频,结果是12M,和8分频,结果是9M,这两个值。这个在程序里要注意一下

江科大STM32 下,STM32,stm32

继续看上面这里是DMA请求,这个就是用于触发DMA进行数据转运的,我们下节再讲。
江科大STM32 下,STM32,stm32

然后是两个数据寄存器,用于存放转换结果的,上面这里还有模拟开门狗,它里面可以存一个阈值高线和阈值低线,如果启动了模拟看门狗,并且指定了看门的通道,那个看门狗就会关注他开门的通道
一旦超过这个阈值范围了,它就会乱叫啊,就会在上面申请一个模拟看门狗的中段,最后通向NVIC,然后对于规则组和注入组而言呢,它们转换完成之后也会有一个EOC转换完成的信号,在这里EOC是规则组的完成信号,JEOC是注入组完成的信号,这两个信号会在状态寄存器里置一个标志位,我们读取这个标志位就能知道是不是转换结束了,同时这两个标志位也可以去到NVIC申请中断,如果开启了NVIC对应的通道,它们就会触发中断。

好,有关ADC的这个框图,我们就介绍完了。

ADC基本结构
那接下来就来看一下我这里总结的一个ADC基本结构图,再来回忆一下。

江科大STM32 下,STM32,stm32

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

接下来我们再了解一些细节的问题。
首先看一下输入通道,刚才我们说了,这里有16个外部通道,那这16个通道对应的都是哪些GPIO口呢,我们就可以看一下这个表,这些就是ADC通道和引脚复用的关系,这个对应关系也可以通过引脚定义表看出来。

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

这个对应关系也可以通过引脚定义表看出来,我们看一下,在这里可以看到ADC_IN0对应的是PA0引脚,IN1对应PA1引脚,然后IN2、IN3、IN4、IN5、IN6、IN7、IN8、IN9,依次对应的是PA2到PB1
,这里只有IN0~IN9,总共只有十个通道,然后其他地方就没有了,所以这个芯片就只能有十个外部输入通道,然后ADC_IN0的意思是,ADC1和ADC2 的IN0都是在PA0上的,然后下面全都是ADC12
,这说明ADC1和ADC12的引脚全都是相同的。

ADC1和ADC2的引脚全都是相同的,既然都相同,那要ADC2还有啥用呢。这个就要再说一个ADC的高级功能了,就是双ADC模式,这个模式比较复杂。这里只简单介绍一下,不需要掌握。双ADC模式就是ADC1和ADC2一起工作,它俩可以配合组成同步模式、交叉模式等等模式。比如交叉模式,ADC1和ADC2交叉地对一个通道进行采样,这样就可以进一步提高采样率。当然ADC1和ADC2也是可以分开使用的,可以分别对不同的硬件而进行采样,这样也是可以的。

规则组的4种转换模式
接下来,我们再来了解一下规则组的4种转换模式,分别是单次转换,非扫描模式和连续转换,扫描模式等。那在我们ADC初始化的结构体里,会有两个参数,一个是选择单次转换还是连续转换的,另一个是选择扫描模式还是非扫描模式的,这两个参数组合起来,就有这4种转换方式。我们来逐一看一下。

江科大STM32 下,STM32,stm32

第一种,单次转换,非扫描模式,最简单,这里我画了一个列表,这个表就是规则组里的菜单,有16个空位,分别是序列1到序列16,你可以在这里“点菜”,就是写入你要转换的通道,在非扫描的模式下,这个菜单就只有第一个序列1的位置有效,这时,菜单同时选中一组的方式就退化为简单地选中一个的方式了。在这里我们可以在序列1的位置指定我们想转换的通道,比如通道2,写到这个位置。然后,我们就可以触发转换,ADC就会对这个通道2进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给EOC标志位置1,整个转换过程就结束了。我们判断这个EOC标志位,如果转换完了, 那我们就可以在数据寄存器里读取结果了。如果我们想再启动一次转换,那就需要再触发一次,转换结束,置EOC标志位,读结果。如果想换一个通道转换,那在转换之前,把第一个位置的通道2改成其他通道,然后再启动转换,这样就行了。这就是单次转换,非扫描的转换模式。没有用到这个菜单列表,也是比较简单的一种模式

江科大STM32 下,STM32,stm32

接下来我们看一下连续转换,非扫描模式。首先,它还是非扫描模式,所以菜单列表就只用第一个,然后它与上一种单次转换不同的是,它在一次转换结束后不会停止,而是立刻开始下一轮的转换,然后一直持续下去。这样就只需要最开始触发一次,之后就可以一直转换了。这个模式的好处就是,开始转换之后不需要等待一段时间的,因为它一直都在转换,所以你就不需要手动开始转换了,也不用判断是否结束的,想要读AD值的时候,直接从数据寄存器取就是了。这就是连续转换,非扫描的模式

江科大STM32 下,STM32,stm32

然后继续看,单次转换,扫描模式。这个模式也是单次转换,所以每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。然后它是扫描模式,这就会用到这个菜单列表了,你可以在这个菜单里点菜,比如第一个菜是通道2,第二个菜是通道5,等等等等,这里每个位置是通道几可以任意指定,并且也是可以重复的,然后初始化结构体里还会有个参数,就是通道数目。因为这16个位置你可以不用完,只用前几个,那你就需要再给一个通道数目的参数,告诉它,我有几个通道。比如这里指定通道数目为7,那它就只看前7个位置,然后每次触发之后,它就依次对这前7个位置进行AD转换,转换结果都放在数据寄存器里,这里为了防止数据被覆盖,就需要用DMA及时将数据挪走。那7个通道转换完成之后,产生EOC信号,转换结束,然后再触发下一次,就又开始新一轮的转换,这就是单次转换,扫描模式的工作流程。

江科大STM32 下,STM32,stm32

那最后再看一下连续转换,扫描模式。它就是在上一个模式的基础上,变了一点,就是一次转换完成后,立刻开始下一次的转换。和上面这里非扫描模式的单次和连续是一个套路,这就是连续转换,扫描模式。

当然在扫描模式的情况下,还可以有一种模式,叫间断模式。它的作用是,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。这个模式没有列出来,要不然模式太多了。大家了解一下就可以了,暂时不需要掌握,好,这些就是STM32 ADC的4种转换模式。

几个小知识点细节:
触发控制

江科大STM32 下,STM32,stm32

这个表就是规则组的触发源,也就是ADC总框图中的ADC。在这个表里,有来自定时器的信号;还有这个来自引脚或定时器的信号,这个具体是引脚还是定时器,需要用AFIO重映射来确定;最后是软件控制位,也就是我们之前说的软件触发。这些触发信号怎么选择,可以通过设置右边这个寄存器来完成,当然使用库函数的话,直接给一个参数就行了,这就是触发控制。

数据对齐
江科大STM32 下,STM32,stm32

我们这个ADC是12位的,它的转换结果就是一个12位的数据,但是这个数据寄存器是16位的,所以就存在一个数据对齐的问题,这里第一种是数据右对齐,就是12位的数据向右靠高位多出来的几位就补零,第二种是数据左对齐,是12位的数据向左靠,低位多出来的补零,在这里我们一般使用的都是第一种右对齐,这样读取这个16位寄存器,直接就是转换结果,如果选择左对齐直接读的话,得到的数据会比实际的大,因为数据左对齐实际上就是把数据左移了四次,二进制有个特点,就是数据左移一次,就等效于把这个数据乘二,那这里左移四次,就相当于把结果乘16了,所以直接读的话会,会比实际值大16倍。

那要这个左对齐有啥用呢,这个用途就是如果你不想要这么高的分辨率,你觉得0~4095数太大了,我就做个简单的判断,不需要这么高分辨率,那你就可以选择左对齐,然后再把这个数据的高八位取出来,这样就舍弃掉了后面四位的精度,这个12位的ADC就退化成了八位的ADC,这就是左对齐的作用,不过我们一般用的话,选右对齐就行了哈,如果需要裁减一些分辨率,大不了就先把12位都取出来再做处理,这也是可以的哈,就是多算了一步而已。

转换时间
江科大STM32 下,STM32,stm32

这个大概讲一下,不过转换时间这个参数,我们一般不太敏感,因为一般AD转换都很快,如果不需要非常高速的转换频率,那转换时间就可以忽略了。
我们来看一下,之前我们说了,AD转换是需要一小段时间的,就像厨子做菜一样,也是需要等一会儿才能上菜的,那AD转换的时候都有哪些步骤需要花时间呢?AD转换的步骤,有4步,分别是采样,保持,量化,编码,其中采样保持可以放在一起,量化编码可以放在一起,总共是这两大步。量化编码好理解,就是我们之前讲过的,ADC逐次比较的过程,这个是要花一段时间的,一般位数越多,花的时间就越长。

那采样保持是干啥的呢?这个我们前面这里并没有涉及,为什么需要采样保持呢?这是因为,我们的AD转换,就是后面的量化编码,是需要一小段时间的,如果在这一小段时间里,输入的电压还在不断变化,那就没法定位输入电压到底在哪了,所以在量化编码之前,我们需要设置一个采样开关。先打开采样开关,收集一下外部的电压,比如可以用一个小容量的电容存储一下这个电压,存储好了之后,断开采样开关,再进行后面的AD转换。这样在量化编码的期间,电压始终保持不变,这样才能精确地定位未知电压的位置,这就是采样保持电路。

那采样保持的过程,需要闭合采样开关,过一段时间再断开,这里就会产生一个采样时间。那回到这里,我们就得到了第二条,STM32 ADC的总转换时间为TCONV=采样时间+12.5个ADC周期,采样时间是采样保持花费的时间,这个可以在程序中进行配置,采样时间越大,越能避兔一些毛刺信号的干扰,不过转换时间也会相应延长。12.5个ADC周期是量化编码花费的时间,因为是12位的ADC,所以需要花费12个周期,这里多了半个周期,可能是做其他一些东西花的时间。ADC周期就是从RCC分频过来的ADCCLK,这个ADCCLK最大是14MHz。

所以下面有个例子,这里就是最快的转换时间,当ADCCLK=14MHz,采样时间为1.5个ADC周期,TCONV = 1.5 +12.5 = 14个ADC周期,在14MHz ADCCLK的情况下就 = 1us,这就是转化时间最快1us时间的来源。如果你采样周期再长些,它就达不到1us了;另外你也可以把ADCCLK的时钟设置超过14MHz,这样的话ADC就是在超频了,那转换时间可以比1us还短,不过这样稳定性就没法保证了。

校准
江科大STM32 下,STM32,stm32

这个看上去挺复杂,但是我们不需要理解,这个校准过程是固定的。我们只需要在ADC初始化的最后,加几条代码就行了,至于怎么计算、怎么校准的,我们不需要管。

ADC外围电路设计
对于ADC的外围电路,我们应该怎么设计呢?

江科大STM32 下,STM32,stm32

这里我给出了三个电路图,第一个是电位器产生一个可调的电压,这里电位器的两个固定端,一端接3.3V,另一端接GND,这样中间的滑动端就可以输出一个0~3.3伏可调的电压输出来,我们这里可
以接ADC的输入通道,比如PA0口,当滑动端往上滑时,电压增大,往下滑时电压减小,另外注意一下这个电阻的阻值啊,不要给太小,因为这个电阻两端也是直接跨接在电源正负极的,如果阻值太小
那这个电阻就会比较费电,再小就有可能发热冒烟了,一般至少要接千欧级的电阻啊,比如这里接的是10k的电路,这是电位器产生可调电压的电路。

中间第二个是传感器输出电压的电路,一般来说像光敏电阻,热敏电阻,红外接收管,麦克风等等
,都可以等效为一个可变电阻,那电阻阻值没法直接测量,所以这里就可以通过和一个固定电阻串联分压,来得到一个反应电阻值电压的电路,那这里传感器阻值变小时下拉作用变强,输出端电压就下降,传感器阻值变大时下拉作用变弱,输出端受上拉电阻的作用,电压就会升高,这个固定电阻一般可以选择和传感器阻值相近的电阻,这样可以得到一个位于中间电压区域比较好的输出,但这里传感器和固定电阻的位置也可以换过来,这样的话输出电压的极性就翻过来了,这就是这个分压方法来输出传感器阻值的电路。

最后这个电路,这是一个简单的电压转换电路,比如你想测个0~ 5V的VIN电压,但是ADC只能接收0~ 3.3伏的电压,那就可以搭建一个这样的简易转换电路,在这里还是使用电阻进行分压,上面阻值17k,下面阻值33k加一起是50k,所以根据分压公式,中间的电压就是VIN/50Kx33k,最后得到的电压范围就是0~3.3就可以进入ADC转换了,这就是这个简单的电压转换电路。如果你想采集5V,10V这些电压的话,可以使用这个电压转换电路,但是如果你电压再高一些,就不建议使用这个电路了,那可能会比较危险。高电压采集最好使用一些专用的采集芯片,比如隔离放大器等等,做好高低电压的隔离,保证电路的安全。

手册粗讲

江科大STM32 下,STM32,stm32
江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

最后就是一个寄存器的总表,这里有所有的寄存器,中间有这些寄存器的复位值,也就是说上电复位后,寄存器都会变成复位值,这里复位值对应的就是各个外设的默认配置,比如GPIO口上电后默认配置为浮空输入的模式,输出数据寄存器默认输出低电平等等,如果你想了解上电后的默认配置,就可以参考这里的寄存器默认值,再对照相应的寄存器描述,这样就能知道默认配置了,这个寄存器一般上电都默认全为零哈,不过也有的不是,你大家可以找找看。

示例代码(AD单通道&AD多通道)

AD单通道,看一下这个接线图还是比较简单,在这里我们接了一个电位器,这个电位器有三个引脚,分别插在这三排孔里,电位器的内部结构是这样的,左边和右边的两个引脚,接的是电阻的两个固定端,中间这个引脚接的是滑动抽头,电位器外边这里有个十字形状的槽可以拧,往左拧抽头就往左靠,往右拧抽头就往右靠,我们把左边的固定端接在负极,右边的固定端接在正极,中间就可以输出从负极到正极可调到电压了,然后右边这里我们把可调的电压输出接在PA0口,在这里根据引脚定义表,PA0到PB7这十个引脚是ADC的十个通道,所以这十个硬件你可以任意选,接在哪个都行,但是其他的这些引脚不是ADC的通道,就不能接模拟电压了。
江科大STM32 下,STM32,stm32

7-1 AD单通道
程序现象:在面包板的中间,也就是芯片左边接了一个电位器,就是滑动变阻器。用这个电位器产生一个0~3.3V连续变化的模拟电压信号。然后接到STM32的PA0口上,之后用STM32内部的ADC读取电压数据,显示在屏幕上。这里屏幕第一行显示的是AD转换后的原始数据,第二行是经过处理后实际的电压值。电位器往左拧,AD值减小,电压值也减小,AD值最小是0,对应的电压就是0V;反之同理STM32的ADC是12位的,所以AD结果最大值是4095,也就是2^12-1,对应的电压是3.3V。

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

我们把这个结构打通,在原理上能够让这个外设运转起来,那该怎么配置,自己心里就应该有数了。

第一步,开启RCC时钟,包括ADC和GPIO的时钟,另外这里ADCCLK的分频器,也需要配置一下

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

第二步,配置GPIO。把需要用的GPIO配置成模拟输入的模式

第三步,配置这里的多路开关。把左边的通道接入到右边的规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜,列在菜单里

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

第四步,就是配置ADC转换器了。在库函数里,是用结构体来配置的,可以配置这一大块电路的参数。包括ADC是单次转换还是连续转换、扫描还是非扫描、有几个通道,触发源是什么,数据对齐是左对齐还是右对齐。

江科大STM32 下,STM32,stm32

如果你需要模拟看门狗,那会有几个函数用来配置阈值和监测通道的,如果你想开启中断,那就在中断输出控制里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置一下优先级,这样就能触发中断了。不过这一块,模拟看门狗和中断,我们本节暂时不用,如果你需要的话,可以自己配置试一下。

接下来,就是开关控制,调用一下ADC_Cmd函数,开启ADC,这样ADC就配置完成了,就能正常工作了。
江科大STM32 下,STM32,stm32

当然,在开启ADC之后,根据手册里的建议,我们还可以对ADC进行一下校准,这样可以减小误差,那在ADC工作的时候,如果想要软件触发转换,那会有函数可以触发,如果想读取转换结果,那也会有函数可以读取结果,这个等会儿介绍库函数的时候就可以看到了。

这里有四个函数,对应校准的四个步骤:第一步,调用第一个函数ADC_ResetCalibration,复位校准;第二步,调用第二个函数ADC_GetResetCalibrationStatus,等待复位校准完成;第三步,调用第三个函数ADC_StartCalibration,开始校准;第四步,调用第四个函数ADC_GetCalibrationStatus,等待校准完成。

江科大STM32 下,STM32,stm32

如果想要软件触发转换,那会有函数可以触发。如果想读取转换结果,那也会有函数可以读取结果,这个等会儿介绍库函数的时候就可以看到了。好,这些就是我们程序的大概思路了。
首先,软件触发转换;然后等待转换完成,也就是等待EOC标志位置1;最后,读取ADC数据寄存器,就完事了。

江科大STM32 下,STM32,stm32

AD.h

#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(void);

#endif

AD.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;			//模拟输入,在AIN模式下GPIO口是无效的,断开GPIO防止GPIO口的输入输出对模拟电压造成干扰
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA0引脚初始化为模拟输入
	
	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		//规则组序列1的位置,配置为通道0  通道采样时间随便选的55.5个ADCCLK的周期(需要更快的转换就选择小的参数,反之需要更稳定的转换就..)
	//目前只有PA0一个通道,使用的非扫描模式,所以指定的通道就放在第一个序列1的位置
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式(还有双ADC模式),即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发转换选择,使用软件触发,不需要外部触发
	//单次转换非扫描的模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1

	// 中断和模拟看门狗,如果需要可在此处继续配置
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/  	//固定流程,内部有电路会自动执行校准
	ADC_ResetCalibration(ADC1);								//复位校准	
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);		//返回复位校准的状态
	ADC_StartCalibration(ADC1);								//开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:无
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次(如果是连续转换模式,只需要触发一次就可以了)
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t ADValue;			//定义AD值变量
float Voltage;				//定义电压变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();			//OLED初始化
	AD_Init();				//AD初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "ADValue:");
	OLED_ShowString(2, 1, "Voltage:0.00V");
	
	while (1)
	{
		ADValue = AD_GetValue();					//获取AD转换的值
		Voltage = (float)ADValue / 4095 * 3.3;		//将AD值线性变换到0~3.3的范围,表示电压
		
		OLED_ShowNum(1, 9, ADValue, 4);				//显示AD值
		OLED_ShowNum(2, 9, Voltage, 1);				//显示电压值的整数部分
		OLED_ShowNum(2, 11, (uint16_t)(Voltage * 100) % 100, 2);	//显示电压值的小数部分(注意浮点数不能取余,所以先强制类型转换成uint16_t)
		
		Delay_ms(100);			//延时100ms,手动增加一些转换的间隔时间
	}
}

这一位由软件清除或由读取ADC_DR时清除,ADC_DR是数据计算器,一般EOC标志位置1,我们就会来读取数据,所以它就多设计了一个功能,就是这一位可以在读取数据寄存器之后自动清除,就不需要你再手动清除了,可以省一条代码,这就是这个标志位,当它为0时,表示转换未完成,为1表示转换完成。
江科大STM32 下,STM32,stm32
所以在这里当EOC标志位等于等于reset时,转化未完成,while条件为真执行空循环,转换完成后
EOC由硬件自动置1,那while循环就自动跳出来,这样就是等待转换完成的代码,那具体会等待多长时间,我们刚才配置的时候,指定了这个通道的采样周期是55.5,转换周期是固定的12.5,加在一起就是68个周期,前面我们配置的ADCCLK是72MHZ的六分频就是12MHz,12MHz进行68个周期转换才能完成,最终的时间可以算一下,就是1/12MHz x 68,结果大概是5.6us,这个我们上一小节讲转换时间的时候,也讲过,所以这个while循环,大概会等待5.6微秒,这就是等待的时间,那等待完成之后
我们就可以取结果了。

实验发现AD值的末尾会有一些抖动,这是正常的波动,如果你想对这个值进行判断,再执行一些操作
比如光线的AD值小于某一阈值就开灯,大于某一阈值就关灯,那可能会存在这样的情况,比如光线逐渐变暗,AD值逐渐变小,但是由于波动,AD值会在判断阈值附近来回跳变,这会导致输出产生抖动
来回开灯关灯,那如何避免这种情况呢,这个可以使用迟滞比较的方法来完成,设置两个阈值,低于下阈值时开灯,高于上阈值时关灯,这就可以避免输出抖动的问题了,这个跟我们GPIO那一节讲的施密特触发器是一个原理,另外如果你觉得数据跳变太厉害,还可以采用滤波的方法,让AD值平滑一些
比如均值滤波啊,就是读取十个或20个值,取平均值作为滤波的AD值,或者还可以裁剪分辨率
把数据的尾数去掉,这样也可以减少数据波动,这都是可行的方法,大家实际遇到这方面问题的话
可以考虑一下。

7-2 AD多通道

在这里我们使用了4个AD通道,第一个通道还是电位器,接在PA0口之后,上面又接了三个传感器模块,分别是光敏传感器、热敏传感器、反射式红外传感器,他们的VCC和GND都分别接在面包板的正负极,然后这个AO就是模拟量的输出引脚,三个模块的AO分别接在PA1、PA2和PA3,加上电位器的PA0 总共是四个输入通道,同样在这些GPIO口也是可以在PA0到PB1之间任意选择的,我这里就选择前四个了。
江科大STM32 下,STM32,stm32

如何实现多通道呢?
我们首先想到的应该是后面这两种扫描模式(连续转换、扫描模式和单次转换、扫描模式),利用这个列表把四个通道都填进去,然后触发转换,这样就能实现多通道了,这样确实是一种不错的方法,但是还是那个数据覆盖的问题,如果想要用扫描模式实现多通道,最好要配合DMA来实现,我们下节讲完DMA之后,再来试一下扫描模式。

那你可能会问
我们一个通道转换完成之后,手动把数据转运出来不就行了,为啥非要用DMA来转运呢,这个方案看似简单,但是实际操作起来会有一些问题,第一个问题就是在扫描模式下,你启动列表之后,它里面每一个单独的通道转换完成之后,不会产生任何的标志位,也不会触发中断,你不知道某一个通道是不是转,换完了,它只有在整个列表都转换完成之后,才会产生一次EOC标志位才能触发中断,而这时前面的数据就已经覆盖丢失了,第二个问题就是AD转换是非常快的,刚才我们也计算过
转换一个通道大概只有几微秒,也就是说,如果你不能在几微秒的时间内把数据转运走,那数据就会丢失,这对我们程序手动转运数据,要求就比较高了,所以在扫描模式下,手动转移数据是比较困难的哈,不过比较困难也不是说手动转运不可行啊,我们可以使用间断模式,在扫描的时候每转换一个通道就暂停一次,等我们手动把数据转运走之后,再继续触发下一次转换,这样可以实现手动转移数据的功能哈,但是由于单个通道转换完成之后,没有标注位,所以启动转换完成之后,只能通过delay延时的方式,延时足够长的时间,才能保证转换完成,这种方式既不,能让我们省心,也不能提高效率,所以我们暂时不推荐使用。

那这些方法都不行,我们本节是不是就不能实现多通道了呢,答案是能实现,而且非常简单,怎么实现呢,我们可以使用上面的这个单次转换非扫描的模式来实现多通道,只需要在每次触发转换之前
手动更改一下列表第一个位置的通道就行了,比如第一次转换先写入通道0之后触发、等待、读值
第二次转换,再把通道0改成通道1之后触发、等待、读值等等,这样在转换前先指定一下通道,再启动转换,就可以轻松地实现多通道转换的功能。代码这里也只需做一些简单的修改就行了。

AD.h

#ifndef __AD_H
#define __AD_H

void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);

#endif

AD.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	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);					//将PA0、PA1、PA2和PA3引脚初始化为模拟输入
	
	/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);	//在每次转换前,根据函数形参灵活更改规则组的通道1 我们要指定的通道是0、1、2、3,对应上面的GPIO 0、1、2、3
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

uint16_t AD0, AD1, AD2, AD3;	//定义AD值变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	
	/*显示静态字符串*/
	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);		//单次启动ADC,转换通道0
		AD1 = AD_GetValue(ADC_Channel_1);		//单次启动ADC,转换通道1
		AD2 = AD_GetValue(ADC_Channel_2);		//单次启动ADC,转换通道2
		AD3 = AD_GetValue(ADC_Channel_3);		//单次启动ADC,转换通道3
		
		OLED_ShowNum(1, 5, AD0, 4);				//显示通道0的转换结果AD0
		OLED_ShowNum(2, 5, AD1, 4);				//显示通道1的转换结果AD1
		OLED_ShowNum(3, 5, AD2, 4);				//显示通道2的转换结果AD2
		OLED_ShowNum(4, 5, AD3, 4);				//显示通道3的转换结果AD3
		
		Delay_ms(100);			//延时100ms,手动增加一些转换的间隔时间
	}
}

DMA直接存储器存取

实验现象:

DMA数据转运
在这个程序里,我们将使用DMA进行存储器到存储器的数据转运,也就是把一个数组里面的数据复制到另一个数组里,先看一下最终的代码,这里我定义了一个数组,DataA里面存的是0x01、0x02、0x03、0x04,作为待转运的源数据,然后下面再定义一个数组DataB,里面存的是4个0,作为转运数据的目的地,之后我们将会写一个模块叫MyDMA,进行初始化,把原数组和目的数组的地址传进去
,再传入转运数据的长度,使用DMA进行数据转运,和直接for循环,使用CPU一个个手动的转运数据效果是一样的,最后再显示一下DataA和DataB,看一下数据是不是从DataA转运到了DataB。

这里只定义了四个数据,演示一下现象,也可以定义100个1000个等等数据,然后使用DMA来进行转运都是可以的。

江科大STM32 下,STM32,stm32

DMA+AD多通道

看第二个程序,DMA+AD多通道,这个程序就是我们上一节预告过的,使用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运,最终AD转换的数据就会像这样直接自动的跑到我们定义的数组里面来,之后我们就只需要用OLED显示一下就行了,看上去还是非常方便的。

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

DMA这个外设是可以直接访问STM32内部的存储器的,包括运行内存SRAM、程序存储器FLASH和寄存器等等,DMA都有权限访问他们,所以DMA才能完成数据转运的工作。

DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无需CPU干预,节省了CPU的资源,这里外设指的就是外设寄存器,一般是外设的数据寄存器器DR, Data Register,比如ADC的数据寄存器,串口的数据寄存器等等,这里存储器指的就是运行内存SRAM和程序存储器flash,是我们存储变量数组和程序代码的地方,在外设和存储器或者存储器和存储器之间进行数据转运,就可以使用DMA来完成,并且在转运的过程中无需CPU的参与,节省了CPU的资源,CPU省下时间就可以干一些其他的更加专业的事,情搬运数据这种杂活,交给DMA就行了。

12个独立可配置的通道,这个通道就是数据转移的路径,从一个地方移动到另一个地方,就需要占用一个通道,如果有多个通道进行转运,那它们之间可以各转各的,互不干扰,这就是DMA的通道。

每个通道都支持软件触发和特定的硬件触发,这里如果执行的是存储器到存储器的转运,比如我们想把flash里的一批数据转运到SRAM里去,那就需要软件触发,使用软件触发之后,DMA就会一股脑的把这批数据以最快的速度全部转运完成,这也是我们想要的效果哈,那如果DMA进行的是外设到存储器的数据转运,就不能一股脑的转运了,因为外设的数据是有一定时机的,所以这时我们就需要用硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次,这样数据才是正确的,才是我们想要的效果,所以存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发,那这里我写的是特定的硬件触发,意思就是每个DMA的通道,它的硬件触发源是不一样的,你要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道,这个我们等会儿再详细分析。

C8T6的DMA资源是DMA1(7个通道),我们这个芯片只有DMA1的7个通道,没有DMA2,这个注意看一下数据手册。

江科大STM32 下,STM32,stm32

我们来看一下STM32的存储器映象,既然DMA是在存储器之间进行数据转运的,那我们就应该要了解一下STM32中都有哪些存储器,这些存储器又是被安排到了哪些地址上,这就是存储器映象的内容
,那我们知道计算机系统的五大组成部分是运算器、控制器、存储器、输入设备和输出设备,其中运算器和控制器一般会合在一起,叫做CPU,所以计算机的核心关键部分就是CPU和存储器,存储器又有两个重要知识点,一个是存储器的内容,另一个就是存储器的地址,那STM32也不例外,这个表就是STM32中所有类型的存储器和他们所被安排的地址,在STM32 的数据手册,这里也会有个存储器印象的图,如下所示,我这个表就是从这个图里总结出来的,都是一个意思。

在这个表里,无论是flash还是SRAM还是外设寄存器,它们都是存储器的一种,包括外设寄存器
实际上也是存储器,我们前面这里说的是外设到存储器,存储器到存储器,本质上其实都是存储器之间的数据转运,说成外设的存储器,只不过是STM32 它特别指定了可以转运外设的存储器而已,这个了解一下。

存储器总共分成两大类,rom和ram,rom就是只读存储器,是一种非易失性、掉电不丢失的存储器
ram是随机存储器,是一种易失性、掉电丢失的存储器,其中rom分为了三块,第一块是程序存储器flash,也就是主闪存,它的用途就是存储c语言编译后的程序代码,也就是我们下载程序的位置
,运行程序一般也是从主闪存里面开始运行的,这一块存储器STM2 给它分配的地址是0x08000000
起始地址,也就是第一个字节的地址是0800这个哈,然后剩余字节的地址依次增长,每个字节都分配一个独一无二的地址,就像给每个住户编门牌号一样,只有分配了独一无二的门牌号程序才能精准地访问这个存储器,最终终止地址是多少呢,这取决于它的容量,编到哪里,哪里就是终止地址,这就是主闪存的地址范围,你之后如果在软件里看到某个数据的地址是0800开头的,那你就可以确定它是属于主闪存的数据。

接着继续看系统存储器和选项字节,这两块存储器也是rom的一种,掉电不丢失,实际上他们的存储介质也是flash,只不过我们一般说flash指的是主闪存flash,而不止这两块区域,那看一下地址,他们的地址都是1fff开头的,紧跟着2000开头的就是ram区的,所以可以看出这两块存储器的位置是在rom区的最后面,它们的用途看下右边说明,系统存储器的用途是存储bootloader,用于串口下载,这个下一节讲串口的时候再给大家演示,那个bootloader程序存储的位置就被分配到了这里,bootloader程序是芯片出厂自动写入的哈,一般也不允许我们修改,之后选项字节,主要是保存一些配置,它的位置是在rom区的最后面,你下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里存的主要是flash的读保护写保护,还有看门狗等等的配置,这个如果需要的话,可以了解一下。

然后我们看一下ram区域,首先是运行内存sram,分配的地址是0x20000000 ,用途是存储运行过程中的临时变量,也就是我们在程序中定义变量数组结构体的地方,你可以试一下,定义一个变量,再取他的地址显示出来,那这个地址肯定就是2000开头的,类比于电脑的话运行内存就是内存条,然后ram区剩下的还有外设寄存器,它的地址是0x40000000这块区域,用途是存储各个外设的配置参数,也就是我们初始化各个外设最终所读写的东西,刚才我们说了,外设寄存器也是存储器的一种,它的存储介质其实也是sram,只不过我们一般习惯把运行内存叫SRAM,外设寄存器就直接叫寄存器了。

最后是内核外设寄存器地址是0xE0000000这片区域,用途是存储内核各个外设的配置参数,内核外设就是NVIC和SysTick,因为内核外设和其他外设不是一个厂家设计的,所以他们的地址也是被分开的
,内核外设是E000,其他外设是4000,那以上这些就是STM32里的存储器和他们被安排的地址。

江科大STM32 下,STM32,stm32

我们在这个图里也看一下,在STM32中,所有的存储器都被安排到了0~ffffffff这个地址范围内,因为cpu是32位的,所以寻址范围就是32位的范围,32位的寻址范围是非常大的,最大可以支持4gb容量的存储器,而我们STM32的存储器都是kb级别的,所以这个4gb的寻址空间会有大量的地址都是空的
,算一下地址的使用率还不到1%,在这个图里有灰色填充的就是Reserve的区域,也就是保留区域没有使用到,然后这个零地址啊实际上也是没有存储器的,他这里写的是别名到flash或者系统存储器取决于boot引脚(Aliased to Flash or…),因为程序是从零地址开始运行的,所以这里需要把我们想要执行的程序映射到零地址来,如果映射在flash区,就从flash执行,如果映射在系统存储器区,就是从系统存储器运行bootloader,如果映射到sram,就是从sram启动,怎么选择,由boot0和boot1两个硬件来决定,这就是零地址里的别名区,接着剩下的0800开始的flash区,用于存储程序代码,1fff开始的系统存储器和选项字节是在rom区的,最后面存在什么东西,刚才也都介绍过,之后2000开始的是sram区,4000开始的是外设寄存器区,里面可以展开,就是右边这些东西,具体到每个外设又有他们自己的起始地址,比如TIM 2的地址是40000000,TIM3是40000400,然后外设里面又可以具体细分到每个寄存器的地址,寄存器里每个字节的地址,最终所有字节的地址就都可以算出来了,就上面这里E000开始的区域存放的就是内核里面的外设寄存器了,那到这里相信你对STM32 里面有哪些存储器,每种存储器都对应在哪个地址区间里就应该清楚了。

DMA框图

江科大STM32 下,STM32,stm32

我们看下DMA框图,左上角这里是Cortex-M3内核,里面包含了CPU和内核外设等等,剩下的这所有东西,你都可以把它看成是存储器,所以总共就是CPU和存储器两个东西。Flash是主闪存,SRAM是运行内存,各个外设,都可以看成是寄存器,也是一种SRAM存储器。
寄存器是一种特殊的存储器,一方面,CPU可以对奇存器进行读写,就像读写运行内存一样,另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当做计数器、数据寄存器等等。所以,寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。
回到这里,既然外设就是寄存器,寄存器就是存储器,那使用DMA进行数据转运,就都可以归为一类问题了。就是从某个地址取内容,再放到另一个地址去。

江科大STM32 下,STM32,stm32

我们看图,为了高效有条理地访问存储器,这里设计了一个总线矩阵,总线矩阵的左端,是主动单元,也就是拥有存储器的访问杈,右边这些,是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有DCode和系统总线,可以访问右边的存储器,其中DCode总线是专门访问Flash的,系统总线是访问其他东西的,另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。那主动单元,除了内核CPU,剩下的就是DMA总线了。这里DMA1有一条DMA总线,DMA2也有一条DMA总线,下面这还有一条DMA总线,这是以太网外设自己私有的DMA,这个可以不用管的。
在DMA1和DMA2里面,可以看到,DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立地工作了。

江科大STM32 下,STM32,stm32

接着下面这里有个仲裁器,这个是因为,虽然多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,那就会由仲裁器,根据通道的优先级来决定谁先使用和后使用。另外在总线矩阵这里,也会有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU也能正常的工作。

江科大STM32 下,STM32,stm32

下面这里是AHB从设备,也就是DMA自身的寄存器,因为DMA作为一个外设,它自己也会有相应的配置寄存器,这里连接在了总线右边的AHB总线上,所以DMA,既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。CPU通过这一条线路,就可以对DMA进行配置了。

江科大STM32 下,STM32,stm32

接着继续看这里,是DMA请求,请求就是触发的意思,这条线路右边的触发源,是各个外设,所以这个DMA请求就是DMA的硬件触发源。比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了。这就是DMA请求的作用。

到这里,有关DMA的结构就讲的差不多了,其中包括:用于访问各个存储器的DMA总线;内部的多个通道,可以进行独立的数据转运;仲裁器,用于调度各个通道,防止产生冲突;AHB从设备,用于配置DMA参数;DMA请求,用于硬件触发DMA的数据转运,这就是这个DMA的各个部分和作用。

江科大STM32 下,STM32,stm32

注意一下:就是这里的Flash,它是ROM只读存储器的一种,如果通过总线直接访问的话,无论是CPU,还是DMA,都是只读的,只能读取数据,而不能写入,如果你DMA的目的地址,填了Flash的区域,那转运时,就会出错。当然Flash也不是绝对的不可写入,我们可以配置这个Flash接口控制器,对Flash进行写入,这个流程就比较麻烦了,要先对Flash按页进行擦除,再写入数据,不过这是另一个课题了。总之就是CPU或者DMA直接访问Flash的话,是只可以读而不可以写的,然后SRAM是运行内存,可以任意读写,没有问题,外设寄存器的话,得看参考手册里面的描述。有的寄存器是只读的,有的寄存器是只写的,不过我们主要用的是数据寄存器,数据寄存器都是可以正常读写的。

DMA基本结构

江科大STM32 下,STM32,stm32

这里是我总结的DMA基本结构图,如果想编写代码实际去控制DMA的话,那这个图就是必不可少的了
刚才这个框图只是一个笼统的结构图,对于DMA内部的执行细节,它还是没体现出来,所以我们再来分析一下这个图,看看DMA具体是怎么工作的。
江科大STM32 下,STM32,stm32

在这个图里,这两部分就是数据转运的两大站点了,左边是外设寄存器站点,右边是存储器站点,包括Flash和SRAM,在STM32手册里,他所说的存储器啊,一般是特指flash和sram,不包含外设寄存器,外设寄存器它一般直接称作外设,所以就是外设到存储器,存储器到存储器这样来描述,虽然我们刚才说了,寄存器也是存储器的一种,但是STM32还是使用了外设和存储器来作为区分,这个注意一下描述方法的不同,那在这里可以看到DMA的数据转运可以是从外设到存储器,也是可以从存储器到外设,具体是向左还是向右,有一个方向的参数可以进行控制,另外还有一种转运方式,就是存储器到存储器,比如flash到sram或者sram到sram这两种方式,由于flash是只读的,所以dma不可以进行sram到flash或者flash到flash的转移操作,然后我们继续看这两边的参数,既然要进行数据转运
,那肯定就要指定从哪里转到哪里,具体怎么转呢,所以外设和存储器两个站点就都有三个参数
第一个是起始地址,有外设端的起始地址和存储器端的起始地址,这两个参数决定了数据是从哪里来到哪里去的,之后第二个参数是数据宽度,这个参数的作用是指定一次转运要按多大的数据宽度来进行,他可以选择字节byte、半自HalfWord和字word,字节就是八位,也就是一次转运一个uint8_t这么大的数据,半字是16位就是一次转运一个uint16_t这么大,字是32位,就是一次转运unit32_t这么大
比如转运ADC的数据,ADC的结果是unit16_t这么大,所以这个参数就要选择半字,一次转运一个unit16_t,然后第三个参数是地址是否自增,这个参数的作用是指定一次转移完成后,下一次转运是不是要把地址移动到下一个位置去,这就相当于是指针p++这个意思,比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边显然地址是不用指针的,如果自增,那下一次转运就跑到别的寄存器那里去了,存储器这边地址就需要指针,每转运一个数据后就往后挪个坑,要不然下次再转就把上次的覆盖掉了,这就是地址是否自增的作用,就是指定是不是要转运一次挪个坑这个意思,这就是外设站点和存储基站点各自的三个参数了。

如果要进行存储器到存储器的数据转运,那我们就需要把其中一个存储器的地址放在外设的这个站点
,这样就能进行存储器到存储器的转运,只要你在外设起始地址里写flash或者sram的地址,那他就会去flash或sram找数据,这个站点虽然叫外设存储器,但是它就只是个名字而已,并不是说这个地址只能写寄存器的地址,如果写flash的地址,那他就会去flash里找,写sram他就会去sram里找
这个没有限制,甚至你可以在外设站点写存储器的地址,存储器站点写外设的地址,然后方向参数给反过来,这样也是可以的,只是ST公司给他起了这样的名字而已,所以我这里就按照它的名字来做的ppt,你也可以把它叫做站点a站点b,从a到b或者从b到a转运数据,不必拘泥于他写的外设站点存储器站点这个名字。

传输计数器和自动重装器:

江科大STM32 下,STM32,stm32

传输计数器这个东西就是用来指定我总共需要转运几次的,这个传输计数器是一个自减计数器,比如你给他写个5,那DMA就只能进行5次数据转运,转运过程中每转运一次计数器的数就会减一,当传输计数器减到零之后,DMA就不会再进行数据转运了,另外它减到零之后,之前自增的地址也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转换,在传输计数器的右边有一个自动重装器,这个自动重装器的作用就是传输计数器减到零之后,是否要自动恢复到最初的值,比如最初传输计数器给5
,如果不使用自动重装器,那转运5次后DMA就结束了,如果使用自动重装器,那转运5次计数器减到零后就会立即重装到初始值5,这个就是自动重装器,它决定了转运的模式,如果不重装就是正常的单次模式,如果重装就是循环模式,比如如果你想转运一个数组,那一般就是单次模式转运一轮就结束了,如果是ADC扫描模式加连续转换,那为了配合ADC,DMA也需要使用循环模式,所以这个循环模式和ADC的连续模式差不多啊,都是指定一轮工作完成后,是不是立即开始下一轮工作。

然后继续往下看啊,这一块就是DMA的触发控制了,触发就是决定DMA需要在什么时机进行转运的
触发源有硬件触发和软件触发,具体选择哪个,由M2M这个参数决定,M2M就是memory to memory
因为2的英文two和to同音,所以M2M就是m to m存储器到存储器的意思,当我们给M2M位1时
DMA就会选择软件触发,这个软件触发并不是调用某个函数一次触发一次,这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换,所以这里的软件触发和我们之前外部中断和ADC的软件出发,可能不太一样,你可以把它理解成连续触发,那这个软件触发和循环模式不能同时用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,那DMA就停不下来了,这就是软件触发,软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运,是软件启动不需要时机,并且想尽快完成的任务,所以上面这里M2M位给1就是软件触发,就是应用在存储器到存储器转运的情况,M2M位给0,那就是使用硬件触发,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传个信号过来来触发DMA进行转运,这就是硬件触发。

触发控制部分:
江科大STM32 下,STM32,stm32

然后最后,就是开关控制了,也就是DMA_Cmd函数。当给DMA使能后,DMA就准备就绪,可以进行转运了。

DMA进行转运有几个条件
第一就是开关控制,DMA_Cmd必须使能,第二就是传输计数器必须大于0,第三就是触发源必须有触发信号,触发一次转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了,此时就需要DMA_CMD给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作,注意一下
,写传输计数器时,必须要先关闭DMA再进行,不能在DMA开启时写传输计数器,这是手册里的规定。

几个小知识点细节:
DMA请求

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

这张图表示的就是我们上面这里的这部分结构,DMA触发的部分,我们来看一下这张图是DMA1的请求映像,下面是DMA的7个通道,每个通道都有一个数据选择器,可以选择硬件触发或软件触发,这里画的图我觉得可能不太好理解,你看他把EN位画在了数据选择器的侧边,一般数据选择器的侧边是输入选择控制位,难道这里的意思是EN给1选择硬件触发,EN给0选择软件触发吗,那显然不对啊,而且他左边这里写的是软件触发括号memory to memory位,难道M2M位是软件触发吗,这个也不太好理解哈,所以这个图我重新给他布了个局,就是上面我这里画的这样,M2M位是数据选择器的控制位,用于选择是硬件触发还是软件触发,EN位是开关控制,EN等于0时不工作,EN等于1时工作,这样就好理解一些了,那这里他这样画的意思应该是,EN并不是数据选择器的控制位啊,而是决定这个数据选择器要不要工作,EN等于0数据选择器不工作,EN等于1数据选择器工作,然后软件触发后面跟个M2M位的意思应该是当M2M位等于1时选择软件触发,这样理解的话就跟我这个图里是一个意思了,那然后继续看左边的硬件触发源,这里是外设请求信号啊,可以看到每个通道的硬件触发源都是不同的,如果你需要用ADC1来触发的话,那就必须选择通道一,如果需要定时器二的更新事件来触发的话,那就必须选择通道二,剩下的也是同理哈,因为每个通道的硬件触发源都不同,所以如果你想使用某个硬件触发源的话,就必须使用它所在的通道,这就是硬件触发的注意事项,而如果使用软件触发的话,那通道就可以任意选择了,因为每个通道的软件触发都是一样的,所以在ppt的在前面写的是每个通道都支持软件触发和特定的硬件触发,这就是特定的意思,选择硬件触发是要看通道的。

然后回来继续看哈,这里通道1的硬件触发是ADC1 、定时器2的通道3和定时器4的通道1,那到底是选择哪个触发源呢,这个是对应的外设是否开启了DMA输出来决定的,比如你要使用ADC1 ,那会有个库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效,如果想选择定时器2的通道3,那也会有个TIM_DMACmd函数,用来进行DMA输出控制,所以这三个触发源具体使用哪个,取决于你把哪个外设的DMA输出开启了,如果三个都开启了,那这边是一个或门,理论上三个硬件都可以进行触发,不过一般情况下我们都是开启其中一个,之后这七个触发源进入到仲裁器进行优先级判断,最终产生内部的DMA1请求,这个优先级的判断啊,类似于中断的优先级,默认优先级是通道号越小优先级越高,当然也可以在程序中配置优先级,这个其实影响并不是很大,大家了解一下就行。

数据宽度与对齐
DMA数据转运的两个站点,都有一个数据宽度的参数,如果数据宽度都一样,那就是正常的一个个转运,如果数据宽度不一样,那会怎么处理呢?
这个表就是来说明问题的,

江科大STM32 下,STM32,stm32

我们看一下这里第一列是源端宽度,第二列是目标宽度,第三列是传输数目,当源端和目标都是8位时
转运第一步,在源端的0位置读数据B0 ,在目标的0位置写数据b0 ,就是把这个B0从左边挪到右边
之后的步骤,就是把B1从左边挪到右边,接着B2、B3,这是源端和目标都是8位的情况,操作也很正常,接着继续,源端是8位,目标是16位,那他的操作就是在源端读B0,在目标写00B0,之后读B1,写00B1,等等,这个意思就是,如果你目标的数据宽度比源端的数据宽度大,那就在目标数据前面多出来的空位补0,之后8位转运到32位,也是一样的处理哈,前面空出来的都补0,然后下面当目标数据宽度比源端数据宽度小时,比如由16位转到8位去,现象就是读b1 b0 只写入b0 ,读b3 b2 只写入b2 ,也就是把多出来的高位舍弃掉,之后的各种情况也都是类似的操作。

总之一这个表的意思就是如果你把小的数据转到大的里面去,高位就会补0;如果把大的数据转到小的里面去,高位就会舍弃掉;如果数据宽度一样,那就没事。就是跟unit8_t、unit16_t和unit32_t变量之间相互赋值一样,不够就补0,超了就舍弃高位,这是一个道理。

那最后,我们再来看两个例子,看看在这些实际的任务下,DMA是如何工作的。这两个例子和程序例子对应的。

数据转运+DMA
江科大STM32 下,STM32,stm32

这个例子的任务是将SRAM里的数组DataA,转运到另一个数组DataB中,我们看一下这种情况下,这个基本结构里的各个参数该如何配置。
首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。那在这个任务里,外设地址显然应该填DataA数组的首地址,存储器地址,给DataB数组的首地址,然后数据宽度,两个数组的类型都是uint8_t,所以数据宽度都是按8位的字节传输。之后地址是否自增,在中间可以看到,我们想要的效果是DataA[0]转到DataB[0],DataA[1]转到DataB[1],等等。所以转运完DataA[0]和DataB[0]之后,两个站点的地址都应该自增,都移动到下一个数据的位置,继续转运DataA[1]和DataB[1],这样来进行。
之后,这里的方向参数,那显然就是外设站点转运到存储器站点了,当然如果你想把DataB的数据转运到DataA,那可以把方向参数换过来,这样就是反向转运了。
然后是传输计数器和是否要自动重装,在这里,显然要转运7次,所以传输计数器给7,自动重装暂时不需要,之后触发选择部分,这里,我们要使用软件触发。因为这是存储器到存储器的数据转运,是不需要等待硬件时机的,尽快转运完成就行了。
那最后,调用DMA_Cmd,给DMA使能,这样数据就会从DataA转运到DataB了。转运7次之后,传输计数器自减到0,DMA停止,转运完成。这里的数据转运是一种复制转运,转运完成后DataA的数据并不会消失,这个过程相当于是把DataA的数据复制到了DataB的位置。

ADC扫描模式+DMA

江科大STM32 下,STM32,stm32

左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面。那我们要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。所以在这里DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址。
之后数据宽度,因为ADC_DR和SRAM数组,我们要的都是uint16_t的数据,所以数据宽度都是16位的半字传输。
接着判断地址是否自增,那从这个图里,显然是外设地址不自增,存储器地址自增;传输方向,是外设站点到存储器站点;传输计数器,这里通道有7个,所以计数7次;计数器是否自动重装,这里可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止,如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。
最后是触发选择,这里ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发。
最后硬件触发这里要说明一下,我们上一节说了,ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断。所以我们程序不太好判断,某一个通道转换完成的时机是什么时候。但是根据UP主的研究,虽然单个通道转换完成后,不产生任何标志位和中断,但是它应该会产生DMA请求,去触发DMA转运,这部分内容,手册里并没有详细描述,根据我实际实验,单个通道的DMA请求肯定是有的。
这些就是ADC扫描模式和DMA配合使用的流程。一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,这个缺陷使ADC和DMA成为了最常见的伙伴,ADC对DMA的需求是非常强烈的,像其他的一些外设,使用DMA可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多是损失一些性能,但是这个ADC的扫描模式,如果不使用DMA,功能都会受到很大的限制,所以ADC和DMA的结合最为常见。

参考手册

江科大STM32 下,STM32,stm32
江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

之后这里还有一个位段区域,这两个位段区映射了外设寄存器和SRAM中全部的位,这个位段区就相当于是位寻址,它把外设寄存器和SRAM中所有的位都分配了地址,你操作这个新的地址就相当于操作其中某一个位,因为32位的地址有99%都是空的,所以地址空间很充足,即使把每一位都单独编码
那毫无压力,所以就存在了这样一个位段,用于单独操作计算器或sram的某一位,位段区是另找了一个地方,开辟了一段地址区域,其中sram位段区域是2200开头的区域,外设寄存器的位段区是4200开头的区域,需要用的话可以了解一下。

江科大STM32 下,STM32,stm32
嵌入式闪存,闪存被分为了很多页,他们的地址都是0800开头的,在闪存区的最后就是系统存储器和选项字节,这两个区域统称为信息块,下面这是闪存接口寄存器,这是外设的一部分,你看它的地址是40开头的,所以它显然是一个外设,这个外设可以对闪存进行读写哈,这就是闪存的部分。

江科大STM32 下,STM32,stm32
启动配置,配置boot0和boot1两个引脚来选择程序从哪里启动,这个我们第一节和刚才也介绍过。

江科大STM32 下,STM32,stm32

示例代码(DMA数据转运&&DMA+AD多通道)

这个电路和OLED显示屏是一样的,因为数据转运都是在STM32内部进行的,其他的模块都不需要
,然后面包板这里也就是一个oled显示屏的电路就行了。
江科大STM32 下,STM32,stm32

先来验证存储器映像的内容
江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32
看一下我们定义的数据,它到底是不是真的存在了这相应的地址区间里,下载看一下,aa这个变量它被存储的地址是20000000,地址是20开头的,对照前面存储器映像里的表就知道了,aa这个变量存储的位置是sram区,在sram区它的地址肯定是20开头的,那它的具体地址是多少呢,这个是由编译器来确定的,目前sram区还没什么东西,所以编译器就把这个变量放在了sram区的第一个位置,也就是20000000,我们可以在这个变量前面加一个const关键字,表示的是常量的意思,被const的修饰的变量在程序中只能读不能写,那我们上一小节说了,flash里面的数据也是只能读不能写的,所以const和flash就联系起来了,在STM32 中,使用const定义的变量是存储在flash里面的,当然这里就不应该说是变量了,而应该说是常量,因为它是不能变的,这个变量的值只能在定义的时候给,如果你在程序这里尝试再次给它赋值,那就会报错啊,错误意思就是不能给cos的常量赋值,那我们下载看一下,,这里就可以看到aa这个变量的地址就变成0800开头的了,在ppt的表格里可以知道现在aa是被存储在了flash里,在flash里存储的是程序代码,当然还有常量数据哈,这里没写出来,那这里的地址尾部有些偏移哈,不像sram里那样直接安排在第一个位置,这是因为flash里还有程序代码这些东西放在了前面,所以编译器给这个常量安排的地址就相对靠后了一些,这就是定义变量和常量的方法,正常情况下我们使用的都是变量哈,直接定义就行,不需要加const的,那什么时候需要定义常量呢,这个是当我们程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省SRAM的空间,比如查找表,字库数据等等,我们可以打开这个oled_font.h文件,这里面就是oled显示英文的字库,这是一个数组哈,它里面的数据决定了每个字符应该显示哪些像素点,这个数组非常的长啊,而且是不需要更改的,所以在这里就可以加一个const,把它定义在flash里面,这样就可以节省sram的空间,这里如果你一不小心把这个const去掉了,那程序功能并不会有任何影响,但是sram里会有和这个数组一样大的空间被浪费掉了,如果数值很小,那影响也不大,如果数组很大,那就得考虑一下sram是不是消耗得起来,这就是cos的关键字的用途。

接下来我们再研究一下外设寄存器的地址,那对于变量或者常量来说,它的地址是由编译器确定的
不同的程序地址可能不一样,是不固定的,对于外设计算器来说,它的地址是固定的,在手册里都能查得到,在程序里也可以用结构体很方便的访问寄存器,比如要访问ADC1的DR寄存器,就可以写ADC1->DR,这样就可以访问ADC1的DR寄存器了。
江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

可以看到ADC1的DR寄存器地址是4001244C,对照ppt的表可以知道他确实是外设寄存器的区域,这个具体地址4001244c是固定的,在手册里也可以查到。

起始地址加偏移就是这个寄存器器的实际地址,手册如下:

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

江科大STM32 下,STM32,stm32

8-1 DMA数据转运
把一个数组里面的数据,复制到另一个数组里

这就是我们第一个代码的任务定义一下,DMA转运的源端数组和目的数组,初始化DMA,然后让DMA把这里DataA的数据,转运到DataB里面去。

江科大STM32 下,STM32,stm32
江科大STM32 下,STM32,stm32

初始化第一步,RCC开启DMA的时钟
注意:这里开启DMA时钟的时候,根据型号不同开启时钟参数也不同

江科大STM32 下,STM32,stm32

第二步,就可以直接调用DMA_Init,初始化这里的各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增,方向、传输计数器、是否需要自动重装、选择触发源、通道优先级,那这所有的参数,通过一个结构体,就可以配置好了

江科大STM32 下,STM32,stm32

例:

/* Initialize the DMA Channel1 according to the DMA_InitStructure 
members */ 
DMA_InitTypeDef DMA_InitStructure; 
DMA_InitStructure.DMA_PeripheralBaseAddr = 0x40005400; 
DMA_InitStructure.DMA_MemoryBaseAddr = 0x20000100; 
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; 
DMA_InitStructure.DMA_BufferSize = 256; 
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; 
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; 
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; 
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; 
DMA_Init(DMA_Channel1, &DMA_InitStructure); 

之后,就可以进行开关控制,DMA_Cmd,给指定的通道使能,就完成了。那在这里,如果你选择的是硬件触发不要忘了在对应的外设调用一下XXX_DMACmd,开启一下触发信号的输出;如果你需要DMA的中断,那就调用DMA_ITConfig,开启中断输出,再在NVIC里,配置相应的中断通道,然后写中断函数就行了。

最后,在运行的过程中,如果转运完成,传输计数器清0了。这时想再给传输计数器赋值的话,就DMA失能、写传输计数器、DMA使能,这样就行了。

MyDMA.h

#ifndef __MYDMA_H
#define __MYDMA_H

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);

#endif

MyDMA.c

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;					//定义全局变量,用于记住Init函数的Size,供Transfer函数使用

/**
  * 函    数:DMA初始化
  * 参    数:AddrA 原数组的首地址
  * 参    数:AddrB 目的数组的首地址
  * 参    数:Size 转运的数据大小(转运次数)
  * 返 回 值:无
  */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;					//将Size写入到全局变量,记住参数Size
	
	/*开启时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);						//开启DMA的时钟
	
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;										//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;						//外设基地址,给定形参AddrA
	//对于SRAM数组,地址是编译器分配的,并不固定,所以我们一般不会写绝对地址,而是通过数组名获取地址,所以此处把数组名传过来就行
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			//外设地址自增,选择使能,数组之间的转运地址需要自增
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;							//存储器基地址,给定形参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_Init,配置DMA1的通道1(存储器到存储器的转运,用的软件触发,通道随便选)
	
	/*DMA使能*/
	DMA_Cmd(DMA1_Channel1, DISABLE);	//这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}

/**
  * 函    数:启动DMA数据转运
  * 参    数:无
  * 返 回 值:无
  */
void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);					//DMA失能,在写入传输计数器之前,需要DMA暂停工作
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);	//写入传输计数器,指定将要转运的次数
	DMA_Cmd(DMA1_Channel1, ENABLE);						//DMA使能,开始工作
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);	//等待DMA工作完成    如果中途出错了,这里是不就卡死了?
	DMA_ClearFlag(DMA1_FLAG_TC1);						//清除工作完成标志位
}

mian.c:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"

// 这里给成千上万个数才能体现DMA优势,这里只是测试用
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};				//定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0};							//定义测试数组DataB,为数据目的地

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);	//DMA初始化,把源数组和目的数组的地址传入
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	
	/*显示数组的首地址*/
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
		
	while (1)
	{
		DataA[0] ++;		//变换测试数据
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		OLED_ShowHexNum(2, 1, DataA[0], 2);		//显示数组DataA
		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);		//显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);
		
		Delay_ms(1000);		//延时1s,观察转运前的现象
		
		MyDMA_Transfer();	//使用DMA转运数组,从DataA转运到DataB
		
		OLED_ShowHexNum(2, 1, DataA[0], 2);		//显示数组DataA
		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);		//显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);

		Delay_ms(1000);		//延时1s,观察转运后的现象
	}
}

江科大STM32 下,STM32,stm32

如果你想把flash的数据转运到sram里的话,可以在这个DataA前面加一个const,把DataA定义在flash里面,那下面这里DataA++就不能要了,因为const数据不能重新更改,然后编译下载看一下。

江科大STM32 下,STM32,stm32

8-2 DMA+AD多通道
用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运

接下来我们来写第二个程序ADC加DMA应用,先看一下接线图,这里接线图和上一节AD多通道是一样的,也是PA0接个电位器,PA1到PA3接三个传感器模块的AO输出。

江科大STM32 下,STM32,stm32

AD.h

#ifndef __AD_H
#define __AD_H

extern uint16_t AD_Value[4];

void AD_Init(void);

#endif

AD.c

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];					//定义用于存放AD转换结果的全局数组

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//开启DMA1的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	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);					//将PA0、PA1、PA2和PA3引脚初始化为模拟输入
	
	/*规则组通道配置  扫描PA0到PA3这四个通道  点四个菜 菜单上1~4号空位,我填上了0~3这四个通道 这个通道和次序可任意修改*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);	//规则组序列1的位置,配置为通道0
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);	//规则组序列2的位置,配置为通道1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);	//规则组序列3的位置,配置为通道2
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);	//规则组序列4的位置,配置为通道3
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;											//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;							//模式,选择独立模式,即单独使用ADC1
	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_NbrOfChannel确定(告诉厨师,我点了四个菜,你不要只盯着一个菜看)
	ADC_InitStructure.ADC_NbrOfChannel = 4;										//通道数,为4,扫描规则组的前4个通道
	ADC_Init(ADC1, &ADC_InitStructure);											//将结构体变量交给ADC_Init,配置ADC1
	
	/*DMA初始化  想象成一个服务员,ADC这个厨师把菜做好了,DMA这个服务员尽快端菜防止被覆盖*/
	DMA_InitTypeDef DMA_InitStructure;											//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//外设基地址,给定形参AddrA(端菜的源头地址 之前算过ADC1的DR寄存器地址是0X4001 244C可以这样来填,这里库函数已经算好了)
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//外设数据宽度,选择半字,对应16为的ADC数据寄存器  来转运
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//外设地址自增,选择失能,始终以ADC数据寄存器为源(不自增,始终转运同一个位置的数据)
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;					//存储器基地址(端菜的目的地,我们想把数据存在SRAM数组里),给定存放AD转换结果的全局数组AD_Value
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			//存储器数据宽度,选择半字,与源数据宽度对应
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存储器地址自增,选择使能,每次转运后,数组移到下一个位置
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
	DMA_InitStructure.DMA_BufferSize = 4;										//转运的数据大小(转运次数),与ADC通道数一致,4个ADC通道传输4次
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;								//模式,选择循环模式,与ADC的连续转换一致 (可以给单次,也可以给自动重装载的循环模式)
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								//存储器到存储器,选择失能,不使用软件触发,硬件触发 触发源为ADC1 数据由ADC外设触发转运到存储器(厨师每个菜做好了,叫我一下我再去端菜,这样时机才合适)
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;						//优先级,选择中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);								//将结构体变量交给DMA_Init,配置DMA1的通道1  这里必须填通道一,前面图里可以看到ADC1的硬件触发只接在了DMA1的通道1上
	
	/*DMA和ADC使能*/
	DMA_Cmd(DMA1_Channel1, ENABLE);							//DMA1的通道1使能
	ADC_DMACmd(ADC1, ENABLE);								//ADC1触发DMA1的信号使能 开启DMA触发信号
	ADC_Cmd(ADC1, ENABLE);									//ADC1使能
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	/*ADC触发*/
	//ADC连续扫描+DMA循环转运的模式
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
	
	//ADC单次扫描+DMA单次转运的模式  放到主函数while循环里
	/*
	void AD_GetValue(void)
	{
		DMA_Cmd(DMA1_Channel1, DISABLE);					//DMA失能,在写入传输计数器之前,需要DMA暂停工作
		DMA_SetCurrDataCounter(DMA1_Channel1, 4);			//写入传输计数器,指定将要转运的次数
		DMA_Cmd(DMA1_Channel1, ENABLE);						//DMA使能,开始工作

		ADC_SoftwareStartConvCmd(ADC1, ENABLE);	
		
		while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);	//等待DMA工作完成    如果中途出错了,这里是不就卡死了?
		DMA_ClearFlag(DMA1_FLAG_TC1);						//清除工作完成标志位
	}
	*/
	
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while (1)
	{
		OLED_ShowNum(1, 5, AD_Value[0], 4);		//显示转换结果第0个数据
		OLED_ShowNum(2, 5, AD_Value[1], 4);		//显示转换结果第1个数据
		OLED_ShowNum(3, 5, AD_Value[2], 4);		//显示转换结果第2个数据
		OLED_ShowNum(4, 5, AD_Value[3], 4);		//显示转换结果第3个数据
		
		Delay_ms(100);							//延时100ms,手动增加一些转换的间隔时间
	}
}

江科大STM32 下,STM32,stm32

ADC连续扫描+DMA循环转运的模式,这样也可以完成ad多通道转换的功能,可以看到此时硬件外设已经实现了相互配合和高度的自动化,各种操作都是硬件自己完成的,极大的减轻的软件负担,软件什么都不需要做,也不需要进任何中断,硬件自动就把活干完了,另外这里你还可以再加一个外设
比如定时器,ADC用单次扫描,再用定时器去定时触发,这样就是定时器触发ADC,ADC触发DMA
整个过程完全自动,不需要程序手动进行操作,节省软件资源,这就是STM2中硬件自动化的一大特色
各个外设互相连接,互相交织,不再是传统的一个cpu单独控制多个独立的外设这样的星型结构,而是外设之间互相连接,互相合作,形成一个网状结构,这样在完成某些简单且繁琐的工作的时候
就不需要cpu来统一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作,这样不仅可以减轻cpu的负担,还可以大大提高外设的性能,在我们之前的学习中,也经常遇到过这样的设计,比如定时器的输出可以通向ADC、DAC或其他定时器,ADC的触发源可以来自定时器或外部中断,DMA的触发源可以来自ADC、定时器、串口等等,这就是这个STM32外设互相配合工作的特色。

本节课的内容就差不多了,还有一个存储器到外设的情况我们目前还没有讲,比如串口发送一批数据
就可以使用DMA进行存储器到外设的转运,这个就留给大家以后自己去研究了。文章来源地址https://www.toymoban.com/news/detail-795527.html

到了这里,关于江科大STM32 下的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【STM32】学习笔记-江科大

    GPIO(General Purpose Input Output)通用输入输出口可配置为8种输入输出模式引脚电平:0V~3.3V,部分引脚可容忍5V输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平

    2024年02月11日
    浏览(35)
  • 江科大 STM32 标准库

    1.创建一个存放工程的文件夹,自定义重命名STM32Project,工程都存在这个文件夹下,方便管理。 2.打开keil5软件,点击Project→New uVision Project,然后选择我们刚才新建的文件夹,在这里面要再新建一个文件夹,用来存放本次的工程,起名2-1 STM32工程模板,然后点进去,接下来给

    2024年02月21日
    浏览(27)
  • 江科大STM32 IIC

    首先理解同步和异步,同步通信对于时间的要求没有那么高,一般需要一根时钟线。异步对于时钟要求就非常高,比如在发送接收数据的时候不能被中断打断去做其他的事情。因此可以区别串口和iic通信, 串口异步通信 ,没有时钟线。 iic 也是两根通讯线,一根是时钟线,一

    2024年02月19日
    浏览(14)
  • 江科大STM32学习笔记(上)

    基础篇 是到时候我自己找其它视频补充(就比如寄存器影射,时钟树),到时候写在其它文章里。 主篇(外设篇)目前是跟着@江协科技的STM32入门教程-2023版 细致讲解 中文字幕视频来学习的,大家可以边看视频边根据我的笔记做适合自己的笔记; 另外,因为篇幅太长了,我将

    2024年02月05日
    浏览(35)
  • 【江科大】STM32:DMA转运

    直接存储器存取(协助CPU完成数据转运,可以直接访问32位内部存储器,内存SRAM,程序存储器Flash,寄存器等) DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源 12个独立可配置的通道: DMA1(7个通道(C8T6只有这个)), DMA2(

    2024年01月25日
    浏览(12)
  • 江科大stm32之“点灯大师”

    声明:文章中出现的资料多数来自江科大视频资料,本文是我学习stm32的随手笔记 目录 一、准备 二、实现星星点灯 1、接线图 2、原理 3、步骤 (1)使用RCC开启GPIO时钟 (2)使用GPIO_Init函数初始化GPIO (3)用输出或者输入函数控制GPIO口 三、点灯大师 本次学习内容是经典的点

    2024年02月21日
    浏览(32)
  • 【江科大】STM32:中断系统(理论)

    如果没有中断系统,系统就需要不断去查询程序运行是否有异常和异常事件的产生, 比如串口通信,数据没有接收到被覆盖。没有定时器中断,主程序只能靠Delay函数,才能实现定时功能。 有的话,就不用管这些,只需要放心去做自己的事情。有中断就去处理。 当有多个中

    2024年01月23日
    浏览(42)
  • 【stm32】stm32学习笔记(江科大)-详解stm32获取Mpu6050陀螺仪和加速度

    目录 I2C 起始条件: 终止条件:  发送一个字节 接收一个字节  接收发送应答  代码 I2C I2C.C I2C.h Mpu6050 Mpu6050.c Mpu6050.h Mpu6050Reg.h main.c 结果   要想获取Mpu6050陀螺仪和加速度那就需要了解一下Mpu6050。Mpu6050使用的是I2C通讯 先了解一下 起始条件: SCL高电平期间,SDA从高电平切换

    2024年02月16日
    浏览(66)
  • 江科大STM32-3-3,3-4

    一、 图3 N1为可变电阻,阻值可根据环境的光线温度等进行变化,C2是滤波电容,给中间电压输出进行滤波,用来滤出一些干扰保持电压波形稳定。 当N1阻值变小时,下拉作用会增强,中间的AO端电压会拉低。极端情况下,N1阻值为0,AO输出被完全下拉,输出0V;当N1阻值变大,

    2024年03月28日
    浏览(33)
  • 【江科大】STM32:定时器中断

    功能:定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断 HZ 和ms的单位转换:1ms = 1KHZ 1MZ = 1000KHZ 1ms = 1000us 16位计数器、预分频器、自动重装寄存器(记录多少个时钟申请中断)的时基单元,在72MHz计数时钟下可以实现最大59.65s(72/65536/65536 ,再取倒数)的

    2024年01月23日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包