-
字符設備驅動開發
字符設備是 Linux 驅動中最基本的一類設備驅動,字符設備就是一個一個字節,按照字節流進行讀寫操作的設備,讀寫數據是分先後順序的。比如我們最常見的點燈、按鍵、IIC、SPI,LCD 等等都是字符設備,這些設備的驅動就叫做字符設備驅動。
Linux應用程序對驅動的調用順序如下圖所示:
驅動程序主要任務就是“打通”內核與硬件設備之間的通道,最終形成統一的接口(open、write、read...)供內核調用,編寫LED驅動程序實際上就是填充這些接口,下面就開始一步一步編寫一個LED的驅動程序。
1.查看硬件電路,確定LED對應的GPIO口以及工作條件
引腳爲LCD_D23 低電平點亮
2.編寫open、write、read、release函數
- 首先需要引用相關頭文件、定義相關宏
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/bcd.h>
#include <linux/capability.h>
#include <linux/rtc.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/gpio.h>
#include <../arch/arm/mach-mx28/mx28_pins.h>
#define DEVICE_NAME "imx283_led"//驅動名稱
#define major 200 //主設備號 僅靜態分配時需要
#define LED_GPIO MXS_PIN_TO_GPIO(PINID_LCD_D23) //for 283 287A/B
- open調用實現
static int led_open(struct inode *inode ,struct file *flip)
{
int ret = -1;
gpio_free(LED_GPIO);
ret = gpio_request(LED_GPIO, "LED1");
printk("gpio_request = %d\r\n",ret);
return 0;
}
該函數主要實現了向內核申請這個 GPIO 端口,同時把該引腳配置成 GPIO 工作模式。
- write調用實現
static int led_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
int ret = -1;
unsigned char databuf[1];
ret = copy_from_user(databuf,buf,1);
if(ret < 0 )
{
printk("kernel write error \n");
return ret;
}
gpio_direction_output(LED_GPIO, databuf[0]);
return ret;
}
在該函數的傳入參數中有 buf 和 count 參數。buf 表示應用程序調用 write 時,要寫入數
據的緩衝區;count 表示緩衝區中有效數據的長度。
gpio_direction_output函數作用是讓指定的IO口輸出0或者1.
注意:buf 緩衝區是在用戶空間的,而 LED 驅動程序是運行在內核空間的,並且在內核空間的代碼是不能直接訪問用戶空間的緩衝區的,所以需要使用 copy_from_user 宏把用戶空間 buf 中要寫入的數據複製到 data 緩衝區中。
- read調用實現
static int led_read(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
return 0;
}
對於一個LED燈我們只需要操作它輸出0或1即可,所以read操作直接返回0即可。
- release調用實現
static int led_release(struct inode *inode ,struct file *flip)
{
gpio_free(LED_GPIO);
return 0;
}
該函數的作用是釋放GPIO。
3.填充file_operations結構體
file_operations結構體定義在Linux內核源碼include/linux/fs.h中
/*
* NOTE:
* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl
* can be called without the big kernel lock held in all filesystems.
*/
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 *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
在上面的成員函數指針的形參中,struct file 表示一個打開的文件。Struct inode表示一個磁盤上的具體文件。
對於一個簡單的LED驅動只需要實現基本的open、read、write、release接口即可,實際需要實現哪些接口需要根據驅動的需求來決定。
static struct file_operations led_fops=
{
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.read = led_read,
.release = led_release,
};
owner 擁有該結構體的模塊的指針,一般設置爲 THIS_MODULE。
4.註冊與註銷字符設備
- 靜態分配主設備號:
上面已經做好了驅動的準備工作,但是內核還不知道這個設備,所以接下來需要“告訴”內核我們編寫的這個設備驅動,也就是向內核註冊設備。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
對於字符設備,我們一般調用register_chrdev函數註冊,major是需要註冊設備的主設備號,name是設備名稱,fops就是上面第3步填充的file_operations結構體。
爲了方便管理,Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,主設備號表示某一個具體的驅動,次設備號表示使用這個驅動的各個設備。
應用程序對設備文件進行讀寫操作時,都是通過主設備號找到這個設備的驅動文件,然後在驅動文件裏取得具體操作的函數,最後再進行相關操作。
我們可以使用cat /proc/devices 看我們系統中哪些設備號已經被使用了。
上圖就是已經被使用的設備號,我們最好不要再使用。
對於LED驅動,我們再一開始就定義了主設備號爲200 設備名稱爲imx283_led,下面就需要實現設備的初始化函數。
static int __init led_init(void)
{
int ret = -1;
ret = register_chrdev(major,DEVICE_NAME, &led_fops );
if(ret < 0)
{
printk("register chrdev failed!\n");
return ret;
}
printk("module init ok \n");
return ret;
}
注意:內核中只能用printk打印,不可使用printf
led_init函數將在驅動模塊被初始化(insmod)時調用。
同樣的,有註冊設備,就一定有註銷設備,函數unregister_chrdev就是註銷(卸載)一個字符設備,它的原型如下:
static inline void unregister_chrdev(unsigned int major, const char *name)
它只需要提供設備的主設備號和設備名稱即可。
我們還需要實現設備的註銷函數:
static void __exit led_exit(void)
{
unregister_chrdev(major,DEVICE_NAME);
printk("module exit ok\n");
}
-
動態分配主設備號:
register_chrdev函數第一個參數爲0,則表示需要內核動態分配主設備號,其合法返回值(大於0)就是分配的主設備號。
因此我們可以通過如下方式讓內核動態分配主設備號,而不需要我們手動設置。
static int MAJOR = 0;
static int __init led_init(void)
{
MAJOR = register_chrdev(0,DEVICE_NAME, &led_fops );
printk("major=%d\n",MAJOR);
printk("module init ok \n");
return 0;
}
static void __exit led_exit(void)
{
unregister_chrdev(MAJOR,DEVICE_NAME);
printk("module exit ok\n");
}
由於我們是以模塊的形式加載或者卸載設備驅動,我們還需要向內核註冊模塊的加載和卸載函數,就是說上面實現的led_init和led_exit兩個函數也需要向內核註冊,不然執行insmod或rmmod時,是不會調用led_init或led_exit函數的。
模塊的加載和卸載註冊函數如下:
module_init(xxx_init); //註冊模塊加載函數
module_exit(xxx_exit); //註冊模塊卸載函數
參數 xxx_init 就是需要註冊的具體函數,這裏就是剛剛說的led_init和led_exit函數。
module_init(led_init);
module_exit(led_exit);
5.添加作者和LICENSE信息
LICENSE信息必不可少,否則編譯會報錯。
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("imx283 first driver");//描述信息
MODULE_AUTHOR("xzx2020");//作者信息
到此,一個LED驅動就完成了,接下來就是編譯和編寫測試程序(應用程序)。
6.驅動程序Makefile編譯腳本
/*obj-m:內核模塊文件,指將myleds.o編譯成myleds.ko*/
obj-m:=led_driver.o
PWD:=$(shell pwd)
KDIR:=/ZLG_linux/linux-2.6.35.3
all:
$(MAKE) -C $(KDIR) M=$(PWD)
/*M=pwd :指定當前目錄*/
/*make -C $(KERN_DIR) 表示將進入(KERN_DIR)目錄,執行該目錄下的Makefile*/
clean:
rm -rf *.ko *.order *.symvers *.cmd *.o *.mod.c *.tmp_versions .*.cmd .tmp_versions
led_driver是驅動文件名(我這裏命名爲led_driiver.c)
KDIR 是內核源碼的主目錄
保證驅動文件(.c文件)和MakeFile在同一目錄下,執行make就可以生成驅動的.ko文件。
最終生成的led_driiver.ko就是我們需要的文件。
7.編寫測試程序
測試程序很簡單,就是打開led設備文件,然後控制led間隔200ms閃爍一段時間,最後再關閉該設備文件。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <limits.h>
#include <asm/ioctls.h>
#include <time.h>
#include <pthread.h>
int main(void)
{
int fd = -1,i;
char buf[1]={0};
fd = open("/dev/imx283_led",O_RDWR);
if(fd < 0)
{
printf("open /dev/imx283_led fail fd=%d\n",fd);
}
for(i=0;i<50;i++)
{
buf[0] = 0;
write(fd,buf,1);
usleep(200000);
buf[0] = 1;
write(fd,buf,1);
usleep(200000);
}
fd = close(fd);
if(fd < 0)
{
printf("led test error\n");
}
return 0;
}
同樣的,我們也可以寫一個測試程序的MakeFile編譯測試程序,或者直接用命令
arm-fsl-linux-gnueabi-gcc led_test.c -o led_test
即可編譯生成測試程序的可執行文件。
測試程序makefile:
EXEC = ./led_test
OBJS = led_test.o
CROSS = arm-fsl-linux-gnueabi-
CC = $(CROSS)gcc
STRIP = $(CROSS)strip
CFLAGS = -Wall -g -O2
all: clean $(EXEC)
$(EXEC):$(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
$(STRIP) $@
clean:
-rm -f $(EXEC) *.o
這時候,把上面生成的.ko文件和led_test文件想辦法弄到開發板上就可以測試了(我這裏用的是U盤拷貝)。
8.測試
首先執行insmod加載驅動。
可以看到,模塊加載成功,此時執行cat /proc/devices 應該能看到我們剛剛加載的設備:
接着我們需要手動創建設備節點,因爲此時/dev目錄下是沒有我們這個imx283_led設備的。
//格式 mknod /dev/xxx 設備類型 主設備號 次設備號
//主設備號是cat /proc/devices裏看到的 次設備號需要我們手動填寫這裏設置爲0 最大255
mknod /dev/imx283_led c 200 0
接着再查看/dev下有沒有生成LED設備節點。
ls -l /dev|grep led
↵
可以看到/dev下已經生成了imx283_led設備節點,主設備號200,次設備號0,名稱imx283_led。
這時候就可以執行測試程序了。
不出意外的話,開發板上的led燈應該已經開始閃爍了。
最後,再執行rmmod卸載設備驅動
卸載也沒有什麼問題。
9.總結
鑑於筆者水平有限,同時也是Linux驅動初學者,以上只是個人學習的總結,難免會有錯誤紕漏之處,望各位網友多多批評指教。
由於現在較新的Linux內核(2.6以上)的字符設備驅動開發已經不提倡這種註冊方式,所以下一篇博客已對此驅動作了一些改進:i.MX283開發板第一個Linux驅動-LED驅動改進
本文參考:
1.《嵌入式Linux應用完全開發手冊》
2.《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.0》
3.《EasyARM-iMX28xx Linux開發指南 20150901 V1.03》