从零开始学嵌入式之STM32——4.使用寄存器点亮一个LED灯--代码优化

前言

在上一篇博客中,我们使用操作寄存器的方式点亮了一盏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 库代码执行效率更高)。

相关推荐
【赫兹威客】浩哥2 小时前
【赫兹威客】ESP32点灯实验
单片机·嵌入式硬件·esp32
卜锦元2 小时前
Mac 上无痛使用 Windows 双系统的完整实践(Intel 或 Apple M芯片都可以)
windows·单片机·macos·金融·系统架构
mftang2 小时前
STM32 RTC 唤醒中断功能实现低功耗功能
stm32·单片机·嵌入式硬件·rtc·超低功耗
CQ_YM11 小时前
ARM时钟与定时器
arm开发·单片机·嵌入式硬件·arm
哄娃睡觉11 小时前
stm32 mcu SWD和SPI下载模式有什么区别?
stm32
xiebs_12 小时前
0127TR
单片机·嵌入式硬件
A9better14 小时前
嵌入式开发学习日志50——任务调度与状态
stm32·嵌入式硬件·学习
草丛中的蝈蝈16 小时前
STM32向FLASH写入数据后,重新读出的数据和原写入数据不一致
stm32
DLGXY16 小时前
STM32——EXTI外部中断(六)
stm32·单片机·嵌入式硬件