概述: 字符设备驱动程序: 是按照字符设备要求完成的由操作系统调用的代码。
重点理解以下内容:
1. 驱动是写给操作系统的代码,它不是直接给用户层程序调用的,而是给系统调用的
2. 所以驱动要向系统注册。
3. 注册的时候,要求驱动必须符合一定的规范,否则系统就会不认识。这就是程序架构。
4. 字符设备驱动对应一个cdev 结构, 需要向系统注册或申请设备号,注册cdev设备,
完成cdev 设备需要的操作,诸如读,写,ioctl操作等。
5. 系统下驱动以模块的形式而存在
6. 用户空间验证,需要先建立设备节点
例如 mknod /dev/gpio c 126 0 将创建/dev/gpio 节点, 主设备号126,从设备号0
然后用 echo 'a' >/dev/gpio 查看写入
cat /dev/gpio 查看读出。
你也可以书些标准的文件访问来测试 /dev/gpio, 这里从略。
这个126 如果是系统申请的,则是动态的,你需要用cat /proc/devices 去查询系统给你的驱动分配了什么设备号。
然后再创建设备结点。
如果启用了sysfs, 则在 /sys/module/gpio 目录下有相应的属性信息描述。
补充:
1。可以用cat /proc/devices | grep <设备名> 查看系统分配(或自己指定)的主设备号。
2. 当mknod 以后,可以用 ll /dev | grep <设备名> 查看设备的主设备号,从设备号
下面给出一个实例:
gpioadaptor.c 演示字符设备向系统注册的情景。
gpio.c 是真正的硬件驱动代码。(这里只是用printk 打印了相关信息)
在centos 3.10 内核上测试通过
--------------------------------------------------------------------------------
/*======================================================================
A gpio driver as an example of char device drivers
author: hjjdebug
date: Fri May 9 17:54:33 CST 2014
======================================================================*/
// gpioadaptor.c
// struct cdev 定义, 字符型设备结构体。标准化结构
#include <linux/cdev.h>
// struct file 定义, 设备操作是按文件来操作的,所以用到文件指针
#include <linux/fs.h>
#include <linux/slab.h> // kmalloc, kfree 声明, 为设备变量分配缓存
#include <asm/uaccess.h> // copy_to_user , copy_from_user 声明, 数据copy
#include "gpio.h"
#define GPIO_SIZE 0x4 // 4字节做为gpio 缓存, 你可以定义的更大一些。
#define GPIO_MAJOR 254 /*预设的gpio的主设备号*/
// IOCTL 命令定义
#define ALL_MEM_CLEAR 0x1 /*清0全部内存*/
#define SET_MEM_ADDR 0x2 // 设置操作的gpio 地址
#define WRITE_DATA 0x3 // 写io 端口
#define READ_DATA 0x4 // 读io 端口
/*gpio设备结构体, 定义自己使用的变量,并要包含一个cdev 成员,与系统字符设备接口*/
struct gpio_dev
{
struct cdev cdev; /*cdev结构体*/
unsigned char mem[GPIO_SIZE]; /*全局内存*/
int addr; // gpio 地址, 操作哪一个gpio
};
// 全局变量定义
static int gpio_major = GPIO_MAJOR; // 保留申请的主设备号
struct gpio_dev *gpio_devp; /*设备结构体指针*/
/*文件打开函数, 将gpio_devp 传递给file 结构的私有数据*/
int gpio_open(struct inode *inode, struct file *filp)
{
/*将设备结构体指针赋值给文件私有数据指针*/
filp->private_data = gpio_devp;
return 0;
}
/*文件释放函数*/
int gpio_release(struct inode *inode, struct file *filp)
{
return 0;
}
// ioctl设备控制函数
// 对于简单的gpio. 也许ioctl就已经足够了,而不许要read,write 接口了。
// 这里为了完整,仍然写了read, write,等,完成批量内存操作
long gpio_ioctl(struct file *filp, unsigned
int cmd, unsigned long arg)
{
struct gpio_dev *pDev = filp->private_data;/*获得设备结构体指针*/
switch (cmd)
{
case ALL_MEM_CLEAR:
memset(pDev->mem, 0, GPIO_SIZE);
printk(KERN_INFO "all gpio is set to zero\n");
break;
case SET_MEM_ADDR:
pDev->addr = arg;
printk(KERN_INFO "addr is %d\n",pDev->addr);
break;
case WRITE_DATA:
pDev->mem[pDev->addr]=arg;
GPIOSetData(pDev->addr, arg);
printk(KERN_INFO "Data Write: %d\n",(int)arg);
break;
case READ_DATA:
arg=pDev->mem[pDev->addr];
printk(KERN_INFO "Data Read: %d\n",(int)arg);
break;
default:
return - EINVAL;
}
return 0;
}
//读函数, 可以一次读多个gpio 的数值,似乎有些多余,但体现read 的能力
static ssize_t gpio_read(struct file *filp, char __user *buf, size_t size,
loff_t *ppos)
{
unsigned long offset = *ppos;
unsigned int count = size;
int ret = 0;
struct gpio_dev *pDev = filp->private_data; /*获得设备结构体指针*/
printk("need size:%ld, offset:%ld\n",size,offset);
/*分析和获取有效的写长度*/
if (offset > GPIO_SIZE)
{
return count ? - ENXIO: 0;
}
else if(offset == GPIO_SIZE)
{
return 0; // 防止测试cat /dev/gpio 时 文件尾出现错误提示
}
if (count > GPIO_SIZE - offset)
{
count = GPIO_SIZE - offset;
}
/*内核空间->用户空间*/
if (!copy_to_user(buf, (void*)(pDev->mem + offset), count))
{
*ppos += count;
printk(KERN_INFO "read %d bytes(s) from %ld addr\n", count, offset);
ret = count;
}
else
{
ret = - EFAULT;
}
return ret;
}
/*写函数*/
static ssize_t gpio_write(struct file *filp, const char __user *buf,
size_t size, loff_t *ppos)
{
unsigned long offset = *ppos;
unsigned int count = size;
int ret = 0;
int i;
struct gpio_dev *pDev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (offset >= GPIO_SIZE)
{
return count ? - ENXIO: 0;
}
if (count > GPIO_SIZE - offset)
{
count = GPIO_SIZE - offset;
}
/*用户空间->内核空间*/
if (!copy_from_user(pDev->mem + offset, buf, count))
{
*ppos += count;
for(i=0; i< count; i++)
{
GPIOSetData(offset+i, pDev->mem[offset+i]);
}
printk(KERN_INFO "written %d bytes(s) to %ld addr\n", count, offset);
ret = count;
}
else
{
ret = - EFAULT;
}
return ret;
}
/* seek文件定位函数 */
static loff_t gpio_llseek(struct file *filp, loff_t offset, int orig)
{
loff_t ret = 0;
switch (orig)
{
case 0: /*相对文件开始位置偏移*/
if (offset < 0)
{
ret = - EINVAL;
break;
}
if ((unsigned int)offset > GPIO_SIZE)
{
ret = - EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /*相对文件当前位置偏移*/
if ((filp->f_pos + offset) > GPIO_SIZE)
{
ret = - EINVAL;
break;
}
if ((filp->f_pos + offset) < 0)
{
ret = - EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;
}
/*文件操作结构体*/
static const struct file_operations gpio_fops =
{
.owner = THIS_MODULE,
.llseek = gpio_llseek,
.read = gpio_read,
.write = gpio_write,
.compat_ioctl = gpio_ioctl,
.open = gpio_open,
.release = gpio_release,
};
/*向系统注册设备*/
static void gpio_setup_cdev(struct gpio_dev *pDev, int index)
{
int err, devno = MKDEV(gpio_major, index);
cdev_init(&pDev->cdev, &gpio_fops);
pDev->cdev.owner = THIS_MODULE;
pDev->cdev.ops = &gpio_fops;
err = cdev_add(&pDev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding CDEV%d", err, index);
}
/*模块加载函数*/
int gpio_init(void)
{
int result = -1;
dev_t devno = MKDEV(gpio_major, 0);
/* 申请设备号*/
if (gpio_major)
{
result = register_chrdev_region(devno, 1, "gpio");
}
if (result < 0) // 设备号已被占用等
{
/* 动态申请设备号 */
result = alloc_chrdev_region(&devno, 0, 1, "gpio");
gpio_major = MAJOR(devno);
}
if (result < 0)
{
printk("gpio module register devno failed!, result:%d\n",result);
return result;
}
/* 动态申请设备结构体的内存*/
gpio_devp = kmalloc(sizeof(struct gpio_dev), GFP_KERNEL);
if (!gpio_devp) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(gpio_devp, 0, sizeof(struct gpio_dev));
gpio_setup_cdev(gpio_devp, 0);
// 调用硬件层初始化
GPIOInit(NULL, GPIO_SIZE);
printk("gpio module installed!\n");
return 0;
fail_malloc: unregister_chrdev_region(devno, 1);
return result;
}
/*模块卸载函数*/
void gpio_exit(void)
{
if(gpio_devp)
{
cdev_del(&gpio_devp->cdev); /*注销cdev*/
kfree(gpio_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(gpio_major, 0), 1); /*释放设备号*/
}
gpio_devp = 0;
printk(KERN_INFO "gpio module released!\n");
}
module_init(gpio_init);
module_exit(gpio_exit);
MODULE_AUTHOR("HJJDEBUG");
MODULE_LICENSE("GPL");
--------------------------------------------------------------------------------
// 真正的驱动,虚拟
#include <linux/kernel.h>
#include "gpio.h"
/***************************************************
* 这里是个虚拟的驱动, 所有的硬件寄存器操作全部忽略。
***************************************************/
int GPIOInit(int *pAddr, int size)
{
printk("all gpio has inited ok!\n");
return 0;
}
int GPIOSetData(int addr, int data)
{
printk("gpio addr:%d, data:%d\n", addr, data);
return 0;
}
--------------------------------------------------------------------------------
Makefile:
ifneq ($(KERNELRELEASE),)
# obj-m := test.oobj-m := m_gpio.o
m_gpio-y:= gpioadaptor.o gpio.o
else
PWD=$(shell pwd)
KVER=$(shell uname -r)
KDIR=/lib/modules/$(KVER)/build
all:
make -C $(KDIR) M=$(PWD)
clean:
rm *.o *.ko modules.* Module.symvers *.mod.c
endif
--------------------------------------------------------------------------------
补充一个字符设备测试程序,注意open 的模式, 如果写成0(RD_ONLY), 写两个字符会出错。错误号为9, Bad file descriptor. 但写一个字符还是可以的
[root@hjj]# cat test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
char *data="ab";
int main(int argc, char *argv[])
{
int fd = open("/dev/udp",O_WRONLY); // 注意文件打开模式
if(fd== -1)
{
printf("error open device.\n");
exit(1);
}
printf("fd:%d\n",fd);
ssize_t size=write(fd,data,strlen(data));
if(size==-1)
{
printf("errno:%d string:%s\n",errno,strerror(errno));
perror("reason:");
}
else
{
printf("size:%d bytes write\n",size);
}
close(fd);
return 0;
}
一个驱动可以被多次打开,会返回不同的fd, 不同的fd, 会对应不同的filp.从而可以存储各自的数据
这样依据fd, 就可以操作不同的数据。
补充:
也可以自动创建创建设备节点,省去了手工创建设备节点的过程,具体代码请搜索互联网