先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题,二维码如下:
本篇文章是为了记录学习韦东山老师的嵌入式linux教学视频的课程笔记,给大家一个购买韦东山老师视频的链接
关于linux操作系统,相信做过嵌入式开发的人或多或少都有所了解,都听说过它是一个优秀的,开源的os,在嵌入式设备行业、服务器行业,我们无处不见linux的身影,在这里想给大家纠正一个不太好的观念(可能是我自己以前一直有这样一个误解),很多嵌入式开发人员会认为windows系统真的很糟糕,完全比不上linux,其实不是这样的,windows没有我们认为的那么糟糕,linux也没有我们认为的那么完善,存在即合理,一款os能够存在并能够在某一行业广泛使用,是有它的道理的。
在开始介绍linux字符设备驱动之前,想给大家一个我认为比较有效学习linux的方法。
1. 先熟悉了解并使用linux,这里的使用不是说让大家做linux kernel的移植,而是先熟悉在这个环境下办公。比如说,现状一个ubuntu的虚拟机,然后在这个虚拟机下面写c代码,编译并执行可执行程序。
2. 熟悉在linux环境下的交叉编译,比如基于cortex-M系列的ARM芯片程序的开发,如何交叉编译生成arm芯片的可执行程序,在此给大家推荐电子发烧友上面的韦东山老师的嵌入式linux学习课程的第一部分(不是打广告,不是打广告,不是打广告),虽说我也是自动控制专业出生,但是之前完51,msp430,pic单片机的时候,没有太多关注芯片底层的东西,也没有从系统的角度去理解一款SoC芯片的产品设计开发过程,学完韦老师的课程之后,真的受益颇多,以前很多一知半解、模棱两可的东西突然就都明白了。
3. 熟悉linux系统环境的各种command line的命令,与windows图形界面为主的os所不同的是,linux下面的命令行命令极其丰富(可能是我孤陋寡闻,windows也许也很丰富)
4. 学习linux系统的真正底层部分,包括uboot移植,kernel移植,busybox制作,文件系统制作等等;
5. 修炼内功,和第4步一起阅读源码,掌握linux系统的线程、进程调度原理,同步方式,通信机制等等;
上面说了那么一大堆正确的废话,接下来进入我们本篇主题,字符驱动设备开发,引入我们今天的几个问题
为什么需要字符驱动设备程序?我自己写一个驱动不就好了吗?
越是庞大的系统,越涉及多层次的协作,如果上层应用层开发人员完全没有底层硬件驱动的概念,如果没有这些设备驱动程序,他们将完全不懂如何开发。
在linux系统下,有一句很经典的话,就是"一切皆文件",上层业务开发人员可以把所有需要访问的硬件资源,当成文件一样去操作,那么对于文件的操作一般都有:打开文件、关闭文件、读文件、写文件,所以linux驱动开发需要对这些硬件资源实现这些操作:打开操作、关闭操作、读操作、写操作,linux系统会帮我们封装好这些调用流程,所以这里不得不说linux系统的强大,调用流程大致如下所示
根据上面的分析,字符设备驱动中,我们需要实现的操作有:打开操作,读操作,写操作,关闭操作,这些已经是我们已经知道要做的准备了,那么是不是就只需要实现这些操作就可以了呢?我们还需要哪些准备工作,内核才能正确调用我们实现的这些操作呢?考虑如下几个问题
a. 怎么告诉内核,我们定义的这些操作函数?
b. 谁来调用?或者说驱动的入口在哪里?
请牢记这两个问题,下面我们先实现打开操作、读操作、写操作者三个函数的编写
打开操作,我们先设计好函数名以及函数主体部分,内部实现暂时不用管
static int first_drv_open(struct inode *inode, struct file *file)
{
return 0;
}
同样地,写操作
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
return 0;
}
以及读操作
static int first_drv_read(struct file *filp, char __user *buff,
size_t count, loff_t *offp)
{
return 0;
}
三个函数的主体框架部分设计完,很多人可能会有疑问,为什么这些函数都需要返回值?为什么这些函数的参数必须是这样,缘由是什么?
这两个问题,从宽泛的角度来说,因为linux系统本身设计时,调用这些字符设备驱动程序的时候,就决定了这些函数的格式,我们设计的函数必须满足linux系统的要求,必须如此定义。针对初学者,我建议大家可以考虑忽略这些,没必要花很多时间在这些细节上,我们前期最主要的目的是如何编写一个字符设备驱动程序。
ok,实现完这三个函数的主体部分,回到我们开始提到的两个问题:
a. 怎么告诉linux内核
定义一个linux系统提供的file_operations结构体,来告诉内核
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
.write = first_drv_write,
};
b. 驱动的入口在哪里
第一步 写一个入口初始化函数,我们这里的函数名为first_drv_init,在初始化字符设备的节点号的时候,我们采用了register_chrdev函数实现,这是一个比较老版本的实现函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。
另外两个申请字符设备节点号的函数分别是register_chrdev_region以及alloc_chrdev_region,区别请读者自行查找,这里不展开来介绍
int major;
static int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 内核自动帮我们分配设备号
/* 老版本使用的api为 class_create 和 class_device_create*/
/*
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
*/
/* 新版本使用的api为 class_create 和 device_create */
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");
return 0;
}
第二步 利用momdule_init修饰
module_init(first_drv_init);
根据以上过程,我们就搭建了一个字符设备驱动程序的框架,目前还只是一个框架,还不能实现任何功能,如果各位读者想要验证该框架的功能,可以在每个我们自己实现的操作函数中增加打印,看看上层在调用open,write,read函数时,是否调用了我们字符设备驱动文件中的first_drv_open、first_drv_write、first_drv_read函数。
在此,再次强调一遍,上面的函数操作中,实际上我们使用了很多系统内核的结构体数据结构定义,对于初学者没必要花太多时间在这些结构体定义上,我们先主要掌握如何使用这些结构体就行了,等我们后期熟悉linux系统之后,我们可以花时间去研究这些数据结构定义。
给一个完整的字符设备驱动的完整代码,第5步中的测试就是基于该驱动
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
static struct class *firstdrv_class;
static struct class_device *firstdrv_class_dev;
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
static int first_drv_open(struct inode *inode, struct file *file)
{
//printk("first_drv_open\n");
/* 配置GPF4,5,6为输出 */
*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
return 0;
}
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
//printk("first_drv_write\n");
/* 内核空间和用户空间数据传递 */
copy_from_user(&val, buf, count); // copy_to_user();
if (val == 1)
{
// 点灯
*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
}
else
{
// 灭灯
*gpfdat |= (1<<4) | (1<<5) | (1<<6);
}
return 0;
}
static struct file_operations first_drv_fops = {
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = first_drv_open,
.write = first_drv_write,
};
int major;
static int first_drv_init(void)
{
major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核
/* 老版本使用的api为 class_create 和 class_device_create*/
/*
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
*/
/* 新版本使用的api为 class_create 和 device_create */
firstdrv_class = class_create(THIS_MODULE, "firstdrv");
firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");
gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
gpfdat = gpfcon + 1;
return 0;
}
static void first_drv_exit(void)
{
unregister_chrdev(major, "first_drv"); // 卸载
/* 老版本使用的api
class_device_unregister(firstdrv_class_dev);
*/
/* 新版本使用的api */
device_unregister(firstdrv_class_dev);
class_destroy(firstdrv_class);
iounmap(gpfcon);
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");
对于软件开发来说,写完一段功能性代码,一定会手动验证一下代码功能,那么针对字符设备驱动的代码功能,我们应该如何验证呢?
#include
#include
#include
#include
/* firstdrvtest on
* firstdrvtest off
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/xyz", O_RDWR);
if (fd < 0)
{
printf("can't open!\n");
return 0;
}
if (argc != 2)
{
printf("Usage :\n");
printf("%s \n", argv[0]);
return 0;
}
if (strcmp(argv[1], "on") == 0)
{
val = 1;
}
else
{
val = 0;
}
write(fd, &val, 4);
return 0;
}
KERN_DIR = /work/system/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += first_drv.o
arm-linux-gcc -o firstdrvtest firstdrvtest.c
cp firstdrvtest /work/nfs_root/first_fs
cp first_drv.ko /work/nfs_root/first_fs
内容如下:
6. 启动设备,查看当前设备号信息
因为我们设置的nfs启动方式,设备启动之后,会将服务器上的内容拷贝到设备本地,我们可以通过命令cat /proc/device查看当前设备信息
7. 加载动态库,忽略警告信息
insmod first_drv.ko
Too be continued