知识引导
怎么学习STM32?
学习stm32的本质就是去操作它的各个外设,虽然各种型号的单片机寄存器定义库函数不一样,但是对外设的基本操作是一样的。如串口,spi、i2c这些外设的通信协议是不变的,操作这些外设的过程就是通过单片机去实现软件层次的通信协议。
什么是存储器的映射?
对单片机的操作是通过向各个寄存器里写数据实现的,这些寄存器就是一个个的存储单元,这些存储单元,就像教学楼的一个个教室一样,我们要想在无数的教室中找到我们要上课的教室,最好的办法就是给所有的教室起一个名字,比如“初三一班,A410等等”。同理我们要从无数的寄存器中找到要操作的寄存器,就需要给寄存器起个名字,这就是存储器的映射。
知识总览
Cortex-M4存储器映射
图1,是M4内核的推荐存储器映射,将4GB的存储空间分为多个区域。
![](https://img-blog.csdnimg.cn/0e15f93c893c464ab03bd764e38b7bfb.png)
32位单片机的寻址范围是4GB,这个4GB是按字节编址算出来的。一个字节是8位,实际上在stm32中是以四字节(32位)对齐的,这个从地址的偏移量上来看就知道了(如下图2所示),stm32的地址偏移每次是四。
![](https://img-blog.csdnimg.cn/cf4d1c7856394b3980d5fdab1256924d.png)
STM32F407的存储器映射
![](https://img-blog.csdnimg.cn/20595345b90a493b8ca9794c78611ceb.png)
如图3所示,stm32f407的存储器被分为八个块,每个块0.5GB。
如何访问寄存器
以GPIOA的寄存器组为例:
已知GPIOA的起始地址为0x40020000
各寄存器的偏移地址如下:
MODER; /*Address offset: 0x00 */
OTYPER; /*Address offset: 0x04 */
OSPEEDR; /*Address offset: 0x08 */
PUPDR; /*Address offset: 0x0C */
IDR; /*Address offset: 0x10 */
ODR; /*Address offset: 0x14 */第二
BSRR; /*Address offset: 0x18 */
LCKR; /*Address offset: 0x1C */
第一种方式:直接操作寄存器
优点:编译后代码所占空间小,相比于库函数的方式代码运行速度快。
这种方式的缺点很明显。stm32的寄存器太多了,而且后期代码不利于维护。所以我们一般不使用直接操作寄存器的方式。
#define GPIOA_BASE ( (unsigned int ) 0x40020000 )
#define GPIOA_ODR ( GPIOA_BASE + 0x14 )
//读操作
val = *(unsigned int *) GPIOA_ODR ;
//写操作
*(unsigned int *) GPIOA_ODR = val ;
//改进
#define GPIOA_ODR ( *(unsigned int *) ( GPIOA_BASE + 0x14 ))
val = GPIOA_ODR ; //读
GPIOA_ODR = val ; //写
unsingned int *是强制类型转换;*(unsingned int *)是操作指针所对应的地址(也就是指针所指向的存储区域)。
第二种方式:使用库函数
用上面的方法去定义地址,还是稍显繁琐、根据我们每一类外设对应的寄存器组地址都是连续增长的特点,我们引入 C 语言中的结构体语法对寄存器进行封装
typedef struct {
uint32_t MODER; /*Address offset: 0x00 */
uint32_t OTYPER; /*Address offset: 0x04 */
uint32_t OSPEEDR; /*Address offset: 0x08 */
uint32_t PUPDR; /*Address offset: 0x0C */
uint32_t IDR; /*Address offset: 0x10 */
uint32_t ODR; /*Address offset: 0x14 */
uint32_t BSRR; /*Address offset: 0x18 */
uint32_t LCKR; /*Address offset: 0x1C */
} GPIO_TypeDef;
#define GPIOA_BASE ( (unsigned int ) 0x40020000 )
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
//对结构体成员的访问举例
GPIOA->MODER = 0x20 ;
GPIOA->OSPEEDR = 0x16 ;
每一组寄存器都是在基地址的基础上以四个字节为增量连续增长的。正好可以定义为unsigned int类型的。
结构体的存储空间是连续的就像数组一样,只需要知道头数据的地址,就可以算出其余数据的地址,这也就是为什么数组的名字可以当做指针使用。
#define GPIOA_BASE ( (unsigned int ) 0x40020000 )
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
这里指明了GPIOA的基地址以上两句就等价于下面的语句
#define GPIOA ((GPIO_TypeDef *) ( (unsigned int ) 0x40020000 ))
这样就指明了结构体中的第一个数据的地址。
补充知识点
结构体指针的使用;
通过结构体指针可以获取结构体成员,一般有两种方式:
第一种方式:
(*pointer).memberName
“.”的优先级高于*,(*pointer)两边的括号不能少。如果去掉括号写作*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就完全不对了。
第二种方式:
pointer->memberName
->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;
这也是->在C 语言中的唯一用途。
我们一般使用第二种箭头的方式来访问结构体成员。
C