前言

STM32基础,基于正点原子探索者开发板,必须掌握的部分写在前面,入门之后再扩充其他内容,所以顺序并不完全按照正点原子教程的顺序,去除一些个人认为暂时不需要重点掌握的内容,加上一些比较重要的东西。学习方法:重点章节教程视频过一遍,可以有很多疑问先直接跳过,然后看下面总结好的东西,实验自己写一边,再把视频1.5倍速过一遍,查漏补缺,之后再深入,以项目主导学习,缺什么补什么,切忌陷入太多细节问题,切忌太过追求全面。

开发板资源

先了解开发板有哪些资源,可直接跳过,以后学习用到了哪些资源再回头来看;

开发板资源图

r1JFl4.jpg

开发板资源列表

  • CPU STM32F407ZGT6 LQFP144 FLASH 1024K SRAM 192K
  • 外扩 SRAM XM8A51216 1M字节
  • 外扩 SPI FLASH W25Q128 16M字节
  • 1个电源指示灯(蓝色)
  • 2个状态指示灯( DS0:红色 DS1:绿色
  • 1个红外接收头,并配备一款小巧的红外遥控器
  • 1个 EEPROM芯片, ,24C02,容量 256字节
  • 1个六轴(陀螺仪 +加速度)传感器芯片, MPU6050
  • 1个高性能音频编解码芯片, WM8978
  • 1个 2.4G无线模块接口,支持 NRF24L01无线模块
  • 1路 CAN接口,采用 TJA1050芯片
  • 1路 485接口,采用 SP3485芯片
  • 2路 RS232串口(一公一母)接口,采用 SP3232芯片
  • 1路单总线接口,支持 DS18B20/DHT11等单总线传感器
  • 1个 ATK模块接口,支持 ALIENTEK蓝牙 /GPS模块
  • 1个光敏传感器
  • 1个标准的 2.4/2.8/3.5/4.3/7寸 LCD接口,支持电阻 /电容触摸屏
  • 1个摄像头模块接口
  • 1个 OLED模块接口
  • 1个 USB串口,可用于程序下载和代码调试( USMART调试)
  • 1个 USB SLAVE接口,用于 USB从机通信
  • 1个 USB HOST(OTG)接口,用于 USB主机通信
  • 1个有源蜂鸣器
  • 1个 RS232/RS485选择接口
  • 1个 RS232/模块选择接口
  • 1个 CAN/USB选择接口
  • 1个串口选择接口
  • 1个 SD卡接口(在板子背面)
  • 1个百兆以太网接口( RJ45
  • 1个标准的 JTAG/SWD调试下载口
  • 1个录音头( MIC/咪头)
  • 1路立体声音频输出接口
  • 1路立体声录音输入接口
  • 1路扬声器输出接口,可接 1W左右小喇叭
  • 1组多功能端口( DAC/ADC/PWM DAC/AUDIO IN/TPAD
  • 1组 5V电源供应 /接入口
  • 1组 3.3V电源供应 /接入口
  • 1个参考电压设置接口
  • 1个直流电源输入接口(输入电压范围: DC6~16V
  • 1个启动模式选择配置接口
  • 1个 RTC后备电池座,并带电池
  • 1个复位按钮,可用于复位 MCU和 LCD
  • 4个功能按钮,其中 KEY_UP(即 WK_UP)兼具唤醒功能
  • 1个电容触摸按键
  • 1个电源开关,控制整个板的电源
  • 一键下载功能
  • 除晶振占用的 IO口外,其余所有 IO口全部引出

一些常识

stm32的命名规则:

r3iN6g.png

RAM: 随机存储器,可读可写,特点是掉电会丢失数据。RAM又分为SRAM(Static RAM)和DRAM(Dynamic RAM),SRAM是读写速度非常快的存储设备,但价格昂贵。DRAM比ROM速度快,但是比SRAM速度慢,价格低于SRAM,计算机内存使用的就是DRAM;

FLASH: 闪存,这种存储器结合了ROM和RAM的优点,既可以保证掉电不丢失又可以有很高的读写速度。可以用来存储一些用户不希望掉电丢失的一些数据;

在以往,单片机内部包含ROM和RAM,ROM的硬件实现主要EEPROM,但近年来Flash逐渐取代了他的位置,成为ROM实现的主要硬件。例如,51单片机有4KB的ROM和256B的RAM,这里的ROM实现为EEPROM,而STM31F103有64KB的Flash和20K的SRAM,这里Flash的一部分作为ROM来使用;

stm32常用的开发工具: MDK(keil),IAR,STM32CubeMX;

程序下载

ISP串口下载: 速度太慢,一般使用J-LINK下载;

STM32的ISP下载,只能使用串口1,也就是对应串口发送接收引脚PA9,PA10。不能使用其他串口(例如串口2:PA2,PA3)用来ISP下载;

J-LINK下载: 一般使用SWD模式,只需要两根线就可以下载和调试,编译好程序后点击LOAD下载程序到开发板但需要提前配置好下载环境,MDK配置,在Debug选项卡选择J-LINK/J-TAG Cortex,再点setting,选择SW,还要在下载选项卡设置添加对应的设备,两项设置好了才能下载,不然会报错无法下载;

一般来说下好程序后,重启芯片时,SYSCLK的第4个上升沿,BOOT引脚的值将被锁存,用户可以通过设置BOOT1和BOOT0引脚的状态,来选择在复位后的启动模式。

STM32上电或者复位后,代码区始终从0x00000000开始,三种启动模式其实就是将各自存储空间的地址映射到0x00000000中:

  1. 从Flash启动,将主Flash地址0x08000000映射到0x00000000,这样代码启动之后就相当于从0x08000000开始;
  2. 从RAM启动,将RAM地址0x20000000映射到0x00000000,这样代码启动之后就相当于从0x20000000开始;
  3. 从系统存储器启动。首先控制BOOT0 BOOT1管脚,复位后,STM32与上述两种方式类似,从系统存储器地址0x1FFF F000开始执行代码,系统存储器存储的其实就是STM32自带的bootloader代码,在bootloader中提供了UART1的接口,通过此接口可以将用户所需的程序代码下载到主Flash中,下载完毕后,此时程序代码已经存储在主Flash当中,这时切换启动模式(从主Flash启动),复位后所执行的就是刚刚下载到Flash中的代码了;

STM32三种启动模式对应的存储介质均是芯片内置的,如下图:

r36m8J.png

  1. 用户闪存 : 芯片内置的Flash,正常的工作模式。
  2. SRAM: 芯片内置的RAM区,就是内存,可以用于调试。
  3. 系统存储器: 芯片内部一块特定的区域,芯片出厂时在这个区域预置了一段Bootloader,就是通常说的ISP程序,这个区域的内容在芯片出厂后没有人能够修改或擦除,即它是一个ROM区。启动的程序功能由厂家设置。

Main Flash memory: 是STM32内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。

System memory: 从系统存储器启动,这种模式启动的程序功能是由厂家设置的。一般来说,这种启动方式用的比较少。
系统存储器是芯片内部一块特定的区域,STM32在出厂时,由ST在这个区域内部预置了一段BootLoader,也就是我们常说的ISP程序,这是一块ROM,出厂后无法修改。

一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。但是这个下载方式需要以下步骤:

Step1:将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader;

Step2:最后在BootLoader的帮助下,通过串口下载程序到Flash中;

Step3:程序下载完成后,又有需要将BOOT0设置为GND,手动复位,这样,STM32才可以从Flash中启动,可以看到,利用串口下载程序还是比较的麻烦,需要跳帽跳来跳去的,非常的不注重用户体验,所以主流的开发板都设计了一键下载电路,方便用户下载程序;

Embedded Memory:

内置SRAM,既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试,

假如我只修改了代码中一个小小的地方,然后就需要重新擦除整个Flash,比较的费时,可以考虑从这个模式启动代码(也就是STM32的内存中),用于快速的程序调试,等程序调试完成后,在将程序下载到Flash中;

开发BOOT模式选择:

  1. 通常使用程序代码存储在主闪存存储器,配置方式:BOOT0=0,BOOT1=X;两个启动引脚通过10K电阻接地;

r3gu0x.png

  1. Flash锁死解决办法:开发调试过程中,由于某种原因导致内部Flash锁死,无法连接SWD以及Jtag调试,无法读到设备,可以通过修改BOOT模式重新刷写代码,修改为BOOT0=1,BOOT1=0即可从系统存储器启动,ST出厂时自带Bootloader程序,SWD以及JTAG调试接口都是专用的。重新烧写程序后,可将BOOT模式重新更换到BOOT0=0,BOOT1=X即可正常使用;

嵌入式系统的启动还需要一段启动代码(bootloader),类似于启动PC时的BIOS,一般用于完成微控制器的初始化工作和自检,STM32的启动代码在startup_stm32f40x_xx.s(xx根据微控制器所带的大、中、小容量存储器分别为hd、md、ld)中,其中的程序功能主要包括初始化堆栈、定义程序启动地址、中断向量表和中断服务程序入口地址,以及系统复位启动时,从启动代码跳转到用户main函数的入口地址 ,在后面的启动文件介绍中会有更详细描述;

USB接口,相应电平逻辑遵照USB原则,而单片机上的串行通信通过单片机的RXD、TXD、VCC、GND四个引脚,相应电平逻辑遵照TTL原则,如果要通过USB和单片机串口进行通信,需要转成一样的原则,一种方法是USB转TTL,一种是,TTL转USB,开发板使用的是通过CH340G将USB转TTL,与单片机进行通信;

一键下载电路:

rh2UNF.png

通过上位机控制BOOT电平,从而实现一键下载,下载时设置DTR的低电平复位,RTS高电平进BootLoader,但是这个上位机是按232标准编写的,与USB标准是相反的,所以实际是DTR的高电平复位,RTS低电平进BootLoader,要分析这个电路,需要对照复位电路和单片机上的RESET连接

so3xBV.jpg

当下载程序时,DTR输出高电平,RTS输出低电平,Q2,Q3,D7导通,RESET和RST是连接上的,即为低电平复位,BOOT0被上拉至高电平,下载完程序后,DTR输出低电平,RTS输出高电平,Q2,Q3,D7截止,RESET被上拉至高电平,Q3截止所以BOOT0为低电平,恢复正常运行模式

工程模板

实验0即是模板,复制过来用就行,怎么用:改主函数和HARDWARE文件夹里的内容,自己写的头文件添加到宏,不需要的宏删除,后面看实验教程看都看会了,不需要特意学视频教程中的工程模板创建;

GPIO原理

GPIO基本结构:

r1d1c8.png

四种输入模式:

  • 输入浮空

    r1y2LV.jpg

    I/O端口的电平信号直接进入输入数据寄存器。MCU直接读取I/O口电平,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的, (接用电压表测量其引脚电压为1点几伏,这是个不确定值) 以用来做KEY识别

  • 输入上拉

    r1y0Ig.jpg

    IO内部接上拉电阻,此时如果IO口外部没有信号输入或者引脚悬空,IO口默认为高电平 如果I/O口输入低电平,那么引脚就为低电平,MCU读取到的就是低电平

  • 输入下拉

    r1yHQ1.jpg

    IO内部接下拉电阻,此时如果IO口外部没有信号输入或者引脚悬空,IO口默认为低电平 如果I/O口输入高电平,那么引脚就为高电平,MCU读取到的就是高电平

  • 模拟输入

    r1yzJH.jpg

    当GPIO引脚用于ADC采集电压的输入通道时,用作"模拟输入"功能,此时信号不经过施密特触发器,直接直接进入ADC模块,并且输入数据寄存器为空 ,CPU不能在输入数据寄存器上读到引脚状态

    当GPIO用于模拟功能时,引脚的上、下拉电阻是不起作用的,这个时候即使配置了上拉或下拉模式,也不会影响到模拟信号的输入输出

除了 ADC 和 DAC 要将 IO 配置为模拟通道之外其他外设功能一律 要配置为复用功能模式

四种输出模式:

  • 开漏输出(带上拉或者下拉)

    r14zY6.jpg

    在开漏输出模式时,只有N-MOS管工作,如果我们控制输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出低电平,I/O端口的电平就是低电平,若控制输出为1时,高电平,则P-MOS管和N-MOS管都关闭,输出指令就不会起到作用,此时I/O端口的电平就不会由输出的高电平决定,而是由I/O端口外部的上拉或者下拉决定 如果没有上拉或者下拉 IO口就处于悬空状态

    并且此时施密特触发器是打开的,即输入可用,通过输入数据寄存器GPIOx_IDR可读取I/O的实际状态。,I/O口的电平不一定是输出的电平

  • 开漏复用功能(带上拉或者下拉)

    r15up8.jpg

    GPIO复用为其他外设,输出数据寄存器GPIOx_ODR无效; 输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取I/O实际状态 除了输出信号的来源改变 其他与开漏输出功能相同

  • 推挽式输出(带上拉或者下拉)

    r15iOH.jpg

    在推挽输出模式时,N-MOS管和P-MOS管都工作,如果我们控制输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出低电平,I/O端口的电平就是低电平,若控制输出为1 高电平,则P-MOS管导通N-MOS管关闭,使输出高电平,I/O端口的电平就是高电平, 外部上拉和下拉的作用是控制在没有输出时IO口电平

    此时施密特触发器是打开的,即输入可用,通过输入数据寄存器GPIOx_IDR可读取I/O的实际状态。I/O口的电平一定是输出的电平

  • 推挽式复用功能(带上拉或者下拉)

    r15Jkq.jpg

    GPIO复用为其他外设(如 I2C),输出数据寄存器GPIOx_ODR无效; 输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取I/O实际状态 除了输出信号的来源改变 其他与开漏输出功能相同

开漏输出和推挽输出的区别:

  • 推挽输出:可以输出强高低电平,连接数字器件

    推挽结构一般是指两个MOS管分别受两互补信号的控制,总是在一个管导通的时候另一个截止.

  • 开漏输出:可以输出强低电平,高电平得靠外部电阻拉高。

    输出端相当于MOS管漏极. 需要外接上拉电阻,才能实现输出高电平 合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内),在使用开漏模式时,都需要接上拉电阻,否则只能输出低电平;

STM32F4的大部分端口都具有复用功能,所谓复用,就是一些端口不仅仅可以做为通用IO口,还可以复用为一些外设引脚,比如PA9,PA10可以复用为STM32F4的串口1引脚,作用:最大限度的利用端口资源

所有IO口都可以作为中断输入;

跑马灯实验

硬件连接:

r1v9pj.png

共阳极连接,所以输出为低电平时,LED灯亮,输出高电平时,LED灯灭

一些常用的GPIO库函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//1个初始化函数:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

//2个读取输入电平函数:
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用于读取一个或几个IO口的输入状态
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);//用于读取一组IO口的输入状态

//2个读取输出电平函数:
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用于读取一个或几个IO口的输出状态
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);//用于读取一组IO口的输出状态

//4个设置输出电平函数:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用于设置一个或者多个IO口为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用于设置一个或者多个IO口为低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);//用于设置一个或者多个IO口为高电平或者低电平
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//用于设置一组IO口为高电平或低电平

跑马灯实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//led.h
#ifndef _LED_H_
#define _LED_H_
void LED_Init(void);
#endif

//main.c
#include "stm32f4xx.h"
#include "led.h"
#include "delay.h"
int main()
{
delay_init(168);
LED_Init();
for(;;)
{
GPIO_SetBits(GPIOF,GPIO_Pin_9);//set置1,reset置0
GPIO_ResetBits(GPIOF,GPIO_Pin_10);
delay_ms(500);
GPIO_ResetBits(GPIOF,GPIO_Pin_9);
GPIO_SetBits(GPIOF,GPIO_Pin_10);
delay_ms(500);
//位带操作
/*
PFout(9)=0;
PFout(10)=1;
delay_ms(500);
PFout(9)=1;
PFout(10)=0;
delay_ms(500);
*/
}
}

//LED.c
#include "led.h"
#include "stm32f4xx.h"
void LED_Init(void)
{

/*GPIO_InitTypeDef GPIO_Init_Structure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);
//F9
GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_OUT;
GPIO_Init_Structure.GPIO_OType=GPIO_OType_PP;
GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_9;
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_DOWN;
GPIO_Init_Structure.GPIO_Speed=GPIO_Fast_Speed;
GPIO_Init(GPIOF,&GPIO_Init_Structure);
GPIO_SetBits(GPIOF,GPIO_Pin_9);
//F10
GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_OUT;
GPIO_Init_Structure.GPIO_OType=GPIO_OType_PP;
GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_10;
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_DOWN;
GPIO_Init_Structure.GPIO_Speed=GPIO_Fast_Speed;
GPIO_Init(GPIOF,&GPIO_Init_Structure);
GPIO_setBits(GPIOF,GPIO_Pin_10);*/
GPIO_InitTypeDef GPIO_Init_Structure;//定义一个GPIO_InitTypeDef类型的结构体变量
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);//使能GPIOF时钟
GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_OUT;//设置输出模式
GPIO_Init_Structure.GPIO_OType=GPIO_OType_PP;//设置推挽模式
GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;//设置9和10引脚
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_UP;//设置上拉模式
GPIO_Init_Structure.GPIO_Speed=GPIO_Fast_Speed;//设置传输速度
GPIO_Init(GPIOF,&GPIO_Init_Structure);//按照设置初始化
GPIO_SetBits(GPIOF,GPIO_Pin_9|GPIO_Pin_10);//设置9和10引脚输出高电平
}

