文章目錄
- Linux驅動程序分類
- Linux應用程序和驅動程序的關系
- 簡單的測試驅動程序
- 在petalinux中添加LED驅動
- 新字符設備驅動
Linux驅動程序分類
驅動程序分為字符設備驅動、塊設備驅動和網絡設備驅動。
字符設備是按字節訪問的設備,比如以一個字節收發數據的串口,字符設備在Linux外設中占比最大。
塊設備的特點是按一定格式存取的數據,具體的格式由文件系統決定。塊設備以存儲設備為主,存儲設備的特點是以存儲塊為基礎,因此得名塊設備。
網絡設備不同于上面兩種,應用程序和網絡設備驅動之間的通信由庫和內核提供的一套數據包傳輸函數替代了open()、read()、write()等函數。
Linux應用程序和驅動程序的關系
(1)應用程序調用庫函數提供的open()函數打開某個設備文件,該設備文件是在驅動加載成功之后在目錄/dev中生成的,是應用程序調用相應硬件的入口。
(2)庫根據open()函數的輸入參數引起CPU異常進入內核,系統調用處于內核空間,應用程序無法直接訪問,因此需要陷入到內核,方法就是軟中斷,陷入內核后還要指定系統調用號;
(3)內核的異常處理函數根據輸入參數找到相應的驅動程序,返回文件句柄給庫,庫函數再返回給應用程序;
(4)應用程序再使用得到的文件句柄調用write()、read()等函數發出控制指令;
(5)庫根據write()、read()等函數的輸入參數引起CPU異常,進入內核;
(6)內核的異常處理函數根據輸入參數調用相應的驅動程序執行相應的操作。
Linux應用程序調用驅動程序的步驟如下圖所示。
應用程序中涉及到的open()、read()、write()等是由庫提供的系統調用,通過執行某條指令引發異常進入內核,是應用程序操作硬件的途徑。應用程序執行系統調用后進入內核,然后會使用驅動程序中對應的函數,驅動程序中的open()、read()、write()等函數是需要驅動開發人員實現的。應用程序運行于用戶空間,驅動程序運行于內核空間,Linux系統可以通過MMU限制應用程序運行在某個內存塊中,以避免這個應用程序出錯導致整個系統崩潰,運行于內核空間的驅動程序是系統的一部分,驅動程序出錯有可能牽連整個系統。
簡單的測試驅動程序
在進行LED驅動開發之前,先使用下面的代碼簡單測試一下。
#include <linux/module.h>static int __init chardev_init(void)
{printk("Hello!\n");return 0;
}static void __exit chardev_exit(void)
{printk("GoodBye!\n");
}module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
如果使用Linux內核編譯上面的C文件,需要自己寫Makefile,然后編譯出驅動文件到7020開發板上驗證,驗證的結果如下圖所示。
提示這個驅動是無效的模塊格式,可能ZYNQ開發板就需要使用petalinux這樣特定的工具進行開發,下面來看具體流程。
首先在petalinux的安裝路徑下設置環境變量。
source /opt/pkg/petalinux/settings.sh
進入到定制系統的根目錄下,使用下面的命令添加字符設備驅動。
petalinux-create -t modules -n chardev
在當前路徑下的/project-spec/meta-user/recipes-modules/下生成了一個名為chardev的文件夾,該文件夾下有以下三個文件,其中.c文件就是需要寫入驅動代碼的文件。
Makefile在創建工程的時候已經創建好了,里面的內容如下圖所示,也不需要修改。
在C文件中寫入驅動代碼后,返回到自定義的/zynq7020目錄下,使用下面的命令進行編譯。
petalinux-build -c chardev
編譯完成后的信息打印如下圖所示。
由于編譯成的驅動文件存放路徑比較難找,因此直接在搜索欄中直接搜索驅動的名稱就會出現,但是文件夾必須打開到自定義的工程的這一層才能搜索到。
可以右鍵該文件打開文件的具體存在位置為/opt/pkg/petalinux/zynq7020/build/tmp/sysroots-components/plnx_zynq7/chardev/lib /modules/4.14.0-xilinx-v2018.3/extra。
接下來就可以在開發板上驗證該驅動了,驗證的結果如下圖所示。
在petalinux中添加LED驅動
同上面的示例,先在petalinux的安裝路徑下設置環境變量。
source /opt/pkg/petalinux/settings.sh
然后進入到定制系統的根目錄下,使用下面的命令添加驅動。
petalinux-create -t modules -n psled1-driver
需要注意的是,驅動文件命名不能使用下劃線,而要使用"-"代替。
創建成功以后,打印的消息提示創建的模塊在當前路徑下的/project-spec/meta-user/recipes-modules/psled1-driver中,進到這個目錄下。
一步步進到最終的目錄下,在files文件夾下的.c文件就是要寫入驅動代碼的地方,在里面鍵入下面的代碼。
//該代碼來自ZYNQ教程,教程在文末給出
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h> /* 驅動名稱 */
#define DEVICE_NAME "ps_led1"
/* 驅動主設備號 */
#define GPIO_LED_MAJOR 200 /* gpio寄存器虛擬地址 */
static unsigned int gpio_add_minor;
/* gpio寄存器物理基地址 */
#define GPIO_BASE 0xE000A000
/* gpio寄存器所占空間大小 */
#define GPIO_SIZE 0x1000
/* gpio方向寄存器 */
#define GPIO_DIRM_0 (unsigned int *)(0xE000A204 - GPIO_BASE + gpio_add_minor)
/* gpio使能寄存器 */
#define GPIO_OEN_0 (unsigned int *)(0xE000A208 - GPIO_BASE + gpio_add_minor)
/* gpio控制寄存器 */
#define GPIO_DATA_0 (unsigned int *)(0xE000A040 - GPIO_BASE + gpio_add_minor) /* 時鐘使能寄存器虛擬地址 */
static unsigned int clk_add_minor;
/* 時鐘使能寄存器物理基地址 */
#define CLK_BASE 0xF8000000
/* 時鐘使能寄存器所占空間大小 */
#define CLK_SIZE 0x1000
/* AMBA外設時鐘使能寄存器 */
#define APER_CLK_CTRL (unsigned int *)(0xF800012C - CLK_BASE + clk_add_minor) /* open函數實現, 對應到Linux系統調用函數的open函數 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{ /* 把需要修改的物理地址映射到虛擬地址 */gpio_add_minor = (unsigned int)ioremap(GPIO_BASE, GPIO_SIZE); clk_add_minor = (unsigned int)ioremap(CLK_BASE, CLK_SIZE);/* MIO_0時鐘使能 */ *APER_CLK_CTRL |= 0x00400000; /* MIO_0設置成輸出 */ *GPIO_DIRM_0 |= 0x00000001; /* MIO_0使能 */ *GPIO_OEN_0 |= 0x00000001; printk("gpio_test module open\n"); return 0;
} /* write函數實現, 對應到Linux系統調用函數的write函數 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
{ int rst; char writeBuf[5] = {0}; printk("gpio_test module write\n"); rst = copy_from_user(writeBuf, buf, len); if(0 != rst) { return -1; } if(1 != len) { printk("gpio_test len err\n"); return -2; } if(1 == writeBuf[0]) { *GPIO_DATA_0 &= 0xFFFFFFFE; printk("gpio_test ON\n"); } else if(0 == writeBuf[0]) { *GPIO_DATA_0 |= 0x00000001; printk("gpio_test OFF\n"); } else { printk("gpio_test para err\n"); return -3; } return 0;
} /* release函數實現, 對應到Linux系統調用函數的close函數 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{ printk("gpio_test module release\n"); return 0;
} /* file_operations結構體聲明, 是上面open、write實現函數與系統調用函數對應的關鍵 */
static struct file_operations gpio_leds_fops = { .owner = THIS_MODULE, .open = gpio_leds_open, .write = gpio_leds_write, .release = gpio_leds_release,
}; /* 模塊加載時會調用的函數 */
static int __init gpio_led_init(void)
{ int ret; /* 通過模塊主設備號、名稱、模塊帶有的功能函數(及file_operations結構體)來注冊模塊 */ ret = register_chrdev(GPIO_LED_MAJOR, DEVICE_NAME, &gpio_leds_fops); if (ret < 0) { printk("gpio_led_dev_init_error\n"); return ret; } else { /* 注冊成功 */ printk("gpio_led_dev_init_ok\n"); } return 0;
} /* 卸載模塊 */
static void __exit gpio_led_exit(void)
{ /* 釋放對虛擬地址的占用 */ iounmap((unsigned int *)gpio_add_minor); iounmap((unsigned int *)clk_add_minor); /* 注銷模塊, 釋放模塊對這個設備號和名稱的占用 */ unregister_chrdev(GPIO_LED_MAJOR, DEVICE_NAME);printk("gpio_led_dev_exit_ok\n");
} /* 標記加載、卸載函數 */
module_init(gpio_led_init);
module_exit(gpio_led_exit); /* 驅動描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("GPIO LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
上面介紹的是直接編譯驅動,也可以以圖形化的形式進行編譯,返回到自定義的/zynq7020目錄下,輸入下面的命令配置根文件系統。
petalinux-config -c rootfs
在彈出的圖形化配置窗口中選擇modules進入子菜單中。
按Y鍵將該驅動包括進來,然后保存退出。
根文件系統就配置成功了,然后使用petalinux-build命令編譯該工程。
編譯成功后打印下面的信息。
在/zynq7020目錄下搜索驅動文件,如下圖所示。
右鍵該文件選擇打開文件存放位置,其存放在/zynq7020/build/tmp/sysroots-components/plnx_zynq7/psled1-driver/lib/modules/4.14.0-xilinx-v2018.3/extra,還是比較難找的,所以以后直接在工程目錄下搜索即可。
在開發板上加載驅動,可以看到相應的設備號已經出現了。
使用下面的命令創建字符設備文件,指定主設備號和次設備號,設備文件名稱為psled1,之后寫應用程序的時候要使用該名稱來操作字符設備。
mknod /dev/psled1 c 200 0
創建設備文件成功之后在/dev目錄下就可以看到新添加的設備,如下圖所示。
接下來寫一個應用端的測試程序,用來傳入數據點亮或者熄滅LED,程序如下。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[])
{int fd;int status;fd = open("/dev/psled1", O_RDWR);if(fd < 0){perror("open /dev/psled1 error!\n"); return fd; }status = atoi(argv[1]);write(fd, &status, 1);close(fd); return 0;
}
將上面的程序通過交叉編譯工具編譯出適合在ARM平臺運行的文件,將其發送到開發板驗證,結果如下圖所示。
如果采用下面的應用程序進行測試,LED將每隔一秒改變一下狀態。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>int main(int argc, char *argv[])
{int fd;int status;fd = open("/dev/psled1", O_RDWR);if(fd < 0){perror("open /dev/psled1 error!\n"); return fd; }while(1){status = 1;write(fd, &status, 1);sleep(1);status = 0;write(fd, &status, 1);sleep(1);}close(fd); return 0;
}
在開發板上執行后的結果如下圖所示。
開發板上PS LED1的狀態開始循環亮滅,如下動圖所示。
終端里也是每隔一秒打印一次LED關閉或打開的狀態。
卸載驅動程序后打印下面的信息。
新字符設備驅動
上面驅動代碼中將設備號寫死了,這樣做有很多不便之處,因為編譯驅動代碼前需要查看目標系統中設備號的占用情況,驅動注冊函數中僅有主設備號沒有次設備號,這意味著一個設備會占用所有的次設備號,十分浪費資源。針對這些問題,Linux內核提出了新的字符設備注冊方法,并由內核來管理設備號。
注冊字符設備號的函數原型如下。
int register_chrdev_region(dev_t from,unsigned count,const char* name);
from :需要申請的起始設備號,取代了原有的主設備號和次設備號,在需要指定主次設備號的情況下,可以通過方法from = MKDEV(major,minor); 來實現。
count :需要申請的設備號個數。
name :設備名稱。
在不需要指定主次設備號的情況下,設備號由內核來分配,傳入指針來獲取設備號,注冊、注銷設備號的函數原型如下。
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char* name);
void unregister_chrdev_region(dev_t from,unsigned count);
dev :設備號指針,注冊成功之后,主次設備號可以通過 major = MAJOR(dev); minor = MINOR(dev); 來獲取。
baseminor :次設備號的起始地址。
新的注冊方法使用cdev結構體來定義一個字符設備,cdev結構體如下。
struct cdev
{struct kobject kobj;struct module *owner;const struct file_operations *ops;struct list_head list;dev_t dev;unsigned int count;
};
cdev結構體的初始化函數原型如下。
void cdev_init(struct cdev *cdev,const struct file_operations *fops);
注冊、注銷字符設備的函數原型如下。
int cdev_add(struct cdev *cdev,dev_t dev,unsigned count);
void cdev_del(struct cdev *cdev);
類的創建和刪除函數原型如下。
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key);
void class_destroy(struct class *cls);
owner指定為THIS_MODULE,name是類的名稱,第三個參數可以省略。
設備節點的創建和刪除函數原型如下。
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void * drvdata, const char *fmt, ...);
void device_destroy(struct class *class, dev_t devt);
class是通過class_create創建的類,parent是父設備,無則填NULL,devt是設備號,drvdata是設備可能用到的數據,沒有則填NULL,fmt是設備名,創建成功后在/dev下生成。
下面代碼使用的是新字符設備方法。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>#define DEVICE_NAME "psled1"
#define DEVID_COUNT 1 //設備號個數
#define DRIVE_COUNT 1 //驅動個數/* gpio寄存器虛擬地址 */
static unsigned int gpio_add_minor;
/* gpio寄存器物理基地址 */
#define GPIO_BASE 0xE000A000
/* gpio寄存器所占空間大小 */
#define GPIO_SIZE 0x1000
/* gpio方向寄存器 */
#define GPIO_DIRM_0 (unsigned int *)(0xE000A204 - GPIO_BASE + gpio_add_minor)
/* gpio使能寄存器 */
#define GPIO_OEN_0 (unsigned int *)(0xE000A208 - GPIO_BASE + gpio_add_minor)
/* gpio控制寄存器 */
#define GPIO_DATA_0 (unsigned int *)(0xE000A040 - GPIO_BASE + gpio_add_minor) /* 時鐘使能寄存器虛擬地址 */
static unsigned int clk_add_minor;
/* 時鐘使能寄存器物理基地址 */
#define CLK_BASE 0xF8000000
/* 時鐘使能寄存器所占空間大小 */
#define CLK_SIZE 0x1000
/* AMBA外設時鐘使能寄存器 */
#define APER_CLK_CTRL (unsigned int *)(0xF800012C - CLK_BASE + clk_add_minor) #if 0
struct chardev
{dev_t devid; //設備號struct cdev cdev; //字符設備struct class *class; //類struct device *device; //設備節點
};static struct chardev alinx_char = {.cdev = {.owner = THIS_MODULE,},
};
#endifdev_t devid; //設備號
struct cdev cdev; //字符設備
struct class *class; //類
struct device *device; //設備節點static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{ gpio_add_minor = (unsigned int)ioremap(GPIO_BASE, GPIO_SIZE); clk_add_minor = (unsigned int)ioremap(CLK_BASE, CLK_SIZE); /* MIO_0時鐘使能 */ *APER_CLK_CTRL |= 0x00400000; /* MIO_0設置成輸出 */ *GPIO_DIRM_0 |= 0x00000001; /* MIO_0使能 */ *GPIO_OEN_0 |= 0x00000001; printk("gpio_test module open\n"); return 0;
} static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
{ int rst; char writeBuf[5] = {0}; printk("gpio_test module write\n"); rst = copy_from_user(writeBuf, buf, len); if(0 != rst) { return -1; } if(1 != len) { printk("gpio_test len err\n"); return -2; } if(1 == writeBuf[0]) { *GPIO_DATA_0 &= 0xFFFFFFFE; printk("gpio_test ON\n"); } else if(0 == writeBuf[0]) { *GPIO_DATA_0 |= 0x00000001; printk("gpio_test OFF\n"); } else { printk("gpio_test para err\n"); return -3; } return 0;
} static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{ printk("gpio_test module release\n"); return 0;
} static struct file_operations chardev_fops = { .owner = THIS_MODULE, .open = gpio_leds_open, .write = gpio_leds_write, .release = gpio_leds_release,
}; static int __init gpio_led_init(void)
{ #if 0alloc_chrdev_region(&alinx_char.devid, 0, DEVID_COUNT, DEVICE_NAME); //注冊設備號cdev_init(&alinx_char.cdev, &chardev_fops); //初始化字符設備結構體cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT); //注冊字符設備 alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME); //創建類if(IS_ERR(alinx_char.class)) {return PTR_ERR(alinx_char.class);}alinx_char.device = device_create(alinx_char.class, NULL, alinx_char.devid, NULL, DEVICE_NAME); //創建設備節點printk("alloc success, major = %d minor = %d\n",MAJOR(alinx_char.devid),MINOR(alinx_char.devid));if (IS_ERR(alinx_char.device)) {return PTR_ERR(alinx_char.device);}#endifalloc_chrdev_region(&devid, 0, DEVID_COUNT, DEVICE_NAME); //注冊設備號cdev.owner = THIS_MODULE;cdev_init(&cdev, &chardev_fops); //初始化字符設備結構體cdev_add(&cdev, devid, DRIVE_COUNT); //注冊字符設備 class = class_create(THIS_MODULE, DEVICE_NAME); //創建類if(IS_ERR(class)) {return PTR_ERR(class);}device = device_create(class, NULL, devid, NULL, DEVICE_NAME); //創建設備節點printk("alloc success, major = %d minor = %d\n",MAJOR(devid),MINOR(devid));if (IS_ERR(device)) {return PTR_ERR(device);}return 0;
}static void __exit gpio_led_exit(void)
{ iounmap((unsigned int *)gpio_add_minor); iounmap((unsigned int *)clk_add_minor); #if 0cdev_del(&alinx_char.cdev); //注銷字符設備unregister_chrdev_region(alinx_char.devid, DEVID_COUNT); //注銷設備號device_destroy(alinx_char.class, alinx_char.devid); //刪除設備節點class_destroy(alinx_char.class); //刪除類#endifcdev_del(&cdev); //注銷字符設備unregister_chrdev_region(devid, DEVID_COUNT); //注銷設備號device_destroy(class, devid); //刪除設備節點class_destroy(class); //刪除類printk("gpio_led_dev_exit_ok\n");
} module_init(gpio_led_init);
module_exit(gpio_led_exit);
MODULE_LICENSE("GPL");
加載驅動之后,內核就會為設備指定主設備號和次設備號,不用再使用命中自己指定了。
參考文檔:course_s6_ZYNQ那些事兒-Linux驅動篇V1.05