Bootstrap

【STM32】知识点总结

谨以此篇,记录我的stm32学习路线和感悟。

文章包括GPIO,时钟树,中断,定时器,串口,ADC相关部分讲解

我一共接触过两款stm32的单片机,第一款是b站江科大自化协(如今叫江协科技)stm32系列课程中所教的STM32F103C8T6,第二款是野火的STM32F407ZGT6霸天虎开发板。

我认为其中比较重要的知识点为单片机内部的时钟树中断控制器和外设资源中的定时器串口ADC。而其他的知识点比如I2C,SPI等通信协议,电源控制等完全可以在实际项目中的时候去学习。先去把最基础的框架搭建好,再去进行深入的学习。

再搞的差不多之后可以去学习FREERTOS,这是一个实时性操作系统,对于比较复杂的工程可以大大提高代码的逼格(不是),系统工作的实时性。后面又是ARM+Linux,慢慢来吧。

感兴趣也可以学点硬件电路。

( 温馨提示:PC端观看本文观感会好一点 )


OK了,那么好,第一件事,通电............

系统上电启动流程

启动文件的作用

  • 初始化堆栈指针 SP
  • 初始化程序计数器指针 PC
  • 设置堆、栈的大小
  • 设置中断向量表的入口地址     
  • 调用 SystemIni() 函数配置 STM32 的系统时钟
  • 从flash中加载中断向量表
  • 设置 C库的分支入口“__main”(最终用来调用 main 函数)                                                                                                              

  • 调用 SystemIni() 函数配置 STM32 的系统时钟
  • 从flash中加载中断向量表
  • 设置 C库的分支入口“__main”(最终用来调用 main 函数)

程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:

__scatterload():负责把RW/RO输出段从装载域地址复制到运行域地址,并完成了ZI运行域的初始化工作。

__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向main()函数

在系统上电的时候第一个执行的是启动文件里面由汇编编写的复位函数 Reset_Handler,复位函数的最后会调用 C库函数__main。__main 函数的主要工作是初始化系统的堆和栈,最后调用 C 中 的main函数,从而去到C的世界。


目录

GPIO

CortexM4内核的F4系列的GPIO工作模式及配置

1.引脚选择和模式设置:

2.输出模式配置:

3.数据读取和写入:

结构体成员配置介绍

GPIO_Mode:有四种模式选择分别是:输入模式、输出模式、复用模式以及模拟模式

GPIO_Speed:就是配置引脚的响应速度,有四个挡可以选择。

GPIO_OType:有两个可选

GPIO_PuPd:有上拉、下拉、和不上拉也不下拉  三种模式

接下来介绍cortexm3内核的STM32F103系列的GPIO相关部分

在Cortex-M3里,对于GPIO的配置种类有8种

4种输入模式:

输入浮空(GPIO_Mode_IN_FLOATING)

输入上拉  (GPIO_Mode_IPU) 

输入下拉  (GPIO_Mode_IPD)

模拟输入  (GPIO_Mode_AIN) 

4种输出模式:

开漏输出  (GPIO_Mode_Out_OD)

开漏复用输出  (GPIO_Mode_AF_OD)

推挽式输出  (GPIO_Mode_Out_PP)

推挽式复用输出  (GPIO_Mode_AF_PP)

时钟树

STM32的5个时钟源

STM32F407ZGT6时钟树

时钟配置:

2、初始化之后的状态

STM32F103C8T6时钟树

SysTick(系统定时器)的使用方法

NVIC                       

NVIC结构体

结论:

外部中断

中断资源

外部中断结构体配置

串口

串口资源

串口结构体代码示例

定时器

 STM32F407的 定时器资源         

定时器的溢出时间计算:

STM32F103的 定时器资源         

定时器功能

PWM相关知识点

定时器结构体初始化

输出比较结构体

最后

GPIO

先来简单说一下GPIO。

GPIO通用输入输出端口的简称,是STM32 可控制的引脚,STM32 芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能

接下来介绍F407和F103的GPIO部分

STM32F103的GPIO结构体初始化成员与F407有所不同

主要区别为103把引脚的输入输出模式都用一个成员GPIO_Mode来表示,而407中把这个结构体成员拆分为GPIO_Mode,GPIO_OType,GPIO_PuPd 三个成员。


CortexM4内核的F4系列的GPIO工作模式及配置

在STM32F407中,STM32F407ZGT6

  • 一共有7组IO口,GPIOA到GPIOG。
  • 每组IO口有16个IO
  • 一共16X7=112个IO
  • 外加2个PH0和PH1

一共114个IO口

F407系列微控制器中的GPIO(通用输入输出)是一种重要的外设,用于与外部设备进行数字信号的输入和输出。GPIO以寄存器的方式进行配置和控制,下面是在STM32F407中使用GPIO的一般步骤和一个详细示例:

1.引脚选择和模式设置:

  • 首先,选择一个可用的GPIO引脚,每个GPIO引脚都有一个唯一的标识符(如GPIOA、GPIOB等)。
  • 使用RCC寄存器使能对应GPIO端口的时钟。例如,要使用GPIOA,需要设置RCC_AHB1ENR寄存器中的GPIOAEN位为1,以使能GPIOA端口的时钟。
  • 根据需求,配置GPIO引脚的工作模式(输入、输出、复用,模拟)。这可以通过修改GPIOx_MODER寄存器中的位来完成。

2.输出模式配置:

  • 如果将GPIO引脚配置为输出模式,可以设置GPIOx_OTYPER寄存器中的对应位来选择推挽输出(Push-pull)或开漏输出(Open-drain)。
  • 通过设置GPIOx_OSPEEDR寄存器中的位,可以选择GPIO引脚的输出速度。
  • 通过设置GPIOx_PUPDR寄存器中的位,可以配置引脚的上拉或下拉电阻。
  • 输入模式配置:
  • 如果将GPIO引脚配置为输入模式,可以设置GPIOx_PUPDR寄存器中的位,选择引脚的上拉或下拉电阻。
  • 若要使用外部中断或事件触发,还需要配置对应的GPIO寄存器,如GPIOx_EXTICRn、GPIOx_IMR和GPIOx_RTSR等。

3.数据读取和写入:

  • 要从GPIO引脚读取输入数据,可以通过读取GPIOx_IDR寄存器中的位来获取引脚状态。
  • 要向GPIO引脚写入输出数据,可以通过设置GPIOx_BSRR和GPIOx_BRR寄存器中的位,分别对应于设置引脚为高电平和低电平。
     

                    

此图展示的是F407的GPIO结构体成员,包括引脚选择,配置输入或输出,配置引脚速率,配置输入和输出模式

结构体成员配置介绍

GPIO_Mode:有四种模式选择分别是:输入模式、输出模式、复用模式以及模拟模式

  • 输入模式:就是外部高低电平输入到芯片内去,如使用按键实现输入高低电平以实现具体功能。
  • 输出模式:就是内部芯片向外边输出电平,如将led灯的阳极接芯片引脚,引脚输出高电平时灯亮。
  • 复用功能:也叫第二功能,顾名思义就是把引脚设置成具有其他功能的模式,如使用串口USART通信时就要设置端口的复用模式。
  • 模拟模式:多用于ADC功能

GPIO_Speed:就是配置引脚的响应速度,有四个挡可以选择。

GPIO_OType:有两个可选

分别是推挽和开漏:推挽具有一定的驱动能力,能简单驱动外设设备,而开漏是没有驱动能力的,需加上拉电阻才能输出高电平。

GPIO_PuPd:有上拉、下拉、和不上拉也不下拉  三种模式

  • 当外部有效电平为高电平时,无效电平不确定就可以选择下拉模式把电平拉低确定引脚在无效电平时的电平;
  • 当外部有效电平为低电平时,无效电平不确定就可以选择上拉模式把电平拉高确定引脚在无效电平时的电平;
  • 当引脚的有效和无效时的电平都被确定时,就可以选择不上拉也不下拉模式。

以上就是对stm32f4系列各个结构体成员以及配置模式的介绍

以配置STM32F407GPIOF6引脚为例,

RCC_AHB1PeriphClockCmd (RCC_AHB1Periph_GPIOF, ENABLE); 

GPIO_InitTypeDef GPIO_InitStructure;

/*选择要控制的GPIO引脚*/	

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;	//选择GPIOF引脚 

/*设置引脚模式为输出模式*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;  //选择GPIOF引脚  
    
/*设置引脚的输出类型为推挽输出*/
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;  //推挽输出类型  (输出类型寄存器)
    
/*设置引脚为上拉模式*/
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;   //推挽输出类型  (输出类型寄存器)

/*设置引脚速率为2MHz */   
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;  //输出速度100MHz  (输出速度寄存器)

 //结构体成员数据全部传入配置函数

/*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);	

补充一句,在STM32F407中把引脚模式配置为复用功能的时候,需要使用GPIO_PinAFConfig()函数将引脚与相应的功能连接上,否则不好使。这是我在调试一个陀螺仪的时候注意到的,我以为和103一样,都默认初始化引脚之后就不用管了

 /* 连接 PXx 到 USARTx_Tx*/
  GPIO_PinAFConfig(GPIOA,GPIO_PinSource3, GPIO_AF_USART2);

  /*  连接 PXx 到 USARTx__Rx*/
  GPIO_PinAFConfig(GPIOA,GPIO_PinSource2, GPIO_AF_USART2);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);    
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

STM32F103 微控制器提供了多个 GPIO 引脚,用于与外部设备进行数字信号的输入和输出。每个引脚都可以通过配置来灵活地设置为输入或输出。作为输入引脚时,GPIO 可以读取外部设备的状态,如按钮的按下状态或传感器的测量值。作为输出引脚时,GPIO 可以控制外部设备的操作,如驱动 LED 灯的亮灭。

接下来介绍cortexm3内核的STM32F103系列的GPIO相关部分

typedef struct
{
  uint16_t GPIO_Pin;
    uint16_t GPIO_Speed;
    uint16_t GPIO_Mode;
}GPIO_InitTypeDef;

Cortex-M3里,对于GPIO的配置种类有8种


4种输入模式

  • 输入浮空(GPIO_Mode_IN_FLOATING)

可以做KEY识别,RX1    浮空输入状态下,IO的电平状态是不确定的,完全由外部输入决定,如果在该引脚悬空的情况下,读取该端口的电平是不确定的

  • 输入上拉  (GPIO_Mode_IPU) 

IO内部上拉电阻输入.   将一个输入端口连接至高电平信号的电路拓扑中。这种情况,在外部没有将该输入口拉向地线时,输入端口处于高电平状态。

  • 输入下拉  (GPIO_Mode_IPD)

  IO内部下拉电阻输入。  将一个输入端口连接至低电平信号的电路拓扑中,一般为地。这种情况,在外部没有将该输入口拉向高电平时,输入端口处于低电平状态。

  • 模拟输入  (GPIO_Mode_AIN) 

 应用ADC模拟输入,或者低功耗下省电。  将连续的物理信号转换为数字信号(即模拟信号),将其送入数字电路中。通常,模拟输入要采用模数转换器(ADC)等外部元件来实现。

4种输出模式

  • 开漏输出  (GPIO_Mode_Out_OD)

输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。适合于做电流型的驱动,其吸收电流的能力相对强(一般20mA以内)。

在开漏输出模式下,一个输出端口可以踢动低电平先后,但是无法提供高电平信号。

IO输出0接GND,IO输出1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为1时,IO口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样IO口也就可以由外部电路改变为低电平或不变。

可以读IO输入电平变化,实现C51的IO双向功能。

开漏输出和推挽输出的区别最普遍的说法就是开漏输出无法真正输出高电平,即高电平时没有驱动能力,需要借助外部上拉电阻完成对外驱动。

开漏输出的这一特性一个明显的优势就是可以很方便的调节输出的电平,因为输出电平完全由上拉电阻连接的电源电平决定。所以在需要进行电平转换的地方,非常适合使用开漏输出。
开漏输出的这一特性另一个好处在于可以实现"线与"功能,所谓的"线与"指的是多个信号线直接连接在一起,只有当所有信号全部为高电平时,合在一起的总线为高电平;只要有任意一个或者多个信号为低电平,则总线为低电平。而推挽输出就不行,如果高电平和低电平连在一起,会出现电流

开漏输出应用:

模拟I2C使用开漏输出_OUT_OD,接上拉电阻,能够正确输出0和1;

倒灌,损坏器件。所以总线一般会使用开漏输出.

  • 开漏复用输出  (GPIO_Mode_AF_OD)

GPIO_AF_OD——片内外设功能(TX1,MOSI,MISO.SCK.SS)。

  • 推挽式输出  (GPIO_Mode_Out_PP)

推挽电路是两个参数相同的三极管或MOSFET,以推挽方式存在于电路中

各负责正负半周的波形放大任务

电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。

输出既可以向负载灌电流,也可以从负载抽取电流。

推拉式输出级既提高电路的负载能力,又提高开关速度。

GPIO_OUT_PP ——IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的。

推挽输出结构是由两个MOS或者三极管受到互补控制的信号控制,两个管子时钟一个在导通,一个在截止。

推挽输出的最大特点是可以真正的输出高电平和低电平,在两种电平下都具有驱动能力。
所谓的驱动能力,就是指输出电流的能力。对于驱动大负载(即负载内阻越小,负载越大)时,例如IO输出为5V,驱动的负载内阻为10ohm,于是根据欧姆定律可以正常情况下负载上的电流为0.5A(推算出功率为2.5W)。一般的IO不可能输出这么大的电流。于是造成的结果就是输出电压会被拉下来,达不到标称的5V。
推挽输出高低电平的电流都能达到几十mA。
当输出引脚需要高电平时,PMOS晶体管被打开,输出端口拉向VCC。当输出引脚需要低电平时,NMOS晶体管被打开,将输出端口拉向GND。
推挽输出的缺点是,如果当两个推挽输出结构相连在一起,一个输出高电平,另一个输出低电平,电流会从第一个引脚的VCC通过上端MOS再经过第二个引脚的下端MOS直接流向GND,也就是会发生短路,进而可能造成端口的损害。这也是为什么推挽输出不能实现" 线与"的原因。

推挽输出在输出的时候是通过单片机内部的电压,所以他的电压是不能改变的。
一般情况下,使用推挽输出。
注意:推挽状态下,是可以读取IO口的电平状态的。

  • 推挽式复用输出  (GPIO_Mode_AF_PP)

GPIO_AF_PP ——片内外设功能(I2C的SCL,SDA)。

以初始化GPIOA6,A7两个引脚为例,F103

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_SetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7);


时钟树

单片机工作的心脏,梦开始的地方!

时钟树是STM32为了实现低功耗而设计的功能完善构成复杂的时钟系统,它可以根据不同的外设和应用场合,选择合适的时钟源和频率,以提高系统性能和降低功耗。

STM32的5个时钟源

  • 1、HSE:高速外部时钟
  • 高速外部时钟信号 (HSE) 有 2 个时钟源:(1) HSE 外部晶振/陶瓷谐振器(晶振频率取决于外部晶振材料的频率,频率范围为4MHz~26MHz)、(2) HSE 外部用户时钟
  • 2、HSI:高速内部时钟
  • HSI 时钟信号由内部 RC 振荡器生成,可直接用作系统时钟,或者用作 PLL 输入。(F407内部HSI时钟频率为 16 MHZ,F103内部HSI时钟频率为 8 MHZ)
  • 3、LSE:低速外部时钟
  • LSE 晶振是 32.768 kHz 低速外部 (LSE) 晶振或陶瓷谐振器,可作为实时时钟外设 (RTC) 的时钟源来提供时钟/日历或其它定时功能,具有功耗低且精度高的优点。
  • 4、LSI:低速内部时钟
  • LSI RC 可作为低功耗时钟源在停机和待机模式下保持运行,供独立看门狗 (IWDG) 和自动唤醒单元 (AWU) 使用。时钟频率在 32 kHz 左右. LSI可以直接作为RTC时钟或者提供给MCO
  • 5、PLL:倍频输出时钟

任何一个外设在使用之前,必须首先使能其相应的时钟。

一般情况下通过高速外部晶振(HSE)产生时钟信号,经过PLL锁相环先经过分频因子分频,再经过倍频因子倍频 ,之后再经过一个分频因子分频。构成锁相环(PLL)时钟,最终使用PLL时钟作为系统时钟。


STM32F407ZGT6时钟树

以STM32F407为例,霸天虎开发板的外部晶振为25MHZ,在时钟树图里,HSE经过分频因子 M=25 (M的值取决于外部晶振的频率,通过配置M的值将外部晶振分频为1MHZ,如果外部晶振为8MHZ,则将M配置为8) 将25MHZ分频为1MHZ,经倍频因子  N=336  倍频为336MHZ,再经过分频因子 P=2 分频为168MHZ,最终锁相环时钟(PLL)作为系统时钟源,AHB总线的最大时钟为168MHZ,APB2时钟线为84MHZ,APB1时钟线为42MHZ。

