1. 程式人生 > >ZedBoard學習手記(五)為自定義外設編寫Linux驅動

ZedBoard學習手記(五)為自定義外設編寫Linux驅動

原文地址http://www.eefocus.com/nightseas/blog/12-11/288078_7a826.html#articletop

寫完上一篇部落格後,部門開了新專案,雖然只是開始,但是兔子也不敢懈怠,加之北京氣溫驟降,又颳起大風,可能是天冷的原因吧,胃又不太舒服了,白天忙完了晚上回來就頓覺十分疲憊,因而這篇手記一直拖到現在才動筆。

經過前面的工作,現在終於可以開始為自定義外設編寫驅動了。首先宣告兔子不是搞軟體的,而是個硬體工程師,有時候也肩負起邏輯的工作,因而做的有問題的地方還需要童鞋們指出,共同進步啊。其實Linux驅動程式與一般的微控制器C程式差別不大,只是在呼叫硬體裝置的同時,實現了一個與作業系統的標準介面,只要完成了驅動部分,上層軟體就能通過Linux系統的標準介面來訪問裝置,而不用關心暫存器等具體的硬體問題。

不過為了讓不熟悉驅動和軟體的同學不至於一頭霧水,就稍稍做些普及工作吧。

下面是一個簡單的驅動模組:

#include

#include

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)

{

     printk("Module init complete!\n");

     return 0;

}

static void hello_exit(void)

{

     printk("Module exit!\n");

}

module_init(hello_init);

module_exit(hello_exit);