蜂鸣器实验

硬件连接:

r3i6pT.png

输出为高电平时,蜂鸣器响

蜂鸣器实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//beep.h
#ifndef _BEEP_H_
#define _BEEP_H_
void BEEP_Init(void);
#endif

//main.c
#include "stm32f4xx.h"
#include "delay.h"
#include "BEEP.h"
int main()
{
BEEP_Init();
delay_init(168);
while(1)
{
PFout(8)=0;
delay_ms(500);
PFout(8)=1;
delay_ms(500);
}
}

//beep.c
#include "BEEP.h"
#include "sys.h"
void BEEP_Init()
{
GPIO_InitTypeDef GPIO_Init_Structure;//定义一个GPIO_InitTypeDef类型的结构体变量
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF,ENABLE);//使能GPIOF时钟
GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_OUT;//设置输出模式
GPIO_Init_Structure.GPIO_OType=GPIO_OType_PP;//设置推挽模式
GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_8;//设置8引脚
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_DOWN;//设置下拉
GPIO_Init_Structure.GPIO_Speed=GPIO_Fast_Speed;//设置传输速度
GPIO_Init(GPIOF,&GPIO_Init_Structure);//按照设置初始化
}

按键输入实验

硬件连接:

rGejs0.png

实现支持连续按和不支持连续按两种模式切换 ,当mode=0时,不支持连续按,伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;
if(mode==1) key_up=1;//支持连续按
if(key_up && KEY按下)
{
delay_ms(10);//延时,防抖
key_up=0;//标记这次key已经按下
if(KEY确实按下)
{
return KEY_VALUE;
}
}else if(KEY没有按下) key_up=1;
return 没有按下
}

按键输入实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//led.h beep.h led.c beep.c和跑马灯和蜂鸣器实验一样
//key.h
#ifndef _KEY_H_
#define _KEY_H_
#include "sys.h"

#define KEY_0 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)
#define KEY_1 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)
#define KEY_2 GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_2)
#define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)

void KEY_Init(void);
u8 KEY_Scan(u8 mode);

#endif

//main.c
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "key.h"
int main()
{
u8 keyvalue;
delay_init(168);
LED_Init();
BEEP_Init();
KEY_Init();
PFout(9)=1;
PFout(10)=0;

while(1)
{
keyvalue=KEY_Scan(0);//不支持连续按
if(keyvalue)
{
switch(keyvalue)
{
case 1:
PFout(9)=!PFout(9);//绿灯翻转
break;
case 2:
PFout(10)=!PFout(10);//红灯翻转
break;
case 3:
PFout(9)=!PFout(9);
PFout(10)=!PFout(10);//两个灯都翻转
break;
case 4:
PFout(8)=!PFout(8);//蜂鸣器响和不响
break;
default:
break;
}
}
else delay_ms(10);
}
}