主 PLL(PLL)由 HSE 或者 HSI 提供时钟信号(通过选择器),并具有两个不同的输出时钟第一个输出 PLLP 用于生成高速的系统时钟(最高 168MHz)第二个输出 PLLQ 用于生成 USB OTG FS 的时钟(48MHz),随机数发生器的时钟和 SDIO时钟。
专用 PLL(PLLI2S)用于生成精确时钟,从而在 I2S 接口实现高品质音频性能。

时钟配置:

1、对于HSI、HSE、PLL等时钟源配置,没有专门的固件库函数,可以通过SystemInit函数来操作配置。该函数具体实现过程如下(也可以根据寄存器自己操作):
(1)、系统复位之后,先调用SystemInit函数,该函数的作用是初始化系统时钟,设置PLL等
(2)、打开HSE,等待其稳定,
(3)、设置AHB、APBx、等分频系数
(4)、设置HSE为主PLL时钟源,并且配置主PLL里面的分频和倍频参数,然后产生PLLCLK并将其使能,并选择系统时钟(SYSCLC)为PLLCLK

2、初始化之后的状态


SYSCLK(系统时钟) =168MHZ
AHB总线时钟(HCLK=SYSCLK)=168MHZ
APB1总线时钟(PCLK1=SYSCLK/4)=42MHZ
APB2总线时钟(PCLK2=SYSCLK/2)=84MHZ
PLL主时钟 =168MHZ


系统复位后先调用的是SystemInit函数,其次是main函数,这一点在启动文件里面写了。


STM32F103C8T6时钟树

STM32F103的时钟树由四个时钟源、一个总线矩阵、一个PLL(锁相环)倍频器、一个CSS(时钟安全系统)检测器、一个USB预分频器、一个RTC预分频器、一个MCO输出选择器等部分组成

103外接晶振一般为4~16MHZ

总线矩阵:由多层AHB总线矩阵构成,用于连接Cortex-M内核、DMA控制器、外设和存储器。总线矩阵包括以下几条总线:

  • ICode总线:用于访问存储空间里指令的总线;
  • DCode总线:用于访问存储空间里数据的总线;
  • System总线:用于访问指令、数据以及调试模块接口;
  • DMA总线:用于内存与外设之间的数据传输;
  • AHB总线:高性能总线,连接CPU、内存、DMA等高速设备,最高频率可达72MHz;
  • APB1总线:低速外设总线,连接DAC、UART等外设,最高频率可达36MHz;
  • APB2总线:高速外设总线,连接ADC、GPIO等外设,最高频率可达72MHz;
  • PLL(锁相环)倍频器:用于将输入的时钟信号进行倍频,以提高系统时钟的频率。PLL的输入时钟源可以是HSI/2或者HSE,倍频系数可以是2~16之间的整数。PLL的输出时钟可以作为系统时钟或者提供给USB预分频器或者MCO.

PLLXTPRE是一个分频器,它可以选择HSE时钟的一分频或二分频作为PLL的输入时钟源;
PLLSRC是一个选择器,它可以选择HSI时钟的二分频或HSE时钟(经过PLLXTPRE分频)作为PLL的输入时钟源;
PLLMUL是一个倍频器,它可以将PLL的输入时钟源进行2~16倍的倍频,得到PLL的输出时钟源;
prescalear为预分频。


 


SysTick(系统定时器)的使用方法


简介:该定时器(也称“滴答计时器”)寄存器,24位,只能递减,该寄存器存在于内核,嵌套在NVIC中,所有的Cortex-M内核单片机都具有该定时器。SysTick_Config(uint32_t ticks)初始化函数位于Core_cm4.h中,计数器每计数一次的时间为 1/SYSCLK 秒,一般我们设置系统时钟 SYSCLK 等于 168M(F103一般设置为72M)。调用Systick定时器,只需要调用SysTick_Config(uint32_t ticks)函数,向函数中写入初始值,如果时钟源选择的是AHB=168MHZ,那么,每递减一次的时间就是1/168M,需要多少时间就设多大初始值。当递减到零时会产生异常(中断)请求。   


代码讲解(通常做delay函数):

系统初始化之后可以通过变量SystemCoreClock获取系统变量,如果SYSCLK(系统时钟)=168MHZ,  那么变量等于168000000,那么 SysTick_Config(SystemCoreClock / 1000000)  就代表每计数 168M  / 1000000 次就产生中断,以F407为例,如若选择AHB168M做时钟源,每计数一次的时间为1/168M秒,那么计数(168000000)/  1000000 次之后,也就是 (168000000)/  1000000 * 1/168M =1/1000000秒,也就是1us就产生一次中断,这样就实现了1us的延时,同理,若修改1000000的值为1000,那么就是160M/1000  *  1/168M 秒,也就是1ms.F103延时计算原理和F407一样,无非就是时钟源频率不同罢了。

                       

static __IO u32 TimingDelay;

void SysTick_Init(void)
{
	/* SystemFrequency / 1000    1ms中断一次
	 * SystemFrequency / 100000	 10us中断一次
	 * SystemFrequency / 1000000 1usÖ中断一次
	 */
	if (SysTick_Config(SystemCoreClock / 1000000))
	{ 
		
		while (1);
	}
}

void Delay_us(__IO u32 nTime)
{ 
	TimingDelay = nTime;	

	while(TimingDelay != 0);
}

void TimingDelay_Decrement(void)
{
	if (TimingDelay != 0x00)
	{ 
		TimingDelay--;
	}
}

void SysTick_Handler(void)
{
	TimingDelay_Decrement();
}

NVIC                       

NVIC的全称是Nested vectoredinterrupt controller,即嵌套向量中断控制器。
对于M3和M4内核的MCU,每个中断的优先级都是用寄存器中的8位来设置的。8位的话就可以设置2^8 =256级中断,实际中用不了这么多,所以芯片厂商根据自己生产的芯片做出了调整。比如ST的STM32F1xx和F4xx只使用了这个8位中的高四位[7:4],低四位取零,这样2^4=16,只能表示16级中断嵌套。
对于这个NVIC,有个重要的知识点就是优先级分组,抢占优先级和子优先级,下面就以STM32为例进行介绍,STM32F1xx和F4xx都是只使用了这个8位寄存器的高四位[7:4]。

具有高抢占式优先级的中断可以在具有低抢占式优先级的中断服务程序执行过程中被响应,即中断嵌套,或者说高抢占式优先级的中断可以抢占低抢占式优先级的中断的执行。在抢占式优先级相同的情况下,有几个子优先级不同的中断同时到来,那么高子优先级的中断优先被响应。

在抢占式优先级相同的情况下,如果有低子优先级中断正在执行,高子优先级的中断要等待已被响应的低子优先级中断执行结束后才能得到响应,即子优先级不支持中断嵌套。Reset、NMI、Hard Fault 优先级为负数,高于普通中断优先级,且优先级不可配置。

对于初学者还有一个比较纠结的问题就是系统中断(比如:PendSV,SVC,SysTick)是不是一定比外部中断(比如SPI,USART)要高,答案:不是的,它们是在同一个NVIC下面设置的。


NVIC结构体

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_x); 一个工程里面只进行一次分组就好

NVIC_InitStructure.NVIC_IRQChannel = 中断源; //选择中断源 ,比如串口中断,定时器中断或者外部中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //设置抢占优先级为2 具体数字为几需根据中断优先级分组进行设置,如果分组为0,那么设置抢占优先级为1是无效的,因为0组不包括抢占优先级只包括16个(4bit)响应优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //设置响应优先级为2 
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能中断; 
NVIC_Init(&NVIC_InitStructure); //初始化以上参数;

配置完中断需要写出对应中断源的中断函数,且函数名必须与中断向量表里的函数名保持一致。

要注意的几点是:

1)如果指定的抢占式优先级别或响应优先级别超出了选定的优先级分组所限定的范围,将可能得到意想不到的结果;

2)抢占式优先级别相同的中断源之间没有嵌套关系;

3)如果某个中断源被指定为某个抢占式优先级别,又没有其它中断源处于同一个抢占式优先级别,则可以为这个中断源指定任意有效的响应优先级别

结论:

1)抢占优先级越小,优先级越高;相同抢占优先级的中断不能嵌套;

2)相同抢占优先级N个中断发生时,响应优先级越小的中断首先执行(不能嵌套),如果响应优先级也均相同,则根据各中断对应向量表的位置来确定,向量表中越靠前的中断先响应

外部中断

中断资源

STM32F407 的中断控制器支持 22 个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。STM32F407 的 22 个外部中断为:

  • EXTI 线 0~15:对应外部 IO 口的输入中断。
  • EXTI 线 16:连接到 PVD 输出。
  • EXTI 线 17:连接到 RTC 闹钟事件。
  • EXTI 线 18:连接到 USB OTG FS 唤醒事件。
  • EXTI 线 19:连接到以太网唤醒事件。
  • EXTI 线 20:连接到 USB OTG HS(在 FS 中配置)唤醒事件。
  • EXTI 线 21:连接到 RTC 入侵和时间戳事件。
  • EXTI 线 22:连接到 RTC 唤醒事件。

 STM32F103 的中断控制器支持 19 个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。 STM32F103的 19 个外部中断为:

  • EXTI 线 0~15:对应外部 IO 口的输入中断。
  • EXTI 线 16:连接到 PVD 输出。
  • EXTI 线 17:连接到 RTC 闹钟事件。
  • EXTI 线 18:连接到 USB 唤醒事件。

外部中断结构体配置

 接下来就来到了配置的环节,我们将一步一步配置好我们的外部中断函数,就让我们开始吧!(具体就不一一介绍怎么编写的了,其实就是复制粘贴,找到相应的参数就好了) 

  • 使能 IO 口时钟,初始化 IO 口为输入
  • 开启 SYSCFG 时钟,设置 IO 口与中断线的映射关系。
  • 初始化线上中断,设置触发条件等。
  • 配置中断分组(NVIC),并使能中断。
  • 编写中断服务函数。

所以,一个外部中断配置过程需要初始化GPIO结构体,NVIC结构体,EXTI外部中断结构体

其中外部中断初始化结构体的成员为

EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Linex; //选择外部中断线
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //设置触发模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //设置上升沿触发或者下降沿触发或者双边沿触发,以按键触发外部中断为例,按键一头接地另一头接GPIO,当按键按下的时候电位升高此时为上升沿,当按键松开之后点位降低,此时为下降沿。如果此处配置为上升沿,那么只有在产生上升沿的时候才会触发外部中断,也就是按键按下的时候触发一次松开按键不触发。如果此处配置为下降沿,那么只有在产生下降沿的时候才会触发外部中断,也就是按键按下的时候不触发松开按键的时候才会触发,如果配置为双边沿,那么按键按下和松开的时候都会触发外部中断
EXTI_InitStructure.EXTI_LineCmd = ENABLE;//使能

同样的,配置完NVIC之后需要写出外部中断函数,以外部中断配置按键控制LED举例

// 外部中断4服务程序
void EXTI4_IRQHandler(void)
{
    delay_ms(10);    //消抖
    if(KEY0==0)     // 下降沿触发
    {                 
        LED0 =! LED0;    
        LED1 =! LED1;    
    }         
     EXTI_ClearITPendingBit(EXTI_Line4);//清除LINE4上的中断标志位  
}

串口

串口资源

不同型号的芯片所具有的串口资源不一样,STM32F103C8T6只有3个串口,而STM32F407具有六个串口

APB2  :   USART1
APB1  :   USART2、USART3

在这里引用一下江科大老师的PPT,简单介绍下各种通信协议和对串口通信的简介,在STM32F407中的串口资源为

两个进行串口通信的设备需要共地

串口作为工具,了解其工作原理,并会修改波特率等代码参数就行了


串口结构体代码示例

下面介绍一个STM32F407串口通用代码,与F103的区别主要为GPIO初始化结构体成员有变化,以及两个系列板子的串口资源有所不同以外,其他都一样

代码中使用了串口接收中断,有中断就要配置NVIC结构体,所以代码配置了GPIO,NVIC,USART结构体,基本套路为打开相应外设时钟,初始化各个结构体成员,通过修改结构体成员来配置为我需要的工作模式,比如修改波特率

#include "usart.h"

uint8_t USARTx_RxData;
uint8_t USARTx_RxFlag;
 /**
  * @brief  USART GPIO 配置,工作模式配置。115200 8-N-1 
  * @param  无
  * @retval 无
  */
void USARTx_Config(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  USART_InitTypeDef USART_InitStructure;
		
  RCC_AHB1PeriphClockCmd(USARTx_RX_GPIO_CLK|USARTx_TX_GPIO_CLK,ENABLE);

  /* 使能 USART 时钟 */
  USARTx_CLOCKCMD(USARTx_CLK, ENABLE);
  
  /* GPIO初始化 */
  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;  
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  
  /* 配置Tx引脚为复用功能  */
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin =  USARTx_TX_PIN  ;  
  GPIO_Init(USARTx_TX_GPIO_PORT, &GPIO_InitStructure);

  /* 配置Rx引脚为复用功能 */
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin =  USARTx_RX_PIN;
  GPIO_Init(USARTx_RX_GPIO_PORT, &GPIO_InitStructure);
  
 /* 连接 PXx 到 USARTx_Tx*/
  GPIO_PinAFConfig(USARTx_RX_GPIO_PORT,USARTx_RX_SOURCE,USARTx_RX_AF);

  /*  连接 PXx 到 USARTx__Rx*/
  GPIO_PinAFConfig(USARTx_TX_GPIO_PORT,USARTx_TX_SOURCE,USARTx_TX_AF);
	
	
  NVIC_InitTypeDef NVIC_InitStructure;
  
  /* 嵌套向量中断控制器组选择 */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  
  /* 配置USART为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  /* 抢断优先级为1 */
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  /* 子优先级为1 */
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  /* 使能中断 */
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  /* 初始化配置NVIC */
  NVIC_Init(&NVIC_InitStructure);


  /* 配置串DEBUG_USART 模式 */
  /* 波特率设置:DEBUG_USART_BAUDRATE */
  USART_InitStructure.USART_BaudRate = USARTx_BAUDRATE;
  /* 字长(数据位+校验位):8 */
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;	
  /* 停止位:1个停止位 */
  USART_InitStructure.USART_StopBits = USART_StopBits_1;
  /* 校验位选择:无校验 */  
	USART_InitStructure.USART_Parity = USART_Parity_No;
  /* 硬件流控制:不使用硬件流 */
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  /* USART模式控制:同时使能接收和发送 */
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  /* 完成USART初始化配置 */
  USART_Init(USARTx, &USART_InitStructure); 
	
  /* 使能串口 */
  USART_Cmd(USARTx, ENABLE);
}

///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
		/* 发送一个字节数据到串口 */
		USART_SendData(USARTx, (uint8_t) ch);
		
		/* 等待发送完毕 */
		while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);		
	
		return (ch);
}

void USARTx_SendByte(uint8_t Byte)//发送单个字节
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}

void USARTx_SendArray(uint8_t *Array,uint16_t Length)//
{
	uint16_t i;
	for(i=0;i<Length;i++)
	{
		USARTx_SendByte(Array[i]);
	}
}

void USARTx_SendString(char *String)//发送字符串
{
	uint8_t i;
	for (i =0; String[i] != '\0';i ++)
	{
		USARTx_SendByte(String[i]);
	}
}

uint32_t USARTx_Pow(uint32_t X,uint32_t Y)
{
	uint32_t Result = 1;
	while(Y--)
	{
		Result *=X;
	}
	return Result;
}

void USARTx_SendNumber(uint32_t Number, uint8_t Length)//发送数字
{
	uint8_t i;
	for (i=0;i<Length;i++)
	{
		USARTx_SendByte(Number /USARTx_Pow(10,Length-i-1)%10+'0');
	}
}

uint8_t USARTx_GetRxFlag(void) 
{
	if (USARTx_RxFlag == 1)
	{
		USARTx_RxFlag = 0;
		return 1;
	}
	return 0;
}

uint8_t USARTx_GetRxData(void)//返回接收数据
{
	return USARTx_RxData;
}

void USARTx_IRQHandler(void)//串口接收中断
{
	if(USART_GetFlagStatus(USARTx,USART_IT_RXNE)==SET)
	{
		USARTx_RxData = USART_ReceiveData(USARTx);
		USARTx_RxFlag=1;
		USART_ClearITPendingBit(USARTx,USART_IT_RXNE);//清除中断标志位
	}
}


/*********************************************END OF FILE**********************/
#ifndef __USART_H
#define	__USART_H

#include "stm32f4xx.h"
#include <stdio.h>


//引脚定义
/*******************************************************/
#define USARTx                             USART1

