linux驅(qū)動(dòng)是連接軟件和硬件的一個(gè)中間介質(zhì),實(shí)現(xiàn)了對(duì)硬件的配置和控制。進(jìn)一步將硬件抽象化,為軟件操作硬件提供了簡(jiǎn)單的接口。不論硬件的具體形式如何,linux驅(qū)動(dòng)都將其映射到一個(gè)文件,軟件端對(duì)硬件的讀寫(xiě)操作等都被抽象成文件操作了。本篇從hello world開(kāi)始,簡(jiǎn)要介紹驅(qū)動(dòng)的基本結(jié)構(gòu),然后再進(jìn)一步介紹LED硬件的搭建,以及驅(qū)動(dòng)的編寫(xiě),設(shè)備樹(shù)的修改。讓大家對(duì)linux驅(qū)動(dòng)有一個(gè)基本的認(rèn)識(shí)。
1. Hello world驅(qū)動(dòng)
hello world幾乎成了所有編程書(shū)的第一個(gè)程序,用來(lái)介紹程序的大體結(jié)構(gòu)。一個(gè)簡(jiǎn)單的hello world讓人感覺(jué)這本書(shū)學(xué)起來(lái)真的很容易,不知不覺(jué)就進(jìn)入圈套。被誘導(dǎo)入坑的我,也來(lái)用一個(gè)hello world誘別人入坑。先上程序:
#include
static int hello_init(void){
printk(KERN_INFO "Hello world/n");
return 0;
}
static void hello_exit(void){
printk(KERN_INFO "Goodbye, cruel world/n");
}
module_init(hello_init);
module_exit(hello_exit);
一個(gè)驅(qū)動(dòng)的使用過(guò)程包括:模塊的裝載,軟件調(diào)用,模塊卸載。程序中module_init和module_exit是內(nèi)核中的宏,是一個(gè)驅(qū)動(dòng)必須包含部分。當(dāng)驅(qū)動(dòng)被裝載時(shí),就會(huì)調(diào)用module_init指定的初始化函數(shù)hello_init,而當(dāng)驅(qū)動(dòng)被卸載時(shí),就會(huì)調(diào)用hello_exit函數(shù)。初始化函數(shù)通常都是進(jìn)行設(shè)備樹(shù)檢查,內(nèi)存分配映射,硬件配置,文件和硬件關(guān)聯(lián)等操作。清除函數(shù)用于釋放內(nèi)存,硬件的清零等操作。通常驅(qū)動(dòng)還會(huì)定義一些文件IO操作,比如write,read,ioctrl等。Hello world只給出一個(gè)驅(qū)動(dòng)編寫(xiě)格式和流程,文件操作在LED驅(qū)動(dòng)中再介紹。MODULE_LISENCE用于告訴內(nèi)核該模塊采用的許可證類(lèi)型,這個(gè)一般和linux內(nèi)核采用
的許可證一致就好了。
現(xiàn)在來(lái)看驅(qū)動(dòng)模塊是如何編譯的,看Makefile:
obj-m:=hello.o
ARCH=arm
CROSS_COMPILE=arm-xilinx-linux-gnueabi-
CC:=$(CROSS_COMPILE)gcc
LD:=$(CROSS_COMPILE)ld
KERNELDIR:=/home/anpingbo/Design/linux/linux-xlnx
PWD:=$(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
如果我們要構(gòu)造的模塊名稱(chēng)為hello.ko,那么就需要指定obj-m為hello.o,這個(gè)是hello.ko生成的依賴(lài)選項(xiàng)。是在zynq平臺(tái)上編譯模塊,需要制定ARCH類(lèi)型為arm,以及交叉編譯工具,如果使用本機(jī)linux系統(tǒng)默認(rèn)gcc就不能在zynq平臺(tái)下加載模塊,這里的交叉工具鏈為arm-xilinx-linux-gnueabi-gcc,arm-xilinx-linux-gnueabi-ld是交叉連接器,把程序鏈接成可以在arm平臺(tái)運(yùn)行的模塊。同時(shí)還需要制定內(nèi)核系統(tǒng)文件夾,因?yàn)槟K編譯要用到內(nèi)核的庫(kù)。All下就是編譯模塊了。設(shè)置好交叉工具的環(huán)境變量后,直接執(zhí)行make,機(jī)會(huì)生成hello.ko,這個(gè)就是編譯好的驅(qū)動(dòng)模塊。
圖1.1 編譯hello驅(qū)動(dòng)過(guò)程
我們打開(kāi)zynq系統(tǒng),加載hello.ko,加載使用insmod命令,卸載使用rmmod命令。我們發(fā)現(xiàn)沒(méi)有打印出任何信息,這是不是模塊編譯有錯(cuò)誤,其實(shí)模塊沒(méi)有錯(cuò)誤,流程也都對(duì)。因?yàn)閜rintk打印函數(shù)是有級(jí)別的,只有低于這個(gè)級(jí)別值才能打印到terminal中。我們可以修改內(nèi)核級(jí)別,比如我們將級(jí)別降低:
echo 8 > /proc/sys/kernel/printk
我們還可以使用dmesg來(lái)查看驅(qū)動(dòng)打印的日志。其會(huì)打印所有驅(qū)動(dòng)加載和卸載的打印日志。
圖1.2 insmod hello.ko
2. LED驅(qū)動(dòng)
2.1 vivado工程
我們通過(guò)axi_gpio來(lái)連接4個(gè)LED燈,通過(guò)linux驅(qū)動(dòng)來(lái)點(diǎn)亮LED燈。Block如圖2.1,我們?cè)O(shè)置gpio寬度為4。還有一個(gè)我們需要用到的就是LED映射到的內(nèi)存,可以在address editor中看到,gpio0是LED的。接下來(lái)就是一般的流程,管腳約束,綜合,編譯。然后導(dǎo)出工程,打開(kāi)SDK,新建fsbl,編譯,生成設(shè)備樹(shù),制作boot.bin。
圖2.1 LED硬件工程
2.2 LED驅(qū)動(dòng)
LED屬于字符類(lèi)驅(qū)動(dòng),符合字符類(lèi)驅(qū)動(dòng)的寫(xiě)法。Linux內(nèi)核中每個(gè)模塊實(shí)際上是以文件形式存在的,這些文件存放于/dev目錄下。而不同的驅(qū)動(dòng)模塊具有不同的主設(shè)備號(hào)和次設(shè)備號(hào)。主設(shè)備號(hào)用于區(qū)別不同的驅(qū)動(dòng),而次設(shè)備號(hào)用于更具體的指向驅(qū)動(dòng)指向的設(shè)備。設(shè)備號(hào)就相當(dāng)于門(mén)牌號(hào),用于唯一區(qū)別不同驅(qū)動(dòng)。編寫(xiě)驅(qū)動(dòng)過(guò)程中就需要為驅(qū)動(dòng)分配設(shè)備號(hào)。現(xiàn)在進(jìn)一步分析LED驅(qū)動(dòng)代碼:
#define LED_DATA 0x41200000
#define LED_CTRL 0x41200004
這兩行定義了LED數(shù)據(jù)控制寄存器和數(shù)據(jù)讀寫(xiě)寄存器的內(nèi)存地址。實(shí)際上驅(qū)動(dòng)對(duì)LED硬件的配置和讀寫(xiě)都是通過(guò)配置其寄存器實(shí)現(xiàn)的。具體要看GPIO的硬件信息。
dev_t led_devt;
void __iomem *baseaddr;
dev_t定義了設(shè)備編號(hào),__iomem定義了linux內(nèi)核的存儲(chǔ)指針。硬件的內(nèi)存需要映射到linux內(nèi)核空間才能操作。在使用和配置LED時(shí),需要先將其物理內(nèi)存映射到linux內(nèi)核的內(nèi)存空間。映射函數(shù)ioremap就是專(zhuān)門(mén)用于IO端口內(nèi)存映射的。
static int led_major=25;
struct cdev *led_dev;
int led_value;
上述定義了一個(gè)led主設(shè)備號(hào)。通過(guò)ls –l命令可以查看/dev下驅(qū)動(dòng)的設(shè)備號(hào)。當(dāng)然這是一種不方便的方式,正常情況下設(shè)備號(hào)也可以自動(dòng)分配。Cdev是字符設(shè)備的結(jié)構(gòu)體,定義如下:
struct cdev {
struct kobject kobj; // 內(nèi)嵌的kobject對(duì)象
struct module *owner; // 所屬模塊
const struct file_operations *ops; // 文件操作結(jié)構(gòu)體
struct list_head list; //linux內(nèi)核所維護(hù)的鏈表指針
dev_t dev; //設(shè)備號(hào)
unsigned int count; //設(shè)備數(shù)目
};
文件操作結(jié)構(gòu)體實(shí)際上提供了軟件操作LED的接口,上文講過(guò)驅(qū)動(dòng)都被映射成/dev下的一個(gè)文件,軟件調(diào)用驅(qū)動(dòng)的時(shí)候,以打開(kāi),讀寫(xiě),關(guān)閉對(duì)應(yīng)設(shè)備文件來(lái)進(jìn)行操控。我們看一下LED驅(qū)動(dòng)中的文件結(jié)構(gòu):
struct file_operations led_fops={
.owner=THIS_MODULE,
.read=led_read,
.write=led_write,
.unlocked_ioctl=led_ioctl,
.open=led_open,
.release=led_release,
};
其中包含了文件應(yīng)該有的四種基本操作open, read, write, release實(shí)際上是close。文件結(jié)構(gòu)體還提供了ioctrl函數(shù),這個(gè)函數(shù)為軟件提供了一種更為靈活的操縱底層硬件的方法。
接下來(lái)對(duì)文件結(jié)構(gòu)體中的每個(gè)函數(shù)進(jìn)行分析。
1) led_open
static int led_open(struct inode *inode, struct file *filp){
struct resource *res;
int reg;
printk("begin: open led/n");
filp->private_data=inode->i_cdev;
res=request_mem_region(LED_DATA, 0x10000, "LED");
if(!res){
printk("failed requesting resource/n");
return 0;
}
printk("begin: remap led/n");
baseaddr=ioremap(LED_DATA, 0x10000);
if(!baseaddr){
printk("ERROR: couldn't allocate baseaddr/n");
return 0;
}
printk("baseaddr is %x/n", baseaddr);
printk("begin: read led/n");
reg=ioread32(baseaddr);
printk("begin: write led %d/n", reg);
reg &= 0xFFFFFFF0;
iowrite32(reg, baseaddr+4);
printk("SUCCESS: gpio init/n");
return 0;
}
內(nèi)核用inode結(jié)構(gòu)在內(nèi)部表示文件,inode結(jié)構(gòu)中包含了大量有關(guān)文件的信息。當(dāng)我們?cè)趌inux中創(chuàng)建一個(gè)文件時(shí),就會(huì)在相應(yīng)的文件系統(tǒng)創(chuàng)建一個(gè)inode與之對(duì)應(yīng)。文件實(shí)體和文件的inode是一一對(duì)應(yīng)的。當(dāng)打開(kāi)文件時(shí),就獲得了inode。通過(guò)inode可以獲得字符設(shè)備結(jié)構(gòu)體i_cdev。Inode在驅(qū)動(dòng)開(kāi)發(fā)中很少進(jìn)行填充,通常都是用于查看。在使用LED時(shí),需要為其分配內(nèi)存,首先通過(guò)函數(shù)request_mem_region來(lái)看是否有空閑linux內(nèi)核內(nèi)存可分配,如果可以就通過(guò)ioremap進(jìn)行分配,返回linux內(nèi)存首地址。之后讀寫(xiě)LED的時(shí)候就可以通過(guò)向這個(gè)地址寫(xiě)數(shù)據(jù)來(lái)控制LED了。iowrite32(reg, baseaddr+4)是在配置LED,使能了LED。
2) led_write
因?yàn)辄c(diǎn)亮LED是輸出數(shù)據(jù),所以實(shí)際上用不上讀操作,只有寫(xiě)操作。
ssize_t led_write(struct file * filp, const char __user *buf, size_t cnt, loff_t *f_ops){
if(copy_from_user(&led_value, buf, cnt))
return -EFAULT;
led_gpio_set();
return 1;
}
void led_gpio_set(void){
iowrite32(led_value, baseaddr);
}
Copy_from_user是linux內(nèi)核從用戶(hù)空間獲得要寫(xiě)入LED的數(shù)據(jù)。Led_gpio_set函數(shù)中通過(guò)iowrite函數(shù)將用戶(hù)空間的數(shù)據(jù)寫(xiě)入LED。
3) led_release
static int led_release(struct inode *inode, struct file *filp){
iounmap(baseaddr);
release_mem_region(LED_DATA, 0x10000);
return 0;
}
這個(gè)是在調(diào)用close函數(shù)的時(shí)候會(huì)調(diào)用這個(gè)函數(shù)來(lái)釋放內(nèi)存,解除LED內(nèi)存映射。
現(xiàn)在再來(lái)看初始化和驅(qū)動(dòng)清除函數(shù):
static void led_setup_dev(int index){
int err;
int devno;
devno=MKDEV(led_major, index);
printk("MKDEV devno %d/n", devno);
cdev_init(led_dev, &led_fops);
printk("cdev_init %d/n", devno);
led_dev->owner=THIS_MODULE;
led_dev->ops=&led_fops;
err=cdev_add(led_dev, devno, 1);
if(err)
printk(KERN_ERR "ERROR: %d adding LED%d", err, index);
printk("SUCCESS: add dev %d/n", devno);
}
static int __init led_init(void){
int result;
printk("INIT:------------/n");
result=alloc_chrdev_region(&led_devt, 0, 1, "LED");
led_major=MAJOR(led_devt);
if(result printk(KERN_ERR "ERROR: allocate chrdev %d", led_devt);
return result;
}
printk("SUCCESS: allocate chrdev %d/n", led_devt);
led_dev=cdev_alloc();
if(!led_dev){
printk(KERN_ERR "ERROR: allocate device mem %d", led_devt);
result=-ENOMEM;
goto fail_malloc;
}
printk("SUCCESS: allocate device mem %d/n", led_devt);
led_setup_dev(0);
printk("SUCCESS: init device %d/n", led_devt);
return 0;
fail_malloc:
unregister_chrdev_region(led_devt, 1);
return result;
}
static void __exit led_cleanup(void){
cdev_del(led_dev);
unregister_chrdev_region(led_devt, 1);
printk("SUCCESS: exit device %d/n", led_devt);
}
LED驅(qū)動(dòng)初始化操作首先要為led_devt動(dòng)態(tài)分配設(shè)備號(hào),然后可以通過(guò)MAJOR來(lái)得到主設(shè)備號(hào)。這個(gè)主設(shè)備號(hào)在將字符設(shè)備添加到驅(qū)動(dòng)中會(huì)用到。獲得了設(shè)備號(hào)后就對(duì)led字符設(shè)備結(jié)構(gòu)體led_dev分配空間,然后調(diào)用函數(shù)led_setup_sev完成對(duì)LED設(shè)備的初始化,添加等。這些就是一般過(guò)程了。作為初學(xué)者,先會(huì)用,以后在驅(qū)動(dòng)的調(diào)試中會(huì)深入去理解。
清除函數(shù)就是釋放LED字符設(shè)備空間。
2.3 LED軟件操作
現(xiàn)在來(lái)看如何在用戶(hù)端操作LED,上代碼:
#include
#include
#include
#include
int main(int argc, char *argv[]){
int fd;
fd=open("/dev/LED", O_RDWR);
if(fd printf("ERROR: cannot open/n");
return 0;
}
int value=0xF;
write(fd, &value, 4);
//close(fd);
return 0;
}
Open函數(shù)打開(kāi)LED字符設(shè)備,然后通過(guò)write函數(shù)來(lái)點(diǎn)亮LED。
2.4 實(shí)驗(yàn)
首先加載led.ko:
圖2.2 加載led驅(qū)動(dòng)
ls查看/dev下發(fā)現(xiàn)并沒(méi)有LED文件,還需要通過(guò)mknod命令為其添加設(shè)備節(jié)點(diǎn),我們才能在用戶(hù)空間進(jìn)行操作。但是如何知道主設(shè)備號(hào)呢?cat /proc/devices可以看到LED的主設(shè)備號(hào)為245。完成LED設(shè)備節(jié)點(diǎn)分配后,就看到/dev下有個(gè)文件LED,這時(shí)候就可以通過(guò)在用戶(hù)空間進(jìn)行寫(xiě)操作了。
圖2.3 查看LED的主設(shè)備號(hào)
圖2.4 分配設(shè)備節(jié)點(diǎn)
總結(jié)
本篇通過(guò)兩個(gè)簡(jiǎn)單的驅(qū)動(dòng)程序,介紹了驅(qū)動(dòng)的基本結(jié)構(gòu),編寫(xiě)方法和編譯過(guò)程。總結(jié)一下就是:通過(guò)init來(lái)進(jìn)行設(shè)備的基本配置,和硬件配置,然后定義文件操作結(jié)構(gòu)體,以及結(jié)構(gòu)體中函數(shù),通過(guò)open,read,write,release等函數(shù)來(lái)控制對(duì)LED的讀寫(xiě)操作。
編輯:hfy
-
led
+關(guān)注
關(guān)注
242文章
23252瀏覽量
660587 -
Linux驅(qū)動(dòng)
+關(guān)注
關(guān)注
0文章
43瀏覽量
9962
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論