0. 引入
单片机的最小系统:
芯片(CPU + 总线 + 外设控制器) + 晶振电路 + 复位电路 + 供电电路
一个完整的系统:最小系统 + 其它的外设
芯片:
整个系统的核心,相当于人类的大脑,会提供引脚与外部电路相连
芯片四周那些银白色的引脚是从芯片内部引申出来的。它负责芯片内部的控制单元与外部硬件的连接
那么一个引脚其实本质上就是一根"电线"
引脚拥有输入功能 / 输出功能,比如:1. 引脚可以输出 / 输入一个电平信号(1/0)
那么电平信号是针对于CPU来说的,因为CPU只能识别二进制0/1(数字信号)
2. 引脚可以输入/输出一个高低电压
因为对于外部电路来说,外部电路的工作是采用模拟电压信号(即多少V电压)
为什么一个引脚对CPU和外部的电路的输入/输出是不一样的信号?它是怎么做到的?
这是因为引脚在芯片内部还需要通过"控制单元"才能够进入CPU,CPU只能识别二进制0/1(数字信号)
控制单元:
不同硬件对应不同的控制单元:
GPIO --->GPIO控制器
USART --->USART控制器
......
在单片机产品中,我们常常可以见到三种模块:LED灯、KEY按键、BEEP蜂鸣器
LED灯:一个比较常见的LED电路
LED1 ------ 通过控制 LED0 引脚(电线)
给它一个低电平(低电压),LCD1 灯就会亮
给它应该高电平(高电压),LED1 灯就会灭
1 ——> 高电平
0 ——> 低电平
电流:从电势高的地方流向电势低的地方
CPU ===> 往 LED0 引脚去 写1,写0"output" 输出功能
KEY按键:一个比较常见的KEY电路
KEY0 ------ 通过读取 KEY0 引脚的电平状态来知晓用户是否按下按键高电平(1) ---> 弹起
低电平(0) ---> 按下
CPU ===> 读取KEY0引脚的电平状态"input" 输入功能
BEEP蜂鸣器:一个比较常见的BEEP电路
BEEP ----------- 通过控制BEEP引脚(电线)
给它一个高电平(高电压),BEEP就会响
给它一个低电平(低电压),BEEP就不响
CPU ===> 往BEEP引脚去 写1,写0
"output" 输出功能
两种三极管:PNP and NPN ===> P指向N
这些引脚最终是接入到MCU的某个引脚(GPIO)上去的控制LED灯、KEY按键、BEEP蜂鸣器等,可以在MCU上面写程序去控制这些引脚
1. GPIO到底是什么?
GPIO:General Purpose Input Output 通用功能的输入输出 线
GPIO就是从芯片内部引出一根功能复用的口线("电线"),可以由CPU配置成不同的功能
如:输入功能,输出功能,复用功能,模拟模式
根据数据手册中列出的每个 I/O 端口的特性,可通过软件将通用 I/O(GPIO)端口的各个端口位分别配置为多种模式如:输入浮空、输入上拉、输入下拉、模拟功能、具有上拉或下拉功能的开漏输出、具有上拉或下拉功能的推挽输出、具有上拉或下拉功能的复用功能推挽、具有上拉或下拉功能的复用功能开漏
芯片或CPU控制整个世界就是通过这样的引脚(口线,GPIO)STM32F4xx共有144个GPIO口线(引脚,pin),分为9组,记为GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,GPIOF,GPIOG,GPIOH,GPIOI. 每组管理16个GPIO引脚,编号从0~15
如:GPIOA这一组有16个引脚,分别记为GPIOA0,GPIOA1,GPIOA2,... GPIOA15
其他组类似GPIOA0 -----> PA0
GPIOB3 ------> PB3
......
这些GPIO引脚都是功能复用的,并且由GPIO控制器来控制它们的
如果我们要使用这个GPIO,那么就必须先去配置它的寄存器组
所有的外设都是由"外设控制器"来控制,外设是相当于CPU而言的(不是芯片)
GPIO控制器由不同的寄存器来配置或控制它们(GPIOx)
可以通过<STM32F4xx中文参考手册>第2章可以查看 边界地址 外设 总线 0x4002 2000 - 0x4002 23FF GPIOI 0x4002 1C00 - 0x4002 1FFF GPIOH 0x4002 1800 - 0x4002 1BFF GPIOG 0x4002 1400 - 0x4002 17FF GPIOF 0x4002 1000 - 0x4002 13FF GPIOE AHB1 0x4002 0C00 - 0x4002 0FFF GPIOD 0x4002 0800 - 0x4002 0BFF GPIOC 0x4002 0400 - 0x4002 07FF GPIOB 0x4002 0000 - 0x4002 03FF GPIOA 上述的表有几个名字需要解释: 边界地址:指寄存器组的起始地址(基址)和结束地址 外设:该寄存器组对应的硬件控制器 总线:该硬件控制器挂载的时钟线
2. STM32F4xx GPIO内部结构原理
每个GPIO内部都可以配置成:
1. 输入功能:input mode
CPU可以获取该GPIO口的外部输入的一个电平状态
输入功能有四种模式:
(1) 输入悬空(input floating):不接上拉和下拉电阻
输入引脚处于浮空状态,即没有特定电压状态,引脚悬浮在空中
IO引脚的电平状态完全是外部输入所决定的,这时CPU能够通过读取数据的操作知道状态
(2) 带上拉输入(input pull-up):内部接上拉电阻
该引脚被设置为上拉输入时,引脚悬空的状态下,CPU读取到的电平状态为高电平,因为内部有一个上拉电阻;唯有当被外部输入信号下拉时,CPU读取到的电平才为低电平
(3) 带下拉输入(input pull-down): 内部接下拉电阻
该引脚被设置为下拉输入时,引脚悬空的状态下,CPU读取到的电平状态为低电平。唯有当被外部输入信号上拉时,CPU读取到的电平状态才为高电平
(4) 模拟输入(Input Analog)
该引脚被设置为模拟输入时,能够获取外部的模拟信号,通过芯片内的ADC转换为数字量,如变化的电压值
2. 输出功能:output modeCPU可以往该GPIO口输出一个电平状态
输入功能有两种模式:
(1) 输出推挽(Push-Pull):可以输出高、低电平
可以往外部引脚输出一个高电平(1)或低电平(0)
1:MOS管上方导通,下方不导通,在此处数字量变成模拟量,输出高电平
0:MOS管下方导通,上方不导通,在此处数字量变成模拟量,输出低电平
(2) 输出开漏(Open-Drain):不输出电压
低电平接地,高电平不接地(悬空状态)
如果外部电路接上拉电阻,则在CPU输出1时会接到外部上拉电阻的电源电压上
0:ping 接地
1:ping 悬空 此时需要外部电路中设计上拉电阻
3. 复用功能:Alternate Function
复用功能是指GPIO口用作其它的功能口线,如: I2C,UART,SPI 等
每个GPIO口都可以配置成多达16种复用功能,记为: AF0,AF1,AF2 ... AF15,具体哪个GPIO口可以配置成哪种复用功能,需要看原理图
其实在芯片设计的,每个GPIO口能够复用的功能,就已经定了
UART0 ---> TX0 是不是任意一个GPIO口都可以复用这个Tx0这个引脚呢?
肯定不是啦,这个在芯片设计的时候,就已经定了
STM32F4xx每个GPIO口内部都有一个上拉/下拉电阻,你可以enable/disable它,根据具体应用场景需要
每个GPIO口的操作都是通过GPIO的寄存器来控制的
3. STM32F4xx GPIO寄存器说明
CPU是通过地址总线根据相应地址访问设备的,我们将 GPIO 看做设备,GPIO 内部寄存器看做设备中的空间,也就是说,如果我们需要操作相应寄存器相当于对 GPIO 设备内部的空间进行操作
关于设备地址,参考 <STM32F4xx中文参考手册.pdf> 第二章第三节 存储器映像
每个通用 I/O 端口包括:4 个 32 位配置寄存器
(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR)
2 个 32 位数据寄存器(GPIOx_IDR 和 GPIOx_ODR)1 个 32 位置位/复位寄存器(GPIOx_BSRR)
1 个 32 位锁定寄存器(GPIOx_LCKR)
2 个 32 位复用功能选择寄存器(GPIOx_AFRL 和 GPIOx_AFRH)
可通过字节(8位)、半字(16位) 或 字(32位) 对 GPIO 寄存器进行访问(1) GPIOx_MODER
模式选择寄存器,该寄存器的地址偏移为 0x00
地址偏移:每一组GPIO寄存器都有一个地址范围如:GPIOA : 0x4002 0000 ~ 0x4002 03FF
GPIOA这一组的寄存器的基地址是0x4002 0000
因此, GPIOA_MODER 的地址就是:0x4002 0000 + 0x00
模式寄存器用来配置GPIO的功能的(input / output / analog(模拟) / AF)该寄存器用来控制x(x=A,B,C...,I)组的16个GPIO引脚的模式,每个GPIO口占2bits(总共有四种模式,四种功能)
编号为y(y=0,1,2...,15)的GPIO引脚在该寄存器的bit位为GPIOx_MODER[2y+1:2y]
具体配置如下:
GPIOx_MODER[2y+1:2y] 模式
00 输入模式
01 通用输出模式
10 复用功能,Alternate Function
11 模拟输入
例子:用C代码把PF9配置为输出模式 分析: PF组的基址:0x4002 1400 模式寄存器的偏移地址:0x00 所以GPIOF_MODER地址:0x4002 1400 + 0x00 如果要把PF9配置为输出模式,就需要将GPIOF_MODER[19:18] --> 01 把地址为0x40021400的寄存器中bit19清0,bit18置1 在STM32中表示地址用 unsigned long 来表示地址(32位:long(4字节),64位:long(8字节)) unsigned long *p = (unsigned long *)0x40021400; 但是一般情况下我们会在地址前加一个volatile,如下: volatile unsigned long *p = (volatile unsigned long *)0x40021400; volatile的作用是作为指令关键字,禁止编译器优化,访问的就是实际地址,不需要优化 bit19清0,bit18置1: *p = *p & ~(1<<19); *p = *p | (1<<18); 但是如上的操作实际上对寄存器进行了两次操作,效率比较低,能不能对寄存器一次性修改到位? 可以 比如: unsigned long r = *p; r &= (~(1 << 19)); r |= (1 << 18); *p = r; // 通过中间变量,一步到位
(2) GPIOx_OTYPER
输出类型寄存器,该寄存器的地址偏移为 0x04
该寄存器用来控制x(x=A,B,C,...,I)分组的16个GPIO的输出类型,每个GPIO占1bit编号为y(y=0,1,2,...,15)的GPIO在该寄存器的bit位置为GPIOx_OTYPER[y]
具体输出类型如下:GPIOx_OTYPER[y] 输出类型
0 输出推挽(Push-Pull)
1 输出开漏(Open-Drain)
注:
输出推挽(Push-Pull),不带上下拉电阻
cpu写1 ----> (外部引脚)高电平
0 -----> (外部引脚)低电平
输出开漏(Open-Drain),不带上下拉电阻cpu写0 ----> (外部引脚)接地
1 ----> (外部引脚)悬空
(3) GPIOx_OSPEEDR端口输出速度寄存器,偏移地址:0x08
该寄存器用来控制x(x=A,B,C,...,I)分组的16个GPIO的输出速度,每个GPIO口2bits编号为y(y=0,1,2,...,15)的GPIO在该寄存器中的bit位置为GPIOx_OSPEEDRGPIO
[2y+1:2y]
具体输出速度如下:
GPIOx_OSPEEDRGPIO[2y+1:2y] 输出速度
00 2Mhz (低速)
01 25Mhz (中速)
10 50Mhz (快速)
11 30pF 时为 100Mhz(高速)
15pF 时为 80Mhz输出(最大速度)
(4) GPIOx_PUPDR
pu: pull up 上拉
pd: pull down 下拉
端口上拉/下拉寄存器,地址偏移为:0x0C
该寄存器用来控制x(x=A,B,C,..,I)分组的16个GPIO内部上下拉电阻的选择,每个GPIO口2bits
编号为y(y=0,1,2..., 15)的GPIO口在该寄存器中的bit 位置为 GPIOx_PUPDR[2y+1:2y]
具体选择情况如下:
GPIOx_PUPDR[2y+1:2y] 上下拉情况
00 无上拉也无下拉 disable pull-up disable pull_down
01 上拉 enable pull-up
10 下拉 enable pull-down
11 保留,没用到
(5) GPIOx_IDRInput Data Register 输入数据寄存器
端口输入数据寄存器,偏移地址为: 0x10,复位值: 0x0000 XXXX (开机后高16位值为0,低16位的值不确定)
该寄存器用来表示x(x=A,B,C,...,I)分组的16GPIO引脚输入的值,其中31:16保留
高16bits,必须保持复位值。bit 15:0 表示相应的GPIO引脚的输入电平的状态。这些bit,只能为只读,只能在字模式下访问比如:CPU想要知道GPIOA7是高电平还是低电平 if (GPIOA_IDR & (1 << 7)) { PA7是高电平 } else { PA7是低电平 }
(6) GPIOx_ODR
Output Data Register 输出数据寄存器端口输出数据寄存器,偏移地址为:0x14,复位值为: 0x0000 0000
该寄存器用来表示x(x=A,B,C,...,I)分组的16个GPIO的输出值,其中31:16保留。 低位15:0 表示相应的GPIO口线的输出状态可以读取也可以写入
(7) GPIOx_BSRRBSR: Bit Set Reset 位置位复位操作,该寄存器允许对GPIO寄存器进行原子读 / 修改操作
Set ----> 把相应的bit位置1
Reset -----> 把相应的bit位置0
端口置位 / 复位寄存器,偏移地址:0x18,复位值为:0x0000 0000
位 31:16 BRx: Bit Reset 端口的复位bit位,这些位为只写形式,只能在字、半字、或字节模式下访问往相应的bit位写:
1: 对相应的ODR(x-16)位进行复位(0)
0: 不会对相应的ODR(x-16)执行任何操作
位 15:0 BSx: Bit Set 端口x置位,这些位为只写形式,只能在字、半字、或字节模式下访问
往相应的bit位写:1: 对相应的ODRx位进行置位(1)
0: 不会对相应的ODRx位进行操作
注意:如果同时对BSx和BRx置位,则BSx的优先级更高
(8) GPIOx_LCKR
端口配置锁定寄存器,偏移地址为:0x1C,复位值为:0x0000 0000
LOCK:
当一个GPIO引脚的各配置寄存器设定好后,为了防止程序对这些已经配置好的寄存器的一些误操作,可以 LOCK一下
如果是一个LOCK的状态,该GPIO引脚的配置就不能再修改了
如果要修改就必须 UNLOCK
位 31:17保留,必须保持复位值
位 16 LCKK[16]:锁定键,可随时读取此位
![]()
(9) GPIOx_AFRL
AFR:Alternate Function Register 复用功能寄存器
GPIO 复用功能低位寄存器,偏移量 0x20
(10) GPIOx_AFRL
GPIO 复用功能低位寄存器,偏移量 0x24
每一个GPIO口可以复用多达16种复用功能,每一个GPIO口就需要4bits,一组GPIO口
(16个GPIO口),就需要 16x4= 64bits
所以复用功能寄存器就需要两个32bits的寄存器 GPIOx_AFRL,GPIOx_AFRH
编号 0...7 -----> GPIOx_AFRL(x = A...I)
编号 8...15 -----> GPIOx_AFRH(x = A...I)
0000:AF00001:AF1
0010:AF2
......
1111:AF15
AFx到底是复用何种功能,得看芯片手册
volatile:
在C语言中,volatile是一个关键字,告诉编译器该变量值容易发生改变,在编译、读取、存储该变量的时候都不要做任何优化,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取存储数据,不做优化,因为它可能会被程序之外的因素改变
我们首先需要知道,什么是编译器优化:int a; void main() { a = 1; } /* 在之前的内容中我们已经了解到,对于a=1的底层操作是这样的: 1. CPU 将 1 存入到 CPU内部寄存器中,比如说R0 2. CPU 将寄存器内容存入到 &a 对应的空间中 */ int a,b; void main() { a = 1; b = a; } /* 程序在执行的时候, 对于a=1这条语句: 3. CPU将 1 存入到 CPU内部寄存器中,比如说R0 4. CPU将寄存器内容存入到 &a 对应的空间中 对于b=a这条语句: 5. 会先把a这个内存地址的值(也就是1)取出来先存到寄存器里 6. 然后再把寄存器里的值存储到变量b的内存地址里 */ /* 上面的逻辑中有没有问题呢? 注意对比 4 5 步骤,CPU明明已经存储了 a 的内容,为什么还要加载一遍, 直接存储到变量b的空间中不就行了吗? 所以,你都能想到的问题,设计编译器的技术大佬会没有想过吗。所以编译 器编译时会基于一些规律/规则对代码进行优化。比如说:一般访问CPU内部寄存器要 比访问内存(RAM)的效率高 */
也就是说,在上述的例子经过编译器代码优化以后,为了执行效率更高,执行这段程序的流程就会被优化
最终程序执行可能就直接把寄存器的值赋值给变量b这个内存地址了,而不是重新从变量a的内存地址里读取到寄存器,这样效率就提高了编译器优化原则之一:减少对内存访问的次数,因为从内存里读写数据效率比较低也就是说,遇到这个关键字 volatile 声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问;如果不使用valatile,则编译器将对所声明的语句进行优化
4. STM32F4xx GPIO时钟使能
在STM32中,外围设备的寄存器在上电的情况下默认是没有时钟的,不给时钟的情况下操作外设是无效的,外设也不会工作,这样的目的是降低功耗。 所以在操作外设之前必须要先使能它的时钟,这就需要我们用RCC来完成时钟的使能
参考<STM32F4xx中文参考手册.pdf> 2.3 存储器映射可知:
GPIO所有的分组全部属于AHB1时钟线时钟的相关配置 ===> RCC (Reset Clock Control 复位时钟控制)
参考<STM32F4xx中文参考手册.pdf> 2.3 存储器映射 可知:
那么我们现在的目的就是找到有关AHB1外设使能的寄存器,参考<STM32F4xx中文参考手册.pdf> 6.3.12 RCC AHB1 外设时钟使能寄存器(RCC_AHB1RSTR)可知:
此寄存器[8:0]分别控制了GPIOx的时钟使能
1 使能时钟
0 禁止时钟
比如:
把RCC_AHB1ENR[5] ---> 1
使能了GPIOF组的时钟
总结:利用寄存器实现GPIO功能的配置的步骤 1) 从原理图找出对应的引脚 比如:点灯 LED1 ----- LED0 ----- PF9 经过分析:CPU对PF9输出一个低电平,D1就会亮 2) 配置 GPIO 分组时钟 3) 配置 GPIO 模式寄存器 4) 配置 GPIO 输出类型 5) 配置 GPIO 输出速率寄存器 6) 配置 GPIO 上下拉寄存器 7) 输出模式,对输出数据寄存器进行操作,输入模式,读取输入数据寄存器
5. 寄存器点灯
led_reg.h
#ifndef __LED_REG_H__ #define __LED_REG_H__ /* LED1 PF9 LED2 PF10 LED3 PE13 LED4 PE14 */ // AHB1 时钟总线寄存器 #define rRCC_AHB1ENR *((volatile unsigned long *)0x40023830) // GPIO 每组的基址 #define GPIOA_BASE 0x40020000 #define GPIOB_BASE 0x40020400 #define GPIOC_BASE 0x40020800 #define GPIOD_BASE 0x40020C00 #define GPIOE_BASE 0x40021000 #define GPIOF_BASE 0x40021400 #define GPIOG_BASE 0x40021800 #define GPIOH_BASE 0x40021C00 #define GPIOI_BASE 0x40022000 // GPIOF相关的寄存器的地址 // 模式选择寄存器,偏移地址:0x00 #define rGPIOF_MODER *((volatile unsigned long *)(GPIOF_BASE + 0x00)) // 输出类型寄存器,偏移地址:0x04 #define rGPIOF_OTYPER *((volatile unsigned long *)(GPIOF_BASE + 0x04)) // 端口输出速度寄存器,偏移地址:0x08 #define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE + 0x08)) // 端口上拉/下拉寄存器,地址偏移为:0x0C #define rGPIOF_PUPDR *((volatile unsigned long *)(GPIOF_BASE + 0x0C)) // 输入数据寄存器,地址偏移为:0x10 #define rGPIOF_IDR *((volatile unsigned long *)(GPIOF_BASE + 0x10)) // 输出数据寄存器,地址偏移为:0x14 #define rGPIOF_ODR *((volatile unsigned long *)(GPIOF_BASE + 0x14)) // 端口置位/复位寄存器,偏移地址:0x18 #define rGPIOF_BSRR *((volatile unsigned long *)(GPIOF_BASE + 0x18)) // LED1灯 亮 void LED1_ON(void); // LED1灯 灭 void LED1_OFF(void); void LED1_Init(void); #endif
led_reg.c
#include "led_reg.h" // LED1灯 亮 void LED1_ON(void) { rGPIOF_BSRR |= 1 << 15; } // LED1灯 灭 void LED1_OFF(void) { unsigned long r = rGPIOF_BSRR; r &= ~(1 << 15); r |= 1 << 9; rGPIOF_BSRR = r; } void LED1_Init(void) { unsigned long r = 0; // 使能时钟 rRCC_AHB1ENR |= (1 << 5); // 模式选择寄存器 ---> 输出模式[19:18] 01 r = rGPIOF_MODER; r &= ~(1 << 19); r |= (1 << 18); rGPIOF_MODER = r; // 输出类型寄存器 ---> 推挽输出 0 rGPIOF_OTYPER &= ~(1 << 9); // 端口输出速度寄存器 ---> [19:18] 11 r = rGPIOF_OSPEEDR; r |= (1 << 19); r |= (1 << 18); rGPIOF_OSPEEDR = r; // 端口上拉/下拉寄存器 --->无上下拉 [19:18] 00 r = rGPIOF_PUPDR; r &= ~(1 << 19); r &= ~(1 << 18); rGPIOF_PUPDR = r; LED1_ON(); }
6. 使用stm32官方固件库来操作GPIO口
stm32固件库移植 -----> 具体移植操作参考文档
(1) 配置AHB1总线上的外设时钟 RCC_AHB1PeriphClockCmd ----> rcc.h
// 等同于RCC_AHB1ENR寄存器 void RCC_AHB1PeriphClockCmd(uint32_t RCC_AHB1Periph, FunctionalState NewState); @RCC_AHB1Periph:指定AHB1总线外设,也就是要配置时钟的外设 可以是以下任意一个宏: RCC_AHB1Periph_GPIOA RCC_AHB1Periph_GPIOB ... RCC_AHB1Periph_GPIOI @NewState:指定该外设的时钟状态 ENBALE 使能,为该外设提供时钟信号 DISBALE 禁止,不提供 ------------------------------------------------------------------ 比如:使能GPIOF时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);
(2) 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) GPIO_init ----> gpio.h
void GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_InitStruct); @GPIOx:指定要初始化的GPIO分组 GPIOA GPIOB ... GPIOI @GPIO_InitStruct:指向GPIO初始化信息结构体 该结构体原型如下所示:(已经定义在头文件中,直接使用就好) typedef struct { uint32_t GPIO_Pin; GPIOMode_TypeDef GPIO_Mode; GPIOSpeed_TypeDef GPIO_Speed; GPIOOType_TypeDef GPIO_OType; GPIOPuPd_TypeDef GPIO_PuPd; } GPIO_InitTypeDef; @GPIO_Pin:指定要配置的GPIO引脚(可以位或多个,表示配置同一组的多个引脚为相 同模式,如:GPIO_Pin_0 | GPIO_Pin_1) GPIO_Pin_0 GPIO_Pin_1 ... GPIO_Pin_15 @GPIO_Mode:指定要配置的GPIO引脚的功能模式 GPIO_Mode_IN 输入模式 GPIO_Mode_OUT 输出模式 GPIO_Mode_AF 复用功能模式 GPIO_Mode_AN 模拟模式 @GPIO_Speed:指定引脚速率 GPIO_Speed_2MHz 2M低速 GPIO_Speed_25MHz 25M中速 GPIO_Speed_50MHz 50M快速 GPIO_Speed_100MHz 100M高速 @GPIO_OType:指定输出类型 GPIO_OType_PP 输出推挽 GPIO_OType_OD 输出开漏 @GPIO_PuPd:指定上下拉选择 GPIO_PuPd_NOPULL 无上拉,也无下拉 GPIO_PuPd_UP 上拉 GPIO_PuPd_DOWN 下拉 --------------------------------------------------------------- 比如:配置PF9为带下拉的推挽输出模式 // 定义GPIO初始化信息结构体 GPIO_InitTypeDef g; // 根据配置需要,对结构体成员赋值 g.GPIO_Pin = GPIO_Pin_9; // 9号引脚 g.GPIO_Mode = GPIO_Mode_OUT; // 输出模式 g.GPIO_Speed = GPIO_Speed_50MHz; // 50MHz g.GPIO_OType = GPIO_OType_PP; // 输出推挽 g.GPIO_PuPd = GPIO_PuPd_DOWN; // 下拉 // 根据结构体信息,完成GPIO配置 GPIO_Init(GPIOF, &g);
(3) 输入:从配置好的GPIO引脚,获取外部电平
a. 获取输入寄存器的指定的GPIO引脚的值 uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); @GPIOx:指定GPIO分组 GPIOA GPIOB GPIOC ... GPIOI @GPIO_Pin:指定GPIO引脚编号 GPIO_Pin_0 GPIO_Pin_1 ... GPIO_Pin_15 返回值: 返回指定GPIO引脚的电平状态(1个引脚的状态) Bit_SET 表示该GPIO引脚输入为高电平 ===> 1 Bit_RESET 表示该GPIO引脚输入为低电平 ===> 0 -------------------------------------------------------------------------------- b. 获取指定分组的输入寄存器整组的值(16位) uint16_t GPIO_ReadInputData(GPIO_TypeDef *GPIOx); @GPIOx:指定GPIO分组 GPIOA GPIOB ... GPIOI 返回值: 返回获取到的GPIO分组的输入数据寄存器中数据 要注意的是该返回值是uint16_t类型,具有16bits,分别对应该GPIO分组的16个GPIO引脚的输入 电平状态 bit0 ---> 0号引脚 ... bit15 ---> 15号引脚 -------------------------------------------------------------------------------- c. 获取输出数据寄存器的指定的GPIO引脚的值 uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); -------------------------------------------------------------------------------- d. 获取指定分组的输出寄存器整组的值(16位) uint16_t GPIO_ReadOutputData(GPIO_TypeDef *GPIOx);
(4) 输出:CPU通过引脚向外部电路输出一个电平值
a. 向指定的引脚输出指定的电平值 void GPIO_WriteBit(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, BitAction BitVal); @GPIOx:指定GPIO分组 GPIOA GPIOB GPIOC ... GPIOI @GPIO_Pin:指定GPIO引脚编号 GPIO_Pin_0 GPIO_Pin_1 ... GPIO_Pin_15 @BitVal:想输出的电平 Bit_RESET 低电平 ===> 0 Bit_SET 高电平 ===> 1 ------------------------------------------------------------------------------ b. 往指定的GPIO分组整组输出(16个引脚一块输出) void GPIO_Write(GPIO_TypeDef *GPIOx, uint16_t PortVal); @GPIOx:指定GPIO分组 GPIOA GPIOB GPIOC ... GPIOI @PortVal:uint16_t类型(16位的整数) bit0 ---> 引脚0 bit1 ---> 引脚1 bit2 ---> 引脚2 ... bit15 ---> 引脚15 比如: GPIO_Write(GPIOF, 0xF0F0); // 0xF0F0:1111 0000 1111 0000 将PF15~PF12和PF7~PF4输出高电平,其它的输出低电平 ------------------------------------------------------------------------------ c. 用来将指定的GPIO引脚输出为高电平(GPIO_BSRR:位置位复位寄存器) void GPIO_SetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); ------------------------------------------------------------------------------ d. 用来将指定的GPIO引脚输出低电平(GPIO_BSRR:位置位复位寄存器) void GPIO_ResetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); ------------------------------------------------------------------------------ e. 将指定引脚的输出状态进行翻转(1--->0,0--->1) void GPIO_ToggleBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
(5) 当GPIO引脚被配置为复用模式的时候才需要使用
void GPIO_PinAFConfig(GPIO_TypeDef *GPIOx, uint16_t GPIO_PinSource, uint8_t GPIO_AF); @GPIOx:指定的GPIO分组 GPIOA GPIOB ..... GPIOI @GPIO_PinSource:指定GPIO引脚(不可以位或) GPIO_PinSource0 GPIO_PinSource1 ..... GPIO_PinSource15 @GPIO_AF:指定复用成什么功能 GPIO_AF_TIM1 ......
7. 固件库操作GPIO
利用按键去控制灯,蜂鸣器
第一次按下 KYE1 按键的时候,LED1 和 LED2 亮,第二次按下 KEY1 按键的时候,LED1和LED2灭
KEY2 ---> LED3 和 LED4
KEY3 ---> BEEP
消抖:
抖动的原因:单片机上的按键大部分都是机械弹性按键,这种按键在按下或者弹起的时候发生抖动,对实验造成影响
解决方法:
1. 硬件消抖
在按键的电路上并联一个电容,利用电容的充放电特性对毛刺进行平滑出来,有效果,但是现在基本不会用这种方法
增加了成本
2. 软件消抖
增加一个小延时跳过抖动的时间,大部分的按键延时 10ms 左右就可以跳过抖动了
led.h#ifndef __LED_H__ #define __LED_H__ #include "stm32f4xx.h" /* LED1:PF9 LED2:PF10 LED3:PE13 LED4:PE14 */ // 哪组寄存器(LED灯引脚所在的GPIO分组) #define LED1_GPIO GPIOF #define LED2_GPIO GPIOF #define LED3_GPIO GPIOE #define LED4_GPIO GPIOE // 哪个引脚(LED灯引脚) #define LED1_Pin GPIO_Pin_9 #define LED2_Pin GPIO_Pin_10 #define LED3_Pin GPIO_Pin_13 #define LED4_Pin GPIO_Pin_14 // LED灯需要配置的时钟外设 #define LED1_RCC_AHB1Periph RCC_AHB1Periph_GPIOF #define LED2_RCC_AHB1Periph RCC_AHB1Periph_GPIOF #define LED3_RCC_AHB1Periph RCC_AHB1Periph_GPIOE #define LED4_RCC_AHB1Periph RCC_AHB1Periph_GPIOE // 使能时钟 #define LED1_Clock() RCC_AHB1PeriphClockCmd(LED1_RCC_AHB1Periph, ENABLE) #define LED2_Clock() RCC_AHB1PeriphClockCmd(LED2_RCC_AHB1Periph, ENABLE) #define LED3_Clock() RCC_AHB1PeriphClockCmd(LED3_RCC_AHB1Periph, ENABLE) #define LED4_Clock() RCC_AHB1PeriphClockCmd(LED4_RCC_AHB1Periph, ENABLE) // 开 #define LED1_ON() GPIO_ResetBits(LED1_GPIO, LED1_Pin) // 关 #define LED1_OFF() GPIO_SetBits(LED1_GPIO, LED1_Pin) #define LED2_ON() GPIO_ResetBits(LED2_GPIO, LED2_Pin) #define LED2_OFF() GPIO_SetBits(LED2_GPIO, LED2_Pin) #define LED3_ON() GPIO_ResetBits(LED3_GPIO, LED3_Pin) #define LED3_OFF() GPIO_SetBits(LED3_GPIO, LED3_Pin) #define LED4_ON() GPIO_ResetBits(LED4_GPIO, LED4_Pin) #define LED4_OFF() GPIO_SetBits(LED4_GPIO, LED4_Pin) // 将LED灯的输出状态进行翻转(亮--->灭,灭--->亮) #define LED1_reversal_status() GPIO_ToggleBits(LED1_GPIO, LED1_Pin) #define LED2_reversal_status() GPIO_ToggleBits(LED2_GPIO, LED2_Pin) #define LED3_reversal_status() GPIO_ToggleBits(LED3_GPIO, LED3_Pin) #define LED4_reversal_status() GPIO_ToggleBits(LED4_GPIO, LED4_Pin) /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @LEDx_GPIO:指定要初始化的GPIO分组 LED1_GPIO LED2_GPIO LED3_GPIO LED4_GPIO @LEDx_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) LED1_Pin LED2_Pin LED3_Pin LED4_Pin */ void LED_Init(GPIO_TypeDef *LEDx_GPIO, uint16_t LEDx_Pin); #endif
led.c
#include "led.h" /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @LEDx_GPIO:指定要初始化的GPIO分组 LED1_GPIO LED2_GPIO LED3_GPIO LED4_GPIO @LEDx_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) LED1_Pin LED2_Pin LED3_Pin LED4_Pin */ void LED_Init(GPIO_TypeDef *LEDx_GPIO, uint16_t LEDx_Pin) { // 1.定义GPIO初始化信息结构体 GPIO_InitTypeDef g; // 2.根据配置需要,对结构体成员赋值 g.GPIO_Pin = LEDx_Pin; // 几号引脚 g.GPIO_Mode = GPIO_Mode_OUT; // 输出模式 g.GPIO_Speed = GPIO_Speed_2MHz; // 2MHz g.GPIO_OType = GPIO_OType_PP; // 输出类型 g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉也无下拉 // 3.根据结构体信息,完成GPIO配置 GPIO_Init(LEDx_GPIO, &g); }
key.h
#ifndef __KEY_H__ #define __KEY_H__ #include "stm32f4xx.h" /* KEY1:PA0 KEY2:PE2 KEY3:PE3 KEY3:PE4 */ // 哪组寄存器(KEY按键引脚所在的GPIO分组) #define KEY1_GPIO GPIOA #define KEY2_GPIO GPIOE #define KEY3_GPIO GPIOE #define KEY4_GPIO GPIOE // 哪个引脚(KEY按键引脚) #define KEY1_Pin GPIO_Pin_0 #define KEY2_Pin GPIO_Pin_2 #define KEY3_Pin GPIO_Pin_3 #define KEY4_Pin GPIO_Pin_4 // KEY按键需要配置的时钟外设 #define KEY1_RCC_AHB1Periph RCC_AHB1Periph_GPIOA #define KEY2_RCC_AHB1Periph RCC_AHB1Periph_GPIOE #define KEY3_RCC_AHB1Periph RCC_AHB1Periph_GPIOE #define KEY4_RCC_AHB1Periph RCC_AHB1Periph_GPIOE // 使能时钟 #define KEY1_Clock() RCC_AHB1PeriphClockCmd(KEY1_RCC_AHB1Periph, ENABLE) #define KEY2_Clock() RCC_AHB1PeriphClockCmd(KEY2_RCC_AHB1Periph, ENABLE) #define KEY3_Clock() RCC_AHB1PeriphClockCmd(KEY3_RCC_AHB1Periph, ENABLE) #define KEY4_Clock() RCC_AHB1PeriphClockCmd(KEY4_RCC_AHB1Periph, ENABLE) /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @KEYx_GPIO:指定要初始化的GPIO分组 KEY1_GPIO KEY2_GPIO KEY3_GPIO KEY4_GPIO @KEYx_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) KEY1_Pin KEY2_Pin KEY3_Pin KEY4_Pin */ void KEY_Init(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin); /* 获取输入寄存器的指定的GPIO引脚的值 @KEYx_GPIO:指定要初始化的GPIO分组 KEY1_GPIO KEY2_GPIO KEY3_GPIO KEY4_GPIO @KEYx_Pin:指定要配置的GPIO引脚 KEY1_Pin KEY2_Pin KEY3_Pin KEY4_Pin 返回值: 高电平(1) ---> 弹起 低电平(0) ---> 按下 */ int KEY_ReadInputData(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin); #endif
key.c
#include "key.h" /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @KEYx_GPIO:指定要初始化的GPIO分组 KEY1_GPIO KEY2_GPIO KEY3_GPIO KEY4_GPIO @KEYx_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) KEY1_Pin KEY2_Pin KEY3_Pin KEY4_Pin */ void KEY_Init(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin) { // 1.定义GPIO初始化信息结构体 GPIO_InitTypeDef g; // 2.根据配置需要,对结构体成员赋值 g.GPIO_Pin = KEYx_Pin; // 几号引脚 g.GPIO_Mode = GPIO_Mode_IN; // 输入模式 g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉也无下拉 // 3.根据结构体信息,完成GPIO配置 GPIO_Init(KEYx_GPIO, &g); } /* 获取输入寄存器的指定的GPIO引脚的值 @KEYx_GPIO:指定要初始化的GPIO分组 KEY1_GPIO KEY2_GPIO KEY3_GPIO KEY4_GPIO @KEYx_Pin:指定要配置的GPIO引脚 KEY1_Pin KEY2_Pin KEY3_Pin KEY4_Pin 返回值: 高电平(1) ---> 弹起 低电平(0) ---> 按下 */ int KEY_ReadInputData(GPIO_TypeDef *KEYx_GPIO, uint16_t KEYx_Pin) { if (GPIO_ReadInputDataBit(KEYx_GPIO, KEYx_Pin) == Bit_SET) { return 1; } return 0; }
beep.h
#ifndef __BEEP_H__ #define __BEEP_H__ #include "stm32f4xx.h" /* BEEP:PF8 */ // 哪组寄存器(BEEP蜂鸣器引脚所在的GPIO分组) #define BEEP_GPIO GPIOF // 哪个引脚(BEEP蜂鸣器引脚) #define BEEP_Pin GPIO_Pin_8 // BEEP需要配置的时钟外设 #define BEEP_RCC_AHB1Periph RCC_AHB1Periph_GPIOF // 使能时钟 #define BEEP_Clock() RCC_AHB1PeriphClockCmd(BEEP_RCC_AHB1Periph, ENABLE) // 响 #define BEEP_ON() GPIO_SetBits(BEEP_GPIO, BEEP_Pin) // 不响 #define BEEP_OFF() GPIO_ResetBits(BEEP_GPIO, BEEP_Pin) // 将BEEP蜂鸣器的输出状态进行翻转(响--->不响,不响--->响) #define BEEP_reversal_status() GPIO_ToggleBits(BEEP_GPIO, BEEP_Pin) /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @BEEPx_GPIO:指定要初始化的GPIO分组 BEEP_GPIO @BEEP_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) BEEP_Pin */ void BEEP_Init(GPIO_TypeDef *BEEPx_GPIO, uint16_t BEEPx_Pin); #endif
beep.c
#include "beep.h" /* 初始化配置GPIO(完成GPIO的4个配置寄存器的赋值) @BEEPx_GPIO:指定要初始化的GPIO分组 BEEP_GPIO @BEEP_Pin:指定要配置的GPIO引脚 (可以位或多个,表示配置同一组的多个引脚为相同模式) BEEP_Pin */ void BEEP_Init(GPIO_TypeDef *BEEPx_GPIO, uint16_t BEEPx_Pin) { // 1.定义GPIO初始化信息结构体 GPIO_InitTypeDef g; // 2.根据配置需要,对结构体成员赋值 g.GPIO_Pin = BEEPx_Pin; // 指定要配置的GPIO引脚 g.GPIO_Mode = GPIO_Mode_OUT; // 指定要配置的GPIO引脚的功能模式 g.GPIO_Speed = GPIO_Speed_2MHz; // 指定引脚速率 g.GPIO_OType = GPIO_OType_PP; // 指定输出类型 g.GPIO_PuPd = GPIO_PuPd_NOPULL; // 指定上下拉选择 // 3. 根据结构体信息,完成GPIO配置 GPIO_Init(BEEPx_GPIO, &g); }
main.c
#include "stm32f4xx.h" #include "systick.h" #include "led.h" #include "beep.h" #include "key.h" int main(void) { /* LED1 */ LED1_Clock(); LED_Init(LED1_GPIO, LED1_Pin); LED1_OFF(); /* LED2 */ LED2_Clock(); LED_Init(LED2_GPIO, LED2_Pin); LED2_OFF(); /* LED3 */ LED3_Clock(); LED_Init(LED3_GPIO, LED3_Pin); LED3_OFF(); /* LED4 */ LED4_Clock(); LED_Init(LED4_GPIO, LED4_Pin); LED4_OFF(); /* KEY1 */ KEY1_Clock(); KEY_Init(KEY1_GPIO, KEY1_Pin); /* KEY2 */ KEY2_Clock(); KEY_Init(KEY2_GPIO, KEY2_Pin); /* KEY3 */ KEY3_Clock(); KEY_Init(KEY3_GPIO, KEY3_Pin); /* KEY4 */ KEY4_Clock(); KEY_Init(KEY4_GPIO, KEY4_Pin); /* BEEP */ BEEP_Clock(); BEEP_Init(BEEP_GPIO, BEEP_Pin); BEEP_OFF(); while (1) { // 按键KEY1 if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) { // 按下 // 消抖 delay_ms(10); if (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0) { // 确定是人为按下 LED1_reversal_status(); LED2_reversal_status(); } while (KEY_ReadInputData(KEY1_GPIO, KEY1_Pin) == 0); } // 按键KEY2 if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) { // 按下 // 消抖 delay_ms(10); if (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0) { // 确定是人为按下 LED3_reversal_status(); LED4_reversal_status(); } while (KEY_ReadInputData(KEY2_GPIO, KEY2_Pin) == 0); } // 按键KEY3 if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) { // 按下 // 消抖 delay_ms(10); if (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0) { // 确定是人为按下 BEEP_reversal_status(); } while (KEY_ReadInputData(KEY3_GPIO, KEY3_Pin) == 0); } } }