/* 不同的串口挂载的总线不一样,时钟使能函数也不一样,移植时要注意 
 * 串口1和6是      RCC_APB2PeriphClockCmd
 * 串口2/3/4/5/7是 RCC_APB1PeriphClockCmd
 */
#define USARTx_CLK                         RCC_APB2Periph_USART1
#define USARTx_CLOCKCMD                    RCC_APB2PeriphClockCmd
#define USARTx_BAUDRATE                    115200  //串口波特率

#define USARTx_RX_GPIO_PORT                GPIOA
#define USARTx_RX_GPIO_CLK                 RCC_AHB1Periph_GPIOA
#define USARTx_RX_PIN                      GPIO_Pin_10
#define USARTx_RX_AF                       GPIO_AF_USART1
#define USARTx_RX_SOURCE                   GPIO_PinSource10

#define USARTx_TX_GPIO_PORT                GPIOA
#define USARTx_TX_GPIO_CLK                 RCC_AHB1Periph_GPIOA
#define USARTx_TX_PIN                      GPIO_Pin_9
#define USARTx_TX_AF                       GPIO_AF_USART1
#define USARTx_TX_SOURCE                   GPIO_PinSource9

/************************************************************/

void USARTx_Config(void);
void USARTx_SendByte(uint8_t Byte);
void USARTx_SendArray(uint8_t *Array,uint16_t Length);
void USARTx_SendString(char *String);
void USARTx_SendNumber(uint32_t Number, uint8_t Length);
uint8_t USARTx_GetRxData(void);
uint8_t USARTx_GetRxFlag(void);

#endif /* __USART_H */

移植修改时只需要修改宏定义里的串口号以及其相应的GPIO口,波特率就OK了


定时器

定时器分为三种,这三种定时器都能进行最基本的定时中断。

之后通用定时器可以进行输入捕获和输出比较的功能,常用的也就是输出PWM波,和测量占空比以及配置编码器。

高级控制定时器比通用定时器增加了可编程死区互补输出、重复计数 器、带刹车 (断路) 功能,这些功能都是针对工业电机控制方面。

 STM32F407的 定时器资源         

定时器的溢出时间计算:

Tout(溢出时间)=(ARR+1)(PSC+1)/Tclk

ARR:自动重装载寄存器,用于装载计数器

PSC:PSC预分频器(分频范围1~65535)

Tclk:外设时钟周期,可以在STM32F407的芯片手册中找到时钟框图

(备注:ARR是代表计数值,而外设时钟经过分频之后,为ARR提供计数时钟,【即每个时钟来,ARR就加1】)


STM32F103的 定时器资源         

STM32F103系列的单片机一共有11个定时器,其中:

  • 2个高级定时器
  • 4个普通定时器
  • 2个基本定时器
  • 2个看门狗定时器
  • 1个系统嘀嗒定时器


除去看门狗定时器和系统滴答定时器的八个定时器列表;

8个定时器分成3个组;
TIM1和TIM8是高级定时器
TIM2-TIM5是通用定时器
TIM6和TIM7是基本的定时器
这8个定时器都是16位的,它们的计数器的类型除了基本定时器TIM6和TIM7都支持向上,向下,向上/向下这3种计数模式


定时器功能


计数器三种计数模式
向上计数模式:从0开始,计到arr预设值,产生溢出事件,返回重新计时
向下计数模式:从arr预设值开始,计到0,产生溢出事件,返回重新计时
中央对齐模式:从0开始向上计数,计到arr产生溢出事件,然后向下计数,计数到1以后,又产生溢出,然后再从0开始向上计数。(此种技术方法也可叫向上/向下计数)

基本定时器(TIM6,TIM7)的主要功能:
只有最基本的定时功能,基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动,可产生定时中断

通用定时器(TIM2~TIM5)的主要功能:
除了基本的定时器的功能外,还具有测量输入信号的脉冲长度( 输入捕获) 或者产生输出波形( 输出比较和PWM)

高级定时器(TIM1,TIM8)的主要功能:
高级定时器不但具有基本,通用定时器的所有的功能,还具有控制交直流电动机所有的功能,你比如它可以输出6路互补带死区的信号,刹车功能等等



定时器时钟原理

通用定时期内部时钟的产生:


从截图可以看到通用定时器(TIM2-7)的时钟不是直接来自APB1,而是通过APB1的预分频器以后才到达定时器模块。
当APB1的预分频器系数为1时,这个倍频器就不起作用了,定时器的时钟频率等于APB1的频率;
当APB1的预分频系数为其它数值(即预分频系数为2、4、8或16)时,这个倍频器起作用,定时器的时钟频率等于APB1时钟频率的两倍。

自动装在寄存器arr值的计算:
Tout= ((arr+1)*(psc+1))/Tclk;
Tclk:TIM3的输入时钟频率(单位为Mhz)。
Tout:TIM3溢出时间(单位为us)。
计时1S,输入时钟频率为72MHz,加入PSC预分频器的值为35999,那么:
((1+psc )/72M)*(1+arr )=((1+35999)/72M)*(1+arr)=1秒
则可计算得出自动窗装载寄存器arr=1999


PWM相关知识点


通用定时器PWM工作原理

以PWM模式2,定时器3向上计数,有效电平是高电平,定时器3的第3个PWM通道为例:



定时器3的第3个PWM通道对应是PB0这引脚,三角顶点的值就是TIM3_ARR寄存器的值,上图这条红线的值就TIM3_CCR3。


当定时器3的计数器(TIM3_CNT)刚开始计数的时候是小于捕获/比较寄存器(TIM3_CCR3)的值,此时PB0输出低电平,随着计数器(TIM3_CNT)值慢慢的增加,
当计数器(TIM3_CNT)大于捕获/比较寄存器(TIM3_CCR3)的值时,这时PB0电平就会翻转,输出高电平,计数器(TIM3_CNT)的值继续增加,


当TIM3_CNT=TIM3_ARR的值时,TIM3_CNT重新回到0继续计数,PB0电平翻转,输出低电平,此时一个完整的PWM信号就诞生了。


PWM输出模式;


STM32的PWM输出有两种模式:
模式1和模式2,由TIMx_CCMRx寄存器中的OCxM位确定的(“110”为模式1,“111”为模式2)。区别如下:
110:PWM模式1,在向上计数时,一旦TIMx_CNT
在向下计数时,一旦TIMx_CNT>TIMx_CCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。
111:PWM模式2-在向上计数时,一旦TIMx_CNTTIMx_CCR1时通道1为有效电平,否则为无效电平。
由以上可知:
模式1和模式2正好互补,互为相反,所以在运用起来差别也并不太大。而从计数模式上来看,PWM也和TIMx在作定时器时一样,也有向上计数模式、向下计数模式和中心对齐模式


PWM的输出管脚:


不同的TIMx输出的引脚是不同(此处设计管脚重映射
TIM3复用功能重映射:

注:重映射是为了PCB的设计方便。值得一提的是,其分为部分映射和全部映射

PWM输出频率的计算:
PWM输出的是一个方波信号,信号的频率是由TIMx的时钟频率和TIMx_ARR这个寄存器所决定的
输出信号的占空比则是由TIMx_CRRx寄存器确:
占空比=(TIMx_CRRx/TIMx_ARR)*100%


F就是PWM输出的频率,单位是:HZ;
ARR就是自动重装载寄存器(TIMx_ARR);
PSC 就是预分频器(TIMx_PSC);


STM32 高级定时器PWM的输出

一路带死区时间的互补PWM的波形图

STM32F407ZGT6这款单片机一共有1个高级定时器TIM1和TIM8
这2个高级定时器都可以同时产生3路互补带死区时间的PWM信号和一路单独的PWM信号,
具有刹车输入功能,在紧急的情况下这个刹车功能可以切断PWM信号的输出
还具有支持针对定位的增量(正交)编码器和霍尔传感器电路
高级控制定时器(TIM1 和TIM8 ) 由一个16位的自动装载计数器组成,它由一个可编程的预分频器驱动

它适合多种用途,包含测量输入信号的脉冲宽度( 输入捕获) ,或者产生输出波形(输出比较、PWM、嵌入死区时间的互补PWM等)。
使用定时器预分频器和RCC时钟控制预分频器,可以实现脉冲宽度和波形周期从几个微秒到几个毫秒的调节。
高级控制定时器(TIM1 和TIM8) 和通用定时器(TIMx) 是完全独立的,它们不共享任何资源


死区时间

H桥电路为避免由于关断延迟效应造成上下桥臂直通,有必要设置死区时间
死区时间可有效地避免延迟效应所造成的一个桥臂未完全关断,而另一桥臂又处于导通状态,避免直通炸开关管。
死区时间越大,电路的工作也就越可靠,但会带来输出波形的失真以及降低输出效率。
死区时间小,输出波形要好一些,但是会降低系统的可靠性,一般这个死区时间设置为us级

元器件死区时间是不可以改变的,它主要是取决于元器件的制作工艺和材料!

原则上死区时间当然越小越好。设置死区时间的目的,其实说白了就是为了电路的安全。最佳的设置方法是:在保证安全的前提下,设置的死区时间越小越好。以不炸功率管、输出不短路为目的。

STM32死区时间探究
设置寄存器:就是刹车和死区控制寄存器(TIMx_BDTR)

这个寄存器的第0—7位,这8个位就是用来设置死区时间的,使用如下:


以TIM1为例说明其频率是如何产生的。

定时器1适中产生路线:
系统时钟-> AHB预分频 -> APB2预分频 –> TIM1倍频器–> 产生TIM1的时钟系统
要想知道TIM1的时钟,就得知道系统时钟,AHB预分频器的值,还有APB2预分频器的值,只要知道了这几个值,即可算出TIM1的时钟频率?
这些值从何来,在“SystemInit()”这个时钟的初始化函数中已经给我们答案了,在这个函数中设置的系统时钟是72MZ,AHB预分频器和APB2预分频器值都是设置为1,由此可算出:TIM1时钟频率:
72MHZ了,TDTS=1/72MHZ=13.89ns


定时器结构体初始化

下面介绍TIM结构体初始化

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);  //开启时钟,TIM2是APB1总线的外设
	
	TIM_InternalClockConfig(TIM2);
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
	TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;	//指定参数分频值(选择1分频),DIV22分频 DIV4 44分频 
	TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up;//选择计数模式;
	TIM_TimeBaseInitStructure.TIM_Period=10000-1;      //ARR
	TIM_TimeBaseInitStructure.TIM_Prescaler=720-1;      //PSC
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;//重复计数器,高级定时器才拥有,因此此处给0.
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);			//配置时基单元
	
	TIM_ClearFlag(TIM2,TIM_FLAG_Update);
	
	TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);

在这里PSC为预分频器的数值,在STM32F103中,APB1总线的频率为36MHZ,经定时器倍频区倍频为72MHZ,预分频器的作用是设置定时器多久计数一次,以上面代码为例,在这里配置PSC的值为720-1(定时器从0开始计时,所以需要减1),那么定时器计数一次的时间为 72M/720=1/100000秒,ARR的值为计数了ARR次之后产生定时器中断,以上代码中ARR配置为10000,那么定时器的经过10000次1/100000秒就会产生一次定时器中断,也就是100ms。

此处用公式计算,定时器的频率为 72M  / 720/10000=1/10秒

在F407中,定时器频率计算方法,设置方法,定时器结构体成员与F103一样,不过是F407的时钟线频率更快而已。


输出比较结构体

//输出比较单元配置
	TIM_OCInitTypeDef TIM_OCInitStruct;
	TIM_OCStructInit(&TIM_OCInitStruct);
	TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1 ;
	
	TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High ;
	
	TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;
	TIM_OCInitStruct.TIM_Pulse=0;//设置CCR数值
 
	TIM_OC2Init(TIM2, &TIM_OCInitStruct);

此处用到的是定时器2的通道二引脚,通过引脚定义表可知,此处应该初始化的GPIO引脚为GPIOA1,如需要更改或增加其他引脚,只需要按照引脚定义表增加或更改相应通道即可。

在PWM模式下,CCR(Capture/Compare Register)用于控制占空比。占空比是指高电平信号在一个周期内的持续时间与整个周期的比例。通过调整CCR的值,可以改变PWM信号的占空比,从而控制输出信号的电平。

在输出比较模式下,CCR用于控制初始相位。初始相位是指输出信号与参考信号之间的相位差。通过调整CCR的值,可以改变输出信号的初始相位,从而实现相位控制。

总结起来,CCR在PWM模式下用于控制占空比,在输出比较模式下用于控制初始相位

此处的占空比在结构体里面设置为0

可以在main函数中使用库函数单独更改CCR的值来更该占空比

此处通道2的配置代码为

void pwm_setcompare(uint16_t compare)
{
	TIM_SetCompare2(TIM2, compare);
}

DMA

https://blog.csdn.net/as480133937/article/details/104927922

DMA的基本介绍

DMA,全称Direct Memory Access,即直接存储器访问。

DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。

我们知道CPU有转移数据、计算、控制程序转移等很多功能,系统运作的核心就是CPU 。

CPU无时不刻的在处理着大量的事务,但有些事情却没有那么重要,比方说数据的复制和存储数据,如果我们把这部分的CPU资源拿出来,让CPU去处理其他的复杂计算事务,是不是能够更好的利用CPU的资源呢?

因此:转移数据(尤其是转移大量数据)是可以不需要CPU参与。比如希望外设A的数据拷贝到外设B,只要给两种外设提供一条数据通路,直接让数据由A拷贝到B 不经过CPU的处理,

DMA就是基于以上设想设计的,它的作用就是解决大量数据转移过度消耗CPU资源的问题。有了DMA使CPU更专注于更加实用的操作–计算、控制等。


STM32F4xx 系列的 DMA 功能齐全,工作模式众多,适合不同编程环境要求。STM32F4xx 系列 的 DMA 支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。这里的外设一般指外设的数据寄存器,比如 ADC、SPI、I2C、DCMI 等等外设的数据寄存器存储器 一般是指片内 SRAM、外部存储器、片内 Flash 等等。

DMA传输方式
DMA的作用就是实现数据的直接传输,而去掉了传统数据传输需要CPU寄存器参与的环节,主要涉及四种情况的数据传输,但本质上是一样的,都是从内存的某一区域传输到内存的另一区域(外设的数据寄存器本质上就是内存的一个存储单元)。


四种情况的数据传输如下:

  • 外设到内存
  • 内存到外设
  • 内存到内存
  • 外设到外设

外设到存储器传输  就是把外设数据寄存器内容转移到指定的内存空间。比如进行 ADC 采集时我 们可以利用 DMA 传输把 AD 转换数据转移到我们定义的存储区中,这样对于多通道采集、采样 频率高、连续输出数据的 AD 采集是非常高效的处理方法。

存储区到外设传输  就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送 通信。

存储器到存储器传输  就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于 C 语 言内存拷贝函数 memcpy,利用 DMA 传输可以达到更高的传输效率,特别是 DMA 传输是不占 用 CPU 的,可以节省很多 CPU 资源。

STM32F4xx 系列不支持外设到外设


DMA传输参数


我们知道,数据传输,首先需要的是

  • 1 数据的源地址
  • 2 数据传输位置的目标地址 ,
  • 3 传递数据多少的数据传输量 ,
  • 4 进行多少次传输的传输模式

DMA所需要的核心参数,便是这四个

当用户将参数设置好,主要涉及源地址、目标地址、传输数据量这三个,DMA控制器就会启动数据传输,当剩余传输数据量为0时 达到传输终点,结束DMA传输 ,当然,DMA 还有循环传输模式 当到达传输终点时会重新启动DMA传输。
  
也就是说只要剩余传输数据量不是0,而且DMA是启动状态,那么就会发生数据传输。


DMA的主要特征

每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;

在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);


独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;


支持循环的缓冲器管理;


每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;


存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;
闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
可编程的数据传输数目:最大为65535。(2的16次方)


每次DMA传送由3个操作组成:

从外设数据寄存器或者从当前外设/存储器地址寄存器指示的存储器地址取数据,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
存数据到外设数据寄存器或者当前外设/存储器地址寄存器指示的存储器地址,第一次传输时的开始地址是DMA_CPARx或DMA_CMARx寄存器指定的外设基地址或存储器单元;
执行一次DMA_CNDTRx寄存器的递减操作,该寄存器包含未完成的操作数目。


外设通道选择

STM32F4xx 系列资源丰富,具有两个 DMA 控制器,同时外设繁多,为实现正常传输,DMA 需要 通道选择控制。每个 DMA 控制器具有 8 个数据流,每个数据流对应 8 个外设请求。在实现 DMA 传输之前,DMA 控制器会通过 DMA 数据流 x 配置寄存器 DMA_SxCR(x 为 0~7,对应 8 个 DMA 数据流) 的 CHSEL[2:0] 位选择对应的通道作为该数据流的目标外设。

外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。

DMA1

DMA2

每个外设请求都占用一个数据流通道,相同外设请求可以占用不同数据流通道。

