1.stm32命名方法

stm32F103VET6举例。
在这里插入图片描述
具体如下:
在这里插入图片描述

2.stm32架构

STM32F103采用的是Cortex-M3内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。 芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如GPIO、USART(串口)、I2C、SPI等都叫做片上外设。
在这里插入图片描述
STM32F10xx系统框图
在这里插入图片描述

ICode总线

ICode中的I表示Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在Flash中,内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,它是专门用来取指的。

四个驱动单元(ARM)

DCode总线

DCode中的D表示Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种, 常量就是固定不变的,用C语言中的const关键字修饰,是放到内部的Flash当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

系统总线

系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。

DMA总线(2个)

DMA总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的FLASH。 因为数据可以被Dcode总线和DMA总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。

四个被动单元(ST)

内部的闪存存储器

内部的闪存存储器即Flash,我们编写好的程序就放在这个地方。内核通过ICode总线来取里面的指令。

内部的SRAM

内部的SRAM,即我们通常说的RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过DCode总线来访问它。

FSMC

FSMC的英文全称是Flexible static memory controller,叫灵活的静态的存储器控制器, 是STM32F10xx中一个很有特色的外设, 通过FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH和NORFLASH。但有一点我们要注意的是,FSMC只能扩展静态的内存, 即名称里面的S:static,不能是动态的内存,比如SDRAM就不能扩展。

AHB到APB的桥

从AHB总线延伸出来的两条APB2和APB1总线,上面挂载着STM32各种各样的特色外设。我们经常说的GPIO、串口、I2C、SPI这些外设就挂载在这两条总线上, 这个是我们学习STM32的重点,就是要学会编程这些外设去驱动外部的各种设备。

3.存储器映射

STM32F10xx系统框图中,被控单元的FLASH,RAM,FSMC和AHB到APB的桥(即片上外设),这些功能部件共同排列在一个4GB的地址空间内。 我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过C语言对它们进行数据的读和写)。
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射, 具体见图存储器映射。如果给存储器再分配一个地址就叫存储器重映射。 在这里插入图片描述
在这4GB的地址空间中,ARM已经粗线条的平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表格存储器功能分类。 每个块的大小都有512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。 在这里插入图片描述
在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部FLASH,Block1用来设计成内部RAM, Block2用来设计成片上的外设,下面我们简单的介绍下这三个Block里面的具体区域的功能划分。

存储器Block0内部区域功能划分

Block0主要用于设计片内的FLASH,STM32F103VET6的FLASH是512KB,属于大容量。要在芯片内部集成更大的FLASH或者SRAM都意味着芯片成本的增加,往往片内集成的FLASH都不会太大, ST能在追求性价比的同时做到512KB,实乃良心之举。存储器Block0内部区域功能划分。
在这里插入图片描述

存储器Block1内部区域功能划分

Block1用于设计片内的SRAM。STM32F103VET6的SRAM都是64KB, 存储器Block1内部区域功能划分。
在这里插入图片描述

存储器Block2内部区域功能划分

Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2, 具体见表格。存储器Block2内部区域功能划分。
在这里插入图片描述

4.寄存器映射

存储器本身没有地址,给存储器分配地址的过程叫存储器映射,在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能, 当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元, 如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名, 这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。

STM32的外设地址映射

片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2和AHB挂载高速外设。 相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。

总线基地址在这里插入图片描述

表格总线基地址的“相对外设基地址偏移”即该总线地址与“片上外设”基地址0x4000 0000的差值。

外设基地址

总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。
这里面我们以GPIO这个外设来讲解外设的基地址,GPIO属于高速的外设 ,挂载到APB2总线上,具体见表格,外设GPIO基地址。
在这里插入图片描述

外设寄存器

在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO外设为例,GPIO是通用输入输出端口的简称, 简单来说就是STM32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED灯的阴极, LED灯的阳极接电源,然后通过STM32控制该引脚的电平,从而实现控制LED灯的亮灭。 GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列, 寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB端口为例,来说明GPIO都有哪些寄存器。
具体见表格GPIOB端口的寄存器地址列表。
在这里插入图片描述

5.C语言对寄存器的封装

在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名。
总线和外设基址宏定义
/* 外设基地址 /
#define PERIPH_BASE ((unsigned int)0x40000000)
/
总线基地址 */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)

/* GPIO外设基地址 */
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)

封装寄存器列表

