位操作
在嵌入式編程中,常常需要涉及到寄存器的位操作,使能某個功能,設(shè)置 gpio select, 配置外設(shè)等。
回顧一下 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)。 -
>>二進制右移運算符。將一個數(shù)的各二進制位全部右移若干位,正數(shù)左補 0,負(fù)數(shù)左補 1,右邊丟棄。
對寄存器的訪問是通過內(nèi)存地址進行,一個地址可訪問一個字 4byte(4*8=32bit) 的寄存器數(shù)據(jù)。
- 字 = 4byte
- 半字 = 2byte
- 字節(jié) = byte = 8bit
- 位 = 1/8 字節(jié)
需要注意的是,所有涉及寄存器值修改的操作。必須遵循以下三步流程:
- 獲取該地址上的值
- 按需修改 bit 位上的值
- 將修改后的值設(shè)置至該地址
寄存器訪問需要注意大端小端的問題,大小端問題也稱為字節(jié)序問題。關(guān)于大小端問暫時不做闡述。
- Big-Endian: 低地址存放高位;
- Little-Endian: 低地址存放低位;
bit 操作
本節(jié)描述了如何將一個 bit 設(shè)置成 0 或者 1。
若需要修改 bit 31 為 1,則可以 a = a|(1<<31);
- 對于 bit 16 ,無論其值為何,均會被設(shè)置為 1
- 對于 其余各位,無論其值為何均保持不變。
0x ???? ???? ???? ???? ???? ???? ???? ????
|
0x 0100 0000 0000 0000 0000 0000 0000 0000
=
0x ?1?? ???? ???? ???? ???? ???? ???? ????
若需要修改 bit 31 為 0,稍微復(fù)雜一點,可以 a = a& (~(1<<31))
- 使用
~(1<<31)構(gòu)造操作數(shù) - & 操作后,對于 bit31,無論其值為何,均會被設(shè)置為 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 時鐘,需要設(shè)置寄存器BUS_CLK_GATING_REG3(默認(rèn)值 0x00000000) 第 16 bit 為 1
0b 0000 0000 0000 0000 0000 0000 0000 0000
;設(shè)置 bit 16 為1
0b 0000 0000 0000 0000 1000 0000 0000 0000
=
0x 0 0 0 0 8 0 0 0
可以直接將值 0x00008000 設(shè)置至寄存器 BUS_CLK_GATING_REG3
writel(BUS_CLK_GATING_REG3,0x8000)
但是!這樣做會有一些問題,我們僅需要操作的是 bit16, 如果其他 bit 上已經(jīng)配置了值,經(jīng)過上述操作后,就會被覆蓋。
所以在進行修改時要確保其他位不被改變。
所以需要使用位運算進行修改。并且必須遵循寄存器修改三步驟。若需要修改 bit 16 為 1,則可以 a = a|(1<<16);
- 對于 bit 16 ,無論其值為何,均會被設(shè)置為 1
- 對于 其余各位,無論其值為何均保持不變。
那么上述操作可以變成:
reg = readl(BUS_CLK_GATING_REG3)
reg = reg & (1<<16)
writel(BUS_CLK_GATING_REG3,reg)
解釋一下:
- 使用 readl 宏獲取到該寄存器值,如果沒有宏定義也可以直接使用
*((volatile unsigned int *)(addr)),addr 為內(nèi)存地址。 - 使用位操作 & 對 寄存器值和操作數(shù)
(1<<16)進行按位與運算,將第 16 位設(shè)置為 1. 如果對1<<16有疑問,請看例 2. - 使用 writel 宏設(shè)置該寄存器值,如果沒有宏定義也可以直接使用
*((volatile unsigned int *)(addr)) = (value),addr 為內(nèi)存地址,value 為值。
相關(guān)參考: 關(guān)鍵字volatile
上述代碼僅作為示例,在實際的編程中通常使用如下簡寫方式:
readl(BUS_CLK_GATING_REG3) &= (1<<16)
或者使用匯編語言編寫:
<略>
多 bit 操作
多 bit 操作思路與單 bit 操作一樣,只是在計算修改至?xí)r使用了不同的操作數(shù).
為了使能 uart0 gpio,需要設(shè)置寄存器 PA_CFG0_REG(默認(rèn)值 0x77777777) bit 22:20(PA5_SELECT) 為 0x2(0b010),設(shè)置 bit 18:16(PA4_SELECT) 為 0x2(0b010)
如果是默認(rèn)值 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
但需要注意的是,在該地址的其他位上還存在其他配置,在進行修改時要確保其他位不被改變。需要遵循寄存器修改三步驟。
需要設(shè)置寄存器 bit 22:20 18:16 均為 0x2(0b010),則是:
- 設(shè)置 bit 21,17 為 1, 可以使用
a|= (0b10001<<17) - 設(shè)置 bit 22,20 18,16 為 0,可以使用
a&(~0b1010101<<16)
int reg
reg = readl(PA_CFG0_REG)
/* 設(shè)置bit _,21,_,_,_,17_, 為1 */
/* 同等于 reg|=(0x22<<16);reg|=(0b100010<<16) */
/* 同等于 reg|=(0x11<<17);reg|=(0b010001<<17) */
/* 同等于 reg|=(0b00000000000100010000000000000000) */
reg|=0x00220000
/* 設(shè)置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 均設(shè)置為 0x2,然后使用
- 清除,
readl(PA_CFG0_REG)&=0xff<<16 - 設(shè)置,
readl(PA_CFG0_REG)|=0x22<<16
的方式進行寄存器修改。
值得注意的是,在無法一次到位(非原子)的寄存器修改中,可能存在競態(tài)或者中間狀態(tài)。
例如在 a&=0xff<<16 執(zhí)行完成,下一步還未開始時,GPIO 寄存器被設(shè)置為 0x000 其代表了設(shè)置 GPIO 為 out 狀態(tài)而非 uart0 。
所以在涉及多 bit 操作時,需要使用內(nèi)存對寄存器值進行中轉(zhuǎn),計算完成后再寫入。
int tmp
tmp = readl(PA_CFG0_REG)
tmp &=(0xff<<16)
tmp |=(0x22<<16)
writel(PA_CFG0_REG,tmp)
在實際的編程中,通常使用類似
reg&= (1<<16)的可讀性更高的方式,(1<<16) 會在編譯優(yōu)化時被優(yōu)化為常量。