注意:本文作为学习笔记整理之用,并非完全原创。主要基于*下载的《正点原子STM32开发手册——标准库篇》,并转载整合了部分博客。如有侵权,请向我提出。
- 2021.9.20:整理了一部分外设配置方法。CAN,ADC,DAC,DMA,FLASH,I2C,RTC,SPI,SYSTICK等外设配置尚未整理,将不定时更新。
1. JTAG、SWD、仿真器
1.1 JTAG
JTAG(Joint Test Action Group,联合测试行动小组)是一种国际标准测试协议(IEEE 1149.1兼容),主要用于芯片内部测试。现在多数的高级器件都支持JTAG协议,如ARM、DSP、FPGA器件等。标准的JTAG接口是4线:TMS、 TCK、TDI、TDO,分别为模式选择、时钟、数据输入和数据输出线。 相关JTAG引脚的定义为:
- TMS:测试模式选择,TMS用来设置JTAG接口处于某种特定的测试模式;
- TCK:测试时钟输入;
- TDI:测试数据输入,数据通过TDI引脚输入JTAG接口;
- TDO:测试数据输出,数据通过TDO引 脚从JTAG接口输出;
- TRST:JTAG复位,连接到目标CPU的nTRST引脚,用于复位CPU调试接口的TAP控制器;目标板上应将此脚上拉到高电位,避免意外复位
- RTCK:目标CPU提供给仿真器的时钟信号。有些目标要求JTAG的输入与其内部时钟同步。仿真器利用此引脚的输入可动态地控制自己的TCK速率。若不使
- 此功能,在目标板上将此脚接地,有些芯片可能要求上拉;
- RESET:仿真器输出至目标CPU的系统复位信号。
标准JTAG接口定义如图所示:
正点原子的STM32开发板接口位置颠倒。正手持开发版,接口如图所示:
1.2 SWD
串行调试(Serial Wire Debug),应该可以算是一种和JTAG不同的调试模式,使用的调试协议也应该不一样,所以最直接的体现在调试接口上,与JTAG的20个引脚相比,SWD只需要4个(或者5个)引脚,结构简单,但是使用范围没有JTAG广泛,主流调试器上也是后来才加的SWD调试模式。
SWD和传统的调试方式区别:
- SWD模式比JTAG在高速模式下面更加可靠。在大数据量的情况下面JTAG下载程序会失败,但是SWD发生的几率会小很多。基本使用JTAG仿真模式的情况下是可以直接使用SWD模式的,只要你的仿真器支持,所以推荐大家使用这个模式。
- 在大家GPIO刚好缺一个的时候,可以使用SWD仿真,这种模式支持更少的引脚。
- 在大家板子的体积有限的时候推荐使用SWD模式,它需要的引脚少,当然需要的PCB空间就小啦!比如你可以选择一个很小的2.54间距的5芯端子做仿真接口。
1.3 JLINK
J-Link是德国SEGGER公司推出基于JTAG的仿真器。简单地说,是给一个JTAG协议转换盒,即一个小型USB到JTAG的转换盒,其连接到计算机用的是USB接口,而到目标板内部用的还是jtag协议。它完成了一个从软件到硬件转换的工作。
JLINK是一个通用的开发工具,可以用于KEIL、IAR、ADS 等平台。速度,效率,功能都很好,据说是众多仿真器里最强悍的。
1.4 ULINK
ULINK是ARM/KEIL公司推出的仿真器,目前网上可找到的是其升级版本,ULINK2和ULINK Pro仿真器。ULINK/ULINK2可以配合Keil软件实现仿真功能,并且仅可以在Keil软件上使用,增加了串行调试(SWD)支持,返回时钟支持和实时代理等功能。开发工程师通过结合使用RealView MDK的调试器和ULINK2,可以方便的在目标硬件上进行片上调试(使用on-chip JTAG,SWD和OCDS)、Flash编程。
但是要注意的是,ULINK是KEIL公司开发的仿真器,专用于KEIL平台下使用,ADS、IAR下不能使用。
1.5 ST-LINK
ST-LINK是专门针对意法半导体STM8和STM32系列芯片的仿真器。ST-LINK /V2指定的SWIM标准接口和JTAG / SWD标准接口,其主要功能有:
- 编程功能:可烧写FLASH ROM、EEPROM、AFR等;
- 仿真功能:支持全速运行、单步调试、断点调试等各种调试方法,可查看IO状态,变量数据等;
- 仿真性能:采用USB2.0接口进行仿真调试,单步调试,断点调试,反应速度快;
- 编程性能:采用USB2.0接口,进行SWIM / JTAG / SWD下载,下载速度快;
2. 正点原子库模板
-
CORE:
-
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport 下面的 core_cm3.c和 文 件 core_cm3.h
-
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm 下面的 startup_stm32f10x_hd.s 文件
-
-
FWLIB:
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver 下面的src,inc文件夹,分别存储c和h文件
-
USER:
-
STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 下面的三个文件 stm32f10x.h,system_stm32f10x.c,system_stm32f10x.h
-
STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template 下面的 4 个文件main.c,stm32f10x_conf.h,stm32f10x_it.c,stm32f10x_it.h
-
3. M3时钟树
在STM32中,有五个时钟源,为HSI、HSE、LSI、LSE、PLL:
- HSI:高速内部时钟,RC振荡器,频率为8MHz。
- HSE:高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~16MHz。
- LSI:低速内部时钟,RC振荡器,频率为40kHz。
- LSE:低速外部时钟,接频率为32.768kHz的石英晶体。
- PLL:锁相环倍频输出,其时钟输入源可选择为HSI/2、HSE或者HSE/2。倍频可选择为2~16倍,但是其输出频率最大不得超过72MHz。
三条时钟总线外设映射:
-
AHB总线:
- Flash 存储器;
- DMA;
- 复位和时钟控制;
- SYSTICK时钟;
- CRC;
- 以太网;
- SDIO;
-
APB2总线:
- USART1;
- 高级控制定时器TIM1和TIM8;
- 模数转换器ADC1、ADC2、ADC3;
- SPI1;
- 外部中断EXTI;
- 复用IO,AFIO;
- 通用IO:GPIOA~G;
-
APB1总线:
- 定时器TIM2到TIM7;
- RTC;
- WDT看门狗;
- SPI2 、SPI3;
- USART2、USART3;
- UART4、UART5;
- I2C1,I2C2;
- USB./CAN共享的512字节SRAM;
- bXCAN1、bXCAN2;
- 后备寄存器BKP;
- 电源控制PWR;
- DAC
4. M4时钟树
- STMF4xx系统共计有三个主要时钟源( HSI、HSE和 PLL)和两个次要时钟源( LSE、LSI)。
- SYSCLK可以来自 HSI、HSE和 PLL,多数采用 PLL频率最高能达到 168MHz。
- RTC时钟可以来自 LSE、LSI和 HSE,但只有用 LSE时,才能保证系统电源掉电时 RTC仍能正常工作。
- 可通过多个预分频器配置 AHB 频率、高速 APB (APB2) 和低速 APB (APB1) 。 AHB 域的最大频率为 168 MHz。高速 APB2 域的最大允许频率为 84 MHz。低速 APB1 域的最大允许频率为 42 MHz。
- STM32F405xx/07xx 和 STM32F415xx/17xx 的定时器时钟频率由硬件自动设置。 如果 APB预分频器为 1,定时器时钟频率等于 APB 域的频率。 否则,等于 APB 域的频率的两倍 (×2)。
- 除以下时钟外,所有外设时钟均由系统时钟 (SYSCLK) 提供:
- 来自于特定 PLL 输出 (PLL48CLK) 的 USB OTG FS 时钟 (48 MHz)、基于模拟技术的随机数发生器 (RNG) 时钟 (<=48 MHz) 和 SDIO 时钟 (<= 48 MHz)
- 由外部 PHY 提供的 USB OTG HS (60 MHz) 时钟
- 由外部 PHY 提供的以太网 MAC 时钟( TX、 RX 和 RMII)
- I2S时钟
-
AHB1:168Mhz MAX
- GPIOA~K
- RCC_AHB1Periph_CRC
- FLITF
- SRAM1
- SRAM2
- BKPSRAM
- SRAM3
- CCMDATARAMEN
- DMA1
- DMA2
- DMA2D
- ETH_MAC、ETH_MAC_Tx、ETH_MAC_Rx、ETH_MAC_PTP
- OTG_HS、OTG_HS_ULPI
-
AHB2:168Mhz MAX
- DCMI
- CRYP
- HASH
- RNG
- OTG_FS
-
APB1:42Mhz MAX
- TIM2~14
- WWDG
- SPI2-3
- USART2-3
- UART4-5,7-8
- I2C1~3
- CAN1~2
- PWR
- DAC
-
APB2:84Mhz MAX
- TIM1,8~11
- USART1,6
- ADC
- ADC1~3
- SDIO,1, 4,5,6
- SYSCFG
- SAI1
- LTDC
5. GPIO
5.1 电路
- 浮空输入模式
浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。
- 上拉输入模式
上拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在高电平;并且在I/O端口输入为低电平的时候,输入端的电平也还是低电平。
- 下拉输入模式
下拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在低电平;并且在I/O端口输入为高电平的时候,输入端的电平也还是高电平。
- 模拟输入模式
模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等。
- 开漏输出模式
开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经N-MOS管,最终输出到I/O端口。这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,I/O端口的电平不一定是输出的电平。
- 开漏复用输出模式
开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。
- 推挽输出模式
推挽输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经P-MOS管和N-MOS管,最终输出到I/O端口。这里要注意P-MOS管和N-MOS管,当设置输出的值为高电平的时候,P-MOS管处于开启状态,N-MOS管处于关闭状态,此时I/O端口的电平就由P-MOS管决定:高电平;当设置输出的值为低电平的时候,P-MOS管处于关闭状态,N-MOS管处于开启状态,此时I/O端口的电平就由N-MOS管决定:低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平。
- 推挽复用输出模式
推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。
-
总结与分析
-
什么是推挽结构和推挽电路?
推挽结构一般是指两个参数相同的三极管或MOS管分别受两互补信号的控制,总是在一个三极管或MOS管导通的时候另一个截止。高低电平由输出电平决定。
推挽电路是两个参数相同的三极管或MOSFET,以推挽方式存在于电路中,各负责正负半周的波形放大任务。电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载抽取电流。推拉式输出级既提高电路的负载能力,又提高开关速度。 -
开漏输出和推挽输出的区别?
- 开漏输出:只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极。适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)。
- 推挽输出:可以输出强高、低电平,连接数字器件。
关于推挽输出和开漏输出,最后用一幅最简单的图形来概括:
-
该图中左边的便是推挽输出模式,其中比较器输出高电平时下面的PNP三极管截止,而上面NPN三极管导通,输出电平VS+;当比较器输出低电平时则恰恰相反,PNP三极管导通,输出和地相连,为低电平。右边的则可以理解为开漏输出形式,需要接上拉。
- 在STM32中选用怎样选择I/O模式?
- 浮空输入IN_FLOATING:可以做KEY识别
- 带上拉输入IPU:IO内部上拉电阻输入
- 带下拉输入IPD:IO内部下拉电阻输入
- 模拟输入AIN:应用ADC模拟输入,或者低功耗下省电
- 开漏输出OUT_OD:IO输出0接GND,IO输出1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为1时,IO口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样IO口也就可以由外部电路改变为低电平或不变。可以读IO输入电平变化,实现C51的IO双向功能
- 推挽输出OUT_PP :IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的
- 复用功能的推挽输出AF_PP:片内外设功能(I2C的SCL、SDA)
- 复用功能的开漏输出_AF_OD:片内外设功能(TX1、MOSI、MISO.SCK.SS)
5.2 配置方法
- 8种输入方式
- 输入浮空
- 输入上拉
- 输入下拉
- 模拟输入
- 开漏输出
- 推挽输出
- 推挽式复用功能
- 开漏复用功能
-
GPIO_InitTyepDef说明:
typedef struct{ uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; }GPIO_InitTypeDef;
下面我们通过一个 GPIO 初始化实例来讲解这个结构体的成员变量的含义。
通过初始化结构体初始化 GPIO 的常用格式是:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度 50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure);//根据设定参数配置 GPIO
-
GPIO_Mode_TypeDef说明:
typedef enum{ GPIO_Mode_AIN = 0x0, //模拟输入 GPIO_Mode_IN_FLOATING = 0x04, //浮空输入 GPIO_Mode_IPD = 0x28, //下拉输入 GPIO_Mode_IPU = 0x48, //上拉输入 GPIO_Mode_Out_OD = 0x14, //开漏输出 GPIO_Mode_Out_PP = 0x10, //通用推挽输出 GPIO_Mode_AF_OD = 0x1C, //复用开漏输出 GPIO_Mode_AF_PP = 0x18 //复用推挽输出 }GPIOMode_TypeDef;
-
GPIO_Speed_TypeDef说明:
typedef enum{ GPIO_Speed_10MHz = 1, GPIO_Speed_2MHz, GPIO_Speed_50MHz }GPIOSpeed_TypeDef;
-
固件库中操作 IDR 寄存器
在固件库中操作 IDR 寄存器读取 IO 端口数据是通过 GPIO_ReadInputDataBit 函数实现的:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
比如我要读 GPIOA.5 的电平状态,那么方法是:
*GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5);*
返回值是 1(Bit_SET)或者 0(Bit_RESET);
-
固件库中操作 ODR 寄存器
ODR 是一个端口输出数据寄存器,也只用了低 16 位。该寄存器为可读写,从该寄存器读出来的数据可以用于判断当前 IO 口的输出状态。而向该寄存器写数据,则可以控制某个 IO 口的输出电平。
在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数GPIO_Write 来实现的:
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); *GPIO_Write(GPIOD,0x0100); //使PD8置高*
该函数一般用来往一次性一个 GPIO 的多个端口设值。
-
固件库中操作BSRR和BRR寄存器
在 STM32 固件库中,通过 BSRR 和 BRR 寄存器设置 GPIO 端口输出是通过函数GPIO_SetBits()和函数 GPIO_ResetBits()来完成的。
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
在多数情况下,我们都是采用这两个函数来设置 GPIO 端口的输入和输出状态。比如我们要设置 GPIOB.5 输出 1,那么方法为:
GPIO_SetBits(GPIOB, GPIO_Pin_5);
反之如果要设置 GPIOB.5 输出位 0,方法为:
GPIO_ResetBits (GPIOB, GPIO_Pin_5);
-
GPIO配置操作步骤
-
使能 IO 口时钟。调用函数为 RCC_APB2PeriphClockCmd()。
-
初始化 IO 参数。调用函数 GPIO_Init();
-
操作 IO。操作 IO 的方法就是上面我们讲解的方法。
-
6. USART/UART
-
串口配置的一般步骤:
- 串口时钟使能,GPIO 时钟使能
- 串口复位
- GPIO 端口模式设置
- 串口参数初始化
- 开启中断并且初始化 NVIC(如果需要开启中断才需要这个步骤)
- 使能串口
- 编写中断处理函数
-
串口时钟 使能。
串口是挂载在 APB2 下面的外设,所以使能函数为:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1);
比如我们要使能USART1串口,方法为:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟
-
串口复位。
当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外设的操作。
复位的是在函数 USART_DeInit()中完成:
void USART_DeInit(USART_TypeDef* USARTx);//串口复位
比如我们要复位串口 1,方法为:
USART_DeInit(USART1); //复位串口1
-
串口参数初始化。
串口初始化是通过 USART_Init()函数实现的:
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
-
第一个入口参数是指定初始化的串口标号,这里选择 USART1。
-
第二个入口参数是一个 USART_InitTypeDef 类型的结构体指针,这个结构体指针的成员变量用来设置串口的一些参数。
-
-
数据发送与接收。
STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。
STM32 库函数操作 USART_DR 寄存器发送数据的函数是:
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);
通过该函数向串口寄存器 USART_DR 写入一个数据。
STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是:
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);
通过该函数可以读取串口接受到的数据。
-
串口状态。
串口的状态可以通过状态寄存器 USART_SR 读取。这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
- RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将该位清零,也可以向该位写 0,直接清除。
- TC(发送完成),当该位被置1的时候,表示 USART_DR 内的数据已经被发送完成了。如果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写USART_DR。2)直接向该位写 0。
在我们固件库函数里面,读取串口状态的函数是:
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);
这个函数的第二个入口参数非常关键,它是标示我们要查看串口的哪种状态,比如上面讲解的RXNE(读数据寄存器非空)以及 TC(发送完成)。
例如我们要判断读寄存器是否非空(RXNE),操作库函数的方法是:
USART_GetFlagStatus(USART1, USART_FLAG_RXNE);
我们要判断发送是否完成(TC),操作库函数的方法是:
USART_GetFlagStatus(USART1, USART_FLAG_TC);
这些标识号在 MDK 里面是通过宏定义定义的。
-
串口使能。
串口使能是通过函数 USART_Cmd()来实现的,使用方法是:
USART_Cmd(USART1, ENABLE); //使能串口
-
开启串口响应中断。
有些时候当我们还需要开启串口中断,那么我们还需要使能串口中断,使能串口中断的函数是:
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT,FunctionalState NewState)
这个函数的第二个入口参数是标示使能串口的类型,也就是使能哪种中断,因为串口的中断类型有很多种。比如在接收到数据的时候(RXNE 读数据寄存器非空),我们要产生中断,那么我们开启中断的方法是:
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断,接收到数据中断
我们在发送数据结束的时候(TC,发送完成)要产生中断,那么方法是:
USART_ITConfig(USART1,USART_IT_TC,ENABLE);
-
获取相应中断状态。
当我们使能了某个中断的时候,当该中断发生了,就会设置状态寄存器中的某个标志位。经常我们在中断处理函数中,要判断该中断是哪种中断,使用的函数是:
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
比如我们使能了串口发送完成中断,那么当中断发生了, 我们便可以在中断处理函数中调用这个函数来判断到底是否是串口发送完成中断,方法是:
USART_GetITStatus(USART1, USART_IT_TC);
返回值是 SET,说明是串口发送完成中断发生。
7. NVIC
- 简介
STM32的中有一个强大而方便的NVIC(嵌套中断向量控制器),它是属于Cortex内核的器件,不可屏蔽中断 (NMI)和外部中断都由它来处理,而__SYSTICK不是由NVIC来控制的。__
特性:
- 60个可屏蔽中断通道(不包含16个Cortex™-M3的中断线)
- 16个可编程的优先等级(使用了4位中断优先级)
- 低延迟的异常和中断处理
- 电源管理控制
- 系统控制寄存器的实现
-
中断优先级分组
STM32(Cortex-M3)中有两个优先级的概念:抢占式优先级和响应优先级,每个中断源都需要被指定这两种优先级。
具有高抢占式优先级的中断可以在具有低抢占式优先级的中断处理过程中被响应,即中断嵌套,或者说高抢占式优先级的中断可以嵌套在低抢占式优先级的中断中。
当两个中断源的抢占式优先级相同时,这两个中断将没有嵌套关系,当一个中断到来后,如果正在处理另一个中断,这个后到来的中断就要等到前一个中断处理完之后才能被处理。
如果这两个中断同时到达,则中断控制器根据他们的响应优先级高低来决定先处理哪一个。如果他们的抢占式优先级和响应优先级都相等,则根据他们在中断表中的排位顺序决定先处理哪一个。
-
Cortex-M3的中断分组
Cortex内核具有强大的异常响应系统,它把能够打断当前代码执行流程的事件分为__异常(exception)和中断(interrupt)__,并把它们用一个表管理起来,编号为0~15的称为内核异常(不可屏蔽中断),而16以上的则称为外部中断,这个表就称为中断向量表。
正是因为每个中断源都需要被指定这两种优先级,就需要有相应的寄存器位记录每个中断的优先级;在Cortex-M3中定义了8个比特位用于设置中断源的优先级,这8个比特位可以有8种分配方式,如下:
- 所有8位用于指定响应优先级
- 最高1位用于指定抢占式优先级,最低7位用于指定响应优先级
- 最高2位用于指定抢占式优先级,最低6位用于指定响应优先级
- 最高3位用于指定抢占式优先级,最低5位用于指定响应优先级
- 最高4位用于指定抢占式优先级,最低4位用于指定响应优先级
- 最高5位用于指定抢占式优先级,最低3位用于指定响应优先级
- 最高6位用于指定抢占式优先级,最低2位用于指定响应优先级
- 最高7位用于指定抢占式优先级,最低1位用于指定响应优先级
Cortex-M3允许具有较少中断源时使用较少的寄存器位指定中断源的优先级。
-
STM32的中断分组
STM32对这个表重新进行了编排,把编号从-3至6的中断向量定义为系统异常,编号为负的内核异常不能被设置优先级,如复位(Reset)、不可屏蔽中断 (NMI)、硬错误(Hardfault)。从编号 7开始的为外部中断,这些中断的优先级都是可以用户更改的。详细的 STM32中断向量号可以在startup_stm32f10x_XX.s中查找。
因此STM32把指定中断优先级的寄存器位减少到4位,这4个寄存器位的分组方式如下:
- 所有4位用于指定响应优先级(16种)
- 最高1位用于指定抢占式优先级,最低3位用于指定响应优先级(8种)
- 最高2位用于指定抢占式优先级,最低2位用于指定响应优先级(4种)
- 最高3位用于指定抢占式优先级,最低1位用于指定响应优先级(2种)
- 所有4位用于指定抢占式优先级
-
中断优先级分组函数实现
NVIC_PriorityGroupConfig(u32 NVIC_PriorityGroup);
这个函数的参数(NVIC_PriorityGroup值)有下列5种:
-
NVIC_PriorityGroup_0 => 选择第0组
-
NVIC_PriorityGroup_1 => 选择第1组
-
NVIC_PriorityGroup_2 => 选择第2组
-
NVIC_PriorityGroup_3 => 选择第3组
-
NVIC_PriorityGroup_4 => 选择第4组
-
举个例子,假如现在有4个外部中断,还有一个EXTI9_5中断,那么如果选择优先级分组为第1组,那么抢占式优先级便只有两种,5个中断就至少有3个在抢占式优先级上是相同的优先级上,其他两个在令一优先级别。接着设置响应优先级可以有8种选择;假如现在同时有两个抢占式优先级别相同的中断发生,那么处理的顺序是谁的响应优先级高则谁优先进入中断,另外如果此时进入这个中断之后又来了一个抢占式优先级相同但是响应优先级更高的中断,这时也是不会打断已有的中断的。
8. EXTI
-
外部中断配置的一般步骤:
- 初始化 IO 口为输入
- 开启 AFIO 时钟
- 设置 IO 口与中断线的映射关系
- 初始化线上中断,设置触发条件等
- 配置中断分组(NVIC),并使能中断
- 编写中断服务函数
代码主要分布在固件库的 stm32f10x_exti.h 和 stm32f10x_exti.c 文件中。
-
STM32外部中断介绍
STM32 的每个 IO 都可以作为外部中断的中断输入口,这点也是 STM32 的强大之处。STM32F103 的中断控制器支持 19 个外部中断/事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。
STM32F103 的19 个外部中断为:
-
线 0~15:对应外部 IO 口的输入中断。
-
线 16:连接到 PVD 输出。
-
线 17:连接到 RTC 闹钟事件。
-
线 18:连接到 USB 唤醒事件。
从上面可以看出,STM32 供 IO 口使用的中断线只有 16 个,但是 STM32 的 IO 口却远远不止 16 个,那么 STM32 是怎么把 16 个中断线和 IO 口一一对应起来的呢?于是 STM32 就这样设计,GPIO 的管教 GPIOx.0~GPIOx.15(x=A,B,C,D,E,F,G)分别对应中断线 0~15,这样每个中断线对应了最多 7 个 IO 口。
以线 0 为例:它对应了GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0。而中断线每次只能连接到 1 个 IO 口上,这样就需要通过配置来决定对应的中断线配置到哪个 GPIO 上了。
-
-
设置 IO 口与中断线的映射关系
在库函数中,配置 GPIO 与中断线的映射关系的函数 GPIO_EXTILineConfig()来实现的:
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
该函数将 GPIO 端口与中断线映射起来,使用范例是:
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
-
初始化线上中断,设置触发条件等
将中断线 2 与 GPIOE 映射起来,那么很显然是 GPIOE.2 与 EXTI2 中断线连接了。设置好中断线映射之后,那么到底来自这个 IO 口的中断是通过什么方式触发的呢?接下来我们就要设置该中断线上中断的初始化参数了。
中断线上中断的初始化是通过函数 EXTI_Init()实现的,其定义是:
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);
其中,EXTI_InitTypeDef 的成员变量是:
typedef struct{ uint32_t EXTI_Line; EXTIMode_TypeDef EXTI_Mode; EXTITrigger_TypeDef EXTI_Trigger; FunctionalState EXTI_LineCmd; }EXTI_InitTypeDef;
下面我们用一个使用范例来说明这个函数的使用:
EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line=EXTI_Line2; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的参数初始化外设 EXTI 寄存器
有 4 个参数需要设置:
- 中断线的标号,取值范围为EXTI_Line0~EXTI_Line15。这个在上面已经讲过中断线的概念。也就是说,这个函数配置的是某个中断线上的中断参数。
- 中断模式,可选值为中断EXTI_Mode_Interrupt 和事件EXTI_Mode_Event。
- 触发方式,可以是下降沿触发EXTI_Trigger_Falling,上升沿触发EXTI_Trigger_Rising,或者任意电平(上升沿和下降沿)触发EXTI_Trigger_Rising_Falling。
- 使能中断线。
-
设置中断优先级
我们接着上面的范例, 设置中断线 2 的中断优先级。
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //中断优先级分组初始化
-
编写中断服务函数
配置完中断优先级之后,接着我们要做的就是编写中断服务函数。中断服务函数的名字是在 MDK 中事先有定义的。
需要说明一下,STM32 的 IO 口外部中断函数只有 6 个,分别为:
EXPORT EXTI0_IRQHandler EXPORT EXTI1_IRQHandler EXPORT EXTI2_IRQHandler EXPORT EXTI3_IRQHandler EXPORT EXTI4_IRQHandler EXPORT EXTI9_5_IRQHandler EXPORT EXTI15_10_IRQHandler
中断线0-4每个中断线对应一个中断函数
中断线5-9共用中断函数EXTI9_5_IRQHandler
中断线10-15共用中断函数EXTI15_10_IRQHandler
在编写中断服务函数的时候会经常使用到两个函数:
-
判断某个中断线上的中断是否发生(标志位是否置位):
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
这个函数一般使用在中断服务函数的开头。
-
清除某个中断线上的中断标志位:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
这个函数一般应用在中断服务函数结束之前.
常用的中断服务函数格式为:
void EXTI3_IRQHandler(void){ if(EXTI_GetITStatus(EXTI_Line3)!=RESET)//判断某个线上的中断是否发生 { \\中断逻辑… EXTI_ClearITPendingBit(EXTI_Line3); //清除 LINE 上的中断标志位 } }
固件库还提供了两个函数用来判断外部中断状态以及清除外部状态:
标志位的函数EXTI_GetFlagStatus和EXTI_ClearFlag,他们的作用和前面两个函数的作用类似。只是在EXTI_GetITStatus函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而EXTI_GetFlagStatus 直接用来判断状态标志位。
-
9. TIM
9.1 中断配置
-
打开相关外设的时钟。以定时器TIM3为例,由stm32的时钟树可以看到,TIM3时钟挂接在APB1上面,所以打开TIM3时钟时使用库函数:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
如果其中还使用到了其他外设,如GPIO等,再打开相关的外设时钟就可以了。
-
清除中断挂起位。由于各种不可知的因素作用,在程序运行前要操作的定时器的中断挂起位有可能会被置位,这样就会导致在程序一开始就会进入定时器中断的中断服务程序。为了消除这种影响,我们在程序的一开始就将中断挂起位清除。在固件库中使用:
void TIM_ClearITPendingBit(TIM_TypeDef*TIMx, u16 TIM_IT);
来清除中断挂起位。
-
定时器基本配置初始化。在这一步骤中主要确定定时器的预分频和设置自动重装载寄存器周期的值,并确定计数模式,这主要使用固件库中的 TIM_TimeBaseInit()函数进行操作,该函数的原型为:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct)
第二个输入参数是一个结构体,该结构体的定义如下:
typedef struct{ u16 TIM_Period; //重装载寄存器周期值 u16 TIM_Prescaler; //预分频值 u8 TIM_ClockDivision;//分割系数,一般设0 u16 TIM_CounterMode; //计数模式 }TIM_TimeBaseInitTypeDef;
-
使能定时器TIMx。使用函数TIM_Cmd()函数就可以了,比如使能定时器TIM3外设,使用TIM_Cmd(TIM3,ENABLE)。
-
使能TIMx中断。调用函数即可。因为我们要使用 TIM3 的更新中断, 寄存器的相应位便可使能更新中断。 在库函数里面定时器中断使能是通过 TIM_ITConfig 函数来实现的:
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
-
第一个参数是选择定时器号,取值为 TIM1~TIM17。
-
第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。
-
第三个参数配置定时器失能还是使能,取值ENABLE或DISABLE
例如我们要使能 TIM3 的更新中断,代码为:
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
-
-
配置中断优先级,也就是配置嵌套向量终端控制器NVIC。进行本步骤首先需要配置优先级的分组,使用库函数NVIC_PriorityGroupConfig()进行。再配置NVIC初始化,使用库函数NVIC_Init(),参考NVIC章。
-
编写中断服务程序。在这里,我们首先要清除中断挂起位。
void TIM4_IRQHandler(void){ if(TIM_GetITStatus(TIM4,TIM_IT_Update)) { //填写中断中要完成的任务 } TIM_ClearITPendingBit(TIM4,TIM_IT_Update); }
9.2 PWM波配置
TIM配置PWM波繁琐一些,在此以TIM3的CH2生成PWM波为例。
-
开启 TIM3 时钟以及复用功能时钟,配置 PB5 为复用输出
使能TIM3和复用时钟,配置 PB5 为复用输出,这是因为__TIM3_CH2通道将重映射到 PB5 上,此时,PB5属于复用功能输出。__
库函数设置 AFIO 时钟的方法是:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
列出 GPIO 初始化的一行代码:
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
-
设置 TIM3_CH2 重映射到 PB5上
因为 TIM3_CH2 默认是接在 PA7 上的,所以我们需要设置 TIM3_REMAP 为部分重映射(通过 AFIO_MAPR 配置),让 TIM3_CH2 重映射到 PB5 上面。
在库函数函数里面设置重映射的函数是:
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
注意:STM32 重映射只能重映射到特定的端口,具体请查阅数据手册。
第一个入口参数可以理解为设置重映射的类型,比如 TIM3 部分重映射入口参数为GPIO_PartialRemap_TIM3,这点可以顾名思义了。所以 TIM3 部分重映射的库函数实现方法是:
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
-
初始化 TIM3, 设置 TIM3 的ARR 和 PSC
在开启了 TIM3 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM 的周期。当 PWM 周期太慢(低于 50Hz)的时候,我们就会明显感觉到闪烁了。因此,PWM 周期在这里不宜设置的太小。这在库函数是通过 TIM_TimeBaseInit 函数实现的,调用的格式为:
TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx
-
设置 TIM3_CH2的PWM 模式,使能 TIM3的CH2输出
接下来,我们要设置 TIM3_CH2 为 PWM 模式(默认是冻结的),因为我们的 DS0 是低电平亮,而我们希望当 CCR2 的值小的时候,DS0 就暗,CCR2 值大的时候,DS0 就亮,所以我们要通过配置 TIM3_CCMR1 的相关位来控制 TIM3_CH2 的模式。在库函数中,PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我们使用的是通道 2,所以使用的函数是 TIM_OC2Init():
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
结构体 TIM_OCInitTypeDef的定义:
typedef struct{ uint16_t TIM_OCMode; uint16_t TIM_OutputState; uint16_t TIM_OutputNState; */ uint16_t TIM_Pulse; uint16_t TIM_OCPolarity; uint16_t TIM_OCNPolarity; uint16_t TIM_OCIdleState; uint16_t TIM_OCNIdleState; } TIM_OCInitTypeDef;
这里我们讲解一下与我们要求相关的几个成员变量:
- 参数 TIM_OCMode 设置模式是 PWM 还是输出比较,这里我们是 PWM 模式。
- 参数TIM_OutputState 用来设置比较输出使能,也就是使能 PWM 输出到端口。
- 参数 TIM_OCPolarity 用来设置极性是高还是低。
- 其他的参数 TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是高级定时器TIM1 和 TIM8才用到的。
要实现我们的功能,方法是:
TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择 PWM 模式 2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性高 TIM_OC2Init(TIM3, &TIM_OCInitStructure); //初始化 TIM3 OC2
-
使能 TIM3
在完成以上设置了之后,我们需要使能 TIM3。使能 TIM3 的方法前面已经讲解过:
TIM_Cmd(TIM3, ENABLE); //使能 TIM3
-
修改TIM3_CCR2来控制占空比
最后,在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM3_CCR2 则可以控制 CH2 的输出占空比。继而控制 DS0 的亮度。
在库函数中,修改 TIM3_CCR2 占空比的函数是:
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
理所当然,对于其他通道,分别有一个函数名字,函数格式为 TIM_SetComparex(x=1,2,3,4)。