不知道有没有人和我一样,一开始对Linux驱动和Linux应用概念模糊,现在明确了。
Qt creator
比较多,类似于C#的Winform
最开始要明确的概念是用户态和内核态,即user和kernel。写了Linux驱动后需要用应用(App)来操作内核,App属于用户态,和内核是隔绝开的。
Linux中一切都是文件,驱动加载成功后会在/dev生成一个相应的文件,例如/dev/DCmotor,就是直流电机的文件。对该文件进行相应的操作即实现对硬件的操作。
使用open
, close
, write
, read
几个函数实现打开设备,关闭设备,向设备写数据实现操作和读取设备状态等功能。这些实现硬件操作的函数集合可以参考include/linux/fs.h
中的file_operation结构体。字符设备驱动开发中主要工作就是根据据具体驱动需求来实现这些函数。
在驱动编程时,最好的方式是遵守这种“框架”,即把xxx_open
, xxx_read
, xxx_write
, xxx_release
这几个函数都写进去,如果要实现具体功能,则编写相应代码,否则只需要return 0
即可。
以一个最简单的LED设备为例,首先建一个字符设备结构体:
struct led_dev{
dev_t devid; //设备号
struct cdev cdev; //cdev
struct class *class; //类
struct device *device; //设备
int major; //主设备号
int minor; //次设备号
} leddev;
设备open的时候将private_data
指向设备结构体
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &leddev;
return 0;
}
不需要读取led的状态,所以read
函数直接返回0即可
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
write
比较重要,就是通过这个来控制LED设备的暗灭
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt); //从用户态拷贝字符到内核态的系统函数
if(retvalue < 0) {
printk("kernel write failed!\r\n"); //注意这里不是printf
//Linux内核中的打印函数是printk,运行在内核态
//printk是可以设置显示级别
//高于某个级别的可以显示在控制台上。
return -EFAULT; //EFAULT是errno-base.h里面定义的错误编号
//这里不需要管具体是多少,只把它定义为负值即可
}
ledstat = databuf[0]; // 获取状态值
if(ledstat == LEDON) {
led_switch(LEDON);
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
/*
函数参数:
filp-设备文件名
buf-要写入设备的数据,__user修饰的指针表明这个指针指向的数据位于用户空间,不是内核空间
cnt-写入数据长度
offt-相对于文件首地址的偏移
函数返回值:
成功则返回0,返回其他值表示写入失败
*/
release
和read
一样,也return 0
即可。
下面这段必不可少,是把内核态和用户态对文件的操作链接起来
static struct file_operations leddev_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
之后要有驱动入口函数和驱动出口函数,即module_init(xxx_init)
和module_exit(xxx_exit)
。具体的实现函数是
static int __init led_init(void)
{
... // 其他的一些初始化部分
/*创建设备号操作*/
if(leddev.major){ //定义了设备号,即这个major不为0
leddev.devid = MKDEV(leddev.major, 0);
register_chrdev_region(...); //注册设备号内核函数
}
else{ //无设备号
alloc_chrdev_region(...); //申请设备号
leddev.major = MAJOR(leddev.devid);
leddev.minor = MINOR(leddev.devid); //获取分配得到的主、次设备号
}
}
static void __exit xxx_exit(void)
{
... //其他出口操作
cdev_del(&leddev.cdev);/* 删除cdev */
unregister_chrdev_region(leddev.devid, NEWCHRLED_CNT); /* 注销设备号 */
device_destroy(leddev.class, leddev.devid);
class_destroy(leddev.class);
}
module_init(xxx_init);
module_exit(xxx_exit); //这两句话不能少
Linux驱动有两种运行方式,一是将驱动编译进Linux Kernel里面,Kernel启动后会自动运行该驱动,二是将驱动编译成模块(.ko文件
),用方法二比较多。具体流程:
modprobe xxx.ko
,通过lsmod
查看是否注册成功模块,cat /proc/devices
查看系统中设备号和设备;ls /dev/xxx -l
命令查看申请到的主设备号和次设备号(不是数字的1,是字母的l);rmmod xxx.ko
,卸载驱动后在设备里面就找不到xxx了。可以通过编写基于命令行的应用来测试驱动是否工作正常,这里就不赘述了。
接下去要学习Linux设备树
(未完待续)