位操作
在嵌入式编程中,常常需要涉及到寄存器的位操作,使能某个功能,设置 gpio select, 配置外设等。
回顾一下 C 语言的位运算
-
~
按位取反运算符,按二进制位进行"取反"运算。~0b001=0b110; ~0b00=0b11; 1=-2;0=-1 -
^
按位异或运算符,按二进制位进行"异或"运算。0^0=0; 0^1=1; 1^0=1; 1^1=0; -
|
按位或运算符,按二进制位进行"或"运算。0|0=0; 0|1=1; 1|0=1; 1|1=1; -
&
按位与操作,按二进制位进行"与"运算。0&0=0; 0&1=0; 1&0=0; 1&1=1; -
<<
二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补 0)。 -
>>
二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。
对寄存器的访问是通过内存地址进行,一个地址可访问一个字 4byte(4*8=32bit) 的寄存器数据。
- 字 = 4byte
- 半字 = 2byte
- 字节 = byte = 8bit
- 位 = 1/8 字节
需要注意的是,所有涉及寄存器值修改的操作。必须遵循以下三步流程:
- 获取该地址上的值
- 按需修改 bit 位上的值
- 将修改后的值设置至该地址
寄存器访问需要注意大端小端的问题,大小端问题也称为字节序问题。关于大小端问暂时不做阐述。
- Big-Endian: 低地址存放高位;
- Little-Endian: 低地址存放低位;
bit 操作
本节描述了如何将一个 bit 设置成 0 或者 1。
若需要修改 bit 31 为 1,则可以 a = a|(1<<31)
;
- 对于 bit 16 ,无论其值为何,均会被设置为 1
- 对于 其余各位,无论其值为何均保持不变。
0x ???? ???? ???? ???? ???? ???? ???? ????
|
0x 0100 0000 0000 0000 0000 0000 0000 0000
=
0x ?1?? ???? ???? ???? ???? ???? ???? ????
若需要修改 bit 31 为 0,稍微复杂一点,可以 a = a& (~(1<<31))
- 使用
~(1<<31)
构造操作数 - & 操作后,对于 bit31,无论其值为何,均会被设置为 0
- & 操作后,对于其余各位,无论其值为何均保持不变。
0x ???? ???? ???? ???? ???? ???? ???? ????
&
(
~
0x 0100 0000 0000 0000 0000 0000 0000 0000
)
=
0x ?0?? ???? ???? ???? ???? ???? ???? ????
0x ???? ???? ???? ???? ???? ???? ???? ????
&
0x 1011 1111 1111 1111 1111 1111 1111 1111
=
0x ?0?? ???? ???? ???? ???? ???? ???? ????
示例
以配置 uart0 为例,配置 uart 中有如下两个步骤,下文以这两个步骤进行举例:
- 使能 uart 时钟
- 配置 gpio 选择 uart 模式
为了使能 uart0 时钟,需要设置寄存器BUS_CLK_GATING_REG3
(默认值 0x00000000) 第 16
bit 为 1
0b 0000 0000 0000 0000 0000 0000 0000 0000
;设置 bit 16 为1
0b 0000 0000 0000 0000 1000 0000 0000 0000
=
0x 0 0 0 0 8 0 0 0
可以直接将值 0x00008000
设置至寄存器 BUS_CLK_GATING_REG3
writel(BUS_CLK_GATING_REG3,0x8000)
但是!这样做会有一些问题,我们仅需要操作的是 bit16, 如果其他 bit 上已经配置了值,经过上述操作后,就会被覆盖。
所以在进行修改时要确保其他位不被改变。
所以需要使用位运算进行修改。并且必须遵循寄存器修改三步骤。若需要修改 bit 16 为 1,则可以 a = a|(1<<16)
;
- 对于 bit 16 ,无论其值为何,均会被设置为 1
- 对于 其余各位,无论其值为何均保持不变。
那么上述操作可以变成:
reg = readl(BUS_CLK_GATING_REG3)
reg = reg & (1<<16)
writel(BUS_CLK_GATING_REG3,reg)
解释一下:
- 使用 readl 宏获取到该寄存器值,如果没有宏定义也可以直接使用
*((volatile unsigned int *)(addr))
,addr 为内存地址。 - 使用位操作 & 对 寄存器值和操作数
(1<<16)
进行按位与运算,将第 16 位设置为 1. 如果对1<<16
有疑问,请看例 2. - 使用 writel 宏设置该寄存器值,如果没有宏定义也可以直接使用
*((volatile unsigned int *)(addr)) = (value)
,addr 为内存地址,value 为值。
相关参考: 关键字volatile
上述代码仅作为示例,在实际的编程中通常使用如下简写方式:
readl(BUS_CLK_GATING_REG3) &= (1<<16)
或者使用汇编语言编写:
<略>
多 bit 操作
多 bit 操作思路与单 bit 操作一样,只是在计算修改至时使用了不同的操作数.
为了使能 uart0 gpio,需要设置寄存器 PA_CFG0_REG
(默认值 0x77777777) bit 22:20
(PA5_SELECT) 为 0x2(0b010)
,设置 bit 18:16
(PA4_SELECT) 为 0x2(0b010)
如果是默认值 0x77777777
0x 7 7 7 7 7 7 7 7
0x 0111 0111 0111 0111 0111 0111 0111 0111
则需要修改为 0x77772277
0x 7 7 7 2 2 7 7 7
0x 0111 0111 0111 0010 0010 0111 0111 0111
但需要注意的是,在该地址的其他位上还存在其他配置,在进行修改时要确保其他位不被改变。需要遵循寄存器修改三步骤。
需要设置寄存器 bit 22:20 18:16 均为 0x2(0b010),则是:
- 设置 bit 21,17 为 1, 可以使用
a|= (0b10001<<17)
- 设置 bit 22,20 18,16 为 0,可以使用
a&(~0b1010101<<16)
int reg
reg = readl(PA_CFG0_REG)
/* 设置bit _,21,_,_,_,17_, 为1 */
/* 同等于 reg|=(0x22<<16);reg|=(0b100010<<16) */
/* 同等于 reg|=(0x11<<17);reg|=(0b010001<<17) */
/* 同等于 reg|=(0b00000000000100010000000000000000) */
reg|=0x00220000
/* 设置bit 22,_,20, 18,_,16 为0 */
/* 同等于 reg|=(~0b00000000001010101000000000000000) */
/* 同等于 reg|=(~0x55<<16);reg|=(0b1010101<<16) */
reg&=(~0x550000)
writl(PA_CFG0_REG,reg)
更好的, 由于 GPIO 的一个引脚选择由 4bit 控制,在配置时可以将两个位操作视为整体。
上述操作也可视为将 bit 24:21
bit 20:17
均设置为 0x2
,然后使用
- 清除,
readl(PA_CFG0_REG)&=0xff<<16
- 设置,
readl(PA_CFG0_REG)|=0x22<<16
的方式进行寄存器修改。
值得注意的是,在无法一次到位(非原子)的寄存器修改中,可能存在竞态或者中间状态。
例如在 a&=0xff<<16
执行完成,下一步还未开始时,GPIO 寄存器被设置为 0x000
其代表了设置 GPIO 为 out 状态而非 uart0 。
所以在涉及多 bit 操作时,需要使用内存对寄存器值进行中转,计算完成后再写入。
int tmp
tmp = readl(PA_CFG0_REG)
tmp &=(0xff<<16)
tmp |=(0x22<<16)
writel(PA_CFG0_REG,tmp)
在实际的编程中,通常使用类似
reg&= (1<<16)
的可读性更高的方式,(1<<16) 会在编译优化时被优化为常量。
网友评论