对于每个GPI/O端口的寄存器:(32位)
GPIO端口的每个位可以由软件分别配置成多种模式,I/0端口寄存器必须按32位字被访问(不允许半字或字节访问,如下文提到16位访问,是因为编译环境会自动补齐到32位),关于几种模式在后文介绍
2个32位的配置寄存器分别为配置低寄存器(GPIOx_CRL) 和 配置高寄存器(GPIOx_CRH)
① 端口配置低寄存器(GPIOx_CRL) (x=A…E)
结合下图来说明,低位寄存器是用来配置GPIO0到GPIO7的,A~E共5组GPIO,相对应的可以知道高位寄存器则用来配置A ~ E组寄存器的GPIO8到GPIO15;可以发现,每一个GPIO引脚由4个比特位控制,其中第2位是MODE,用来配置该GPIO是输入还是输出模式,如果是输出,对应引脚输出的最大速度;其中的高2位是CNF,用来配置引脚输入/输出时的具体功能模式,不同应用需求对应不同的模式设置(后文解释)
② 端口配置高寄存器(GPIOx_CRH) (x=A…E)
同上面的低位配置寄存器一样,只不过高位配置寄存器是用来配置GPIO8~GPIO15的;要知道,一般配置一个GPIO的输入输出模式时,先配置MODE,确定引脚是用来输入还是输出,其次再配置CNF来设置具体的工作模式
2个32位的数据寄存器分别是输入数据寄存器(GPIOx_IDR) 和 输出数据寄存器(GPIOx_ODR)
① 输入数据寄存器(GPIOx_IDR) (x=A…E)
1、
输入数据寄存器是32位的,但是只用到0~15位,分别对应一组GPIO的16个引脚,仅可读;
2、
0~15位组成一“字”,名为IDR,读的时候只能以字的形式读取,即一次读取16位的比特位;
② 输出数据寄存器(GPIOx_ODR) (x=A…E)
1、
输出数据寄存器也是32位,只用到0~15位,分别对应一组GPIO的16个引脚,可读写;
2、
读写寄存器时也只能以字的形式操作
这是一个32位的寄存器,低16位(BS)用来设置端口的位,高16位(BR)则是用来清除端口的位;两者均仅可写,写操作时需以字的形式写入
如对于置位GPIO15时,令BS15为1表示置位,此时BR15就不能设为1了,如果BS为1时BR也为1,则默认BS生效
锁定寄存器(GPIOx_LCKR) (x=A…E)是一个32位的寄存器,当设置了位16(LCKK)时,则会锁定端口位的配置,位[15:0]对应的GPIO的16个引脚,当对相应的端口位执行了LOCK后,在下次系统复位之前将不能再更改端口位的配置
每个锁定位锁定控制寄存器(CRL, CRH)中相应的4个位,需要注意锁键的输入序列,顺序不要搞错,并且位[15:0]只能在LCKK为0时才可写
位清除寄存器(GPIOx_BRR) (x=A…E) 的高16位保留,不使用;低16位以字的形式进行写入,写入1时才会对位有影响
1、
BRR和BSRR的最显著用处就是可以只改变某一个或某几个针脚的值而不改变其他(因为0无效)
2、
ODR改变时则是改变全部位的状态(仅可以字形式写)
//对于ODR操作GPIO的伪代码如下:
disable_irq() //关闭中断
save_gpio_pin_sate = read_gpio_pin_state();
save_gpio_pin_sate = xxxx;
chang_gpio_pin_state(save_gpio_pin_sate);
enable_irq();
可见当控制GPIO的状态时最好不要使用ODR,关闭中断明显会延迟或丢失一事件的捕获
3、
BSRR的高16位和BRR的低16位具有相同功能,举例:
对GPIOE的16个IO,改变低8位的数据而保持高8位不变
① GPIOE->BSRR = (0x34) & 0xff; //操作BSRR[0,15]
② GPIOE->BRR = ~(0x34) & 0xff;
一次完成对8位的操作:
GPIOE->BSRR = ((0x34) & 0xff) | ( (~(0x34) & 0xff)<<16 );
一次完成对16位的操作:
GPIOE->BSRR = ((0x34)& 0xffff) | ( (~(0x34))<<16 );
BSRR的高16位并非是多余的,对GPIOE的位7置’1’,位6置’0’:
GPIOE->BSRR = 0x400080;
不使用BSRR高16位,要分2次操作,造成位7和位6的变化不同步
GPIOE->BSRR = 0x80;
GPIOE->BRR = 0x40;
4、
有些IO时序要求非常严格,同时对一个GPIO置1和对另一个GPIO清0是很必要的
5、
BSRR还有一个特点,就是Set比Reset的级别高
内置外设:如串口、ADC、DCA等等(属于stm32内部功能模块)
端口复用:当一个GPIO作为内置外设的功能引脚时,称为复用。比如说,STM32的串口1的引脚对应的I/O位PA9、PA10。而PA9、PA10默认的功能都是GPIO,所以说当PA9、PA10引脚作为串口1使用的时候就是端口复用
端口重映射:把引脚的内置外设功能映射到其他的引脚上,但不是可以随便映射的,需要参照数据手册
推挽结构:两个参数相同的三极管或MOS管分别受两互补信号的控制,总是在一个三极管或MOS管导通的时候另一个截止。高低电平由输出电平决定。有推有拉,任何时候IO口的电平都是确定的,不需要外接上拉或者下拉电阻
开漏输出:只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极。适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内);
GPIO内部电路结构:
FT:代表GPIO口兼容3.3V和5V
保护二极管:IO引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入
P-MOS、N-MOS管:由P-MOS管和N-MOS管组成的单元电路使得GPIO具有“推挽输出”和“开漏输出”的模式
TTL肖特基触发器:信号经过触发器后,模拟信号转化为0和1的数字信号
GPIO支持4种输入模式,三种最大翻转速度(2MHz、10MHz、50MHz)
① 浮空输入(GPIO_Mode_IN_FLOATING)
逻辑器件与引脚即不接高电平,也不接低电平,相当于浮在空中呈高阻态,上面用绳子一拉就上去了,下面用绳子一拉就沉下去了,一般用来做ADC输入用,这样可以减少上下拉电阻对结果的影响
② 输入上拉(GPIO_Mode_IPU)
上拉就是通过上拉电阻把电位拉高,比如拉到Vcc,将输入的不确定的信号嵌位在高电平
③ 输入下拉(GPIO_Mode_IPD)
下拉就是把电压拉低,拉到GND,下拉电阻另一端接地即可,原理与上拉的一样
④ 模拟输入(GPIO_Mode_AIN)
模拟输入是指0,1的二进制数字信号,通过数模转换,转换成模拟信号,模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等
① 开漏输出(GPIO_Mode_Out_OD)
输出端相当于三极管的集电极,c极是开路的,可以接一个电阻到3.3V,也可以接一个电阻到5V,这样,在输出1的时候,就可以是5V电压,也可以是3.3V电压
开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经N-MOS管,最终输出到I/O端口
这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;同时,I/O端口的电平也可以通过输入电路进行读取;但是I/O端口的电平不一定是输出的电平
当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平
输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行,适合于做电流型的驱动,其吸收电流的能力相对强(一般20mA以内)
1、利用外部电路的驱动能力,减少IC内部的驱动
2、开漏引脚不连接外部的上拉电阻时,只能输出低电平,用来连接不同电平的器件,匹配电平用的
② 开漏复用功能(GPIO_Mode_AF_OD)
开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的
③ 推挽式输出(GPIO_Mode_Out_PP)
可以输出高,低电平,连接数字器件;推挽结构一般是指两个三级管分别受到互补信号的控制,总是在一个三极管导通的时候另一个截止。高低电平由IC的电源低定。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平
1、两只对称的功率开关管每次只有一个导通,所以导通损耗小,效率高
2、可以向负载灌电流
3、提高电路的负载能力,又提高开关速度
④ 推挽式复用功能(GPIO_Mode_AF_PP)
推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的
小结
通常有5种方式使用某个引脚功能,它们的配置方式如下:
1)作为普通GPIO输入:根据需要配置该引脚为浮空输入、带弱上拉输入或带弱下拉输入,同时不要使能该引脚对应的所有复用功能模块。
2)作为普通GPIO输出:根据需要配置该引脚为推挽输出或开漏输出,同时不要使能该引脚对应的所有复用功能模块。
3)作为普通模拟输入:配置该引脚为模拟输入模式,同时不要使能该引脚对应的所有复用功能模块。
4)作为内置外设的输入:根据需要配置该引脚为浮空输入、带弱上拉输入或带弱下拉输入,同时使能该引脚对应的某个复用功能模块。
5)作为内置外设的输出:根据需要配置该引脚为复用推挽输出或复用开漏输出,同时使能该引脚对应的所有复用功能模块。
端口位配置表
输出模式位
其他
1字(word)= 2字节(byte)
1字节(byte) = 8位(bit)
32位计算机的CPU一次最多能处理32位数据,即4字节
64位计算机的CPU一次最多能处理64位数据,即8字节
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 (特定位清零) |
I | 或 | 两个位都为0时,结果才为0 (特定位置1) |
^ | 异或 | 两个位相同为0,相异为1 (特定位取反) |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0 |
寄存器操作要求:在设定特定位时不能影响其他位。寄存器的读写是整体32位一起进行的(也就是说你只想修改bit5~bit7是不行的,必须整体32bit全部写入)
以下代码演示环境:gcc + 64位系统
① 获取一字节数据:
#include
void HextoTwo(int num) //递归,16进制转2进制
{
int remainder;
char buf[16][5] = {"0000","0001","0010","0011",\
"0100","0101","0110","0111",\
"1000","1001","1010","1011",\
"1100","1101","1110","1111"};
if(0 == num)
return;
remainder = num % 16;
HextoTwo(num >> 4);
printf("%s ", buf[remainder]);
}
void main()
{
unsigned int a = 0x12345678; //32bit
unsigned int b = 0x000000ff;
printf("\n原数据为:%x\n",a);
printf("unsigned int类型的大小:%ld 字节\n",sizeof(a));
printf("a的二进制数为:");HextoTwo(a);printf("\n");
printf("b的二进制数为:");HextoTwo(b);printf("\n\n\n");
printf("0 bit a >> 0 :");HextoTwo((a>>0));printf("\n");
printf("8 bit a >> 8 :");HextoTwo((a>>8));printf("\n");
printf("16bit a >> 16 :");HextoTwo((a>>16));printf("\n");
printf("24bit a >> 24 :");HextoTwo((a>>24));printf("\n\n\n");
printf("第一个字节:");HextoTwo((a>>0)&b);printf("\t\t%x",(a>>0)&b);printf("\n");
printf("第二个字节:");HextoTwo((a>>8)&b);printf("\t\t%x",(a>>8)&b);printf("\n");
printf("第三个字节:");HextoTwo((a>>16)&b);printf("\t\t%x",(a>>16)&b);printf("\n");
printf("第四个字节:");HextoTwo((a>>24)&b);printf("\t\t%x",(a>>24)&b);printf("\n\n\n");
}
运行结果如下图,对0x12345678(共32bit,4字节)操作,在黄色框里可以看到每右移增加8个bit(相当于右移1个字节),二进制数据段变少,其实是高位补0没有显示出来;然后将移位后的数据与0x000000ff相与,实际上是只有低8位能够保留,得到的自然就是最后一个字节了
② 获取某一位:
使用宏定义,比如获取0x68的底3位,先让1左移3,得到1000,1000与01101000相与,得到1000,1000右移3得到1,即第3位为1
#include
#define GET_BIT(x, bit) ((x & (1 << bit)) >> bit) /* 获取第bit位 */
void main()
{
unsigned int a = 0x68; /*二进制为:0110 1000*/
printf("\n\n");
printf("0x68的第0位:%d\n",GET_BIT(a,0));
printf("0x68的第1位:%d\n",GET_BIT(a,1));
printf("0x68的第2位:%d\n",GET_BIT(a,2));
printf("0x68的第3位:%d\n",GET_BIT(a,3));
printf("0x68的第4位:%d\n",GET_BIT(a,4));
printf("0x68的第5位:%d\n",GET_BIT(a,5));
printf("0x68的第6位:%d\n",GET_BIT(a,6));
printf("0x68的第7位:%d\n\n\n",GET_BIT(a,7));
}
③ 对32bit数据的字节清零、置1操作:
#include
#define CLEAR_LOW_BYTE0(x) (x &= 0xffffff00) /* 清零第0个字节 */
#define CLEAR_LOW_BYTE3(x) (x &= 0xff00ffff) /* 清零第3个字节 */
#define SET_LOW_BYTE0(x) (x |= 0x000000ff) /* 第0个字节置1 */
#define SET_LOW_BYTE3(x) (x |= 0xff000000) /* 第3个字节置1 */
void main()
{
unsigned int a = 0x11111111;
unsigned int b = 0x11111111;
unsigned int c = 0x22222222;
unsigned int d = 0x22222222;
printf("\n\n");
printf(" 原数据:%x\n",a);
printf("清零第0个字节后:%x\n",CLEAR_LOW_BYTE0(a));
printf("清零第3个字节后:%x\n\n\n",CLEAR_LOW_BYTE3(b));
printf(" 原数据:%x\n",c);
printf("置位第0个字节后:%x\n",SET_LOW_BYTE0(c));
printf("置位第3个字节后:%x\n",SET_LOW_BYTE3(d));
}
配置一个初始化led端口的函数:
#include "led.h"
//初始化 PA8为输出口.并使能的时钟
//LED IO 初始化
void LED_Init(void)
{
RCC->APB2ENR |= 1<<2; //使能 PORTA 时钟
GPIOA->CRH &= 0XFFFFFFF0;
GPIOA->CRH |= 0X00000003;//PA8 推挽输出
GPIOA->ODR |= 1<<8; //PA8 输出高
}
由于还没有写到时钟相关的寄存器,这里先不看时钟;先对GPIOA的CRH寄存器的低4位进行清0操作,CRH寄存器是端口配置高寄存器,控制8-15的io口的配置,则完成了对PA8相关配置寄存器的清零操作
接着将PA8端口的配置寄存器对应位设为0x3,即0011,00表示通用推挽输出,11表示输出模式,最大速度50MHz,这就完成了基础的配置设置
最后再通过ODR寄存器,对第8位(即PA8端口)写1,代表初始化后该端口点平为高
如有错误,还望指正!✌