//key.c
#include "sys.h"
#include "key.h"
#include "delay.h"
void KEY_Init()
{
GPIO_InitTypeDef GPIO_Init_Structure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOE,ENABLE);
GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4;
GPIO_Init_Structure.GPIO_Mode=GPIO_Mode_IN;
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_UP;
GPIO_Init_Structure.GPIO_Speed=GPIO_Fast_Speed;
GPIO_Init(GPIOE,&GPIO_Init_Structure);

GPIO_Init_Structure.GPIO_Pin=GPIO_Pin_0;
GPIO_Init_Structure.GPIO_PuPd=GPIO_PuPd_DOWN;
GPIO_Init(GPIOA,&GPIO_Init_Structure);
}
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;
if(mode)
{
key_up=1;
}
if(key_up&&(KEY_0==0||KEY_1==0||KEY_2==0||WK_UP==1))
{
delay_ms(10);
key_up=0;
if(KEY_0==0)
{
return 1;
}
if(KEY_1==0)
{
return 2;
}
if(KEY_2==0)
{
return 3;
}
if(WK_UP==1)
{
return 4;
}
}
else if(KEY_0==1&&KEY_1==1&&KEY_2==1&&WK_UP==0)
{
key_up=1;
}
return 0;
}

时钟树

时钟系统是CPU的脉搏,就像人的心跳一样, STM32F4的时钟系统比较复杂,不像简单的51单片机一个系统时钟就可以解决一切,这是因为STM32本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比如看门狗以及RTC只需要几十k的时钟即可,同一个电路,时钟越快功耗越大,同时抗电磁干扰能力也会越弱,所以对于较为复杂的MCU一般都是采取多时钟源的方法来解决这些问题;

STM32F4的时钟系统图:

rJzx9f.jpg

在STM32F4中,有5个最重要的时钟源,为LSI、LSE、HSI、HSE、PLL,其中PLL实际是分为两个时钟源,分别为主PLL和专用PLL。从时钟频率来分可以分为高速时钟源和低速时钟源,在这5个中HSI,HSE以及PLL是高速时钟,LSI和LSE是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中HSE和LSE是外部时钟源,其他的是内部时钟源;

位置: 顺序从上到下LSI、LSE、HSI、HSE、PLL,带E的时钟和PLL靠近外侧,带I的时钟靠近内侧;

STM32F4的5个时钟源:

  • LSI是低速内部时钟,RC振荡器,频率为32kHz左右,供独立看门狗和自动唤醒单元使用

  • LSE是低速外部时钟,接频率为32.768kHz的石英晶体,这个主要是RTC的时钟源

  • HSI是高速内部时钟,RC振荡器,频率为16MHz,可以直接作为系统时钟或者用作PLL输入

  • HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHz,HSE也可以直接做为系统时钟或者PLL输入

  • PLL为锁相环倍频输出,STM32F4有两个PLL,均由HSE 或者HSI 提供时钟输入信号:

    • 主PLL两个不同的输出时钟。

      • 第一个输出PLLP用于生成高速的系统时钟(最高168MHz)

      • 第二个输出PLLQ用于USB OTG FS 的时钟(48M)、RNG(随机数发生器) 和SDIO 时钟

    • 专用PLL(PLLI2S)用于生成精确时钟,从而在I2S接口实现高品质音频性能

主PLL时钟第一个高速时钟输出PLLP的计算方法:

主PLL时钟的时钟源要先经过一个分频系数为M(2 ~ 63)的分频器,得到VCO输入信号(范围1 ~2M)然后经过倍频系数为N的倍频器出来之后,得到VCO输出信号(范围193 ~ 432M)还需要经过一个分频系数为P(第一个输出PLLP)或者Q(第二个输出PLLQ)的分频器分频之后,最后才生成最终的主PLL时钟

假设外部晶振选择8MHz,同时设置相应的分频器M=8,倍频器倍频系数N=336,

分频器分频系数P=2,那么主PLL生成的第一个输出高速时钟PLLP为:

PLL=8MHz * N/ (M*P)=8MHz * 336 /(8 * 2) = 168MHz

如果我们选择HSE为PLL时钟源,同时SYSCLK时钟源为PLL,那么SYSCLK时钟为168MHz,后面的实验都是采用这样的配置

IWDGCLK: 看门狗时钟的输入,看门狗时钟源只能是低速的LSI时钟;

RTCCLK: RTC时钟源,RTC的时钟源可以选择LSI,LSE,以及HSE分频后的时钟,HSE分频系数为2~31,通常的做法是由LSE 给RTC 提供时钟,大小为32.768KHZ,LSE由外接的晶体谐振器产生,所配的谐振电容精度要求高,不然很容易不起震,选择方式是编程 RCC 备份域控制寄存器 (RCC_BDCR) 中的 RTCSEL[1:0] 位和 RCC 时钟配置寄存器 (RCC_CFGR) 中的 RTCPRE[4:0] 位;

SYSCLK: 系统时钟,SYSCLK系统时钟来源:HSI,HSE和PLL,在实际应用中,因为对时钟速度要求都比较高我们才会选用STM32F4这种级别的处理器,所以一般情况下,都是采用PLL作为SYSCLK时钟源,当HSE 出现故障的时候(PLL时钟来源HSE时钟),系统时钟会切换为HSI=16M,直到HSE 恢复正常为止根据前面的计算公式,可以算出你的系统的SYSCLK是多少,具体的由时钟配置寄存器RCC_CFGR的SW 位配置;

HCLK: AHB 总线时钟,系统时钟SYSCLK 经过AHB 预分频器分频之后得到时钟叫AHB 总线时钟,即HCLK,分频因子可以是:[1, 2,4,8,16,64,128,256,512],具体的由时钟配置寄存器RCC_CFGR 的HPRE 位设置,片上大部分外设的时钟都是经过HCLK 分频得到,至于AHB总线上的外设的时钟设置为多少,得等到使用该外设的时候才设置,这里设置为1 分频,即HCLK=SYSCLK=168M;

**HCLK2:**APB2 总线时钟PCLK2, 由HCLK 经过高速APB2 预分频器得到,分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器RCC_CFGR 的PPRE2 位设置。HCLK2 属于高速的总线时钟,片上高速的外设就挂载到这条总线上

HCLK1: APB1 总线时钟PCLK1 由HCLK 经过低速APB 预分频器得到,分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器RCC_CFGR 的PPRE1 位设置,HCLK1 属于低速的总线时钟,最高为42M,片上低速的外设就挂载到这条总线上

I2S时钟源: I2S的时钟源来源于PLLI2S或者映射到I2S_CKIN引脚的外部时钟,I2S出于音质的考虑,对时钟精度要求很高,STM32F4开发板使用的是内部PLLI2SCLK。

输出时钟MCO1和MCO2: MCO1是向芯片的PA8引脚输出时钟,它有四个时钟来源分别为:HSI,LSE,HSE和PLL时钟,MCO2是向芯片的PC9输出时钟,它同样有四个时钟来源分别为:HSE,PLL,SYSCLK以及PLLI2S时钟,MCO输出时钟频率最大不超过100MHz;

以太网PTP时钟,AHB时钟,APB2高速时钟,APB1低速时钟,这些时钟都是来源于SYSCLK系统时钟。其中以太网PTP时钟是使用系统时钟,AHB,APB2和APB1时钟是经过SYSCLK时钟分频得来,AHB最大时钟为168MHz, APB2高速时钟最大频率为84MHz,而APB1低速时钟最大频率为42MHz;

STM32F4内部以太网MAC时钟的来源: 对于MII接口来说,必须向外部PHY芯片提供25Mhz的时钟,这个时钟,可以由PHY芯片外接晶振,或者使用STM32F4 的MCO输出来提供。然后,PHY 芯片再给STM32F4提供ETH_MII_TX_CLK和ETH_MII_RX_CLK时钟。对于RMII接口来说,外部必须提供50Mhz的时钟驱动PHY和STM32F4的ETH_RMII_REF_CLK,这个50Mhz时钟可以来自PHY、有源晶振或者STM32F4的MCO,开发板使用的是RMII 接口,使用PHY 芯片提供50Mhz时钟驱动STM32F4 的ETH_RMII_REF_CLK;

外部PHY提供的USB OTG HS(60MHZ)时钟: F407 的USB 没有集成PHY,要想实现USB 高速传输的话,必须外置USB PHY 芯片,常用的芯片是USB3300。当外接USB PHY 芯片时,PHY 芯片需要给MCU 提供一个时钟;

时钟初始化配置

