第5章 什么是寄存器
STM32编程通常有两种方法:一种是寄存器编程;另外一种是固件库编程,其中寄存器编程是基础,而固件库编程是在寄存器编程的基础上升级而来的一种易于学习和开发的编程方法,是学习STM32编程时需重点掌握的一种编程方法。固件库编程对于项目开发固然简单和快速,但是从学习的角度出发,寄存器编程的方法也不能不掌握。其实,我们在学习8位或者16位单片机的时候,大多采用寄存器编程。
在探索寄STM32寄存器深层次的问题时,我们从STM32芯片的外观开始,逐层深入学习。最后,在学习完本章内容之后,看看大家能否用一句话给寄存器下一个定义。
5.1 STM32芯片外观
我们开发板中使用的芯片是100pin的STM32F103VET6,具体见图5-1。这个就是我们接下来要学习的STM32,它将带领我们进入嵌入式的领域。
图5-1 STM32F103VET6实物图(方框中部分)
芯片正面是丝印,ARM应该是表示该芯片使用的是ARM的内核,STM32F103VET6是芯片型号,后面的字与生产批次相关,左下角的是ST的LOGO。
芯片四周是引脚,左下角的小圆点表示引脚1,然后从引脚1起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的),见图5-2。开发板中把芯片的引脚引出来,连接到各种传感器上,然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制各种传感器工作,通过做实验的方式来学习STM32芯片的各个资源。开发板是一种评估板,板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。
图5-2 STM32F103VET6正面引脚图
5.2 芯片里面有什么
我们看到的STM32芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设的关系就如同电脑上的CPU与主板、内存、显卡、硬盘的关系。
STM32F103采用的是Cortex-M3内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片。这些内核之外的部件被称为核外外设或片上外设,如GPIO、USART(串口)、I2C、SPI等都叫作片上外设,具体见图5-3。
图5-3 STM32芯片架构简图
芯片(这里指内核,或者叫CPU)和外设之间通过各种总线连接,其中驱动单元有4个,被动单元也有4个,具体见图5-4。为了方便理解,可以把驱动单元理解成CPU部分,而把被动单元理解成外设。下面我们简单介绍下驱动单元和被动单元的各个部件。
图5-4 STM32F10xx系统框图(不包括互联型)
1.ICode总线
ICode中的I表示Instruction,即指令。我们写好的程序编译之后都是一条条指令,存放在Flash中,内核要读取这些指令来执行程序就必须通过ICode总线,它几乎每时每刻都需要被使用,是专门用来取指的。
2.驱动单元
(1)DCode总线
DCode中的D表示Data,即数据,说明这条总线是用来取数的。我们在写程序的时候,数据有常量和变量两种,常量就是固定不变的,在C语言中用const关键字修饰,是放到内部的Flash当中的;变量是可变的,不管是全局变量还是局部变量都放在内部的SRAM中。因为数据可以被DCode总线和DMA总线访问,为了避免访问冲突,在取数的时候需要经过一个总线矩阵来仲裁,决定哪个总线取数。
(2)System总线
System(系统)总线主要是访问外设的寄存器,我们通常说的寄存器编程,即读写寄存器都是通过这根系统总线来完成的。
(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的重点,就是要学会编程这些外设去驱动外部的各种设备。
5.3 存储器映射
在图5-4中,被动单元Flash、RAM、FSMC和AHB到APB的桥(片上外设)这些功能部件共同排列在一个4GB的地址空间内。我们在编程的时候,可以通过它们的地址找到它们,进而操作它们(通过C语言对它们进行数据的读和写)。
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程称为存储器映射,具体见图5-5。如果给存储器再分配一个地址就叫存储器重映射。
图5-5 存储器映射(摘自《参考手册》存储器映射章节)
在这4GB的地址空间中,ARM已经粗线条地把它平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表5-1。每个块的大小都有512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,只用了其中的一部分而已。
表5-1 存储器功能分类
在这8个块里面,有3个块非常重要,也是我们最关心的3个块:Block0被设计成内部Flash,Block1被设计成内部RAM,Block2被设计成片上的外设。下面我们简单地介绍下这3个Block里面的具体区域的功能划分。
1.存储器Block0内部区域功能划分
Block0主要用于设计片内的Flash,我们使用的STM32F103ZET6(霸道)和STM32F103VET6(指南者)的Flash都是512k B,属于大容量。要在芯片内部集成更大的Flash或者SRAM都意味着芯片成本的增加,往往片内集成的Flash都不会太大,ST能在追求性价比的同时做到512k B,实属不易。Block0内部区域的功能划分具体见表5-2。
表5-2 存储器Block0内部区域功能划分
2.储存器Block1内部区域功能划分
Block1用于设计片内的SRAM。我们使用的STM32F103ZET6(霸道)和STM32F103VET6(指南者)的SRAM都是64k B。Blockl内部区域的功能划分具体见表5-3。
表5-3 存储器Block1内部区域功能划分
3.储存器Block2内部区域功能划分
Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,具体见表5-4。
表5-4 存储器Block2内部区域功能划分
5.4 寄存器映射
我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射。那什么叫寄存器映射?寄存器到底是什么?
在存储器Block2这块区域,设计的是片上外设,它们以4个字节为一个单元,共32位,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元。但如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,那么可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的、有特定功能的内存单元取别名的过程就叫寄存器映射。
比如,我们找到GPIOB端口的输出数据寄存器ODR的地址是0x40010C0C(至于这个地址如何找到可以先跳过,后面会有详细的讲解),ODR寄存器是32位,低16位有效,对应16个外部IO,写0/1对应的IO输出低/高电平。现在我们通过C语言指针的操作方式,让GPIOB的16个IO都输出高电平,具体见代码清单5-1。
代码清单5-1 通过绝对地址访问内存单元
1 // GPIOB端口全部输出高电平 2 *(unsigned int*)(0x4001 0C0C) = 0x FFFF;
0x40010C0C在我们看来是GPIOB端口ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,得进行强制类型转换,把它转换成指针,即(unsigned int *)0x40010C0C,然后再对这个指针进行*操作。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见代码清单5-2。
代码清单5-2 通过寄存器别名方式访问内存单元
1 // GPIOB端口全部输出高电平 2 #define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C) 3 * GPIOB_ODR = 0x FF;
为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见代码清单5-3。
代码清单5-3 通过寄存器别名访问内存单元
1 // GPIOH端口全部输出高电平 2 #define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C) 3 GPIOB_ODR = 0x FF;
5.4.1 STM32的外设地址映射
片上外设区分为3条总线,根据不同的外设速度,不同总线挂载着不同的外设,APB1挂载低速外设,APB2和AHB挂载高速外设。相应总线的最低地址称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中APB1总线的地址最低,片上外设从这里开始,它也叫外设基地址。
1.总线基地址
总线基地址见表5-5。
表5-5 总线基地址
表5-5中的“相对外设基地址的偏移”即该总线地址与“片上外设”基地址0x40000000的差值。关于地址的偏移后面还会讲到。
2.外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”,也叫XX外设的边界地址。具体有关STM32F10xx外设的边界地址请参考《STM32F10xx参考手册》的2.3小节中的存储器映射的表1:STM32F10xx寄存器边界地址。
这里面我们以GPIO这个外设来讲解外设的基地址,GPIO属于高速的外设,挂载到APB2总线上,具体见表5-6。
表5-6 外设GPIO基地址
3.外部寄存器
在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO外设为例,GPIO是“通用输入输出端口”的简称,简单来说就是STM32可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED的阴极,LED的阳极接电源,然后通过STM32控制该引脚的电平,从而实现控制LED亮灭的功能。
GPIO有很多寄存器,每一个都有特定的功能。每个寄存器为32位,占4个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOB端口为例,来说明GPIO都有哪些寄存器,具体见表5-7。
表5-7 GPIOB端口的寄存器地址列表
有关外设的寄存器说明可参考《STM32F10xx参考手册》具体章节中的寄存器描述部分,在编程的时候我们需要反复查阅外设的寄存器说明。
这里我们以“GPIO端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见图5-6。
图5-6 GPIO端口置位/复位寄存器说明
(1)名称
寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…E)”这段的意思是:该寄存器名为“GPIOx_BSRR”,其中的“x”可以为A~E,也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOE,这些GPIO端口都有这样的一个寄存器。
(2)地址偏移
地址偏移是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x18,从参考手册中我们可以查到GPIOA外设的基地址为0x40010800,由此可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x40010800+0x18;同理,由于GPIOB的外设基地址为0x40010C00,可算出GPIOB_BSRR寄存器的地址为:0x40010C00+0x18。其他GPIO端口以此类推即可。
(3)寄存器位表
紧接着的是本寄存器的位表,表中列出它的0~31位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中w表示只写,r表示只读,rw表示可读写。本寄存器中的位权限都是w,所以只能写,如果要读本寄存器,则无法保证读取到它真正内容。而有的寄存器为只读,一般用于表示STM32外设的某种工作状态的,由STM32硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。
(4)位功能说明
位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一位的功能。例如本寄存器中有两种寄存器位,分别为BRy及BSy,其中的y数值可以是0~15,这里的0~15表示端口的引脚号,如BR0、BS0用于控制GPIOx的第0个引脚,若x表示GPIOA,那就是控制GPIOA的第0引脚,而BR1、BS1就是控制GPIOA的第1个引脚。
其中BRy引脚的说明是“0:对对相应的ODRy位不产生影响;1:清除对相应ODRy位为0”。说明中的ODRx是另一个寄存器的寄存器位,我们只需要知道ODRx位为1的时候,对应的引脚x输出高电平,为0的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器GPIOx_ODR的说明了解)。所以,如果对BR0写入“1”的话,那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位BSy与BRy是相反的操作。
5.4.2 C语言对寄存器的封装
以上所有关于存储器映射的内容,最终都是为大家更好地理解如何用C语言控制读写外部寄存器做准备,本节是本章的重点内容。
1.封装总线和外设基地址
在编程上,为了方便理解和记忆,我们把总线基地址和外设基地址都用相应的宏定义,总线或者外设都以它们的名字作为宏名,具体见代码清单5-4。
代码清单5-4 总线和外设基址宏定义
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)
代码清单5-4 首先定义了“片上外设”基地址PERIPH_BASE,接着在PERIPH_BASE上加入各个总线的地址偏移,得到APB1、APB2总线的地址APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到GPIOA~CPIOG的外设地址,最后在外设地址上加入各寄存器的地址偏移,即可得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写,具体见代码清单5-5。
代码清单5-5 使用指针控制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.封装寄存器列表
用上面的方法定义地址,还是稍显繁琐,例如GPIOA~GPIOE都各有一组功能相同的寄存器,如GPIOA_ODR、GPIOB_ODR、GPIOC_ODR等,它们只是地址不一样,但却要为每个寄存器都定义地址。为了更方便地访问寄存器,我们引入C语言中的结构体语法对寄存器进行封装,具体见代码清单5-6。
代码清单5-6 使用结构体对GPIO寄存器组的封装
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_Type Def;
这段代码用typedef关键字声明了名为GPIO_Type Def的结构体类型,结构体内有7个成员变量,变量名正好对应寄存器的名字。C语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4个字节,16位的变量占用2个字节,具体见图5-7。
图5-7 GPIO_Type Def结构体成员的地址偏移
也就是说,我们定义的这个GPIO_Type Def,假如这个结构体的首地址为0x40010C00(这也是第1个成员变量CRL的地址),那么结构体中第2个成员变量CRH的地址即为0x40010C00 +0x04,加上的这个0x04,正是代表CRL所占用的4个字节地址的偏移量,其他成员变量相对于结构体首地址的偏移,在上述代码右侧注释中给出。
这样的地址偏移与STM32 GPIO外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,具体见代码清单5-7。
这段代码先用GPIO_Type Def类型定义一个结构体指针GPIOx,并让指针指向地址GPIOB_BASE(0x40010C00),使用地址确定下来,然后根据C语言访问结构体的语法,用GPIOx->ODR及GPIOx->IDR等方式读写寄存器即可。
最后,更进一步,直接使用宏定义好GPIO_Type Def类型的指针,而且指针指向各个GPIO端口的首地址,使用时直接用该宏访问寄存器即可,具体见代码清单5-8。
代码清单5-7 通过结构体指针访问寄存器
1 GPIO_Type Def * GPIOx; //定义一个GPIO_Type Def型结构体指针GPIOx 2 GPIOx = GPIOB_BASE; //把指针地址设置为宏GPIOH_BASE地址 3 GPIOx->IDR = 0x FFFF; 4 GPIOx->ODR = 0x FFFF; 5 6 7 uint32_t temp; 8 temp = GPIOx->IDR; //读取GPIOB_IDR寄存器的值到变量temp中
代码清单5-8 定义好GPIO端口首地址指针
1 /*使用GPIO_Type Def把地址强制转换成指针*/ 2 #define GPIOA ((GPIO_Type Def *) GPIOA_BASE) 3 #define GPIOB ((GPIO_Type Def *) GPIOB_BASE) 4 #define GPIOC ((GPIO_Type Def *) GPIOC_BASE) 5 #define GPIOD ((GPIO_Type Def *) GPIOD_BASE) 6 #define GPIOE ((GPIO_Type Def *) GPIOE_BASE) 7 #define GPIOF ((GPIO_Type Def *) GPIOF_BASE) 8 #define GPIOG ((GPIO_Type Def *) GPIOG_BASE) 9 #define GPIOH ((GPIO_Type Def *) GPIOH_BASE) 10 11 12 13 /*使用定义好的宏直接访问*/ 14 /*访问GPIOB端口的寄存器*/ 15 GPIOB->BSRR = 0x FFFF; //通过指针访问并修改GPIOB_BSRR寄存器 16 GPIOB->CRL = 0x FFFF; //修改GPIOB_CRL寄存器 17 GPIOB->ODR =0x FFFF; //修改GPIOB_ODR寄存器 18 19 uint32_t temp; 20 temp = GPIOB->IDR; //读取GPIOB_IDR寄存器的值到变量temp中 21 22 /*访问GPIOA端口的寄存器*/ 23 GPIOA->BSRR = 0x FFFF; 24 GPIOA->CRL = 0x FFFF; 25 GPIOA->ODR =0x FFFF; 26 27 uint32_t temp; 28 temp = GPIOA->IDR; //读取GPIOA_IDR寄存器的值到变量temp中
这里我们仅是以GPIO这个外设为例,给大家讲解了C语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作已经由固件库帮我们完成了,这里我们只是分析这个封装的过程,让大家知其然,也知其所以然。
3.修改寄存器的位操作方法
使用C语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其他的寄存器位不变,这个时候就需要用到C语言的位操作方法了。
(1)把变量的某位清零
此处我们以变量a代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量a的某一位清零,且其他位不变,方法见代码清单5-9。
代码清单5-9 对某位清零
1 //定义一个变量a = 10011111 b (二进制数) 2 unsigned char a = 0x9f; 3 4 //对bit2清零 5 6 a &= ~(1<<2); 7 8 //括号中的1左移两位,(1<<2)得二进制数:00000100 b 9 //按位取反,~(1<<2)得11111011 b 10 //假如a中原来的值为二进制数:a = 10011111 b 11 //所得的数与a作”位与&”运算,a = (10011111 b)&(11111011 b), 12 //经过运算后,a的值a=10011011 b 13 // a的bit2 位被清零,而其他位不变
(2)把变量的某几个连续位清零
由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零,且其他位不变,方法见代码清单5-10。
代码清单5-10 对某几个连续位清零
1 2 //若把a中的二进制位分成两个一组 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)得二进制数:00001100 b 10 //按位取反,~(3<<2*1)得11110011 b 11 //假如a中原来的值为二进制数:a = 10011111 b 12 //所得的数与a作”位与&”运算,a = (10011111 b)&(11110011 b), 13 //经过运算后,a的值a=10010011 b 14 // a的第1组的bit2、bit3被清零,而其他位不变 15 16 //上述(~(3<<2*1))中的(1)即为组编号;如清零第3组bit6、bit7此处应为3 17 //括号中的(2)为每组的位数,每组有两个二进制位;若分成4个一组,此处即为4 18 //括号中的(3)是组内所有位都为1时的值;若分成4个一组,此处即为二进制数“1111 b” 19 20 //例如对第2组bit4、bit5清零 21 a &= ~(3<<2*2);
(3)对变量的某几位进行赋值
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其他位不变,方法见代码清单5-11。这时候写入的数值一般就是需要设置寄存器的位参数。
代码清单5-11 对某几位进行赋值
1 //a = 10000011 b 2 //此时对清零后的第2组bit4、bit5设置成二进制数“01 b ” 3 4 a |= (1<<2*2); 5 //a = 10010011 b,成功设置了第2组的值,其他组不变
(4)对变量的某位取反
某些情况下,我们需要对寄存器的某个位进行取反操作,即1变0,0变1,这可以直接用如下操作,其他位不变,见代码清单5-12。
代码清单5-12 对某位进行取反操作
1 //a = 10010011 b 2 //把bit6取反,其他位不变 3 4 a ^=(1<<6); 5 //a = 11010011 b
关于修改寄存器位的这些操作,在下一章中有应用实例代码,可配合阅读。