在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
我们有了字符设备驱动框架的基础,写一个LED驱动是非常简单的,因为就是将裸机程序(单片机程序)填充到框架中去,只不过在这个过程中需要用到一些特殊的内核接口,仅此而已。当然本篇也只是重点介绍怎么用这些内核接口,至于接口的实现原理则需要有心的同学自己挖掘了,挖着挖着就挖到了操作系统原理和内核实现,所以,深度还是要自己看需要把控,并不是越深越好,尤其是前期,挖太深了容易迷路或者劝退, 深度和广度一定要匹配才能走得更远 。
1 LED驱动程序
由于代码量比较小,我就先把完整代码贴在这里,对LED驱动有一个整体的认识,后续再针对重点分小节娓娓道来。
LED驱动最大的改动就是在前面字符驱动框架的基础上新增了一个 led_write() 函数,在 cdriver_open() 和 cdriver_release() 函数中添加了一些初始化配置和去配置的代码。
/* 点亮一个LED的Linux驱动代码;
板子上的一个LED接在GPF的5引脚上 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> /*ioremap()*/
#include <linux/uaccess.h> /*copy_from_user()*/
#define GPFCON_ADD_BASE 0x56000050 /*定义GPIO F端口控制寄存器的物理地址基址*/
/* 定义指向gpf control和data寄存器的指针,
注意硬件映射的寄存器内数据是易变的,因此
要使用volatile进行修饰,防止编译器错误优化 */
volatile unsigned int *pgpfcon = NULL;
volatile unsigned int *pgpfdata = NULL;
int cdriver_open(struct inode *inode, struct file *file)
{
/* 拿到GPIO F端口的物理地址映射出来的虚拟地址 */
pgpfcon = ioremap(GPFCON_ADD_BASE, sizeof(int)*2);
if (pgpfcon == NULL)
return -ENOMEM;
pgpfdata = pgpfcon + 1;
/* 配置GPIO相关寄存器 */
*pgpfcon &= ~(0x3 << 10); /*将GPF5设置为输出引脚*/
*pgpfcon |= 0x1 << 10;
*pgpfdata |= 0x1 << 5; /*将LED的初始状态设置为关闭*/
return 0;
}
int cdriver_release(struct inode *inode, struct file *file)
{
/* 关闭设备节点时,取消io映射,释放虚拟地址资源 */
iounmap(pgpfcon);
return 0;
}
ssize_t led_write(struct file *file, const char __user *user_buff, size_t len, loff_t * offset)
{
int cmd_code = 0xff;
/* 入参检查,防止内存越界访问 */
if (len != sizeof(int))
return -EINVAL;
/* 将用户空间数据拷贝到内核空间 */
if (copy_from_user(&cmd_code, user_buff, len))
return -EFAULT;
printk("cmd_code is %#x \n", cmd_code); /*调试代码*/
/* 根据cmd_code操作LED灯 */
if (1 == cmd_code)
*pgpfdata &= ~(0x1 << 5);
else if (0 == cmd_code)
*pgpfdata |= 0x1 << 5;
else ;
return 0;
}
struct file_operations cdriver_fops = {
.owner = THIS_MODULE,
.open = cdriver_open,
.write = led_write, /* 将led写函数实现赋值给指针接口 */
.release = cdriver_release,
};
int __init cdriver_init(void)
{
register_chrdev(110, "cdriver", &cdriver_fops);
return 0;
}
void __exit cdriver_exit(void)
{
unregister_chrdev(110, "cdriver");
}
module_init(cdriver_init);
module_exit(cdriver_exit);
MODULE_LICENSE("GPL");
接下来,我们针对与裸机驱动的不同点进行着重说明。
1.1 访问I/O
在操作系统下开发驱动程序与裸机驱动的最大区别就是对地址的操作不同了:在裸机驱动中是直接操作物理地址,但是系统下的驱动需要操作虚拟地址。因为Linux启动时已经开启了MMU,此时CPU只认识虚拟地址,如果还使用物理地址会产生段错误。
明白了这点,只需要多加一个步骤就可以了,就是调用内核提供的 ioremap() 接口,将I/O物理地址转换为虚拟地址。该函数使用起来也很简单,第一个入参是物理地址,第二个参数是映射的长度(用多少就映射多少,不要过长,因为地址资源也是有限的),返回值就是映射(分配)的虚拟地址基址。
int cdriver_open(struct inode *inode, struct file *file)
{
/* 拿到GPIO F端口的物理地址映射出来的虚拟地址 */
pgpfcon = ioremap(GPFCON_ADD_BASE, sizeof(int)*2);
if (pgpfcon == NULL)
return -ENOMEM;
/* 对于任何地址,获得了基址都可以通过指针方便访问,只不过要注意千万不要越界访问*/
pgpfdata = pgpfcon + 1;
...
return 0;
}
下面简单说下ioremap到底做了哪些工作,有兴趣和精力的同学可以自己深入,最终会涉及到内存管理相关的知识,深度自己掌控。
CPU能够看到的地址范围就是存储器域和I/O域地址空间了。有些处理器架构的存储器和I/O是统一编址,比如arm架构;而有些处理器架构两者并不是统一编址,比如x86架构。但无论何种架构,为了方便地支持进程机制和安全管理,都会使用MMU和页表机制。在内核启动阶段内存地页表已经建立了,建立过程可以参照博客https://albert-genius.blog.csdn.net/article/details/108957846中的说明,而I/O外设的物理地址还没有分配对应的虚拟地址,因此需要我们自己调用ioremap()函数来进行映射和分配。
而ioremap()的主要工作就是检查传入的物理地址的合法性;建立页表(包括访问权限)以完成物理地址与虚拟地址的映射关系。
1.2 用户空间和内核空间交换数据
另外,在系统下编写驱动和裸机驱动的最大不同是:裸机只有一个空间,所以所有内存都是共享的,但是在系统上编程就要区分内核空间和用户空间,内存访问存在一定限制,因此也要借助专用的接口( copy_to_user() / copy_from_user() )来进行两个空间内数据的搬移工作。
Tips:mmap()可以将io物理地址直接映射到用户空间,而ioremap()只能将io物理地址映射到内核空间,因此需要数据搬移工作。
中规中矩的使用方式就像下述代码一样。
ssize_t led_write(struct file *file, const char __user *user_buff, size_t len, loff_t * offset)
{
int cmd_code = 0xff;
/* 入参检查,防止内存越界访问 */
if (len != sizeof(int))
return -EINVAL;
/* 将用户空间数据拷贝到内核空间 */
if (copy_from_user(&cmd_code, user_buff, len))
return -EFAULT;
...
return 0;
}
但内核其实是可以直接访问用户空间的,为什么不省略掉copy_from_user()的调用呢?有如下两点原因。
- 该函数会验证用户空间地址的有效性,防止传入的地址超出用户空间地址范围。
- 在使用MMU的系统上,用户空间的内存在访问时可能还没有分配,此时会产生缺页异常,异常处理会及时分配这段内存。这些动作在用户态是自动完成的,但是在内核态编码就需要程序员自己处理这种情况,而恰恰copy_from_user()函数就承担了这部分工作。
2 应用程序
驱动程序完成了,我们需要写一个应用程序来验证驱动程序的功能。应用程序比较简单,如下所示。
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd = 0;
int val = 0xff;
/* 如果没有入参打印使用方法 */
if (argc != 2) {
printf("usage: <on | off> \n");
return -1;
}
/* 打开设备文件节点,获得文件句柄 */
fd = open("/dev/led_dev", O_RDWR);
if (fd < 0) {
printf("open /dev/led_dev failed!\n");
return -1;
}
/* 根据入参给命令码赋值 */
if (!strcmp(argv[1], "on"))
val = 1;
else
val = 0;
/* 将命令码写入设备节点 */
if (write(fd, &val, sizeof(val)) < 0) {
printf("write to file failed. \n");
perror("write");
}
(void)close(fd);
return 0;
}
3 实验验证
代码写好了,我们要上板(S3C2440)验证了~由于前几篇已经将这个过程图文展示过,在这里就不多费笔墨占用篇幅了,简单列一下步骤就收工了。
- 使用交叉编译工具链编译LED驱动程序和应用程序,分别生成led_dr.ko和led_elf文件。
- 使用insmod插入led_dr.ko模块。
- 使用mknod /dev/led_dev c 110 0 创建设备文件节点。
- 运行led_elf on / off 来点亮和关闭LED灯。
下一篇,我们介绍自动创建设备文件节点功能,并利用次设备号创建多个LED设备,在应用程序中实现一个流水灯~~
恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~