STM32F4时钟系统初始化是在system_stm32f4xx.c中的SystemInit()函数中完成的,总共6个步骤,初始化之后的状态:

  • SYSCLK(系统时钟) =168MHz
  • AHB总线时钟(HCLK=SYSCLK) =168MHz
  • APB1总线时钟(PCLK1=SYSCLK/4) =42MHz
  • APB2总线时钟(PCLK2=SYSCLK/2) =84MHz
  • PLL主时钟 =168MHz
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
* 使用HSE 时,设置系统时钟的步骤
* 1、开启HSE ,并等待 HSE 稳定
* 2、设置 AHB、APB2、APB1 的预分频因子
* 3、设置PLL 的时钟来源
* 设置VCO 输入时钟 分频因子 m
* 设置VCO 输出时钟 倍频因子 n
* 设置PLLCLK 时钟分频因子 p
* 设置OTG FS,SDIO,RNG 时钟分频因子 q
* 4、开启PLL,并等待PLL 稳定
* 5、把PLLCK 切换为系统时钟SYSCLK
* 6、读取时钟切换状态位,确保PLLCLK 被选为系统时钟
*/
#define PLL_M 25
#define PLL_N 336
#define PLL_P 2
#define PLL_Q 7
// 要超频的话,修改PLL_N 这个宏即可,取值范围为:192~432。
void SetSysClock(void)
{
__IO uint32_t StartUpCounter = 0, HSEStatus = 0;

// 1、使能HSE
RCC->CR |= ((uint32_t)RCC_CR_HSEON);

// 等待HSE 启动稳定
do
{
HSEStatus = RCC->CR & RCC_CR_HSERDY;
StartUpCounter++;
} while ((HSEStatus==0)&&(StartUpCounter!=HSE_STARTUP_TIMEOUT));

if ((RCC->CR & RCC_CR_HSERDY) != RESET)
{
HSEStatus = (uint32_t)0x01;
}
else
{
HSEStatus = (uint32_t)0x00;
}

// HSE 启动成功
if (HSEStatus == (uint32_t)0x01)
{
// 调压器电压输出级别配置为1,以便在器件为最大频率
// 工作时使性能和功耗实现平衡
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
PWR->CR |= PWR_CR_VOS;

// 2、设置AHB/APB2/APB1 的分频因子
// HCLK = SYSCLK / 1
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
// PCLK2 = HCLK / 2
RCC->CFGR |= RCC_CFGR_PPRE2_DIV2;
// PCLK1 = HCLK / 4
RCC->CFGR |= RCC_CFGR_PPRE1_DIV4;

// 3、配置主PLL 的时钟来源,设置M,N,P,Q
// Configure the main PLL
RCC->PLLCFGR = PLL_M|(PLL_N<<6)|(((PLL_P >> 1) -1) << 16) |(RCC_PLLCFGR_PLLSRC_HSE) |(PLL_Q << 24);

// 4、使能主PLL
RCC->CR |= RCC_CR_PLLON;

// 等待PLL 稳定
while ((RCC->CR & RCC_CR_PLLRDY) == 0)
{
}
/*----------------------------------------------------*/
// 配置FLASH 预取指,指令缓存,数据缓存和等待状态
FLASH->ACR = FLASH_ACR_PRFTEN|FLASH_ACR_ICEN|FLASH_ACR_DCEN|FLASH_ACR_LATENCY_5WS;
/*---------------------------------------------------*/

// 5、选择主PLLCLK 作为系统时钟源
RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
RCC->CFGR |= RCC_CFGR_SW_PLL;

// 6、读取时钟切换状态位,确保PLLCLK 选为系统时钟
while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS )!= RCC_CFGR_SWS_PLL);
{
}
}
else
{
// HSE 启动出错处理
}
}

SystemInit函数介绍