GPIOA-GPIOE都各有一组功能相同的寄存器,GPIOA_ODR/GPIOB_ODR/GPIOC_ODR等等, 它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装。
在这里插入图片描述
使用结构体对GPIO寄存器组的封装
typedef unsigned int uint32_t; /无符号32位变量/
typedef unsigned short int uint16_t; /无符号16位变量/
/* GPIO寄存器列表 */
typedef struct {
uint32_t CRL; /*GPIO端口配置低寄存器 地址偏移: 0x00 */
uint32_t CRH; /*GPIO端口配置高寄存器 地址偏移: 0x04 */
uint32_t IDR; /*GPIO数据输入寄存器 地址偏移: 0x08 */
uint32_t ODR; /*GPIO数据输出寄存器 地址偏移: 0x0C */
uint32_t BSRR; /*GPIO位设置/清除寄存器 地址偏移: 0x10 */
uint32_t BRR; /*GPIO端口位清除寄存器 地址偏移: 0x14 */
uint16_t LCKR; /*GPIO端口配置锁定寄存器 地址偏移: 0x18 */
} GPIO_TypeDef;

这段代码用typedef 关键字声明了名为GPIO_TypeDef的结构体类型,结构体内有7个 成员变量,变量名正好对应寄存器的名字。 C语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4个字节,16位的变量占用2个字节。
也就是说,我们定义的这个GPIO_TypeDef 假如这个结构体的首地址为0x4001 0C00(这也是第一个成员变量CRL的地址), 那么结构体中第二个成员变量CRH的地址即为0x4001 0C00 +0x04 ,加上的这个0x04,正是代表CRL所占用的4个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。最后,我们更进一步,直接使用宏定义好GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址, 使用时我们直接用该宏访问寄存器即可。

              定义好GPIO端口首地址址针

/使用GPIO_TypeDef把地址强制转换成指针/
#define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)

/使用定义好的宏直接访问/
/访问GPIOB端口的寄存器/
GPIOB->BSRR = 0xFFFF; //通过指针访问并修改GPIOB_BSRR寄存器
GPIOB->CRL = 0xFFFF; //修改GPIOB_CRL寄存器
GPIOB->ODR =0xFFFF; //修改GPIOB_ODR寄存器

uint32_t temp;
temp = GPIOB->IDR; //读取GPIOB_IDR寄存器的值到变量temp中

/访问GPIOA端口的寄存器/
GPIOA->BSRR = 0xFFFF;
GPIOA->CRL = 0xFFFF;
GPIOA->ODR =0xFFFF;

uint32_t temp;
temp = GPIOA->IDR; //读取GPIOA_IDR寄存器的值到变量temp中

这里我们仅是以GPIO这个外设为例,给大家讲解了C语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。

6.修改寄存器的位操作方法

使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到C语言的位操作方法了。

把变量的某位清零

此处我们以变量a代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a的某一位清零,且其它位不变。
对某位清零
//定义一个变量a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;
//对bit2 清零
a &= ~(1<<2);
//括号中的1左移两位,(1<<2)得二进制数:0000 0100 b
//按位取反,~(1<<2)得1111 1011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 1011 b),
//经过运算后,a的值 a=1001 1011 b
// a的bit2 位被清零,而其它位不变。

把变量的某几个连续位清零

由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零, 且其它位不变。

对某几个连续位清零
//若把a中的二进制位分成2个一组
//即bit0、bit1为第0组,bit2、bit3为第1组,
// bit4、bit5为第2组,bit6、bit7为第3组
//要对第1组的bit2、bit3清零

a &= ~(3<<2*1);

//括号中的3左移两位,(3<<21)得二进制数:0000 1100 b
//按位取反,~(3<<2
1)得1111 0011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算,a = (1001 1111 b)&(1111 0011 b),
//经过运算后,a的值 a=1001 0011 b
// a的第1组的bit2、bit3被清零,而其它位不变。

//上述(~(3<<2*1))中的(1)即为组编号;如清零第3组bit6、bit7此处应为3
//括号中的(2)为每组的位数,每组有2个二进制位;若分成4个一组,此处即为4
//括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数“1111 b”

//例如对第2组bit4、bit5清零
a &= ~(3<<2*2);
6.5.3.3. 对变量的某几位进行赋值。
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变 ,这时候写入的数值一般就是需要设置寄存器的位参数。

对某几位进行赋值
//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”

a |= (1<<2*2);
//a = 1001 0011 b,成功设置了第2组的值,其它组不变

对变量的某位取反

某些情况下,我们需要对寄存器的某个位进行取反操作,即 1变0 ,0变1,这可以直接用如下操作,其它位不变, 见 代码清单:寄存器-12 。

代码清单: 对某位进行取反操作
//a = 1001 0011 b
//把bit6取反,其它位不变

a ^=(1<<6);
//a = 1101 0011 b

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