在很多嵌入式系統(tǒng)里,有可能需要實現(xiàn)數(shù)字開關量輸出,比如:
嵌入式Linux一般需求千變?nèi)f化,也不可能這些需求都有現(xiàn)成設備驅動代碼可供使用,所以如何學會完成一個開關量輸出設備的驅動,一方面點個燈可以比較快了解如何具體寫一個字符類設備驅動,另一方面實際項目中對于開關量輸出設備就可以這樣干,所以是具有較強的實用價值的。
要完成這樣一個開關量輸出GPIO的驅動程序,需要梳理梳理下面這些概念:
字符設備是通過文件系統(tǒng)內(nèi)的設備名稱進行訪問的,其本質是設備文件系統(tǒng)樹的節(jié)點。故Linux下設備也是一個文件,Linux下字符設備在/dev目錄下??梢栽陂_發(fā)板的控制臺或者編譯的主Linux系統(tǒng)中利用ls -l /dev查看,如下圖:
對于ls -l列出的屬性,做一個比較細的解析:
細心的朋友或許會發(fā)現(xiàn)設備號屬性,在有的文件夾下列出來不是這樣,這就對了!普通文件夾下是這樣:
差別在于一個是文件大小,一個是設備號。
再細心一點的朋友或許還會問,這些/dev下的文件時間屬性為神馬都相差無幾?這是因為/dev設備樹節(jié)點是在內(nèi)核啟動掛載設備驅動動態(tài)生成的,所以時間就是系統(tǒng)開機后按次序生成的,你如不信,不妨重啟一下系統(tǒng)在查看一下。
常見文件類型:
d: directory 文件夾 l: link 符號鏈接 p: FIFO pipe 管道文件,可以用mkfifo命令生成創(chuàng)建 s: socket 套接字文件 c: char 字符型設備文件 b: block 塊設備文件 -:常規(guī)文件
回到設備號,設備號是一個32位無符號整型數(shù),其中:
這怎么理解呢,看下串口類設備就比較清楚了:
主設備號一樣證明這些設備共用了一個驅動程序,而次設備號不一樣,則對應了不同的串口設備。那么怎么得到設備號呢?
/*下列定義位于./include/linux/types.h */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
/* 下面宏用于生成主設備號,次設備號 */
/* 下列定義位于./include/linux/Kdev_t.h */
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
使用舉例:
/* 主設備號 */
MAJOR(dev_t dev);
/* 次設備號 */
MINOR(dev_t dev);
為簡化問題,本文描述一下動態(tài)加載設備驅動模塊,暫不考慮設備樹。參考<<Linux設備驅動程序>>一書??蓞⒄涨拔膶Ⅱ寗泳幾g成模塊,然后利用下面腳步動態(tài)加載模塊。由前面描述,知道設備最終需要在/dev目錄下生成一個設備文件,那么這個設備文件節(jié)點是怎么生成呢,看看下面的腳本:
#!/bin/sh
#-----------------------------------------------------------------------
module='led'
device='led'
mode='664'
group='staff'
# 利用insmod命令加載設備模塊
insmod -f $module.ko $* || exit 1
# 獲取系統(tǒng)分配的主設備號
major=`cat /proc/devices | awk '\\$2==\'$module\' {print \\$1}'`
# 刪除舊節(jié)點
rm -f /dev/${device}
#創(chuàng)建設備文件節(jié)點
mknod /dev/${device} c $major 0
#設置設備文件節(jié)點屬性
chgrp $group /dev/${device}
chmod $mode /dev/${device}
這里要提一下/proc/devices,這是一個文件記錄了字符和塊設備的主設備號,以及分配到這些設備號的設備名稱。比如使用cat命令來列出這個文件內(nèi)容:
字符設備由什么關鍵數(shù)據(jù)結構進行抽象的呢,來看看:
file_operations定義在./include/linux/fs.h cdev定義在./include/linux/cdev.h
cdev中與字符設備驅動編程相關兩個數(shù)據(jù)域:
文件操作符是一個龐大的數(shù)據(jù)結構,常規(guī)字符設備驅動一般需要實現(xiàn)下面一些函數(shù)指針:
先上代碼(可左右滑動顯示):
#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/kernel.h> /* printk() */
#include <linux/major.h>
#include <linux/cdev.h>
#include <linux/fs.h> /* everything... */
#include <linux/gpio.h>
#include <asm/uaccess.h> /* copy_*_user */
/*這里具體參考不同開發(fā)板的電路 GPIOC24 */
#define LED_CTRL (2*32+24)
static const unsigned int led_pad_cfg = LED_CTRL;
struct t_led_dev{
struct cdev cdev;
unsigned char value;
};
struct t_led_dev led_dev;
static dev_t led_major;
static dev_t led_minor=0;
static int led_open(struct inode * inode,struct file * filp)
{
filp->private_data = &led_dev;
printk ('led is opened!\n');
return 0;
}
static int led_release(struct inode * inode,
struct file * filp)
{
return 0;
}
static ssize_t led_read(struct file * file,
char __user * buf,
size_t count,
loff_t *ppos)
{
ssize_t ret=1;
if(copy_to_user(&(led_dev.value),buf,1))
return -EFAULT;
printk ('led is read!\n');
return ret;
}
static ssize_t led_write(struct file * filp,
const char __user *buf,
size_t count,loff_t *ppos)
{
unsigned char value;
ssize_t retval = 0;
if(copy_from_user(&value,buf,1))
return -EFAULT;
if(value&0x01)
gpio_set_value(led_pad_cfg, 1);
else
gpio_set_value(led_pad_cfg, 0);
printk ('led is written!\n');
return retval;
}
static const struct file_operations led_fops = {
.owner = THIS_MODULE,
.read = led_read,
.write = led_write,
.open = led_open,
.release = led_release,
};
static void led_setup_cdev(struct t_led_dev * dev, int index)
{
/* 初始化字符設備驅動數(shù)據(jù)域 */
int err,devno = MKDEV(led_major,led_minor+index);
cdev_init(&(dev->cdev),&led_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &led_fops;
/* 字符設備注冊 */
err = cdev_add(&(dev->cdev),devno,1);
if(err)
printk(KERN_NOTICE 'Error %d adding led %d',err,index);
}
static int led_gpio_init(void)
{
if (gpio_request(LED_CTRL, 'led') < 0) {
printk('Led request gpio failed\n');
return -1;
}
printk('Led gpio requested ok\n');
gpio_direction_output(LED_CTRL, 1);
gpio_set_value(LED_CTRL, 1);
return 0;
}
/* 注銷設備 */
void led_cleanup(void)
{
dev_t devno = MKDEV(led_major, led_minor);
gpio_set_value(LED_CTRL, 0);
gpio_free(LED_CTRL);
cdev_del(&led_dev.cdev);
unregister_chrdev_region(devno, 1); //注銷設備號
}
/* 注冊設備 */
static int led_init(void)
{
int result;
dev_t dev = MKDEV( led_major, 0 );
/* 動態(tài)分配設備號 */
result = alloc_chrdev_region(&dev, 0, 1, 'led');
if(result<0)
return result;
led_major = MAJOR(dev);
memset(&led_dev,0,sizeof(struct t_led_dev));
led_setup_cdev(&led_dev,0);
led_gpio_init();
printk ('led device initialised!\n');
return result;
}
module_init(led_init);
module_exit(led_cleanup);
MODULE_DESCRIPTION('Led device demo');
MODULE_AUTHOR('embinn');
MODULE_LICENSE('GPL');
來總結一下要點:
init函數(shù),需要用module_init宏包起來,本例中即為led_init,module_init宏的作用就是選編譯為模塊或進內(nèi)核的底層實現(xiàn),建議剛開始不必深究。一般而言主要實現(xiàn):
exit函數(shù),一樣需要用module_exit包起來,主要負責:
用戶空間與驅動數(shù)據(jù)交換
善用printk進行驅動調(diào)試,這是內(nèi)核打印函數(shù)。
gpio相關操作函數(shù),這里就不一一列舉其作用了,比較容易理解。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define READ_SIZE 10
int main(int argc, char **argv){
int fd,count;
float value;
unsigned char buf[READ_SIZE+1];
printf( 'Cmd argv[0]:%s,argv[1]:%s,argv[2]:%s\n',argv[0],argv[1],argv[2] );
if( argc<2 ){
printf( '[Usage: test device_name ]\n' );
exit(0);
}
if(strlen(argv[2]!=1)
printf( 'Invalid parameter\n' );
if(( fd = open(argv[1],O_WRONLY ))<0){
printf( 'Error:can not open the device: %s\n',argv[1] );
exit(1);
}
if(argv[2][0] == '1')
buf[0] = 1;
else if(argv[2][0] == '0')
buf[0] = 0;
else
printf( 'Invalid parameter\n' );
printf('write: %d\n',buf[0]);
if( (count = write( fd, buf ,1 ))<0 ){
perror('write error.\n');
exit(1);
}
close(fd);
printf('close device %s\n',argv[1] );
return 0;
}
編譯成可執(zhí)行文件,調(diào)用前面的腳本加載設備后,在/dev下就可以看到led設備了。比如測試代碼編譯成ledTest執(zhí)行文件,則使用下面命令運行測試程序就可以看到led控制效果了:
/*打開led 具體取決電路是高有效還是低有效*/
./ledTest /dev/led 1
./ledTest /dev/led 0
這樣就實現(xiàn)了用戶空間驅動底層設備了,實際應用代碼就可以這樣去訪問底層的字符型設備。
本文總結了簡單字符設備的驅動開發(fā)的一些要點,以及如何動態(tài)加載,在設備文件系統(tǒng)樹上創(chuàng)建設備節(jié)點,并演示了驅動以及驅動使用的基本要點。
一口君個人微信