3.字符設備驅動開發
3.1 什么是字符設備驅動
字符設備:就是一個個字節,按照字節流進行讀寫操作的設備,讀寫是按照先后順序的。
舉例子:IIC 按鍵 LED SPI LCD 等
Linux 應用程序調用驅動程序流程:
Linux中驅動加載成功后會在/dev 中生成一個/dev/xxx(xxx是驅動文件名字)文件 應用程序對/dev/xxx進行open write read close 等操作
應用程序open函數調用c庫函數,c庫中的open函數調用系統內核態的open函數,然后調用驅動中的open函數。
3.2字符設備驅動開發
3.2.1 驅動模塊的加載和卸載
驅動運行方式有兩種:
1、將驅動編譯到內核中啟動時自動運行驅動程序
2、在 Linux 內核啟動以后使用“modprobe”或者“insmod”命令加載驅動模塊
這里有兩個函數:
module_init(xxx_init); //注冊模塊加載函數
module_exit(xxx_exit); //注冊模塊卸載函數
使用modprobe時module_init函數被調用
使用rmmod時module_exit函數被調用
入口函數:
static int __init xxx_init(void)
{/* 入口函數具體內容 */return 0;
}
出口函數:
static void __exit xxx_exit(void)
{
/*出口函數內容*/
}
insmod和modprobe的關系:
insmod 命令不能解決模塊的依賴關系,比如 drv.ko 依賴 first.ko 這個模塊,就必須先使用insmod 命令加載 first.ko 這個模塊,然后再加載 drv.ko 這個模塊。
modprobe 就不會存在這個問題,modprobe 會分析模塊的依賴關系,然后會將所有的依賴模塊都加載到內核中,因此 modprobe 命令相比 insmod 要智能一些。
modprobe 命令主要智能在提供了模塊的依賴性分
析、錯誤檢查、錯誤報告等功能,推薦使用 modprobe 命令來加載驅動。modprobe 命令默認會去/lib/modules/<kernel-version>目錄中查找模塊,kernel-version為內核版本號。
驅動模塊卸載
驅動模塊的卸載使用命令“rmmod”即可,比如要卸載 drv.ko,使用如下命令即可:
rmmod drv.ko
也可以使用“modprobe -r”命令卸載驅動,比如要卸載 drv.ko,命令如下:
modprobe -r drv
使用 modprobe 命令可以卸載掉驅動模塊所依賴的其他模塊,前提是這些依賴模塊已經沒有被其他模塊所使用,否則就不能使用 modprobe 來卸載驅動模塊。所以對于模塊的卸載,還是推薦使用 rmmod 命令。
3.2.2 字符設備注冊與注銷
當驅動模塊加載成功后,需要注冊字符設備。當驅動模塊卸載成時,需要將字符設備注銷。
注冊和注銷設備驅動函數原型如下:
static inline int register_chrdev(unsigned int major,
const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major,
const char *name)
register_chrdev 函數用于注冊字符設備,此函數一共有三個參數,這三個參數的含義如下:
major:主設備號,Linux 下每個設備都有一個設備號,設備號分為主設備號和次設備號兩部分,
name:設備名字,指向一串字符串。
fops:結構體 file_operations 類型指針,指向設備的操作函數集合變量。
unregister_chrdev 函數用戶注銷字符設備,此函數有兩個參數,這兩個參數含義如下:
major:要注銷的設備對應的主設備號。
name:要注銷的設備對應的設備名。
查看已使用的設備號:cat /proc/devices
3.2.3 實現設備的具體操作函數
file_operation結構體就是具體的操作函數,如open read write release
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
指定初始化器(designated initializer),.成員名 = 值 的語法格式在 C99 標準中被引入。
3.2.4 添加LICENSE和作者信息
LICENSE 是必須添加的,
否則的話編譯的時候會報錯,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加
使用如下兩個函數:
MODULE_LICENSE() //添加模塊 LICENSE 信息
MODULE_AUTHOR() //添加模塊作者信息
GPL(GNU General Public License,GNU通用公共許可證)是一種開源軟件許可協議,由自由軟件基金會(FSF)制定,旨在保護軟件自由的使用、修改和再發布權利。
GPL 是“你用了我的代碼,你就得開源你的代碼” 的開源協議。
3.3設備號
3.3.1什么是設備號
設備號(Device Number)是 Linux 內核中用于標識一個設備的數字標識,它由兩個部分組成:
🧩 1. 主設備號(Major Number)
表示這個設備屬于哪一個驅動程序。
也就是說,哪個內核驅動模塊來處理這個設備的請求。
🧩 2. 次設備號(Minor Number)
表示由同一個驅動程序管理的多個設備中的哪一個。
相當于:驅動負責整條生產線,次設備號表示哪個產品。
Linux 提供了一個名為 dev_t 的數據類型表示設備號,dev_t 定義在文件 include/linux/types.h 里面,定義
如下:
13 typedef u32 __kernel_dev_t;
......
16 typedef __kernel_dev_t dev_t;
可以看出 dev_t 是 u32 類型的,也就是 unsigned int,所以 dev_t 其實就是 unsigned int 類型,是一個 32 位的數據類型。這 32 位的數據構成了主設備號和次設備號兩部分,其中高 12位為主設備號,低 20 位為次設備號。因此 Linux 系統中主設備號范圍為 0~4095,所以大家在選擇主設備號的時候一定不要超過這個范圍。
在文件 include/linux/kdev_t.h 中提供了幾個關于設備號的操作函數(本質是宏),如下所示:
7 #define MINORBITS 20
8 #define MINORMASK ((1U << MINORBITS) - 1)
9
10 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
11 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
12 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
第 7 行,宏 MINORBITS 表示次設備號位數,一共是 20 位。
第 8 行,宏 MINORMASK 表示次設備號掩碼。
第 10 行,宏 MAJOR 用于從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可。
第 11 行,宏 MINOR 用于從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值即可。
第 12 行,宏 MKDEV 用于將給定的主設備號和次設備號的值組合成 dev_t 類型的設備
號。
3.3.2 設備號的分配
設備號分配為:靜態分配和動態分配
1、靜態分配
cat /proc/devices 查看當前設備中使用的設備號如果沒有被使用的設備號就能注冊
2、動態分配
在注冊字符設備之前先申請一個設備號,系統會自動給你一個沒有被使用的設備號,這樣就避免了沖突。卸載驅動的時候釋放掉這個設備號即可,設備號的申請函數如下
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函數 alloc_chrdev_region 用于申請設備號,此函數有 4 個參數:
dev:保存申請到的設備號。
baseminor:次設備號起始地址,alloc_chrdev_region 可以申請一段連續的多個設備號,這
些設備號的主設備號一樣,但是次設備號不同,次設備號以 baseminor 為起始地址地址開始遞
增。一般 baseminor 為 0,也就是說次設備號從 0 開始。
count:要申請的設備號數量。
name:設備名字。
注銷字符設備之后要釋放掉設備號,設備號釋放函數如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函數有兩個參數:
from:要釋放的設備號。
count:表示從 from 開始,要釋放的設備號數量。
3.4 chrdevbase 字符設備驅動開發實驗
我們就以 chrdevbase 這個虛擬設備為
例,完整的編寫一個字符設備驅動模塊。chrdevbase 不是實際存在的一個設備,是為了方便講解字符設備的開發而引入的一個虛擬設備。chrdevbase 設備有兩個緩沖區,一個讀緩沖區,一個寫緩沖區,這兩個緩沖區的大小都為 100 字節。在應用程序中可以向 chrdevbase 設備的寫緩沖區中寫入數據,從讀緩沖區中讀取數據。
驅動文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>#define CHRDEVBASE_MAJOR 200 /* 主設備號 */
#define CHRDEVBASE_NAME "chrdevbase" /* 設備名 */static char readbuf[100]; /* 讀緩沖區 */
static char writebuf[100]; /* 寫緩沖區 */
static char kerneldata[] = {"kernel data!"};/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量* 一般在open的時候將private_data指向設備結構體。* @return : 0 成功;其他 失敗*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{//printk("chrdevbase open!\r\n");return 0;
}/** @description : 從設備讀取數據 * @param - filp : 要打開的設備文件(文件描述符)* @param - buf : 返回給用戶空間的數據緩沖區* @param - cnt : 要讀取的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 讀取的字節數,如果為負值,表示讀取失敗*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 向用戶空間發送數據 */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");return 0;
}/** @description : 向設備寫數據 * @param - filp : 設備文件,表示打開的文件描述符* @param - buf : 要寫給設備寫入的數據* @param - cnt : 要寫入的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 寫入的字節數,如果為負值,表示寫入失敗*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 接收用戶空間傳遞給內核的數據并且打印出來 */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");return 0;
}/** @description : 關閉/釋放設備* @param - filp : 要關閉的設備文件(文件描述符)* @return : 0 成功;其他 失敗*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{//printk("chrdevbase release!\r\n");return 0;
}/** 設備操作函數結構體*/
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE, .open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};/** @description : 驅動入口函數 * @param : 無* @return : 0 成功;其他 失敗*/
static int __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");return 0;
}/** @description : 驅動出口函數* @param : 無* @return : 無*/
static void __exit chrdevbase_exit(void)
{/* 注銷字符設備驅動 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\r\n");
}/* * 將上面兩個函數指定為驅動的入口和出口函數 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);/* * LICENSE和作者信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("123");
MODULE_INFO(intree, "Y");
驅動程序書寫流程:
1、寫入口函數 出口函數
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
2、實現入口函數出口函數
入口函數中注冊字符設備
出口函數注銷字符設備
static int __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");return 0;
}/** @description : 驅動出口函數* @param : 無* @return : 無*/
static void __exit chrdevbase_exit(void)
{/* 注銷字符設備驅動 */unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);printk("chrdevbase exit!\r\n");
}
3、實現file_operations 結構體 和open read write release函數
/** @description : 打開設備* @param - inode : 傳遞給驅動的inode* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量* 一般在open的時候將private_data指向設備結構體。* @return : 0 成功;其他 失敗*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{//printk("chrdevbase open!\r\n");return 0;
}/** @description : 從設備讀取數據 * @param - filp : 要打開的設備文件(文件描述符)* @param - buf : 返回給用戶空間的數據緩沖區* @param - cnt : 要讀取的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 讀取的字節數,如果為負值,表示讀取失敗*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 向用戶空間發送數據 */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");return 0;
}/** @description : 向設備寫數據 * @param - filp : 設備文件,表示打開的文件描述符* @param - buf : 要寫給設備寫入的數據* @param - cnt : 要寫入的數據長度* @param - offt : 相對于文件首地址的偏移* @return : 寫入的字節數,如果為負值,表示寫入失敗*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{int retvalue = 0;/* 接收用戶空間傳遞給內核的數據并且打印出來 */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");return 0;
}/** @description : 關閉/釋放設備* @param - filp : 要關閉的設備文件(文件描述符)* @return : 0 成功;其他 失敗*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{//printk("chrdevbase release!\r\n");return 0;
}/** 設備操作函數結構體*/
static struct file_operations chrdevbase_fops = {.owner = THIS_MODULE, .open = chrdevbase_open,.read = chrdevbase_read,.write = chrdevbase_write,.release = chrdevbase_release,
};
APP文件
#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!"};/** @description : main主程序* @param - argc : argv數組元素個數* @param - argv : 具體參數* @return : 0 成功;其他 失敗*/
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];/* 打開驅動文件 */fd = open(filename, O_RDWR);if(fd < 0){printf("Can't open file %s\r\n", filename);return -1;}if(atoi(argv[2]) == 1){ /* 從驅動文件讀取數據 */retvalue = read(fd, readbuf, 50);if(retvalue < 0){printf("read file %s failed!\r\n", filename);}else{/* 讀取成功,打印出讀取成功的數據 */printf("read data:%s\r\n",readbuf);}}if(atoi(argv[2]) == 2){/* 向設備驅動寫數據 */memcpy(writebuf, usrdata, sizeof(usrdata));retvalue = write(fd, writebuf, 50);if(retvalue < 0){printf("write file %s failed!\r\n", filename);}}/* 關閉設備 */retvalue = close(fd);if(retvalue < 0){printf("Can't close file %s\r\n", filename);return -1;}return 0;
}
Makefile
KERNELDIR := /home/lk/rk3588_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
# obj-m 是內核模塊編譯規則中的一個特殊變量。
# obj-m 定義了要生成的模塊目標文件(即 .ko 文件)。
# obj-m 表示編譯時將 chrdevbase.o 作為模塊(module)對象,最終會生成 chrdevbase.ko。
# chrdevbase.o# chrdevbase.o 是將 chrdevbase.c 文件編譯為目標文件(.o 文件)的名稱。
# 生成的目標文件會自動鏈接成內核模塊 chrdevbase.ko。
obj-m := chrdevbase.o
# make 會首先檢查 kernel_modules 目標。
# 如果 kernel_modules 目標沒有生成或需要更新,make 會執行 kernel_modules 的命令。
# 執行完 kernel_modules 后,build 目標就算完成了。
build : kernel_modules# kernel_modules# 定義一個名為 kernel_modules 的目標。
# 當執行 make kernel_modules 時,會觸發后面的命令。# $(MAKE)# $(MAKE) 是一個特殊的變量,表示 make 命令本身。
# 使用 $(MAKE) 而不是直接調用 make 可以在嵌套調用時保持參數一致性。
# -C $(KERNELDIR)# -C 選項表示切換到 $(KERNELDIR) 目錄下執行命令。
# $(KERNELDIR) 是一個變量,通常指定為 Linux 內核源碼的構建目錄。
# 在內核源碼目錄中調用 make 會使用內核的構建系統。
# M=$(CURRENT_PATH)# M= 選項告訴內核構建系統,當前模塊的源代碼位于 $(CURRENT_PATH) 目錄下。
# modules# modules 是內核構建系統的一個目標,表示要構建模塊(.ko 文件)。
# 當傳入 modules 目標時,內核會根據 obj-m 定義的模塊進行編譯。
# 總結 使用make buil 就會檢查kernel_modules是否存在或者更新 ,kernel_modules會執行$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
#也就是 make 內核路徑 當前文件路徑 生成modules即obj-m 對應的 chrdevbase.o生成chrdevbase.ko文件
kernel_modules :$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
Makefile 用法 根據配置
KERNELDIR := /home/lk/rk3588_linux_sdk/kernel
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
運行build 然后運行kernel_modules 后的
其中:= 是簡單賦值 :是目標依賴
M= 是 Make 運行參數,不是 Makefile 自身定義變量的語法
-C:切換目錄到內核源碼目錄
modules:告訴它“我要構建模塊”