ISP程序下载流程:
STM32启动模式(M3、M4)
ST-Link程序下载
HAL库入门
固件库和寄存器的区别:
固件库就是函数的集合(API:接口函数),把寄存器操作封装起来;开发过程直接操作固件库函数;固件库函数的本质就是直接封装的寄存器操作,所以单独操作寄存器是可行的;
HAL库包和关键文件:
GPIO工作原理
GPIO入门知识:
GPIO:通用输入输出端口。可以做输入也可以做输出。GPIO端口可通过程序配置成输入或者输出。
引脚和GPIO的区别和联系:
STM32的引脚中,有部分是做GPIO使用,部分是电源引脚/复位引脚/启动模式引脚/晶振引脚/调试下载引脚。
STM32F429IGT6的GPIO资源分布:
①一共有9组IO: PA~PI
②其中PA~PH 每组16个IO,PI只有PI0~PI11
③一共有140个IO口:16*8+12=140
GPIO复用的意义:STM32的大部分引脚除了当GPIO使用外,还可以复用为外设功能引脚(比如串口)。一个引脚,可以作为IO口,同时也可以作为复用功能外设引脚。这部分知识我们会在后面讲解。以下主要讲解引脚做IO使用方面的知识。
复用原理:通过开关控制对引脚进行复用;达到用有限的引脚实现更多的功能;
具体的芯片外设资源展示:查看ST MCU最新选型手册
查看GPIO引脚功能:每个STM32芯片的芯片数据手册都会提供引脚功能描述
GPIO的8种工作模式:
4种输入模式:
输入浮空
输入上拉
输入下拉
模拟输入
4种输出模式(带上下拉):
开漏输出(带上拉或者下拉)
开漏复用功能(带上拉或者下拉)
推挽式输出(带上拉或者下拉)
推挽式复用功能(带上拉或者下拉)
4种最大输出速度:
-2MHZ 低速
-25MHz 中速
-50MHz 快速
-100MHz 高速
在F429参考手册描述可以直接找到速度值
GPIO的内部机构
输入模式
输出模式
开漏输出模式(带上拉或者下拉)
(P-MOS管高电平导通,低电平关闭,下方的N-MOS低电平导通,高电平关闭)
在开漏输出模式时,只有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口的电平不一定是输出的电平
推挽输出模式(带上拉或者下拉)
(P-MOS管高电平导通,低电平关闭,下方的N-MOS低电平导通,高电平关闭)
在推挽输出模式时,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口的电平一定是输出的电平
复用开漏输出(带上拉或者下拉)
GPIO复用为其他外设,输出数据寄存器GPIOx_ODR无效; 输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取I/O实际状态 除了输出信号的来源改变 其他与开漏输出功能相同
复用推挽输出(带上拉或者下拉)
GPIO复用为其他外设(如 I2C),输出数据寄存器GPIOx_ODR无效; 输出的高低电平的来源于其它外设,施密特触发器打开,输入可用,通过输入数据寄存器可获取I/O实际状态 除了输出信号的来源改变 其他与开漏输出功能相同
推挽输出:
可以输出强高低电平,连接数字器件
开漏输出:
只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极. 要得到高电平状态需要上拉电阻才行. 适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)
上电复位后,GPIO默认为输入浮空状态,部分特殊功能引脚为特定状态。复位后,调试引脚处于复用功能上拉/下拉状态:
PA15:JTDI处于上拉状态
PA14:JTCK/SWCLK处于下拉状态
PA13:JTMS/SWDAT处于下拉状态
PB4:NJTRST处于上拉状态
PB3:JTDO处于浮空状态
配置IO口的8种模式:(IO口的寄存器配置)
每组GPIO端口的寄存器包括:
重点说明:
每组IO口由10个寄存器组成,如果芯片有GPIOA~GPIOI 9个组,那么一共有对应9X10=90个寄存器。
如果配置一个IO口需要2个位,那么刚好32位寄存器配置一组IO口16个IO口
如果配置一个IO口只需要1个位,一般高16位保留
BSRR寄存器32位分为低16位BSRRL和高16位BSRRH,BSRRL配置一组IO口的16个IO口的置位状态(1),BSRRH配置复位状态(0)。
1.端口模式寄存器(GPIOx_MODER)(x=A……K)
该寄存器有32位,分成16组,每组对应2个位,刚好每组的IO口有16个,即每两个位控制一个IO口,两个位对应四种控制模式;
2.端口输出类型寄存器(GPIOx_OTYPER)(x=A……K)
该寄存器有32位,但是只用了其中的低16位,分成16组,每组对应一个位,一个位控制一个IO口,对应两种输出类型:
3.端口输出速度寄存器(GPIOx_OSPEEDR)(x=A……K)
该寄存器有32位,分成16组,每组对应2个位,刚好每组的IO口有16个,即每两个位控制一个IO口,两个位对应四种输出速度模式;
4种最大输出速度:
-2MHZ 低速
-25MHz 中速
-50MHz 快速
-100MHz 高速
4.端口上下拉寄存器(GPIOx_PUPDR)(x=A……K)
该寄存器有32位,分成16组,每组对应2个位,刚好每组的IO口有16个,即每两个位控制一个IO口,两个位对应四种端口上下拉模式;根据端口模式寄存器(GPIOx_MODER)和端口上下拉寄存器(GPIOx_PUPDR)(x=A……K)的复位设置可以看出端口在复位之后是处于浮空输入的状态;
5.端口输入数据寄存器(GPIOx_IDR)(x=A……K)
该寄存器有32位,低16位有效,用于存储输入数据的值:
6.端口输出数据寄存器(GPIOx_ODR)(x=A……K)
该寄存器有32位,低16位有效,每一个位控制对应IO口的输出电平:
6.端口置位/复位寄存器(GPIOx_BSRR) 位设置/清除寄存器
该寄存器有32位,分成两组,为低16位和高16位,低16位叫端口置位寄存器,高16位叫端口复位寄存器,低16位寄存器写入1时,对应的IO口输出1,写入0时,对IO口不产生影响,高16位寄存器则相反,写入1时,对应的IO口输出0,写入0时,对IO口不产生影响;
BSRR最终会映射到ODR寄存器上;
7.端口配置锁定寄存器(GPIOx_LCKR)(x=A……K)
该寄存器有32位,只有低17位有效,第17位(即16)为锁定键,该位为0,则端口配置锁定键未激活,该位为1,则端口配置锁定键已激活,而位0-15则是用于锁定对应的端口,端口锁定时,无法对端口进行操作:
8.复用功能寄存器(GPIOx_AFRL、AFRH)
该寄存器有32位,两个寄存器则有64位,一个IO口由两个寄存器进行控制,分为高位AFRH和低位AFRL,分别控制16个IO口,即4个位控制1个IO口,其中前者控制8-15的IO口,后者控制0-7的IO口:
跑马灯实验-寄存器笔记
寄存器的定义位于F429:stm32f429xx.h
typedef struct
{
__IO uint32_t MODER;
__IO uint32_t OTYPER;
__IO uint32_t OSPEEDR;
__IO uint32_t PUPDR;
__IO uint32_t IDR;
__IO uint32_t ODR;
__IO uint32_t BSRR;
__IO uint32_t LCKR;
__IO uint32_t AFR[2];
} GPIO_TypeDef;
端口结构体指针
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
操作方法
GPIOA->ODR 即可访问GPIOA端口的ODR输出寄存器。
跑马灯实验的硬件连接:
因为LED0灯后面连接着一个电阻和VCC3.3,故只有GPIOB0输出为低电平的时候才会产生压差,LED0灯才会亮,当GPIOB0输出为高电平时则不存在压差,所以LED0灯不会亮,即需要输出为0时,GPIO输出低电平,输出为1时,GPIO输出高电平,由于开漏输出不能直接输出高电平,需要外部电阻进行上下拉,而推挽输出可以直接输出高电平和低电平,故选择推挽输出;
而上拉、下拉电阻:控制引脚默认状态的电压,开启上拉的时候引脚默认电压为高电平,开启下拉的时候引脚默认电压为低电平,在推挽输出模式时,N-MOS管和P-MOS管都工作,如果我们控制输出为0,低电平,则P-MOS管关闭,N-MOS管导通,使输出低电平,I/O端口的电平就是低电平,若控制输出为1 高电平,则P-MOS管导通N-MOS管关闭,使输出高电平,I/O端口的电平就是高电平, 外部上拉和下拉的作用是控制在没有输出时IO口电平,故这里选择推挽输出上拉,确保没有输出信号时LED灯不亮;
配置寄存器操作IO口步骤:
1.初始化HAL库:HAL_Init();
2.初始化系统时钟
STM32F429: Stm32_Clock_Init(360,25,2,8);
STM32F767: Stm32_Clock_Init(432,25,2,9);
3.使能IO口时钟。配置IO口时钟使能寄存器: RCC->AHB1ENR
AHB1外设时钟使能寄存器(RCC_AHB1ENR)
4.初始化IO口模式。配置四个配置寄存器
GPIOx_MODER GPIOx_OTYPER
GPIOx_OSPEEDR GPIOx_PUPDR
对应的寄存器如上:
5.操作IO口,输出高低电平。
配置寄存器GPIOX_ODR或者GPIOx_BSRR 。对应的寄存器如上:
对应步骤的代码如下:
Int main(void)
{
HAL_Init(); //初始化HAL库:HAL_Init();
Stm32_Clock_Init(360,25,2,8); //初始化系统时钟
delay_init(180); //初始化延时函数,加入延时函数让肉眼可以看到LED灯高低电平的转换闪烁
RCC->AHB1ENR |= 1<<1; //使能IO口时钟。配置IO口时钟使能寄存器,将GPIOB的外设时钟使能
GPIOB->MODE = 0x00000005; //初始化IO口模式,配置GPIOB的引脚PB0和PB1模式为通用输出即寄存器的低四位值为0101,转换为十六进制则是0x00000005;
GPIOB->OTYPER= 0x00000000; //初始化IO口模式,配置GPIOB的引脚PB0和PB1端口输出模式为推挽输出即寄存器的低四位值为0000,转换为十六进制则是0x00000000;
GPIOB->OSPEEDR= 0x0000000F; //初始化IO口模式,配置GPIOB的引脚PB0和PB1输出速度为高速即寄存器的低四位值为1111,转换为十六进制则是0x0000000F;
GPIOB->PUPDR= 0x00000005; //初始化IO口模式,配置GPIOB的引脚PB0和PB1上下拉模式为上拉即寄存器的低四位值为0101,转换为十六进制则是0x00000005;
While(1)
{
GPIOB->BSRR = 0x00000003; //操作IO口,输出高低电平。,配置GPIOB的引脚PB0和PB1端口电平为高电平,配置寄存器GPIOx_BSRR的低2位设置为11,转换为十六进制则是0x00000003;故LED灯亮;
delay_ms(500);
GPIOB->BSRR = 0x00030000; //操作IO口,输出高低电平。配置GPIOB的引脚PB0和PB1端口电平为低电平,配置寄存器GPIOx_BSRR的高16/17位设置为11,转换为十六进制则是0x00030000;故LED灯不亮;
因为该寄存器的低16位寄存器写入1时,对应的IO口输出1,写入0时,对IO口不产生影响,高16位寄存器则相反,写入1时,对应的IO口输出0,写入0时,对IO口不产生影响;
delay_ms(500);
}
}
跑马灯实验-HAL库笔记
GPIO操作HAL库函数分布文件:
头文件:stm32f4xx_gpio.h stm32f7xx_gpio.h
源文件:stm32f4xx_gpio.c stm32f7xx_gpio.c
GPIO库函数介绍:
1个初始化函数:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
1个读取输入电平函数:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
1个设置输出电平函数:
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
1个电平翻转函数:
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
1个引脚电平锁定函数:
HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
2个外部中断相关函数:(后续补上)
初始化函数:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);
作用:初始化一个或者多个IO口(同一组)的工作模式,输出类型,速度以及上下拉方式。也就是一组IO口的4个配置寄存器。
(GPIOx->MODER, GPIOx->OSPEEDR,GPIOx->OTYPER,GPIOx->PUPDR)
初始化样例:
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_0|GPIO_PIN_1; //PB1,0
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
可以一次初始化一个IO组下的多个IO,前提是这些IO口的配置方式一样。
读取输入电平函数:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
作用:读取某个GPIO的输入电平。实际操作的是GPIOx_IDR寄存器。
读取输入样例:GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5);//读取GPIOA.5的输入电平
设置引脚输出电平函数:
void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin,GPIO_PinState PinState);
设置引脚输出样例:HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET); //设置GPIOB1的输出电平为0,GPIO_PIN_RESET为0,GPIO_PIN_SET为1;
输出电平翻转函数:
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
输出电平翻转样例:HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_1);//翻转GPIOA1的输出电平
配置HAl库操作IO口步骤:
初始化HAL库:HAL_Init();
初始化系统时钟 :Stm32_Clock_Init( );
使能IO口时钟。
操作寄存器:配置IO口时钟使能寄存器: RCC->AHB1ENR
HAL库方法:__HAL_RCC_GPIOB_CLK_ENABLE();
初始化IO口模式。
操作寄存器:GPIOx_MODER OTYPER OSPEEDR PUPDR
HAL库方法:HAL_GPIO_Init();
操作IO口,输出高低电平。
操作寄存器:配置寄存器GPIOX_ODR或者GPIOx_BSRR 。
HAL库方法:HAL_GPIO_WritePin();
对应步骤的代码如下:
Int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO_InitStructure结构体变量
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180MHz
Delay_init(180);
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能PB时钟
GPIO_InitStructure.Mode=GPIO_MODE_OUTPUT_PP;//推挽输出
GPIO_InitStructure.Pin=GPIO_PIN_0 | GPIO_PIN_1; //第0号引脚和第1号引脚
GPIO_InitStructure.Pull=GPIO_PULLUP; //上拉
GPIO_InitStructure.Speed=GPIO_SPEED_FREQ_VERY_HIGH;//设置为高速速度
HAL_GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化GPIOB的结构体
While(1)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PPIN_0,GPIO_PIN_RESET); //PB0=0
HAL_GPIO_WritePin(GPIOB,GPIO_PPIN_1,GPIO_PIN_RESET); //PB0=0
Delay_ms(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PPIN_0,GPIO_PIN_SET); //PB0=1
HAL_GPIO_WritePin(GPIOB,GPIO_PPIN_1,GPIO_PIN_SET); //PB0=0
Delay_ms(500);
}
}
跑马灯实验-位操作笔记
位操作基本原理
位带操作:将每个比特膨胀成为一个32的位的字,当访问这些字的时候就达到了访问比特的目的,比如:BSRR寄存器有32个位,那么可以映射到32个地址上,故我们去访问(读-写-改)这32个地址就达到了访问32个比特的目的;
支持位操作的区域:其中一个是 SRAM 区的最低 1MB 范围,0x20000000‐0x200FFFFF(SRAM 区中的最低 1MB),第二个则是片内外设区的最低 1MB范围,0x40000000‐0x400FFFFF(片上外设区中最低 1MB)
使用位带操作和不使用位带操作的区别:
左边是不使用位带功能的操作流程,需要进行读-改-写三步操作;而右边是使用了位带功能的操作流程,直接在膨胀后的地址写入1,即可直接映射到对应的寄存器,只需要一步就可以完成;
位带操作的优越性:
位带操作的映射关系:
位带区:支持位带操作的地址区
位带别名:对别名地址的访问最终作用到位带区的访问上(注意:这中间有一个地址映射过程)
具体的映射关系集成在sys.h文件上:
sys.h里面对GPIO输入输出部分功能实现了位带操作,如下:
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
…
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
…
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
手把手写跑马灯实验-位带操作步骤:
使能IO口时钟。__HAL_RCC_GPIOX_CLK_ENABLE();
初始化IO口模式。 HAL_GPIO_Init();
操作IO口,输出高低电平。使用位带操作。
对应步骤的代码如下:
Int main(void)
{
GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO_InitStructure结构体变量
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180MHz
Delay_init(180);
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能PB时钟
GPIO_InitStructure.Mode=GPIO_MODE_OUTPUT_PP;//推挽输出
GPIO_InitStructure.Pin=GPIO_PIN_0 | GPIO_PIN_1; //第0号引脚和第1号引脚
GPIO_InitStructure.Pull=GPIO_PULLUP; //上拉
GPIO_InitStructure.Speed=GPIO_SPEED_FREQ_VERY_HIGH;//设置为高速速度
HAL_GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化GPIOB的结构体
While(1)
{ //使用位带操作,用PBout(0)=1直接映射到寄存器
PBout(0)=1;
PBout(1)=1;
Delay_ms(500);
PBout(0)=0;
PBout(1)=0;
Delay_ms(500);
}
}
按键输入实验-GPIO做输入的使用
按键实验硬件连接
KEY0->PH3 上拉输入 KEY1->PH2 上拉输入 KEY2->PC13 上拉输入
因为这三个按键的下端连接的是GND,故应选择上拉输入模式;
WK_UP->PA0 下拉输入(唤醒引脚)
因为这个按键的上端连接的是VCC3.3,故应选择下拉输入模式;
GPIO输入操作说明
读取IO口输入电平调用库函数为:
GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
读取IO口输入电平操作寄存器为:
GPIOx_IDR:端口输入寄存器
使用位带操作读取IO口输入电平方法:
PEin(4) -读取GPIOE.4口电平
PEin(n) -读取GPIOE.n口电平
按键实验步骤:
1.使能按键对应IO口时钟:
__HAL_RCC_GPIOx_CLK_ENABLE;
2.初始化IO模式:上拉/下拉输入。
void HAL_GPIO_Init();
3.扫描IO口电平(库函数/寄存器/位操作):
HAL库函数:GPIO_PinState HAL_GPIO_ReadPin();
寄存器: GPIOx_IDR
位操作: PHin(1);
4.编写按键扫描逻辑
连续按下模式:按下为0,没按下为1,对按键的输入值进行检测,当检测到低电平时即代表按键按下了;如果按着不松开,则根据检测速度进行累加,比如:1s检测10次,那么按下不松开持续10s,则等同于按下了10次,会连续执行10次按键操作;
不连按模式:按下为0,没按下为1,对按键的输入值进行检测,当检测到低电平时进行前后判断,只对第一次检测的电平变化取有效,对后续无电平变化的按键输入值取无效;即现在按下+之前没按为有效,现在按下+之前按下为无效;
对应步骤代码如下:
void KEY_Init(void) //所需使用GPIO口的模式设置和初始化
{
GPIO_InitTypeDef GPIO_Initure; //定义一个控制GPIO的结构体
__HAL_RCC_GPIOA_CLK_ENABLE(); //开启GPIOA时钟
__HAL_RCC_GPIOC_CLK_ENABLE(); //开启GPIOC时钟
__HAL_RCC_GPIOH_CLK_ENABLE(); //开启GPIOH时钟
GPIO_Initure.Pin=GPIO_PIN_0; //PA0
GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入
GPIO_Initure.Pull=GPIO_PULLDOWN; //下拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure); //使能A组
GPIO_Initure.Pin=GPIO_PIN_13; //PC13
GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOC,&GPIO_Initure); //使能C组
GPIO_Initure.Pin=GPIO_PIN_2|GPIO_PIN_3; //PH2,3
GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOH,&GPIO_Initure); //使能H组
}
按键扫描思路:支持连按与不支持连按:
连续按下模式:按下为0,没按下为1,对按键的输入值进行检测,当检测到低电平时即代表按键按下了;如果按着不松开,则根据检测速度进行累加,比如:1s检测10次,那么按下不松开持续10s,则等同于按下了10次,会连续执行10次按键操作;
不连按模式:按下为0,没按下为1,对按键的输入值进行检测,当检测到低电平时进行前后判断,只对第一次检测的电平变化取有效,对后续无电平变化的按键输入值取无效;即现在按下+之前没按为有效,现在按下+之前按下为无效;
按键扫描(支持连按)思路:
u8 KEY_Scan(void)
{
if(检测到KEY按下)
{
delay_ms(10);//延时10-20ms,防抖。
if(KEY确实按下)
{
return KEY_Value;//返回按键的键值
}
return 无效值;
}
}
代码弊端:将其放在while(1)循环里面进行执行的话,只要按键是处于按下的情况,无论之前是否松开,都视为按下一次按键,如果我要实现:按键按下,没有松开,只能算按下一次,这个函数无法实现。
修改知识补充:static关键字
int getValue(void)
{
int flag=0;
flag++;
return flag;
} //flag值会被重新定义为0;
int getValue(void)
{
static int flag=0; //只会初始化一次,相当于全局变量
flag++;
return flag;
} //flag值会被保存在内存单元中,不被释放,不会被重新初始化为0;而是随着代码运行叠加
修改后的代码:
按键扫描(不支持连按)思路:
u8 KEY_Scan(void) //不支持连续按:就是说,按键按下了,没有松开,只能算一次。
{
static u8 key_up=1; //定义一个静态内存单元用于保存按键信息,key_up代表按键松开,为了进入下方的if判断,故将按键松开的标志定义为真,即1;
if(key_up && KEY按下) //当按键松开和按键按下时,进入判断
{
delay_ms(10) ; //延时,防抖
key_up=0; //标记这次key已经按下,没有松开
if(KEY确实按下) //按键按下了,返回按键值
{
return KEY_VALUE;
}
}
else if(KEY没有按下) key_up=1; //按键没有检测到按下,将按键标记为松开状态;
}
将二者进行融合得:
按键信息处理代码如下:
u8 KEY_Scan(u8 mode) //设置一个变量来进行模式的切换,默认为0,即不支持连按
{
static u8 key_up=1; //按键松开标志
if(mode==1)key_up=1; //判断模式,是否切换成支持连按
if(key_up&&(KEY0==0||KEY1==0||KEY2==0||WK_UP==1))
{
delay_ms(10);
key_up=0; //标记这次key已经按下,没有松开
if(KEY0==0) return KEY0_PRES; //按下不同的按键,返回不同的键值
else if(KEY1==0) return KEY1_PRES; //按下不同的按键,返回不同的键值
else if(KEY2==0) return KEY2_PRES; //按下不同的按键,返回不同的键值
else if(WK_UP==1) return WKUP_PRES; //按下不同的按键,返回不同的键值
}else if(KEY0==1&&KEY1==1&&KEY2==1&&WK_UP==0)
key_up=1;//如果没有检测到按下,将key_up的值重新设为一,标记为松开状态,确保代码循环重新检测时可以进入二次判断;
return 0; //无按键按下
}
对应的main.c文件调用代码如下:
int main(void)
{
u8 key;
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init(); //初始化LED
KEY_Init(); //初始化按键
while(1)
{
key=KEY_Scan(0); //按键扫描,选择不支持连按模式
switch(key) //判断返回的键值
{
case WKUP_PRES: //控制LED0,LED1互斥点亮
LED1=!LED1;
LED0=!LED1;
break;
case KEY2_PRES: //控制LED0翻转
LED0=!LED0;
break;
case KEY1_PRES: //控制LED1翻转
LED1=!LED1;
break;
case KEY0_PRES: //同时控制LED0,LED1翻转
LED0=!LED0;
LED1=!LED1;
break;
}
delay_ms(10);
}
}
STM32时钟系统精讲
时钟系统框图讲解:
LSI可以作为独立看门狗和RTC时钟源
LSE可以作为RTC时钟源
HSI可以作为系统时钟源和PLL时钟源(经过分频后倍频),还可以输出时钟信号
HSE可以作为系统时钟源和RTC时钟源(经过分频后),还作为PLL时钟源(经过分频后倍频)
PLL在经过分频后可以作为48MHz的外设时钟电源
PLL可以作为外设时钟(经过分频后)
1. STM32 有5个时钟源:HSI、HSE、LSI、LSE、PLL。
①、HSI是高速内部时钟,RC振荡器,频率为16MHz,精度不高。可以直接作为系统时钟或者用作PLL时钟输入。
②、HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHz。
③、LSI是低速内部时钟,RC振荡器,频率为32kHz,提供低功耗时钟。主要供独立看门狗和自动唤醒单元使用。
④、LSE是低速外部时钟,接频率为32.768kHz的石英晶体。RTC
⑤、PLL为锁相环倍频输出。
PLL为锁相环倍频输出。
STM32F4有三个PLL:
主PLL(PLL)由HSE或者HSI提供时钟信号,并具有两个不同的输出时钟。
①第一个输出PLLP用于生成高速的系统时钟(最高180MHz)
②第二个输出PLLQ为48M时钟,用于USB OTG FS时钟,随机数发生器的时钟和SDIO时钟。
第一个专用PLL(PLLI2S)生成精确时钟,在I2S和SAI1上实现高品质音频
N是用于PLLI2S vco的倍频系数,其取值范围是:192~432;
R是I2S时钟的分频系数,其取值范围是:2~7;
Q是SAI时钟分频系数,其取值范围是:2~15;P没用到。
第二个专用PLL(PLLSAI)同样用于生成精确时钟,用于SAI1输入时钟,同时还为LCD_TFT接口提供精确时钟。
N是用于PLLSAI vco的倍频系数,其取值范围是:192~432;
Q是SAI时钟分频系数,其取值范围是:2~15;
R是LTDC时钟的分频系数,其取值范围是:2~7;P没用到。
主PLL时钟计算:PLLCLK= HSE * N / (M*P)
2.系统时钟SYSCLK可来源于三个时钟源:HSI振荡器时钟、HSE振荡器时钟、PLL时钟,系统时钟的作用主要如下图所示:
3.STM32F4时钟信号输出MCO1(PA8)和MCO2(PC9)。
MCO1:用户可以配置预分频器(1~5)向MCO1引脚PA8输出4个不同的时钟源:HIS、LSE、HSE、PLL
MCO2:用户可以配置预分频器(1~5)向MCO2引脚PC9输出4个不同的时钟源:HSE、PLL、SYSCLK、PLLI2S
MCO最大输出时钟不超过100MHz
4.任何一个外设在使用之前,必须首先使能其相应的时钟。
RCC时钟控制相关寄存器定义在stm32f429xx.h中。结构体:RCC_TypeDef;
RCC时钟相关定义和函数在文件
stm32f4xx_hal_rcc.h stm32f4xx_hal_rcc.c
Stm32_Clock_Init时钟系统初始化函数剖析
系统初始化函数:SystemInit();使用HAL库函数的时候,在系统启动之后会自动调用:
时钟系统配置一般步骤:
1.使能PWR时钟:调用函数__HAL_RCC_PWR_CLK_ENABLE()。
2.设置调压器输出电压级别:调用函数__HAL_PWR_VOLTAGESCALING_CONFIG()。
配置调压器输出级别:__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); 。
PWR_CR寄存器来配置:
3.选择是否开启Over-Driver功能:调用函数HAL_PWREx_EnableOverDrive()。
开启Over-Driver功能:
HAL_StatusTypeDef HAL_PWREx_EnableOverDrive(void);
等待周期:FLASH等待周期表(STM32F429)
4.配置时钟源相关参数:调用函数HAL_RCC_OscConfig()。
时钟源配置函数:HAL_RCC_OscConfig
__weak HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef*RCC_OscInitStruct)
HAL_RCC_OscConfig函数配置方法:
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开HSE
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL时钟源选择HSE
RCC_OscInitStructure.PLL.PLLM=pllm;//主PLL和音频PLL分频系数(PLL之前的分频)
RCC_OscInitStructure.PLL.PLLN=plln; //主PLL倍频系数(PLL倍频)
RCC_OscInitStructure.PLL.PLLP=pllp; //系统时钟的主PLL分频系数(PLL之后的分频)
RCC_OscInitStructure.PLL.PLLQ=pllq; //USB/SDIO/随机数产生器等的主PLL分频系数
HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
5.配置系统时钟源以及AHB,APB1和APB2的分频系数:调用函数HAL_RCC_ClockConfig()。
函数HAL_RCC_ClockConfig配置实例:
时钟配置函数:HAL_RCC_ClockConfig
HAL_StatusTypeDef HAL_RCC_ClockConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency)
typedef struct
{
uint32_t ClockType;
uint32_t SYSCLKSource;
uint32_t AHBCLKDivider;
uint32_t APB1CLKDivider;
uint32_t APB2CLKDivider;
}RCC_ClkInitTypeDef;
//选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|
RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//系统时钟源
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB分频系数为1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4; //APB1分频系数为4
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2; //APB2分频系数为2
(&RCC_ClkInitStructure,FLASH_LATENCY_5);//同时设置FLASH延时周期为5WS,也就是6个CPU周期。
以上步骤所对应的代码如下:
//时钟系统配置函数
//Fvco=Fs*(plln/pllm);
//SYSCLK=Fvco/pllp=Fs*(plln/(pllm*pllp));
//Fusb=Fvco/pllq=Fs*(plln/(pllm*pllq));
//Fvco:VCO频率
//SYSCLK:系统时钟频率
//Fusb:USB,SDIO,RNG等的时钟频率
//Fs:PLL输入时钟频率,可以是HSI,HSE等.
//plln:主PLL倍频系数(PLL倍频),取值范围:64~432.
//pllm:主PLL和音频PLL分频系数(PLL之前的分频),取值范围:2~63.
//pllp:系统时钟的主PLL分频系数(PLL之后的分频),取值范围:2,4,6,8.(仅限这4个值!)
//pllq:USB/SDIO/随机数产生器等的主PLL分频系数(PLL之后的分频),取值范围:2~15.
//外部晶振为25M的时候,推荐值:plln=360,pllm=25,pllp=2,pllq=8.
//得到:Fvco=25*(360/25)=360Mhz
// SYSCLK=360/2=180Mhz
// Fusb=360/8=45Mhz
//返回值:0,成功;1,失败
void Stm32_Clock_Init(u32 plln,u32 pllm,u32 pllp,u32 pllq) //时钟系统配置函数
{
HAL_StatusTypeDef ret = HAL_OK;
RCC_OscInitTypeDef RCC_OscInitStructure;//系统时钟设置
RCC_ClkInitTypeDef RCC_ClkInitStructure; //外设时钟设置
__HAL_RCC_PWR_CLK_ENABLE(); //使能PWR时钟
//下面这个设置用来设置调压器输出电压级别,以便在器件未以最大频率工作
//时使性能与功耗实现平衡,此功能只有STM32F42xx和STM32F43xx器件有,
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);//设置调压器输出电压级别1
RCC_OscInitStructure.OscillatorType=RCC_OSCILLATORTYPE_HSE; //时钟源为HSE
RCC_OscInitStructure.HSEState=RCC_HSE_ON; //打开HSE
RCC_OscInitStructure.PLL.PLLState=RCC_PLL_ON; //打开PLL
RCC_OscInitStructure.PLL.PLLSource=RCC_PLLSOURCE_HSE; //PLL时钟源选择HSE
RCC_OscInitStructure.PLL.PLLM=pllm; //主PLL和音频PLL分频系数(PLL之前的分频),取值范围:2~63.
RCC_OscInitStructure.PLL.PLLN=plln; //主PLL倍频系数(PLL倍频),取值范围:64~432.
RCC_OscInitStructure.PLL.PLLP=pllp; //系统时钟的主PLL分频系数(PLL之后的分频),取值范围:2,4,6,8.(仅限这4个值!)
RCC_OscInitStructure.PLL.PLLQ=pllq; //USB/SDIO/随机数产生器等的主PLL分频系数(PLL之后的分频),取值范围:2~15.
ret=HAL_RCC_OscConfig(&RCC_OscInitStructure);//初始化
if(ret!=HAL_OK) while(1);
ret=HAL_PWREx_EnableOverDrive(); //开启Over-Driver功能
if(ret!=HAL_OK) while(1);
//选中PLL作为系统时钟源并且配置HCLK,PCLK1和PCLK2
RCC_ClkInitStructure.ClockType=(RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStructure.SYSCLKSource=RCC_SYSCLKSOURCE_PLLCLK;//设置系统时钟时钟源为PLL
RCC_ClkInitStructure.AHBCLKDivider=RCC_SYSCLK_DIV1;//AHB分频系数为1
RCC_ClkInitStructure.APB1CLKDivider=RCC_HCLK_DIV4; //APB1分频系数为4
RCC_ClkInitStructure.APB2CLKDivider=RCC_HCLK_DIV2; //APB2分频系数为2
ret=HAL_RCC_ClockConfig(&RCC_ClkInitStructure,FLASH_LATENCY_5);//同时设置FLASH延时周期为5WS,也就是6个CPU周期。
if(ret!=HAL_OK) while(1);
}
SysTick定时器:
1.Systick定时器基础知识讲解
Systick定时器,是一个简单的定时器,对于ST的CM3,CM4,CM7内核芯片,都有Systick定时器。
Systick定时器常用来做延时,或者实时系统的心跳时钟。这样可以节省MCU资源,不用浪费一个定时器。比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟。
Systick定时器就是系统滴答定时器,一个24 位的倒计数定时器,计到0 时,将从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作。
SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15)。
Systick中断的优先级也可以设置。
2.Systick相关寄存器库函数讲解
4个Systick寄存器
CTRL SysTick 控制和状态寄存器
对于STM32,外部时钟源是 HCLK(AHB总线时钟)的1/8,内核时钟是 HCLK时钟;配置函数:HAL_SYSTICK_CLKSourceConfig();
LOAD SysTick 自动重装载除值寄存器
VAL SysTick 当前值寄存器
CALIB SysTick 校准值寄存器
3.delay延时函数讲解(Systick应用)
HAL库中的Systick相关函数:
stm32f4xx_hal_cortex.h/stm32f7xx_hal_cortex.h文件中:
HAL_SYSTICK_CLKSourceConfig () ; //Systick时钟源选择
core_cm7.h/core_cm4.h文件中:
SysTick_Config (uint32_t ticks) //初始化systick,时钟为HCLK,并开启中断
Systick中断服务函数:
void SysTick_Handler (void);
系统中是采用轮询的方式来进行delay延时;
//延时nus
//nus:要延时的us数.
//nus:0~190887435(最大值即2^32/fac_us@fac_us=22.5)
void delay_us(u32 nus)
{
u32 ticks;
u32 told,tnow,tcnt=0;
u32 reload=SysTick->LOAD; //LOAD的值
ticks=nus*fac_us; //需要的节拍数
delay_osschedlock(); //阻止OS调度,防止打断us延时
told=SysTick->VAL; //刚进入时的计数器值
while(1)
{
tnow=SysTick->VAL;
if(tnow!=told)
{
if(tnow<told)tcnt+=told-tnow; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks)break; //时间超过/等于要延迟的时间,则退出.
}
};
delay_osschedunlock(); //恢复OS调度
}
以上主要是我自己的学习笔记总结,主要是为了后面方便查询,如果其中有涉及知识点错误的,可以联系我,方便进行改正,其中有涉及到正点原子的相关课件以及一部分其他博主的文章思路和照片等,如有侵权,麻烦联系我,可做删除和隐藏处理;谢谢!