比如 SPI3_RX 请求,即 SPI3 数据发送请求,占用 DMA1 的数据流 0 的通道 0,因此当我们使用该请求时,我 们需要在把 DMA_S0CR 寄存器的 CHSEL[2:0] 设置为“000”,[2:0]指的是第二位到第0位共三位,最大值是7(111),最小值是0(000),刚好满足8个数据流。

此时相同数据流的其他通道不被 选择,处于不可用状态,比如此时不能使用数据流 0 的通道 1 即 I2C1_RX 请求等等。

查阅表 DMA1 各个通道的请求映像 可以发现 SPI3_RX 请求不仅仅在数据流 0 的通道 0,同时数 据流 2 的通道 0 也是 SPI3_RX 请求,实际上其他外设基本上都有两个对应数据流通道,这两个 数据流通道都是可选的,这样设计是尽可能提供多个数据流同时使用情况选择。


仲裁器

一个 DMA 控制器对应 8 个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息

如果我们需要同时使用同一个 DMA 控制器 (DMA1 或 DMA2) 多个外设请求时,那必然需要同时 使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。

仲裁器管理数据流方法分为两个阶段。

第一阶段属于软件阶段,我们在配置数据流时可以通过寄 存器设定它的优先级别,具体配置 DMA_SxCR 寄存器 PL[1:0] 位,可以设置为非常高、高、中和 低四个级别。

第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样,则他们优先 级取决于数据流编号,编号越低越具有优先权,比如数据流 2 优先级高于数据流 3。


FIFO

每个数据流都独立拥有四级 32 位 FIFO(先进先出存储器缓冲区)。DMA 传输具有 FIFO 模式和直 接模式。

直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果 DMA 配置为存储器到 外设传输那 DMA 会将一个数据存放在 FIFO 内,如果外设启动 DMA 传输请求就可以马上将数据传输过去。

FIFO 用于在源数据传输到目标地址之前临时存放这些数据。可以通过 DMA 数据流 xFIFO 控制 寄存器 DMA_SxFCR 的 FTH[1:0] 位来控制 FIFO 的阈值,分别为 1/4、1/2、3/4 和满。如果数据存储量达到阈值级别时,FIFO 内容将传输到目标中。

FIFO 对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据, 而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来 4 个 8 位字节的数据拼凑成 一个 32 位字数据。此时使用 FIFO 功能先把数据缓存起来,分别根据需要输出数据。

FIFO 另外一个作用使用于突发 (burst) 传输


存储器端口、外设端口

DMA 控制器实现双 AHB 主接口,更好利用总线矩阵和并行传输。DMA 控制器通过存储器端口 和外设端口与存储器和外设进行数据传输,关系见图两个 DMA 控制器系统实现 。DMA 控制器 的功能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。

DMA2(DMA 控制器 2) 的存储器端口和外设端口都是连接到 AHB 总线矩阵,可以使用 AHB 总线 矩阵功能。DMA2 存储器和外设端口可以访问相关的内存地址,包括有内部 Flash、内部 SRAM、 AHB1 外设、AHB2 外设、APB2 外设和外部存储器空间。

DMA1 的存储区端口相比 DMA2 的要减少 AHB2 外设的访问权,同时 DMA1 外设端口是没有连 接至总线矩阵的,只有连接到 APB1 外设,所以 DMA1 不能实现存储器到存储器传输。


编程端口

AHB 从器件编程端口是连接至 AHB2 外设的。AHB2 外设在使用 DMA 传输时需要相关控制信 号。


DMA 数据配置

DMA 工作模式多样,具有多种可能工作模式,具体可能配置见表 DMA 配置可能情况 。


DMA 传输模式

DMA2 支持全部三种传输模式,而 DMA1 只有外设到存储器和存储器到外设两种模式。

模式选择可以通过 DMA_SxCR 寄存器的 DIR[1:0] 位控制,进而将 DMA_SxCR 寄存器的 EN 位置 1 就可以使能 DMA 传输。

在 DMA_SxCR 寄存器的 PSIZE[1:0] 和 MSIZE[1:0] 位分别指定外设和存储器数据宽度大小,可以 指定为字节 (8 位)、半字 (16 位) 和字 (32 位),我们可以根据实际情况设置。

直接模式要求外设和存储器数据宽度大小一样,实际上在这种模式下 DMA 数据流直接使用 PSIZE,MSIZE 不被使 用。


源地址和目标地址

DMA 数据流 x 外设地址 DMA_SxPAR(x 为 0~7) 寄存器用来指定外设地址,它是一个 32 位数据 有效寄存器。

DMA 数据流 x 存储器 0 地址 DMA_SxM0AR(x 为 0~7) 寄存器和 DMA 数据流 x 存 储器 1 地址 DMA_SxM1AR(x 为 0~7) 寄存器用来存放存储器地址,其中 DMA_SxM1AR 只用于 双缓冲模式,DMA_SxM0AR 和 DMA_SxM1AR 都是 32 位数据有效的。

当选择外设到存储器模式时,即设置 DMA_SxCR 寄存器的 DIR[1:0] 位为“00”,DMA_SxPAR 寄 存器为外设地址,也是传输的源地址,DMA_SxM0AR 寄存器为存储器地址,也是传输的目标地 址。

对于存储器到存储器传输模式,即设置 DIR[1:0] 位为“10”时,采用与外设到存储器模式相 同配置。

而对于存储器到外设,即设置 DIR[1:0] 位为“01”时,DMA_SxM0AR 寄存器作为为源地址,DMA_SxPAR 寄存器作为目标地址。


流控制器

流控制器主要涉及到一个控制 DMA 传输停止问题。

DMA 传输在 DMA_SxCR 寄存器的 EN 位被 置 1 后就进入准备传输状态,如果有外设请求 DMA 传输就可以进行数据传输。很多情况下,我 们明确知道传输数据的数目,比如要传 1000 个或者 2000 个数据,这样我们就可以在传输之前设 置 DMA_SxNDTR 寄存器为要传输数目值,DMA 控制器在传输完这么多数目数据后就可以控制 DMA 停止传输。

DMA 数据流 x 数据项数 DMA_SxNDTR(x 为 0~7) 寄存器用来记录当前仍需要传输数目,它是一 个 16 位数据有效寄存器,即最大值为 65535,这个值在程序设计是非常有用也是需要注意的地 方。

我们在编程时一般都会明确指定一个传输数量,在完成一次数目传输后 DMA_SxNDTR 计数 值就会自减,当达到零时就说明传输完成。

如果某些情况下在传输之前我们无法确定数据的数目,那 DMA 就无法自动控制传输停止了,此 时需要外设通过硬件通信向 DMA 控制器发送停止传输信号。

这里有一个大前提就是外设必须是 可以发出这个停止传输信号,只有 SDIO 才有这个功能,其他外设不具备此功能。


循环模式

循环模式相对应于一次模式。一次模式就是传输一次就停止传输,下一次传输需要手动控制,而 循环模式在传输一次后会自动按照相同配置重新传输,周而复始直至被控制停止或传输发生错 误。

通过 DMA_SxCR 寄存器的 CIRC 位可以使能循环模式。


传输类型

DMA 传输类型有单次 (Single) 传输和突发 (Burst) 传输。

突发传输就是用非常短时间结合非常高 数据信号率传输数据,相对正常传输速度,突发传输就是在传输阶段把速度瞬间提高,实现高速 传输,在数据传输完成后恢复正常速度,有点类似达到数据块“秒传”效果。为达到这个效果突 发传输过程要占用 AHB 总线,保证要求每个数据项在传输过程不被分割,这样一次性把数据全 部传输完才释放 AHB 总线;而单次传输时必须通过 AHB 的总线仲裁多次控制才传输完成。

单次和突发传输数据使用具体情况参考表 22‑4。其中 PBURST[1:0] 和 MBURST[1:0] 位是位于 DMA_SxCR 寄存器中的,用于分别设置外设和存储器不同节拍数的突发传输,对应为单次传 输、4 个节拍增量传输、8 个节拍增量传输和 16 个节拍增量传输。PINC 位和 MINC 位是寄存器 DMA_SxCR 寄存器的第 9 和第 10 位,如果位被置 1 则在每次数据传输后数据地址指针自动递 增,其增量由 PSIZE 和 MSIZE 值决定,比如,设置 PSIZE 为半字大小,那么下一次传输地址将 是前一次地址递增 2。

突发传输与 FIFO 密切相关,突发传输需要结合 FIFO 使用,具体要求 FIFO 阈值一定要是内存突 发传输数据量的整数倍。FIFO 阈值选择和存储器突发大小必须配合使用,具体参考表 FIFO 阈值 配置 。


直接模式

默认情况下,DMA 工作在直接模式,不使能 FIFO 阈值级别。

直接模式在每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址和目标地 址的数据宽度必须一致,所以只有 PSIZE 控制,而 MSIZE 值被忽略。突发传输是基于 FIFO 的所 以直接模式不被支持。另外直接模式不能用于存储器到存储器传输。

在直接模式下,如果 DMA 配置为存储器到外设传输那 DMA 会见一个数据存放在 FIFO 内,如 果外设启动 DMA 传输请求就可以马上将数据传输过去。


双缓冲模式

设置 DMA_SxCR 寄存器的 DBM 位为 1 可启动双缓冲传输模式,并自动激活循环模式。双缓冲不 应用与存储器到存储器的传输。双缓冲模式下,两个存储器地址指针都有效,即 DMA_SxM1AR 寄存器将被激活使用。开始传输使用 DMA_SxM0AR 寄存器的地址指针所对应的存储区,当这个 存储区数据传输完 DMA 控制器会自动切换至 DMA_SxM1AR 寄存器的地址指针所对应的另一 块存储区,如果这一块也传输完成就再切换至 DMA_SxM0AR 寄存器的地址指针所对应的存储 区,这样循环调用。

当其中一个存储区传输完成时都会把传输完成中断标志 TCIF 位置 1,如果我们使能了 DMA_SxCR 寄存器的传输完成中断,则可以产生中断信号,这个对我们编程非常有用。另外一 个非常有用的信息是 DMA_SxCR 寄存器的 CT 位,当 DMA 控制器是在访问使用 DMA_SxM0AR 时 CT=0,此时 CPU 不能访问 DMA_SxM0AR,但可以向 DMA_SxM1AR 填充或者读取数据;当 DMA 控制器是在访问使用 DMA_SxM1AR 时 CT=1,此时 CPU 不能访问 DMA_SxM1AR,但可 以向 DMA_SxM0AR 填充或者读取数据。另外在未使能 DMA 数据流传输时,可以直接写 CT 位, 改变开始传输的目标存储区。

双缓冲模式应用在需要解码程序的地方是非常有效的。比如 MP3 格式音频解码播放,MP3 是被 压缩的文件格式,我们需要特定的解码库程序来解码文件才能得到可以播放的 PCM 信号,解码 需要一定的实际,按照常规方法是读取一段原始数据到缓冲区,然后对缓冲区内容进行解码,解 码后才输出到音频播放电路,这种流程对 CPU 运算速度要求高,很容易出现播放不流畅现象。如 果我们使用 DMA 双缓冲模式传输数据就可以非常好的解决这个问题,达到解码和输出音频数据 到音频电路同步进行的效果。


DMA 中断

每个 DMA 数据流可以在发送以下事件时产生中断:

1) 达到半传输:DMA 数据传输达到一半时 HTIF 标志位被置 1,如果使能 HTIE 中断控制位将产 生达到半传输中断;

2) 传输完成:DMA 数据传输完成时 TCIF 标志位被置 1,如果使能 TCIE 中断控制位将产生传输 完成中断;

3) 传输错误:DMA 访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器 时 TEIF 标志位被置 1,如果使能 TEIE 中断控制位将产生传输错误中断; \

4) FIFO 错误:发生 FIFO 下溢或者上溢时 FEIF 标志位被置 1,如果使能 FEIE 中断控制位将产生 FIFO 错误中断;

5) 直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没 有完成被传输到存储器空间上,此时 DMEIF 标志位被置 1,如果使能 DMEIE 中断控制位将产生 直接模式错误中断。


DMA 初始化结构体详解

标准库函数对每个外设都建立了一个初始化结构体 xxx_InitTypeDef(xxx 为外设名称),结构体成 员用于设置外设工作参数,并由标准库函数 xxx_Init() 调用这些设定参数进入设置外设相应的寄 存器,达到配置外设工作环境的目的。

结构体 xxx_InitTypeDef 和库函数 xxx_Init 配合使用是标准库精髓所在,理解了结构体 xxx_InitTypeDef 每个成员意义基本上就可以对该外设运用自如了。结构体 xxx_InitTypeDef 定义 在 stm32f4xx_xxx.h(后面 xxx 为外设名称) 文件中,库函数 xxx_Init 定义在 stm32f4xx_xxx.c 文件 中,编程时我们可以结合这两个文件内注释使用。

typedef struct
{
  uint32_t DMA_Channel;            /*!< Specifies the channel used for the specified stream. 
                                        This parameter can be a value of @ref DMA_channel */
 
  uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Streamx. */

  uint32_t DMA_Memory0BaseAddr;    /*!< Specifies the memory 0 base address for DMAy Streamx. 
                                        This memory is the default memory used when double buffer mode is
                                        not enabled. */

  uint32_t DMA_DIR;                /*!< Specifies if the data will be transferred from memory to peripheral, 
                                        from memory to memory or from peripheral to memory.
                                        This parameter can be a value of @ref DMA_data_transfer_direction */

  uint32_t DMA_BufferSize;         /*!< Specifies the buffer size, in data unit, of the specified Stream. 
                                        The data unit is equal to the configuration set in DMA_PeripheralDataSize
                                        or DMA_MemoryDataSize members depending in the transfer direction. */

  uint32_t DMA_PeripheralInc;      /*!< Specifies whether the Peripheral address register should be incremented or not.
                                        This parameter can be a value of @ref DMA_peripheral_incremented_mode */

  uint32_t DMA_MemoryInc;          /*!< Specifies whether the memory address register should be incremented or not.
                                        This parameter can be a value of @ref DMA_memory_incremented_mode */

  uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
                                        This parameter can be a value of @ref DMA_peripheral_data_size */

  uint32_t DMA_MemoryDataSize;     /*!< Specifies the Memory data width.
                                        This parameter can be a value of @ref DMA_memory_data_size */

  uint32_t DMA_Mode;               /*!< Specifies the operation mode of the DMAy Streamx.
                                        This parameter can be a value of @ref DMA_circular_normal_mode
                                        @note The circular buffer mode cannot be used if the memory-to-memory
                                              data transfer is configured on the selected Stream */

  uint32_t DMA_Priority;           /*!< Specifies the software priority for the DMAy Streamx.
                                        This parameter can be a value of @ref DMA_priority_level */

  uint32_t DMA_FIFOMode;          /*!< Specifies if the FIFO mode or Direct mode will be used for the specified Stream.
                                        This parameter can be a value of @ref DMA_fifo_direct_mode
                                        @note The Direct mode (FIFO mode disabled) cannot be used if the 
                                               memory-to-memory data transfer is configured on the selected Stream */

  uint32_t DMA_FIFOThreshold;      /*!< Specifies the FIFO threshold level.
                                        This parameter can be a value of @ref DMA_fifo_threshold_level */

  uint32_t DMA_MemoryBurst;        /*!< Specifies the Burst transfer configuration for the memory transfers. 
                                        It specifies the amount of data to be transferred in a single non interruptable 
                                        transaction. This parameter can be a value of @ref DMA_memory_burst 
                                        @note The burst mode is possible only if the address Increment mode is enabled. */

  uint32_t DMA_PeripheralBurst;    /*!< Specifies the Burst transfer configuration for the peripheral transfers. 
                                        It specifies the amount of data to be transferred in a single non interruptable 
                                        transaction. This parameter can be a value of @ref DMA_peripheral_burst
                                        @note The burst mode is possible only if the address Increment mode is enabled. */  
}DMA_InitTypeDef;

1) DMA_Channel:DMA 请求通道选择,可选通道 0 至通道 7,每个外设对应固定的通道,具体设 置值需要查表 DMA1 各个通道的请求映像 和表 DMA2 各个通道的请求映像 ;它设定 DMA_SxCR 寄存器的 CHSEL[2:0] 位的值。例如,我们使用模拟数字转换器 ADC3 规则采集 4 个输入通道的 电压数据,查表 DMA2 各个通道的请求映像 可知使用通道 2。

2) DMA_PeripheralBaseAddr:外设地址,设定 DMA_SxPAR 寄存器的值;一般设置为外设的数据 寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区地址。ADC3 的数据寄存器 ADC_DR 地址为 ((uint32_t)ADC3+0x4C)。

