嵌入式寄存器位操作

位操作

在嵌入式编程中,常常需要涉及到寄存器的位操作,使能某个功能,设置 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 字节

需要注意的是,所有涉及寄存器值修改的操作。必须遵循以下三步流程:

  1. 获取该地址上的值
  2. 按需修改 bit 位上的值
  3. 将修改后的值设置至该地址

寄存器访问需要注意大端小端的问题,大小端问题也称为字节序问题。关于大小端问暂时不做阐述。

  • 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 中有如下两个步骤,下文以这两个步骤进行举例:

  1. 使能 uart 时钟
  2. 配置 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)

解释一下:

  1. 使用 readl 宏获取到该寄存器值,如果没有宏定义也可以直接使用 *((volatile unsigned int *)(addr)),addr 为内存地址。
  2. 使用位操作 & 对 寄存器值和操作数 (1<<16) 进行按位与运算,将第 16 位设置为 1. 如果对 1<<16 有疑问,请看例 2.
  3. 使用 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) 会在编译优化时被优化为常量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。