本文希望在上一节的基础上,把指针操作过渡到寄存器的使用,来帮助读者深入理解寄存器。
主函数里出现了强制转换与指针的操作,程序不那么容易理解。我们把寄存器的地址进行宏定义,可以增强可读性。
#define RCC_APB2ENR (*(unsigned int *)0x40021018)
#define GPIOB_CRH (*(unsigned int *)0x40010c04)
#define GPIOB_ODR (*(unsigned int *)0x40010c0c)
int main(void)
{
RCC_APB2ENR = 0x00000008;
GPIOB_CRH = 0x44444443;
GPIOB_ODR = 0x00000000;
return 0;
}
一个工程里可能有多个.c文件,假如另外一个.c文件也想使用这些寄存器,需要重复定义,很麻烦。所以,程序再次修改,增加一个头文件main.h,把宏定义放入头文件内。如果别的.c文件也想使用这些寄存器,只需要包含main.h头文件即可。
在main.c文件的同一级目录下,新建一个main.h并在main.h内输入以下代码:
#define RCC_APB2ENR (*(unsigned int *)0x40021018)
#define GPIOB_CRH (*(unsigned int *)0x40010c04)
#define GPIOB_ODR (*(unsigned int *)0x40010c0c)
在main.c内把代码改为:
#include "main.h"
int main(void)
{
RCC_APB2ENR = 0x00000008;
GPIOB_CRH = 0x44444443;
GPIOB_ODR = 0x00000000;
return 0;
}
在.c文件中,用“#include “main.h””这行代码代替了原先的3行宏定义,宏定义都放到了main.h文件里。编译工程,并下载程序,可以看出现象仍是LED1点亮。
可以想象出,GPIO与时钟相关的寄存器都是很常用的寄存器,如果每一次操作这些IO口都需要看数据手册的话,太累,所以人家做芯片的把常用的寄存器对应的地址都设置好了,并放到一个头文件内,就是
上一篇文章已经计算过结构体中元素的地址偏移,这里直接给出结论:
代码GPIOB->CRH就是操作地址0x4010 0C04。同理,时钟使能的操作也要使用结构体。
代码修改为:
//#include "main.h"
#include
int main(void)
{
RCC->APB2ENR = 0x00000008;
GPIOB->CRH = 0x44444443;
GPIOB->ODR = 0x00000000;
return 0;
}
寄存器是可以按位操作的,为了使第一个代码足够简单,所以用了一些错误的写法,操作了无关的引脚,如果被“连带”的引脚恰好连接了别的外设,这外设多无辜。现在拨乱反正。
先复习按位与或非的知识
A | 1 | 1 | 0 | 0 |
---|---|---|---|---|
B | 1 | 0 | 1 | 0 |
&与 | 1 | 0 | 0 | 0 |
"|"或 | 1 | 1 | 1 | 0 |
^异或 | 0 | 1 | 1 | 0 |
~A 非A | 0 | 0 | 1 | 1 |
注意,跟逻辑非的操作不同,按位非的操作符是~,而不是!感叹号。
按位操作主要的作用就是清0或者置1,会用到以下的表达式:
0&n = 0 1&n = n
1|n = 1 0|n = n
如果想保持数据a其它位不变,把第二位写成零,a&=0b 1101。
把第二位写成1,a|=0b 0010。
修改代码
#include
int main(void)
{
RCC->APB2ENR |= 0x00000008;
GPIOB->CRH &= 0xfffffff0;//先清零
GPIOB->CRH |= 0x00000003;
GPIOB->ODR &= 0xfffffeff;
return 0;
}
还可以优化,代码不好看懂。代码是写给机器运行的,却是写给人看的。看到这些代码的人,是不是还需要去翻看数据手册和原理图,才知道去操作哪个引脚呢?接下来修改为按位操作:
#include
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
GPIOB->CRH &= ~(0xf<<(0*4));
GPIOB->CRH |= 0x3<<(0*4);
GPIOB->ODR &= ~(1<<8);
return 0;
}
为什么费尽心机要把这些寄存器的配置改为按位操作?因为这样的操作有比较好的可读性和扩展性。有的人认为这些代码不好读,这是因为读的太少,例如 对于ODR寄存器,牢牢记住“或操作”置1,输出高电平,“与操作”置0,输出低电平。
为什么左移0*4位,这个操作就是不左移呀?因为这样的写法很容易看出是操作引脚0或8,并且便于修改。例如,我现在想改为操作PB1,代码只需要稍微修改下:
#include
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
GPIOB->CRL &= ~(0xf<<(1*4));
GPIOB->CRL |= 0x3<<(1*4);
GPIOB->ODR &= ~(1<<1);
return 0;
}
其中14,因为这个1代表的就是PB1(注意,同时CRH也换成了CRL),4代表的是每4位设置一个IO。如果是PB2,那么代码就改为GPIOB->CRL |= 0x2<<(24);,是PBn就改为GPIOB->CRL |= 0x2<<(n*4);第n个引脚输出0,也可以改为GPIOB->ODR &= ~(1<