前言
在上一篇博客中,我们使用操作寄存器的方式点亮了一盏LED灯,但是存在着两个问题:
1.这样的代码根本看不懂
2.寄存器采用的是直接赋值的方式,在修改需要修改的bit的同时也影响了其他的bit
在本文中,我们对上边的两个问题进行了优化。
主要内容
下面是上篇博客使用寄存器点亮一个LED灯的代码:
cpp
include <stdint.h>
int main(void)
{
//设置端口时钟
*((uint32_t*)(0x40021000 + 0x18)) = 0x04;
//打开GPIO端口A0、A1、A8
*((uint32_t*)(0x40010800 + 0x00)) = 0x33;
*((uint32_t*)(0x40010800 + 0x04)) = 0x03;
//端口输出数据寄存器GPIOx_ODR
*(uint32_t *)(0x40010800 + 0x0c) = 0xfefc;
//用一个死循环保持状态
while(1)
{}
}
一、关于寄存器地址的优化
观察这个代码可以发现:每个寄存器的地址是固定的,这样的话其实可以通过宏定义的方式对代码进行优化,那么在STM32官方提供的头文件中是否有相关的定义呢?答案是:有!
在STM32官方提供的stm32f10x.h(不同系列的芯片可能不同)这个头文件中可以找到相关的定义,以时钟寄存器RCC为例:
stm32f10x.h头文件中首先定义了一个RCC的结构体
cpp
typedef struct
{
__IO uint32_t CR;
__IO uint32_t CFGR;
__IO uint32_t CIR;
__IO uint32_t APB2RSTR;
__IO uint32_t APB1RSTR;
__IO uint32_t AHBENR;
__IO uint32_t APB2ENR;
__IO uint32_t APB1ENR;
__IO uint32_t BDCR;
__IO uint32_t CSR;
#ifdef STM32F10X_CL
__IO uint32_t AHBRSTR;
__IO uint32_t CFGR2;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || defined (STM32F10X_HD_VL)
uint32_t RESERVED0;
__IO uint32_t CFGR2;
#endif /* STM32F10X_LD_VL || STM32F10X_MD_VL || STM32F10X_HD_VL */
} RCC_TypeDef;
结合官方芯片手册中的RCC寄存器地址映像表可以发现:结构体是模拟了一个RCC寄存器的结构,其中的每一个寄存器都按照顺序在结构体中对应,偏移量就是结构体的成员变量的大小。
定义结构体类型后在该头文件中又定义了一个该结构体类型的指针:
cpp
#define RCC ((RCC_TypeDef *) RCC_BASE)
这里的RCC_BASE在该头文件中也有定义:
cpp
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
继续往下找AHBPERIPH_BASE:
cpp
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
再继续找PERIPH_BASE:
cpp
#define PERIPH_BASE ((uint32_t)0x40000000)
最终我们发现:
RCC这个(RCC_TypeDef *)类型的结构体指针的值:(uint32_t)0x40000000 + 0x20000 + 0x1000 = (uint32_t)0x40021000
要打开GPIOA接口的时钟,需要访问寄存器APB2ENR,这时就可以通过结构体指针直接访问了:
cpp
RCC->APB2ENR = 0x4;//这句代码就等价于之前的:*((uint32_t*)(0x40021000 + 0x18)) = 0x4;
这样一来代码就具备了语义,便于我们编程和阅读。按照这个思路,可以将之前的代码改造成如下的样子:
cpp
#include "stm32f10x.h"
int main(void)
{
//设置端口时钟
RCC->APB2ENR = 0x04;
//打开GPIO端口A0、A1、A8
GPIOA->CRL = 0x33;
GPIOA->CRH = 0x03;
//端口输出数据寄存器GPIOx_ODR
GPIOA->ODR = 0xfefc;
//用一个死循环保持状态
while(1)
{}
}
二、关于寄存器操作的优化
这里还有一个问题:在操作寄存器的时候,规范的做法是只对需要操作的位置进行更改,其余位置不做修改,因此对寄存器的设置方法修正如下:
cpp
#include "stm32f10x.h"
int main(void)
{
//设置端口时钟,将APB2ENR的bit2位置1
RCC->APB2ENR |= 0x04;
//打开GPIO端口A0、A1、A8,将CRL寄存器的bit5、bit4、bit1、bit0位置1;将CRH寄存器的bit1、bit0位置1
GPIOA->CRL |= 0x33;
GPIOA->CRH |= 0x03;
//端口输出数据寄存器GPIOx_ODR,将ODR的bit8、bit1、bit0位置0
GPIOA->ODR &= 0xfefc;
//用一个死循环保持状态
while(1)
{}
}
这个代码还可以进一步提高可读性,因为后边这些"0x04、0x33"数据是没有明确的含义的,我们可以思考一下:我们做的这些操作其实就是将寄存器的某一位进行了改变,那我们是不是也可以将寄存器的这一位做一个宏定义呢?答案是肯定的!而且ST公司也为我们做了对应的宏定义:
cpp
//这是截取其头文件中的宏定义
/****************** Bit definition for RCC_APB2ENR register *****************/
#define RCC_APB2ENR_AFIOEN ((uint32_t)0x00000001) /*!< Alternate Function I/O clock enable */
#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000004) /*!< I/O port A clock enable */
#define RCC_APB2ENR_IOPBEN ((uint32_t)0x00000008) /*!< I/O port B clock enable */
#define RCC_APB2ENR_IOPCEN ((uint32_t)0x00000010) /*!< I/O port C clock enable */
#define RCC_APB2ENR_IOPDEN ((uint32_t)0x00000020) /*!< I/O port D clock enable */
#define RCC_APB2ENR_ADC1EN ((uint32_t)0x00000200) /*!< ADC 1 interface clock enable */
上面的代码是stm32f10x.h头文件中的节选。
根据上面的头文件,我们的代码可以做进一步的进化:
cpp
#include "stm32f10x.h"
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;//使能GPIOA端口的时钟寄存器,打开时钟
GPIOA->CRL &= ~GPIO_CRL_CNF0;//配置GPIOA端口CRL寄存器的CNF位,配置为推挽输出模式
GPIOA->CRL |= GPIO_CRL_MODE0;//配置GPIOA端口CRL寄存器的MODE位,配置为最大速度50MHz
GPIOA->CRL &= ~GPIO_CRL_CNF1;//同上
GPIOA->CRL |= GPIO_CRL_MODE1;//同上
GPIOA->CRH &= ~GPIO_CRH_CNF8;//同上
GPIOA->CRH |= GPIO_CRH_MODE8;//同上
GPIOA->ODR &= ~(GPIO_ODR_ODR0 | GPIO_ODR_ODR1 | GPIO_ODR_ODR8);//打开第0、1、8号端口
//用一个死循环保持状态
while(1)
{}
}
三、总结
STM32 寄存器编程的最终形态核心是 "语义化宏定义 + 精准位操作":用官方宏替代魔法数字,用&=清零、|=置位,避免直接赋值破坏寄存器其他位;
"先清零再置位" 是寄存器配置的通用规范,能保证配置的准确性和鲁棒性;
这种写法兼顾了底层操作的灵活性和代码的可读性,是裸机开发的最优实践(比直接操作地址易维护,比 HAL 库代码执行效率更高)。