linux驅動是連接軟件和硬件的一個中間介質,實現了對硬件的配置和控制。進一步將硬件抽象化,為軟件操作硬件提供了簡單的接口。不論硬件的具體形式如何,linux驅動都將其映射到一個文件,軟件端對硬件的讀寫操作等都被抽象成文件操作了。本篇從hello world開始,簡要介紹驅動的基本結構,然后再進一步介紹LED硬件的搭建,以及驅動的編寫,設備樹的修改。讓大家對linux驅動有一個基本的認識。
1. Hello world驅動
hello world幾乎成了所有編程書的第一個程序,用來介紹程序的大體結構。一個簡單的hello world讓人感覺這本書學起來真的很容易,不知不覺就進入圈套。被誘導入坑的我,也來用一個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);
一個驅動的使用過程包括:模塊的裝載,軟件調用,模塊卸載。程序中module_init和module_exit是內核中的宏,是一個驅動必須包含部分。當驅動被裝載時,就會調用module_init指定的初始化函數hello_init,而當驅動被卸載時,就會調用hello_exit函數。初始化函數通常都是進行設備樹檢查,內存分配映射,硬件配置,文件和硬件關聯等操作。清除函數用于釋放內存,硬件的清零等操作。通常驅動還會定義一些文件IO操作,比如write,read,ioctrl等。Hello world只給出一個驅動編寫格式和流程,文件操作在LED驅動中再介紹。MODULE_LISENCE用于告訴內核該模塊采用的許可證類型,這個一般和linux內核采用
的許可證一致就好了。
現在來看驅動模塊是如何編譯的,看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
如果我們要構造的模塊名稱為hello.ko,那么就需要指定obj-m為hello.o,這個是hello.ko生成的依賴選項。是在zynq平臺上編譯模塊,需要制定ARCH類型為arm,以及交叉編譯工具,如果使用本機linux系統默認gcc就不能在zynq平臺下加載模塊,這里的交叉工具鏈為arm-xilinx-linux-gnueabi-gcc,arm-xilinx-linux-gnueabi-ld是交叉連接器,把程序鏈接成可以在arm平臺運行的模塊。同時還需要制定內核系統文件夾,因為模塊編譯要用到內核的庫。All下就是編譯模塊了。設置好交叉工具的環境變量后,直接執行make,機會生成hello.ko,這個就是編譯好的驅動模塊。
圖1.1 編譯hello驅動過程
我們打開zynq系統,加載hello.ko,加載使用insmod命令,卸載使用rmmod命令。我們發現沒有打印出任何信息,這是不是模塊編譯有錯誤,其實模塊沒有錯誤,流程也都對。因為printk打印函數是有級別的,只有低于這個級別值才能打印到terminal中。我們可以修改內核級別,比如我們將級別降低:
echo 8 > /proc/sys/kernel/printk
我們還可以使用dmesg來查看驅動打印的日志。其會打印所有驅動加載和卸載的打印日志。
圖1.2 insmod hello.ko
2. LED驅動
2.1 vivado工程
我們通過axi_gpio來連接4個LED燈,通過linux驅動來點亮LED燈。Block如圖2.1,我們設置gpio寬度為4。還有一個我們需要用到的就是LED映射到的內存,可以在address editor中看到,gpio0是LED的。接下來就是一般的流程,管腳約束,綜合,編譯。然后導出工程,打開SDK,新建fsbl,編譯,生成設備樹,制作boot.bin。
圖2.1 LED硬件工程
2.2 LED驅動
LED屬于字符類驅動,符合字符類驅動的寫法。Linux內核中每個模塊實際上是以文件形式存在的,這些文件存放于/dev目錄下。而不同的驅動模塊具有不同的主設備號和次設備號。主設備號用于區別不同的驅動,而次設備號用于更具體的指向驅動指向的設備。設備號就相當于門牌號,用于唯一區別不同驅動。編寫驅動過程中就需要為驅動分配設備號。現在進一步分析LED驅動代碼:
#define LED_DATA 0x41200000
#define LED_CTRL 0x41200004
這兩行定義了LED數據控制寄存器和數據讀寫寄存器的內存地址。實際上驅動對LED硬件的配置和讀寫都是通過配置其寄存器實現的。具體要看GPIO的硬件信息。
dev_t led_devt;
void __iomem *baseaddr;
dev_t定義了設備編號,__iomem定義了linux內核的存儲指針。硬件的內存需要映射到linux內核空間才能操作。在使用和配置LED時,需要先將其物理內存映射到linux內核的內存空間。映射函數ioremap就是專門用于IO端口內存映射的。
static int led_major=25;
struct cdev *led_dev;
int led_value;
上述定義了一個led主設備號。通過ls –l命令可以查看/dev下驅動的設備號。當然這是一種不方便的方式,正常情況下設備號也可以自動分配。Cdev是字符設備的結構體,定義如下:
struct cdev {
struct kobject kobj; // 內嵌的kobject對象
struct module *owner; // 所屬模塊
const struct file_operations *ops; // 文件操作結構體
struct list_head list; //linux內核所維護的鏈表指針
dev_t dev; //設備號
unsigned int count; //設備數目
};
文件操作結構體實際上提供了軟件操作LED的接口,上文講過驅動都被映射成/dev下的一個文件,軟件調用驅動的時候,以打開,讀寫,關閉對應設備文件來進行操控。我們看一下LED驅動中的文件結構:
struct file_operations led_fops={
.owner=THIS_MODULE,
.read=led_read,
.write=led_write,
.unlocked_ioctl=led_ioctl,
.open=led_open,
.release=led_release,
};
其中包含了文件應該有的四種基本操作open, read, write, release實際上是close。文件結構體還提供了ioctrl函數,這個函數為軟件提供了一種更為靈活的操縱底層硬件的方法。
接下來對文件結構體中的每個函數進行分析。
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;
}
內核用inode結構在內部表示文件,inode結構中包含了大量有關文件的信息。當我們在linux中創建一個文件時,就會在相應的文件系統創建一個inode與之對應。文件實體和文件的inode是一一對應的。當打開文件時,就獲得了inode。通過inode可以獲得字符設備結構體i_cdev。Inode在驅動開發中很少進行填充,通常都是用于查看。在使用LED時,需要為其分配內存,首先通過函數request_mem_region來看是否有空閑linux內核內存可分配,如果可以就通過ioremap進行分配,返回linux內存首地址。之后讀寫LED的時候就可以通過向這個地址寫數據來控制LED了。iowrite32(reg, baseaddr+4)是在配置LED,使能了LED。
2) led_write
因為點亮LED是輸出數據,所以實際上用不上讀操作,只有寫操作。
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內核從用戶空間獲得要寫入LED的數據。Led_gpio_set函數中通過iowrite函數將用戶空間的數據寫入LED。
3) led_release
static int led_release(struct inode *inode, struct file *filp){
iounmap(baseaddr);
release_mem_region(LED_DATA, 0x10000);
return 0;
}
這個是在調用close函數的時候會調用這個函數來釋放內存,解除LED內存映射。
現在再來看初始化和驅動清除函數:
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驅動初始化操作首先要為led_devt動態分配設備號,然后可以通過MAJOR來得到主設備號。這個主設備號在將字符設備添加到驅動中會用到。獲得了設備號后就對led字符設備結構體led_dev分配空間,然后調用函數led_setup_sev完成對LED設備的初始化,添加等。這些就是一般過程了。作為初學者,先會用,以后在驅動的調試中會深入去理解。
清除函數就是釋放LED字符設備空間。
2.3 LED軟件操作
現在來看如何在用戶端操作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函數打開LED字符設備,然后通過write函數來點亮LED。
2.4 實驗
首先加載led.ko:
圖2.2 加載led驅動
ls查看/dev下發現并沒有LED文件,還需要通過mknod命令為其添加設備節點,我們才能在用戶空間進行操作。但是如何知道主設備號呢?cat /proc/devices可以看到LED的主設備號為245。完成LED設備節點分配后,就看到/dev下有個文件LED,這時候就可以通過在用戶空間進行寫操作了。
圖2.3 查看LED的主設備號
圖2.4 分配設備節點
總結
本篇通過兩個簡單的驅動程序,介紹了驅動的基本結構,編寫方法和編譯過程。總結一下就是:通過init來進行設備的基本配置,和硬件配置,然后定義文件操作結構體,以及結構體中函數,通過open,read,write,release等函數來控制對LED的讀寫操作。
編輯:hfy
-
led
+關注
關注
240文章
23134瀏覽量
658418 -
Linux驅動
+關注
關注
0文章
43瀏覽量
9950
發布評論請先 登錄
相關推薦
評論