看前需知:
在你编写寄存器的时候,首先可以总结一下有多少种方式可以把存储器的地址表示出来。如果看官您有带书,你就会知道存储器的数据类型分为:data bdata xdata idata pdata code 只是这些指向了不同的存储位置。在学习51单片机的时候,面对一系列特殊功能寄存器,你会发现在头文件中,通常以sfr 来编写地址 用 sbit --位寄存器 来定义需要的位
例如 sfr PMOD =0X89; sbit P1_0 =P1^0;
当你的寄存器数量变得多了起来,对于其绝对地址的访问,使用三种主要方式来访问:
1、宏定义 #define RCC ((RCC_TypeDef *) RCC_BASE)
2、指针访问 uchar data *dp3 =0x20;
3、at 关键字 uchar dp3 _at_ 0x20;
在STM32中,外设寄存器是通过内存映射I/O(Memory-Mapped I/O, MMIO)的方式访问的,这意味着你可以像访问内存一样通过指针来读写这些寄存器。每个外设有自己的一组寄存器,用于配置、控制状态以及数据传输等。
#define USART1_BASE ((uint32_t)0x40013800)
访问USART1的波特率寄存器
void ConfigUSART1BaudRate(uint32_t baudRate)
{
// USART_BRR寄存器地址偏移量
USART_TypeDef *USARTx = (USART_TypeDef *)USART1_BASE;
uint16_t div = ((uint32_t)USARTx->CLK / (baudRate * 16)) - 1; // 简化的计算方法,实际应用中需根据官方公式精确计算
// 设置波特率寄存器
USARTx->BRR = div;
}
试着阅读以上代码片段,其中USART1_BASE 宏定义了一个32位数据(注意它只是宏,用一个别名代替了一个式子的操作,这个32位数据到了USART_TypeDef *的位置,就开始转化为从该地址开始的结构体指针类型。不太理解也没有关系,你只需要知道,少了这个强制类型转换,相当于将一个未知数据类型的地址赋值给结构体指针,编译器是不允许的。
以上的代码片段便是以第一种方式来编写寄存器的地址,也是这篇博客推行的写法。
我们本次要介绍的便是 如何完成有关GPIO 代码的编写(都说了是入门)
1---如何新建STM32的寄存器版本工程文件 (Keil版本)
当你下载完STM32F1系列固件包(你用其他的固件包也可以),我们只需要用到 CMSIS里的核心文件,主要包括以下(你可以将其全部归类到一起):
新建完工程后,工程文件设置如下图:
主要添加一下启动文件就可以,图中所示其他文件都是用户后期编写的。
调整编译器魔术棒设置如下:编译器使用版本5
C99模式 不使用GNU拓展
以上这些我不再赘述,不了解的可以私信我。
2---查看存储寄存器地址的头文件 、芯片手册
芯片手册可以从官网获取,或者从一些商城平台获取,例如嘉立创、半岛小芯等。
这里我们以STM32F100XXX的芯片手册为例:
首先,我们需要对芯片的开发流程有个大致开发过程的了解:
先开时钟配置时钟,然后配置外设,然后考虑是否要编写中断函数。
至于手册,一般都是按照基础资源介绍、电源、、复位、时钟等目录一步一步介绍。
这里我们着重先看时钟:
STM32的系统时钟主要是由HSI (高速内部时钟信号)HSE (高速外部时钟信号)PLL 三种时钟源来驱动
芯片手册上通常会根据时钟驱动情况画一张时钟树,能够看到这里还有LSE LSI (低速),例如LSE 可以用来驱动RTC实时时钟 在树图上可以知道如果我们需要驱动什么外设需要开启什么时钟且它的分频情况。注意这个菱形框--RTCSEL[1:0] 是一个时钟源选择位,左边三条横线说明三种时钟源可以作为时钟源。
上面这张目录里展示了RCC下有哪些与时钟相关的寄存器,我们选择其中一个来看:
看一下RCC_CFGR寄存器位的功能,比如最后两位SW[1:0],手册上告知了这是用来切换系统时钟的。这对于我们后面编写代码提供指导意见。
下面我们看头文件是如何布局的:
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;
} RCC_TypeDef;
#define RCC_BASE (AHBPERIPH_BASE + 0x00001000UL)
#define RCC ((RCC_TypeDef *)RCC_BASE)
#define RCC_CFGR_SW_Pos (0U)
#define RCC_CFGR_SW_Msk (0x3UL << RCC_CFGR_SW_Pos)
#define RCC_CFGR_SW RCC_CFGR_SW_Msk
#define RCC_CFGR_SW_0 (0x1UL << RCC_CFGR_SW_Pos)
#define RCC_CFGR_SW_1 (0x2UL << RCC_CFGR_SW_Pos)
头文件里一般包括 数据类型结构体 地址的宏定义 偏移位的宏定义
比如这里 RCC_BASE 定义的其实是一个32位数据(本质上就是地址),到了RCC这里进行了强制类型转换,以前面定义的RCC_BASE为基础,才使得这个RCC指针指向了这么一个地址。
后面关于SW寄存器的宏定义,我们可以看到有0U 这个便是SW位在寄存器中的偏移位(你往回看,SW位在寄存器CFGR位置0), 0X3UL << RCC_CFGR_SW_Pos 的意思是3向高位移动0个位置,3转化为二进制就是0011 向左移动0位 还是0011
因为 这个寄存器是32位的,而SW是位置0,那如果是位置3 ,需要赋值的话就偏移3位,这样如果要给寄存器赋值11的话,是不是就可以直接将 RCC_CFGR_SW_Msk 直接赋值给寄存器CFGR
3---代码简单的编写
相对HAL库开发来说,浪费你更多时间的是阅读芯片手册,一般的老工程师可能也推荐你这样干。
虽然STM32可以使用CUBEMX进行快速开发,但是如果你用的是其他类型的芯片,你还是得像这样进行开发。
这里提一嘴你的代码编写风格,我个人喜欢注释全一点,函数名称采用容易看懂的单词进行组装(首字母大写)以前我的导工还喜欢单独成三行给这个函数注明日期、参数、功能。
这里使用-> 是根据结构体指针里面的成员来的,如果是结构体,用的是点. 如果是结构体指针,用的就是 -> 例如RCC->CR 指的就是RCC里的CR寄存器,RCC是结构体指针。
这里使用 |= (或等 A |= B-----> A = A | B)是因为寄存器通常复位都是0,但并不全都是0,所以如果有1,尽量要保证1还在,所以采用或运算。当然你也可以一次性就把寄存器写完。
像写GPIOB->CRL = 0x111111111 ,这样直接写也可以,但不是很显著,你也可以仿照宏定义自己写偏移位。
编译下载烧录后,在开发板上你就可以看见两个亮起的灯。
4---总结
如果你还在新手保护期,希望这篇博客能对你有一些指导意见,也希望你能在嵌入式的道路上渐行渐远,而不是浅尝辄止。
事实上,你可以相信,只要你有足够的数学理解能力,哪怕到了后期上OS系统,你也是能够驾驭的。嵌入式最考验人的便是调试代码的煎熬,有时候工作一次性调试几千行代码,用起来很多不同的外设,很难说一次性就完成。