Linux学习之驱动:字符设备驱动(一)
重要结构体
字符设备主要就是通过以下重要的结构体进行实现
struct cdev {
struct kobject kobj; //内核对象
struct module *owner;
const struct file_operations *ops;//设备对应的操作方法
struct list_head list;//设备链表
dev_t dev;//设备号
unsigned int count;
};
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
..........
..........
};
字符设备驱动几个要点
1.注册接口
负责驱动的注册工作
2.卸载接口
负责驱动的卸载工作
3.file_operation
提供设备驱动的操作方法,常见的write、read、iocrtl等接口
驱动初始化
static int __init xxx_chrdev_init(void)
{
...
}
主要做的工作如下:
1.第一步:申请主、次设备号(可以自己定义也可以动态申请)
静态申请设备号并注册:
dev_id = MKDEV(MYMAJOR, 0);//获取主次设备号
retval = register_chrdev_region (dev_id, MYCNT, MYDEVNAME);//注册MYCNT个设备,主次设备号信息存储在dev_id中,设备名为MYDEVNAME="testchar"
if (retval < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success....\n");
动态申请设备号并注册:
//动态自动获取设备号
retval = alloc_chrdev_region (&dev_id,0, MYCNT, MYDEVNAME);//注册MYCNT个设备,主次设备号信息存储在dev_id中,设备名为MYDEVNAME="testchar"
if (retval < 0)
{
printk(KERN_ERR "alloc_chrdev_region fail\n");
goto flag1;
}
2.第二步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);//void cdev_init(struct cdev *cdev, const struct file_operations *fops)
retval = cdev_add(&test_cdev, dev_id, MYCNT);//int cdev_add(struct cdev *p, dev_t dev, unsigned count)
if (retval)
{
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
设备驱动安装
我们为了方便调试,编译的时候是用的module的方式编译,最后生成.ko文件copy到开发板中再进行安装
几个命令需要介绍:
lsmod #显示当前安装的module
insmod xxx.ko #安装module
rmmod xxx.ko #卸载module
mknod /dev/xxx c 设备号 次设备号 #c表示char设备 此操作可以根据主次设备号在/dev目录下创建对应的设备文件,后续用户可以通过访问该设备文件对设备进行操作
通过以下命令可以查看当前注册的设备
cat /proc/devices
实验
1.编译驱动和应用测试程序(后面附带完整程序和脚本)
2.把生成的.ko文件和app copy到开发板中
3.安装模块
4.生成设备节点
5.执行程序应用程序
驱动代码led_test.c
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h> // __init __exit
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h> // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
#define MYDEVNAME "testchar"
#define MYMAJOR 200
#define MYCNT 1
#define GPJ0CON S5PV210_GPJ0CON
#define GPJ0DAT S5PV210_GPJ0DAT
#define rGPJ0CON *((volatile unsigned int *)GPJ0CON)
#define rGPJ0DAT *((volatile unsigned int *)GPJ0DAT)
#define GPJ0CON_PA 0xE0200240
#define GPJ0DAT_PA 0xE0200244
unsigned int * pGPJ0CON;
unsigned int * pGPJ0DAT;
char kbuf[100]; //内核空间的buf
dev_t dev_id;
static struct cdev test_cdev;
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
/*
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
*/
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "test_chrdev_release\n");
/*
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); // 灭
iounmap(pGPJ0CON);//解绑
release_mem_region(GPJ0CON_PA, 4);//释放
iounmap(pGPJ0DAT);//解绑
release_mem_region(GPJ0DAT_PA, 4);//释放
*/
return 0;
}
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
//调用函数static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) 把内容从应用层复制到内核空间
ret = copy_from_user(kbuf,ubuf,count);
if(ret)//说明没有完全拷贝完
{
printk(KERN_INFO "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success\n");
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
//调用函数static inline long copy_to_user(void __user *to,const void *from, unsigned long n) 把内容从内核复制到应用层
ret = copy_to_user(ubuf,kbuf,count);
if(ret)//说明没有完全拷贝完
{
printk(KERN_INFO "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success\n");
return 0;
}
const struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = test_chrdev_open,
.write = test_chrdev_write,
.read = test_chrdev_read,
.release = test_chrdev_release,
};
// 模块安装函数
/*
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
*/
static int __init chrdev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
//注册/分配设备号
/*
dev_id = MKDEV(MYMAJOR, 0);//获取主次设备号
retval = register_chrdev_region (dev_id, MYCNT, MYDEVNAME);//注册MYCNT个设备,主次设备号信息存储在dev_id中,设备名为MYDEVNAME="testchar"
if (retval < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success....\n");
*/
//动态自动获取设备号
retval = alloc_chrdev_region (&dev_id,0, MYCNT, MYDEVNAME);//注册MYCNT个设备,主次设备号信息存储在dev_id中,设备名为MYDEVNAME="testchar"
if (retval < 0)
{
printk(KERN_ERR "alloc_chrdev_region fail\n");
goto flag1;
}
printk(KERN_INFO "alloc_chrdev_region success....\n");
printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(dev_id), MINOR(dev_id));//使用宏获取设备号信息
// 第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);//void cdev_init(struct cdev *cdev, const struct file_operations *fops)
retval = cdev_add(&test_cdev, dev_id, MYCNT);//int cdev_add(struct cdev *p, dev_t dev, unsigned count)
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
goto flag2;
}
printk(KERN_INFO "cdev_add success\n");
// 使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
goto flag2;
if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))
goto flag2;
pGPJ0CON = ioremap(GPJ0CON_PA, 4);
pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
*pGPJ0CON = 0x11111111;
*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); // 亮
return 0;
// 如果第1步才出错跳转到这里来
flag4:
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
// 如果第2步才出错跳转到这里来
flag3:
cdev_del(&test_cdev);
// 如果第3步才出错跳转到这里来
flag2:
unregister_chrdev_region(dev_id, MYCNT);
// 如果第4步才出错跳转到这里来
flag1:
return -EINVAL;
}
// 模块卸载函数
static void __exit chrdev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭
// 解除映射
iounmap(pGPJ0CON);
iounmap(pGPJ0DAT);
release_mem_region(GPJ0CON_PA, 4);
release_mem_region(GPJ0DAT_PA, 4);
//卸载
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(&test_cdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(dev_id,1);//void unregister_chrdev_region(dev_t from, unsigned count)
}
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("zcc"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
应用测试程序
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#define FILEPNAME "/dev/led_test"
char buf[100];
int main(void)
{
int fd = -1;
char write_buf[50] = "--zcc led test--";
fd = open(FILEPNAME,O_RDWR);
if(fd < 0)
{
printf("open fail %s\n",FILEPNAME);
return -1;
}
printf("open %s success \n",FILEPNAME);
//读写文件
printf("write %s to kernel buf \n",write_buf);
write(fd, write_buf, strlen(write_buf));
read(fd, buf, 100);
printf("read from kernel is %s \n",buf);
//sleep(1);
close(fd);
return 0;
}
Makefile 脚本
#ubuntu的内核源码树,如果要编译在ubuntu中安装的模块就打开这2个
#KERN_VER = $(shell uname -r)
#KERN_DIR = /lib/modules/$(KERN_VER)/build
# 开发板的linux内核的源码树目录
#KERN_DIR = /root/driver/kernel
# ubuntu 20_04 中的源码树路径
KERN_DIR = /home/zcc/x210/driver/kernel
obj-m += led_test.o
all:
make -C $(KERN_DIR) M=`pwd` modules
arm-linux-gcc app.c -o led_test.app
cp:
#cp *.ko /root/porting_x210/rootfs/rootfs/driver_test
#cp app /root/porting_x210/rootfs/rootfs/driver_test
cp *.ko /home/zcc/x210/porting_x210/rootfs/rootfs/driver_test
cp *.app /home/zcc/x210/porting_x210/rootfs/rootfs/driver_test
.PHONY: clean
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf *.app