linux 字符驱动开发详解和ST7735s LCD驱动应用层API编写小结


前言

最近一段时间在看linux的字符设备驱动,看了很多内容但是都没有一个清晰的开发流程。如下我做一个简单的自我学习小结,不足错误之处还请大佬们指出。(使用的开发环境是ubuntu18.04系统,LCD是ST7735s)。

一、字符设备驱动是什么?有什么用?

在John Madieu[法]约翰·马迪厄的linux设备驱动开发一书中是这样描述的:字符设备通过字符(一个接一个的字符)以流的方式向用户程序传递数据,就像串行端口那样。字符设备驱动通过/dev目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户应用程序之间交换数据,也可以通过它来控制实际的物理设备。字符设备驱动程序是最基本的设备驱动程序。

说的通俗一点就是字符设备驱动程序相当于是硬件设备和用户程序之间沟通的一座桥梁,你可以通过他来直接控制硬件设备,也可以通过提供一个API接口来实现由上面的应用层间接控制硬件设备。

二、如何编写一个简单的设备驱动程序?

1.如何编写Makefile文件?

在主目录下创建一个test的文件。

在这里插入图片描述进入test文件夹,使用vim 编辑器创建Makefile文件。
在这里插入图片描述
没有了解过vim编辑器的可以去菜鸟教程看看怎么用,毕竟vim是用起来非常舒服而且编辑效率很高的一款编辑器。 你也可以使用gedit文本编辑器或在nano,这些用起来更简单。

将下面的代码复制到Makefile文件中
代码如下:

obj-m := lcd.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

PWD:=$(shell pwd)
all:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
        rm -rf *.o *.ko *.mod.* *.symvers *.order

这里面最重要的就是 $(MAKE) -C ( K E R N E L D I R ) M = (KERNELDIR) M= (KERNELDIR)M=(PWD) modules这一句。
它的作用是到KERNELDIR对应的内核源码树下编译模块。
其它的可以去网上搜索,这里不再做解释。

2.编写驱动代码

在test文件夹下通过vim编辑器将下列代码复制到lcd.c里。
在这里插入图片描述

驱动代码如下:

#include<linux/module.h>
#include<linux/init.h>
#include<linux/fs.h>
#include<linux/uaccess.h>
#include<linux/cdev.h>
#include<linux/miscdevice.h>
#include<linux/types.h>
#include<linux/moduleparam.h>
#include<linux/slab.h>
#include<linux/delay.h>
#include<linux/ioctl.h>

#define DEVICE_NAME "First_lcd_driver" /*安装模块成功后可以在/dev目录下查看到此设备名*/


#ifdef __GNUC__
typedef long long loff_t;
#endif

static char kbuf[100];
/*
static int device_major =0;
static int device_minor =0;
*/


struct cdev my_lcd_cdev;

/*int (*open) (struct inode *, struct file *)*/

static int lcd_dev_open(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "lcd_dev_open open\n");
	return 0;
}


/*int (*release) (struct inode *,struct file *);*/

static int lcd_dev_close(struct inode *inode, struct file *file)
{
	printk(KERN_INFO "lcd_dev_close close\n");
	return 0;
}

static ssize_t lcd_dev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	int ret = -1;
	ret = copy_to_user(buf, kbuf, sizeof(kbuf));/*应用层面的内存buf与内核中的buf数据不能直接交换,所以要使用copy_to_user和copy_from_user*/

	if(ret){
		printk(KERN_INFO "lcd device read error\n");
		}
	
	printk(KERN_INFO "lcd device read success\n");
	return 0;

}


static ssize_t lcd_dev_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos){
	int ret = -1;
	
	memset(kbuf,0,sizeof(kbuf));/*先给kbuf清零*/
	
	ret = copy_from_user(kbuf, user_buf, count);
	if(ret){
		printk(KERN_ERR "kernel lcd write error\n");
		return -EINVAL;
	}
	printk(KERN_INFO "lcd device write success\n");
	
	return 0;
}


