从零开始学嵌入式之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 库代码执行效率更高)。

相关推荐
吠品6 小时前
智谱开源 ZCode 之后,真正的考试才开始
单片机·嵌入式硬件·1024程序员节
Chery11407 小时前
高速ADC与精密DAC怎么选?DP9230D与DP8532D应用解析
嵌入式硬件·物联网·射频工程
小李不想当小白10 小时前
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
LCG米10 小时前
STM32驱动W5500以太网模块实测:TCP吞吐瓶颈定位、TX指针回绕陷阱与断线重连保活落地
stm32·单片机·嵌入式硬件·tcp/ip
尼喃12 小时前
662K/65Z5三脚稳压芯片规格书—PW6566B33/B30引脚功能
嵌入式硬件
创界工坊工作室12 小时前
63-基于STC89C52霍尔传感器的智能化电能表设计
stm32·单片机·嵌入式硬件·51单片机·proteus
智购科技自动售货机厂家13 小时前
让AI经营自动售货机:从Project Vend看工具权限与业务规则~YH
大数据·服务器·单片机·嵌入式硬件·深度学习
Zwawa14 小时前
从 printf 到曲线:把数据送上 VOFA+
c语言·单片机·嵌入式
意法半导体STM3214 小时前
【官方原创】LAT6048 如何扩大MP2的A35核和M33核共享内存
stm32·单片机·嵌入式硬件·内存映射·stm32mp2
en.en..15 小时前
STM32(F429)启动模式与两种烧录方式
stm32·单片机·嵌入式硬件