[GPIO]推薦一種超簡單的硬體位帶bitband操作方法,讓變數,暫存器控制,IO訪問更便捷,無需使用者計算位置
說明:
M3,M4核心都支援硬體位帶操作,M7核心不支援。
硬體位帶操作優勢
優勢1:
比如我們在地址0x2000 0000定義了一個變數unit8_ta, 如果我們要將此變數的bit0清零,而其它bit不變。
a & = ~0x01
這個過程就需要讀變數a,修改bit0,然後重新賦值給變數a,也就是讀 - 修改 - 寫經典三部曲,如果我們使用硬體位帶就可以一步就完成,也就是所謂的原子操作,優勢是不用擔心中斷或者RTOS任務打斷。
優勢2:
操作便捷,適合用於需要頻繁操作修改的場合,移植性強。不頻繁的直接標準庫或者HAL庫配置即可。
背景知識
這個點知道不知道都沒有關係,不影響我們使用硬體位帶,可以直接看下面案例的操作方法,完全不需要使用者去了解。
位帶操作就是對變數每個bit的操作,以M4核心的STM32F4為例:
(1)將1MB地址範圍 0x20000000 - 0x200FFFFF對映到32MB空間範圍0x22000000 -0x23FFFFFF ----> 這個對應STM32F4的通用RAM空間。
也就是說1MB空間每個bit都拓展為32bit來訪問控制
下面這個圖非常具有代表性。
0x20000000地址的位元組變數 bit0 對映到0x22000000來控制。
0x20000000地址的位元組變數 bit1 對映到0x22000004來控制。
0x20000000地址的位元組變數 bit2 對映到0x22000008來控制。
..........依次類推
(2)將1MB地址範圍 0x40000000 - 0x400FFFFF對映到32MB空間範圍0x42000000 -0x43FFFFFF ----> 這個對應STM32F4的外設空間。
同樣也是1MB空間每個bit都拓展為32bit來訪問控制
(3)舉例,比如訪問0x2000 0010地址裡面位元組變數的bit2
那麼實際要訪問的就是:
bit_word_addr = bit_band_base + (byte_offset x 32) + (bit_number × 4)
0x22000208 = 0x22000000 + (0x10*32) + (2*4)
通過對地址空間0x22000208 進行賦值為0x01就表示bit2置位,賦值為0x00就表示bit2清零,對這個地址空間讀取操作就可以反應bit2的數值。
超簡單實現方案和四個經典案例
這種硬體未帶讓使用者去使用非常不方便,還需要倒騰地址計算。
這裡以MDK為例,提供一種IDE支援的,直接加字尾__attribute__((bitband))即可,對於M3和M4可以直接轉換為硬體位帶實現。
案例1:超簡單控制RAM空間變數:
定義:
typedef struct { uint8_t bit0 : 1; uint8_t bit1 : 1; uint8_t bit2 : 1; uint8_t bit3 : 1; uint8_t bit4 : 1; uint8_t bit5 : 1; uint8_t bit6 : 1; uint8_t bit7 : 1; } TEST __attribute__((bitband)); TEST tTestVar;
我們定義了一個8bit的變數tTestVar,控制每個bit的方法如下:
tTestVar.bit0 = 1; tTestVar.bit1 = 1; tTestVar.bit2 = 1; tTestVar.bit3 = 0; tTestVar.bit4 = 0; tTestVar.bit5 = 1; tTestVar.bit6 = 1; tTestVar.bit7 = 1;
看彙編,已經修改為硬體位帶:
案例2:超簡單控制GPIO輸入輸出暫存器:
GPIO裡面最常用的就是輸入輸出。
GPIO輸出暫存器定義如下,每個bit控制一個IO引腳。
我們軟體定義如下:
typedef struct { uint16_t ODR0 : 1; uint16_t ODR1 : 1; uint16_t ODR2 : 1; uint16_t ODR3 : 1; uint16_t ODR4 : 1; uint16_t ODR5 : 1; uint16_t ODR6 : 1; uint16_t ODR7 : 1; uint16_t ODR8 : 1; uint16_t ODR9 : 1; uint16_t ODR10 : 1; uint16_t ODR11 : 1; uint16_t ODR12 : 1; uint16_t ODR13 : 1; uint16_t ODR14 : 1; uint16_t ODR15 : 1; uint16_t Reserved : 16; } GPIO_ORD __attribute__((bitband)); GPIO_ORD *GPIOA_ODR = (GPIO_ORD *)(&GPIOA->ODR); GPIO_ORD *GPIOB_ODR = (GPIO_ORD *)(&GPIOB->ODR); GPIO_ORD *GPIOC_ODR = (GPIO_ORD *)(&GPIOC->ODR); GPIO_ORD *GPIOD_ODR = (GPIO_ORD *)(&GPIOD->ODR); GPIO_ORD *GPIOE_ODR = (GPIO_ORD *)(&GPIOE->ODR); GPIO_ORD *GPIOF_ODR = (GPIO_ORD *)(&GPIOF->ODR); GPIO_ORD *GPIOJ_ODR = (GPIO_ORD *)(&GPIOJ->ODR); GPIO_ORD *GPIOK_ODR = (GPIO_ORD *)(&GPIOK->ODR);
GPIO輸入暫存器定義如下:
我們軟體定義如下:
typedef struct { uint16_t IDR0 : 1; uint16_t IDR1 : 1; uint16_t IDR2 : 1; uint16_t IDR3 : 1; uint16_t IDR4 : 1; uint16_t IDR5 : 1; uint16_t IDR6 : 1; uint16_t IDR7 : 1; uint16_t IDR8 : 1; uint16_t IDR9 : 1; uint16_t IDR10 : 1; uint16_t IDR11 : 1; uint16_t IDR12 : 1; uint16_t IDR13 : 1; uint16_t IDR14 : 1; uint16_t IDR15 : 1; uint16_t Reserved : 16; } GPIO_IDR __attribute__((bitband)); GPIO_IDR *GPIOA_IDR = (GPIO_IDR *)(&GPIOA->IDR); GPIO_IDR *GPIOB_IDR = (GPIO_IDR *)(&GPIOB->IDR); GPIO_IDR *GPIOC_IDR = (GPIO_IDR *)(&GPIOC->IDR); GPIO_IDR *GPIOD_IDR = (GPIO_IDR *)(&GPIOD->IDR); GPIO_IDR *GPIOE_IDR = (GPIO_IDR *)(&GPIOE->IDR); GPIO_IDR *GPIOF_IDR = (GPIO_IDR *)(&GPIOF->IDR); GPIO_IDR *GPIOJ_IDR = (GPIO_IDR *)(&GPIOJ->IDR); GPIO_IDR *GPIOK_IDR = (GPIO_IDR *)(&GPIOK->IDR);
實際操作效果動態,注意看除錯狀態暫存器變化,控制GPIOA的PIN0到PIN3
案例3:超方便的暫存器修改:
比如定時器TIM1的CR暫存器:
我們的定義如下:
typedef struct { uint16_t CEN : 1; uint16_t UDIS : 1; uint16_t URS : 1; uint16_t OPM : 1; uint16_t DIR : 1; uint16_t CMS : 2; uint16_t APRE : 1; uint16_t CKD : 2; uint16_t Reserved : 6; } TIM_CR1 __attribute__((bitband)); TIM_CR1 *TIM1_CR1 = (TIM_CR1 *)(&TIM1->CR1);
實際操作動態效果,注意看除錯狀態暫存器變化,設定TIM1 CR1暫存器的每個bit控制:
由於標準庫,HAL庫配置這些已經非常方便了,我們再使用這種方式意義不是很大,但對於需要頻繁操作的地方,這種方式就非常好使了,言簡意賅,移植性強,強力推薦,而且是原子操作方式,不用怕中斷打斷。
案例4:應用進階:
最後我們來個進階,比如我們通過32位頻寬的FMC匯流排擴展出來32個GPIO,如果我們採用如下使用方式就非常不直觀
#defineHC574_PORT *(uint32_t *)0x64001000
操作bit1 =0清零,就需要如下操作:
HC574_PORT &= ~(1<<1);
操作bit2和bit10置位,就需要如下操作:
HC574_PORT |= ( ( 1<< 2) | (1<<10))
這種操作會導致以後的程式碼修改非常不便,別人移植使用也非常不方便。如果我們改成如下方式,就方便太多了。
typedef struct { uint32_t tGPRS_TERM_ON : 1; uint32_t tGPRS_RESET :1; uint32_t tNRF24L01_CE :1; uint32_t tNRF905_TX_EN :1; uint32_t tNRF905_TRX_CE :1; uint32_t tNRF905_PWR_UP :1; uint32_t tESP8266_G0 :1; uint32_t tESP8266_G2 :1; uint32_t tLED1 :1; uint32_t tLED2 :1; uint32_t tLED3 :1; uint32_t tLED4 :1; uint32_t tTP_NRST :1; uint32_t tAD7606_OS0 :1; uint32_t tAD7606_OS1 :1; uint32_t tAD7606_OS2 :1; uint32_t tY50_0 :1; uint32_t tY50_1 :1; uint32_t tY50_2 :1; uint32_t tY50_3 :1; uint32_t tY50_4 :1; uint32_t tY50_5 :1; uint32_t tY50_6 :1; uint32_t tY50_7 :1; uint32_t tAD7606_RESET :1; uint32_t tAD7606_RANGE :1; uint32_t tY33_2 :1; uint32_t tY33_3 :1; uint32_t tY33_4 :1; uint32_t tY33_5 :1; uint32_t tY33_6 :1; uint32_t tY33_7 :1; }FMCIO_ODR __attribute__((bitband)); FMCIO_ODR *FMC_EXTIO = (FMCIO_ODR *)0x60001000;
比如控制AD7606的OS0引腳高電平就是
FMC_EXTIO->tAD7606_OS0= 1;
控制OS0引腳是低電平就是:
FMC_EXTIO->tAD7606_OS0= 0;
簡單易用,超方便。
M7核心為什麼不支援
M核心權威指南作者Joseph Yiu回覆:
1、Cache問題,如果SRAM所在區域開啟了讀寫Cache,使用位帶操作的話,會有資料一致性問題。
2、位帶需要匯流排鎖機制,在AHB匯流排協議中這相對容易實現,但在AXI匯流排協議中這有點混亂,並且在鎖定序列期間,它可能導致其他匯流排主控的延遲更長。