在51中让一个引脚输出高低电平只需要一个步骤,而在32中至少需要三个步骤。
- 开启对应GPIO的时钟
- 配置对应IO口
- 设置IO口
本文将一步步进阶的讲解,三种寄存器编程的方法。
- 使用地址赋值进行配置
- 使用ST的宏进行配置
- 只控制需要的位(位运算)
- 与(&),或(|)
- 左移<<,右移>>
- 使用ST的宏进行位运算
使用地址赋值进行配置
第一步:启动对应IO口时钟,这里我们以PA0,PA1,PA8为例。
从数据手册上可以看出,GPIOA在APB2时间线上,所以启动对应IO口时钟线,就是启动APB2。
如何打开寄存器时钟?
这里以APB2外设使能寄存器(RCC_APB2ENR) 为例。启动寄存器本质上就是,找到寄存器的地址后赋值。当赋值的时候可以看上图,有0~31,它们从0开始,4个为一组为二进制数组,32个函数组成8个二进制数组,而把每组二进制转化为十六进制,就是赋值的值了。
从上图可得,当我们 区要启动GPIOA的时钟时,位2需要置位成1。
位3 | 位2 | 位1 | 位0 |
0 | 1 | 0 | 0 |
0100的二进制转化十六进制为4,所以等等给RCC_APB2ENR赋值的值为4。
而从第一张图我们可以看到RCC_APB2ENR的偏移地址:0x18,其地址=偏移地址+基地址。
打开STM32F10x参考手册里面有一个“存储器映像”里面可以看到其基地址。
从上图可以看出我们需要的“复位和时钟控制”基地址为0x4002100。
所以RCC_APB2ENR的地址=0x40021000+0x18。
#include <stm32f10x.h>
int main(void)
{
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
//2.配置对应IO口
//3.设置IO口
}
右边的*是强制类型转换,左边的*是声明指针。
从位0开始看,MODE0和CNF0为一组,控制GPIO0,那具体是GPIOA还是B还是其他的,就要看配置是GPIOx_CRL的x是什么了,可能是A,B或者其他的。CPIOx_CRL只能控制0-7寄存器,所以被叫做端口配置低寄存器。GPIOx_CRL控制8-15寄存器,所以被称为端口配置高寄存器。
配置PA0,为输出模式所以MODE0=11,CNF0=00,00为通用推挽输出模式,可以去搜一下GPIO的八种工作模式。所以现在GPIOA_CRL=3,这里注意如果此时我们还是按上面的(unit32_t*)去强制转换的话3可能机会变成0x00000003,这就不对了,因为其他口的模式全部都被配置为00了,正常情况下应该是要保留的。后续会写到,如何保留,这里还是先用地址赋值的方式配置一下PA0。
这里可以看到GPIOA的基地址为0x40010800。同理:
#include <stm32f10x.h>
int main(void)
{
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
//2.配置对应IO口
*(uint32_t*)(0x40010800+0x00)=3;
//3.设置IO口
}
这里把PA0设置0,其他PA设置为1,即15~0为1111 1111 1111 1110,转化为十六进制即为fffe。
#include <stm32f10x.h>
int main(void)
{
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
//2.配置对应IO口
*(uint32_t*)(0x40010800+0x00)=3;
//3.设置IO口
*(uint32_t*)(0x40010800+0x0C)=0xfffe;
}
使用ST的宏进行配置
这里以RCC(Reset Clock Controller)为例。
通过右击RCC,转到定义。
RCC_TypeDef一看就是一个结构体。RCC_BASE和结构体我们都打开看一下。
一层一层打开后发现,就是一个大的地址,一点点通过分块,定位到小的地址。
在刚才启动GPIO口的时钟就是这样的。
PERIPH_BASE=0x40000000,
APB2PERIPH_BASE(PERIPH_BASE+0x20000),
RCC_BASE=(APB2PERIPH_BASE+0x1000);
这样一计算,我们就可以得到基地址=0x40000000+0x200000+0x1000=0x40021000
结构体的第一个成员的地址和结构体的地址是一样的。所以CR的地址就是结构体的地址。而结构体又有一个特征,结构体的成员的地址是连续的。假设CR=0,则CFGR=4;明明是连续的为什么CFGR不等于1呢?因为这里是uint32_t,4个字节。所以CIR=8,以此类推APB2ENR=24。
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
可是为什么这里是0x18呢?因为这个是十六进制。十进制的24转化成十六进制就是18。
所以我们可以这样写程序。
RCC->APB2ENR=4;
两个代码是一模一样的。下面这种可读性也很好。
同理GPIOA的配置也是一样的。
所以代码就可以换成下面这种。
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
//2.配置对应IO口
*(uint32_t*)(0x40010800+0x00)=3;
//3.设置IO口
*(uint32_t*)(0x40010800+0x0C)=0xfffe;
RCC->APB2ENR=4;
GPIOA->CRL=3;
GPIOA->ODR=0xfffe;
只控制需要的位(位运算)
与(&),或(|)
RCC->APB2ENR = (RCC->APB2ENR+4);
RCC->APB2ENR |=4;
//1.开启对应GPIO的时钟
*(uint32_t*)(0x40021000+0x18)=4;
//2.配置对应IO口
*(uint32_t*)(0x40010800+0x00)=3;
//3.设置IO口
*(uint32_t*)(0x40010800+0x0C)=0xfffe;
RCC->APB2ENR=4;
GPIOA->CRL=3;
GPIOA->ODR=0xfffe;
RCC->APB2ENR |=4;
//这四步就等于GPIOA->CRL = 3;一步
GPIOA->CRL |=1;
GPIOA->CRL |=2;
GPIOA->CRL &=~4;
GPIOA->CRL &=~8;
//
GPIOA->ODR &=~1;
上下两个代码是一样的。建议写下面那个,这样改参数的时候就不会改到其他东西。
1001=9
原式 | 1 | 0 | 1 | 0 |
| 9 | 1 | 0 | 1 | 1 |
& 9 | 1 | 0 | 0 | 0 |
也就是说,如果你先把某一位写1 那就或(|)一下,想写0你就取反后与(&)一下。
在以后可能会遇到更改GPIO的情况,建议先把你想编写的位置清零其余位置不变,再去编写,不仅可以让代码更加直观,也能减少错误,而且不用像上面那种一步要分为四步。
例如:
#define SDA_OUT() {GPIOB->CRL &= 0x0FFFFFFF; GPIOB->CRL |= 0x30000000;}
#define SDA_IN() {GPIOB->CRL &= 0x0FFFFFFF; GPIOB->CRL |= 0x40000000;}
当GPIOB_CRL&0x0FFFFFFF时,因为与(&)必须都为 1才能是1,所以前面0的位置位15,14,13,12被全部清零了,然后再去通过或(|)更改CRL。
左移(<<),右移(>>)
RCC->APB2ENR |= 1<<2;
GPIOA->CRL |=1<<0;
GPIOA->CRL |=1<<1;
GPIOA->CRL &=~(1<<2);
GPIOA->CRL &=~(1<<3);
GPIOA->ODR &=~(1<<0);
这种就非常直观了,只需要对着操作手册,改哪就位移哪,1用(|),0用(&)。
上面几种,已经非常方便了,但是如果不看数据手册,根本看不出来改了什么。所以就有更好的一种方式,不仅更改了配置,还能一样看出来改了什么。所以我们就可以用下面这个方法。
使用ST的宏进行位运算
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
GPIOA->CRL |= GPIO_CRL_MODE0_0;
GPIOA->CRL |= GPIO_CRL_MODE0_1;
//也可以放一起GPIOA->CRL |=(GPIO_CRL_MODE0_0|GPIO_CRL_MODE0_1)
GPIOA->ODR &=~GPIO_CRL_CNF0_0;
GPIOA->ODR &=~GPIO_CRL_CNF0_1;
GPIOA->ODR &=~GPIO_ODR_ODR0;
举个例子:
GPIOA->CRL |= GPIO_CRL_MODE0_0;
控制GPIOA_CRL的mode0中的0,(|)则表示将其置1。
我们转到定义看一下
可以看出方法4和方法3其实是一样的,只不过方法4是ST已经帮我们定义好了,我们直接用就可以了。