static long lcd_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	printk(KERN_INFO "user ioctl running\n");
	return cmd;
}




/*创建file_operation 结构体*/
static const struct file_operations lcd_dev_fops ={
	.owner = THIS_MODULE,
	.open = lcd_dev_open,
	.release = lcd_dev_close,
	.read = lcd_dev_read,
	.write = lcd_dev_write,
	.unlocked_ioctl = lcd_dev_ioctl,	
	
	
};


/*使用misc_register函数注册一个混杂设备,在/dev/下会产生相应的节点*/
static struct miscdevice lcd_device={
	.minor=MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops=&lcd_dev_fops,
};




/*模块下载函数,驱动的入口函数*/
static int __init lcddev_init(void)
{
	int ret;
	
	printk(KERN_INFO"lcddev_init\n");


	misc_register(&lcd_device);/*内核会自动为你的设备在/dev下创建lcd node节点,对设备进行注册*/
	
/*
	miscdevice(device_minor,"lcd",&lcd_dev_fops);通过miscdev函数向内核注册*/

	/*提取主次设备号*/
/*	device_major = MAJOR(lcd_device);
	device_minor = MINOR(lcd_device);*/

	/*把主设备号和file_operations绑定起来*/
	/*cdev_init(&my_lcd_cdev, &lcd_dev_fops);*/
	

	/*真正的注册函数*/
	/*cdev_add(&my_lcd_cdev,MYDEV,1)*/


	printk(KERN_INFO "lcd_dev regist success\n");
	return 0;
	
}

/*模块安装函数,驱动的出口函数*/
static void __exit lcddev_exit(void)
{
	

	printk(KERN_INFO "lcd device exit\n");
	
	
	
	/*注销lcd设备驱动*/
	/*cdev_del(&my_lcd_cdev);*/

	/*移除lcd混杂设备*/
	misc_deregister(&lcd_device);

/*	unregister_chrdev_region(jiacheng_dev,1);*/
	printk(KERN_INFO "lcd_dev unregist success\n");
	
}


module_init(lcddev_init);
module_exit(lcddev_exit);

MODULE_LICENSE("GPL");// 描述模块的许可证
MODULE_AUTHOR("LJC");// 描述模块的作者
MODULE_DESCRIPTION("lcd test");// 描述模块的介绍信息
MODULE_ALIAS("alias lcdtest");// 描述模块的别名信息

下面解释以下这个驱动代码的各个部分。

我在这里使用了miscdev混杂设备来注册(misc设备只适用于驱动一个设备),原因是如果我使用chrdev函数来注册设备的话,需要手动设置主次设备号,这就有可能导致设置的设备号已经被占用的情况。

并且使用struct chrdev函数需要通过alloc_chrdev_region()分配主次设备号。使用class_create()创建自己的设备类。使用struct file_operations创建设备,使用cdev_init()和cdev_add()来以一个字符设备注册自己。而一个misc驱动,则可以只用一个调用misc_register()来几乎就可以完成这所有的步骤(也需要struct file_operations创建设备)。

在Linux驱动中把无法归类的五花八门的设备定义为混杂设备(用miscdevice结构体表述)。miscdevice共享一个主设备号MISC_MAJOR(即10),但次设备号不同。

主设备号可以在/proc/devices里查看到。使用(ls -l /dev)可以查看到对应的驱动的文件类型、权限、主次设备号、日期、设备名。

在此代码中,设备文件操作是一个非常重要的步骤,它的实例是struct file_operations.这个结构体定义了一组回调函数用来处理文件上的所有用户空间系统调用。 比如,如果想让用户在设备文件上执行write操作,就必须在驱动中实现与write函数对应的回调函数,并把它添加到绑定在设备上的struct file_operations 中。

linux内核源码include/linux/fs.h下定义的file_operations结构体原型如下:

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 *);
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};

我作为尝试只定义了read,write,open, release,unlocked_ioctl.其中的.owner = THIS_MODULE 表示指定初始化,这样写的好处是无需按照固定的顺序,提高了灵活性。