3) DMA_Memory0BaseAddr:存储器 0 地址,设定 DMA_SxM0AR 寄存器值;一般设置为我们 自定义存储区的首地址。我们程序先自定义一个 16 位无符号整形数组 ADC_ConvertedValue[4] 用 来 存 放 每 个 通 道 的 ADC 值, 所 以 把 数 组 首 地 址 (直 接 使 用 数 组 名 即 可) 赋 值 给 DMA_Memory0BaseAddr。

4) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。它设定 DMA_SxCR 寄存器的 DIR[1:0] 位的值。ADC 采集显然使用外设到存储器模式。

5) DMA_BufferSize:设定待传输数据数目,初始化设定 DMA_SxNDTR 寄存器的值。这里 ADC是采集 4 个通道数据,所以待传输数目也就是 4。

6) DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它 设定 DMA_SxCR 寄存器的 PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使 能该位。ADC3 的数据寄存器地址是固定并且只有一个所以不使能外设地址递增。

7) DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它 设定 DMA_SxCR 寄存器的 MINC 位的值;我们自定义的存储区一般都是存放多个数据的,所以 使能存储器地址自动递增功能。我们之前已经定义了一个包含 4 个元素的数字用来存放数据,使 能存储区地址递增功能,自动把每个通道数据存放到对应数组元素内。

8) DMA_PeripheralDataSize:外设数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位),它设定 DMA_SxCR 寄存器的 PSIZE[1:0] 位的值。ADC 数据寄存器只有低 16 位数据有效,使用半字数 据宽度。

9) DMA_MemoryDataSize:存储器数据宽度,可选字节 (8 位)、半字 (16 位) 和字 (32 位),它设定 DMA_SxCR 寄存器的 MSIZE[1:0] 位的值。保存 ADC 转换数据也要使用半字数据宽度,这跟我 们定义的数组是相对应的。

10) DMA_Mode:DMA 传输模式选择,可选一次传输或者循环传输,它设定 DMA_SxCR 寄存器 的 CIRC 位的值。我们希望 ADC 采集是持续循环进行的,所以使用循环传输模式。

11) DMA_Priority:软件设置数据流的优先级,有 4 个可选优先级分别为非常高、高、中和低,它 设定 DMA_SxCR 寄存器的 PL[1:0] 位的值。DMA 优先级只有在多个 DMA 数据流同时使用时才 有意义,这里我们设置为非常高优先级就可以了。

12) DMA_FIFOMode:FIFO 模式使能,如果设置为 DMA_FIFOMode_Enable 表示使能 FIFO 模式 功能;它设定 DMA_SxFCR 寄存器的 DMDIS 位。ADC 采集传输使用直接传输模式即可,不需要 使用 FIFO 模式。

13) DMA_FIFOThreshold:FIFO 阈值选择,可选 4 种状态分别为 FIFO 容量的 1/4、1/2、3/4 和满; 它设定 DMA_SxFCR 寄存器的 FTH[1:0] 位;DMA_FIFOMode 设置为 DMA_FIFOMode_Disable, 那 DMA_FIFOThreshold 值无效。ADC 采集传输不使用 FIFO 模式,设置改值无效。

14) DMA_MemoryBurst:存储器突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍 的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存器的 MBURST[1:0] 位的值。 ADC 采集传输是直接模式,要求使用单次模式。

15) DMA_PeripheralBurst:外设突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍 的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存器的 PBURST[1:0] 位的值。 ADC 采集传输是直接模式,要求使用单次模式。


DMA寄存器配置流程

通道配置过程 下面是配置DMA通道x的过程(x代表通道号):

在DMA_CPARx寄存器中设置外设寄存器的地址。发生外设数据传输请求时,这个地址将 是数据传输的源或目标。


在DMA_CMARx寄存器中设置数据存储器的地址。发生外设数据传输请求时,传输的数 据将从这个地址读出或写入这个地址。


在DMA_CNDTRx寄存器中设置要传输的数据量。在每个数据传输后,这个数值递减。


在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。


在DMA_CCRx寄存器中设置数据传输的方向、循环模式、外设和存储器的增量模式、外 设和存储器的数据宽度、传输一半产生中断或传输完成产生中断。


设置DMA_CCRx寄存器的ENABLE位,启动该通道。


一旦启动了DMA通道,它既可响应连到该通道上的外设的DMA请求。 当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生 一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位 (TCIE)时,将产生一个中断请求。


DMA 存储器到存储器模式实验

DMA 工作模式多样,具体如何使用需要配合实际传输条件具体分析。接下来我们通过两个实验 详细讲解 DMA 不同模式下的使用配置,加深我们对 DMA 功能的理解

DMA 运行高效,使用方便,在很多测试实验都会用到,这里先详解存储器到存储器和存储器到 外设这两种模式。

存储器到存储器模式可以实现数据在两个内存的快速拷贝。我们先定义一个静态的源数据,然后 使用 DMA 传输把源数据拷贝到目标地址上,最后对比源数据和目标地址的数据,看看是否传输 准确。


编程要点

1) 使能 DMA 数据流时钟并复位初始化 DMA 数据流;

2) 配置 DMA 数据流参数;

3) 使能 DMA 数据流,进行传输;

4) 等待传输完成,并对源数据和目标地址数据进行比较。

/* 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定 */
#define DMA_STREAM               DMA2_Stream0
#define DMA_CHANNEL              DMA_Channel_0
#define DMA_STREAM_CLOCK         RCC_AHB1Periph_DMA2 
#define DMA_FLAG_TCIF            DMA_FLAG_TCIF0

#define BUFFER_SIZE              32
#define TIMEOUT_MAX              10000 /* Maximum timeout value */

/* 定义aSRC_Const_Buffer数组作为DMA传输数据源
  const关键字将aSRC_Const_Buffer数组变量定义为常量类型 */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
                                    0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
                                    0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
                                    0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
                                    0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
                                    0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
                                    0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
                                    0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
                                    0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};
/* 定义DMA传输目标存储器 */
uint32_t aDST_Buffer[BUFFER_SIZE];

存储器到存储器传输必须使用 DMA2,但对数据流编号以及通道选择就没有硬性要求,可以自由 选择

aSRC_Const_Buffer[BUFFER_SIZE] 是定义用来存放源数据的,并且使用了 const 关键字修饰,即 常量类型,使得变量是存储在内部 flash 空间上。


DMA 数据流配置

static void DMA_Config(void)
{
  DMA_InitTypeDef  DMA_InitStructure;
  __IO uint32_t    Timeout = TIMEOUT_MAX;
    
  /* 使能DMA时钟 */
  RCC_AHB1PeriphClockCmd(DMA_STREAM_CLOCK, ENABLE);
  
  /* 复位初始化DMA数据流 */
  DMA_DeInit(DMA_STREAM);

  /* 确保DMA数据流复位完成 */
  while (DMA_GetCmdStatus(DMA_STREAM) != DISABLE)
  {
  }
  
  /* DMA数据流通道选择 */
  DMA_InitStructure.DMA_Channel = DMA_CHANNEL;  
  /* 源数据地址 */
  DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer;
  /* 目标地址 */
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)aDST_Buffer;
  /* 存储器到存储器模式 */
  DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory;
  /* 数据数目 */
  DMA_InitStructure.DMA_BufferSize = (uint32_t)BUFFER_SIZE;
  /* 使能自动递增功能 */
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
  /* 使能自动递增功能 */
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  /* 源数据是字大小(32位) */
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
  /* 目标数据也是字大小(32位) */
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
  /* 一次传输模式,存储器到存储器模式不能使用循环传输 */
  DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
  /* DMA数据流优先级为高 */
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  /* 禁用FIFO模式 */
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;     
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  /* 单次模式 */
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  /* 单次模式 */
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
  /* 完成DMA数据流参数配置 */
  DMA_Init(DMA_STREAM, &DMA_InitStructure);
  
  /* 清除DMA数据流传输完成标志位 */
  DMA_ClearFlag(DMA_STREAM,DMA_FLAG_TCIF);
  
  /* 使能DMA数据流,开始DMA数据传输 */
  DMA_Cmd(DMA_STREAM, ENABLE);

  /* 检测DMA数据流是否有效并带有超时检测功能 */
  Timeout = TIMEOUT_MAX;
  while ((DMA_GetCmdStatus(DMA_STREAM) != ENABLE) && (Timeout-- > 0))
  {
  }
   
  /* 判断是否超时 */
  if (Timeout == 0)
  {
    /* 超时就让程序运行下面循环:RGB彩色灯闪烁 */
    while (1)
    {      
      LED_RED;
      Delay(0xFFFFFF);
      LED_RGBOFF;
      Delay(0xFFFFFF);
    }
  } 
}

使用 DMA_InitTypeDef 结构体定义一个 DMA 数据流初始化变量,这个结构体内容我们之前已经 有详细讲解。定义一个无符号 32 位整数变量 Timeout 用来计数超时。

调用 RCC_AHB1PeriphClockCmd 函数开启 DMA 数据流时钟,使用 DMA 控制器之前必须开启对 应的时钟。

DMA_DeInit 函数见数据流复位到缺省配置状态。

使用 DMA_GetCmdStatus 函数获取当前 DMA 数据流状态,该函数接收一个 DMA 数据流的参数, 返回当前数据流状态,复位 DMA 数据流之前需要调用该函数来确保 DMA 数据流复位完成。

存储器到存储器模式通道选择没有具体规定,源地址和目标地址使用之前定义的数组首地址,只 能使用一次传输模式不能循环传输,最后我调用 DMA_Init 函数完成 DMA 数据流的初始化配置。

DMA_ClearFlag 函数用于清除 DMA 数据流标志位,代码用到传输完成标志位,使用之前清除标 志位以免产生不必要干扰。DMA_ClearFlag 函数需要两个形参,一个是 DMA 数据流,一个是事 件标志位,可选有数据流传输完成标志位、半传输标志位、FIFO 错误标志位、传输错误标志位 以及直接模式错误标志位。

DMA_Cmd 函数用于启动或者停止 DMA 数据流传输,它接收连个参数,第一个是 DMA 数据流, 另外一个是开启 ENABLE 或者停止 DISABLE。

开启 DMA 传输后需要使用 DMA_GetCmdStatus 函数获取 DMA 数据流状态,确保 DMA 数据流 配置有效,为防止程序卡死,添加了超时检测功能。


数据对比函数

/**
  * 判断指定长度的两个数据源是否完全相等,
  * 如果完全相等返回1,只要其中一对数据不相等返回0
  */
uint8_t Buffercmp(const uint32_t* pBuffer, 
                  uint32_t* pBuffer1, uint16_t BufferLength)
{
  /* 数据长度递减 */
  while(BufferLength--)
  {
    /* 判断两个数据源是否对应相等 */
    if(*pBuffer != *pBuffer1)
    {
      /* 对应数据源不相等马上退出函数,并返回0 */
      return 0;
    }
    /* 递增两个数据源的地址指针 */
    pBuffer++;
    pBuffer1++;
  }
  /* 完成判断并且对应数据相对 */
  return 1;  
}

判断指定长度的两个数据源是否完全相等,如果完全相等返回 1;只要其中一对数据不相等返回0。它需要三个形参,前两个是两个数据源的地址,第三个是要比较数据长度。


主函数

int main(void)
{
  /* 定义存放比较结果变量 */
  uint8_t TransferStatus;
  
	/* LED 端口初始化 */
	LED_GPIO_Config();
    
  /* 设置RGB彩色灯为紫色 */
  LED_PURPLE;  
  
  /* 简单延时函数 */
  Delay(0xFFFFFF);  
  
  /* DMA传输配置 */
  DMA_Config(); 
  
  /* 等待DMA传输完成 */
  while(DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_TCIF)==DISABLE)
  {
    
  }   
  
  /* 比较源数据与传输后数据 */
  TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
  
  /* 判断源数据与传输后数据比较结果*/
  if(TransferStatus==0)  
  {
    /* 源数据与传输后数据不相等时RGB彩色灯显示红色 */
    LED_RED;
  }
  else
  { 
    /* 源数据与传输后数据相等时RGB彩色灯显示蓝色 */
    LED_BLUE;
  }

	while (1)
	{		
	}
}

确定 DMA 传输完成之后就可以调用 Buffercmp 函数比较源数据与 DMA 传输后目标地址的数据 是否一一对应。TransferStatus 保存比较结果,如果为 1 表示两个数据源一一对应相等说明 DMA 传输成功;相反,如果为 0 表示两个数据源数据存在不等情况,说明 DMA 传输出错。


DMA 存储器到外设模式实验

DMA 存储器到外设传输模式非常方便把存储器数据传输外设数据寄存器中,这在 STM32 芯片 向其他目标主机,比如电脑、另外一块开发板或者功能芯片,发送数据是非常有用的。RS-232 串 口通信是我们常用开发板与 PC 端通信的方法。我们可以使用 DMA 传输把指定的存储器数据转 移到 USART 数据寄存器内,并发送至 PC 端,在串口调试助手显示。


编程要点

1) 配置 USART 通信功能;

2) 设置 DMA 为存储器到外设模式,设置数据流通道,指定 USART 数据寄存器为目标地址,循 环发送模式;

3) 使能 DMA 数据流;

4) 使能 USART 的 DMA 发送请求;

5) DMA 传输同时 CPU 可以运行其他任务。


宏定义

#define DEBUG_USART                       USART1
#define DEBUG_USART_CLK                   RCC_APB2Periph_USART1
#define DEBUG_USART_RX_GPIO_PORT          GPIOA
#define DEBUG_USART_RX_GPIO_CLK           RCC_AHB1Periph_GPIOA
#define DEBUG_USART_RX_PIN                GPIO_Pin_10
#define DEBUG_USART_RX_AF                 GPIO_AF_USART1
#define DEBUG_USART_RX_SOURCE             GPIO_PinSource10

#define DEBUG_USART_TX_GPIO_PORT          GPIOA
#define DEBUG_USART_TX_GPIO_CLK           RCC_AHB1Periph_GPIOA
#define DEBUG_USART_TX_PIN                GPIO_Pin_9
#define DEBUG_USART_TX_AF                 GPIO_AF_USART1
#define DEBUG_USART_TX_SOURCE             GPIO_PinSource9

#define DEBUG_USART_BAUDRATE              115200

//DMA
#define DEBUG_USART_DR_BASE               (USART1_BASE+0x04)		
#define SENDBUFF_SIZE                     5000				//发送的数据量
#define DEBUG_USART_DMA_CLK               RCC_AHB1Periph_DMA2	
#define DEBUG_USART_DMA_CHANNEL           DMA_Channel_4
#define DEBUG_USART_DMA_STREAM            DMA2_Stream7

void USART_DMA_Config(void)
{
  DMA_InitTypeDef DMA_InitStructure;

  /*开启DMA时钟*/
  RCC_AHB1PeriphClockCmd(DEBUG_USART_DMA_CLK, ENABLE);
  
  /* 复位初始化DMA数据流 */
  DMA_DeInit(DEBUG_USART_DMA_STREAM);

  /* 确保DMA数据流复位完成 */
  while (DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != DISABLE)  {
  }

  /*usart1 tx对应dma2,通道4,数据流7*/	
  DMA_InitStructure.DMA_Channel = DEBUG_USART_DMA_CHANNEL;  
  /*设置DMA源:串口数据寄存器地址*/
  DMA_InitStructure.DMA_PeripheralBaseAddr = DEBUG_USART_DR_BASE;	 
  /*内存地址(要传输的变量的指针)*/
  DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff;
  /*方向:从内存到外设*/		
  DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;	
  /*传输大小DMA_BufferSize=SENDBUFF_SIZE*/	
  DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;
  /*外设地址不增*/	    
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 
  /*内存地址自增*/
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;	
  /*外设数据单位*/	
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
  /*内存数据单位 8bit*/
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;	
	/*DMA模式:一次循环*/
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
  /*DMA模式:不断循环*/
	//DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;	 
  /*优先级:中*/	
  DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;      
  /*禁用FIFO*/
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;        
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;    
  /*存储器突发传输 16个节拍*/
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;    
  /*外设突发传输 1个节拍*/
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;    
  /*配置DMA2的数据流7*/		   
  DMA_Init(DEBUG_USART_DMA_STREAM, &DMA_InitStructure);
  
  /*使能DMA*/
  DMA_Cmd(DEBUG_USART_DMA_STREAM, ENABLE);
  
  /* 等待DMA数据流有效*/
  while(DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != ENABLE)
  {
  }   
}

使用 DMA_InitTypeDef 结构体定义一个 DMA 数据流初始化变量,这个结构体内容我们之前已经 有详细讲解。

调用 RCC_AHB1PeriphClockCmd 函数开启 DMA 数据流时钟,使用 DMA 控制器之前必须开启对 应的时钟。

DMA_DeInit 函数见数据流复位到缺省配置状态。

