目录
一、问题的引入
学习STM32单片机前,要先了解单片机的最小系统组成
单片机的最小系统由以下四部分组成:
芯片+晶振电路+复位电路+供电电路
一个完整的系统则是:
最小系统+其它的外设
芯片:
整个系统的核心,相当于人类的大脑,会提供引脚与外部电路相连
芯片四周那些银白色的引脚是从芯片内部引申出来的。它负责芯片内部的控制单元与外部硬件的连接。
那么一个引脚其实本质上就是一根"电线"。
引脚拥有输入功能/输出功能,比如:
1.引脚可以输出/输入一个电平信号(1/0)
那么电平信号是针对于CPU来说的,因为CPU只能识别二进制0/1(数字信号)
2.也可以称引脚可以输入/输出一个高低电压
因为对于外部电路来说,外部电路的工作是采用模拟电压信号(即多少V电压)
为什么一个引脚对CPU和外部的电路的输入/输出是不一样的信号?它是怎么做到的?
这是因为引脚在芯片内部还需要通过"控制单元"才能够进入CPU
控制单元:
不同硬件对应不同的控制单元:
GPIO --->GPIO控制器
USART --->USART控制器
......
二、STM32F4xxGPIO内部结构
参考<STM32F4xx中文参考手册.pdf>,英语基础比较好的同学也可以去看<STM32F4xx英文参考手册.pdf>。这两个手册可以到官网去下载,但是一般是全英文的。想要下载中文版的可以进入意法半导体STM32/STM8技术社区www.stmcu.org.cn ,里面有着STM32/STM8相关型号的芯片的各种资料。
通过图得知,每个GPIO可以独立配置成不同的功能
GPIO配置的功能如下:
(1)输入功能
CPU可以通过该GPIO获取外部电路输入的一个电平状态
输入又分成以下几种模式:
a.浮空输入:不接上拉电阻也不接下拉电阻
在这种情况下,IO引脚的电平状态完全由外部输入决定 3.3V-->1 0V-->0
b.带上拉的输入:通过上拉电阻,接了VDD
此时,如果外部的引脚没有任何的输入的时候,CPU也能获取一个高电平(1)
只有在外部输入一个低电平的时候,才能读取到一个低电平
c.带下拉的输入:通过下拉电阻,接入了VSS
此时,如果外部的引脚没有任何的输入的时候,CPU也能获取一个低电平(0)
只有在外部输入一个高电平的时候,才能读取到一个高电平
d.模拟输入
该引脚被配置为模拟输入,能够获取到的就是模拟信号
通过ADC转换获取数字量
(2)输出功能
CPU可以通过该GPIO向外部电路输出一个电平
a.输出推挽(PP)
CPU可以向外部电路输出高电平也可以输出低电平
b.输出开漏(OD)
CPU输出不了高电平,没有P-MOS管
CPU正常输出低电平
(3)复用功能(AF)
复用功能是指GPIO用作其它的外设功能线
比如:
IIC、SPI、USART.....
每个GPIO口理论上都可以配置成16种复用功能
具体哪个引脚可以复用成什么功能,由原理图以及手册决定
三、 STM32F4xxGPIO引脚简介
GPIO是什么 ? 通用功能输入输出
GPIO就是从芯片内部引出的一根功能复用的线
功能复用:GPIO引脚可以由CPU去配置成不同的功能
比如:
输入功能、输出功能、复用功能、模拟功能等等
STM32F407 共有144个引脚
分成9组,记为GPIOA、GPIOB、.....GPIOI
简写模式:PA、PB.....
每个组有16个引脚,编号从0~15
比如GPIOA组的引脚就有:
PA0~PA15
而这些GPIO引脚的功能,都有独立的寄存器组来配置,如果我们要使用这个GPIO,那么就必须先去配置它的寄存器组
那么如果我们要去配置这些寄存器,就必须知道寄存器的地址。
每组GPIO地址分布如下:
可以通过<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
上述的表有几个名字需要解释:
边界地址:指寄存器组的起始地址(基址)和结束地址
外设:该寄存器组对应的硬件控制器
总线:该硬件控制器挂载的时钟线
那么对于GPIO来说,这些寄存器分别用来干嘛?
四、STM32F4xxGPIO寄存器说明
每个通用 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_AFRH 和 GPIOx_AFRL)
其中,上述寄存器名称中x表示GPIO分组,x=A.....I
下面学习寄存器
(1)GPIO端口模式寄存器(GPIOx_MODER) (x = A..I)
偏移地址:0x00 (基址+偏移地址 == 寄存器地址)
比如:GPIOC_MODER地址就是0x40020800+0x00
该寄存器用来控制GPIOx组的16个GPIO引脚的模式(4种:输入、输出、模拟、复用)
每个引脚占用2bits
编号为y(0~15)的GPIO引脚对应的位是该寄存器的[2y+1:2y]
具体的配置如下:
GPIOx_MODER[2y+1:2y] 模式
00 输入模式
01 输出模式
10 复用模式
11 模拟模式
例子:
用C代码把PF9配置为输出模式
分析:
PF组的基址:0x40021400
模式寄存器的偏移地址:0x00
所以GPIOF_MODER地址:0x40021400+0x00
如果要把PF9配置为输出模式,就需要将GPIOF_MODER[19:18]-->01
把地址为0x40021400的寄存器中bit19清0,bit18置1
怎么做?
在STM32中表示地址用 unsigned long来表示地址
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)GPIO 端口输出类型寄存器 (GPIOx_OTYPER) (x = A..I)
偏移地址:0x04
输出类型有两种:推挽和开漏
该寄存器用来选择GPIOx组的16个引脚的输出类型
每个引脚占1bit,编号为y的引脚对应比特位就是GPIOx_OTYPER[y]
具体配置如下:
GPIOx_OTYPER[y] 输出类型
1 开漏
0 推挽
(3)GPIO端口输出速度寄存器 (GPIOx_OSPEEDR) (x = A..I/)
偏移地址:0x08
用来控制GPIOx组的16个引脚的输出速率,每个引脚占2bits,编号为y的引脚对应比特位为[2y+1:2y]
具体配置速率如下:
GPIOx_OSPEEDR[2y+1:2y] 速率
00 低速
01 中速
10 快速
11 高速
(4)GPIO 端口上拉/下拉寄存器 (GPIOx_PUPDR) (x = A..I/)
偏移地址:0x0C
用来控制GPIOx组的16个引脚的上下拉电阻的选择
每个引脚占2bits,编号为y的引脚对应比特位为[2y+1:2y]
具体配置如下:
GPIOx_PUPDR[2y+1:2y] 上下拉选择
00 无上下拉
01 上拉
10 下拉
11 保留,未使用
(5)GPIO 端口输入数据寄存器 (GPIOx_IDR) (x = A..I)
偏移地址:0x10
该寄存器可以用来表示GPIOx组的16个引脚的输入电平值
每个引脚占1bits,该寄存器中只有[15:0]是可以使用的
GPIOx_IDR[y] --->GPIOxy的输入电平
因此
GPIOx_IDR[y] y号引脚的电平
1 高电平
0 低电平
比如:
CPU想要知道GPIOA7是高电平还是低电平:
if(GPIOA_IDR & (1<<7))
{
PA7是高电平
}
else
{
PA7是低电平
}
(6)GPIO 端口输出数据寄存器 (GPIOx_ODR) (x = A..I)
偏移地址:0x14
表示GPIOx组的16个引脚的输出值
具体配置
GPIOx_ODR[y] y号引脚的电平
1 高电平
0 低电平
(7)GPIO 端口置位/复位寄存器 (GPIOx_BSRR) (x = A..I)
偏移地址:0x18
Bit Set --->bit位置1
Bit Reset --->bit位清0
该寄存器用来表示GPIOx组的16个引脚的输出状态
其中
高16位用作端口复位寄存器
低16位用作端口置位寄存器
这个寄存器有点特殊,写1有效,写0是无效
注意:如果同时对 BSx 和 BRx 置位,则 BSx 的优先级更高
(8)GPIO 端口配置锁定寄存器 (GPIOx_LCKR) (x = A..I)
(9)GPIO 复用功能低位寄存器 (GPIOx_AFRL) (x = A..I)
(10)GPIO 复用功能高位寄存器 (GPIOx_AFRH) (x = A..I)
GPIOx_AFRL和GPIOx_AFRH两个寄存器放在一起使用的
因为一个GPIO引脚最多有16种复用功能,那么1一个引脚需要占用4bits
所以一个组的16个引脚必须需要2个寄存器才能够用
GPIO编号为0~7由GPIOx_AFRL寄存器管理
GPIO编号为8~15由GPIOx_AFRH寄存器管理
具体哪个值是何种复用,要结合电路图和功能手册才能看
五、STM32F4xxGPIO时钟使能
根据上述的寄存器,就可以去实现所有基于GPIO能够完成的功能
比如:
LED点灯
蜂鸣器
按键
.....
之前有说过,任何一个硬件控制器想要工作,都必须使能时钟(GPIO所有的分组全部属于AHB1时钟线)
那么时钟的相关配置,参考RCC
RCC:Reset Clock Control 复位时钟控制
0x4002 3800 - 0x4002 3BFF RCC 基址:0x40023800
那么我们现在的目的就是找到有关AHB1外设使能的寄存器
RCC_AHB1ENR 偏移地址:0x30
此寄存器[8:0]分别控制了GPIOx的时钟使能
1 使能时钟
0 禁止时钟
比如:
把RCC_AHB1ENR[5]--->1
使能了GPIOF组的时钟
总结:
利用寄存器实现GPIO功能的配置的步骤
1)从原理图找出对应的引脚 比如:点灯
VCC---D1--PF9
经过分析:CPU对PF9输出一个低电平,D1就会亮
2)配置GPIO分组时钟
3)配置GPIO模式寄存器
4)配置GPIO输出类型
5)配置GPIO输出速率寄存器
6)配置GPIO上下拉寄存器
7)输出模式,对输出数据寄存器进行操作,输入模式,读取输入数据寄存器
1、练习: 点亮四个灯
D1--PF9
D2--PF10
D3--PE13
D4--PE14
leg_reg.c
#include "led_reg.h"
void led_reg_init(void)
{
unsigned long r = 0;
//1.使能GPIOE组和GPIOF组的时钟
rRCC_AHB1ENR |= (1<<5);
rRCC_AHB1ENR |= (1<<4);
//2.配置功能模式 输出模式
//PF9
r = rGPIOF_MODER;
r &= ~(1<<19);
r |= (1<<18);
//PF10
r &= ~(1<<21);
r |= (1<<20);
rGPIOF_MODER = r;
//PE13
r = rGPIOE_MODER;
r &= ~(1<<27);
r |= (1<<26);
//PE14
r &= ~(1<<29);
r |= (1<<28);
rGPIOE_MODER = r;
//3.配置输出类型 推挽输出
r = rGPIOF_OTYPER;
r &= (~(1<<9));
r &= (~(1<<10));
rGPIOF_OTYPER = r;
r = rGPIOE_OTYPER;
r &= (~(1<<13));
r &= (~(1<<14));
rGPIOE_OTYPER = r;
//4.配置输出速率
r = rGPIOF_OSPEEDR;
r &= ~(1<<19);
r |= (1<<18);
r &= ~(1<<21);
r |= (1<<20);
rGPIOF_OSPEEDR = r;
r = rGPIOE_OSPEEDR;
r &= ~(1<<27);
r |= (1<<26);
r &= ~(1<<29);
r |= (1<<28);
rGPIOE_OSPEEDR = r;
//5.配置上下拉 输入悬空
r = rGPIOF_PUPDR;
r &= ~(1<<19);
r &= ~(1<<18);
r &= ~(1<<21);
r &= ~(1<<20);
rGPIOF_PUPDR = r;
r = rGPIOE_PUPDR;
r &= ~(1<<27);
r &= ~(1<<26);
r &= ~(1<<29);
r &= ~(1<<28);
rGPIOE_PUPDR = r;
//6.默认状态(输出1)
r = rGPIOF_ODR;
r |= (1<<9);
r |= (1<<10);
rGPIOF_ODR = r;
r = rGPIOE_ODR;
r |= (1<<13);
r |= (1<<14);
rGPIOE_ODR = r;
}
void led_Ctrl(int led_num,int status)
{
if(led_num == D1)
{
status ? D1_OFF : D1_ON;
}
if(led_num == D2)
{
status ? D2_OFF : D2_ON;
}
if(led_num == D3)
{
status ? D3_OFF : D3_ON;
}
if(led_num == D4)
{
status ? D4_OFF : D4_ON;
}
}
void delay(void) //延时函数、延时的时间可以自己改
{
int i;
for(i=0;i<100000;i++);
}
leg_reg.h
#ifndef __LED_REG_H__
#define __LED_REG_H__
/*AHB1寄存器*/
#define rRCC_AHB1ENR *((volatile unsigned long *)(0x40023800+0x30))
/*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相关的寄存器*/
#define rGPIOF_MODER *((volatile unsigned long *)(GPIOF_BASE+0x00))
#define rGPIOF_OTYPER *((volatile unsigned long *)(GPIOF_BASE+0x04))
#define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE+0x08))
#define rGPIOF_PUPDR *((volatile unsigned long *)(GPIOF_BASE+0x0C))
#define rGPIOF_IDR *((volatile unsigned long *)(GPIOF_BASE+0x10))
#define rGPIOF_ODR *((volatile unsigned long *)(GPIOF_BASE+0x14))
/*GPIOE相关的寄存器*/
#define rGPIOE_MODER *((volatile unsigned long *)(GPIOE_BASE+0x00))
#define rGPIOE_OTYPER *((volatile unsigned long *)(GPIOE_BASE+0x04))
#define rGPIOE_OSPEEDR *((volatile unsigned long *)(GPIOE_BASE+0x08))
#define rGPIOE_PUPDR *((volatile unsigned long *)(GPIOE_BASE+0x0C))
#define rGPIOE_IDR *((volatile unsigned long *)(GPIOE_BASE+0x10))
#define rGPIOE_ODR *((volatile unsigned long *)(GPIOE_BASE+0x14))
/*灯的编号*/
enum LED_NUM
{
D1,
D2,
D3,
D4
};
/*灯的状态*/
enum LED_STATUS
{
ON,
OFF
};
//灯的控制
#define D1_ON (rGPIOF_ODR &= ~(1<<9))
#define D1_OFF (rGPIOF_ODR |= (1<<9))
#define D2_ON (rGPIOF_ODR &= ~(1<<10))
#define D2_OFF (rGPIOF_ODR |= (1<<10))
#define D3_ON (rGPIOE_ODR &= ~(1<<13))
#define D3_OFF (rGPIOE_ODR |= (1<<13))
#define D4_ON (rGPIOE_ODR &= ~(1<<14))
#define D4_OFF (rGPIOE_ODR |= (1<<14))
void led_reg_init(void);
void led_Ctrl(int led_num,int status);
void delay(void);
#endif
main.c
#include "led_reg.h"
int main()
{
led_reg_init();
led_Ctrl(D1, ON);
led_Ctrl(D2, ON);
led_Ctrl(D3, ON);
led_Ctrl(D4, ON);
while(1);
// while(1)
// {
// led_Ctrl(D1, ON);
// delay();
// led_Ctrl(D1, OFF);
//
// led_Ctrl(D2, ON);
// delay();
// led_Ctrl(D2, OFF);
// led_Ctrl(D3, ON);
// delay();
// led_Ctrl(D3, OFF);
// led_Ctrl(D4, ON);
// delay();
// led_Ctrl(D4, OFF);
// }
// beep_Ctrl(beep_ON);
}
手动编写一个假延时函数:
void mydelay(int ms)
{
int i;
while(ms--)
{
for(i=0;i<0x6000;i++);
}
}
2、练习:实现流水灯
现象:D1亮-->延时-->D1灭-->D2亮-->D2灭->......>D4灭
调用上面的leg_reg.c和leg_reg.h,将main()修改一下即可
main.c
#include "led_reg.h"
#include "beep_reg.h"
#include "key_reg.h"
int main()
{
led_reg_init();
// led_Ctrl(D1, ON);
// led_Ctrl(D2, ON);
// led_Ctrl(D3, ON);
// led_Ctrl(D4, ON);
// while(1);
while(1)
{
led_Ctrl(D1, ON);
delay();
led_Ctrl(D1, OFF);
led_Ctrl(D2, ON);
delay();
led_Ctrl(D2, OFF);
led_Ctrl(D3, ON);
delay();
led_Ctrl(D3, OFF);
led_Ctrl(D4, ON);
delay();
led_Ctrl(D4, OFF);
}
// beep_Ctrl(beep_ON);
}
用ARM汇编指令写了一个STM32的项目启动文件
start.s
stack_size EQU 0x200 ;定义堆栈的大小
IMPORT main
;在keil5中定义堆栈段
AREA my_stack,NOINIT,READWRITE,ALIGN=3
stack_mem ;标记堆栈的开始
SPACE stack_size ;开辟堆栈空间
stack_end ;标记堆栈从此处开始结束 栈顶指针
PRESERVE8 ;8字节对齐
THUMB ;处理器均为THUMB状态
;在keil5中定义RESET段
AREA RESET,DATA,READONLY
__Vectors
DCD stack_end ;中断向量表的第0个必须是栈顶指针
DCD __start ;第1个必须是用户代码
SPACE 0x400 ;预留空间
;定义代码段
AREA |.text|,CODE,READONLY
__start PROC
BL main
B .
ENDP
END
3、练习:蜂鸣器的使用
beep_reg.c
#include "beep_reg.h"
void beep_reg_init(void)
{
unsigned long r = 0;
//1.使能GPIOF组的时钟
rRCC_AHB1ENR |= (1<<5);
//2.配置功能模式
//PF8
r = rGPIOF_MODER;
r &= ~(1<<17);
r |= (1<<16);
rGPIOF_MODER = r;
//3.配置输出类型
r = rGPIOF_OTYPER;
r &= (~(1<<8));
rGPIOF_OTYPER = r;
//4.配置输出速率
r = rGPIOF_OSPEEDR;
r |= (1<<17);
r |= (1<<16);
rGPIOF_OSPEEDR = r;
//5.配置上下拉
r = rGPIOF_PUPDR;
r |= (1<<17);
r &= ~(1<<16);
rGPIOF_PUPDR = r;
//6.默认状态(输出0)
r = rGPIOF_ODR;
r &= ~(1<<8);
rGPIOF_ODR = r;
}
void beep_Ctrl(int status)
{
status ? BEEP_OFF : BEEP_ON;
}
beep_reg.h
#ifndef __BEEP_REG_H__
#define __BEEP_REG_H__
/*AHB1寄存器*/
#define rRCC_AHB1ENR *((volatile unsigned long *)(0x40023800+0x30))
/*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相关的寄存器*/
#define rGPIOF_MODER *((volatile unsigned long *)(GPIOF_BASE+0x00))
#define rGPIOF_OTYPER *((volatile unsigned long *)(GPIOF_BASE+0x04))
#define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE+0x08))
#define rGPIOF_PUPDR *((volatile unsigned long *)(GPIOF_BASE+0x0C))
#define rGPIOF_IDR *((volatile unsigned long *)(GPIOF_BASE+0x10))
#define rGPIOF_ODR *((volatile unsigned long *)(GPIOF_BASE+0x14))
/*蜂鸣器的状态*/
enum BEEP_STATUS
{
beep_ON,
beep_OFF
};
//蜂鸣器的控制
#define BEEP_OFF (rGPIOF_ODR &= ~(1<<8))
#define BEEP_ON (rGPIOF_ODR |= (1<<8))
void beep_reg_init(void);
void beep_Ctrl(int status);
#endif