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("
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資料請見美信社群: