一.32系统框图介绍
芯片(这里指内核,或者叫
CPU
)和外设之间通过各种总线连接,其中驱动单元有
4 个,被动单元也有 4
个
。为了方便理解,我们都可以把驱动单元理解成是 CPU 部分,被动单元都理解成外设。下面简单介绍下驱动单元和被动单元的各个部件。
1. ICode
总线
ICode
中的
I
表示
Instruction
,即指令。我们写好的程序编译之后都是一条条指令,存
放在
FLASH
中,内核要读取这些指令来执行程序就必须通过
ICode
总线,它几乎每时每刻
都需要被使用,它是专门用来取指的。
2.
驱动单元
DCode 总线 DCode 中的 D 表示
Data
,即数据,那说明这条总线是用来取数的。我们在写程序的时 候,数据有常量和变量两种,常量就是固定不变的,用 C
语言中的
const
关键字修饰,是
放到内部的
FLASH
当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的
SRAM
。因为数据可以被
Dcode
总线和
DMA
总线访问,所以为了避免访问冲突,在取数
的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
system系统总线系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通 过这根系统总线来完成的。
DMA
总线DMA 总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在 SRAM,可以在内部的
FLASH
。因为数据可以被
Dcode
总线和
DMA
总线访问,所以为了
避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
3.
被动单元
内部的闪存存储器 FLASH
,我们编写好的程序就放在这个地方。内核通过
ICode
总线来取里面的指令。
内部的
SRAM
,即我们通常说的
RAM
,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过
DCode
总线来访问它。
FSMC 叫灵活的静态的存储器控制器, 是 STM32F10xx
中一个很有特色的外设,通过
FSMC
,我们可以扩展内存,如外部的 SRAM,
NANDFLASH
和
NORFLASH
。但有一点我们要注意的是,
FSMC
只能扩展静态的内存,即名称里面的 S
:
static
,不能是动态的内存,比如
SDRAM
就不能扩展。
AHB
到
APB
的桥 从 AHB
总线延伸出来的两条
APB2
和
APB1
总线,上面挂载着
STM32
各种各样的特 色外设。我们经常说的 GPIO
、串口、
I2C
、
SPI
这些外设就挂载在这两条总线上,这个是 我们学习 STM32
的重点,就是要学会编程这些外设去驱动外部的各种设备。
二.32存储器映射介绍
在这
4GB
的地址空间中,
ARM
已经粗线条的平均分成了
8
个块,每块
512MB
,每个
块也都规定了用途,具体分类见下表
在这
8
个
Block
里面,有
3
个块非常重要,也是我们最关心的三个块。
Block0
用来设
计成内部
FLASH
,
Block1
用来设计成内部
RAM
,
Block2
用来设计成片上的外设
三.寄存器映射
通过绝对地址访问内存单元:
版本1:
*(
unsigned int
*)(0x4001 0C0C) = 0xFFFF;
/ GPIOB
端口全部输出 高电平
0x4001 0C0C
在我们看来是
GPIOB
端口
ODR
的地址,但是在编译器看来,这只是一
个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,
把它转换成指针,即
(unsigned int *)0x4001 0C0C
,然后再对这个指针进行
*
操作。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存
器的方式来操作,具体见版本2
。
版本2:通过寄存器别名方式访问内存单元
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
// GPIOB
端口全部输出 高电平
* GPIOB_ODR = 0xFF;
版本3:为了方便操作,我们干脆把指针操作“
*
”也定义到寄存器别名里面,具体见代码
6-3
。
代码
6-3
通过寄存器别名访问内存单元
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
// GPIOB
端口全部输出 高电平
GPIOB_ODR = 0xFF;
通过相对偏移地址访问内存单元:
这里我们以“
GPIO
端口置位
/复位寄存器”为例,教大家如何理解寄存器的说明,
①名称
寄存器说明中首先列出了该寄存器中的名称,“
(GPIOx_BSRR)(x=A…E)
”这段的意
思是该寄存器名为“
GPIOx_BSRR
”其中的“
x
”可以为
A-E
,也就是说这个寄存器说明适
用于
GPIOA
、
GPIOB
至
GPIOE
,这些
GPIO
端口都有这样的一个寄存器。
②偏移地址
偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是
0x18
,
从参考手册中我们可以查到
GPIOA
外设的基地址为
0x4001 0800
,我们就可以算出
GPIOA
的这个
GPIOA_BSRR
寄存器的地址为:
0x4001 0800+0x18
;同理,由于
GPIOB
的
外设基地址为
0x4001 0C00
,可算出
GPIOB_BSRR
寄存器的地址为:
0x4001 0C00+0x18
。
其他
GPIO
端口以此类推即可。
③寄存器位表
紧接着的是本寄存器的位表,表中列出它的
0-31
位的名称及权限。表上方的数字为位
编号,中间为位名称,最下方为读写权限,其中
w
表示只写,
r
表示只读,
rw
表示可读写。
本寄存器中的位权限都是
w
,所以只能写,如果读本寄存器,是无法保证读取到它真正内
容的。而有的寄存器位只读,一般是用于表示
STM32
外设的某种工作状态的,由
STM32
硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。
④位功能说明
位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本
寄存器中有两种寄存器位,分别为
BRy
及
BSy
,其中的
y
数值可以是
0-15
,这里的
0-15
表示端口的引脚号,如
BR0
、
BS0
用于控制
GPIOx
的第
0
个引脚,若
x
表示
GPIOA
,那就
是控制
GPIOA
的第
0
引脚,而
BR1
、
BS1
就是控制
GPIOA
第
1
个引脚。
其中
BRy
引脚的说明是“
0
:不会对相应的
ODRx
位执行任何操作;
1
:对相应
ODRx
位进行复位”。这里的“复位”是将该位设置为
0
的意思,而“置位”表示将该位设置为
1
;说明中的
ODRx
是另一个寄存器的寄存器位,我们只需要知道
ODRx
位为
1
的时候,
对应的引脚
x
输出高电平,为
0
的时候对应的引脚输出低电平即可
(
感兴趣的读者可以查询
该寄存器
GPIOx_ODR
的说明了解
)
。所以,如果对
BR0
写入“
1
”的话,那么
GPIOx
的第
0
个引脚就会输出“低电平”,但是对
BR0
写入“
0
”的话,却不会影响
ODR0
位,所以引
脚电平不会改变。要想该引脚输出“高电平”,就需要对“
BS0
”位写入“
1
”,寄存器位
BSy
与
BRy
是相反的操作。
四.C 语言对寄存器的封装
1. 封装总线和外设基地址
1
/*
外设基地址
*/
2
#define PERIPH_BASE ((unsigned int)0x40000000)
3
4
/*
总线基地址
*/
5
#define APB1PERIPH_BASE PERIPH_BASE
6
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
7
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)
8
9
10
/* GPIO
外设基地址
*/
11
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
12
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
13
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
14
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
15
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
16
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
17
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
18
19
20
/*
寄存器基地址,以
GPIOB
为例
*/
21
#define GPIOB_CRL (GPIOB_BASE+0x00)
22
#define GPIOB_CRH (GPIOB_BASE+0x04)
23
#define GPIOB_IDR (GPIOB_BASE+0x08)
24
#define GPIOB_ODR (GPIOB_BASE+0x0C)
25
#define GPIOB_BSRR (GPIOB_BASE+0x10)
26
#define GPIOB_BRR (GPIOB_BASE+0x14)
27
#define GPIOB_LCKR (GPIOB_BASE+0x18)
使用指针控制
BSRR
寄存器
1
/*
控制
GPIOB
引脚
0
输出低电平
(BSRR
寄存器的
BR0
置
1) */
2
*(
unsigned int
*)GPIOB_BSRR = (0x01<<(16+0));
3
4
/*
控制
GPIOB
引脚
0
输出高电平
(BSRR
寄存器的
BS0
置
1) */
5
*(
unsigned int
*)GPIOB_BSRR = 0x01<<0;
6
7
unsigned int
temp;
8
/*
读取
GPIOB
端口所有引脚的电平
(
读
IDR
寄存器
) */
9
temp = *(
unsigned int
*)GPIOB_IDR;
该代码使用
(unsigned int *)
把
GPIOB_BSRR
宏的数值强制转换成了地址,然后再用
“
*
”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也
是用取指针操作,把寄存器中的数据取到变量里,从而获取
STM32
外设的状态。
2.
封装寄存器列表
用上面的方法去定义地址,还是稍显繁琐为了更方便地访问寄存器,我们引入
C
语言中的结构体
语法对寄存器进行封装.
1 typedef
unsigned int uint32_t
;
/*
无符号
32
位变量
*/
2 typedef
unsigned short int uint16_t
;
/*
无符号
16
位变量
*/
3
4
/* GPIO
寄存器列表
*/
5 typedef
struct
{
6
uint32_t
CRL;
/*GPIO
端口配置低寄存器
地址偏移
: 0x00 */
7
uint32_t
CRH;
/*GPIO
端口配置高寄存器
地址偏移
: 0x04 */
8
uint32_t
IDR;
/*GPIO
数据输入寄存器
地址偏移
: 0x08 */
9
uint32_t
ODR;
/*GPIO
数据输出寄存器
地址偏移
: 0x0C */
10
uint32_t
BSRR;
/*GPIO
位设置
/
清除寄存器
地址偏移
: 0x10 */
11
uint32_t
BRR;
/*GPIO
端口位清除寄存器
地址偏移
: 0x14 */
12
uint16_t
LCKR;
/*GPIO
端口配置锁定寄存器
地址偏移
: 0x18 */
13
} GPIO_TypeDef;
这段代码用
typedef
关键字声明了名为
GPIO_TypeDef
的结构体类型,结构体内有
7
个
成员变量,变量名正好对应寄存器的名字。
C
语言的语法规定,结构体内变量的存储空间
是连续的,其中
32
位的变量占用
4
个字节,
16
位的变量占用
2
个字节,具体见图
也就是说,我们定义的这个
GPIO_TypeDef
,假如这个结构体的首地址为
0x4001
0C00
(这也是第一个成员变量
CRL
的地址), 那么结构体中第二个成员变量
CRH
的地址
即为
0x4001 0C00 +0x04
,加上的这个
0x04
,正是代表
CRL
所占用的
4
个字节地址的偏
移量,其它成员变量相对于结构体首地址的偏移.
这样的地址偏移与
STM32 GPIO
外设定义的寄存器地址偏移一一对应,只要给结构体
设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存
器
。
代码
通过结构体指针访问寄存器
1
GPIO_TypeDef * GPIOx;
//
定义一个
GPIO_TypeDef
型结构体指针
GPIOx
2
GPIOx = GPIOB_BASE;
//
把指针地址设置为宏
GPIOH_BASE
地址
3
GPIOx->IDR = 0xFFFF;
4
GPIOx->ODR = 0xFFFF;
5
6
7
uint32_t
temp;
8
temp = GPIOx->IDR;
//
读取
GPIOB_IDR
寄存器的值到变量
temp
中
这段代码先用
GPIO_TypeDef
类型定义一个结构体指针
GPIOx
,并让指针指向地址
GPIOB_BASE(0x4001 0C00)
,使用地址确定下来,然后根据
C
语言访问结构体的语法,用
GPIOx->ODR
及
GPIOx->IDR
等方式读写寄存器。
最后,我们更进一步,直接使用宏定义好
GPIO_TypeDef
类型的指针,而且指针指向
各个
GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可,定义好
GPIO
端口首地址址针
1
/*
使用
GPIO_TypeDef
把地址强制转换成指针
*/
2
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
3
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
4
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
5
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
6
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
7
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
8
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
9
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
10
11
12
13
/*
使用定义好的宏直接访问
*/
14
/*
访问
GPIOB
端口的寄存器
*/
15
GPIOB->BSRR = 0xFFFF;
//
通过指针访问并修改
GPIOB_BSRR
寄存器
16
GPIOB->CRL = 0xFFFF;
//
修改
GPIOB_CRL
寄存器
17
GPIOB->ODR =0xFFFF;
//
修改
GPIOB_ODR
寄存器
18
19
uint32_t
temp;
20
temp = GPIOB->IDR;
//
读取
GPIOB_IDR
寄存器的值到变量
temp
中
21
22
/*
访问
GPIOA
端口的寄存器
*/
23
GPIOA->BSRR = 0xFFFF;
24
GPIOA->CRL = 0xFFFF;
25
GPIOA->ODR =0xFFFF;
26
27
uint32_t
temp;
28
temp = GPIOA->IDR;
//
读取
GPIOA_IDR
寄存器的值到变量
temp
中
这里我们仅是以
GPIO
这个外设为例,给大家讲解了
C
语言对寄存器的封装。以此类
推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完
成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然
五 修改寄存器的位操作方法
使用
C
语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的
寄存器位不变,这个时候我们就需要用到
C
语言的位操作方法了。
1.
把变量的某位清零
此处我们以变量
a
代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量
a
的某一位清零,且其它位不变
代码
对某位清零
1
//
定义一个变量
a = 1001 1111 b (
二进制数
)
2
unsigned char
a = 0x9f;
3
4
//
对
bit2
清零
5
6
a &= ~(1<<2);
7
8
//
括号中的
1
左移两位,
(1<<2)
得二进制数:
0000 0100 b
9
//
按位取反,
~(1<<2)
得
1111 1011 b
10
//
假如
a
中原来的值为二进制数:
a = 1001 1111 b
11
//
所得的数与
a
作
”
位与
&”
运算,
a = (1001 1111 b)&(1111 1011 b),
12
//
经过运算后,
a
的值
a=1001 1011 b
13
// a
的
bit2
位被被零,而其它位不变。
2.
把变量的某几个连续位清零
由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存
器的某几个连续位清零,且其它位不变,对某几个连续位清零
1
2
//
若把
a
中的二进制位分成
2
个一组
3
//
即
bit0
、
bit1
为第
0
组,
bit2
、
bit3
为第
1
组,
4
// bit4
、
bit5
为第
2
组,
bit6
、
bit7
为第
3
组
5
//
要对第
1
组的
bit2
、
bit3
清零
6
7
a &= ~(3<<2*1);
8
9
//
括号中的
3
左移两位,
(3<<2*1)
得二进制数:
0000 1100 b
10
//
按位取反,
~(3<<2*1)
得
1111 0011 b
11
//
假如
a
中原来的值为二进制数:
a = 1001 1111 b
12
//
所得的数与
a
作
”
位与
&”
运算,
a = (1001 1111 b)&(1111 0011 b),
13
//
经过运算后,
a
的值
a=1001 0011 b
14
// a
的第
1
组的
bit2
、
bit3
被清零,而其它位不变。
15
16
//
上述
(~(3<<2*1))
中的
(1)
即为组编号
;
如清零第
3
组
bit6
、
bit7
此处应为
3
17
//
括号中的
(2)
为每组的位数,每组有
2
个二进制位
;
若分成
4
个一组,此处即为
4
18
//
括号中的
(3)
是组内所有位都为
1
时的值
;
若分成
4
个一组,此处即为二进制数
“1111 b”
19
20
//
例如对第
2
组
bit4
、
bit5
清零
21
a &= ~(3<<2*2);
3.
对变量的某几位进行赋值。
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,
且其它位不变
,这时候写入的数值一般就是需要设置寄存器的位参数。
对某几位进行赋值
1
//a = 1000 0011 b
2
//
此时对清零后的第
2
组
bit4
、
bit5
设置成二进制数
“01 b ”
3
4
a |= (1<<2*2);
5
//a = 1001 0011 b
,成功设置了第
2
组的值,其它组不变
4.
对变量的某位取反
某些情况下,我们需要对寄存器的某个位进行取反操作,即
1
变
0
,
0
变
1
,这可以直
接用如下操作,其它位不变,
对某位进行取反操作
1
//a = 1001 0011 b
2
//
把
bit6
取反,其它位不变
3
4
a ^=(1<<6);
5
//a = 1101 0011 b