前言
上一篇我們分享了字符設備驅動框架:linux驅動基礎篇:hello驅動 ,當時分享的是hello驅動程序。學STM32我們從點燈開始,學Linux驅動我們自然也要點個燈來玩玩,盡量在從這些基礎例程中榨取知識,細摳、細摳,為之后更復雜的知識打好基礎。
與硬件無關的LED驅動
回顧hello驅動程序,我們的根據實際需求對其進行寫字符串與讀字符串操作。這里我們當然也要根據實際來思考我們的LED驅動程序。在STM32點燈的時候,一般輸出低電平點燈,輸出高電平滅燈。在嵌入Linux操作系統的情況下,我們自然也要想到有個寫1/0的思想。類比我們上一篇的hello程序:
我們的LED程序自然要寫入的數據為0/1來點亮、熄滅LED。這里我們做的實驗室與硬件無關的LED實驗:我們的驅動程序在收到應用程序發送過來的0時打印led on、收到1時打印led off。模仿上一篇的hello程序,我們修改得到的與硬件無關的LED程序(核心部分)如下:
LED應用程序:
LED驅動程序:
加載led驅動模塊及運行應用程序:
與硬件有關的LED驅動
上面那一節分享的是與硬件無關的LED驅動實驗,主要是為了理清LED驅動的大體思路。這里我們再加入與硬件有關的相關操作以構造與硬件有關的LED驅動程序。
我們在進行STM32的裸機編程的時候,對一些外設進行配置其實就是操作一些地址的過程,這些外設地址在芯片手冊中可以看到:
這是地址映射圖,這里圖中只是列出的外設的邊界地址,每個外設又有很多寄存器,這些寄存器的地址都是對外設基地址進行偏移得到的。同樣的,對于NXP的IMX6ULL芯片來說,也是有類似這樣的地址的:
此時我們要編寫Linux系統下的led驅動,涉及到硬件操作的地方操作的并不是這些地址(物理地址),而是操作系統給我們提供的地址(虛擬地址)。操作系統根據物理地址來給我們生成一個虛擬地址,我們的led驅動操控這個地址就是間接的操控物理地址。至于這兩個地址是怎么聯系起來的,里面個原理我們暫且不展開。我們從函數層面來看,內核給我們提供了ioremap 函數,這個函數可以把物理地址映射為虛擬地址。這個函數在內核文件arch/arm/include/asm/io.h 中:
void __iomem *ioremap(resource_size_t res_cookie, size_t size);
- res_cookie:要映射給的物理起始地址 。
- size:要映射的內存空間大小。
- 返回值: 指向映射后的虛擬空間首地址。
與ioremap函數相對應的函數為:
void iounmap (volatile void __iomem *addr)
- addr:要取消映射的虛擬地址空間首地址。
地址映射完成之后,我們可以直接通過指針來訪問虛擬地址,如:
*GPIO5_DR &= ~(1 << 3); /* GPIO5_IO03輸出低電平 */
*GPIO5_DR |= (1 << 3); /* GPIO5_IO03輸出高電平 */
這里簡單介紹一下i.MX 6ULL的GPIO。對于i.MX 6ULL來說,以數字來給IO端口(組別)命令,GPIO5為第五組,所以GPIO5_IO03為第五組端口的第3個引腳。而STM32中是以大寫字母來表示端口(組別),如PA3表示A端口的第3個引腳。
i.MX 6ULL有 5 組 GPIO(GPIO1~ GPIO5),每組引腳最多有 32 個:
GPIO1 有 32 個引腳: GPIO1_IO0~GPIO1_IO31;
GPIO2 有 22 個引腳: GPIO2_IO0~GPIO2_IO21;
GPIO3 有 29 個引腳: GPIO3_IO0~GPIO3_IO28;
GPIO4 有 29 個引腳: GPIO4_IO0~GPIO4_IO28;
GPIO5 有 12 個引腳: GPIO5_IO0~GPIO5_IO11;
地址映射完成之后,我們不僅可以通過指針來訪問虛擬地址,而且還可以使用內核給我們提供的一些讀寫函數:
/* 寫操作函數 */
void writeb(u8 value, volatile void __iomem *addr);
void writew(u16 value, volatile void __iomem *addr);
void writel(u32 value, volatile void __iomem *addr);
/* 讀操作函數 */
u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);
writeb、 writew 和 writel 這三個函數分別對應 8bit、 16bit 和 32bit 寫操作,參數 value 是要寫入的數值, addr 是要寫入的地址。
readb、 readw 和 readl 這三個函數分別對應 8bit、 16bit 和 32bit 讀操作,參數 addr 就是要讀取寫內存地址,返回值就是讀取到的數據。
此時我們可以把上一節的led_init函數led_drv_write函數進行修改:
與STM32一樣,對于i.MX 6ULL的GPIO外設來說,也有很多寄存器:
上面我們只是點一個燈,如果是要點多個燈呢?那就得操控多個GPIO。如果進行地址映射的寫法還像上面那樣,代碼就會顯得很臃腫。回想一下我們STM32,GPIO外設通過結構體來管理它的寄存器:
這里的__IO是個宏,代表C語言的關鍵字volatile ,為了防止編譯器對我們的一些硬件操作進行優化,從而得不到想要的結果。比如:
/* 假設REG為寄存器的地址 */
uint32 *REG;
*REG = 0; /* 點燈 */
*REG = 1; /* 滅燈 */
此時若是REG不加volatile進行修飾,則點燈操作將被優化掉,只執行滅燈操作。
在這里,我們也可以模仿STM32那樣子,用一個結構體來對i.MX 6ULL的GPIO的寄存器進行管理,如:
struct GPIO_RegDef
{
volatile unsigned int DR;
volatile unsigned int GDIR;
volatile unsigned int PSR;
volatile unsigned int ICR1;
volatile unsigned int ICR2;
volatile unsigned int IMR;
volatile unsigned int ISR;
volatile unsigned int EDGE_SEL;
};
結構體里的成員排序是要按照特定順序來的:
因為這些寄存器都是相對于GPIO外設的基地址作偏移得到的,比如:
不能打亂順序,否則就不能正確訪問到對應的寄存器了。用結構體進行管理之后,我們就可以用類似下面的方式進行映射:
struct GPIO_RegDef *GPIO5 = ioremap(0x20AC000, sizeof(struct GPIO_RegDef));
然后就可以向STM32那樣來操控GPIO寄存器,如:
GPIO5->DR &= ~(1 << 3); /* GPIO5_IO03輸出低電平 */
GPIO5->DR |= (1 << 3); /* GPIO5_IO03輸出高電平 */
與硬件有關的LED驅動(升級版)
上一節我們分享的LED驅動是一個常規的LED驅動,只能適用于我們當前的開發版,所以是一個專用的LED驅動程序。若是換了另一塊板,led所連接的gpio引腳可能不一樣了,我們就修改我們的驅動程序led_drv.c里與寄存器相關的操作。有沒有更好的辦法不用再修改我們的led_drv.c驅動程序了?
若是led_drv.c不用再修改了,那么這個led_drv.c驅動就是一個通用的驅動程序了。具體可查看韋東山老師的《嵌入式Linux應用開發完全手冊第2版》第五篇第3~7節進行學習。
下面來簡單地梳理一下:
由于篇幅問題,具體的部分就不貼出來了。
之前的筆記中:C語言、嵌入式重點知識:回調函數 中我也有提到通用與專用的含義,可以了解了解加深對這兩個詞的認識。
這里我們學到了很重要的思想軟件分層的思想及技巧,但也只是點了一下,未來的路還很長,需要持續學習,繼續提高。
以上就是本次的分享,如有錯誤,歡迎指出!謝謝
參考/學習資料:
- 百問網《嵌入式Linux應用開發完全手冊第2版》
- 正點原子《I.MX6U嵌入式Linux驅動開發指南V1.2》
- 野火《i.MX Linux開發實戰指南》