【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了


定时器

 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的波形图

STM32F103C8T6这款单片机一共有2个高级定时器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

相关DMA内容转载于  https://blog.csdn.net/as480133937/article/details/104927922

DMA的基本介绍


什么是DMA (DMA的基本定义)
DMA,全称Direct Memory Access,即直接存储器访问。

DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。

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

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

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

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

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

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

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

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

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。
STM32少个DMA资源?
对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。
每个通道都可以配置一些外设的地址。

①DMA1 controller

从外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个DMA请求,通过逻辑或输入到DMA1控制器 其中每个通道都对应着具体的外设:

② DMA2 controller

从外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,其中每个通道都对应着具体的外设:

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

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

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库函数

1.DMA初始化函数

 DMA_DeInit(DMAX_ChannelX);

功能:将DMAyChannelx寄存器的初始化为其默认值

注释:RCC_ResetCmd中对DMA无定义,因此采用的直接操纵DMA寄存器的方式

void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx,  DMA_InitTypeDef* DMA_InitStruct) 

功能:设置要开启的通道,还有一些参数,包括外设基地址,存储器基地址,传输的数据量,增量模式,数据宽度等。

具体看下方结构体代码介绍:

typedef struct {  
 uint32_t DMA_PeripheralBaseAddr;   
 /*设置DMA源地址*/
 uint32_t DMA_MemoryBaseAddr;       
 /*设置DMA目的地址*/
 uint32_t DMA_DIR; 
 /* 设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数 据发送到外设,也就是外设是源地还是目的地
 */                       
 uint32_t DMA_BufferSize;      
 /*设置传输大小*/    
 uint32_t DMA_PeripheralInc;       
 /*设置ReceiveBuff地址是否自增*/      
 uint32_t DMA_MemoryInc; 
 /*设置传输数据时候内存地址是否递,需要开启*/       
 uint32_t DMA_PeripheralDataSize;   
 /*外设的数据长度是为字节传输(8bits),半 字传输(16bits) 还是字传输(32bits) */    
 uint32_t DMA_MemoryDataSize;    
   /*设置内存的数据长度*/
 uint32_t DMA_Mode;      
 /*设置DMA的模式,正常模式/循环模式  是否循环发送*/        
 uint32_t DMA_Priority; 
  /*设置 DMA 通道的优先级,有低,中,高,超高四种模式*/        
 uint32_t DMA_M2M;    
  /*设置是否是存储器到存储器模式传输,*/       }
 DMA_InitTypeDef; 

2.DMA使能函数

void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState  NewState);

功能:使能或者失能DMA外设

例如:DMA_Cmd(DMA1_Channel1 , ENABLE);
 3.DMA中断使能函数

void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT,  FunctionalState NewState);

功能:配置指定的DMAy通道x的中断

注释:DMA_IT_TC:传输完成 DMA_IT_HT:传输一半 DMA_IT_TE:传输错误

例如:DMA_ITConfig(DMA1_Channel1 , DMA_IT_TC , ENABLE);

4.设置CNDTRx和读CNDTRx函数 (通道传输数据量)

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t  DataNumber);
 uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

作用:前者设置DMA通道的传输数据量(DMA处于关闭状态);后者获取当前DMA通道传输剩余数据量(DMA处于开启状态)。

DMA库函数配置过程:
使能DMA时钟:RCC_AHBPeriphClockCmd();
初始化DMA通道:DMA_Init();
//设置通道;传输地址;传输方向;传输数据的数目;传输数据宽度;传输模式;优先级;是否开启存储器到存储器。
使能外设DMA;
以串口为例:使能串口DMA发送,串口DMA使能函数。调用函数:USART_DMACmd();
使能DMA通道传输;函数:DMA_Cmd();
查询DMA传输状态。函数:DMA_GetFlagStatus();
获取当前剩余数据量大小 函数: DMA_GetCurrDataCounter(DMA1_Channel4);
UART DMA传输
DMA就是一个搬运工,可以将数据从一个位置搬运到另一个位置。
以UART为例,如果要接收数据,会触发UART中断,然后CPU介入,在中断中通过CPU将UART输入寄存器的值读出来,存放到内存中;
而DMA方式,产生UART中断后,DMA直接参与,把UART输入寄存器的值搬运到内存中,CPU只需要在去检查内存的值就好了,这样提高了CPU的效率。

DMA代码配置

① DMA初始化配置

void dma_init()
{
 
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

/*DMA配置*/

DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base;//串口数据寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuff; //内存地址(要传输的变量的指针)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //方向(从内存到外设)
DMA_InitStructure.DMA_BufferSize = 500; //传输内容的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址自增
DMA_InitStructure.DMA_PeripheralDataSize =
DMA_PeripheralDataSize_Byte ; //外设数据单位
DMA_InitStructure.DMA_MemoryDataSize =
DMA_MemoryDataSize_Byte ; //内存数据单位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ; //DMA模式:一次传输,循环
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium ; //优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //禁止内存到内存的传输
 
DMA_Init(DMA1_Channel4, &DMA_InitStructure); //配置DMA1的4通道
DMA_Cmd(DMA1_Channel4,ENABLE);
DMA_SetCurrDataCounter(DMA_CH4,DMA1_MEM_LEN);//DMA通道的DMA缓存的大小
DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE);//配置DMA发送完成后产生中断
 
}

DMA中断

void DMA1_Channel4_IRQHandler(void)
{
	if(DMA_GetFlagStatus(DMA1_FLAG_TC4)==SET)
	{
	
		DMA_ClearFlag(DMA1_FLAG_TC4);
	}
}

main函数

#define SEND_BUF_SIZE 500	//发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍.
 
u8 SendBuff[SEND_BUF_SIZE];	//发送数据缓冲区
const u8 TEXT_TO_SEND[]={"STM32F1 DMA 串口实验"};
 uint16_t i;
int main(void)
{	   
	uart_init(115200);	 	//串口初始化为115200
		
	for(i=0;i<500;i++)
	{
	SendBuff[i] =0xaf;
	}	
	USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);  //使能串口dma传输 
	
	while(1);
=
}


最后

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

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

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

祝你一路顺风。

  • 42
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值