MODULE_AUTHOR("

[email protected]");

MODULE_DESCRIPTION("HelloWorldDriver");

MODULE_ALIAS("It's only a test");

它為我們展現了驅動程式模組最基本的封裝形式,init函式會在載入模組時執行,exit函式則在模組解除安裝時執行,在ZedBoard板上載入這個驅動,相應的字串資訊會通過串列埠打印出來。

之前我們已經提到了,對自定義外設的訪問實際就是操作特定的實體地址空間,但Linux系統直接操作物理空間缺乏可移植性也不安全,因此需要對這個實體地址空間對映到虛擬地址空間(具體應該是通過MMU實現的吧,留作疑問),轉變成系統能夠直接訪問的空間。對映是通過ioremap實現的,這個函式定義在Linux核心的asm/io.h檔案中。

對裝置進行對映的操作方法大體如下:

static void __iomem *GPIO_Regs;

printk("my_gpio: Access address to device is:0x%x\n", (unsignedint)GPIO_Regs);

GPIO_Regs指標指向對映後的空間,通過ioremap函式,為I/O地址為MY_GPIO_PHY_ADDR的裝置分配了MY_GPIO_REG_NUM大小的空間。對映後的虛擬地址將通過核心打印出來。

當有了可以直接操作的地址後,通過iowrite32和ioread32就可以簡單便捷地實現寫入和讀取資料的操作。

int val;

val = ioread32(GPIO_Regs);

printk("my_gpio: Read 0x%x from switches, writing to LED...", val);  

iowrite32(val, GPIO_Regs+4);

printk("OK!\n");

執行上述程式碼,GPIO_Regs開頭地址處的32位資料——自定義外設暫存器0,也就是Switch狀態——會被讀取,並寫入到偏移4位元組(32位)之後的地址中,即自定義外設的暫存器1,控制LED的狀態。將這段程式碼加入到上面提到的init函式中,當驅動模組載入後,8個開關的電平狀態會被讀取,並在LED的8個管腳上輸出,使LED亮滅與Switch開關保持一致。

這就是通過對靜態I/O地址(MY_GPIO_PHY_ADDR就是我們在前幾篇中設定的my_gpio外設地址0x75c80000)對映的方式,讓Linux系統有能力訪問裝置。那麼Device Tree又是做什麼用的呢,我曾就這個問題在網上詢問一位國外的工程師,現將他的回答摘錄如下:

The standard way is to add an entry for your peripheral in Linux’ device tree, and have the driver fetch the physical address from there. But for a on-off project, hardcoding the physical address of the peripheral in the driver is fairly acceptable.

事實上,在MicroBlaze中,就可以通過of_platform實現讀取DeviceTree的compatible資訊,來匹配裝置和驅動:

static struct of_device_id my_gpio_of_match[] __devinitdata = {

 { .compatible = "xlnx, my_gpio-1.00.a", },

 {}

};

只是兔子還沒在Zynq平臺上成功實現過,而專案的時間又那麼緊,於是就採取了“acceptable”的方法了,哈哈。有興趣的童鞋可以繼續深究。

當然,只實現對裝置的訪問還是不夠的,這樣的驅動,只有在載入或解除安裝時會執行一些操作,而無法被Linux下的應用程式訪問。因此,我們還要完成一個對上層的介面,這需要利用Linux的檔案系統。下面的兩個結構體正是實現了這樣的功能:

static const struct file_operations my_gpio_fops =

{

     .owner = THIS_MODULE,

     .open = my_gpio_open,

     .release = my_gpio_release,

     .read = my_gpio_read,

     .unlocked_ioctl = my_gpio_ioctl,

};

static struct miscdevice my_gpio_dev =

{

     .minor = MISC_DYNAMIC_MINOR,

     .fops = &my_gpio_fops,

};

兔子在這裡定義了一個MISC型的裝置,併為之建立了open、release、read和ioctl的方法,open和release是開啟和釋放裝置的操作,這裡並無實際功能。

static int my_gpio_open(struct inode * inode , struct file * filp)

{

  return 0;

}

static int my_gpio_release(struct inode * inode, struct file *filp)

{

  return 0;

}

ioctl就是寫暫存器操作的封裝函數了,使用的方法依舊是iowrite32,其中reg_num代表暫存器編號,arg是要寫入的32位值。

static int my_gpio_ioctl(struct file *filp, unsigned int reg_num,unsigned long arg)

{

  if(reg_num>=0 && reg_num<my_gpio_reg_num)

  {

    iowrite32(arg, GPIO_Regs+reg_num*4);

    printk("my_gpio: Write 0x%x to 0x%x!\n", arg, GPIO_Regs+reg_num*4);

  }

  else

  {

    printk("my_gpio:[ERROR] Wrong register number!\n");

    return -EINVAL;

  }

  return 0;

}

read方法則可以將讀取到的資料傳遞給上層,由於其返回值為char型,因此要分幾次讀取(當然使用ioread32再拆分也可以啦)。

static int my_gpio_read(struct file *filp, char *buffer, size_t length, loff_t * offset)

{

  int bytes_read = 0;

  int i=0;

  if (filp->f_flags & O_NONBLOCK)

            return -EAGAIN;

  if (length>0 && length<=(MY_GPIO_REG_NUM*4))

  {

    for(i=0;i<length;i++)

    {

      *(buffer+i)=(char)ioread8(GPIO_Regs+i);

    }

    bytes_read=i;

  }

  return bytes_read;

}

再使用misc_register函式對裝置進行註冊:

int ret;

設備註冊後,會在dev目錄下生成一個以DEVICE_NAME值命名的檔案(兔子這裡是“my_gpio_dev”)。操作這個檔案,就相當於操作我們的裝置了,具體使用的也就是檔案操作的open、close、read等函式。驅動寫完後,還需要進行編譯,兔子寫了一個簡單的Makefile檔案,用於生成可被系統載入的.ko檔案,在原始檔和Makefile的目錄下輸入 make 指令即可。KERN_SRC指的則是linux核心的路徑。Makefile內容如下:

# Cross compiler makefile for my_gpio

# By Nightmare @ EEFOCUS 2012-10-10

KERN_SRC=/arm/zed/linux-3.3-digilent

obj-m := my_gpio.o

all:

     make -C $(KERN_SRC) ARCH=arm M=`pwd` modules

clean:

     make -C $(KERN_SRC) ARCH=arm M=`pwd=` clean

下面是完整的驅動程式碼、Makefile檔案和生成的.ko模組檔案:

將編譯好的驅動檔案通過U盤載入到ZedBoard中:

mount /dev/sda2 /mnt

insmod /mnt/my_gpio.ko

解除安裝驅動的指令為rmmod:

rmmod my_gpio

載入裝置時,LED會根據Switch的狀態來實現亮滅。怎麼樣,Linux下的自定義外設驅動其實挺簡單吧。上效果圖:

歡迎大家在美信DIY大賽專區論壇討論問題:

更多ZedBoard資料請見美信社群: