1. 字符設(shè)備驅(qū)動簡介
字符設(shè)備是Linux驅(qū)動中最基本的一類設(shè)備驅(qū)動,字符設(shè)備就是一個一個字節(jié),按照字節(jié)流進行讀寫操作的設(shè)備,讀寫數(shù)據(jù)是分先后順序的。 比如常見的點燈、按鍵、IIC、SPI、LCD 等等都是字符設(shè)備,這些設(shè)備的驅(qū)動就叫做字符設(shè)備驅(qū)動。
Linux驅(qū)動基本原理 :Linux中一切皆為文件,驅(qū)動加載成功后會在/dev目錄下生成一個相應的文件,應用程序通過對這個名為/dev/xxx的文件進行相應的操作即可實現(xiàn)對硬件的操作。
比如LED驅(qū)動,會有/dev/led驅(qū)動文件,應用程序使用open函數(shù)來打開該文件; 若要點亮或關(guān)閉led,就使用write函數(shù)寫入開關(guān)值; 若要獲取led燈的狀態(tài),就用read函數(shù)從驅(qū)動文件中讀取相應的狀態(tài); 使用完成后使用close函數(shù)關(guān)閉該驅(qū)動文件。
Linux軟件從上到下可分為4層結(jié)構(gòu),如下圖左示。 以控制LED為例,具體過程如下圖右示:
每個系統(tǒng)調(diào)用,在驅(qū)動中都有與之對應的驅(qū)動函數(shù),內(nèi)核include/linux/fs.h文件中有個file_operations結(jié)構(gòu)體,就是Linux內(nèi)核驅(qū)動操作函數(shù)集合:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t*);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
......
......
};
Linux驅(qū)動運行方式有以下兩種:
- 將驅(qū)動編譯進內(nèi)核中, 當Linux內(nèi)核啟動時就會自動運行驅(qū)動程序
- 將驅(qū)動編譯成模塊, 在內(nèi)核啟動后使用insmod命令加載驅(qū)動模塊
在驅(qū)動開發(fā)階段一般都將其編譯為模塊,不需要編譯整個Linux代碼,方便調(diào)試驅(qū)動程序。 當驅(qū)動開發(fā)完成后,根據(jù)實際需要,可以選擇是否將驅(qū)動編譯進Linux內(nèi)核中。
2. Linux設(shè)備號
2.1 設(shè)備號的組成
Linux中每個設(shè)備都有一個設(shè)備號,由主設(shè)備號和次設(shè)備號兩部分組成:
- 主設(shè)備號表示某一個具體的驅(qū)動
- 次設(shè)備號表示使用這個驅(qū)動的各個設(shè)備
Linux 提供了名為dev_t的數(shù)據(jù)類型表示設(shè)備號,其本質(zhì)是32位的unsigned int數(shù)據(jù)類型,其中高12位為主設(shè)備號,低20位為次設(shè)備號,因此Linux中主設(shè)備號范圍為0~4095
在文件include/linux/kdev_t.h中提供了幾個關(guān)于設(shè)備號操作的宏定義:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
- MINORBITS:表示次設(shè)備號位數(shù),一共20位
- MINORMASK:表示次設(shè)備號掩碼
- MAJOR:用于從dev_t中獲取主設(shè)備號,將dev_t右移20位即可
- MINOR:用于從dev_t中獲取次設(shè)備號,取dev_t的低20位的值即可
- MKDEV:用于將給定的主設(shè)備號和次設(shè)備號的值組合成dev_t類型的設(shè)備號
2.2 主設(shè)備號的分配
主設(shè)備號的分配包括靜態(tài)分配和動態(tài)分配。 靜態(tài)分配需要手動指定設(shè)備號,并且要注意不能與已有的重復,一些常用的設(shè)備號已經(jīng)被Linux內(nèi)核開發(fā)者給分配掉了,可使用cat /proc/devices命令查看當前系統(tǒng)中所有已經(jīng)使用了的設(shè)備號。
動態(tài)分配是在注冊字符設(shè)備之前先申請一個設(shè)備號,系統(tǒng)會自動分配一個沒有被使用的設(shè)備號, 這樣就避免了沖突。 在卸載驅(qū)動的時候釋放掉這個設(shè)備號即可。
設(shè)備號的申請函數(shù)
//設(shè)備號申請函數(shù)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
// dev:保存申請到的設(shè)備號
// baseminor:次設(shè)備號起始地址
// count:要申請的設(shè)備號數(shù)量
// name:設(shè)備名字
設(shè)備號的釋放函數(shù)
//設(shè)備號釋放函數(shù)
void unregister_chrdev_region(dev_t from, unsigned count)
// from:要釋放的設(shè)備號
// count:表示從 from 開始,要釋放的設(shè)備號數(shù)量
3. 字符設(shè)備驅(qū)動開發(fā)模板
3.1 加載與卸載
在編寫驅(qū)動的時候需要注冊模塊加載和卸載這兩種函數(shù):
module_init(xxx_init); //注冊模塊加載函數(shù)
module_exit(xxx_exit); //注冊模塊卸載函數(shù)
module_init():向內(nèi)核注冊一個模塊加載函數(shù),參數(shù)xxx_init就是需要注冊的具體函數(shù),當使用insmod命令加載驅(qū)動時,xxx_init函數(shù)就會被調(diào)用
module_exit():向內(nèi)核注冊一個模塊卸載函數(shù),參數(shù)xxx_exit就是需要注冊的具體函數(shù),當使用rmmod命令卸載驅(qū)動時,xxx_exit函數(shù)就會被調(diào)用
字符設(shè)備驅(qū)動模塊加載和卸載模板如下所示:
/* 驅(qū)動入口函數(shù) */
staticint __init xxx_init(void)
{
/*入口函數(shù)具體內(nèi)容*/
return0;
}
/* 驅(qū)動出口函數(shù) */
staticvoid __exit xxx_exit(void)
{
/*出口函數(shù)具體內(nèi)容*/
}
/* 將上面兩個函數(shù)指定為驅(qū)動的入口和出口函數(shù) */
module_init(xxx_init);
module_exit(xxx_exit);
驅(qū)動編譯完成以后擴展名為.ko,有兩種命令可以加載驅(qū)動模塊:
- insmod:最簡單的模塊加載命令,但不能解決模塊的依賴關(guān)系
- modprobe:會分析模塊的依賴關(guān)系,將所有的依賴模塊都加載到內(nèi)核中
卸載驅(qū)動也有兩種命令:
- rmmod:最簡單的模塊卸載命令
- modprobe -r:除了卸載指定的驅(qū)動,還卸載其所依賴的其他模塊,若依賴模塊還在被其它模塊使用,就不能使用該命令來卸載驅(qū)動模塊
3.2 注冊與注銷
對于字符設(shè)備驅(qū)動而言,當驅(qū)動模塊加載成功以后需要注冊字符設(shè)備,卸載驅(qū)動模塊時也要注銷掉字符設(shè)備。 字符設(shè)備的注冊和注銷函數(shù)原型如下所示:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
//major:主設(shè)備號
//name:設(shè)備名字,指向一串字符串
//fops:結(jié)構(gòu)體 file_operations 類型指針,指向設(shè)備的操作函數(shù)集合變量
static inline void unregister_chrdev(unsigned int major, const char *name)
//major:要注銷的設(shè)備對應的主設(shè)備號
//name:要注銷的設(shè)備對應的設(shè)備名
一般字符設(shè)備的注冊在驅(qū)動模塊的入口函數(shù)xxx_init中進行,字符設(shè)備的注銷在驅(qū)動模塊的出口函數(shù)xxx_exit中進行
//定義了一個file_operations結(jié)構(gòu)體變量,就是設(shè)備的操作函數(shù)集合
static struct file_operations test_fops;
/* 驅(qū)動入口函數(shù) */
static int __init xxx_init(void){
/* 入口函數(shù)具體內(nèi)容 */
int retvalue = 0;
/* 注冊字符設(shè)備驅(qū)動 */
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
/* 字符設(shè)備注冊失敗,自行處理 */
}
return 0;
}
/* 驅(qū)動出口函數(shù) */
static void __exit xxx_exit(void){
/* 注銷字符設(shè)備驅(qū)動 */
unregister_chrdev(200, "chrtest");
}
/* 將上面兩個函數(shù)指定為驅(qū)動的入口和出口函數(shù) */
module_init(xxx_init);
module_exit(xxx_exit);
3.3 實現(xiàn)設(shè)備的具體操作函數(shù)
file_operations結(jié)構(gòu)體就是設(shè)備的具體操作函數(shù)。 假設(shè)對chrtest這個設(shè)備有如下兩個要求:
- 能夠?qū)崿F(xiàn)打開和關(guān)閉操作:需要實現(xiàn)open和release這兩個函數(shù)
- 能夠?qū)崿F(xiàn)進行讀寫操作:需要實現(xiàn)read和write這兩個函數(shù)
實現(xiàn)file_operations中的這四個函數(shù),完成后的內(nèi)容框架如下所示:
/* 打開設(shè)備 */
static int chrtest_open(struct inode *inode, struct file *filp){
/* 用戶實現(xiàn)具體功能 */
return 0;
}
/* 從設(shè)備讀取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
/* 用戶實現(xiàn)具體功能 */
return 0;
}
/* 向設(shè)備寫數(shù)據(jù) */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){
/* 用戶實現(xiàn)具體功能 */
return 0;
}
/* 關(guān)閉/釋放設(shè)備 */
static int chrtest_release(struct inode *inode, struct file *filp){
/* 用戶實現(xiàn)具體功能 */
return 0;
}
然后是驅(qū)動的入口(init)和出口(exit) 函數(shù):
//定義了一個file_operations結(jié)構(gòu)體變量test_fops,就是設(shè)備的操作函數(shù)集合
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
}
/* 驅(qū)動入口函數(shù) */
static int __init xxx_init(void){
/* 入口函數(shù)具體內(nèi)容 */
int retvalue = 0;
/* 注冊字符設(shè)備驅(qū)動 */
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
/* 字符設(shè)備注冊失敗,自行處理 */
}
return 0;
}
/* 驅(qū)動出口函數(shù) */
static void __exit xxx_exit(void){
/* 注銷字符設(shè)備驅(qū)動 */
unregister_chrdev(200, "chrtest");
}
/* 將上面兩個函數(shù)指定為驅(qū)動的入口和出口函數(shù) */
module_init(xxx_init);
module_exit(xxx_exit);
3.4 添加LICENSE和作者信息
LICENSE是必須添加的,否則編譯時會報錯,作者信息可加可不加
MODULE_LICENSE() //添加模塊 LICENSE 信息
MODULE_AUTHOR() //添加模塊作者信息
綜上所述,字符設(shè)備驅(qū)動開發(fā)流程如下圖所示:
4. 字符設(shè)備驅(qū)動開發(fā)實驗
下面以正點原子的IMX6ULL開發(fā)板為平臺,完整的編寫一個虛擬字符設(shè)備驅(qū)動模塊。 chrdevbase不是實際存在的一個設(shè)備,只是為了學習字符設(shè)備的開發(fā)的流程
4.1 驅(qū)動程序編寫
宏定義及變量定義
#include
#include
#include
#include
#include
#include
#define CHRDEVBASE_MAJOR 200 /* 主設(shè)備號 */
#define CHRDEVBASE_NAME "chrdevbase" /* 設(shè)備名 */
static char readbuf[100]; /* 讀緩沖區(qū) */
static char writebuf[100]; /* 寫緩沖區(qū) */
static char kerneldata[] = {"kernel data!"};
打開、關(guān)閉、讀取、寫入函數(shù)實現(xiàn)
staticintchrdevbase_open(structinode*inode,structfile*filp){
printk("chrdevbase open!\\r\\n");
return0;
}
staticssize_tchrdevbase_read(structfile*filp,char __user *buf,size_t cnt,loff_t*offt){
int retvalue =0;
/* 向用戶空間發(fā)送數(shù)據(jù) */
memcpy(readbuf, kerneldata,sizeof(kerneldata));
retvalue =copy_to_user(buf, readbuf, cnt);
if(retvalue ==0){
printk("kernel senddata ok!\\r\\n");
}else{
printk("kernel senddata failed!\\r\\n");
}
printk("chrdevbase read!\\r\\n");
return0;
}
staticssize_tchrdevbase_write(structfile*filp,constchar __user *buf,size_t cnt,loff_t*offt){
int retvalue =0;
/* 接收用戶空間傳遞給內(nèi)核的數(shù)據(jù)并且打印出來 */
retvalue =copy_from_user(writebuf, buf, cnt);
if(retvalue ==0){
printk("kernel recevdata:%s\\r\\n", writebuf);
}else{
printk("kernel recevdata failed!\\r\\n");
}
printk("chrdevbase write!\\r\\n");
return0;
}
staticintchrdevbase_release(structinode*inode,structfile*filp){
printk("chrdevbase release!\\r\\n");
return0;
}
驅(qū)動加載與注銷
staticstructfile_operations chrdevbase_fops ={
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*驅(qū)動入口函數(shù) */
staticint __init chrdevbase_init(void){
int retvalue =0;
retvalue =register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,&chrdevbase_fops);
if(retvalue <0){
printk("chrdevbase driver register failed\\r\\n");
}
printk("chrdevbase init!\\r\\n");
return0;
}
/* 驅(qū)動出口函數(shù) */
staticvoid __exit chrdevbase_exit(void){
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\\r\\n");
}
/* 將上面兩個函數(shù)指定為驅(qū)動的入口和出口函數(shù) */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
LICENSE與作者
MODULE_LICENSE("GPL");
MODULE_AUTHOR("andyxi");
4.2 應用程序編寫
應用程序運行在用戶空間,其通過輸入相應的指令來對chrdevbase設(shè)備執(zhí)行讀或者寫操作。 下面將程序進行分段介紹
頭文件和main函數(shù)入口,以及main函數(shù)的傳參處理
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};
int main(int argc, char *argv[]){
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\\r\\n");
return -1;
}
filename = argv[1];
/* 打開驅(qū)動文件 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s\\r\\n", filename);
return -1;
}
對 chrdevbase 設(shè)備的具體操作
if(atoi(argv[2])==1){/* 從驅(qū)動文件讀取數(shù)據(jù) */
retvalue =read(fd, readbuf,50);
if(retvalue <0){
printf("read file %s failed!\\r\\n", filename);
}else{
/* 讀取成功,打印出讀取成功的數(shù)據(jù) */
printf("read data:%s\\r\\n",readbuf);
}
}
if(atoi(argv[2])==2){
/* 向設(shè)備驅(qū)動寫數(shù)據(jù) */
memcpy(writebuf, usrdata,sizeof(usrdata));
retvalue =write(fd, writebuf,50);
if(retvalue <0){
printf("write file %s failed!\\r\\n", filename);
}
}
關(guān)閉設(shè)備
/* 關(guān)閉設(shè)備 */
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\\r\\n", filename);
return -1;
}
return 0;
}
4.3 程序編譯
程序編譯包括驅(qū)動程序編譯和應用程序編譯兩個部分
驅(qū)動程序編譯: 將驅(qū)動程序編譯為.ko模塊
創(chuàng)建Makefile文件
# KERNELDIR:開發(fā)板所使用的Linux內(nèi)核源碼目錄
KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
# CURRENT_PATH:當前路徑,通過運行“pwd”命令獲取
CURRENT_PATH := $(shell pwd)
# obj-m:將 chrdevbase.c 這個文件編譯為chrdevbase.ko模塊
obj-m := chrdevbase.o
build: kernel_modules
# -C 表示切換工作目錄到KERNERLDIR目錄
# M 表示模塊源碼目錄
# modules 表示編譯模塊
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
輸入make命令即可編譯,編譯成功以后就會生成一個叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 設(shè)備的驅(qū)動模塊
注意:若直接make編譯可能會出錯,是因為kernel中沒有指定編譯器和架構(gòu),使用了默認的x86平臺編譯報錯。 解決辦法就是在內(nèi)核頂層Makefile中,直接定義ARCH和CROSS_COMPILE這兩個的變量值為 arm 和 arm-linux-gnueabihf- 即可
應用程序編譯: 無需內(nèi)核參與,直接編譯即可
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
使用file命令,查看生成的chrdevbaseApp文件信息,如下圖示,文件是32位LSB格式,ARM版本的,因此只能在ARM芯片下運行
4.4 運行測試
為了方便測試,Linux系統(tǒng)選擇通過TFTP從網(wǎng)絡(luò)啟動,并且使用NFS掛載網(wǎng)絡(luò)根文件系統(tǒng)。 確保開發(fā)板系統(tǒng)移植成功,能正常啟動。 具體的實現(xiàn)方法可參考之前介紹過的系統(tǒng)移植專輯系列文章
加載驅(qū)動模塊
在根文件系統(tǒng)創(chuàng)建/lib/modules/4.1.15文件夾,用來存放驅(qū)動模塊
- /lib/modules是通用的
- 4.1.15根據(jù)所使用的內(nèi)核版本來設(shè)置,否則modprobe命令無法加載驅(qū)動模塊
在Ubuntu中將chrdevbase.ko和chrdevbaseAPP,復制到根文件系統(tǒng)的 rootfs/lib/modules/4.1.15 目錄中
在開發(fā)板中使用insmod或modprobe命令來加載驅(qū)動文件
輸入lsmod命令即可查看當前系統(tǒng)中存在的模塊,輸入cat /proc/devices命令,查看當前系統(tǒng)中有沒有chrdevbase 這個設(shè)備
創(chuàng)建設(shè)備節(jié)點文件: 驅(qū)動加載成功后,需要在/dev目錄下創(chuàng)建一個與之對應的設(shè)備節(jié)點文件,應用程序通過操作這個設(shè)備節(jié)點文件來完成對具體設(shè)備的操作
使用mknod命令創(chuàng)建/dev/chrdevbase設(shè)備節(jié)點文件
mknod /dev/chrdevbase c 200 0
#/dev/chrdevbase 是要創(chuàng)建的節(jié)點文件
# c 表示這是個字符設(shè)備
# 200 是設(shè)備的主設(shè)備號
# 0 是設(shè)備的次設(shè)備號
創(chuàng)建完后可使用ls /dev/chrdevbase -l命令查看是否存在
操作設(shè)備測試: 使用應用程序讀寫設(shè)備,對/dev/chrdevbase文件進行讀寫操作
# 讀操作命令
./chrdevbaseApp /dev/chrdevbase 1
# 輸出“ kernel senddata ok!”是驅(qū)動程序中chrdevbase_read函數(shù)輸出的信息
# “read data:kernel data!”就是chrdevbaseAPP打印出來的接收到的數(shù)據(jù)
# 寫操作命令
./chrdevbaseApp /dev/chrdevbase 2
# “kernel recevdata:usr data!”,是驅(qū)動程序中的chrdevbase_write函數(shù)輸出的
卸載驅(qū)動模塊: 若不再使用某個設(shè)備的話可以將其驅(qū)動卸載掉。 輸入rmmod命令卸載驅(qū)動后,使用lsmod命令查看chrdevbase這個模塊還存不存在
至此,Linux字符設(shè)備驅(qū)動開發(fā)完成。 本文介紹了驅(qū)動開發(fā)中的字符驅(qū)動開發(fā)的基本模式,并使用一個虛擬的字符設(shè)備驅(qū)動進行測試,了解驅(qū)動程序與應用程序之間的調(diào)用關(guān)系。
評論
查看更多