ioctl方法

我需要重点讲的是ioctl方法。那什么情况下使用到ioctl方法呢?
我们要调用操作系统的资源的话都是需要通过系统调用来向操作系统提出服务申请的。但是linux系统大约350个系统调用列表中只有少数几个链接到了文件操作。所以如果你需要的和文件操作相关的命令系统调用里没有怎么办呢?在这种情况下就可以使用输入/输出控制(ioctl)来扩展与文件或设备文件相关的系统调用列表(其实就是命令)。

ioctl命令由4个帮助宏来创建,其中的参数有8位的MAGIC标识号(也称魔数),SEQ_NO序号,TYPE数据类型,读写。

_IO(MAGIC,SEQ_NO)  //ioctl不需要数据传输
_IOW(MAGIC,SEO_NO,TYPE)//ioctl写入参数,使用copy_from_user
_IOR(MAGIC,SEO_NO,TYPE)//ioctl读入参数,使用copy_to_user
_IORW(MAGIC,SEQ_NO,TYPE)//读写

接下来说说static int __init()和static int __exit(), 几乎每个驱动都会用这两个宏。
在我们使用insmod +xxx.ko时其实就是加载 __init()这个初始化函数。当我们在使用rmmod +XXX卸载驱动时其实就是调用__exit()。 一般你对驱动做的一些操作都会放在__init()里,差不多相关于我们所熟知的main函数,这么说应该会好理解一点。

接下来按照下图编译生成驱动文件。
在这里插入图片描述
先使用insmod lcd来安装lcd模块,再使用lsmod列出已有的模块,可以看到我们的lcd模块在第一个。
在这里插入图片描述

在这里插入图片描述
使用modinfo +xxx.ko来查看你的驱动模块信息。
在这里插入图片描述

ST7735s lcd 驱动应用层API略讲

这部分代码是提供一个API给上层应用使用的,只贴部分代码供参考。

/*
   用户空间API
*/
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include "drv_lcd.h"
/*
   ioctl
   与lcd驱动通讯
*/
#define LCD_DEV_PATH "/dev/lcd"
/*_IOWR(type,nr,datatype); 数据传输的方向(类型,序号,用户数据的大小)*/
#define LCD_DEV_IOC_MAGIC  'L' /*定义ioctl命令的类型(幻数),表示是哪个设备的命令*/
#define LCD_CMD_ON                   _IO(LCD_DEV_IOC_MAGIC,1)
#define LCD_CMD_OFF                  _IO(LCD_DEV_IOC_MAGIC,2)
#define LCD_CMD_SET_PIXEL            _IOW(LCD_DEV_IOC_MAGIC, 3, int)/*向设备驱动中写入*/

/*******************************省略部分代码*******************************************/
static int DRV_ST7735SInitialize( void )
{
    int ret = -1;
    //打开led设备
    lcd_dev_fd = open(LCD_DEV_PATH, O_RDWR);
    if(lcd_dev_fd < 0) {
        printf("/dev/lcd open failed\n");
        return 0;
    }

    return 1;
}
void DRV_LCDOn( void )
{
    if( !initialized )
    {
        return;
    }
    /* Display On */
    printf("DRV_LCDOn:%d\n", LCD_CMD_ON);
    ioctl(lcd_dev_fd, LCD_CMD_ON);
}

void DRV_LCDOff( void )
{
    /* Display Off */
    ioctl(lcd_dev_fd, LCD_CMD_OFF);
}

void DRV_LCDSetPixel( uint16_t x, uint16_t y, uint16_t color )
{
    uint16_t data[3];
    data[0] = x;
    data[1] = y;
    data[2] = color;
    ioctl(lcd_dev_fd, LCD_CMD_SET_PIXEL, data);
}


总结

以上就是我初步学习字符设备驱动和编写ST7735s LCD驱动应用层API接口的小结,做一个总结以巩固所学的知识,如下是我手写(忽略字丑)的做驱动开发和应用层API编写的开发流程,仅供参考。

驱动开发流程
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值