嵌入式寄存器位操作

2021-06-29  本文已影响0人  一个大大大坑

位操作

在嵌入式编程中,常常需要涉及到寄存器的位操作,使能某个功能,设置 gpio select, 配置外设等。

回顾一下 C 语言的位运算

对寄存器的访问是通过内存地址进行,一个地址可访问一个字 4byte(4*8=32bit) 的寄存器数据。

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

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

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

bit 操作

本节描述了如何将一个 bit 设置成 0 或者 1。

若需要修改 bit 31 为 1,则可以 a = a|(1<<31);

0x ???? ???? ???? ???? ???? ???? ???? ????
|
0x 0100 0000 0000 0000 0000 0000 0000 0000
=
0x ?1?? ???? ???? ???? ???? ???? ???? ????

若需要修改 bit 31 为 0,稍微复杂一点,可以 a = a& (~(1<<31))

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);

那么上述操作可以变成:

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),则是:

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,然后使用

的方式进行寄存器修改。

值得注意的是,在无法一次到位(非原子)的寄存器修改中,可能存在竞态或者中间状态。

例如在 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) 会在编译优化时被优化为常量。

上一篇下一篇

猜你喜欢

热点阅读