单片机与嵌入式
51单片机的寻址方式
51单片机的汇编指令系统
51汇编语言的伪指令
51汇编语言程序结构与程序示例
Keil C51程序设计
Keil C51的库函数
PIC单片机的基本结构
PIC单片机的汇编语言指令
PIC单片机的C语言编程
ATmega16单片机基本结构
ATmega16单片机的汇编语言
ATmega128单片机的结构
STM32单片机基础
使用STM32CubeMX开发STM32单片机
uC/OS-II嵌入式操作系统
uC/OS-II在STM32F10xx上的移植
FreeRTOS系统介绍
Linux系统介绍
Linux系统编程
嵌入式Linux编程
STM32是ST公司开发的基于ARM Cortex-M系列的32位单片机,推出后广受欢迎,在市场广泛使用。
1.STM32单片机的命名:
STM32 |
F |
子系列 |
引脚数 |
闪存容量 |
封装 |
温度范围 |
STM32:基于ARM Cortex-M的32位微控制器
F:通用系列,其他有A--Automotive,L--Ultra-low power,T--Touch sensing,W--Wireless
子系列:101--基本型,102--USB基本型,103--增强型,105/107--互联型
引脚数:D--14pin,K--32pin,T--36pin,S--44pin,C--48pin,R--64pin,V--100pin,Z--144pin
闪存容量:4--16kB,6--32kB,8--64kB,B--128kB,C--256kB,D--384kB,E--512kB,G--1024kB
封装:B--DIP,M--SO,P--TSSOP,H--BGA,T--LQFP,U--VFQFPN,Y--WLCSP64
温度范围:6--温度范围-40~85℃,7--温度范围-40~105℃
2.STM32芯片内部结构:
1)总线:
芯片内核CPU和外设之间通过各种总线连接,常用有4种总线:
⑴Icode总线:I表示Instruction,即指令
程序编译后是一条条指令,存放在Flash中,内核要读取这些指令就必须通过ICode总线,是用来取指的。
⑵Dcode总线:D表示Data,即数据,用来取数据的。
程序中,数据有常量和变量2种,常量在C语言中用const修饰,放在内部Flash中;变量都放置在内部SRAM中。
⑶System总线:主要用于访问外设的寄存器
读写寄存器是通过这条总线来完成的。
⑷DMA总线:
DMA总线主要用来产生数据,这些数据可以在外设的数据寄存器中,也可以在SRAM中,或内部Flash中。因为数据可以被DCode总线和DMA总线访问,所以为了避免访问冲突,取数据时需要经过一个总线矩阵来仲裁,决定哪条总线取数。
2)被动单元:
⑴内部Flash:
内部的闪存存储器即内部Flash,是存放程序的地方,内核通过ICode总线来取指令。
⑵内部SRAM:
程序的变量、堆栈等都基于内部的SRAM,内核通过DCode总线来访问SRAM。
⑶FSMC:
FSMC是Flexible static memory controller的缩写,即灵活的静态存储器控制器。通过FSMC可以扩展内存,如外部的SRAM、NAND Flash和NOR Flash,不能扩展动态内存,如SDRAM。
⑷AHB到APB桥:
从AHB总线延伸出来2条APB1和APB2总线,上面挂着各种外设,如GPIO、串口、I2C、SPI等。
3)存储器映射:
Flash、RAM、FSMC和AHB到APB的桥等部件共同排列在一个4GB的地址空间内。存储器的地址是由芯片厂商或用户分配,给存储器分配地址称为存储器映射。
4GB地址空间中,ARM已经平均分成了8个块,每块512MB,每个块的用途见表:
序号 |
用途 |
地址范围 |
Block0 |
Code |
0x0000 0000~0x1FFF FFFF |
Block1 |
SRAM |
0x2000 0000~0x3FFF FFFF |
Block2 |
片上外设 |
0x4000 0000~0x5FFF FFFF |
Block3 |
FSMC的bank1~bank2 |
0x6000 0000~0x7FFF FFFF |
Block4 |
FSMC的bank3~bank4 |
0x8000 0000~0x9FFF FFFF |
Block5 |
FSMC寄存器 |
0xA000 0000~0xBFFF FFFF |
Block6 |
没有使用 |
0xC000 0000~0xDFFF FFFF |
Block7 |
Cortex-M3内部外设 |
0xE000 0000~0xFFFF FFFF |
芯片厂商在每个块的范围内设计各具特色的外设,但一般只使用了其中一部分。
其中的8个块,有3个块很重要,Block1设计成内部Flash,Block1设计成内部RAM,Block2设计成片上外设。这3个块内也有具体的功能划分。
⑴Block0内部功能划分:
Block0主要用于设计片上Flash,最大512MB。内部区域的功能划分为:
地址范围 |
用途 |
0x0000 0000~0x0007 FFFF |
取决于Boot引脚,为Flash、系统存储器、SRAM的别名 |
0x0008 0000~0x07FF FFFF |
预留 |
0x0800 0000~0x0807 FFFF |
Flash,用户程序放置的位置 |
0x0808 0000~0x1FFF EFFF |
预留 |
0x1FFF F000~0x1FFF F7FF |
系统存储器,是ST出厂时的ISP自举程序BootLoader |
0x1FFF F800~0x1FFF F80F |
选项字节,用于配置读写保护、BOR级别、看门狗等 |
0x1FFE C008~0x1FFF FFFF |
预留 |
⑵Block1内部区域功能划分:
Block1用于设计片内的SRAM,对内部为64kB的SRAM的芯片,内部功能划分为:
地址范围 |
用途 |
0x2000 0000~0x2000 FFFF |
SRAM 64kB |
0x2001 0000~0x3FFF FFFF |
预留 |
⑶Block2内部区域功能划分:
Block2用于设计片内的外设,根据外设的速度不同,Block2分成了APB和AHB两部分,其中APB又分为APB1和APB2:
地址范围 |
用途 |
0x4000 0000~0x4000 77FF |
APB1总线外设 |
0x4001 0000~0x4001 3FFF |
APB2总线外设 |
0x4001 8000~0x5003 FFFF |
AHB总线外设 |
4)寄存器映射:
存储器Block2区域,设计的是片上外设,以4字节为一个单元,每单元32位,每一个单元对应不同的功能。可以找到每个单元的起始地址,通过C语言指针来操作相关单元,通过控制这些单元就可以驱动外设工作。但通过地址来访问不容易记忆,一般以功能为每个单元取一个别名,这个别名就是寄存器,这个给已经分配了地址的特定功能内存单元取别名的过程就是寄存器映射。
比如GPIOB端口的输出数据寄存器ODR地址为0x40010C0C,有32位,低16位有效,对应16个外部IO,写0/1对应IO输出低/高电平。通过C语言指针让GPIOB的16个端口输出高电平的代码为:
*(unsigned int*)(0x4001 0C0C)=0xFFFF;
对编译器来说,0x4001 0C0C这个寄存器地址只是一个立即数,如果要成为一个指针要进行强制类型转换,即(unsigned int*)0x4001 0C0C,然后再对这个指针进行*操作。
通过绝对地址访问内存单元难以记忆,容易出错,一般通过寄存器方式来操作:
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
*GPIOB_ODR=0xFF;
为了操作方便,还可以把指针操作“*”定义到寄存器别名内:
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
*GPIOB_ODR=0xFF;
5)STM32的外设地址映射:
片上外设分为3条总线,根据不同的外设速度挂在不同总线上。APB1挂载低速外设,APB2和AHB挂载高速外设,相应总线的最低地址为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址,其中APB1总线的地址最低,也是外设基地址。
⑴总线基地址:
总线名称 |
总线基地址 |
相对外设基地址的偏移 |
APB1 |
0x4000 0000 |
0x0 |
APB2 |
0x4001 0000 |
0x0001 0000 |
AHB |
0x4001 8000 |
0x0001 8000 |
相对外设基地址的偏移即该总线地址与外设基地址0x4000 0000的差值。
⑵外设基地址:
总线上挂载各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为XX外设基地址,也称外设的边界地址,详细地址见STM32相关器件的参考手册。其中STM32F10x的GPIO的基地址为:
外设名称 |
外设基地址 |
相对APB2总线的地址偏移 |
GPIOA |
0x4001 0800 |
0x0000 0800 |
GPIOB |
0x4001 0C00 |
0x0000 0C00 |
GPIOC |
0x4001 1000 |
0x0000 1000 |
GPIOD |
0x4001 1400 |
0x0000 1400 |
GPIOE |
0x4001 1800 |
0x0000 1800 |
GPIOF |
0x4001 1C00 |
0x0000 1C00 |
GPIOG |
0x4001 2000 |
0x0000 2000 |
GPIO属于高速的外设,挂载到APB2总线上。
⑶外部寄存器:
在某个外设的地址范围内,分布着的就是该外设的寄存器。比如GPIO就有多个寄存器,每个寄存器为32位,占4字节,具有特殊功能,在该外设的基地址上按顺序排列,寄存器的位置以相对于该外设基地址的偏移地址来描述。GPIOB端口的寄存器地址列表为:
寄存器名称 |
寄存器地址 |
相对GPIOB基址的偏移 |
GPIOB_CRL |
0x4001 0C00 |
0x00 |
GPIOB_CRH |
0x4001 0C04 |
0x04 |
GPIOB_IDR |
0x4001 0C08 |
0x08 |
GPIOB_ODR |
0x4001 0C0C |
0x0C |
GPIOB_BSRR |
0x4001 0C10 |
0x10 |
GPIOB_BRR |
0x4001 0C14 |
0x14 |
GPIOB_LCKR |
0x4001 0C18 |
0x18 |
每种寄存器的含义可参考相关器件的参考手册,编程时需要仔细查阅外设的寄存器说明。
⑷GPIOx_BSRR寄存器说明:
GPIOx_BSRR是GPIO端口的置位/复位寄存器,结构为:
BR15 |
BR14 |
BR13 |
BR12 |
BR11 |
BR10 |
BR9 |
BR8 |
BR7 |
BR6 |
BR5 |
BR4 |
BR3 |
BR2 |
BR1 |
BR0 |
BS15 |
BS14 |
BS13 |
BS12 |
BS11 |
BS10 |
BS9 |
BS8 |
BS7 |
BS6 |
BS5 |
BS4 |
BS3 |
BS2 |
BS1 |
BS0 |
·位31:16为BRy,清除端口x的位y,y=0~15,这些位只能写入并只能以字(16位)的形式操作。0--对应的ODRy位不影响;1--清除对应的ODRy位为0。
·位15:0为BRy,设置端口x的位y,y=0~15,这些位只能写入并只能以字(16位)的形式操作。0--对应的ODRy位不影响;1--设置对应的ODRy位为1。
如果同时设置了Bsy和BRy的对应位,BSy位起作用。
6)C语言对寄存器的封装:
⑴封装总线和外设基地址:
在编程上,为了方便理解和记忆,把总线基地址和外设地址都使用宏定义,使用寄存器名为宏名,示例:
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE+0x00010000)
#define AHBPERIPH_BASE (PERIPH_BASE+0x00020000)
#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)
#define GPIOB_CRL (GPIOB_BASE+0x00)
#define GPIOB_CRH (GPIOB_BASE+0x04)
#define GPIOB_IDR (GPIOB_BASE+0x08)
#define GPIOB_ODR (GPIOB_BASE+0x0C)
#define GPIOB_BSRR (GPIOB_BASE+0x10)
#define GPIOB_BRR (GPIOB_BASE+0x14)
#define GPIOB_LCKR (GPIOB_BASE+0x18)
编程时,通过基址加上相应地址偏移,即可得到特定寄存器的地址,然后可以用指针读写:
*(unsigned int *)GPIOB_BSRR=(0x01<<(16+0)); //BSRR寄存器的BR0置1,使输出低电平
*(unsigned int *)GPIOB_BSRR=0x01<<0; //BSRR寄存器的BS0置1,使输出高电平
unsigned int temp;
temp=*(unsigned int *)GPIOB_IDR; //读IDR寄存器,即读取GPIOB端口所有引脚的电平
⑵封装寄存器列表:
为了更方便地访问寄存器,使用C语言的结构体对寄存器进行封装:
typedef unsigned int uint32_t; //无符号32位变量
typedef undigned short int uint16_t; //无符号16位变量
typedef struct{
uint32_t CRL; //GPIO端口配置低寄存器,地址偏移0x00
uint32_t CRH; //GPIO端口配置高寄存器,地址偏移0x04
uint32_t IDR; //GPIO端口数据输入寄存器,地址偏移0x08
uint32_t ODR; //GPIO端口数据输出寄存器,地址偏移0x0C
uint32_t BSSR; //GPIO端口位设置/清除寄存器,地址偏移0x10
uint32_t BRR; //GPIO端口位清除寄存器,地址偏移0x14
uint16_t LCKR; //GPIO端口配置锁定寄存器,地址偏移0x18
}GPIO_TypeDef;
代码声明了名为GPIO_TypeDef的结构体类型,包括7个变量,变量名对应寄存器名,结构体内变量的存储空间是连续的,其中32位的变量占用4字节,16位变量占用2字节。定义好后,就可以使用结构体形式访问寄存器:
GPIO_TypeDef * GPIOx; //定义结构体指针
GPIOx=GPIOB_BASE; //把指针地址设置为宏GPIOB_BASE地址
GPIOx->IDR=0xFFFF;
GPIOx->ODR=0xFFFF;
uint32_t temp;
temp=GPIOx->IDR; //读取GPIOB_IDR寄存器的值到变量temp中
代码中先定义一个结构体指针GPIOx,并让指针指向地址GPIOB_BASE,然后根据C语言访问结构体的语法读写寄存器。
可以直接使用宏来定义GPIO_TypeDef类型的指针,而且指针指向各个GPIO端口的首地址,使用时直接用宏来访问寄存器:
#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)
⑶修改寄存器的位操作方法:
使用C语言对寄存器赋值时,常常要求只修改该寄存器的某几位的值,而其他值不变,这就需要使用C语言的位操作方法。
◆一位清零:
unsigned char a=0x9f;
a &= ~(1<<2);
代码中先把1左移2位,(1<<2)得二进制数0000 0100b,按位取反得1111 1011b,所得的数与a按位与“&”运算,得到的值就是a的bit2被清零,其他位不变。
◆连续几位清零:
把数值中bit0、bit1为第0组,bit2、bit3为第1组,bit4、bit5为第2组,bit6、bit7为第3组。如果要对第1组的bit2、bit3清零:
a &= ~(3<<2*1);
代码中先把3左移2位,(3<<2*1)得二进制数0000 1100b,按位取反得1111 0011b,与a按位与操作,第1组的bit2、bit3被清零,而其他位不变。如果对第2组的bit4、bit5清零:
a &= ~(3<<2*2);
也就是最后面那个数字为组编号。
◆对几位赋值:
经过清零操作,就可以方便对其中几位写入需要的数值了。
a |= (1<<2*2);
代码中设置了第2组的值,其他组不变。
◆取反:
当需要对其中某个位取反,可以直接进行如下操作:
a ^= (1<<6);
代码中对bit6取反,其他位不变。