使用 DMA_GetCmdStatus 函数获取当前 DMA 数据流状态,该函数接收一个 DMA 数据流的参数, 返回当前数据流状态,复位 DMA 数据流之前需要调用该函数来确保 DMA 数据流复位完成。

USART 有固定的 DMA 通道,USART 数据寄存器地址也是固定的,外设地址不可以使用自动递 增,源数据使用我们自定义的数组空间,存储器地址使用自动递增,采用循环发送模式,最后我 调用 DMA_Init 函数完成 DMA 数据流的初始化配置。

DMA_Cmd 函数用于启动或者停止 DMA 数据流传输,它接收连个参数,第一个是 DMA 数据流, 另外一个是开启 ENABLE 或者停止 DISABLE。

开启 DMA 传输后需要使用 DMA_GetCmdStatus 函数获取 DMA 数据流状态,确保 DMA 数据流 配置有效。


主函数

int main(void)
{
  uint16_t i;
  /* 初始化USART */
  Debug_USART_Config(); 

  /* 配置使用DMA模式 */
  USART_DMA_Config();
  
  /* 配置RGB彩色灯 */
  LED_GPIO_Config();

  printf("\r\n USART1 DMA TX 测试 \r\n");
  
  /*填充将要发送的数据*/
  for(i=0;i<SENDBUFF_SIZE;i++)
  {
    SendBuff[i]	 = 'A';
    
  }

  /*为演示DMA持续运行而CPU还能处理其它事情,持续使用DMA发送数据,量非常大,
  *长时间运行可能会导致电脑端串口调试助手会卡死,鼠标乱飞的情况,
  *或把DMA配置中的循环模式改为单次模式*/		
  
  /* USART1 向 DMA发出TX请求 */
  USART_DMACmd(DEBUG_USART, USART_DMAReq_Tx, ENABLE);

  /* 此时CPU是空闲的,可以干其他的事情 */  
  //例如同时控制LED
  while(1)
  {
    LED1_TOGGLE
    Delay(0xFFFFF);
  }
}

使用 for 循环填充源数据,SendBuff[SENDBUFF_SIZE] 是定义在 bsp_usart_dma.c 中的一个全局无 符号 8 位整数数组,是 DMA 传输的源数据,在 USART_DMA_Config 函数中已经被设置为存储 器地址。

USART_DMACmd 函数用于控制 USART 的 DMA 传输启动和关闭。它接收三个参数,第一个参 数用于设置 DMA 数据流,第二个参数设置 DMA 请求,有 USART 发送请求 USART_DMAReq_Tx 和接收请求 USART_DMAReq_Rx 可选,第三个参数用于设置启动请求 ENABLE 或者关闭请求 DISABLE。运行该函数后 USART 的 DMA 发送传输就开始了,根据配置它会通过 USART 循环发 送数据。

DMA 传输过程是不占用 CPU 资源的,可以一边传输一次运行其他任务。


ADC

STM32F407ZGT6 有 3 个 ADC,每个 ADC 有 12 位、10 位、8 位和 6 位可选,每个 ADC 有 16 个 外部通道。另外还有两个内部 ADC 源和 VBAT 通道挂在 ADC1 上。ADC 具有独立模式、双重模 式和三重模式,对于不同 AD 转换要求几乎都有合适的模式可选。ADC 功能非常强大,具体的 我们在功能框图中分析每个部分的功能。



电压输入范围

ADC 输入范围为:VREF- ≤ VIN ≤ VREF+。由 VREF-、VREF+ 、VDDA 、VSSA、这四个外部引脚决定。

我们在设计原理图的时候一般把 VSSA 和 VREF- 接地,把 VREF+ 和 VDDA 接 3V3,得到 ADC 的输 入电压范围为:0~3.3V。

如果我们想让输入的电压范围变宽,去到可以测试负电压或者更高的正电压,我们可以在外部加 一个电压调理电路,把需要转换的电压抬升或者降压到 0~3.3V,这样 ADC 就可以测量了。


输入通道

我们确定好 ADC 输入电压之后,那么电压怎么输入到 ADC?

这里我们引入通道的概念,STM32 的 ADC 多达 19 个通道,其中外部的 16 个通道就是框图中的 ADCx_IN0、ADCx_IN1…ADCx_IN5。 这 16 个通道对应着不同的 IO 口,具体是哪一个 IO 口可以从手册查询到。其中 ADC1/2/3 还有 内部通道:ADC1 的通道 ADC1_IN16 连接到内部的 VSS,通道 ADC1_IN17 连接到了内部参考 电压 VREFINT 连接,通道 ADC1_IN18 连接到了芯片内部的温度传感器或者备用电源 VBAT。ADC2 和 ADC3 的通道 16、17、18 全部连接到了内部的 VSS。


外部的 16 个通道在转换的时候又分为规则通道和注入通道,其中规则通道最多有 16 路,注入通 道最多有 4 路。那这两个通道有什么区别?在什么时候使用?

规则通道

规则通道:顾名思意,规则通道就是很规矩的意思,我们平时一般使用的就是这个通道,或者应 该说我们用到的都是这个通道,没有什么特别要注意的可讲。

注入通道

注入,可以理解为插入,插队的意思,是一种不安分的通道。它是一种在规则通道转换的时候强 行插入要转换的一种通道。如果在规则通道转换过程中,有注入通道插队,那么就要先转换完注 入通道,等注入通道转换完成后,再回到规则通道的转换流程。这点跟中断程序很像,都是不安 分的主。所以,注入通道只有在规则通道存在时才会出现。


转换顺序

规则序列

规则序列寄存器有 3 个,分别为 SQR3、SQR2、SQR1。SQR3 控制着规则序列中的第一个到第六 个转换,对应的位为:SQ1[4:0]~SQ6[4:0],第一次转换的是位 4:0 SQ1[4:0],如果通道 16 想第一 次转换,那么在 SQ1[4:0] 写 16 即可。SQR2 控制着规则序列中的第 7 到第 12 个转换,对应的位 为:SQ7[4:0]~SQ12[4:0],如果通道 1 想第 8 个转换,则 SQ8[4:0] 写 1 即可。SQR1 控制着规则序 列中的第 13 到第 16 个转换,对应位为:SQ13[4:0]~SQ16[4:0],如果通道 6 想第 10 个转换,则 SQ10[4:0] 写 6 即可。具体使用多少个通道,由 SQR1 的位 L[3:0] 决定,最多 16 个通道。

注入序列

注入序列寄存器 JSQR 只有一个,最多支持 4 个通道,具体多少个由 JSQR 的 JL[1:0] 决定。如果 JL 的值小于 4 的话,则 JSQR 跟 SQR 决定转换顺序的设置不一样,第一次转换的不是 JSQR1[4:0], 而是 JCQRx[4:0] ,x =(4-JL),跟 SQR 刚好相反。如果 JL=00(1 个转换),那么转换的顺序是 从 JSQR4[4:0] 开始,而不是从 JSQR1[4:0] 开始,这个要注意,编程的时候不要搞错。当 JL 等于 4 时,跟 SQR 一样。


触发源

通道选好了,转换的顺序也设置好了,那接下来就该开始转换了。ADC 转换可以由 ADC 控制寄 存器 2: ADC_CR2 的 ADON 这个位来控制,写 1 的时候开始转换,写 0 的时候停止转换,这个 是最简单也是最好理解的开启 ADC 转换的控制方式,理解起来没啥技术含量。

除了这种庶民式的控制方法,ADC 还支持外部事件触发转换,这个触发包括内部定时器触发 和外部 IO 触发。触发源有很多,具体选择哪一种触发源,由 ADC 控制寄存器 2:ADC_CR2 的 EXTSEL[2:0] 和 JEXTSEL[2:0] 位来控制。EXTSEL[2:0] 用于选择规则通道的触发源,JEXTSEL[2:0] 用于选择注入通道的触发源。选定好触发源之后,触发源是否要激活,则由 ADC 控制寄存器 2:ADC_CR2 的 EXTTRIG 和 JEXTTRIG 这两位来激活。

如果使能了外部触发事件,我们还可以通过设置 ADC 控制寄存器 2:ADC_CR2 的 EXTEN[1:0] 和 JEXTEN[1:0] 来控制触发极性,可以有 4 种状态,分别是:禁止触发检测、上升沿检测、下降沿 检测以及上升沿和下降沿均检测。


转换时间

ADC 时钟

ADC 输入时钟 ADC_CLK 由 PCLK2 经过分频产生,最大值是 36MHz,典型值为 30MHz,分频 因子由 ADC 通用控制寄存器 ADC_CCR 的 ADCPRE[1:0] 设置,可设置的分频系数有 2、4、6 和 8,注意这里没有 1 分频。对于 STM32F429IGT6 我们一般设置 PCLK2=HCLK/2=84MHz。所以程 序一般使用 4 分频或者 6 分频。

ADC 需要若干个 ADC_CLK 周期完成对输入的电压进行采样,采样的周期数可通过 ADC 采样时 间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0] 位设置,ADC_SMPR2 控制的是通道 0~9, ADC_SMPR1 控制的是通道 10~17。每个通道可以分别用不同的时间采样。其中采样周期最小是 3 个,即如果我们要达到最快的采样,那么应该设置采样周期为 3 个周期,这里说的周期就是 1/ADC_CLK。

ADC 的总转换时间跟 ADC 的输入时钟和采样时间有关,公式为:

Tconv = 采样时间 + 12 个周期

当 ADCCLK = 30MHz,即 PCLK2 为 60MHz,ADC 时钟为 2 分频,采样时间设置为 3 个周期,那 么总的转换时为:Tconv = 3 + 12 = 15 个周期 =0.5us。

一般我们设置 PCLK2=84MHz,经过 ADC 预分频器能分频到最大的时钟只能是 21M,采样周期 设置为 3 个周期,算出最短的转换时间为 0.7142us,这个才是最常用的。


数据寄存器

一切准备就绪后,ADC 转换后的数据根据转换组的不同,规则组的数据放在 ADC_DR 寄存器, 注入组的数据放在 JDRx。如果是使用双重或者三重模式那规矩组的数据是存放在通用规矩寄存 器 ADC_CDR 内的。

规则数据寄存器 ADC_DR

ADC 规则组数据寄存器 ADC_DR 只有一个,是一个 32 位的寄存器,只有低 16 位有效并且只是 用于独立模式存放转换完成数据。因为 ADC 的最大精度是 12 位,ADC_DR 是 16 位有效,这样 允许 ADC 存放数据时候选择左对齐或者右对齐,具体是以哪一种方式存放,由 ADC_CR2 的 11 位 ALIGN 设置。假如设置 ADC 精度为 12 位,如果设置数据为左对齐,那 AD 转换完成数据存 放在 ADC_DR 寄存器的 [4:15] 位内;如果为右对齐,则存放在 ADC_DR 寄存器的 [0:11] 位内。 规则通道可以有 16 个这么多,可规则数据寄存器只有一个,如果使用多通道转换,那转换的数 据就全部都挤在了 DR 里面,前一个时间点转换的通道数据,就会被下一个时间点的另外一个通 道转换的数据覆盖掉,所以当通道转换完成后就应该把数据取走,或者开启 DMA 模式,把数据 传输到内存里面,不然就会造成数据的覆盖。最常用的做法就是开启 DMA 传输。 如果没有使用 DMA 传输,我们一般都需要使用 ADC 状态寄存器 ADC_SR 获取当前 ADC 转换的进度状态,进而进行程序控制。

注入数据寄存器 ADC_JDRx

ADC 注入组最多有 4 个通道,刚好注入数据寄存器也有 4 个,每个通道对应着自己的寄存器,不 会跟规则寄存器那样产生数据覆盖的问题。ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留, 数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由 ADC_CR2 的 11 位 ALIGN 设置。

通用规则数据寄存器 ADC_CDR

规则数据寄存器 ADC_DR 是仅适用于独立模式的,而通用规则数据寄存器 ADC_CDR 是适用于 双重和三重模式的。独立模式就是仅仅适用三个 ADC 的其中一个,双重模式就是同时使用 ADC1 和 ADC2,而三重模式就是三个 ADC 同时使用。在双重或者三重模式下一般需要配合 DMA 数 据传输使用。


中断

转换结束中断

数据转换结束后,可以产生中断,中断分为四种:规则通道转换结束中断,注入转换通道转换结 束中断,模拟看门狗中断和溢出中断。其中转换结束中断很好理解,跟我们平时接触的中断一样, 有相应的中断标志位和中断使能位,我们还可以根据中断类型写相应配套的中断服务程序。

模拟看门狗中断

当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是我们开启了模 拟看门狗中断,其中低阈值和高阈值由 ADC_LTR 和 ADC_HTR 设置。例如我们设置高阈值是 2.5V,那么模拟电压超过 2.5V 的时候,就会产生模拟看门狗中断,反之低阈值也一样。

溢出中断

如果发生 DMA 传输数据丢失,会置位 ADC 状态寄存器 ADC_SR 的 OVR 位,如果同时使能了 溢出中断,那在转换结束后会产生一个溢出中断。

DMA 请求

规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA 请求,把转换好的数据直接 存储在内存里面。对于独立模式的多通道 AD 转换使用 DMA 传输非常有必须要,程序编程简化了很多。对于双重或三重模式使用 DMA 传输几乎可以说是必要的。有关 DMA 请求需要配合 《STM32F4xx 中文参考手册》DMA 控制器这一章节来学习。一般我们在使用 ADC 的时候都会开 启 DMA 传输。


电压转换

模拟电压经过 ADC 转换后,是一个相对精度的数字值,如果通过串口以 16 进制打印出来的话, 可读性比较差,那么有时候我们就需要把数字电压转换成模拟电压,也可以跟实际的模拟电压 (用万用表测)对比,看看转换是否准确。

我们一般在设计原理图的时候会把 ADC 的输入电压范围设定在:0~3.3v,如果设置 ADC 为 12 位的,那么 12 位满量程对应的就是 3.3V,12 位满量程对应的数字值是:2^12。数值 0 对应的就 是 0V。如果转换后的数值为 X ,X 对应的模拟电压为 Y,那么会有这么一个等式成立: 2^12 / 3.3 = X / Y,=> Y = (3.3 * X ) / 2^12。


ADC 初始化结构体详解

ADC_InitTypeDef 结构体

/** 
  * @brief   ADC Init structure definition  
  */ 
typedef struct
{
  uint32_t ADC_Resolution;                /*!< Configures the ADC resolution dual mode. 
                                               This parameter can be a value of @ref ADC_resolution */                                   
  FunctionalState ADC_ScanConvMode;       /*!< Specifies whether the conversion 
                                               is performed in Scan (multichannels) 
                                               or Single (one channel) mode.
                                               This parameter can be set to ENABLE or DISABLE */ 
  FunctionalState ADC_ContinuousConvMode; /*!< Specifies whether the conversion 
                                               is performed in Continuous or Single mode.
                                               This parameter can be set to ENABLE or DISABLE. */
  uint32_t ADC_ExternalTrigConvEdge;      /*!< Select the external trigger edge and
                                               enable the trigger of a regular group. 
                                               This parameter can be a value of 
                                               @ref ADC_external_trigger_edge_for_regular_channels_conversion */
  uint32_t ADC_ExternalTrigConv;          /*!< Select the external event used to trigger 
                                               the start of conversion of a regular group.
                                               This parameter can be a value of 
                                               @ref ADC_extrenal_trigger_sources_for_regular_channels_conversion */
  uint32_t ADC_DataAlign;                 /*!< Specifies whether the ADC data  alignment
                                               is left or right. This parameter can be 
                                               a value of @ref ADC_data_align */
  uint8_t  ADC_NbrOfConversion;           /*!< Specifies the number of ADC conversions
                                               that will be done using the sequencer for
                                               regular channel group.
                                               This parameter must range from 1 to 16. */
}ADC_InitTypeDef;
  

ADC_Resolution:配置 ADC 的分辨率,可选的分辨率有 12 位、10 位、8 位和 6 位。分辨率越高, AD 转换数据精度越高,转换时间也越长;分辨率越低,AD 转换数据精度越低,转换时间也越短。

ScanConvMode:可选参数为 ENABLE 和 DISABLE,配置是否使用扫描。如果是单通道 AD 转换 使用 DISABLE,如果是多通道 AD 转换使用 ENABLE。

ADC_ContinuousConvMode:可选参数为 ENABLE 和 DISABLE,配置是启动自动连续转换还是单 次转换。使用 ENABLE 配置为使能自动连续转换;使用 DISABLE 配置为单次转换,转换一次后 停止需要手动控制才重新启动转换。

ADC_ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发的极性,可 选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均可触发检测。

ADC_ExternalTrigConv:外部触发选择,图单个 ADC 功能框图 中列举了很多外部触发条件,可 根据项目需求配置触发来源。实际上,我们一般使用软件自动触发。

ADC_DataAlign:转换结果数据对齐模式,可选右对齐 ADC_DataAlign_Right 或者左对齐 ADC_DataAlign_Left。一般我们选择右对齐模式。

