【Linux筆記】通俗易懂的LED驅動筆記

前言

上一篇我們分享了字符設備驅動框架,當時分享的是hello驅動程序。學STM32我們從點燈開始,學Linux驅動我們自然也要點個燈來玩玩,儘量在從這些基礎例程中榨取知識,細摳、細摳,爲之後更復雜的知識打好基礎。

與硬件無關的LED驅動

回顧hello驅動程序,我們的根據實際需求對其進行寫字符串與讀字符串操作。這裏我們當然也要根據實際來思考我們的LED驅動程序。在STM32點燈的時候,一般輸出低電平點燈,輸出高電平滅燈。在嵌入Linux操作系統的情況下,我們自然也要想到有個寫1/0的思想。類比我們上一篇的hello程序:

======001

我們的LED程序自然要寫入的數據爲0/1來點亮、熄滅LED。這裏我們做的實驗室與硬件無關的LED實驗:我們的驅動程序在收到應用程序發送過來的0時打印led on、收到1時打印led off。模仿上一篇的hello程序,我們修改得到的與硬件無關的LED程序(核心部分)如下:

LED應用程序:

======002

LED驅動程序:

======003_1

======003

加載led驅動模塊及運行應用程序:

======004

與硬件有關的LED驅動

上面那一節分享的是與硬件無關的LED驅動實驗,主要是爲了理清LED驅動的大體思路。這裏我們再加入與硬件有關的相關操作以構造與硬件有關的LED驅動程序。

我們在進行STM32的裸機編程的時候,對一些外設進行配置其實就是操作一些地址的過程,這些外設地址在芯片手冊中可以看到:

======005

這是地址映射圖,這裏圖中只是列出的外設的邊界地址,每個外設又有很多寄存器,這些寄存器的地址都是對外設基地址進行偏移得到的。同樣的,對於NXP的IMX6ULL芯片來說,也是有類似這樣的地址的:

======006

此時我們要編寫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函數進行修改:

======007

======008

與STM32一樣,對於i.MX 6ULL的GPIO外設來說,也有很多寄存器:

======009

上面我們只是點一個燈,如果是要點多個燈呢?那就得操控多個GPIO。如果進行地址映射的寫法還像上面那樣,代碼就會顯得很臃腫。回想一下我們STM32,GPIO外設通過結構體來管理它的寄存器:

======010

這裏的__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;
};

結構體裏的成員排序是要按照特定順序來的:

======011

因爲這些寄存器都是相對於GPIO外設的基地址作偏移得到的,比如:

======012

不能打亂順序,否則就不能正確訪問到對應的寄存器了。用結構體進行管理之後,我們就可以用類似下面的方式進行映射:

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節進行學習

下面來簡單地梳理一下:

======013

由於篇幅問題,具體的部分就不貼出來了。

之前的筆記中:C語言、嵌入式重點知識:回調函數中我也有提到通用專用的含義,可以瞭解瞭解加深對這兩個詞的認識。

這裏我們學到了很重要的思想軟件分層的思想及技巧,但也只是點了一下,未來的路還很長,需要持續學習,繼續提高。

以上就是本次的分享,如有錯誤,歡迎指出!謝謝

參考/學習資料:

  • 百問網《嵌入式Linux應用開發完全手冊第2版》
  • 正點原子《I.MX6U嵌入式Linux驅動開發指南V1.2》
  • 野火《i.MX Linux開發實戰指南》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章