- 前言
- 环境搭建
- CLion 远程服务配置
- 创建项目
- 修改CMakeLists.txt文件
- 应用程序与驱动的关系
- cdev与设备结构体的关系
- 项目源码
- 驱动代码
- 应用程序
- Makefile
- 开发板测试
- Ubuntu测试
- Tips
- Git下载
前言
这里采用的模拟方法是在内核空间开辟一段内存空间来模拟一个字符设备,其功能就是可以对这块内存空间实现读写的功能,和mmap的功能和原理相似,只是实现方法不同。
环境搭建这里使用的工具是CLion进行编写代码,在虚拟机上进行编译身成.ko文件。因为只是一个模拟嘛,入门。不需要配置设备树啥的,所以也就不在开发板上进行操作了。(操作相同)
CLion 远程服务配置推荐------------B站UP主海牛Rocky
创建项目
cmake_minimum_required(VERSION 3.10)
project(Driver_Work C)
set(ROOT_DIR "/home/jacky/100ask_stm32mp157_pro-sdk/Linux-5.4")
#头文件目录位置
include_directories(
"${ROOT_DIR}/include"
"${ROOT_DIR}/arch/arm/include"
)
# 控制程序走向,目的就是为了不破坏内核源码,又可以添加我们自己的功能
add_definitions(-D__KERNEL__)
add_definitions(-D__GNUC__)
add_definitions(-DMODULE)
#设置语言标准,我这里没加-stdnolib,因为我这个CLion好像有bug,加了-stdnolib所有的头文件都无法使用
#而这个-stdnolib的目的只是取消掉标准C库的头文件
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 ")
add_executable(Driver_Work main.c)
看到CLion打印这个就成功了,直接在工程目录中创建一个.c文件即可
基类与派生类的关系
下面的代码解释都放在注释中,尽可能地将语言变得通俗易懂
驱动代码globalmem_chardev.c
#include应用程序#include #include #include //cdev结构体头文件 #include //互斥体头函数 #include //kzalloc函数头文件 #include //copy_from_user函数头文件 #include //EINVL等出错宏的声明 #include //_IO()宏的头文件 #include //内核中的memset函数 #include #define MEM_CLR _IO('w',2) //关于此宏详情见globalmem_ioctl函数 //定义主设备号 static int major=0; module_param(major,int,0); //描述设备 struct globalmem_chardev{ struct cdev dev_cdev; unsigned char mem_data[1024]; struct mutex mem_mutex; }; struct globalmem_chardev* global_chardev; static int globalmem_open(struct inode* node,struct file* fp){ struct globalmem_chardev* dev; dev=container_of(node->i_cdev,struct globalmem_chardev,dev_cdev); //绑定文件描述符和索引节点对象 fp->private_data=dev; return 0; } static int globalmem_release(struct inode* node,struct file* fp){ return 0; } static ssize_t globalmem_wirte(struct file* fp,const char __user *data,size_t len,loff_t *pos){ //通过file结构体获得设备地址 struct globalmem_chardev* dev=fp->private_data; unsigned long p=*pos; size_t ret;//接收返回值 if(p>=1024){//越界了 return 0; } if(p+len>1024){ len=1024-p; } //上锁 mutex_lock(&dev->mem_mutex); //将用户空间的内容映射到内核空间 if(copy_from_user(dev->mem_data+p,data,len)){ ret=-EFAULT; }else{ *pos+=len;//改变文件指针的位置 ret=len; } //开锁 mutex_unlock(&dev->mem_mutex); return ret; } static ssize_t globalmem_read(struct file* fp,char __user *data,size_t len,loff_t *pos){ struct globalmem_chardev* dev=fp->private_data; unsigned long p=*pos; ssize_t ret; if(p>1024){//越界 return 0; } if(p+len>1024){ len=1024-p; } //上锁 mutex_lock(&dev->mem_mutex); if(copy_to_user(data,dev->mem_data+p,len)){ ret=-EFAULT; }else{ *pos+=len; ret=len; } //开锁 mutex_unlock(&dev->mem_mutex); return ret; } static loff_t globalmem_lseek(struct file* fp,loff_t offset,int whence){ int ret;//接收返回值 switch(whence){ case SEEK_SET://表示开头位置 if((offset<0) || (offset>1024)){ ret=-EINVAL;//无效参数的意思 break; } fp->f_pos=offset; ret=fp->f_pos; break; case SEEK_CUR://表示文件指针当前位置 if((fp->f_pos+offset<0) || (fp->f_pos+offset>1024)){ ret=-EINVAL; break; } fp->f_pos+=offset; ret=fp->f_pos; break; case SEEK_END://表示文件末尾 if(offset>=0){ ret=-EINVAL; break; } fp->f_pos-=offset; ret=fp->f_pos; break; default: ret=-EINVAL; break; } return ret; } static long globalmem_ioctl(struct file* fp,unsigned int cmd,unsigned long arg){ struct globalmem_chardev* dev=fp->private_data; switch(cmd){ //这个宏在文件开头定义,这里进行详细的分析,之前在面试的时候,有问到过魔数区的问题 case MEM_CLR: //进行上锁 mutex_lock(&dev->mem_mutex); memset(dev->mem_data,0,1024); mutex_unlock(&dev->mem_mutex); break; default: return -EINVAL; } return 0; } static struct file_operations fops={ .owner =THIS_MODULE, .open =globalmem_open, .write =globalmem_wirte, .read =globalmem_read, .release =globalmem_release, .llseek =globalmem_lseek, .unlocked_ioctl =globalmem_ioctl, }; //__init的作用就是在注册成功该驱动以后,就释放此函数占用的内存空间 // (因为已经加载到内核中了,就不要占用内存了) static int __init globalmem_init(void){ dev_t dev_id ;//接收主设备号 int rc;//出错检验 int i;//循环的时候用,为什么这么写,为了兼容以前的老编译器,不支持for(int i=0;i<5;i++)的写法 //申请主设备号 if(major){//上面我们调用了module_param函数,如果用户传参数,就以用户的major为主 dev_id=MKDEV(major,0);//0表示此设备号开始,意思就是我要占用major这个教室,从0位置开始的设备号 //向内核占用设备号 rc=register_chrdev_region(dev_id,2,"globalmem");//2表示占用的次设备号数量 }else{//表示用户不指定主设备号,我们就向内核自动申请一个主设备号 rc=alloc_chrdev_region(&dev_id,0,2,"globalmem");//0表示起始位置,2表示个数 major=MAJOR(dev_id); //从设备号获取主设备号,目的就是向内核绑定设备与主设备号 } if(rc<0){ //出错处理 printk("driver major get failedn"); goto failed; } //占用了设备号的空间,现在就需要往空间中放入我们的设备,以及处理函数 //第一步申请设备结构体空间 global_chardev=kzalloc(2*sizeof(struct globalmem_chardev), GFP_KERNEL);//GFP_KERNEL表示内核内存正常分配 if(!global_chardev){ printk("get memery for globalmem_chardev failedn"); goto failed_chardev; } //注册设备(cdev)信息,并初始化对应设备的互斥体 for(i=0;i<2;i++){ cdev_init(&global_chardev[i].dev_cdev,&fops);//绑定设备与处理程序 rc=cdev_add(&global_chardev[i].dev_cdev,MKDEV(major,i),1);//向内核注册 if(rc<0){ printk("cdev_add register dev_num failedn,%d",i); goto failed_num; } //初始化互斥体 mutex_init(&global_chardev[i].mem_mutex); } printk("register successn"); return 0; failed_num: //释放掉申请的内核空间 kfree(global_chardev); failed_chardev: //释放掉占用的设备号 unregister_chrdev_region(MKDEV(major,0),2); failed: return rc; } static void __exit gloabelmem_exit(void){ int i; for(i=0;i<2;i++){ cdev_del(&global_chardev[i].dev_cdev); } kfree(global_chardev); unregister_chrdev_region(MKDEV(major,0),2); } //入口函数 module_init(globalmem_init); //出口函数 module_exit( gloabelmem_exit); //遵循开源协议 MODULE_LICENSE("GPL"); //作者 MODULE_AUTHOR("jacky");
app01.c
#includeMakefile#include #include #include #include #include #include #include #include #define MEM_CLR _IO('w',2) #define device_globalmem "/dev/globalmem" int main(void){ //打开文件获取文件描述符,进行测试wirte,read int fd; int rc;//出错检验 fd=open(device_globalmem,O_RDWR); char buff[20]="hello my_drive_01";//写入字符串 printf("fd:%dn",fd); printf("test_01 write、read and lseekn"); printf("write:%sn",buff); rc=write(fd,buff,sizeof(buff)-1); if(rc==-1) //向驱动中写入字符串 { perror("[wirte error]"); exit(-1); } printf("write_length:%dn",rc); //读取出来 bzero(buff,sizeof(buff)); rc=lseek(fd,0,SEEK_SET); printf("lseek_offset:%dn",rc); if((rc=read(fd,buff,sizeof(buff)-1))==-1){ perror("[read error]"); exit(-1); } printf("read_length:%dn",rc); printf("read:%sn",buff); printf("n"); printf("test_02 ioctln"); if(ioctl(fd,MEM_CLR,0)){ perror("[ioctl]"); exit(-1); } bzero(buff,sizeof(buff)-1); lseek(fd,0,SEEK_SET); if(read(fd,buff,sizeof(buff)-1)==-1){ perror("[ioctl read error]"); exit(-1); } if(strlen(buff)==0){ printf("ioctl is truen"); } printf("test successn"); close(fd); return 0; }
#Ubuntu内核目录 KERN_DIR := /lib/modules/$(shell uname -r)/build #STM32MP157内核目录 #CROSS_COMILE :=arm-buildroot-linux-gnueabihf- #KERN_DIR := /home/jacky/100ask_stm32mp157_pro-sdk/Linux-5.4 #防止出现污染内核的错误 CONFIG_MODULE_SIG=n all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMILE)gcc -o globalmem_test app01.c .PHONY:clean clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order globalmem_test globalmem-y :=globalmem_chardev.o obj-m +=globalmem.o开发板测试
编译,记住这里要开启交叉编译链,并设置好Makefile中的KERN_DIR
拷贝到开发板,并挂载驱动,创建设备节点,这里采用指定主设备号
测试成功
编译,设置为gcc编译,修改Makefile中KERN_DIR
挂载驱动,创建设备节点,这里采用系统分配设备主设备号
完成
1.尽量在开发板上进行调试,因为如果你的驱动有错误,很可能会造成虚拟机死机。这样就很麻烦,开发板直接关机再开就行。
2.有些地方会出现很不容易找到错误的地方,比如我这次驱动中我在globalmem_init函数最后忘记return 0;了。结果驱动还挂载成功了,但是无法在/proc/devices中查看,以及在read函数中,上锁后,忘记开锁了。导致测试程序运行了就卡死了。大家一定要注意,细心。
Git