ADC_NbrOfChannel:AD 转换通道数目。


ADC_CommonInitTypeDef 结构体

ADC 除了有 ADC_InitTypeDef 初始化结构体外,还有一个 ADC_CommonInitTypeDef 通用初始化 结构体。ADC_CommonInitTypeDef 结构体内容决定三个 ADC 共用的工作环境,比如模式选择、 ADC 时钟等等。

ADC_CommonInitTypeDef 结构体也是定义在 stm32_f4xx.h 文件中

/** 
  * @brief   ADC Common Init structure definition  
  */ 
typedef struct 
{
  uint32_t ADC_Mode;                      /*!< Configures the ADC to operate in 
                                               independent or multi mode. 
                                               This parameter can be a value of @ref ADC_Common_mode */                                              
  uint32_t ADC_Prescaler;                 /*!< Select the frequency of the clock 
                                               to the ADC. The clock is common for all the ADCs.
                                               This parameter can be a value of @ref ADC_Prescaler */
  uint32_t ADC_DMAAccessMode;             /*!< Configures the Direct memory access 
                                              mode for multi ADC mode.
                                               This parameter can be a value of 
                                               @ref ADC_Direct_memory_access_mode_for_multi_mode */
  uint32_t ADC_TwoSamplingDelay;          /*!< Configures the Delay between 2 sampling phases.
                                               This parameter can be a value of 
                                               @ref ADC_delay_between_2_sampling_phases */
  
}ADC_CommonInitTypeDef;

ADC_Mode:ADC 工作模式选择,有独立模式、双重模式以及三重模式。

ADC_Prescaler:ADC 时钟分频系数选择,ADC 时钟是有 PCLK2 分频而来,分频系数决定 ADC 时钟频率,可选的分频系数为 2、4、6 和 8。ADC 最大时钟配置为 36MHz。

ADC_DMAAccessMode:DMA 模式设置,只有在双重或者三重模式才需要设置,可以设置三种 模式,具体可参考参考手册说明。

ADC_TwoSamplingDelay:2 个采样阶段之前的延迟,仅适用于双重或三重交错模式。


实验 :独立模式单通道采集实验

STM32 的 ADC 功能繁多,我们设计三个实验尽量完整的展示 ADC 的功能。首先是比较基础实 用的单通道采集,实现开发板上电位器的动触点输出引脚电压的采集并通过串口打印至 PC 端串 口调试助手。

单通道采集适用 AD 转换完成中断,在中断服务函数中读取数据,不使用 DMA 传 输,在多通道采集时才使用 DMA 传输。


编程要点

1) 初始化配置 ADC 目标引脚为模拟输入模式;

2) 使能 ADC 时钟;

3) 配置通用 ADC 为独立模式,采样 4 分频;

4) 设置目标 ADC 为 12 位分辨率,1 通道的连续转换,不需要外部触发;

5) 设置 ADC 转换通道顺序及采样时间;

6) 配置使能 ADC 转换完成中断,在中断内读取转换完数据;

7) 启动 ADC 转换;

8) 使能软件触发 ADC 转换。 ADC 转换结果数据使用中断方式读取,这里没有使用 DMA 进行数据传输。


宏定义

// ADC GPIO 宏定义
#define RHEOSTAT_ADC_GPIO_PORT    GPIOB
#define RHEOSTAT_ADC_GPIO_PIN     GPIO_Pin_0
#define RHEOSTAT_ADC_GPIO_CLK     RCC_AHB1Periph_GPIOB

// ADC 序号宏定义
#define RHEOSTAT_ADC              ADC1
#define RHEOSTAT_ADC_CLK          RCC_APB2Periph_ADC1
#define RHEOSTAT_ADC_CHANNEL      ADC_Channel_8


// ADC 中断宏定义
#define Rheostat_ADC_IRQ            ADC_IRQn
#define Rheostat_ADC_INT_FUNCTION   ADC_IRQHandler

ADC123共用一个中断源


GPIO ADC NVIC初始化

static void Rheostat_ADC_GPIO_Config(void)
{
		GPIO_InitTypeDef GPIO_InitStructure;
	
	// 使能 GPIO 时钟
	RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_GPIO_CLK, ENABLE);
		
	// 配置 IO
	GPIO_InitStructure.GPIO_Pin = RHEOSTAT_ADC_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;	    
  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ; //不上拉不下拉
	GPIO_Init(RHEOSTAT_ADC_GPIO_PORT, &GPIO_InitStructure);		
}

static void Rheostat_ADC_Mode_Config(void)
{
	ADC_InitTypeDef ADC_InitStructure;
  ADC_CommonInitTypeDef ADC_CommonInitStructure;
	
  // 开启ADC时钟
	RCC_APB2PeriphClockCmd(RHEOSTAT_ADC_CLK , ENABLE);

  // -------------------ADC Common 结构体 参数 初始化------------------------
	// 独立ADC模式
  ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
  // 时钟为fpclk x分频	
  ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
  // 禁止DMA直接访问模式	
  ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
  // 采样时间间隔	
  ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;  
  ADC_CommonInit(&ADC_CommonInitStructure);
	
  // -------------------ADC Init 结构体 参数 初始化--------------------------
	ADC_StructInit(&ADC_InitStructure);
  // ADC 分辨率
  ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
  // 禁止扫描模式,多通道采集才需要	
  ADC_InitStructure.ADC_ScanConvMode = DISABLE; 
  // 连续转换	
  ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; 
  //禁止外部边沿触发
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  //外部触发通道,本例子使用软件触发,此值随便赋值即可
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  //数据右对齐	
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  //转换通道 1个
  ADC_InitStructure.ADC_NbrOfConversion = 1;                                    
  ADC_Init(RHEOSTAT_ADC, &ADC_InitStructure);
  //---------------------------------------------------------------------------
	
  // 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
  ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_56Cycles);
	// ADC 转换结束产生中断,在中断服务程序中读取转换值
	

	ADC_ITConfig(RHEOSTAT_ADC, ADC_IT_EOC, ENABLE);
  // 使能ADC
  ADC_Cmd(RHEOSTAT_ADC, ENABLE);  
  //开始adc转换,软件触发
  ADC_SoftwareStartConv(RHEOSTAT_ADC);
}

static void Rheostat_ADC_NVIC_Config(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	
  NVIC_InitStructure.NVIC_IRQChannel = Rheostat_ADC_IRQ;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	
  NVIC_Init(&NVIC_InitStructure);
}

开启时钟,配置结构体参数 ,配置中断,单通道禁用扫描。

引脚为模拟输入模式 。


中断服务函数

extern __IO uint16_t ADC_ConvertedValue;

// ADC 转换完成中断服务程序
void ADC_IRQHandler(void)
{
	if(ADC_GetITStatus(RHEOSTAT_ADC,ADC_IT_EOC)==SET)
	{
  // 读取ADC的转换值
		ADC_ConvertedValue = ADC_GetConversionValue(RHEOSTAT_ADC);

	}
	ADC_ClearITPendingBit(RHEOSTAT_ADC,ADC_IT_EOC);

}	

我们在中断服务函数内直接读取 ADC 转换结果保存在变量 ADC_ConvertedValue(在 main.c 中定义) 中。

ADC_GetConversionValue 函数是获取 ADC 转换结果值的库函数,只有一个形参为 ADC 外设,可 选为 ADC1、ADC2 或 ADC3,该函数还返回一个 16 位的 ADC 转换结果值。


主函数

#include "stm32f4xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./adc/bsp_adc.h"


// ADC转换的电压值通过MDA方式传到SRAM
__IO uint16_t ADC_ConvertedValue = 0;

// 局部变量,用于保存转换计算后的电压值 	 
float ADC_Vol; 

static void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{	
  /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
  Debug_USART_Config();
	Rheostat_Init();	
	
    while (1)
    {
    	
			ADC_Vol =(float) ADC_ConvertedValue/4096*(float)3.3; // 读取转换的AD值

      printf("\r\n The current AD value = 0x%04X \r\n", ADC_ConvertedValue); 
      printf("\r\n The current AD value = %f V \r\n",ADC_Vol);     

      Delay(0xffffee);  
    }
}

打印采集结果


独立模式多通道采集实验

编程要点

1) 初始化配置 ADC 目标引脚为模拟输入模式;

2) 使能 ADC 时钟和 DMA 时钟;

3) 配置 DMA 从 ADC 规矩数据寄存器传输数据到我们指定的存储区;

4) 配置通用 ADC 为独立模式,采样 4 分频;

5) 设置 ADC 为 12 位分辨率,启动扫描,连续转换,不需要外部触发;

6) 设置 ADC 转换通道顺序及采样时间;

7) 使能 DMA 请求,DMA 在 AD 转换完自动传输数据到指定的存储区;

8) 启动 ADC 转换;

9) 使能软件触发 ADC 转换。 ADC 转换结果数据使用 DMA 方式传输至指定的存储区,这样取代单通道实验使用中断服务的 读取方法。实际上,多通道 ADC 采集一般使用 DMA 数据传输方式更加高效方便。


宏定义

#define RHEOSTAT_NOFCHANEL      3

/*=====================通道1 IO======================*/
// PB0 通过调帽接电位器
// ADC IO宏定义
#define RHEOSTAT_ADC_GPIO_PORT1    GPIOB
#define RHEOSTAT_ADC_GPIO_PIN1     GPIO_Pin_0
#define RHEOSTAT_ADC_GPIO_CLK1     RCC_AHB1Periph_GPIOB
#define RHEOSTAT_ADC_CHANNEL1      ADC_Channel_8
/*=====================通道2 IO ======================*/
// PB1 通过调帽接光敏电阻
// ADC IO宏定义
#define RHEOSTAT_ADC_GPIO_PORT2    GPIOB
#define RHEOSTAT_ADC_GPIO_PIN2     GPIO_Pin_1
#define RHEOSTAT_ADC_GPIO_CLK2     RCC_AHB1Periph_GPIOB
#define RHEOSTAT_ADC_CHANNEL2      ADC_Channel_9
/*=====================通道3 IO ======================*/
// PA6 悬空,可用杜邦线接3V3或者GND来实验
// ADC IO宏定义
#define RHEOSTAT_ADC_GPIO_PORT3    GPIOA
#define RHEOSTAT_ADC_GPIO_PIN3     GPIO_Pin_6
#define RHEOSTAT_ADC_GPIO_CLK3     RCC_AHB1Periph_GPIOA
#define RHEOSTAT_ADC_CHANNEL3     ADC_Channel_6

// ADC 序号宏定义
#define RHEOSTAT_ADC              ADC1
#define RHEOSTAT_ADC_CLK          RCC_APB2Periph_ADC1
// ADC DR寄存器宏定义,ADC转换后的数字值则存放在这里
#define RHEOSTAT_ADC_DR_ADDR    ((u32)ADC1+0x4c)


// ADC DMA 通道宏定义,这里我们使用DMA传输
#define RHEOSTAT_ADC_DMA_CLK      RCC_AHB1Periph_DMA2
#define RHEOSTAT_ADC_DMA_CHANNEL  DMA_Channel_0
#define RHEOSTAT_ADC_DMA_STREAM   DMA2_Stream0

GPIO初始化,没什么好说的,通道对应的引脚,打开时钟,模拟输入模式,不上拉不下拉 就完事了。

直接看ADC和DMA的初始化。

static void Rheostat_ADC_Mode_Config(void)
{
	DMA_InitTypeDef DMA_InitStructure;
	ADC_InitTypeDef ADC_InitStructure;
  ADC_CommonInitTypeDef ADC_CommonInitStructure;
	
  // ------------------DMA Init 结构体参数 初始化--------------------------
  // ADC1使用DMA2,数据流0,通道0,这个是手册固定死的
  // 开启DMA时钟
  RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_DMA_CLK, ENABLE); 
	// 外设基址为:ADC 数据寄存器地址
	DMA_InitStructure.DMA_PeripheralBaseAddr = RHEOSTAT_ADC_DR_ADDR;	
  // 存储器地址,实际上就是一个内部SRAM的变量	
	DMA_InitStructure.DMA_Memory0BaseAddr = (u32)ADC_ConvertedValue;  
  // 数据传输方向为外设到存储器	
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;	
	// 缓冲区大小为,指一次传输的数据量
	DMA_InitStructure.DMA_BufferSize = RHEOSTAT_NOFCHANEL;	
	// 外设寄存器只有一个,地址不用递增
	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_Circular;
  // DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  // 禁止DMA FIFO	,使用直连模式
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;  
  // FIFO 大小,FIFO模式禁止时,这个不用配置	
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;  
	// 选择 DMA 通道,通道存在于流中
  DMA_InitStructure.DMA_Channel = RHEOSTAT_ADC_DMA_CHANNEL; 
  //初始化DMA流,流相当于一个大的管道,管道里面有很多通道
	DMA_Init(RHEOSTAT_ADC_DMA_STREAM, &DMA_InitStructure);
	// 使能DMA流
  DMA_Cmd(RHEOSTAT_ADC_DMA_STREAM, ENABLE);
	
	// 开启ADC时钟
	RCC_APB2PeriphClockCmd(RHEOSTAT_ADC_CLK , ENABLE);
  // -------------------ADC Common 结构体 参数 初始化------------------------
	// 独立ADC模式
  ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;
  // 时钟为fpclk x分频	
  ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
  // 禁止DMA直接访问模式	
  ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled;
  // 采样时间间隔	
  ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;  
  ADC_CommonInit(&ADC_CommonInitStructure);
	
  // -------------------ADC Init 结构体 参数 初始化--------------------------
	ADC_StructInit(&ADC_InitStructure);
  // ADC 分辨率
  ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
  // 扫描模式,多通道采集需要	
  ADC_InitStructure.ADC_ScanConvMode = ENABLE; 
  // 连续转换	
  ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; 
  //禁止外部边沿触发
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  //外部触发通道,本例子使用软件触发,此值随便赋值即可
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  //数据右对齐	
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  //转换通道 1个
  ADC_InitStructure.ADC_NbrOfConversion = RHEOSTAT_NOFCHANEL;                                    
  ADC_Init(RHEOSTAT_ADC, &ADC_InitStructure);
  //---------------------------------------------------------------------------
	
  // 配置 ADC 通道转换顺序和采样时间周期
  ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL1, 1, 
	                         ADC_SampleTime_3Cycles);
  ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL2, 2, 
	                         ADC_SampleTime_3Cycles); 
  ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL3, 3, 
	                         ADC_SampleTime_3Cycles); 

  // 使能DMA请求 after last transfer (Single-ADC mode)
  ADC_DMARequestAfterLastTransferCmd(RHEOSTAT_ADC, ENABLE);
  // 使能ADC DMA
  ADC_DMACmd(RHEOSTAT_ADC, ENABLE);
	
	// 使能ADC
  ADC_Cmd(RHEOSTAT_ADC, ENABLE);  
  //开始adc转换,软件触发
  ADC_SoftwareStartConv(RHEOSTAT_ADC);
}

首先,我们使用了 DMA_InitTypeDef 定义了一个 DMA 初始化类型变量,该结构体内容我们在 DMA 篇已经做了非常详细的讲解;另外还使用 ADC_InitTypeDef 和 ADC_CommonInitTypeDef 结 构体分别定义一个 ADC 初始化和 ADC 通用类型变量,这两个结构体我们之前已经有详细讲解。

调用 RCC_APB2PeriphClockCmd() 开启 ADC 时钟以及 RCC_AHB1PeriphClockCmd() 开启 DMA 时 钟。

我们需要对 DMA 进行必要的配置。首先设置外设基地址就是 ADC 的规则数据寄存器地址;存 储器的地址就是我们指定的数据存储区空间,ADC_ConvertedValue 是我们定义的一个全局数组 名,它是一个无符号 16 位含有 4 个元素的整数数组;ADC 规则转换对应只有一个数据寄存器所 以地址不能递增,而我们定义的存储区是专门用来存放不同通道数据的,所以需要自动地址递 增。ADC 的规则数据寄存器只有低 16 位有效,实际存放的数据只有 12 位而已,所以设置数据 大小为半字大小。ADC 配置为连续转换模式 DMA 也设置为循环传输模式。设置好 DMA 相关参 数后就使能 DMA 的 ADC 通道。

接下来我们使用 ADC_CommonInitTypeDef 结构体变量 ADC_CommonInitStructure 来配置 ADC 为 独立模式、分频系数为 4、不需要设置 DMA 模式、20 个周期的采样延迟,并调用 ADC_CommonInit 函数完成 ADC 通用工作环境配置。

我们使用 ADC_InitTypeDef 结构体变量 ADC_InitStructure 来配置 ADC1 为 12 位分辨率、使能扫 描模式、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数据格式、转换通道 为 4,并调用 ADC_Init 函数完成 ADC3 工作环境配置。

