去年开的博客,到今天只有两篇文章,再看看同学的博客,惭愧啊。
写博客虽然额外多花了些时间,不过对自学知识有复习与总结的作用,还能提高语言组织能力,而且,如果有朋友能从我的博客中获益那就再好不过了。希望自己养成写博客的习惯,就从这一篇开始吧。
LED驱动是最简单的字符设备,可以说是Linux设备驱动程序里的HelloWorld,适合用来熟悉字符设备驱动程序开发的基本流程
笔者使用的是JZ2440开发板,板上有三个LED,分别对应GPF4,GPF5,GPF6引脚。本节将实现4个led设备:
/dev/leds
/dev/led0
/dev/led1
/dev/led2
第一个设备对应3个led的整体,后面3个分别对应其中一个led
先看看设备驱动编写时涉及到的基本知识,包括一些数据结构和函数:
设备编号包括主次编号,主编号用来告诉系统使用哪个驱动程序去驱动该设备,次编号用来指定特定的设备
以下结构用于记录设备的编号:
dev_t dev;
在32位机上dev_t为32位数据,其中12位用来记录主编号,20位用于记录次编号。
我们使用以下两个宏来从dev_t中获取主次编号:
MAJOR(dev_t dev);
MINOR(dev_t dev);
反之,在已知主次编号时用以下的宏来得出dev_t:
MKDEV(int major, int minor);
分配设备编号有几种方法,第一种是静态分配:
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first 是要分配的起始设备编号;count是要分配的设备编号总数。
通常编译到内核的设备驱动或常用的设备驱动使用静态方法,为了保证主编号的唯一性,我们自己编写驱动时推荐使用动态方法:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);
firstminor为要分配的第一个次编号,通常取0;count同上
这两个函数通常在模块初始化中调用
释放设备编号:
void unregister_chrdev_region(dev_t first, unsigned int count);
通常在模块退出时调用
加载设备驱动后可以在/proc/devices中找到设备的主次编号
struct file_operations f_ops;
一个函数指针的集合,这些函数负责实现系统调用如open,close,read,write,ioctl等
struct cdev cdev;
系统用来记录字符设备的结构
方法一:
注册:
int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops);
这里其实用一个函数完成了两个功能:使用fops初始化主编号为major的字符设备并且向内核注册该设备
注销:
int unregister_chrdev(unsigned int major, const char *name);
方法二:
把方法一中提到的“两个功能”分两步完成
先调用
void cdev_init(struct cdev *cdev, struct file_operations *fops);
这一步使用fops初始化cdev
再调用
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
这一步向系统注册cdev
如果你想定义自己的设备结构并且在其中包含struct cdev成员,显然你只能使用这个方法
使用这个方法注册的设备要用以下函数注销:
void cdev_del(struct cdev *dev);
在f_ops中的read,write函数通常需要调用以下函数来与用户空间进行数据交互:
unsigned long copy_to_user(void __user *to,
const void *from,unsigned long count);
unsigned long copy_from_user(void *to,
const void __user *from,unsigned long count);
为何不直接用memcpy()?
很显然to和from不在同一个地址空间,所以直接memcpy的话将会发生不可预料的事。
有了这些基础之后,就可以开始编写基本的led驱动程序了
以下是完整代码:
//leds.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEVICE_NAME "led"
#define LED_DEV_NR 4
#define LED_ON 0
#define LED_OFF 1
/*
* 笔者自己定义的表示led设备的结构,之所以写成这样是为了以后编写更复杂的设备
* 驱动时可以拿这个当模板用,到时侯可能往此结构中添加成员
*/
struct led_dev {
struct cdev cdev;
};
struct led_dev *led_devs; //指向led_dev数组的指针
char leds_status; //记录3个led状态的位图,灯亮对应位置位,灯灭对应位复位
DECLARE_MUTEX(leds_lock); //对全局变量操作时要锁住此互斥锁,这是为了保证操作的可重入性
/*
* open()系统调用的实现,这里什么都不做
*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* read()系统调用的实现,功能是读取led设备的状态
*/
static int led_read(struct file *filp, char __user *buff,
size_t count, loff_t *offp)
{
int minor = MINOR(filp->f_dentry->d_inode->i_rdev); //获得目标文件对应的设备的次编号
char val;
switch (minor) {
case 0: // minor=0对应/dev/leds,对其进行读操作将读到3个led的状态对应的二进制值
down(&leds_lock);
val = leds_status;
up(&leds_lock);
if (copy_to_user(buff, (const void *)&val, 1))
return -EFAULT;
break;
case 1: //minor=1,2,3时对应/dev/led0,1,2,对其进行读操作将读到对应led的状态(亮1灭0)
down(&leds_lock);
val = leds_status & 0x1;
up(&leds_lock);
if(copy_to_user(buff, (const void *)&val, 1))
return -EFAULT;
break;
case 2:
down(&leds_lock);
val = (leds_status >> 1) & 0x1;
up(&leds_lock);
if (copy_to_user(buff, (const void *)&val, 1))
return -EFAULT;
break;
case 3:
down(&leds_lock);
val = (leds_status >> 2) & 0x1;
up(&leds_lock);
if (copy_to_user(buff, (const void *)&val, 1))
return -EFAULT;
break;
}
return 1;
}
/*
* write系统调用的实现,将改变led设备的状态
*/
static ssize_t led_write(struct file *filp, const char __user *buff,
size_t count, loff_t *offp)
{
int minor = MINOR(filp->f_dentry->d_inode->i_rdev);
char val;
if (copy_from_user(&val, buff, 1))
return -EFAULT;
switch (minor) {
case 0: //对应/dev/leds,同时改变3个led的状态
down(&leds_lock);
s3c2410_gpio_setpin(S3C2410_GPF4, !(val & 0x1));
s3c2410_gpio_setpin(S3C2410_GPF5, !((val >> 1) & 0x1));
s3c2410_gpio_setpin(S3C2410_GPF6, !((val >> 2) & 0x1));
leds_status = val;
up(&leds_lock);
//printk("led: %d\n", leds_status);
break;
case 1:
s3c2410_gpio_setpin(S3C2410_GPF4, !val);
if (val == 0) {
down(&leds_lock);
leds_status &= ~(1<<0);
up(&leds_lock);
} else {
down(&leds_lock);
leds_status |= (1<<0);
up(&leds_lock);
}
break;
case 2:
s3c2410_gpio_setpin(S3C2410_GPF5, !val);
if (val == 0) {
down(&leds_lock);
leds_status &= ~(1<<1);
up(&leds_lock);
} else {
down(&leds_lock);
leds_status |= (1<<1);
up(&leds_lock);
}
break;
case 3:
s3c2410_gpio_setpin(S3C2410_GPF6, !val);
if (val == 0) {
down(&leds_lock);
leds_status &= ~(1<<2);
up(&leds_lock);
} else {
down(&leds_lock);
leds_status |= (1<<2);
up(&leds_lock);
}
break;
}
return 1;
}
/*
* 模块close()时将调用的函数
*/
int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
struct file_operations led_fops = {
.owner = THIS_MODULE,
.read = led_read,
.write = led_write,
.open = led_open,
.release = led_release,
};
dev_t led_dev_num; //记录起始led设备号
int led_major; //记录led设备主编号
/*
* 模块卸载(rmmod)时将调用的函数
*/
static void __exit leds_cleanup_module(void)
{
int i;
if (led_devs) {
for (i = 0; i < LED_DEV_NR; i ++) {
cdev_del(&led_devs[i].cdev);
}
kfree(led_devs);
}
unregister_chrdev_region(led_dev_num, LED_DEV_NR);
printk(DEVICE_NAME " uninstalled.\n");
}
static void leds_lowlevel_init(void)
{
down(&leds_lock);
s3c2410_gpio_cfgpin(S3C2410_GPF4, S3C2410_GPF4_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF5, S3C2410_GPF5_OUTP);
s3c2410_gpio_cfgpin(S3C2410_GPF6, S3C2410_GPF6_OUTP);
s3c2410_gpio_setpin(S3C2410_GPF4, !0);
s3c2410_gpio_setpin(S3C2410_GPF5, !0);
s3c2410_gpio_setpin(S3C2410_GPF6, !0);
leds_status = 0;
up(&leds_lock);
}
static int __init leds_init_module(void)
{
int result;
int i;
struct cdev *cdev = NULL;
if ((result = alloc_chrdev_region(&led_dev_num, 0, LED_DEV_NR, DEVICE_NAME)) < 0) { //分配LED_DEV_NR个设备号,起始次编号为0,起始设备号存储在led_dev_num中
printk(DEVICE_NAME " fail to get major.\n");
return result;
}
led_major = MAJOR(led_dev_num);
led_devs = kmalloc(LED_DEV_NR * sizeof(struct led_dev), GFP_KERNEL); //分配struct led_dev结构,这是我自己定义的一个表示led设备的结构,其中包含struct cdev
if (!led_devs) {
printk(DEVICE_NAME " fail to allocate mem.\n");
result = -ENOMEM;
goto fail;
}
memset(led_devs, 0, LED_DEV_NR * sizeof(struct led_dev));
leds_lowlevel_init(); //底层硬件初始化可在模块加载时完成
for (i = 0; i < LED_DEV_NR; i ++) {
cdev = &led_devs[i].cdev;
cdev_init(cdev, &led_fops); //初始化cdev
cdev->owner = THIS_MODULE;
if(cdev_add(cdev, MKDEV(led_major, i), 1)) { //注册cdev
printk(DEVICE_NAME": error when adding led%d", i);
result = -1000;
goto fail;
}
}
printk(DEVICE_NAME " initialized.\n");
return 0;
fail:
leds_cleanup_module(); //如果初始化过程中出错,务必将已经分配的资源还给系统再返回!
return result;
}
module_init(leds_init_module);
module_exit(leds_cleanup_module);
MODULE_AUTHOR("ZZ");
MODULE_LICENSE("Dual BSD/GPL");
//end of leds.c
make生成leds.ko,将其拷贝到开发板上,接着加载之:
insmod leds.ko
接下来创建设备节点。先找到主编号:
cat /proc/device
找到上面定义的DEVICE_NAME(即”led”)对应的编号,即led设备的主编号,我这里为252。接着创建节点:
mknod /dev/leds c 252 0
mknod /dev/led0 c 252 1
mknod /dev/led1 c 252 2
mknod /dev/led2 c 252 3
然后就可以向设备文件里读写数据测试了!
下面是我写的测试程序:
//chled.c
#include
#include
#include
#include
int main(int argc, char **argv)
{
int led_no;
int fd;
char val;
char *filename = NULL;
if (argc < 2) {
printf("Input filename.\n");
}
else {
filename = argv[1]; //第一个参数是设备文件名
fd = open (filename, O_RDWR);
if (fd < 0) {
printf("Fail to open %s\n", filename);
return 1;
}
if (argc == 2) { //若(除命令名外)只有1个参数,则读出设备状态
read(fd, &val, 1);
printf("%d\n", val);
return 0;
}
else if (argc == 3) { //若有2个参数则将第二个参数指定的数字写入设备
val = argv[2][0] - '0';
write(fd, &val, 1);
return 0;
}
close(fd);
}
return 1;
}
//end of chled.c