i.MX283開發板第一個Linux驅動-LED驅動

  • 字符設備驅動開發

字符設備是 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》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章