一、驱动框架编写
1.编写驱动文件
打开linux内核,全局搜索module_init函数。
linux内核中已经有了很多厂家写好的驱动模块,所以我们完全可以参考他们的代码。
#include <linux/module.h>
static int __init chrdevbase_init(void)
{
}
static void __exit chrdevbase_exit(void)
{
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
如果头文件下面显示红色波浪线,说明头文件未找到。参考第一节内核路径的配置
2.编写makefile文件
KERNELDIR := /home/zys/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
kernel_modules代表具体的编译指令,后面的modules代表编译的是模块
-C表示将当前的工作路径切换到指定的目录中,也就是KERNELDIR目录
M表示模块源码目录,也就是CURRENTDIR
make modules命令中加入M=dir以后程序会自动跳到指定的dir目录中读取模块源码并将其编译为.ko文件
makefile编写好以后,直接在控制台输入make命令进行编译。
编译成功后,会生成chrdevbase.ko文件
3.运行测试
准备开发板,我用的是正点原子的imx6ull开发板。
首先烧录uboot,先编译uboot
编译完成,烧写u-boot.bin到sd卡中。
sudo fdisk -l //查看sd卡是哪个设备 /dev/sdd
./imxdownload u-boot.bin /dev/sdd //烧写bin文件到sdd设备中
烧写完成把sd卡插到开发板上,sd卡启动。
开发板进入uboot之后呢,需要从ubuntu上下载zImage和dtb设备树,通过tftp的方式。
看一下ubuntu中tftp的资源文件夹
如何让开发板去ubuntu下载指定文件?
首先设置uboot的环境变量,setenv bootcmd
bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-nand.dtb;bootz 80800000 - 83000000
saveenv bootcmd
然后设置bootargs,这里挂载的是网络根文件系统。
bootargs=console=ttymxc0,115200 rw root=/dev/nfs nfsroot=192.168.2.183:/home/zys/linux/nfs/rootfs ip=192.168.2.157:192.168.2.183:192.168.2.1:255.255.255.0::eth0:off
saveenv bootargs
console=ttymxc0是设置终端,这里是串口,
rw是根文件系统可以读也可以写
nfsroot=ubuntu的ip地址 : nfs的路径
ip=本机ip :服务器ip :网关 : 掩码
bootcmd和bootargs命令设置好以后,输入boot
就会自动调用bootcmd命令,从ubuntu下载系统镜像和设备树
根文件系统加载成功。
MobaXterm如何复制?选中就是复制 右键粘贴
3.linux驱动编写的注意事项。
- 编译驱动的时候需要用到linux源码,所以内核源码必须放到在ubuntu中,然后编译内核源码,生成zImage和dtb,设备启动的时候需要从这里下载。
二、加载驱动文件
使用命令 imsmod 或者 modprobe(可以解决依赖)
modprobe命令默认会到 lib/modules/4.1.15 路径下找文件
如果第一次使用modprobe 提示没有这个路径,创建一个即可。
把刚刚编译出来的chrdevbase.ko放到 lib/modules/4.1.15下面
sudo cp chrdevbase.ko /home/zys/linux/nfs/rootfs/lib/modules/4.1.15/ -f
-f 表示替换
modprobe chrdevbase.ko
如果第一次加载模块,需要执行一下depmod命令。提示缺少xx文件,也是需要执行一次这个命令。
查看已经加载了的模块
lsmod
三、卸载驱动
使用命令
rmmod
四、完善驱动
1.设备号的组成
linux中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成。
主设备号表示某一个具体的驱动。
次设备号表示使用这个驱动的各个设备。
linux提供了一个名为dev_t的数据类型来表示设备号,dev_t定义在include/linux/types.h中
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
可以看出dev_t是__u32类型,__u32定义在 include/uapi/asm-generic/int-ll64.h中
typedef unsigned int __u32;
其实dev_t就是unsigned int,无符号32位整形。
其中高12位为主设备号,低20位为次设备号。
因此linux系统中主设备号的范围是0-4095,所以在注册设备号的时候不要超过这个范围。
在头文件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)) //组装设备号 直接赋值给dev_t类型的变量即可
2.设备号的分配
2.1 静态分配设备号
在注册字符设备驱动的时候,我们需要指定一个设备号,比如200。linux内核中已经存在了一些设备号,我们就不能再使用了,所以在注册设备号之前应该看一下此设备号有没有被使用
cat /proc/devices
2.2 动态分配设备号
通过申请设备号来获得,使用如下函数
//参数:
//dev:保存申请到的设备号
//baseminor:次设备号的起始地址,一般为0
//count:申请的设备号的数量,可以连续申请多个,主设备号一样,次设备号不同
//name:设备的名字
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name)
注销设备号,注销字符设备之后必须释放掉之前申请的设备号
//from:要释放的设备号
//count:从from开始,要释放的设备号数量
void unregister_chrdev_region(dev_t from,unsigned count)
3. 重新编写驱动
从头来一遍,驱动要多写,形成肌肉记忆,才算掌握!
3.1 创建文件夹
3.2 把之前的.vscode文件夹拷贝进来
3.3
//#include <linux/init.h>
//#include <linux/types.h> //dev_t
//#include <linux/kernel.h>
//#include <linux/delay.h>
//#include <linux/kdev_t.h> //设备号头文件
//#include <linux/kern_levels.h> //printk 消息级别
#include <linux/printk.h> //printk
#include <linux/ide.h>
#include <linux/module.h>
/********************************************************************
* 文件名:chrdevbase.c
* 作者:张亚胜
* 版本:V1.0
* 描述:chrdevbase驱动文件
* 其他:无
* 网站:www.s123.xyz
* 日志:出版V1.0 2020/7/26 张亚胜创建
* ******************************************************************/
#define CHRDEVBASE_MAJOR 200 /*主设备号*/
#define CHRDEVBASE_NAME "chrdevbase" /*设备名*/
static char readbuf[100]; /*读缓冲区*/
static char writebuf[100]; /*写缓冲区*/
static char kerneldata[] = {"kernal data"}; /*测试数据*/
/*
* @description : 打开设备
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrtest_open(struct inode *inode, struct file *filp)
{
//printk("chrtest_open\r\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param – filp : 设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retval = 0;
memset(readbuf,0,sizeof(readbuf));
memcpy(readbuf,kerneldata,sizeof(kerneldata));
retval = copy_to_user(buf,readbuf,cnt);
if(retval == 0){
printk("kernal read ok\r\n");
}
else
{
printk("kernal read err!\r\n");
return retval;
}
return cnt;
}
/*
* @description : 向设备写入数据
* @param - filp : 设备文件(文件描述符)
* @param - buf : 要写入设备的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retval = 0;
retval = copy_from_user(writebuf,buf,cnt);
if(retval == 0){
printk("write kernal data is %s\r\n",writebuf);
}
else{
printk("write kernal data is err!\r\n");
return retval;
}
return cnt;
}
/*
* @description : 关闭设备
* @param – inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrtest_release(struct inode *inode, struct file *filp)
{
//printk("chrtest_release\r\n");//屏蔽掉 不然容易和app数据发生踩踏
return 0;
}
/* 设备操作函数结构体 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.release = chrtest_release,
.read = chrtest_read,
.write = chrtest_write,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retval = 0;
retval = register_chrdev(CHRDEVBASE_MAJOR,CHRDEVBASE_NAME,&test_fops);
if(retval < 0)
{
printk("%s register err!\r\n",CHRDEVBASE_NAME);
return retval;
}
printk("chrdevbase_init\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
unregister_chrdev(200,CHRDEVBASE_NAME);
}
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZYS");
4.编写测试app前准备
测试应用程序就是编写linux应用,需要用到c库里面和文件相关的函数。
以下函数可以在ubuntu中通过 “man 2 open” 命令查看详细内容
4.1 open函数
//参数:
//pathname:要打开的设备名
//flags:文件打开模式,O_RDONLY,O_WRONLY,O_RDWR,只读,只写,读写模式,还有很多模式,可以使用逻辑或选择多种
//返回值:打开成功返回文件描述符(非负整数)
int open(const char *pathname,int flags)
----------------------------扩展-----------------------------
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行。
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
4.2 read函数
//参数:
//fd:要读取的文件描述符
//buf:读取的数据保存位置
//count:读取数据的长度 字节数
//返回值:读取成功 返回读到的字节数;如果返回0 表示读到了文件末尾;如果返回负数 读取失败
ssize_t read(int fd,void *buf,size_t count)
4.3 write函数
//参数:
//fd:文件描述符
//buf:将要写入的数据
//count:写入数据的长度
//返回值:写入成功返回写入的字节数;返回0表示没有写入数据;返回负数 写入失败
ssize_t write(int fd,const void *buf,size_t count)
4.4 close函数
//参数:
//fd:文件描述符
//返回0成功 负值失败
int close(int fd)
5.编写测试app
先创建文件chrdevbaseApp.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
/********************************************************************
* 文件名:chrdevbaseApp.c
* 作者:张亚胜
* 版本:V1.0
* 描述:chrdevbaseApp测试文件
* 其他:使用方法:./chrdevbaseApp /dev/chrdevbase 1 //读取数据
* ./chrdevbaseApp /dev/chrdevbase 2 //写入数据
* 网站:www.s123.xyz
* 日志:出版V1.0 2020/7/26 张亚胜创建
* ******************************************************************/
/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd,retval;
char *filename;
char readbuf[100],writebuf[]={"I am test App!"};
if(argc != 3){
printf("params err!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename,O_RDWR);
if(fd<0){
printf("open err!\r\n");
return -1;
}
if(atoi(argv[2]) == 1)
{
memset(readbuf,0,100);
if(read(fd,readbuf,11) < 0)
{
printf("read data err!\r\n");
}
else
{
printf("read data:%s\r\n",readbuf);
}
}
else if(atoi(argv[2]) == 2)
{
if(write(fd,writebuf,sizeof(writebuf)) < 0){
printf("write err!\r\n");
}
else{
printf("write ok!\r\n");
}
}
else
{
printf("commod err!\r\n");
}
retval = close(fd);
return retval;
}
编译出chrdevbaseApp
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
6.测试
把chrdevbase.ko 和 chrdevbaseApp拷贝到文件系统目录中
sudo cp chrdevbase.ko chrdevbaseApp /home/zys/linux/nfs/rootfs/lib/modules/4.1.15/ -f
开发板挂载驱动
modprobe chrdevbase.ko
创建文件节点
//c代字符设备
//200 主设备号 0 次设备号
mknod /dev/chrdevbase c 200 0
测试读数据
/lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase 1
kernal read ok
read data:kernal data
测试写
/lib/modules/4.1.15 # ./chrdevbaseApp /dev/chrdevbase 2
write kernal data is I am test App!
write ok!
7.问题
1.测试读取数据的时候,打印的数据有覆盖现象
导致的踩踏 和 乱序现象 ,怎么解决呢? 调试好驱动以后,屏蔽掉驱动中的printk。