参考资料:
[1] 【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.71
[2] C语言中结构体成员变量前的点的作用iyC语言中结构体成员变量前的点的作用_结构体成员前面加点_Lei W.的博客-CSDN博客
1 基础知识
1.1 字符设备介绍
字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
1.2 Linux中一切皆文件
在Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx 是具体的驱动文件名字)的文件进行相应的操作,即可实现对硬件的操作。
1.3 用户空间与内核空间
应用程序运行在用户空间;Linux驱动属于内核的一部分,因此驱动运行于内核空间。另外,用户空间不能直接对内核进行操作。
1.4 设备号及其分配方式
Linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
设备号的类型是32位的无符号整型数据,其中高12位为主设备号, 低 20位为次设备号。在文
件 include/linux/kdev_t.h中提供了几个关于设备号的操作函数 (本质是宏 )
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号的分配方式分为静态分配和动态分配。在静态分配之前,需要先使用下面的命令查看当前系统中所有以经使用的设备号,防止冲突。
cat /proc/devices
下图是使用上述命令的示例。
动态分配时分别在设备注册之前和注销之后使用以下函数进行设备好的申请与释放。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
void unregister_chrdev_region(dev_t from, unsigned count)
1.5 注意事项
- 应用程序使用到的函数在具体驱动程序中要有与之对应的函数;
- 驱动基于框架开发,其中字符驱动的开发是对file_operations的成员变量的实现;在include/linux/fs.h中file_operations结构体表示Linux内核字符驱动操作函数集合;
- 编译驱动时需要使用内核源码、zImage、dtb等;另外,方便调试,将驱动编译成模块。
2 字符设备开发流程
2.1 模块的加载与卸载及其测试
模块的加载与卸载
模块的加载与卸载的程序模板如下代码所示。
#include <linux/module.h>
// Loading and unloading of modules
// 模块加载函数
static int __init xxx_init(void)
{
return 0;
}
// 模块卸载函数
static void __exit xxx_exit(void)
{
}
module_init(xxx_init); // 用于注册模块加载函数
module_exit(xxx_exit); // 用于注册模块卸载函数
module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用insmod命令加载驱动的时候xxx_init这个函数就会被调用。module_exit函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用rmmod命令卸载具体驱动的时候xxx_exit函数就会被调用。
测试
模块加载与卸载的完整代码如下所示。
#include <linux/module.h>
#include <linux/printk.h>
// Loading and unloading of modules
static int __init chardrvbase_init(void)
{
printk("chardrvbase_init\r\n");
return 0;
}
static void __exit chardrvbase_exit(void)
{
printk("chardrvbase_exit\r\n");
}
module_init(chardrvbase_init);
module_exit(chardrvbase_exit);
MODULE_LICENSE("GPL");
Makefile代码如下所示。
KERNELDIR := /home/alientek/test/linux/nxp_kernal/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := chardrvbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
KERNELDIR表示内核代码的位置,根据自己的实际情况调整;obj-m表示编译成模块; -C表示将当前目录切换到指定目录,获取编译器内容及其他。
若模块的名称为chardrvbase.ko,则加载与卸载命令的使用方法如下所示。
modprobe chardrvbase.ko // 加载模块
rmmod chardrvbase.ko // 卸载模块
另外,加载指令有insmod和modprobe,卸载指令有rmmod和modprobe -r。但modprobe会分析模块的依赖关系,并且默认在/lib/modules/<kernel-version>目录中查找模块。一般情况下,使用modprobe加载模块,rmmod卸载模块。
使用modprobe加载一个新模块前,需要先执行depmod命令自动生成一些文件,否则加载失败。
对于 module license 'unspecified' taints kernel.解决方法如图所示。
2.2 字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模
块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示 :
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops);
static inline void unregister_chrdev(unsigned int major, const char *name);
- major:主设备号
- name:设备名字,指向一串字符串
- fops:结构体 file_operations类型指针,指向设备的操作函数集合变量
完整的驱动代码如下所示。
#include <linux/module.h>
#include <linux/printk.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define CHARDRVBASE_MAJOR 200
#define CHARDRVBASE_NAME "chardrvbase"
char readbuf[100], writebuf[100];
const char kernel_data[] = "kernel";
static ssize_t chardrvbase_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
//printk("chardrvbase_read\r\n");
int ret = 0;
memcpy(readbuf, kernel_data, sizeof(kernel_data));
ret = copy_to_user(buf, readbuf, count);
if(ret != 0)
{
printk("kernel read failed\r\n");
}
return 0;
}
static ssize_t chardrvbase_write(struct file *file, const char __user *buf, size_t count,
loff_t *ppos)
{
//printk("chardrvbase_write\r\n");
int ret = 0;
ret = copy_from_user(writebuf, buf, count);
if(ret != 0)
{
printk("kernel write failed\r\n");
}
else
{
printk("kernel recevice data:%s\r\n", writebuf);
}
return 0;
}
static int chardrvbase_open(struct inode *inode, struct file *file)
{
//printk("chardrvbase_open\r\n");
return 0;
}
static int chardrvbase_release(struct inode *inode, struct file *file)
{
//printk("chardrvbase_release\r\n");
return 0;
}
static const struct file_operations chardrvbase_fops = {
.owner = THIS_MODULE,
.read = chardrvbase_read,
.write = chardrvbase_write,
.open = chardrvbase_open,
.release = chardrvbase_release,
};
// Loading and unloading of modules
static int __init chardrvbase_init(void)
{
int flag = 0;
printk("chardrvbase_init\r\n");
flag = register_chrdev(CHARDRVBASE_MAJOR, CHARDRVBASE_NAME, &chardrvbase_fops);
if(flag < 0)
{
printk("register_chrdev fall!\r\n");
}
return 0;
}
static void __exit chardrvbase_exit(void)
{
printk("chardrvbase_exit\r\n");
unregister_chrdev(CHARDRVBASE_MAJOR, CHARDRVBASE_NAME);
}
module_init(chardrvbase_init);
module_exit(chardrvbase_exit);
MODULE_LICENSE("GPL");
2.3 创建设备节点
驱动加载成功需要在 /dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建:
mknod /dev/chardrvbse c 200 0
c表示是字符设备,200为主设备号(根据实际情况),0为次设备号
2.4 app测试程序
完整的app程序如下所示。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
// ./chardrvbaseAPP /dev/chardrvbase 1 read
// ./chardrvbaseAPP /dev/chardrvbase 2 write
const char app_data[] = "app";
int main(int argc, char *argv[])
{
int fd = 0, ret = 0;
char *filename = argv[1], writebuf[100];
char readbuf[100];
if(argc != 3)
{
printf("Error usage!\r\n");
return -1;
}
fd = open(filename, O_RDWR);
if(fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
else
{
}
if(atoi(argv[2]) == 1)
{
ret = read(fd, readbuf, 20);
if(ret < 0)
{
printf("read file %s failed\r\n", filename);
return -1;
}
else
{
printf("app receive data:%s\r\n", readbuf);
}
}
if(atoi(argv[2]) == 2)
{
memcpy(writebuf, app_data, sizeof(app_data));
ret = write(fd, writebuf, 20);
if(ret < 0)
{
printf("write file %s failed\r\n", filename);
return -1;
}
else
{
}
}
ret = close(fd);
if(ret < 0)
{
printf("close file %s failed\r\n", filename);
return -1;
}
else
{
}
return 0;
}
main函数的入口参数argc表示数组元素的个数,argv具体的内容。
因为代码需要运行在arm架构中,编写完成后使用如下命令编译。
arm-linux-gnueabihf-gcc chardrvbaseAPP.c -o chardrvbaseAPP