芯片(这里指内核,或者叫 CPU)和外设之间通过各种总线连接,其中驱动单元有 4个,被动单元也有 4 个。为了方便理解,我们都可以把驱动单元理解成是CPU 部分,被动单元都理解成外设。
芯片架构简图:
系统框图:
1、ICode总线:ICode 中的 I 表示 Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在 FLASH 中,内核要读取这些指令来执行程序就必须通过 ICode 总线,它几乎每时每刻都需要被使用,它是专门用来取指的。
2、驱动单元:
(1)DCode 总线:DCode 中的 D 表示 Data,即数据,那说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种,常量就是固定不变的,用 C 语言中的 const 关键字修饰,是放到内部的 FLASH 当中的,变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
(2)系统总线:系统总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
(3)DMA总线:DMA 总线也主要是用来传输数据,这个数据可以是在某个外设的数据寄存器,可以在SRAM,可以在内部的 FLASH。因为数据可以被 Dcode 总线和 DMA 总线访问,所以为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线在取数。
3、被动单元:(存储器)
(1)内部的闪存存储器:内部的闪存存储器即 FLASH,我们编写好的程序就放在这个地方。内核通过 ICode 总线来取里面的指令。
(2)内部的SRAM:内部的 SRAM,即我们通常说的 RAM,程序的变量,堆栈等的开销都是基于内部的SRAM。内核通过 DCode总线来访问它。
(3)FSMC:FSMC 的英文全称是 Flexible static memory controller,叫灵活的静态的存储器控制器,是 STM32F10xx 中一个很有特色的外设,通过 FSMC,我们可以扩展内存,如外部的SRAM,NANDFLASH 和 NORFLASH。但有一点我们要注意的是,FSMC 只能扩展静态的内存,即名称里面的 S:static,不能是动态的内存,比如 SDRAM就不能扩展。
(4)AHB到APB的桥:从 AHB 总线延伸出来的两条 APB2 和 APB1 总线,上面挂载着 STM32 各种各样的特色外设。我们经常说的 GPIO、串口、I2C、SPI 这些外设就挂载在这两条总线上,这个是我们学习 STM32的重点,就是要学会编程这些外设去驱动外部的各种设备。
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程称为存储器映射,如果再分配一个地址就叫重映射(具体地址分配参考芯片数据手册及中文参考手册)。
存储器功能分类:
给有特定功能的内存单元取一个别名,这个别名就是我们经常说的寄存器,换句话说,寄存器就是一些有特定功能的内存单元。
给已经分配好地址的有特定功能的内存单元起别名的过程就叫寄存器映射。
访问STM32寄存器也就是操作STM32的内存单元,根据C语言指针的特点,可以使用指针来操作STM32的内存单元。以GPIO外设为例
1.首先我们要知道GPIOC挂接在哪个总线上,需要知道其地址,STM32总线地址如下:
2.总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址成为“XX外设基地址”,也叫XX外设的边界地址。这里以GPIO这个外设来讲解外设的基地址,GPIO属于高速的外设,挂载到APB2总线上。
3.在XX外设的地址范围内,分布着的就是该外设的寄存器。GPIO有很多寄存器,每个都有特定的功能。每个寄存器为32位,占4个字节,在该外设的基地址上按照顺序排序,寄存器的位置都以相对该外设基地址的偏移地址来描述。
使用C语言宏定义其端口寄存器地址:
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)
#define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10)
#define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14)
#define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其他的寄存器位不变,这个时候就需要用到C语言的位操作的方法。
1.把变量的某位清零
此处我们以变量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 位被清零,而其他位不变
2.把变量的某几位连续位清零
由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零,且其他位不变。
//若把a中的二进制位分为两个一组
//即bit0、bit1为第0组,bit2、bit3为第1组
// bit4、bit5为第2组,bit6、bit7为第3组
//要对第1组的bit2、bit3清零
a &= ~(3<<2*1);
//括号中的3左移两位,(3<<2*1)的二进制数: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)为每组的位数,每组有两个二进制位;若分成4个一组,此处即为4
//括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数”1111 b“
//例如对第2组bit4、bit5清零
a &= ~(3<<2*2);
3.对变量的某几位进行赋值
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其他位不变。
//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数"01 b"
a |= (1<<2*2);
//a = 1001 0011 b,成功设置了第2组的值,其他组不变
4.对变量的某位取反
某些情况下,我们需要对寄存器的某个位进行取反操作,即1变0,0变1。
//a = 1001 0011 b
//把bit6取反,其他位不变
a ^=(1<<6);
//"^"按位异或符号
//a = 1101 0011 b