/* AUTHOR: Pinus
* Creat on : 2018-10-11
* KERNEL : linux-4.4.145
* BOARD : JZ2440(arm9 s3c2440)
* REFS : 韋東山視頻教程第二期
《LINUX內核源碼情景分析》
*/
概述
Unix類系統將設備也看作是文件,通過操作文件的方式操作硬件。而操作文件的方式無非就是open、read、write、close等,將這些通用的文件操作函數抽象出來,就是file_operation結構體。(實際內核定義如下,可以看見各種會用到的函數裏面都有)
struct file_operations {
struct module *owner;
...
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
int (*open) (struct inode *, struct file *);
...
};
那麼顯然,要想編寫出實際有用的代碼,我們起碼要實現對應的函數,這樣當你在應用程序中調用open,就會實際調用設備驅動的open。其重要性可見一斑。【思考一:內核是如何將app裏的操作函數和驅動裏的操作函數聯繫上的呢?】先通過led實驗瞭解如何簡單實用,和驅動的簡單框架。
實驗
目標:點亮開發板上的led燈
一、包含頭文件,寫出入口和出口函數,添加必要協議和修飾
頭文件,協議和修飾就略了,要是這不知道就去看前一篇吧。《(1) 設備驅動的最基本框架》
1. 入口函數的實現
unsigned int major;
static int __init leds_drv_init(void) /* 入口函數 */
{
unsigned int minor = 0;
gpio_va = ioremap(0x56000000, 0x100000);
if (!gpio_va) {
return -EIO;
}
major = register_chrdev(0, "leds_dev", &jz2440_leds_fops); //註冊 告訴內核
if (major < 0)
{
printk("leds_dev can't register major number\n");
return major;
}
leds_class = class_create(THIS_MODULE, "leds_class"); //創建一個類
leds_dev[0] = device_create(leds_class, NULL, MKDEV(major, 0), NULL, "leds"); //在類下創建設備節點"leds"
for (minor = 1; minor < 4; minor++)
{
leds_dev[minor] = device_create(leds_class, NULL, MKDEV(major, minor), NULL, "led%d", minor); //創建設備節點"ledx" 次設備號不同
}
printk("leds_dev initialized\n");
return 0;
}
2.主設備號的概念:linux中使用ls -l 查看某個設備的具體信息時,可以看見主設備號和次設備號,內核通過主設備來選擇對應的設備和驅動,次設備號則是純軟件的概念,用於用同一個設備驅動使用幾個不同的外設,比如一個led_drv驅動led0,led1,led2... 【思考二;內核怎樣創建設備,主設備號具體應用是什麼呢?】
/* 註冊字符設備
* 參數爲主設備號、設備名字、file_operations結構;
* 這樣,主設備號就和具體的file_operations結構聯繫起來了,
* 操作主設備爲LED_MAJOR的設備文件時,就會調用s3c24xx_leds_fops中的相關成員函數
* LED_MAJOR可以設爲0,表示由內核自動分配主設備號
*/
major = register_chrdev(0, "leds_dev", &jz2440_leds_fops); //註冊 告訴內核
內核提供了一個register_chrdev()函數用來註冊設備(英文翻譯一下register char device),
第一個參數:0,寫0表示讓內核自動分配主設備號,並返回給major;
第二個參數:“leds_dev”, 設備名;
第三個參數;&jz2440_leds_fops,指向一個真實自定義file_operation結構體;
static const struct file_operations jz2440_leds_fops = {
.owner = THIS_MODULE,
.open = leds_drv_open,
.write = leds_drv_write,
};
顯然正確的實現這個結構體便是重點。
3. 註冊設備類,在類下注冊具體設備節點
【思考三:爲什麼要創建類呢?】
leds_class = class_create(THIS_MODULE, "leds_class"); //創建一個類
【思考四:創建設備節點的具體過程?】
leds_dev[0] = device_create(leds_class, NULL, MKDEV(major, 0), NULL, "leds"); //創建設備節點"leds"
for (minor = 1; minor < 4; minor++)
{
leds_dev[minor] = device_create(leds_class, NULL, MKDEV(major, minor), NULL, "led%d", minor); //創建設備節點"ledx" 次設備號不同
}
從這裏也可以看出,主設備號是內核用的,次設備號是純軟的概念,給我們編程用的
4. 操作硬件所需要的具體物理地址映射,將物理地址映射爲內核的虛擬地址,ioremap爲內核提供的函數
static unsigned long gpio_va;
#define GPIO_OFT(x) ((x) - 0x56000000) /* 具體物理地址有s3c2440芯片手冊得來 */
#define GPFCON (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000050)))
#define GPFDAT (*(volatile unsigned long *)(gpio_va + GPIO_OFT(0x56000054)))
gpio_va = ioremap(0x56000000, 0x100000); /* 參數:物理起始地址,要映射的大小1M */
5.出口函數的實現
static void __exit leds_drv_exit(void)
{
unsigned int minor;
iounmap(gpio_va);
unregister_chrdev(major, "leds_dev"); //卸載設備驅動,注意第二個參數要與註冊時保持一致
for (minor = 0; minor < 4; minor++)
{
device_unregister(leds_dev[minor]); //卸載類下的設備
}
class_destroy(leds_class); //卸載類
}
看那些函數,就是調用了在入口函數中調用函數的相反函數,上面註冊,下面註銷,一一對應
二、file_operation結構體內函數的實現
那麼長時間終於再次回到了我們的課題,file_operation
static const struct file_operations jz2440_leds_fops = {
.owner = THIS_MODULE,
.open = leds_drv_open,
.write = leds_drv_write,
};
file_operations 可以用來實現很多功能,這裏我們只要實現兩個,open和write
static int leds_drv_open(struct inode *inode, struct file *file)
{
/*配置GPF4,5,6*/
GPFCON &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2))); //現將對應位清零
GPFCON |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2))); // 01 輸出
return 0;
}
函數名自己取,需要傳遞的參數呢從file_operation結構體複製
有過裸板開發的應該很清楚,想點亮led的無非兩步,
a、配置對應GPIO的配置寄存器,使其爲輸出模式,
b、再修改對應的數據寄存器,使其輸出,就可以點亮led燈了。
顯然這裏的leds_drv_open()函數,就是配置寄存器
static ssize_t leds_drv_write(struct file *file, const char *buf, size_t count, loff_t *pos)
{
unsigned int minor = MINOR(file->f_inode->i_rdev); //MINOR(inode->i_cdev); 得到次設備號
char val;
copy_from_user(&val, buf, count); //將數據從用戶空間傳到內核空間 反之copy_to_user
switch (minor) /* 看,這就是次設備號的純軟的概念,和它的基本應用 */
{
case 0: /* /dev/leds */
{
printk("/dev/leds: %d %d\n", minor, val);
if(val==1)
{
GPFDAT &= ~((1<<4) | (1<<5) | (1<<6));
}
else
{
GPFDAT |= (1<<4) | (1<<5) | (1<<6);
}
break;
}
case 1: /* /dev/led1 */
{
if(val==1)
{
GPFDAT &= ~(1<<4);
}
else
{
GPFDAT |= (1<<4);
}
break;
}
case 2: /* /dev/led2 */
{
if(val==1)
{
GPFDAT &= ~(1<<5);
}
else
{
GPFDAT |= (1<<5);
}
break;
}
case 3: /* /dev/led3 */
{
if(val==1)
{
GPFDAT &= ~(1<<6);
}
else
{
GPFDAT |= (1<<6);
}
break;
}
default: printk("/dev/leds: %d %d\ ?? n", minor, val);
}
return 0;
}
這樣我們就實現了具體的file_operation結構體的具體功能函數,
major = register_chrdev(0, "leds_dev", &jz2440_leds_fops); //在這裏傳入,使之與具體的設備關聯在一起
三、測試程序的實現
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
void print_usage(char *drv);
int main(int argc, char **argv)
{
int fd;
char* filename;
char val = 1;
if (argc != 3)
{
print_usage(argv[0]);
return 1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0)
printf("can't open dev nod !\n");
if (strcmp(argv[2], "on") == 0)
{
val = 1;
}
else if (strcmp(argv[2], "off") == 0)
{
val = 0;
}
else
{
print_usage(argv[0]);
return 1;
}
printf("%s : %d\r\n", argv[1], val);
write(fd, &val, 1);
return 0;
}
/* 打印用法 */
void print_usage(char *drv)
{
printf("Usage : \n");
printf("%s <dev> <on|off>\n", drv);
printf("eg. \n");
printf("%s /dev/leds on\n", drv);
printf("%s /dev/leds off\n", drv);
printf("%s /dev/led1 on\n", drv);
printf("%s /dev/led1 off\n", drv);
// <>表示內部的參數不可省略,|表示或 ?不把argv[0]打印出來避免一個越界錯誤
}
編譯之後也放在板子上,安裝模塊,就可以測試了
文中提到的思考題就在下一篇在解答吧,本人也是初學,如有不足請大佬指正
具體程序可以通過下面下載實現代碼: