我們學(xué)習(xí)編程的時候都會從hello程序開始。同樣的,學(xué)習(xí)Linux驅(qū)動我們也從最簡單的hello驅(qū)動學(xué)起。
還記得實習(xí)那會兒我第一次接觸嵌入式Linux項目的時候,我的導(dǎo)師讓我去學(xué)習(xí)項目的其它模塊,然后嘗試著寫一個串口相關(guān)的應(yīng)用。
那時候知道可以把設(shè)備當(dāng)做文件來操作。但是不知道為什么是這樣,就去網(wǎng)上搜了一些代碼(驅(qū)動代碼),然后和我的應(yīng)用代碼放在同一個文件里。
給導(dǎo)師看了之后,導(dǎo)師說那些驅(qū)動程序不需要我寫,那些驅(qū)動已經(jīng)寫好被編譯到內(nèi)核里了,可以直接用了,我只需關(guān)注應(yīng)用層就好了。我當(dāng)時腦子里就在打轉(zhuǎn)。。what?
STM32用一個串口不就是串口初始化,然后想怎么用就怎么用嗎?后來經(jīng)過學(xué)習(xí)才知道原來是那么一回事呀。這就是單片機轉(zhuǎn)轉(zhuǎn)嵌入式Linux的思維誤區(qū)之一。
學(xué)嵌入式Linux之前我們有必要暫時忘了我們單片機的開發(fā)方式,重新梳理嵌入式Linux的開發(fā)流程。下面看一下STM32裸機開發(fā)與嵌入式Linux開發(fā)的一些區(qū)別:
嵌入式Linux的開發(fā)方式與STM32裸機開發(fā)的方式有點不一樣。在STM32的裸機開發(fā)中,驅(qū)動層與應(yīng)用層的區(qū)分可能沒有那么明顯,常常都雜揉在一起。
當(dāng)然,有些很有水平的裸機程序分層分得還是很明顯的。但是,在嵌入式Linux中,驅(qū)動和應(yīng)用的分層是特別明顯的,最直觀的感受就是驅(qū)動程序是一個.c文件里,應(yīng)用程序是另一個.c文件。
比如我們這個hello驅(qū)動實驗中,我們的驅(qū)動程序為hello_drv.c、應(yīng)用程序為hello_app.c。
驅(qū)動模塊的加載有兩種方式:第一種方式是動態(tài)加載的方式,即驅(qū)動程序與內(nèi)核分開編譯,在內(nèi)核運行的過程中加載;第二種方式是靜態(tài)加載的方式,即驅(qū)動程序與內(nèi)核一同編譯,在內(nèi)核啟動過程中加載驅(qū)動。
在調(diào)試驅(qū)動階段常常選用第一種方式,因為較為方便;在調(diào)試完成之后才采用第二種方式與內(nèi)核一同編譯。
STM32裸機開發(fā)與嵌入式Linux開發(fā)還有一點不同的就是:STM32裸機開發(fā)最終要燒到板子的常常只有一個文件(除開含有IAP程序的情況或者其它情況),嵌入式Linux就需要分開編譯、燒寫。
我們先看一個圖:
當(dāng)我們的應(yīng)用在調(diào)用open、close、write、read等函數(shù)時,為什么就能操控硬件設(shè)備。那是因為有驅(qū)動層在支撐著與硬件相關(guān)的操作,應(yīng)用程序在調(diào)用打開、關(guān)閉、讀、寫等操作會觸發(fā)相應(yīng)的驅(qū)動層函數(shù)。
本篇筆記我們以hello驅(qū)動做分享,hello驅(qū)動屬于字符設(shè)備。實現(xiàn)的驅(qū)動函數(shù)大概是怎么樣的是有套路
可尋的,這個套路在內(nèi)核文件include/linux/fs.h
中,這個文件中有如下結(jié)構(gòu)體:
這個結(jié)構(gòu)體里的成員都是些函數(shù)指針變量,我們需要根據(jù)實際的設(shè)備確定我們需要創(chuàng)建哪些驅(qū)動函數(shù)實體。比如我們的hello驅(qū)動的幾個基本的函數(shù)(打開/關(guān)閉/讀/寫)可創(chuàng)建為(以下代碼來自:百問網(wǎng)):
(1)打開操作
左右滑動查看全部代碼>>>
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
打開函數(shù)的兩個形參的類型要與struct file_operations
結(jié)構(gòu)體里open
成員的形參類型一致,里面有一句打印語句,方便直觀地看到驅(qū)動的運行過程。
關(guān)于函數(shù)指針,可閱讀往期筆記:
(2)關(guān)閉操作
左右滑動查看全部代碼>>>
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
(3)讀操作
左右滑動查看全部代碼>>>
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
copy_to_user
函數(shù)的原型為:
左右滑動查看全部代碼>>>
static inline int copy_to_user(void __user *to, const void *from, unsigned long n);
用該函數(shù)來讀取內(nèi)核空間(kernel_buf
)的數(shù)據(jù)給到用戶空間(buf
)。另外,kernel_buf的定義如下:
static char kernel_buf[1024];
MIN為宏:
#define MIN(a, b) (a < b ? a : b)
把MIN(1024, size)
作為copy_to_user
的實參意在對拷貝的數(shù)據(jù)長度做限制(不能超出kernel_buf的大小)。
(4)寫操作
左右滑動查看全部代碼>>>
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
copy_from_user函數(shù)的原型為:
左右滑動查看全部代碼>>>
static inline int copy_from_user(void *to,const void __user volatile *from,unsigned long n)
用該函數(shù)來將用戶空間(buf
)的數(shù)據(jù)傳送到內(nèi)核空間(kernel_buf
)。
有了這些驅(qū)動函數(shù),就可以給到一個struct file_operations
類型的結(jié)構(gòu)體變量hello_drv
,如:
static struct file_operations hello_drv =
{
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
有些朋友可能沒見過這種結(jié)構(gòu)體初始化的形式(結(jié)構(gòu)體成員前面加個.號),這是C99及C11標(biāo)準(zhǔn)提出的指定初始化器。具體可以去看往期筆記:【C語言筆記】結(jié)構(gòu)體。
上面這個結(jié)構(gòu)體變量hello_drv
容納了我們hello
設(shè)備的驅(qū)動接口,最終我們要把這個hello_drv
注冊給Linux內(nèi)核。
套路就是這樣的:把驅(qū)動程序注冊給內(nèi)核,之后我們的應(yīng)用程序就可以使用open/close/write/read
等函數(shù)來操控我們的設(shè)備,Linux內(nèi)核在這里起到一個中間人的作用,把兩頭的驅(qū)動與應(yīng)用協(xié)調(diào)得很好。
我們前面說了驅(qū)動的裝載方式之一的動態(tài)裝載:把驅(qū)動程序編譯成模塊,再動態(tài)裝載。
動態(tài)裝載的體現(xiàn)就是開發(fā)板已經(jīng)啟動運行了Linux內(nèi)核,我們通過開發(fā)板串口終端使用命令來裝載驅(qū)動。裝載驅(qū)動有兩個命令,比如裝載我們的hello驅(qū)動:
其中modprobe
命令不僅能裝載當(dāng)前驅(qū)動,而且還會同時裝載與當(dāng)前驅(qū)動相關(guān)的依賴驅(qū)動。有了轉(zhuǎn)載就有卸載,也有兩種方式:
其中modprobe
命令不僅卸載當(dāng)前驅(qū)動,也會同時卸載依賴驅(qū)動。
我們在串口終端調(diào)用裝載與卸載驅(qū)動的命令,怎么就會執(zhí)行裝載與卸載操作。對應(yīng)到驅(qū)動程序里我們有如下兩個函數(shù):
module_init(hello_init); //注冊模塊加載函數(shù)
module_exit(hello_exit); //注冊模塊卸載函數(shù)
這里加載與注冊有用到hello_init
、hello_exit
函數(shù),我們前面說的把hello_drv
驅(qū)動注冊到內(nèi)核就是在hello_init
函數(shù)里做,如:
左右滑動查看全部代碼>>>
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 注冊hello驅(qū)動 */
major = register_chrdev(0, /* 主設(shè)備號,為0則系統(tǒng)自動分配 */
"hello", /* 設(shè)備名稱 */
&hello_drv); /* 驅(qū)動程序 */
/* 下面操作是為了在/dev目錄中生成一個hello設(shè)備節(jié)點 */
/* 創(chuàng)建一個類 */
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
/* 創(chuàng)建設(shè)備,該設(shè)備創(chuàng)建在hello_class類下面 */
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
這里這個驅(qū)動程序入口函數(shù)hello_init中注冊完驅(qū)動程序之后,同時通過下面連個創(chuàng)建操作來創(chuàng)建設(shè)備節(jié)點,即在/dev目錄下生成設(shè)備文件。
據(jù)我了解,在之前版本的Linux內(nèi)核中,設(shè)備節(jié)點需要手動創(chuàng)建,即通過創(chuàng)建節(jié)點命令mknod 在/dev目錄下自己手動創(chuàng)建設(shè)備文件。既然已經(jīng)有新的方式創(chuàng)建節(jié)點了,這里就不摳之前的內(nèi)容了。
以上就是分享關(guān)于驅(qū)動一些內(nèi)容,通過以上分析,我們知道,其是有套路(就是常說的驅(qū)動框架)可尋的,比如:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 其她頭文件...... */
/* 一些驅(qū)動函數(shù) */
static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
}
static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
}
static int xxx_open (struct inode *node, struct file *file)
{
}
static int xxx_close (struct inode *node, struct file *file)
{
}
/* 其它驅(qū)動函數(shù)...... */
/* 定義自己的驅(qū)動結(jié)構(gòu)體 */
static struct file_operations xxx_drv = {
.owner = THIS_MODULE,
.open = xxx_open,
.read = xxx_read,
.write = xxx_write,
.release = xxx_close,
/* 其它程序......... */
};
/* 驅(qū)動入口函數(shù) */
static int __init xxx_init(void)
{
}
/* 驅(qū)動出口函數(shù) */
static void __exit hello_exit(void)
{
}
/* 模塊注冊與卸載函數(shù) */
module_init(xxx_init);
module_exit(xxx_exit);
/* 模塊許可證(必選項) */
MODULE_LICENSE("GPL");
按照這樣的套路來開發(fā)驅(qū)動程序的,有套路可尋那就比較好學(xué)習(xí)了,至少不會想著怎么起函數(shù)名而煩惱,哈哈
關(guān)于驅(qū)動的知識,這篇筆記中還可以展開很多內(nèi)容,限于篇幅就不展開了。我們之后再進行學(xué)習(xí)、分享。
下面看一下測試程序/應(yīng)用程序(hello_drv_test.c中的內(nèi)容,以下代碼來自:百問網(wǎng)):
左右滑動查看全部代碼>>>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
* ./hello_drv_test -w abc
* ./hello_drv_test -r
*/
int main(int argc, char **argv)
{
int fd;
char buf[1024];
int len;
/* 1. 判斷參數(shù) */
if (argc < 2)
{
printf("Usage: %s -w <string>\n", argv[0]);
printf(" %s -r\n", argv[0]);
return -1;
}
/* 2. 打開文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 3. 寫文件或讀文件 */
if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
{
len = strlen(argv[2]) + 1;
len = len < 1024 ? len : 1024;
write(fd, argv[2], len);
}
else
{
len = read(fd, buf, 1024);
buf[1023] = '\0';
printf("APP read : %s\n", buf);
}
close(fd);
return 0;
}
就是一些讀寫操作,跟我們學(xué)習(xí)文件操作是一樣的。學(xué)單片機的有些朋友可能不太熟悉main函數(shù)的這種寫法:
int main(int argc, char **argv)
main函數(shù)在C中有好幾種寫法(可查看往期筆記:main()函數(shù)有哪幾種形式?),在Linux中常用這種寫法。
argc與argv這兩個值可以從終端(命令行)輸入,因此這兩個參數(shù)也被稱為命令行參數(shù)。argc
為命令行參數(shù)的個數(shù),argv
為字符串命令行參數(shù)的首地址。
最后,我們把編譯生成的驅(qū)動模塊hello_drv.ko
與應(yīng)用程序hello_drv_test
放到共享目錄錄nfs_share中,同時在開發(fā)板終端掛載共享目錄:
mount -t nfs -o nolock,vers=4 192.168.1.104:/home/book/nfs_share /mnt
關(guān)于ntf網(wǎng)絡(luò)文件系統(tǒng)的使用可查看往期筆記:【Linux筆記】掛載網(wǎng)絡(luò)文件系統(tǒng)。
然后我們通過insmod
命令裝載驅(qū)動,但是出現(xiàn)了如下錯誤:
這是因為我們的驅(qū)動的編譯依賴與內(nèi)核版本,編譯用的內(nèi)核版本與當(dāng)前開發(fā)板運行的內(nèi)核的版本不一致所以會產(chǎn)生該錯誤。
重新編譯內(nèi)核,并把編譯生成的Linux內(nèi)核zImage映像文件與設(shè)備樹文件*.dts文件拷貝到開發(fā)板根文件系統(tǒng)的/boot目錄下,然后進行同步操作:
#mount -t nfs -o nolock,vers=4 192.168.1.114:/home/book/nfs_share /mnt
#cp /mnt/zImage /boot
#cp /mnt/.dtb /boot
#sync
下面是完整的hello驅(qū)動程序(來源:百問網(wǎng)):
左右滑動查看全部代碼>>>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
/* 1. 確定主設(shè)備號 */
static int major = 0;
static char kernel_buf[1024];
static struct class *hello_class;
#define MIN(a, b) (a < b ? a : b)
/* 3. 實現(xiàn)對應(yīng)的open/read/write等函數(shù),填入file_operations結(jié)構(gòu)體 */
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_to_user(buf, kernel_buf, MIN(1024, size));
return MIN(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_open (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
/* 2. 定義自己的file_operations結(jié)構(gòu)體 */
static struct file_operations hello_drv =
{
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
};
/* 4. 把file_operations結(jié)構(gòu)體告訴內(nèi)核:注冊驅(qū)動程序 */
/* 5. 誰來注冊驅(qū)動程序???得有一個入口函數(shù):安裝驅(qū)動程序時,就會去調(diào)用這個入口函數(shù) */
static int __init hello_init(void)
{
int err;
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
major = register_chrdev(0, "hello", &hello_drv); /* /dev/hello */
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
return 0;
}
/* 6. 有入口函數(shù)就應(yīng)該有出口函數(shù):卸載驅(qū)動程序時,就會去調(diào)用這個出口函數(shù) */
static void __exit hello_exit(void)
{
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
}
/* 7. 其他完善:提供設(shè)備信息,自動創(chuàng)建設(shè)備節(jié)點 */
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
嵌入式Linux的學(xué)習(xí)內(nèi)容是很多的、坑也是很多的,需要很有耐心地去學(xué)習(xí)。以上筆記如有錯誤,歡迎指出!
如果覺得文章不錯,轉(zhuǎn)發(fā)、在看,也是我們繼續(xù)更新得動力。