ADC_RegularChannelConfig 函数用来绑定 ADC 通道转换顺序和采样时间。分别绑定四个 ADC 通 道引脚并设置相应的转换顺序。

ADC_DMARequestAfterLastTransferCmd 函数控制是否使能 ADC 的 DMA 请求,如果使能请求,并 调用 ADC_DMACmd 函数使能 DMA,则在 ADC 转换完成后就请求 DMA 实现数据传输。

ADC_Cmd 函数控制 ADC 转换启动和停止。

最后,如果使用软件触发需要调用 ADC_SoftwareStartConvCmd 函数进行使能配置。

和单通道区别就是打开扫描 和 用 ADC_RegularChannelConfig()函数 配置 ADC 通道转换顺序和采样时间周期


主函数

#include "stm32f4xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./adc/bsp_adc.h"

// ADC转换的电压值通过MDA方式传到SRAM
extern __IO uint16_t ADC_ConvertedValue[RHEOSTAT_NOFCHANEL];

// 局部变量,用于保存转换计算后的电压值 	 
float ADC_ConvertedValueLocal[RHEOSTAT_NOFCHANEL]={0}; 

static void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{	
  /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
  Debug_USART_Config();
	Rheostat_Init();	
	
	while (1)
	{
		ADC_ConvertedValueLocal[0] =(float) ADC_ConvertedValue[0]/4096*(float)3.3;
		ADC_ConvertedValueLocal[1] =(float) ADC_ConvertedValue[1]/4096*(float)3.3;
		ADC_ConvertedValueLocal[2] =(float) ADC_ConvertedValue[2]/4096*(float)3.3;
		
		printf("\r\n CH1_C3 value = %f V \r\n",ADC_ConvertedValueLocal[0]);
		printf("\r\n CH2_PA4 value = %f V \r\n",ADC_ConvertedValueLocal[1]);
		printf("\r\n CH3_PA6 value = %f V \r\n",ADC_ConvertedValueLocal[2]);
		
		printf("\r\n\r\n");
		Delay(0xffffff);  
	}
}

直接打印三路测量数值。


三重 ADC 交替模式采集实验

三重 ADC 交叉模式是针对同一个通道的 ADC 采集模式,这种情况跟 单通道实验非 常类似,只是同时使用三个 ADC 对同一通道进行采集,所以电路设计与之相同即可

AD 转换包括采样阶段和转换阶段,在采样阶段才对通道数据进行采集;而在转换阶段只是将采 集到的数据进行转换为数字量输出,此刻通道数据变化不会改变转换结果。独立模式的 ADC 采 集需要在一个通道采集并且转换完成后才会进行下一个通道的采集。

双重或者三重 ADC 的机制 使用两个或以上 ADC 同时采样两个或以上不同通道的数据或者使用两个或以上 ADC 交叉采集 同一通道的数据。双重或者三重 ADC 模式较独立模式一个最大的优势就是转换速度快。

我们这里只介绍三重 ADC 交替模式,关于双重或者三重 ADC 的其他模式与之类似,可以参考 三重 ADC 交替模式使用。

三重 ADC 交替模式是针对同一通道的使用三个 ADC 交叉采集,就是 在 ADC1 采样完等几个时钟周期后 ADC2 开始采样,此时 ADC1 处在转换阶段,当 ADC2 采样 完成再等几个时钟周期后 ADC3 就进行采样此时 ADC1 和 ADC2 处在转换阶段,如果 ADC3 采 样完成并且 ADC1 已经转换完成那么就可以准备下一轮的循环,这样充分利用转换阶段时间达 到增快采样速度的效果。AD 转换过程见图三重 ADC 交叉模式 ,利用 ADC 的转换阶段时间另外 一个 ADC 进行采样,而不用像独立模式必须等待采样和转换结束后才进行下一次采样及转换。


编程要点

1) 初始化配置 ADC 目标引脚为模拟输入模式;

2) 使能 ADC1、ADC2、ADC3 以及 DMA 时钟;

3) 配置 DMA 控制将 ADC 通用规矩数据寄存器数据转存到指定存储区;

4) 配置通用 ADC 为三重 ADC 交替模式,采样 4 分频,使用 DMA 模式 2;

5) 设置 ADC1、ADC2 和 ADC3 为 12 位分辨率,禁用扫描,连续转换,不需要外部触发;

6) 设置 ADC1、ADC2 和 ADC3 转换通道顺序及采样时间;

7) 使能 ADC1 的 DMA 请求,在 ADC 转换完后自动请求 DMA 进行数据传输;

8) 启动 ADC1、ADC2 和 ADC3 转换;

9) 使能软件触发 ADC 转换。 ADC 转换结果数据使用 DMA 方式传输至指定的存储区,这样取代单通道实验使用中断服务的 读取方法。


宏定义

// ADC GPIO 宏定义
#define RHEOSTAT_ADC_GPIO_PORT    GPIOC
#define RHEOSTAT_ADC_GPIO_PIN     GPIO_Pin_2
#define RHEOSTAT_ADC_GPIO_CLK     RCC_AHB1Periph_GPIOC

// ADC 序号宏定义
#define RHEOSTAT_ADC1             ADC1
#define RHEOSTAT_ADC1_CLK         RCC_APB2Periph_ADC1
#define RHEOSTAT_ADC2             ADC2
#define RHEOSTAT_ADC2_CLK         RCC_APB2Periph_ADC2
#define RHEOSTAT_ADC3             ADC3
#define RHEOSTAT_ADC3_CLK         RCC_APB2Periph_ADC3
#define RHEOSTAT_ADC_CHANNEL      ADC_Channel_12
// ADC CDR寄存器宏定义,ADC转换后的数字值则存放在这里
#define RHEOSTAT_ADC_CDR_ADDR    ((uint32_t)0x40012308)

// ADC DMA 通道宏定义,这里我们使用DMA传输
#define RHEOSTAT_ADC_DMA_CLK      RCC_AHB1Periph_DMA2
#define RHEOSTAT_ADC_DMA_CHANNEL  DMA_Channel_0
#define RHEOSTAT_ADC_DMA_STREAM   DMA2_Stream0

双重或者三重 ADC 需要使用通用规则数据寄存器 ADC_CDR,这点跟独立模式不同。定义电位 器动触点引脚作为三重 ADC 的模拟输入,三个 ADC 都是采集同一路信号。


ADC GPIO 初始化函数

static void Rheostat_ADC_Mode_Config(void)
{
	DMA_InitTypeDef DMA_InitStructure;
	ADC_InitTypeDef ADC_InitStructure;
  ADC_CommonInitTypeDef ADC_CommonInitStructure;
	
  // ------------------DMA Init 结构体参数 初始化--------------------------
  // ADC1使用DMA2,数据流0,通道0,这个是手册固定死的
  // 开启DMA时钟
  RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_DMA_CLK, ENABLE); 
	// 外设基址为:ADC 数据寄存器地址
	DMA_InitStructure.DMA_PeripheralBaseAddr = RHEOSTAT_ADC_CDR_ADDR;	
  // 存储器地址,实际上就是一个内部SRAM的变量	
	DMA_InitStructure.DMA_Memory0BaseAddr = (u32)ADC_ConvertedValue;  
  // 数据传输方向为外设到存储器	
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;	
	// 缓冲区大小为,指一次传输的数据量
	DMA_InitStructure.DMA_BufferSize = 3;	
	// 外设寄存器只有一个,地址不用递增
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  // 存储器地址固定
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 
  // // 外设数据大小为半字,即两个字节 
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; 
  //	存储器数据大小也为半字,跟外设数据大小相同
	DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Word;	
	// 循环传输模式
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  // DMA 传输通道优先级为高,当使用一个DMA通道时,优先级设置不影响
	DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  // 禁止DMA FIFO	,使用直连模式
  DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;  
  // FIFO 大小,FIFO模式禁止时,这个不用配置	
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;  
	// 选择 DMA 通道,通道存在于流中
  DMA_InitStructure.DMA_Channel = RHEOSTAT_ADC_DMA_CHANNEL; 
  //初始化DMA流,流相当于一个大的管道,管道里面有很多通道
	DMA_Init(RHEOSTAT_ADC_DMA_STREAM, &DMA_InitStructure);
	// 使能DMA流
  DMA_Cmd(RHEOSTAT_ADC_DMA_STREAM, ENABLE);
	
	// 开启ADC时钟
	RCC_APB2PeriphClockCmd(RHEOSTAT_ADC1_CLK , ENABLE);
  RCC_APB2PeriphClockCmd(RHEOSTAT_ADC2_CLK , ENABLE);
	RCC_APB2PeriphClockCmd(RHEOSTAT_ADC3_CLK , ENABLE);
	
  // -------------------ADC Init 结构体 参数 初始化--------------------------
	ADC_StructInit(&ADC_InitStructure);
	// 独立ADC模式
  ADC_CommonInitStructure.ADC_Mode = ADC_TripleMode_Interl;
  // 时钟为fpclk x分频	
  ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4;
  // 禁止DMA直接访问模式	
  ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_2;
  // 采样时间间隔	
  ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_20Cycles;  
  ADC_CommonInit(&ADC_CommonInitStructure);
	
  // -------------------ADC Init 结构体 参数 初始化--------------------------
  // ADC 分辨率
  ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
  // 禁止扫描模式,多通道采集才需要	
  ADC_InitStructure.ADC_ScanConvMode = DISABLE; 
  // 连续转换	
  ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; 
  //禁止外部边沿触发
  ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;
  //外部触发通道,本例子使用软件触发,此值随便赋值即可
  ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
  //数据右对齐	
  ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  //转换通道 1个
  ADC_InitStructure.ADC_NbrOfConversion = 1; 
	
  ADC_Init(RHEOSTAT_ADC1, &ADC_InitStructure);
  //---------------------------------------------------------------------------
	
   // 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
  ADC_RegularChannelConfig(RHEOSTAT_ADC1, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles);   
  //---------------------------------------------------------------------------
	
	ADC_Init(RHEOSTAT_ADC2, &ADC_InitStructure);
  // 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
  ADC_RegularChannelConfig(RHEOSTAT_ADC2, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles);   
  //---------------------------------------------------------------------------
	
	ADC_Init(RHEOSTAT_ADC3, &ADC_InitStructure);
  // 配置 ADC 通道转换顺序为1,第一个转换,采样时间为3个时钟周期
  ADC_RegularChannelConfig(RHEOSTAT_ADC3, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles); 
	

  // 使能DMA请求 after last transfer (multi-ADC mode)
  ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE);
  // 使能ADC DMA
  ADC_DMACmd(RHEOSTAT_ADC1, ENABLE);
	
  // 使能ADC
  ADC_Cmd(RHEOSTAT_ADC1, ENABLE);  
  ADC_Cmd(RHEOSTAT_ADC2, ENABLE);  
  ADC_Cmd(RHEOSTAT_ADC3, ENABLE);  
  
  //开始adc转换,软件触发
  ADC_SoftwareStartConv(RHEOSTAT_ADC1);
//  ADC_SoftwareStartConv(RHEOSTAT_ADC2);
//  ADC_SoftwareStartConv(RHEOSTAT_ADC3);
}

首先,我们使用了 DMA_InitTypeDef 定义了一个 DMA 初始化类型变量,该结构体内容我们在 DMA 篇已经做了非常详细的讲解;另外还使用 ADC_InitTypeDef 和 ADC_CommonInitTypeDef 结 构体分别定义一个 ADC 初始化和 ADC 通用类型变量,这两个结构体我们之前已经有详细讲解。

调用 RCC_APB2PeriphClockCmd() 开启 ADC 时钟以及 RCC_AHB1PeriphClockCmd() 开启 DMA 时 钟。

我们需要对 DMA 进行必要的配置。首先设置外设基地址就是 ADC 的通用规则数据寄存器地址; 存储器的地址就是我们指定的数据存储区空间,ADC_ConvertedValue 是我们定义的一个全局数 组名,它是一个无符号 32 位有三个元素的整数数字;ADC 规则转换对应只有一个数据寄存器所 以地址不能递增,我们指定的存储区也需要递增地址。ADC 的通用规则数据寄存器是 32 位有效, 我们配置 ADC 为 DMA 模式 2,设置数据大小为字大小。ADC 配置为连续转换模式 DMA 也设 置为循环传输模式。设置好 DMA 相关参数后就使能 DMA 的 ADC 通道。

接下来我们使用 ADC_CommonInitTypeDef 结构体变量 ADC_CommonInitStructure 来配置 ADC 为 三重 ADC 交替模式、分频系数为 4、需要设置 DMA 模式 2、10 个周期的采样延迟,并调用 ADC_CommonInit 函数完成 ADC 通用工作环境配置。

我们使用 ADC_InitTypeDef 结构体变量 ADC_InitStructure 来配置 ADC1 为 12 位分辨率、不使用 扫描模式、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数据格式、转换通 道为 1,并调用 ADC_Init 函数完成 ADC1 工作环境配置。ADC2 和 ADC3 使用与 ADC1 相同配 置即可。

ADC_RegularChannelConfig 函数用来绑定 ADC 通道转换顺序和采样时间。绑定 ADC 通道引脚并 设置相应的转换顺序。

ADC_MultiModeDMARequestAfterLastTransferCmd 函数控制是否使能 ADC 的 DMA 请求,如果使 能请求,并调用 ADC_DMACmd 函数使能 DMA,则在 ADC 转换完成后就请求 DMA 实现数据 传输。三重模式只需使能 ADC1 的 DMA 通道。

ADC_Cmd 函数控制 ADC 转换启动和停止。

最后,如果使用软件触发需要调用 ADC_SoftwareStartConvCmd 函数进行使能配置。


#include "stm32f4xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./adc/bsp_adc.h"


// ADC1转换的电压值通过MDA方式传到SRAM
extern __IO uint32_t ADC_ConvertedValue[3];

// 局部变量,用于保存转换计算后的电压值 	 
float ADC_ConvertedValueLocal[3]; 

static void Delay(__IO uint32_t nCount)	 //简单的延时函数
{
	for(; nCount != 0; nCount--);
}

/**
  * @brief  主函数
  * @param  无
  * @retval 无
  */
int main(void)
{	
  /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
  Debug_USART_Config();
	Rheostat_Init();	
	
  while (1)
  {
    Delay(0xffffee);  
    
    ADC_ConvertedValueLocal[0] =(float)((uint16_t)ADC_ConvertedValue[0]*3.3/4096); 
    ADC_ConvertedValueLocal[1] =(float)((uint16_t)ADC_ConvertedValue[1]*3.3/4096);
    ADC_ConvertedValueLocal[2] =(float)((uint16_t)ADC_ConvertedValue[2]*3.3/4096);
    
    printf("\r\n The current AD value = 0x%08X \r\n", ADC_ConvertedValue[0]); 
    printf("\r\n The current AD value = 0x%08X \r\n", ADC_ConvertedValue[1]); 
    printf("\r\n The current AD value = 0x%08X \r\n", ADC_ConvertedValue[2]); 
    
    printf("\r\n The current ADC1 value = %f V \r\n",ADC_ConvertedValueLocal[0]); 
    printf("\r\n The current ADC2 value = %f V \r\n",ADC_ConvertedValueLocal[1]);
    printf("\r\n The current ADC3 value = %f V \r\n",ADC_ConvertedValueLocal[2]);
  }
}

我们配置了 DMA 数据传输所以它会自动把 ADC 转换完成后数据保存到数组变量 ADC_ConvertedValue 内,根据 DMA 模式 2 的数据存放规则,ADC_ConvertedValue[0] 的低 16 位存放 ADC1 数据、高 16 位存放 ADC2 数据,ADC_ConvertedValue[1] 的低 16 位存放 ADC3 数 据、高 16 位存放 ADC1 数据,ADC_ConvertedValue[2] 的低 16 位存放 ADC2 数据、高 16 位存放 ADC3 数据

我们可以根据需要提取出对应 ADC 的转换结果数据。经过简单地计算就可以得到 每个 ADC 对应的实际电压。

最后就是把相关数据打印至串口调试助手。


最后

总结一下,在此文中先后介绍了GPIO工作模式,结构体等相关知识点,还有系统的时钟框架,无论是延时函数,还是定时器的配置都和时钟树息息相关,然后就是对于中断的理解,首先是NVIC中断控制器,几乎所有的中断都由NVIC控制,有中断就要初始化NVIC,且中断的函数名字必须要与中断向量表里的名字一样,之后就是外部中断,相关应用及配置方法已经写出。最后就是串口以及定时器的配置原理和各种工作模式需要掌握,这些都搞懂了之后对于单片机的理解整体就会愈发清晰,可以决定往下面学的的内容,比如物联网模块,摄像头,RTOS系统,树莓派等等

先写这么多,感谢你读到此处,我也独自走了很远的路,才将这篇文章将送到你的眼前。

身处命运的漩涡,不断前进的信念永远是我最强大的武器。

祝你一路顺风。

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;