在调用main函数前就会调用SystemInit函数,如果使用比较老的标准库,SystemInit要在主函数中调用,使用新的标准库都不需要再在主函数中调用,SystemInit函数完成系统的初始化,在下面代码标出主要进行哪些操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void SystemInit(void)
{
/* FPU设置(浮点运算)*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2)); /* set CP10 and CP11 Full Access */
#endif
/* 重置RCC时钟配置为默认配置*/
/* 打开HSI时钟 */
RCC->CR |= (uint32_t)0x00000001;

/* 重置CFGR寄存器 */
RCC->CFGR = 0x00000000;

/* 关闭HSEON, CSSON and PLLON时钟 */
RCC->CR &= (uint32_t)0xFEF6FFFF;

/* 重置PLLCFGR寄存器 */
RCC->PLLCFGR = 0x24003010;

/* 重置HSEBYP寄存器 */
RCC->CR &= (uint32_t)0xFFFBFFFF;

/* 关闭所有中断 */
RCC->CIR = 0x00000000;

#if defined (DATA_IN_ExtSRAM) || defined (DATA_IN_ExtSDRAM)
SystemInit_ExtMemCtl(); //初始化外部存储器
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */

/* Configure the System clock source, PLL Multiplier and Divider factors,
AHB/APBx prescalers and Flash settings ----------------------------------*/
SetSysClock();//配置时钟,详细介绍参考时钟树章节

/* Configure the Vector Table location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif

启动文件介绍

启动文件由汇编编写,是系统上电复位后第一个执行的程序,主要做了以下工作:

  1. 初始化堆栈指针SP=_ initial_sp _
  2. 初始化PC 指针=Reset_Handler
  3. 初始化中断向量表
  4. 配置系统时钟
  5. 调用C 库函数_main 初始化用户堆栈,从而最终调用main 函数去到C 的世界

Systick定时器

基础知识:

Systick定时器,系统滴答定时器,是一个简单的定时器,对于CM3,CM4内核芯片,都有Systick定时器,Systick定时器常用来做延时,或者实时系统的心跳时钟,这样可以节省MCU资源,不用浪费一个定时器,比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟;

Systick定时器一个24 位的倒计数定时器,计到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作;

SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15),Systick中断的优先级可以设置;

4个Systick寄存器:CTRL,LOAD,VAL,CALIB

CTRL:

位段 名称 类型 复位值 描述
16 COUNTFLAG R 0 如果上次读取本寄存器后,Systick已经数到0,则该位为1.如果读取该位,该位自动清零
2 CLKSOURCE R/W 0 为0使用外部时钟源,为1使用内核时钟源
1 TICKINT R/W 0 为1,倒数到0时产生Systick异常请求,为0则倒数到0时无动作
0 ENABLE R/W 0 使能位

对于STM32,外部时钟源是 HCLK(AHB总线时钟)的1/8,内核时钟是 HCLK时钟

LOAD:

位段 名称 类型 复位值 描述
23:0 RELOAD R/W 0 当倒数到0时,将被重装载的值

VAL:

位段 名称 类型 复位值 描述
23:0 CURRENT R/W 0 读取时返回当前倒计数的值,写它则使之清零,同时还会清除在Systick控制及状态寄存器的COUNTFLAG标志

Systick相关库函数

1
2
3
4
5
SysTick_CLKSourceConfig() ;   //Systick时钟源选择,在misc.c文件中(在SysTick_Config函数重新设置了CTRL的值,这个函数没用,要改时钟源修改SysTick_Config函数)

SysTick_Config(uint32_t ticks) ;//初始化systick,时钟为HCLK,并开启中断,在core_cm3.h/core_cm4.h文件中,修改时钟源,中断优先级在这个函数修改

void SysTick_Handler(void);//Systick中断服务函数

使用Systick定时器中断方式实现跑马灯的延时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include "sys.h"
#include "led.h"
u16 TimeDelay;//定义全局变量
void delay(u16 nTime)
{
//SysTick_CLKSourceConfig(ysTick_CLKSource_HCLK) ;//即使这里配置了时钟源,后面的SysTick_Config会重新配置,所以这句话在这个程序无效,要配置 时钟源需要修改SysTick_Config函数
SysTick_Config(168000);//默认设置了内核时钟源HCLK为Systick时钟,传的参数值-1相当传给VAL寄存器的值
TimeDelay=nTime;
while(TimeDelay);//当ntime不等于0时就卡在这里,等待SysTick_Handler中断服务函数将TimeDelay减为零就退出循环了,实现延时效果
SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;//使用完后关闭Systick定时器
}
void SysTick_Handler(void)
{
if(TimeDelay!=0x00)
{
TimeDelay--;//每产生一次中断减1
}
}
int main(void)
{
LED_Init();
for(;;)
{
GPIO_SetBits(GPIOF,GPIO_Pin_9);
GPIO_ResetBits(GPIOF,GPIO_Pin_10);
delay(500);
GPIO_ResetBits(GPIOF,GPIO_Pin_9);
GPIO_SetBits(GPIOF,GPIO_Pin_10);
delay(500);
}
}

JLINK在线调试+软件调试方法与技巧

JTAG/SWD调试原理: STM32F4xx的内核是CortexTM-M4F,该内核包含用于高级调试功能的硬件,利用这些调试功能,可以在取指(指令断点)或取访问数据(数据断点)时停止内核,内核停止时,可以查询内核的内部状态和系统的外部状态,查询完成后,将恢复内核和系统并恢复程序执行,当调试器与STM32F4xx MCU相连并进行调试时,将使用内核的硬件调试模块;

rUCAfA.png

如何复用 JTAG引脚

rUCum8.png

禁止JTAG-DP和SW-DP的库函数

1
2
GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE);// 改变指定管脚的映射 GPIO_Remap_SWJ_Disable SWJ 完全禁用(JTAG+SW-DP)
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE);// 改变指定管脚的映射 GPIO_Remap_SWJ_JTAGDisable ,JTAG-DP 禁用 + SW-DP 使能

注意:复用后会导致无法使用JTAG/SWD模式下载程序,一般不会这样使用

MDK调试模式下工具条各个图标的功能

rUCo9A.png

调试使用的快捷键和功能整体和VS差不多

在Peripheral工具栏可以查看寄存器,时钟等的值

IO引脚复用和映射

端口复用: STM32有很多的内置外设,这些外设的外部引脚都是与GPIO复用的。也就是说,一个GPIO如果可以复用为内置外设的功能引脚,那么当这个GPIO作为内置外设使用的时候,就叫做复用,例如串口1 的发送接收引脚是PA9,PA10,当我们把PA9,PA10不用作GPIO,而用做复用功能串口1的发送接收引脚的时候,叫端口复用,哪些引脚可以复用为哪些功能可以查询引脚分配表;

STM32F4的端口复用映射原理: STM32F4系列微控制器IO引脚通过一个复用器连接到内置外设或模块,该复用器一次只允许一个外设的复用功能(AF)连接到对应的IO口,这样可以确保共用同一个IO引脚的外设之间不会发生冲突,每个IO引脚都有一个复用器,该复用器采用16路复用功能输入(AF0到AF15),可通过GPIOx_AFRL(针对引脚0-7)和GPIOx_AFRH(针对引脚8-15)寄存器对这些输入进行配置,每四位控制一路复用;

rUiRFe.jpg

复用功能映射配置:

  1. 系统功能

    将lO连接到AFO,然后根据所用功能进行配置:

    • JTAG/SWD:在各器件复位后,会将这些引脚指定为专用引脚,可供片上调试模块立即使用(不受GPIO控制器控制)
    • RTC_REFIN:此引脚应配置为输入浮空模式
    • MCO1和MCO2:这些引脚必须配置为复用功能模式
  2. GPIO

    在GPIO_MODER寄存器中将所需I/O配置为输入或输出

  3. 外设复用功能

    对于ADC和 DAC,在GPIOx_MODER寄存器中将所需IO配置为模拟通道

    对于其它外设:

    • 在GPIOx_MODER寄存器中将所需I/O配置为复用功能
    • 通过GPIOx_OTYPER、GPIOx_PUPDR和GPIOx_OSPEEDER寄存器,分别选择类型、上拉/下拉以及输出速度
    • 在GPIOx_AFRL 或GPIOx_AFRH寄存器中,将IO连接到所需AFx

以PA9,PA10复用为串口1的配置为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能USART1时钟

//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA9,PA10

//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1); //GPIOA9复用为USART1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1); //GPIOA10复用为USART1

NVIC中断优先级管理

STM32F4并没有使用CM4内核的全部东西,而是只用了它的一部分,STM32F40xx/STM32F41xx总共有92个中断,包括10个内核中断和82个可屏蔽中断,具有16级可编程的中断优先级,常见的内核中断包括复位(reset),不可屏蔽中断(NMI),硬错误(Hardfault),而我们常用的就是这82个可屏蔽中断;

中断管理方法: 对STM32中断进行分组,组0~4,同时,对每个中断设置一个抢占优先级和一个响应优先级值;

分组配置是在寄存器SCB->AIRCR中配置:

AIRCR[10:8] IP bit[7:4]分配情况 分配结果
0 111 0:4 0位抢占优先级,4位响应优先级
1 110 1:3 1位抢占优先级,3位响应优先级
2 101 2:2 2位抢占优先级,2位响应优先级
3 100 3:1 3位抢占优先级,1位响应优先级
4 011 4:0 4位抢占优先级,0位响应优先级

抢占优先级 & 响应优先级:

  • 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的

  • 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断

  • 抢占优先级相同的中断,当两个中断同时发生的情况下,哪个响应优先级高,哪个先执行

  • 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行

例如:设置中断优先级组为2,然后设置

中断3(RTC中断)的抢占优先级为2,响应优先级为1

中断6(外部中断0)的抢占优先级为3,响应优先级为0

中断7(外部中断1)的抢占优先级为2,响应优先级为0

那么这3个中断的优先级顺序为:中断7>中断3>中断6;

一般情况下,系统代码执行过程中,只设置一次中断优先级分组,设置好分组之后一般不会再改变分组,随意改变分组会导致中断管理混乱,程序出现意想不到的执行结果;

优先级分组函数:

1
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);//设置分组2

中断初始化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
typedef struct
{
uint8_t NVIC_IRQChannel; //设置中断源
uint8_t NVIC_IRQChannelPreemptionPriority;//设置响应优先级
uint8_t NVIC_IRQChannelSubPriority; //设置抢占优先级
FunctionalState NVIC_IRQChannelCmd; //使能/使能
} NVIC_InitTypeDef;

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口1中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化NVIC寄存器

中断优先级设置步骤:

  1. 系统运行后先设置中断优先级分组。调用函数:

    1
    void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

整个系统执行过程中,只设置一次中断分组

  1. 针对每个中断,设置对应的抢占优先级和响应优先级:

    1
    void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);
  2. 如果需要挂起/解挂,查看中断当前激活状态,分别调用相关函数;

串口通信基本原理

处理器与外部设备通信的两种方式:

并行通信:传输原理:数据各个位同时传输,优点:速度快,缺点:占用引脚资源多;

串行通信:传输原理:数据按位顺序传输,优点:占用引脚资源少,缺点:速度相对较慢;

串行通信按照数据传送方向,分为:

  • 单工: 数据传输只支持数据在一个方向上传输
  • 半双工:允许数据在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信
  • 全双工:允许数据同时在两个方向上传输,因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立
    的接收和发送能力

串行通信的通信方式:

同步通信:带时钟同步信号传输,SPI,IIC通信接口

异步通信:不带时钟同步信号,UART(通用异步收发器),单总线

常见的串行通信接口:

通信标准 引脚说明 通信方式 通信方向
UART
(通用异步收发器)
TXD:发送端
RXD:接收端
GND:公共端
异步通信 全双工
单总线
(1-wire)
DQ:发送/接收端 异步通信 半双工
SPI SCK:同步时钟
MISO:主机输入,从机输出
MOSI:主机暑促,从机输入
同步通信 全双工
I2C SCL:同步时钟
SDA:数据输入/输出端
同步通信 半双工

串口通信接口: USRT:通用异步收发器,USART:通用同步异步收发器;

USRT异步通信方式引脚连接方法:

rDZnS0.jpg

哪些引脚可以复用为USRT/USART可通过原理图、数据手册或引脚分配表查询;

串口通信过程:

rDZx74.jpg

串口通信框图:

rDMdXt.jpg

串口异步通信需要定义的参数:

  • 起始位
  • 数据位(8或带奇偶校验位9位)
  • 奇偶校验位(第9位,可以没有,奇偶是指前面数据位中1的个数是奇数还是偶数,奇偶校验位通过加0或加1的方式保证数据位的1的个数是奇数或者偶数)
  • 停止位(1,2,,5,15位)
  • 波特率设置

串口寄存器和库函数配置

常用的串口相关寄存器:

  • USART_SR 状态寄存器
  • USART_DR 数据寄存器
  • USART_BSR 波特率寄存器

串口操作相关库函数:

1
2
3
4
5
6
7
8
9
10
11
12
//省略了参数
void USART_Init(); //串口初始化:波特率,数据字长,奇偶校验,硬件流控以及收发使能
void USART_Cmd();//使能串口
void USART_ITConfig();//使能相关中断

void USART_SendData();//发送数据到串口,DR
uint16_t USART_ReceiveData();//接受数据,从DR读取接受到的数据

FlagStatus USART_GetFlagStatus();//获取状态标志位
void USART_ClearFlag();//清除状态标志位
ITStatus USART_GetITStatus();//获取中断状态标志位
void USART_ClearITPendingBit();//清除中断状态标志位

波特率的计算:

假设串口要设置115200的波特率,PLCK的时钟(即APB2总线时钟频率)为84M,则

USARTDIV=84000000/(115200*16)=45.572

那么

DIV_Fraction=16*0.572=8=0x09;

DIV_Mantissa=45=0x2d;

硬件连接:PA9,PA10(串口1)连接到了USB串口电路;

串口配置的一般步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RCC_APBxPeriphClockCmd();//1.串口时钟使能,GPIO时钟使能
RCC_AHB1PeriphClockCmd();
GPIO_PinAFConfig();//2.引脚复用映射
GPIO_Init();//3.GPIO端口模式设置,模式设置为GPIO_Mode_AF
USART_Init();//4.串口参数初始化
NVIC_Init();//5.开启中断并且初始化NVIC(如果需要开启中断才需要这个步骤)
USART_ITConfig();
USART_Cmd();//6.使能串口
USARTx_IRQHandler();//7.编写中断处理函数
//8.串口数据收发
void USART_SendData();//发送数据到串口,DR
uint16_t USART_ReceiveData();//接受数据,从DR读取接受到的数据
//9.串口传输状态获取:
FlagStatus USART_GetFlagStatus();
void USART_ClearITPendingBit();

简单串口实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//usrt.h
#ifndef _USRT_H_
#define _USRT_H_

void MyUSRT_Init(void);
#endif

//main.c
#include "stm32f4xx.h"
#include "usrt.h"
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组
MyUSRT_Init();//串口初始化
while(1);//死循环,等待发生串口中断,执行中断服务函数
}

//usrt.c
#include "usrt.h"
#include "stm32f4xx.h"
void MyUSRT_Init()
{
USART_InitTypeDef USART_Init_Structure;//定义一个串口初始化结构体
GPIO_InitTypeDef GPIO_InitTypeDef_Structure;//定义一个GPIO初始化结构体
NVIC_InitTypeDef NVIC_InitTypeDef_Structure;//定义一个中断初始化结构体

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//使能串口时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA ,ENABLE);//使能GPIO时钟

GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);//设置9,10引脚复用为串口
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);

GPIO_InitTypeDef_Structure.GPIO_Mode=GPIO_Mode_AF;//设置为复用模式
GPIO_InitTypeDef_Structure.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;//9和10引脚
GPIO_InitTypeDef_Structure.GPIO_PuPd=GPIO_PuPd_UP;//设置上拉模式
GPIO_InitTypeDef_Structure.GPIO_Speed=GPIO_High_Speed;//设置传输速度
GPIO_Init(GPIOA,&GPIO_InitTypeDef_Structure);//初始化GPIO

USART_Init_Structure.USART_BaudRate=115200;//设置波特率
USART_Init_Structure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//关闭硬件流控制
USART_Init_Structure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;//设置发送和接受模式
USART_Init_Structure.USART_Parity=USART_Parity_No;//设置没有奇偶校验
USART_Init_Structure.USART_StopBits=USART_StopBits_1;//设置停止位为1位
USART_Init_Structure.USART_WordLength=USART_WordLength_8b;//设置数据位为8位
USART_Init(USART1,&USART_Init_Structure);

NVIC_InitTypeDef_Structure.NVIC_IRQChannel=USART1_IRQn;//设置中断通道,产生什么中断
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;//使能中断
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=1;//设置抢占优先级
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=1;//设置响应优先级
NVIC_Init(&NVIC_InitTypeDef_Structure);//初始化中断

USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//设置中断触发方式
USART_Cmd(USART1,ENABLE);//使能串口
}

void USART1_IRQHandler()
{
u8 res;
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE))
{
res=USART_ReceiveData(USART1);
USART_SendData(USART1,res);//接受到什么数据,就将什么数据再发送出去
}
}

串口通信实验

usart.c实现的相关协议:

1
2
3
#define USART_REC_LEN   200  	//定义最大接收字节数 
u8 USART_RX_BUF[USART_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
u16 USART_RX_STA; //接收状态标记
USART_RX_STA
bit15 bit14 bit13~0
接收完成标志 接收到0X0D标志 接收到的有效数据个数

接收到0x0d和0x0a表示所有数据接受完成

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void USART1_IRQHandler(void) 
{
u8 Res;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)//接收完成一个字节数据产生中断
{
Res =USART_ReceiveData(USART1);//接收数据传给RES

if((USART_RX_STA&0x8000)==0)//如果未接收完
{
if(USART_RX_STA&0x4000)//如果上一次接受到了0x0d
{
if(Res!=0x0a) USART_RX_STA=0;//没有接收到0x0a表示并不是接收完成标志
else USART_RX_STA|=0x8000;//接收到了0x0a,数据接收完成
}
else
{
if(Res==0x0d) USART_RX_STA|=0x4000;//接收到了0x0d
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;//将接收到的数据缓存到数组
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1)) USART_RX_STA=0;
}
}
}
}

串口通信实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "stm32f4xx.h"
#include "usart.h"
#include "delay.h"
int main()
{
u8 t=0;
u8 len;
u16 times;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);
while(1)
{
if(USART_RX_STA&0x8000)//接收完成了
{
len=USART_RX_STA&0x3fff;
printf("\r\n发送的消息为:\r\n");//打印(发送)“发送的消息为”到串口
for(t=0;t<len;t++)
{
USART_SendData(USART1,USART_RX_BUF[t]);//发送接收到的数据到串口
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送完成
}
printf("\r\n");
USART_RX_STA=0;
}
else
{
times++;
if(times%5000==0)
{
printf("\r\nALIENTEK 探索者开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if(times%1000==0)printf("请输入数据,以回车键结束\r\n");
delay_ms(10);
}
}
}

外部中断实验

STM32F4的每个IO都可以作为外部中断输入

STM32F4的中断控制器支持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唤醒事件

每个外部中断线可以独立的配置触发方式(上升沿,下降沿或者双边沿触发),触发/屏蔽,专用的状态位;

从上面可以看出,STM32F4供IO使用的中断线只有16个 ,STM32F4xx系列的IO口多达上百个,

中断线怎么跟io口对应:

GPIOx.0映射到EXTI0

GPIOx.1映射到EXTI1

GPIOx.15映射到EXTI15

对于每个中断线,我们可以设置相应的触发方式(上升沿触发,下降沿触发,边沿触发)以及使能;

IO口外部中断在中断向量表中只分配了7个中断向量,也就是只能使用7个中断服务函数

rsyhtO.png

外部中断线5 ~ 9分配一个中断向量,共用一个服务函数外部中断线10 ~ 15分配一个中断向量,共用一个中断服务函数;

EXTI框图

r5ZlLD.png

中断服务函数列表:

1
2
3
4
5
6
7
EXTI0_IRQHandler           
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
EXTI9_5_IRQHandler
EXTI15_10_IRQHandler

常用外部中断库函数:

1
2
3
4
5
void SYSCFG_EXTILineConfig(uint8_t EXTI_PortSourceGPIOx, uint8_t EXTI_PinSourcex);//设置IO口与中断线的映射关系
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);//初始化中断线:触发方式等
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);//判断中断线中断状态,是否发生
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);//清除中断线上的中断标志位
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//使能SYSCFG时钟,非常重要,在使用外部中断的时候一定要先使能SYSCFG时钟

EXTI_Init函数:

1
2
3
4
5
6
7
8
9
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

typedef struct
{
uint32_t EXTI_Line; //指定要配置的中断线
EXTIMode_TypeDef EXTI_Mode; //模式:事件 OR中断
EXTITrigger_TypeDef EXTI_Trigger;//触发方式:上升沿/下降沿/双沿触发
FunctionalState EXTI_LineCmd; //使能 OR失能
}EXTI_InitTypeDef;

外部中断的一般配置步骤:

1
2
3
4
5
6
7
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//1.使能SYSCFG时钟
GPIO_Init();//2.初始化IO口为输入
void SYSCFG_EXTILineConfig();//设置IO口与中断线的映射关系
EXTI_Init();//初始化线上中断,设置触发条件等
NVIC_Init();//配置中断分组(NVIC),并使能中断
EXTIx_IRQHandler();//编写中断服务函数
EXTI_ClearITPendingBit();//清除中断标志位

按键硬件连接:

r1v9pj.png

外部中断实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//main.c
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "key.h"
#include "exti.h"
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级分组2
delay_init(168);
LED_Init();
BEEP_Init();
EXTIX_Init();//外部中断初始化
while(1)
{

}
}

//exti.c
#include "exti.h"
#include "sys.h"
#include "key.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
void EXTI0_IRQHandler(void)//按下WKUP键LED0反转
{
delay_ms(10);
if(WK_UP==1)
{
LED0=!LED0;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
void EXTI2_IRQHandler(void)//按下KEY2键LED1反转
{
delay_ms(10);
if(KEY_2==0)
{
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line2);
}
void EXTI3_IRQHandler(void)//按下KEY1键蜂鸣器反转
{
delay_ms(10);
if(KEY_1==0)
{
BEEP=!BEEP;
}
EXTI_ClearITPendingBit(EXTI_Line3);
}
void EXTI4_IRQHandler(void)//按下KEY0键LED0,1都反转
{
delay_ms(10);
if(KEY_0==0)
{
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line4);
}
//exti.c
void EXTIX_Init(void)
{
NVIC_InitTypeDef NVIC_InitTypeDef_Structure;//定义初始化结构体
EXTI_InitTypeDef EXTI_InitTypeDef_Structure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG,ENABLE);//使能SYSCFG时钟

KEY_Init();//初始化key对应GPIO
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource2);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource3);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE,EXTI_PinSource4);
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA,EXTI_PinSource0);

EXTI_InitTypeDef_Structure.EXTI_Line=EXTI_Line2|EXTI_Line3|EXTI_Line4;
EXTI_InitTypeDef_Structure.EXTI_LineCmd=ENABLE;
EXTI_InitTypeDef_Structure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitTypeDef_Structure.EXTI_Trigger=EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitTypeDef_Structure);

EXTI_InitTypeDef_Structure.EXTI_Line=EXTI_Line0;
EXTI_InitTypeDef_Structure.EXTI_LineCmd=ENABLE;
EXTI_InitTypeDef_Structure.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitTypeDef_Structure.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitTypeDef_Structure);//初始化按键对应GPIO的EXTI

NVIC_InitTypeDef_Structure.NVIC_IRQChannel=EXTI0_IRQn;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitTypeDef_Structure);

NVIC_InitTypeDef_Structure.NVIC_IRQChannel=EXTI2_IRQn;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitTypeDef_Structure);

NVIC_InitTypeDef_Structure.NVIC_IRQChannel=EXTI3_IRQn;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=3;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitTypeDef_Structure);

NVIC_InitTypeDef_Structure.NVIC_IRQChannel=EXTI4_IRQn;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=4;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitTypeDef_Structure);//中断优先级初始化
}

基本定时器原理

定时器功能: 定时、输出比较、输入捕获、互补输出;

定时器分类: 基本定时器、通用定时器、高级定时器;

定时器资源: 407有2个高级定时器、10个通用定时器、2个基本定时器

各个定时器特性:

r7E4u6.png

基本定时器特点:

  • 计数器16bit,只能向上计数,只有TIM6和TIM7
  • 没有外部的GPIO,是内部资源,只能用来定时
  • 时钟来自PCLK1,可实现1~65536分频

基本定时器功能框图:

r7meqP.png

分三个部分:1.时钟源,2.控制器,3.计数器

时钟源来自RCC的TIMx_CLK (属于内部的CK_INT)

TIMx_CLK怎么看是多少:

r7nnyR.jpg

先看定时器挂在哪个时钟上,基本定时器是挂载在APB1上的,分频系数是4,168/4=42M,如果分频系数是1,则总线时钟就等于APB1,如果分频系数不是1,则乘以2,这里分频系数是4,所以是乘以2,得到TIMx_CLK时钟是42*2=84M

控制器用于控制定时器的:复位、使能、计数、触发DAC,涉及到的寄存器为:CR1/2、DIER、EGR、SR;

定时器最主要的就是时基部分: 包括 预分频器、计数器、自动重装载寄存器

预分频器:16位的预分频器TIMx_PSC对内部时钟CK_INT进行分频之后,得到计数器时钟CK_CNT=CK_PSC/PSC+1

计数器CNT在计数器时钟的驱动下开始计数,计数一次的时间为1/CK_CNT

计数器、自动重装载寄存器: 定时器使能(CEN 置 1)后,计数器 CNT在CK_CNT 驱动下向上计数,当 TIMx_CNT 值与 TIMx_ARR 的设定值相等时就自动生成事件并 TIMx_CNT 自动清零,然后自动重新开始计数,如此重复以上过程

影子寄存器: PSC和ARR都有影子寄存器,功能框图上有个影子
影子寄存器的存在起到一个缓冲的作用 ,用户值->寄存器->影子寄存器->起作用,如果不使用影子寄存器则用户值在写到寄存器之后则里面起作用
ARR影子,TIMx_CR1:APRE位控制

如何实现定时0.5s:

方法:先固定一个值再求另一个值,一般让频率分频后=10^nHz,然后计数器的值就比较容易看,比如让频率为10KHz

PSC = 8400-1,定时器频率=84M/(PSC+1)=10000HZ
ARR = 4999,从0计数到4999,则计了5000次
T = 5000 / 10000 = 0.5S

基本定时器中断实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//main.c
#include "stm32f4xx.h"
#include "timx.h"
#include "led.h"

int main()
{
LED_Init();
TIMX_Init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
while(1)
{

}
}

//timx.c
#include "timx.h"
#include "sys.h"
#include "led.h"
void TIMX_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitTypeDef_Structure;
NVIC_InitTypeDef NVIC_InitTypeDef_Structure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7,ENABLE);

//TIM_TimeBaseInitTypeDef_Structure.TIM_ClockDivision= //基本定时器没有外部时钟分频因子,不用设置
//TIM_TimeBaseInitTypeDef_Structure.TIM_CounterMode=TIM_CounterMode_Up;//基本定时器只能向上计数,默认就是向上,不用设置
TIM_TimeBaseInitTypeDef_Structure.TIM_Period=5000-1;//设置自动重装载值
TIM_TimeBaseInitTypeDef_Structure.TIM_Prescaler=8400-1;//设置预分频因子
//TIM_TimeBaseInitTypeDef_Structure.TIM_RepetitionCounter=//基本定时器没有重复计数器,只有高级定时器有,不用设置
TIM_TimeBaseInit(TIM7,&TIM_TimeBaseInitTypeDef_Structure);//初始化定时器

TIM_ITConfig(TIM7,TIM_IT_Update,ENABLE); //使能定时器中断
TIM_Cmd(TIM7,ENABLE); //使能定时器


NVIC_InitTypeDef_Structure.NVIC_IRQChannel=TIM7_IRQn; //设置中断源
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=0x01; //设置抢占优先级
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=0x03; //设置响应优先级
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitTypeDef_Structure);

}
void TIM7_IRQHandler(void)
{
if(TIM_GetITStatus(TIM7,TIM_IT_Update)==SET)
{
LED0=!LED0;
LED1=!LED1;
}
TIM_ClearITPendingBit(TIM7,TIM_IT_Update);
}

通用定时器原理

STM32F40x系列总共最多有14个定时器,12个16位,2个32位(只有通用定时器有)

三种定时器区别:

定时器种类 位数 计数器模式 产生DMA请求 捕获/比较通道 互补输出 特殊应用场景
高级定时器 (TIM1,TIM8) 16 向上,向下,向上/下 可以 4 带可编程死区的互补输出
通用定时器(TIM2,TIM5) 32 向上,向下,向上/下 可以 4 通用。定时计数,PWM输出,输入捕获,输出比较
通用定时器(TIM3,TIM4) 16 向上,向下,向上/下 可以 4 通用。定时计数,PWM输出,输入捕获,输出比较
通用定时器(TIM9~TIM14) 16 向上 没有 2 通用。定时计数,PWM输出,输入捕获,输出比较
基本定时器 (TIM6,TIM7) 16 向上,向下,向上/下 可以 0 主要应用于驱动DAC

通用定时器功能特点:

  • 16 /32 位向上、向下、向上/向下(中心对齐)计数模式,自动装载计数器(TIMx_CNT)
  • 16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数 为 1~65535 之间的任意数值
  • 4 个独立通道(TIMx_CH1~4),这些通道可以用来作为: 输入捕获 ,输出比较,PWM 生成(边缘或中间对齐模式) ,单脉冲模式输出
  • 可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外一个定时器)的同步电路

产生中断/DMA的条件:

  • 更新:计数器溢出,计数器初始化;
  • 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
  • 输入捕获
  • 输出比较
  • 支持针对定位的增量(正交)编码器和霍尔传感器电路
  • 触发输入作为外部时钟或者按周期的电流管理

通用定时器可以被用于: 测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)等

STM32 的每个通用定时器都是完全独立的,没有互相共享的任何资源;

计数器模式:向上计数,向下计数,中心对齐,图形表示

r5neij.png

定时器框图

r5uJnf.jpg

计数器时钟有8种选择

更新事件:将预载寄存器的内容写入影子寄存器(通过自动重载位是否被使能来决定),不使用影子寄存器则立即,使用则在每次更新事件发生时

产生更新事件的条件:

  • 当计数器上溢或者下溢时
  • 当循环计数器计数值为0时(TIM1)
  • 通过软件设置UG(Update Generation)位

更新事件的请求源可以从下面选择

  • URS=1 仅当计数器到达上溢/下溢时,将发出更新请求
  • URS=0 计数器的上溢/下溢、更新位的设置或从模式控制器产生的更新,将发出更新请求

通用定时器中断实验

计数器时钟可以由下列时钟源提供:

  • 内部时钟(CK_INT)
  • 外部时钟模式1:外部输入脚(TIx)
  • 外部时钟模式2:外部触发输入(ETR) (仅适用TIM2,3,4)
  • 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器

内部时钟选择: 除非APB1的分频系数是1,否则通用定时器的时钟等于APB1时钟的2倍;

计数器模式:通用定时器可以向上计数、向下计数、向上向下双向计数模式;

常用寄存器:

  • 计数器当前值寄存器CNT
  • 预分频寄存器TIMx_PSC
  • 自动重装载寄存器(TIMx_ARR)
  • 控制寄存器1(TIMx_CR1)
  • DMA中断使能寄存器(TIMx_DIER)

常用库函数:

1
2
3
4
5
6
7
8
9
10
//在stm32f4xx_tim.c/.h中

void TIM_TimeBaseInit(TIM_TypeDef* TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);//定时器参数初始化
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);//定时器使能函数
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);//定时器中断使能
//几个状态标志位获取和清除函数
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

定时器中断实现步骤:

1
2
3
4
5
RCC_APB1PeriphClockCmd();//1.使能定时器时钟
TIM_TimeBaseInit();//2.初始化定时器,配置ARR,PSC
NVIC_Init();//3.开启定时器中断,配置NVIC
TIM_Cmd();//4.使能定时器
TIMx_IRQHandler();//5.编写中断服务函数

实验代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//main.c
#include "stm32f4xx.h"
#include "timx.h"
#include "led.h"

int main()
{
LED_Init();
TIMX_Init();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
while(1)
{

}
}

//timx.c
#include "timx.h"
#include "sys.h"
#include "led.h"
void TIMX_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitTypeDef_Structure;
NVIC_InitTypeDef NVIC_InitTypeDef_Structure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);

//TIM_TimeBaseInitTypeDef_Structure.TIM_ClockDivision=
TIM_TimeBaseInitTypeDef_Structure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitTypeDef_Structure.TIM_Period=5000-1;
TIM_TimeBaseInitTypeDef_Structure.TIM_Prescaler=8400-1;
//TIM_TimeBaseInitTypeDef_Structure.TIM_RepetitionCounter=
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitTypeDef_Structure);

TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
TIM_Cmd(TIM3,ENABLE);


NVIC_InitTypeDef_Structure.NVIC_IRQChannel=TIM3_IRQn;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelPreemptionPriority=0x01;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelSubPriority=0x03;
NVIC_InitTypeDef_Structure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitTypeDef_Structure);

}
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET)
{
LED0=!LED0;
LED1=!LED1;
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}

高级定时器实验

PWM输出实验

PWM输出配置过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.使能定时器14和相关IO口时钟。
RCC_APB1PeriphClockCmd();//使能定时器14时钟
RCC_AHB1PeriphClockCmd ();//使能GPIOF时钟
//2.初始化IO口为复用功能输出
GPIO_Init();
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
//3.GPIOF复用映射到定时器
GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14);
//4.初始化定时器:ARR,PSC等:
TIM_TimeBaseInit();
//初始化输出比较参数
TIM_OC1Init();
//6.使能预装载寄存器
TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable);
//7.使能自动重装载的预装载寄存器允许位
TIM_ARRPreloadConfig(TIM14,ENABLE);
//8.使能定时器。
//9.不断改变比较值CCRx,达到不同的占空比效果
TIM_SetCompare1();

输入捕获实验

TFT LCD显示实验

几个常用的函数

1
2
3
4
5
6
7
void LCD_Init(void);//初始化LCD
void LCD_Clear(u16 color);//清屏填充色
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode);//显示字符
void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode);//显示数字
void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p);//显示字符串

POINT_COLOR=RED;//画笔颜色设置红色

实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"

int main(void)
{
u8 x=0;
u8 lcd_id[12];
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);

LED_Init();
LCD_Init();
POINT_COLOR=RED;//画笔颜色设置红色
sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id);
while(1)
{
switch(x)
{
case 0:LCD_Clear(WHITE);break;
case 1:LCD_Clear(BLACK);break;
case 2:LCD_Clear(BLUE);break;
case 3:LCD_Clear(RED);break;
case 4:LCD_Clear(MAGENTA);break;
case 5:LCD_Clear(GREEN);break;
case 6:LCD_Clear(CYAN);break;
case 7:LCD_Clear(YELLOW);break;
case 8:LCD_Clear(BRRED);break;
case 9:LCD_Clear(GRAY);break;
case 10:LCD_Clear(LGRAY);break;
case 11:LCD_Clear(BROWN);break;
}
POINT_COLOR=RED;
LCD_ShowString(30,40,210,24,24,"Explorer STM32F4");
LCD_ShowString(30,70,200,16,16,"TFTLCD TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,lcd_id);
LCD_ShowString(30,130,200,12,12,"2014/5/4");
x++;
if(x==12)x=0;
LED0=!LED0;
delay_ms(1000);
}
}

485通信实验

I2C实验

MPU6050实验

SPI实验

2.4G无线通信实验

OLED显示实验

几个常用的函数

1
2
3
4
void OLED_Init(void);//初始化OLED
void OLED_Refresh_Gram(void);//更新显存到LCD,每次更新显存数据就调用该函数来显示
void OLED_ShowNum(u8 x,u8 y,u32 num,u8 len,u8 size);//显示数字,x,y为起点坐标,num是要显示的数字,len表示数字的位数,size表示字体大小
void OLED_ShowString(u8 x,u8 y,const u8 *p,u8 size);//显示字符串,x,y为起点坐标,size表示字体大小,p是字符串首地址

实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "oled.h"

int main(void)
{
u8 t=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init(168);
uart_init(115200);
LED_Init();
OLED_Init();
OLED_ShowString(0,0,"ALIENTEK",24);
OLED_ShowString(0,24, "0.96' OLED TEST",16);
OLED_ShowString(0,40,"ATOM 2014/5/4",12);
OLED_ShowString(0,52,"ASCII:",12);
OLED_ShowString(64,52,"CODE:",12);
OLED_Refresh_Gram();
t=' ';
while(1)
{
OLED_ShowChar(36,52,t,12,1);
OLED_ShowNum(94,52,t,3,12);
OLED_Refresh_Gram();
t++;
if(t>'~')t=' ';
delay_ms(500);
LED0=!LED0;
}
}

CAN实验

硬件随机数发生器

STM32F4自带了硬件随机数发生器(RNG),RNG处理器是一个以连续模拟噪声为基础的随机数发生器,在主机读数时提供一个32位的随机数

两个连续的随机数的间隔为40个PLL48CLK时钟信号周期

通过监控RNG熵来标识异常行为;

可以禁止来降低功耗;

STM32F4的随机数发生器(RNG)采用模拟电路实现,此电路产生馈入线性反馈移位寄存器 (RNG_LFSR) 的种子,用于生成 32 位随机数;

该模拟电路由几个环形振荡器组成,振荡器的输出进行异或运算以产生种子,RNG_LFSR 由专用时钟 (PLL48CLK) 按恒定频率提供时钟信息,因此随机数质量与 HCLK 频率无关 。当将大量种子引入RNG_LFSR后,RNG_LFSR 的内容会传入数据寄存器 (RNG_DR)。

同时,系统会监视模拟种子和专用时钟 PLL48CLK,当种子上出现异常序列,或PLL48CLK时钟频率过低时,可以由RNG_SR寄存器的对应位读取到,如果设置了中断 ,则在检测到错误时,还可以产生中断

随机数发生器框图:

ry226g.png

RNG寄存器:

  1. 控制寄存器RNG_CR
  2. 状态寄存器RNG_SR
  3. 数据寄存器RNG_DR

RNG库函数(在stm32f4xx_rng.h/stm32f4xx_rng.c)

1
2
3
4
5
6
7
8
9
void RNG_DeInit(void);//复位
void RNG_Cmd(FunctionalState NewState);//使能RNG
uint32_t RNG_GetRandomNumber(void);//获取随机数

void RNG_ITConfig(FunctionalState NewState);
FlagStatus RNG_GetFlagStatus(uint8_t RNG_FLAG);
void RNG_ClearFlag(uint8_t RNG_FLAG);
ITStatus RNG_GetITStatus(uint8_t RNG_IT);
void RNG_ClearITPendingBit(uint8_t RNG_IT);

窗口看门狗实验

之所以称为窗口就是因为其喂狗时间是一个有上下限的范围内(窗口),可以通过设定相关寄存器,设定其上限时间(下限固定),喂狗的时间不能过早也不能过晚,而独立看门狗限制喂狗时间在0-x内,x由相关寄存器决定。喂狗的时间不能过晚;

STM32F的窗口看门狗中有一个7位的递减计数器T[6:0] ,它会在出现下述2种情况之一时产生看门狗复位:

  1. 当喂狗的时候如果计数器的值大于某一设定数值W[6:0]时,此设定数值在WWDG_CFR寄存器定义;
  2. 当计数器的数值从0x40减到0x3F时【T6位跳变到0】;

如果启动了看门狗并且允许中断,当递减计数器等于0x40时产生早期唤醒中断(EWI),它可以用于喂狗以避免WWDG复位;

窗口看门狗超时时间:

为什么要窗口看门狗?

对于一般的看门狗,程序可以在它产生复位前的任意时刻刷新看门狗,但这有一个隐患,有可能程序跑乱了又跑回到正常的地方,或跑乱的程序正好执行了刷新看门狗操作,这样的情况下一般的看门狗就检测不出来了;

如果使用窗口看门狗,程序员可以根据程序正常执行的时间设置刷新看门狗的一个时间窗口,保证不会提前刷新看门狗也不会滞后刷新看门狗,这样可以检测出程序没有按照正常的路径运行非正常地跳过了某些程序段的情况

窗口看门狗其他注意事项:

  1. 上窗口值W[6:0]必须大于下窗口值0x40。否则就无窗口了;
  2. 窗口看门狗时钟来源PCLK1(APB1总线时钟)分频后;

常用寄存器:

窗口看门狗配置过程:

1
2
3
4
5
6
7
8
RCC_APB1PeriphClockCmd();//1.使能看门狗时钟
WWDG_SetPrescaler();//2.设置分频系数
WWDG_SetWindowValue();//3.设置上窗口值
WWDG_EnableIT();//4.开启提前唤醒中断并分组(可选)
NVIC_Init();
WWDG_Enable();//5.使能看门狗
WWDG_SetCounter();//6.喂狗
WWDG_IRQHandler();//7.编写中断服务函数

独立看门狗实验

由于单片机的工作常常会受到来自外界电磁场的干扰,造成程序的跑飞,而陷入死循环,程序的正常运行被打断,由单片机控制的系统无法继续工作,会造成整个系统的陷入停滞状态,发生不可预料的后果,用于监测单片机程序运行状态的模块或者芯片 ,俗称“看门狗”(watchdog)

看门狗解决的问题: 在启动正常运行的时候,系统不能复位,在系统跑飞(程序异常执行)的情况,系统复位,程序重新执行;

STM32内置两个看门狗,两个看门狗设备(独立看门狗/窗口看门狗)可以用来检测和解决由软件错误引起的故障,当计数器达到给定的超时值时,触发一个
中断(仅适用窗口看门狗)或者产生系统复位;

独立看门狗(IWDG)由专用的低速时钟(LSI)驱动 ,即使主时钟发生故障它仍有效,独立看门狗适合应用于需要看门狗作为一个在主程序之外 能够完全独立工

作,并且对时间精度要求低的场合

窗口看门狗由从APB1时钟分频后得到时钟驱动 ,通过可配置的时间窗口来检测应用程序非正常的过迟或过早操作,窗口看门狗最适合那些要求看门狗在精确计时窗口起作用的程序

独立看门狗功能描述:在键值寄存器(IWDG_KR)中写入0xCCCC ,开始启用独立看门狗。此时计数器开始从其复位值0xFFF递减,当计数器值计数到尾值0x000时会产生一个复位信号(IWDG_RESET)
无论何时,只要在键值寄存器IWDG_KR中写入0xAAAA(通常说的喂狗) , 自动重装载寄存器IWDG_RLR的值就会重新加载到计数器,从而避免看门狗复位。
如果程序异常,就无法正常喂狗,从而系统复位;

相关寄存器:

  • 键值寄存器IWDG_KR: 0~15位有效,只写寄存器,读出值为0x0000,软件必须以一定时间间隔写入0xAAAA,否则,计数值为0时,看门狗会产生复位
  • 预分频寄存器IWDG_PR:0~2位有效,具有写保护功能,要操作先取消写保护,读值时要保证状态寄存器的PVU位为0,读值才有效
  • 重装载寄存器IWDG_RLR:0~11位有效,具有写保护功能,要操作先取消写保护,只有当状态寄存器的RVU位为0时,才能对寄存器进行修改,读出的值才有效
  • 状态寄存器IWDG_SR:0~1位有效,RVU为1表示重装载值的更新正在进行中,更新结束会由硬件清零,为0时才能修改重装载寄存器的值,PVU则控制预分频寄存器,和RVU逻辑一样;

独立看门狗超时时间:

IWDG独立看门狗操作库函数:

1
2
3
4
5
6
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess);//取消写保护:0x5555使能
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler);//设置预分频系数:写PR
void IWDG_SetReload(uint16_t Reload);//设置重装载值:写RLR
void IWDG_ReloadCounter(void);//喂狗:写0xAAAA到KR
void IWDG_Enable(void);//使能看门狗:写0xCCCC到KR
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);//状态:重装载/预分频 更新

独立看门狗操作步骤:

1
2
3
4
5
IWDG_WriteAccessCmd();//1.取消寄存器写保护
IWDG_SetPrescaler();//2.设置独立看门狗的预分频系数,确定时钟
IWDG_SetReload();//3.设置看门狗重装载值,确定溢出时间
IWDG_Enable();//4.使能看门狗
IWDG_ReloadCounter();//5.应用程序喂狗

待机唤醒实验

很多单片机有低功耗模式,STM32也不例外,在系统或者电源复位后,微控制器出于运行状态之下,HCLK为CPU提供时钟,内核执行代码,当CPU不需要继续运行时,可以利用多种低功耗模式来节省功耗,例如等待某个事件触发;

STM32的3种低功耗模式:

  1. 睡眠模式:内核停止,外设如NVIC,系统时钟Systick仍运行;
  2. 停止模式:所有时钟都已停止。1.8V内核电源工作,PLL,HIS和HSE RC振荡器功能禁止,寄存器和SRAM内容保留;
  3. 待机模式:1.8V内核电源关闭,只有备份寄存器和待机电路维持供电,寄存器和SRAM内容全部丢失。实现最低功耗;

STM32F4的3种低功耗模式:

rcIkDK.png

STM32F4的待机模式:

rcIhKx.png

待机模式理想状态下,只需要2.2uA电流。停机模式下典型电流为350uA;

对于使能了RTC闹钟中断或RTC周期性唤醒等中断的时候,进入待机模式前,必须按如下操作处理:

  1. 禁止RTC中断(ALRAIE、ALRBIE、WUTIE、TAMPIE和TSIE等);
  2. 清零对应中断标志位;
  3. 清除PWR唤醒(WUF)标志(通过设置PWR_CR的CWUF位实现);
  4. 重新使能RTC对应中断;
  5. 进入低功耗模式;
  6. 在有用到RTC相关中断的时候,必须按以上步骤执行之后,才可以进入待机模式,否则可能无法唤醒;

低功耗操作函数:

1
2
3
4
5
6
7
8
9
10
//文件:stm32f4xx_pwr.c  / stm32f4xx_pwr.h
void PWR_EnterSTOPMode();//进入停机模式
void PWR_EnterSTANDBYMode(void);//进入待机模式
void PWR_WakeUpPinCmd(FunctionalState NewState);//使能Wakeup引脚唤醒
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);

//文件: core_cm4.h
__WFI() ;
__WFE() ;

待机唤醒配置步骤:

1
2
3
4
5
6
7
//1.使能电源时钟,因为要配置电源控制寄存器,所以必须先使能电源时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
//2.RTC相关处理:关闭RTC相关中断
//3.设置WK_UP引脚作为唤醒源,设置PWR_CSR的EWUP位,使能WK_UP用于将CPU从待机模式唤醒。
PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能
//4.设置SLEEPDEEP位,设置PDDS位,执行WFI指令,进入待机模式
void PWR_EnterSTANDBYMode(void);