RK系列学习

RK 3568

参考立创开发板:https://wiki.lckfb.com/zh-hans/

开发板的启动模式:

  • Normal 模式:不支持工具烧录。此模式是正常启动的过程,各个组件依次加载,正常进入系统。系统引导 rootfs 启动,加载 rootfs,大多数的开发板都是在这个模式调试的。
  • Loader 模式:支持工具烧录。可以通过工具单独烧写某一个分区镜像文件,方便调试。
  • MaskRom 模式:支持工具烧录。Flash 在未烧录固件时,芯片会引导进入 MaskRom 模式,可以进行初次固件的烧写;开发调试过程中若遇到 uboot 无法正常启动的情况,也可以进入 MaskRom 模式烧写固件。

随着处理器性能的提高,软件的依赖也越来越复杂,所以往往这些源码都是以 SDK 包的形式提供,SDK 包里面有 uboot,kernel,rootfs 以及库文件和其他厂家自己的文件。

瑞芯微官方的 Linux 源码里面只有 buildroot、debian 和 yocto 这三种文件系统,并没有其他的系统,如 Ubuntu,openwrt 等等。这些文件系统使用的是相同的 uboot 源码和内核源码。

Linux 编译

整体编译:

  1. 设置要编译的文件系统
编译buildroot: export RK_ROOTFS_SYSTEM=buildroot
编译yocto:     export RK_ROOTFS_SYSTEM=yocto
编译debian:    export RK_ROOTFS_SYSTEM=debian
/* 编译yocto,Linux源码一定要放在/home/topeet/Linux目录下 */
  1. 编译和打包镜像,分别执行以下命令
./build.sh all
./build.sh firmware
./build.sh updateimg
  1. 生成文件镜像位置
    buildroot: Linux 源码目录下 buildroot/output/rockchip_rk 3568/images
    debian: Linux 源码目录下 debian/linaro-rootfs. img
    yocto: Linux 源码目录下 yocto/build/lastest/rootfs. img

驱动开发

Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为. ko),在 Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。

编写驱动的时候需要注册这两种操作函数:

module_init(xxx_init);   //注册模块加载函数
module_exit(xxx_exit);   //注册模块卸载函数

参数 xxx_init 和 xxx_exit 就是需要注册和卸载的具体函数

驱动编译完成以后扩展名为. ko,有两种命令可以加载驱动模块:insmod 和 modprobe,insmod 是最简单的模块加载命令,此命令用于加载指定的. ko 模块,比如加载 drv. ko 这个驱动模块,命令如下:

insmod drv.ko

insmod 命令不能解决模块的依赖关系,比如 drv. ko 依赖 first. ko 这个模块,就必须先使用 insmod 命令加载 first. ko 这个模块,然后再加载 drv. ko 这个模块。但是 modprobe 就不会存在这个问题,modprobe 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,因此 modprobe 命令相比 insmod 要智能一些。

推荐使用 modprobe 命令来加载驱动。

驱动模块的卸载使用命令“rmmod”即可,比如要卸载 drv. ko,使用如下命令即可:

rmmod drv.ko

也可以使用“modprobe -r”命令卸载驱动

modprobe -r drv.ko

使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。\

字符设备注册与注销

对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数原型如下所示:

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)

register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下: major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两
部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。

选择没有被使用的主设备号,输入命令“cat /proc/devices”可以查看当前已经被使用掉的设备号

实现设备的具体操作函数

需求:
能够对 chrtest 进行打开和关闭操作
对 chrtest 进行读写操作

/* 打开设备  */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    /* 用户实现具体功能  */
    return 0;
}

/* 从设备读取  */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能  */
    return 0;
}

/* 向设备写数据  */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    /* 用户实现具体功能  */
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp) {
    /* 用户实现具体功能  */
    return 0;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

/* 驱动入口函数 */
static int __init xxx_init(void) {
    /* 入口函数具体内容 */
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, "chrtest", &test_fops);
    if(retvalue < 0) {
        /* 字符设备注册失败,自行处理 */
    }
    return retvalue;
}

/* 驱动出口函数 */
static void __exit xxx_exit(void) {
    /* 注销字符设备驱动 */
    unregister_chrdev(200, "chrtest");
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);


MODULE_LICENSE("GPL");         //添加模块 LICENSE 信息 
MODULE_AUTHOR("jialongfei");         //添加模块作者信息

添加 License 和作者信息

最后我们需要在驱动中加入 LICENSE 信息和作者信息,其中 LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。LICENSE 和作者信息的添加使用如下两个函数:

MODULE_LICENSE()         //添加模块 LICENSE 信息 
MODULE_AUTHOR()         //添加模块作者信息

动态分配设备号:
静态分配设备号需要我们检查当前系统中所有被使用了的设备号,然后挑选一个没有使用的。而且静态分配设备号很容易带来冲突问题,Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可,设备号的申请函数如下:

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);

一个基本的字符驱动模块设计到编写内核代码,包括初始化模块、注册字符设备、处理文件操作等。

#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/uaccess.h>

#define DEVICE_NAME “mychardev”
#define CLASS_NAME "mycharclass"

static int major_number;
static struct cdev my_cdev;
static struct class *my_class = NULL;
static struct device *my_device = NULL;

static int dev_open(struct inode *inodep, struct file *filep){
	printk(KERN_INFO "Device has been opened\n");
	return 0;
}

static int dev_release(struct inode *inodep, struct file *filep) {
    printk(KERN_INFO "Device successfully closed\n");
    return 0;
}

static ssize_t dev_read(struct file *filep, char *buffer, size_t len, loff_t *offset){
	printk(KERN_INFO "Reading from the device\n");
	return 0;
}

static ssize_t dev_write(struct file *filep, const char *buffer, size_t len, loff_t *offset){
	printk(KERN_INFO "Writing to the device\n");
	return len;
}

static struct file_operations fops = {
	.open = dev_open,
	.read = dev_read,
	.write = dev_write,
	.release = dev_release,
};

static int __init chardev_init(void){

	printk(KERN_INFO "Loading the character device...\n");

	//尝试动态获取主设备号
	major_number = register_chrdev(0,DEVICE_NAME,&fops);
	if(major_number < 0){
	    print(KERN_ALERT "Failed to register a major number\n");
	    return major_number;
	}

	// 初始化cdev结构
	cdev_init(&my_cdev, &fops);

	// add cdev to kernel
	if(cdev_add(&my_cdev, MKDEV(major_number,0),1) != 0){
	    printk(KERN_ALERT "Failed to add the device\n");
	    unregister_chrdev(major_number, DEVICE_NAME);
	    return -1;
	}

	//create class
	my_class = class_create(THIS_MODULE, CLASS_NAME);
	if(IS_ERR(my_class)){
	    cdev_del(&my_cdev);
	    unregister_chrdev(major_number,DEVICE_NAME);
	    return PTR_ERR(my_class);
	}

	//create device
	my_device = device_create(my_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);
	if(IS_ERR(my_device)){
	    class_destrory(my_class);
	    cdev_del(&my_cdev);
	    unregister_chrdev(major_number, DEVICE_NAME);
	    return PTR_ERR(my_device);
	}
	return 0;
}

static void __exit chardev_exit(void) {
    printk(KERN_INFO "Unloading the character device...\n");

    device_destroy(my_class, MKDEV(major_number, 0));
    class_unregister(my_class);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev(major_number, DEVICE_NAME);
}

module_init(chardev_init);
module_exit(chardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("0.1");

Linux 下 led 灯驱动原理

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。在 Linux 下编写驱动要符合 Linux 的驱动框架。以 I.MX 6 U 开发板上的 LED 连接到 I.MX 6 ULL 的 GPIO_IO 03 这个引脚上。

地址映射

了解 MMU,全程 Memory Manage Unit,内存管理单元。功能为:

  • 完成虚拟空间到物理空间的映射
  • 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性

虚拟地址 VA,物理地址 PA。对于 32 位的处理器,虚拟地址的范围为 2^32=4 GB.
开发板上有 512 MB 的 DDR 3,这 512 MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4 GB 的虚拟空间中。

Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。物理内存和虚拟内存之间的转换,需要用到连个函数:ioremap 和 iounmap。

  1. ioremap 函数

ioremap 函数用于获取指定物理地址空间对应的虚拟地址空间,定义在 arch/arm/include/asm/io.h 文件中。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址。

返回值:__iomem类型的指针,指向映射后的虚拟空间首地址。
  1. iounmap 函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射。

void iounmap(volatile void __iomem *addr);

只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。


当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

读操作函数

u8 readb(const volatile void __iomem *addr);
u16 readw(const volatile void __iomem *addr);
u32 readl(const volatile void __iomem *addr);

readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

写操作函数

void writeb(u8 value, volatile void __iomem *addr);
void writew(u16 value, volatile void __iomem *addr);
void writel(u32 value, volatile void __iomem *addr);

writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作,参数 value 是要写入的数值,addr 是要写入的地址。

LED 灯字符驱动源码

#define LED_MAJOR 200 /* 主设备号 *
#define LED_NAME "led" /* 设备名字 */

#define LEDOFF 0 /* 关灯 */
#define LEDON 1 /* 开灯 */

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

void led_switch(u8 sta) 
{
    u32 val = 0; 
    if(sta == LEDON) { 
        val = readl(GPIO1_DR); 
        val &= ~(1 << 3);    
        writel(val, GPIO1_DR); 
    } else if(sta == LEDOFF) { 
        val = readl(GPIO1_DR); 
        val |= (1 << 3);  
        writel(val, GPIO1_DR); 
    }    
}

static int led_open(struct inode *inode, struct file *filp)  
{  
    return 0;  
}

static ssize_t led_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)  
{  
   return 0;  
}

static ssize_t led_write(struct file *filp, const char __user *buf, 
        size_t cnt, loff_t *offt) 
{
    int retvalue; 
    unsigned char databuf[1]; 
    unsigned char ledstat; 

    retvalue = copy_from_user(databuf, buf, cnt); 
    if(retvalue < 0) { 
        printk("kernel write failed!\r\n"); 
        return -EFAULT; 
    } 

    ledstat = databuf[0];        /* 获取状态值   */

    if(ledstat == LEDON) {   
        led_switch(LEDON);        /* 打开 LED 灯   */ 
    } else if(ledstat == LEDOFF) { 
        led_switch(LEDOFF);   /* 关闭 LED 灯   */ 
    }

    return 0; 
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 设置操作函数 */
static struct file_operations led_fops = {

	.owner = THIS_MODULE;
	.open = led_open;
	.read = led_read;
	.write = led_write;
	.releawse = led_release;
};

static int __init led_init(void){

	int retvalue = 0;
	u32 val = 0;
	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
 
    /* 2、使能 GPIO1 时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);  /* 清除以前的设置 */
    val |= (3 << 26);   /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);
 
    /* 3、设置 GPIO1_IO03 的复用功能,将其复用为 GPIO1_IO03,最后设置 IO 属性。 */
    writel(5, SW_MUX_GPIO1_IO03);
 
    /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */
    writel(0x10B0, SW_PAD_GPIO1_IO03);
 
    /* 4、设置 GPIO1_IO03 为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);   /* 清除以前的设置 */
    val |= (1 << 3);    /* 设置为输出 */
	writel(val, GPIO1_GDIR);
 
    /* 5、默认关闭 LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);     
    writel(val, GPIO1_DR);
 
    /* 6、注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0){
     printk("register chrdev failed!\r\n");
     return -EIO;
    }
    return 0;
} 
 /* 
  * @description   : 驱动出口函数 
  * @param        : 无 
  * @return       : 无 
  */
 static void __exit led_exit(void) 
 {
     /* 取消映射 */
     iounmap(IMX6U_CCM_CCGR1);
     iounmap(SW_MUX_GPIO1_IO03);  
     iounmap(SW_PAD_GPIO1_IO03);
     iounmap(GPIO1_DR);
     iounmap(GPIO1_GDIR);
 
     /* 注销字符设备驱动 */
     unregister_chrdev(LED_MAJOR, LED_NAME);
 }
 
 module_init(led_init);
 module_exit(led_exit);
 MODULE_LICENSE("GPL");
 MODULE_AUTHOR("jialongfei");

在/dev 路径下可以通过 ls -l 查看设备节点

crw-rw----  1 root        video    29,   0 4月  16 10:30 fb0

c: 表示这是一个字符设备文件。
rw-rw----: 文件权限,代表文件所有者和所属组有读写权限,其他用户没有任何权限。
1: 确定这个设备文件的硬链接数。
root: 文件所有者为 root。
video: 文件所属 video 组。
29, 0: 设备号,代表这是主设备号为 29,次设备号为 0 的设备。
4 月 16 10:30: 文件的最后修改时间。
fb 0: 设备文件名,代表此文件是第一个帧缓冲设备的字符设备文件。

静态分配设备号

静态分配设备号需要开发者手动指定设备号,保证每个设备号都唯一。在代码中通过调用 register_chrdev_region () 函数进行注册。这个函数的第一个参数是主设备号,第二个参数是设备数量,第三个参数是设备名,在/proc/devices 和 sysfs 中。

register_chrdev_region () 函数成功返回 0,失败返回一个负数。

dev_t       devno;
int         result;
int         major = 251;
​
devno = MKDEV(major, 0);
​
result = register_chrdev_region(devno, 4, "chrdev");//静态的申请和注册设备号
if(result < 0)
{
    printk(KERN_ERR "chrdev can't use major %d\n", major);
    return -ENODEV;
}

动态分配设备号

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数说明:

  • dev: 用于返回分配的设备号
  • firstminor: 要分配的第一个次设备号,它常常是 0
  • count: 要分配的设备号数量
  • name: 设备名字

动态分配的缺点是你无法提前创建设备节点,因为分配给你的主设备号会发生变化,对于驱动的正常使用这不是问题,但是一旦编号分配了,只能通过查看 /proc/devices 文件才能知道它的值,然后再创建设备节点。

释放主次设备号

注销字符设备后要释放设备号,设备号释放函数如下:

void unregister_chrdev_region(dev_t from, unsigned count);

通常在模块的卸载函数 xxx_exit 中调用该函数,释放模块使用的设备号资源。

字符设备注册

内核在内部使用类型struct cdev的结构体来代表字符设备。在内核调用你的设备操作之前,你必须分配一个这样的结构体并注册给linux内核,在这个结构体里有对于这个设备进行操作的函数,具体定义在file_operation结构体中。

注册 cdev 到内核

分配到 cdev 结构体后,我们将它初始化,并将对该设备驱动所支持的系统调用函数存放在 file_operations 结构体添加进来。然后用 cdev_add 函数将它注册到内核,从而完成一个完整的 Linux 设备注册过程。

int cdev_add(struct cdev *p, dev_t dev, unsigned int count);

p:指向要添加的字符设备对象的 cdev 结构体指针
dev: 指定要添加的设备号
count: 指定添加的设备号的数量

static struct file_operations chrtest_fops =
{
    .owner = THIS_MODULE,
    .open = chrtest_open,
    .release = chrtest_release,
    .unlocked_ioctl = chrtest_ioctl,
};

struct cdev *chrtest_cdev;

chrtest_cdev = cdev_alloc();
if(NULL == chrtest_cdev)
{
    printk(KERN_ERR "S3C %s driver can't alloc for the cdev.\n", DEV_NAME);
    unregister_chrdev_region(devno, dev_count);
    return -ENOMEM;   
}

chrtest_cdev->owner = THIS_MODULE;
// 声明的文件操作结构体是chrtest_fods
cdev_init(chrtest_cdev, &chrtest_fops);

int result = cdev_add(chrtest_cdev, devno, dev_count);

if(0 != result)
{
    printk(KERN_INFO "S3C %s driver can't register cdev: result = %d\n", DEV_NAME, result);
    goto ERROR;
}

// ... 其他代码 ...

ERROR:
// 错误处理代码

/*
代码解释如下:

- 定义了一个名为`chrtest_fops`的`file_operations`结构体,它包含了指向文件操作函数的指针,例如打开(`open`)、释放(`release`)和非锁定的IO控制(`unlocked_ioctl`)。
    
- 声明了一个`cdev`指针`chrtest_cdev`。
    
- 使用`cdev_alloc()`尝试分配一个字符设备结构体。如果分配失败(返回`NULL`),打印错误消息,注销字符设备区域,并返回`-ENOMEM`错误码。
    
- 将分配的`cdev`的`owner`字段设置为`THIS_MODULE`,表示这个设备属于当前模块。
    
- 使用`cdev_init`函数初始化`cdev`结构体,传入`chrtest_cdev`和文件操作结构体`chrtest_fops`。
    
- 调用`cdev_add`函数尝试将`cdev`添加到内核中,传入设备号`devno`和设备计数`dev_count`。
    
- 如果`cdev_add`返回非零值(表示失败),打印一条信息消息,并跳转到`ERROR`标签处进行错误处理。

*/

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mycharclass"

static int major_number;
static struct cdev my_cdev;
static struct class *my_class = NULL;

// 用于打开设备
static int dev_open(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device has been opened\n");
    return 0; // 成功返回0
}

// 用于读取设备
static ssize_t dev_read(struct file *file, char __user *buffer, size_t len, loff_t *offset) {
    printk(KERN_INFO "Reading from the device\n");
    // 实现读取逻辑
    return 0;
}

// 用于写入设备
static ssize_t dev_write(struct file *file, const char __user *buffer, size_t len, loff_t *offset) {
    printk(KERN_INFO "Writing to the device\n");
    // 实现写入逻辑
    return len;
}

// 用于关闭设备
static int dev_release(struct inode *inode, struct file *file) {
    printk(KERN_INFO "Device successfully closed\n");
    return 0;
}

// 文件操作结构体
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = dev_open,
    .read = dev_read,
    .write = dev_write,
    .release = dev_release,
};

// 初始化模块
static int __init mychardev_init(void) {
    printk(KERN_INFO "Loading the character device...\n");

    // 动态申请主设备号
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "Failed to register a major number\n");
        return major_number;
    }

    // 初始化cdev结构
    cdev_init(&my_cdev, &fops);
    if (cdev_add(&my_cdev, MKDEV(major_number, 0), 1) == -1) {
        printk(KERN_ALERT "Failed to add the device\n");
        unregister_chrdev(major_number, DEVICE_NAME);
        return -1;
    }

    // 创建设备类
    my_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(my_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev(major_number, DEVICE_NAME);
        return PTR_ERR(my_class);
    }

    // 创建设备节点
    device_create(my_class, NULL, MKDEV(major_number, 0), NULL, DEVICE_NAME);

    return 0;
}

// 卸载模块
static void __exit mychardev_exit(void) {
    device_destroy(my_class, MKDEV(major_number, 0));
    class_unregister(my_class);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "Unloading the character device...\n");
}

module_init(mychardev_init);
module_exit(mychardev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_VERSION("0.1");

字符设备名字在注册设备号时指定:“jialongfei”

major = register_chrdev(0,"jialongfei", &led_fops);

第一个参数:主设备号,0 由系统分配

第二个参数:字符设备名字

第三个参数:要注册的 file_operation 结构体

返回值:major 为 0 时,返回一个自动分配的设备号


类和设备节点创建

类:同一个类别的设备,比如 led 中的 led 0、led 1

设备节点:具体由几个设备,通过对具体的设备节点操作

led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled0");
device_create(led_class, NULL, MKDEV(major, 1), NULL, "myled1");

Linux Drivers

led.c测试程序

运行测试

编译驱动程序和测试APP

编写Makefile文件,将obj-m变量的值改为led.o

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

obj-m := led.o

clean:

$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

设置obj-m变量的值为led.o,表示编译led.c文件生成led.o文件。

输入如下命令编译出驱动模块文件:

make -j32

编译完成后,在当前目录下会生成led.ko文件。

输入如下命令编译测试ledApp.c这个测试程序:

arm-linux-gnueabihf-gcc ledApp.c -o ledApp

编译成功以后就会生成ledApp这个应用程序。

将编译出来的led.ko和ledApp文件拷贝到开发板文件目录上,重启开发板

进入到对应目录上,输入下面的命令加载led.ko驱动模块

depmod   //第一次加载驱动的时候需要运行此命令

modprobe led.ko   //加载驱动模块

驱动加载成功以后创建 /dev/led 设备节点,命令如下:

mknod /dev/led c 10 20   //创建设备节点

驱动节点创建成功以后可以使用ledApp软件来测试驱动是否工作正常,命令如下:

./ledApp /dev/led 1   //运行ledApp测试程序

测试程序运行成功以后,LED灯就会亮起来。

./ledApp /dev/led 0   //关闭LED灯

关闭LED灯。

卸载驱动的话,输入如下命令:

rmmod led.ko

卸载成功以后,LED灯就会熄灭。

register_chrdev函数用来注册设备节点,unregister_chrdev函数用来注销设备节点。这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动API函数。(并在驱动模块加载的时候自动创建设备节点文件)

块设备和网络设备要比字符设备驱动复杂,半导体厂商一般都已经编写好了。块设备驱动就是存储器设备的驱动。

驱动加载成功后会在/dev 目录下生成一个相应的文件。驱动运行于内核空间。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件 include/linux/fs. h 中有个叫做 file_oerations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。

驱动开发流程

模块的加载和卸载需要注册两种操作函数

module_init(xxx_init);
module_exit(xxx_exit);

加载驱动

insmod drv.ko
modprobe drv.ko //分析模块的依赖关系

卸载驱动

rmmod drv.ko
modbprobe -r drv.ko

驱动加载成功后需要注册字符设备,卸载驱动模块也需要注销掉字符设备。所以需要字符设备的注册和注销函数

原型如下:

/*register_chrdev 用于注册字符设备
major:主设备号
name:设备名字,指向一串字符串
fops:结构体file_operations类型指针,指向设备的操作函数集合变量
*/
static inline int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops)

/*
unregister_chrdev 用于注销字符设备
major:主设备号
name:设备名
*/

static inline void unregister_chrdev(unsigned int major,const char *name)

file_operations 结构体是设备的具体操作函数,初始化其中的 open、release、read 和 write 等具体的设备操作函数。

statlic struct file_operations test_fops;

static int chrdev_open(struct inode *inode,struct file *filp){
 return 0;
}

static ssize_t chrdev_read(struct file *filp, char __user *buf,size_t cnt,loff_t *offt){
   return 0;
}

static ssize_t chrdev_write(struct file *filp, const char __user *buf,size_t cnt,loff_t *offt){
    return 0;
}

static int chrdev_release(struct inode *inode, struct file *filp){
    return 0;
}

static struct file_operations test_fops = {
    .owner = THIS_MODULE,
    .open = chrdev_open,
    .read = chrdev_read,
    .write = chrdev_write,
    .release = chrdev_release,
};
static int __init demo_init(void){
    int retvalue = 0;
    retvalue = register_chrdev(200,"chrdev_test",&test_fops);
    if(retvalue < 0){
        printk("register chrdev failed!\r\n");
    }
}

static void __exit demo_exit(void){
    unregister_chrdev(200,"chrdev_test");
}

module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jialongfei");

上述代码即为字符设备驱动开发的大致步骤。

Linux 设备号

Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types. h 里面。

dev_t 是__u 32 类型的,是 unsigned int 类型,是一个 32 位的数据类型。这 32 位的数据构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。因此 Linux 系统中主设备号范围为 0~4095。

#define MINORBITS   20

#define MINORMASK   ((1U << MINORBITS) - 1)
//从dev_t中获取主设备号,将dev_t右移20位即可
#define MAJOR(dev)  ((unsigned int) ((dev) >> MINORBITS))
//从dev_t中获取次设备号,取dev_t的低20位的值即可
#define MINOR(dev)  ((unsigned int) ((dev) & MINORMASK))
//用于将
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

Linux 社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,避免了冲突。

dev:保存申请到的设备号
baseminor:次设备号起始地址,alloc_chrdev_region可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以baseminor为起始地址开始递增。一般baseminor为0,次设备号从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)

printk 和 printf 的最大区别,可以通过消息级别来决定哪些消息可以显示在控制台上。

因为内核空间不能直接操作用户的内存, 因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。

编写测试 APP 就是编写 Linux 应用,需要用到 C 库里面和文件操作有关的一些函数。

flags:
O_RDONLY    只读模式
O_WRONLY    只写模式
O_RDWR		读写模式
int open(const char *pathname, int flags)

新字符设备驱动原理

使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:

  1. 需要实现确定好哪些主设备号没有使用

  2. 会将一个主设备号下的所有次设备号都使用掉,比如现在设置LED这个主设备号为200,那么0~1048575(2^20-1)这个区间的次设备号就全部都被LED一个设备分走了。这样太浪费次设备号了!一个LED设备肯定只能有一个主设备号,一个次设备号。

解决方法

使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。如果没有指定设备号的话就使用如下函数来申请设备号:


int alloc_chrdev_region(dev_t *dev, unsigned int baseminor, unsigned int count, const char *name);

如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:


int register_chrdev(dev_t from, unsigned count, const char *name);

参数from是要申请的起始设备号,也就是给定的设备号;参数count是要申请的数量,一般都是一个;参数name是设备的名字。

注销字符设备之后要释放掉设备号,不管是通过alloc_chrdev_region函数申请的还是通过register_chrdev函数注册的,都需要使用如下函数来释放设备号:


void unregister_chrdev(dev_t from, unsigned count);

新字符设备驱动下,设备号分配示例代码:


int major;

int minor;

dev_t devid;

  

if(major){

    devid = MKDEV(major, 0);  /* 大部分驱动次设备号都选择0 */

    register_chrdev(devid, 1, "led");  /* 注册设备 */

} else{

    alloc_chrdev_region(&devid, 0, 1, "led");

    major = MAJOR(devid);

    minor = MINOR(devid);

}

在Linux驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择0。如果major有效的话就使用MKDEV来构建设备号,次设备号选择0。

如果major无效的话就使用alloc_chrdev_region来分配设备号,然后获取主设备号和次设备号。

注销设备号的话,使用如下函数:


unregister_chrdev_region(devid, 1);

新的字符设备注册方法


struct cdev{

    struct kobject kobj;

    struct module *owner;

    const struct file_operations *fops;

    struct list_head list;

    dev_t dev;

    unsigned int count;

};

在cdev中有两个重要的成员变量:ops和dev,这两个就是字符设备文件操作函数集合file_operations以及设备号dev_t。编写字符设备驱动之前需要定义一个cdev结构体变量,这个变量就表示一个字符设备。

struct cdev test_cdev;

定义好cdev变量就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下:


void cdev_init(struct cdev *cdev, const struct file_operations *fops);

参数cdev是要初始化的cdev变量,参数fops是字符设备文件操作函数集合。


  

struct cdev testcdev;

  

static struct file_operations test_fops = {

    .owner = THIS_MODULE,

    /* 其他具体的初始项 */

};

  

testcdev.owner = THIS_MODULE;

cdev_init(&testcdev, &test_fops);

  

cdev_add(&testcdev, devid, 1);

  

cdev_del(&testcdev);

  

首先使用cdev_init函数完成对cdev结构体变量的初始化,然后使用cdev_add函数向Linux系统添加这个字符设备。


int cdev_add(struct cdev *cdev, dev_t dev, unsigned count);

参数cdev是要添加的字符设备,参数dev是要添加的设备号,参数count是设备的数量。

当我们使用modprobe加载驱动程序以后还需要使用命令mknod手动创建设备节点。

如何自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动程序的时候就会自动创建设备节点,在/dev目录下创建对应的设备文件。

udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。

使用busybox构建根文件系统的时候,busybox会创建一个udev的简化版本——mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理。

在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备


  

struct class *class;   //类

struct device *device; //设备

dev_t devid;           //设备号

  

static int __init led_init(void)

{

    class = class_create(THIS_MODULE, "led_class");

  

    device = device_create(class, NULL, devid, NULL, "led");

  

    return 0;

}

  

static void __exit led_exit(void)

{

    device_destroy(newchrled.class, newchrled.devid);

  

    class_destroy(newchrled.class);

}

  

module_init(led_init);

module_exit(led_exit);

  

在 Linux 内核模块编程中,__init 和 __exit 是两个特殊的宏,用于标记模块初始化和清理函数。

__init:

这个宏用于标记一个函数为模块初始化函数。

当内核加载一个模块时,会自动调用这个函数。

通常用于分配资源、注册设备驱动、创建数据结构等初始化操作。

这个宏标记的函数在模块加载时执行一次。

__exit:

这个宏用于标记一个函数为模块卸载时的清理函数。

当内核卸载一个模块时,会自动调用这个函数。

通常用于释放资源、注销设备驱动、删除数据结构等清理操作。

这个宏标记的函数在模块卸载时执行一次。

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class),设备(device),开关状态(state)等,在编写驱动的时候可以将这些属性全部写成变量的形式,这样可以方便管理。对于一个设备的所有属性信息,我们最好将其做成一个结构体。编写驱动open函数的时候将设备结构体作为私有数据添加到设备文件中。


  

struct test_dev{

  

dev_t devid;           //设备号

struct cdev cdev;      //字符设备

struct class *class;   //类

struct device *device; //设备

int major;             //主设备号

int minor;             //次设备号

  

};

  

struct test_dev testdev;

  

static int test_open(struct inode *inode, struct file *file)

{

    filp->private_data = &testdev;

    return 0;

}

  

设备文件,file结构体有个叫做private_data的成员变量,一般在open的时候将private_data指向设备结构体。


  

static sszie_t led_write(struct file *file, const __user *buf, size_t cnt, loff_t *offt)

{

    int retvalue;

    unsigned char databuf[1];

    unsigned cha ledstat;

  

    retvalue = copy_from_user(databuf, buf, cnt);

    if(retvalue < 0){

        printk("kernel write failed!\r\n");

        return -EFAULT;

    }

  

    ledstat = databuf[0];

    if(ledstat == LEDON){

        led_switch(LEDON);

    }else if(ledstat == LEDOFF){

        led_switch(LEDOFF);

    }

    return 0;

}

  

static int led_release(struct inode *inode, struct file *file)

{

    return 0;

}

  

/* 设备操作函数 */

static struct file_operations newchrled_fops = {

    .owner = THIS_MODULE,

    .open = led_open,

    .read = led_read,

    .write = led_write,

    .release = led_release,

}

  

static int __init led_init(void)

{

    u32 val = 0;

  

    /* 寄存器地址映射 */

    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);

    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);

    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);

    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

  

    /* 使能GPIO1时钟 */

    val = readl(IMX6U_CCM_CCGR1);

    VAL &= ~(3 << 26);

    val |= (1 << 26);

    writel(val, IMX6U_CCM_CCGR1);

  

    /* 设置GPIO1_IO03的复用功能,将其复用为GPIO1_IO03,最后设置IO属性 */

    writel(5, SW_MUX_GPIO1_IO03);

    writel(0, SW_PAD_GPIO1_IO03);

  

    /* 设置GPIO1_IO03为输出模式 */

    val = readl(GPIO1_GDIR);

    val &= ~(1 << 3); /* 先清除原有属性 */

    val |= (1 << 3);   /* 设置为输出模式 */

    writel(val, GPIO1_GDIR);

  

    /* 默认关闭LED */

    val = readl(GPIO1_DR);

    val |= (1 << 3);

    writel(val, GPIO1_DR);

  

    /* 注册字符设备驱动 */

    /* 1、创建设备号 */

    if (newchrled.major) {       /* 定义了设备号 */

        newchrled.devid = MKDEV(newchrled.major, 0);

        register_chrdev_region(newchrled.devid, NEWCHRLED_CNT,  

        NEWCHRLED_NAME);

    } else {                     /* 没有定义设备号 */

        alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT,  

        NEWCHRLED_NAME);     /* 申请设备号 */

        newchrled.major = MAJOR(newchrled.devid); /* 获取主设备号 */

        newchrled.minor = MINOR(newchrled.devid); /* 获取次设备号 */

    }

    printk("newcheled major=%d,minor=%d\r\n",newchrled.major,  

    newchrled.minor);  

  

    /* 2、初始化 cdev */

    newchrled.cdev.owner = THIS_MODULE;

    cdev_init(&newchrled.cdev, &newchrled_fops);

  

    /* 3、添加一个 cdev */

    cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT);

  

    /* 4、创建类 */

    newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME);

    if (IS_ERR(newchrled.class)) {

        return PTR_ERR(newchrled.class);

    }

  

    /* 5、创建设备 */

    newchrled.device = device_create(newchrled.class, NULL,  

    newchrled.devid, NULL, NEWCHRLED_NAME);

    if (IS_ERR(newchrled.device)) {

        return PTR_ERR(newchrled.device);

    }

  

    return 0;

}

    /*

    * @description   : 驱动出口函数

    * @param        : 无

    * @return       : 无

    */

static void __exit led_exit(void)

{

    /* 取消映射 */

    iounmap(IMX6U_CCM_CCGR1);

    iounmap(SW_MUX_GPIO1_IO03);

    iounmap(SW_PAD_GPIO1_IO03);

    iounmap(GPIO1_DR);

    iounmap(GPIO1_GDIR);

  

    /* 注销字符设备 */

    cdev_del(&newchrled.cdev);/*  删除 cdev */

    unregister_chrdev_region(newchrled.devid, NEWCHRLED_CNT);

  

    device_destroy(newchrled.class, newchrled.devid);

    class_destroy(newchrled.class);

}

  

module_init(led_init);

module_exit(led_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR("jialongfei");

  
  

将编译出来的newchrled.ko和ledApp这两个文件拷贝到对应开发板目录,重启开发板。

第一次加载驱动的时候需要运行此命令:depmod

加载驱动模块:modprobe newchrled.ko

驱动加载成功以后会自动在/dev目录下创建设备节点文件/dev/newchrdev

驱动节点创建成功以后就可以使用ledApp软件来测试驱动是否工作正常,输入如下命令打开LED灯:

./ledApp /dev/newchrled 1   //open led

./ledApp /dev/newchrled 0  //close led

if you want to unload the driver, you can use the following command:

rmmod newchrled.ko


Device Tree

在新版本的Linux中,ARM相关的驱动全部采用了设备树

DTS file use tree structure to describe the hardware and its configuration.

将这些描述板级硬件信息的内容从Linux内分离开来。用一个专属的文件格式来描述,这个专属的文件叫做设备树,文件扩展名为.dts。一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。

DTC工具源码在Linux内核的scripts/dtc目录下。

只编译设备树的话使用make dtbs命令

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键值对。

节点命名格式

  • label:node-name@unti-address *

引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点。

每个节点都有不同属性,属性又有不同的内容,属性都是键值对,但值可以为空或任意字节流。

节点属性

compatible属性

兼容性属性,值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序。

compatible = "fsl,imx6ul-evk-wm8960", "fsl,imx-audio-wm8960";

属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。

一般的驱动程序文件都会有个OF匹配表,里面有设备的各种属性和兼容性属性,通过OF匹配表可以找到驱动程序。


  

static const struct of_device_id imx_wm8960_dt_ids[] = {

    {.compatible = "fsl,imx-audio-wm8960", },

    { /* sentinel */ }

};

  

MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);

  

static struct platform_driver imx_wm8960_driver = {

    .driver = {

        .name = "imx-wm8960",

        .pm = &snd_soc_pm_ops,

        .of_match_table = imx_wm8960_dt_ids,

    },

    .probe = imx_wm8960_probe,

    .remove = imx_wm8960_remove,

};

model属性

属性值是一个字符串,一般model属性描述设备模块信息。

model = "wm8960-audio";

status属性

和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息

  • okay:设备正常工作

  • disabled:设备被禁用,但是可以重新启用

  • fail:设备故障

  • fail-sss:设备故障,sss部分是检测到的错误内容

address-cells属性 和 size-cells属性

都是无符号32位整形,这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性

都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度。


/ {

    soc {

        #address-cells = <2>; // 表示地址使用2个32位单元

        #size-cells = <1>;     // 表示大小使用1个32位单元

        ...

    };

};


spi4 {

    compatible = "spi-gpio";

    #address-cells = <1>;

    #size-cells = <0>;

  

    gpio_spi: gpio_spi@0 {

         compatible = "fairchild,74hc595";

         reg = <0>;

    };

};

  

aips3: aips-bus@02200000 {

    compatible = "fsl,aips-bus", "simple-bus";

    #address-cells = <1>;

    #size-cells = <1>;

  

    dcp: dcp@02280000 {

        compatible = "fsl,imx6sl-dcp";

        reg = <0x02280000 0x4000>;

    };

};

第 3,4 行,节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属

性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。

第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg  属性值为  <0>,因为父节点设置了#address-

cells = <1>,#size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没

有设置地址长度。

第 14,15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,

说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也

为 1。

第 19 行,子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设

置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置

了起始地址为 0x02280000,地址长度为 0x40000。

reg属性

reg 属性值是一个地址描述,可以有两种形式:

  • 形式1:起始地址和长度,如 reg = <0x02280000 0x4000>;

  • 形式2:起始地址和长度,如 reg = <0x02280000 0x4000 0x02280000 0x4000>;

形式1中,起始地址和长度是用两个 32 位单元表示的,形式2中,起始地址和长度是用四个 32 位单元表示的。

一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息

device_type属性

此属性知恩阁用于cpu节点或者memory节点。

编译环境

ubuntu 18.04

cd uboot
U-Boot引导加载程序的源代码和构建脚本
./make.sh rk3566 用于编译和构建适用于RK3566平台的U-Boot引导加载程序

make clean 命令清理之前的编译结果
make distclean 命令进一步清理之前的配置和生成文件

使用make命令与ARCH=arm64(指定了目标架构为64位ARM架构,这意味着编译的结果将适用于基于ARM64架构的处理器)

-j16 表示使用16个线程进行并行编译

make ARCH=arm64 tspi-rk3566-user-v10.img -j16

tspi-rk3566-user-v10.img是构建的目标产物,针对tspi rk3566的镜像文件

提醒内存不足可以降低编译线程数 -j8

boot-debug.img  与boot.img相比,用户版本固件可以烧写此镜像以获取root权限操作

config.cfg:瑞芯微下载工具的配置文件

MiniLoaderAll.bin:在RK3566平台的U-Boot之前运行的一段loader代码(位于U-Boot更早阶段的Loader)

Samba是一种在Unix/Linux和Windows系统之间实现文件和打印机共享的开源软件套件。它允许不同操作系统之间通过网络共享文件和打印机等资源,使Unix/Linux系统能够像Windows系统中的文件共享一样工作,从而实现跨平台的资源共享。

repo是谷歌开发的一个工具,主要用于管理Android源代码仓库。在Android开发中,整个Android系统的代码非常庞大,是由许多不同的代码仓库(repository)组成的。repo可以帮助开发者更方便地从多个代码仓库中同步、管理和提交代码。

Repo通过一个单一的repo仓库来组织和同步多个Git项目,简化大型项目中多个Git仓库的管理。

.mk文件通常是一种用于构建系统的脚本文件。它主要用于自动化软件构建过程,在不同的软件项目和开发环境中由广泛的应用。这些文件一般包含一系列的变量定义、规则和命令,用于告诉构建工具(如make)如何编译、链接程序以及处理依赖关系。

Buildroot是一个用于自动化构建嵌入式Linux系统的工具。它能够通过简单的配置文件,从源代码开始构建一个完整的嵌入式Linux系统,包括内核、根文件系统、交叉编译工具链以及各种应用程序。这方便为特定的嵌入式设备定制适合的操作系统。

Buildroot的核心是配置文件,通常是config文件。

Linux关于内存的命令

free 用于显示系统内存的使用情况,包括物理内存、交换内存以及内核缓冲区使用的内存情况

top 动态查看系统性能的工具,其中也包含了内存使用情况

vmstat 用于报告虚拟内存统计信息,包括内存、进程、I/O等方面的数据

pmap 用于查看一个或多个进程的内存映射情况

为了改一个驱动去编译kernel然后再下载时间很慢,如果单纯修改了驱动可以只编译驱动模块 然后放到开发板中去加载。

配置Makefile
obj-m +=my_touch.o
编译成模块就-m,编译成内核就obj-y

单独编译驱动生成my_touch.ko
make ARCH=arm64 -C . M=./drivers/input/touchscreen/my_touch

make 命令用于构建Linux内核或内核模块
ARCH=arm64 指定了目标架构为64位ARM架构
-C . 指定了内核源代码目录为当前目录

Linux压缩命令

压缩: tar -czf package.tar.gz file1.txt file2.txt directory
解压缩:tar -xzf package.tar.gz

vim命令

删除一行:dd
复制一行:yy

命令模式:
保存文件:w
退出vim:q
保存同时退出:wq
强制退出:q!

Git命令

抽补丁:git diff > my_patch_name.patch
打补丁:patch < my_pathc_name.patch

git diff 用于显示工作目录(或暂存区、分支之间等)的文件差异。当你想创建要给补丁来记录这些差异时,可以使用git diff来生成。这个命令会比较文件的不同版本,包括内容的修改、新增或删除的行等信息,并将这些差异以一种特定的格式输出,这个输出的内容就是补丁文件的主要内容。

设备树:用于描述硬件信息的一个配置文件,因为它描述的拓扑结构很像树,所以就叫做设备树。

设备树时一种树状的结构,由节点(Node)和属性(Property)组成。每个节点描述一个硬件设备或资源,节点通过父子关系和兄弟关系进行连接。

常见术语
DT:Device Tree
DTS:设备树源文件
DTSI:设备树包含文件
DTC:设备树编译器
DTB:设备树二进制文件

泰山派设备树文件存放在:

SDK/kernel/arch/arm64/boot/dts/rockchip

设备树上下文中的Makefile用于自动化编译和构建DTS/DTSI文件,生成对应的DTB文件

编译(从.dts 和.dtsi 到 .dtb)
dtc -I dts -O dtb -o output_file.dtb input_file.dts

反编译(从.dtb 到 .dts)
dtc -I dtb -O dts -o output_file.dts input_file.dtb

DTC编译工具在:
tspi/linux/kernel/scripts/dtc/

属性是节点的一部分,它为节点提供了额外的描述信息。

在dts文件第一行要写版本:
/dts-v1/;      //指定这是一个设备树文件的版本(v1)

设备树语法包含头文件:
/include/ "xxxx.dtsi" //xxxx是你要包含的文件名称

通过c语言语法包含头文件,此法不止能包含dtsi文件还可以包含.h文件

不过需要注意的是`#include`是非设备树语法他是c语言语法,所以直接用dts编译是会报错的,我们需要先用cpp编译生成一个预编译文件,然后在用dtc编译这个预编译文件生成dtb,瑞芯微的设备树就是这么干的。

节点示例:
/dts-v1/;  // 设备树编译器版本信息

/ {  // 根节点
    node1 {  // 第一个子节点
        child_node {  // node1 的子节点
            // 空节点,没有定义任何属性或子节点
        };
    };

    node2 {  // 第二个子节点
        // 空节点,没有定义任何属性或子节点
    };
};

节点名通常全使用小写字母

在设备树的同一级别层次内,节点名称应唯一。

节点名称中可以包含节点所代表的硬件的地址信息和类型。

/dts-v1/;

/ {
    // 串口设备示例,地址不同
    serial@80000000 {
    };
    // 串口设备示例,地址不同
    serial@90000000 {
    };
    // I2C 控制器及设备示例
    i2c@91000000 {
    };
    // USB 控制器示例
    usb@92000000 {
    };
};

标签在节点名中不是必须的,但是我们可以通过他来更方便的操作节点,在设备树文件中有大量使用到。

/dts-v1/;

/ {
    // 串口设备示例,地址不同,uart1是标签
    uart1: serial@80000000 {
        node_add1{
        };
    };
    // 串口设备示例,地址不同,uart2是标签
    uart2: serial@90000000 {
    };
    // I2C 控制器及设备示例,i2c1是标签
    i2c1: i2c@91000000 {
    };
    // USB 控制器示例,USB是标签
    usb1: usb@92000000 {
    };
};

&uart1{ // 通过引用标签的方式往 serial@80000000 中追加一个节点非覆盖。
    node_add2{
    };
};

aliases是一种在设备树中提供简化标识符的方式。

设备树属性 属性值类型:

1. **字符串(String)**:

> - 属性名称:`compatible`
> - 示例值:`compatible = "lckfb,tspi-v10", "rockchip,rk3566";`
> - 描述:指定该设备或节点与哪些设备或驱动兼容。

2. **整数(Integer)**:

> - 属性名称:`reg`
> - 示例值:`reg = <0x1000>;`。
> - 描述:定义设备的物理地址和大小,通常用于描述内存映射的I/O资源。

3. **数组(Array)**:

> - 属性名称:`reg`
> - 示例值:`reg = <0x1000,0x10>;`。
> - 描述:定义设备的物理地址和大小,通常用于描述内存映射的I/O资源。

4. **列表(List)**:

> - 属性名称:`interrupts`
> - 示例值:`interrupts = <0 39 4>, <0 41 4>,<0 40 4>;`。
> - 描述:用于定义例如中断列表,其中每个元组可以表示不同的中断属性(如编号和触发类型)。

5. **空值(Empty)**:

> - 属性名称:`regulator-always-on;`
> - 示例值:`regulator-always-on;`
> - 描述:表示该节点下的regulator是永久开启的,不需要动态控制。

6. **引用(Reference)**:

> - 属性名称:`gpios`
> - 示例值:`gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>;`
> - 描述:提供一个句柄(通常是一个节点的路径或标识符),用于在其他节点中引用该节点。

compatible属性(字符串或字符串列表)

用于标识设备的兼容性字符串。操作系统使用这个属性来匹配设备与相应的驱动程序。

rk_headset: rk-headset {
    compatible = "rockchip_headset","rockchip_headset2";
};

耳机检测驱动中会通过“rockchip_headset”来匹配驱动kernel/drivers/headset_observe/rockchip_headset_core.c

.......
static const struct of_device_id rockchip_headset_of_match[] = {
    { .compatible = "rockchip_headset", },  // 定义设备树匹配项,指定兼容性字符串,与上面的设备树匹配
    {},                                     // 结束符号
};
MODULE_DEVICE_TABLE(of, rockchip_headset_of_match);  // 定义设备树匹配表供内核使用

static struct platform_driver rockchip_headset_driver = {
    .probe  = rockchip_headset_probe,   // 注册设备探测函数
    .remove = rockchip_headset_remove,  // 注册设备移除函数
    .resume = rockchip_headset_resume,  // 注册设备恢复函数
    .suspend =  rockchip_headset_suspend, // 注册设备挂起函数
    .driver = {
        .name   = "rockchip_headset",   // 设备名称
        .owner  = THIS_MODULE,          // 持有模块的指针
        .of_match_table = of_match_ptr(rockchip_headset_of_match),  // 设备树匹配表指针
    },
};
.........

 reg属性(地址,长度对)
 描述了设备的物理地址范围,包括基址与大小,与address-cells和size-cells结合使用
 
gmac1: ethernet@fe010000 {
    reg = <0x0 0xfe010000 0x0 0x10000>;
}

status属性(字符串)

设备树中其实修改的最大的就是打开某个节点或者关闭某个节点,status属性的值是字符串类型的,他可以有以下几个值,最常用的是okay和disabled。

- `"okay"`:表示设备是可操作的,即设备当前处于正常状态,可以被系统正常使用。
- `"disabled"`:表示设备当前是不可操作的,但在未来可能变得可操作。这通常用于表示某些设备(如热插拔设备)在插入后暂时不可用,但在驱动程序加载或系统配置更改后可能会变得可用。
- `"fail"`:表示设备不可操作,且设备检测到了一系列错误,且设备不太可能变得可操作。这通常表示设备硬件故障或严重错误。
- `"fail-sss"`:与 `"fail"` 含义相同,但后面的 `sss` 部分提供了检测到的错误内容的详细信息。

//用户三色灯
&leds {
    status = "okay";
};
//耳机插入检测,不使用扩展板情况需关闭,否则默认会检测到耳机插入
&rk_headset {
    status = "disabled";
};

device_type属性(字符串)

device_type属性通常只用于cpu节点或者memory节点.
device_type = "cpu";

----------------------------------------------------------------------------
屏幕选型与调试

VSYNC:垂直同步信号,此信号跳变时,显示器会开始显示新的一帧画面

VFP:垂直前肩期,一帧图片显示完成后要过多久才会来VSYNC信号

VBP:垂直后肩期

HSYNC:水平同步信号

HFP:水平前肩期
HBP:水平后肩期

最主要的参数是VSYNC和HSYNC这两个,他们控制了屏幕什么时候换行以及什么时候扫描完成一帧图片。

VFP和VBP以及HFP和HBP都是辅助这两个信号的,为什么会有这几个辅助信号?因为屏幕反应需要时间,当你给他发VSYNC和HSYNC信号以后他不是马上就开始下一行或者下一帧。

![[Pasted image 20241030105744.png]]

刷新率怎么计算?

刷新率 = 时钟 / ((总宽度Horizontal Total) * (总高度Vertical Total))

刷新率 = 时钟 / ((有效宽度+HSYNC宽+HBP+HFP) * (有效高度+VSYNC宽+VBP+VFP))

刷新率 = 72300000 / ((1366+32+160+48) * (768+6+32+3))

Rockchip_RK 3566_Datasheet

rk 3566 有两个输出通道:Video output 0 和 Video output 1

在 rk3566 平台的视频输出系统中,只有一个 VOP(Video Output Processor,视频输出处理器)。这个 VOP 被划分为不同的 PORT(端口),在这里有两个 PORT,被命名为 VP0 和 VP1。可以将 VP(这里应该是指 VP0 和 VP1)看作是从这个唯一的 VOP 划分出来的不同输出通道。
![[Pasted image 20241030110228.png]]

HDMI: High Definition Multimedia Interface,高清晰度多媒体接口

EDP 是嵌入式显示端口,具有高数据传输速率,高带宽,高分辨率,高刷新率,低电压,简化接口数量等特点。大多数笔记本电脑屏幕使用此类接口。

最大支持分辨率是主控决定的,通过数据手册可知RK3566的eDP接口最大支持 2560x1600@60Hz ,所以所选的eDP屏幕分辨率不大于这个分辨率就行。

屏幕连接使用注意问题:

  • 接口线序是否一样
  • 排线是正排线还是反排线
  • 电源电压/电流是否匹配
  • 背光供电电压是否匹配

引脚复用:允许一个物理引脚具有多种功能。GPIO 0_C 3_d 可以复用 PWM 4

时钟频率太低会导致闪屏;时钟频率太高会导致花屏

智能手机使用 mipi 接口的屏幕。EDP 屏幕只需要配置屏参,mipi 屏幕还需要发送初始化序列。泰山派通过 31 PIN 0.3 间距下接 FPC 接口引出。

泰山派最高支持 4 lanes 的 mipi 屏幕,分别对于下面的、lane0(20,21)、lane1(14,15)、lane2(11,12)、lane3(23,24)四对引脚,mipi屏幕lanes的多少跟屏幕的分辨率有直接关系,屏幕分辨率越高lanes数量就越多。

在 MIPI 屏幕技术中,“lanes”指的是数据通道的数量,它是 MIPI DSI(Display Serial Interface)接口中用来传输数据的通道。每个“lane”实际上包含两条线,一条用于传输数据(正极性信号链),另一条用于传输数据的互补信息(负极性信号链),这个可以提高信号的抗干扰能力。

屏幕大小不同对电流的要求不同,理解为大一点的屏幕里面的背光 LED 相对会多一点,需要查看屏幕的数据手册,它的背光典型电流是多少,如果背光驱动电流超出很多,屏幕背光发烫和烧背光的风险。

MIPI 初始化序列非常重要,随便一行或者一个值不对都可能导致没办法点亮屏幕。

序列格式

39 00 06 FF 77 01 00 00 11

39: 是 DCS Long Write 命令的操作码,表示这是一个长写命令
00: 通常表示发送延时,这里设置为 0,表示没有额外的延时
06: 表示后面跟随的参数个数,6 个字节
FF 77 01 00 00 11: 是实际的参数数据


15 00 02 E0 00

15:是DCS Short Write命令的操作码,表示这是一个短写命令
00:通常表示发送延时,这里设置为0,表示没有额外的延时
02:表示双字节数据,1 parameter
E0 00 实际的参数数据

转换前
{0xFF,3,{0x98,0x81,0x03}},//page3

[]:转换前为3个参数长度的数据所以我们需要用长写命令0x39;[]:下一行没有任何延时所以时间就是00ms;[]:地址+数据仪器总共有4个数据所以我们藏毒为4;[]:数据内容就是地址和数据一起

转换后
39 00 94 FF 98 81 03


转换前
{0 x 01,1,{0 x 00}},

[]:转换前为1个参数长度的数据所以我们需要用短写命令0x15 []:下一行没有任何延时所以时间就是00ms []:地址+数据一起总共有4个数据所以我们长度为2 []:数据内容就是地址和数据一起

转换后
15 00 02 01 00

整个触摸包含了 GPIO 驱动、中断驱动、IIC 驱动,Input 驱动等

泰山派通过6p 0.5mm的fpc引出触摸接口

线序

RK3566 FPC引脚 功能
GPIO_A1_u 触摸复位
--- VCC_3V3 3v3电源
--- GND
GPIO1_A0_u TP_INT_A 触摸中断引脚
GPIO0_B4_u I2C1_SDA_TP i2c数据线
GPIO0_B3_u I2C1_SCL_TP i2c时钟线

转接板绘制流程

放置器件;转PCB;连线;放置板框;地网络通过铺铜来连接;放置丝印;检查下单。

Makefile同目录下还有一个Kconfig,可以使用menuconfig工具来配置

make ARCH=arm64 menuconfig

make ARCH=arm64 savedefconfig

触摸节点

&i2c1 {
    status = "okay";
    ts@5d { //触摸子节点
        compatible = "goodix,gt9xx"; //这个非常重要,就是靠这个来匹配的驱动
        reg = <0x5d>;
        tp-size = <89>; //触摸大小
        max-x = <1280>; //屏幕最大值
        max-y = <800>;  //屏幕最小值
        //中断引脚
        touch-gpio = <&gpio1 RK_PA0 IRQ_TYPE_LEVEL_LOW>; //自定义属性
        //复位引脚
        reset-gpio = <&gpio1 RK_PA1 GPIO_ACTIVE_LOW>;
    };
};

因为一个i2c下可以挂载多个从设备,既然可以挂那么多设备,要如何和单个设备通讯?地址的作用就来了,通过reg的地址来指定通信的设备。

触摸屏提供一个中断引脚来提升数据读取效率。当屏幕被按下INT脚就会输出中断信号,泰山派配置了中断就可以实现中断来了以后再去读取触摸数据,提高了效率。

当有触摸时,GT9271每个扫描周期均会通过INT脚发出脉冲信号,通知主CPU读取坐标信息。主CPU可以通过相关的寄存器位“INT”来设置触发方式。设为“0”表示上升沿触发,即在有用户操作时,GT9271会在INT口输出上升沿跳变,通知CPU;设为“1”表示下降沿触发,即在有用户操作时,GT9271会在INT口输出下降沿跳变。

STM32中断优先级分为两个部分:抢占优先级和响应优先级。抢占优先级用于决定中断嵌套的规则,即在高抢占优先级的中断可以打断正在执行的低抢占优先级中断;响应优先级用于在抢占优先级相同的情况下,决定中断响应的顺序,响应优先级高的中断先被响应。

触摸驱动

#include <linux/kernel.h> 
#include <linux/hrtimer.h> 
#include <linux/i2c.h> 
#include <linux/input.h> 
#include <linux/module.h> 
#include <linux/delay.h> 
#include <linux/i2c.h> 
#include <linux/proc_fs.h> 
#include <linux/string.h> 
#include <linux/uaccess.h> 
#include <linux/vmalloc.h> 
#include <linux/interrupt.h> 
#include <linux/io.h> 
#include <linux/of_gpio.h> 
#include <linux/gpio.h> 
#include <linux/slab.h> 
#include <linux/timer.h> 
#include <linux/input/mt.h> 
#include <linux/random.h>

#if 1
#define MY_DEBUG(fmt,arg...) printk("MY_TOUCH:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);
#else
#define MY_DEBUG(fmt,arg...)
#endif

struct input_dev *input_dev;

static struct timer_list my_timer;

void my_timer_callback(struct timer_list *timer){
	unsigned int x,y;
	static bool isDown = false;

	get_random_bytes(&x,sizeof(x));
	x %= 1280;

	get_random_bytes(&y,sizeof(y));
	y %= 800;

	MY_DEBUG("isDown:%s x:%d y:%d!\n", isDown, x, y);

	//设定输入设备的触摸槽位
	input_mt_slot(input_dev,0);

	//报告输入设备的触摸槽位状态,MT_TOOL_FINGER表示手指状态,isDown表示是否按下
	input_mt_report_slot_state(input_dev,MT_TOOL_FINGER,isDown);

	isDown !isDown;

	input_report_abs(input_dev,ABS_MT_POSITION_X,x);
	input_report_abs(input_dev,ABS_MT_POSITION_Y,y);

	input_mt_report_pointer_emulation(input_dev,true);

	input_sync(input_dev);

	mod_timer(timer, jiffies + msecs_to_jiffies(200));

}

static int my_touch_ts_probe(struct i2c_client *client,const struct i2c_device_id *id){
	int ret;
	// print debug info
	MY_DEBUG("locat");
	//分配输入设备对象
	input_dev = devm_input_allocate_device(&client->dev);
	if(!input_dev){
		dev_err(&client->dev,"Failed to allocate input device.\n");
		return -ENOMEM;
	}
	//设置输入设备的名称和总线类型
	input_dev->name = "my touch screnn";
	input_dev->id.bustype = BUS_I2C;

	/*设置触摸 x和y 的最大值*/
	input_set_abs_params(input_dev,ABS_MT_POSITION_X,0,1280,0,0);
	input_set_abs_params(input_dev,ABS_MT_POSITION_Y,0,800,0,0);

	//初始化多点触摸设备的槽位
	ret = input_mt_init_slots(input_dev,5,INPUT_MT_DIRECT);
	if(ret){
		dev_err(&client->dev,"Input mt init error\n");
		return ret;
	}

	//注册输入设备
	ret = input_register_device(input_dev);
	if(ret)
		return ret;

	//init timer
	timer_setup(&my_timer,my_timer_callback,0);

	//设置定时器,5s后第一次触发
	mod_timer(&my_timer,jiffies + msecs_to_jiffies(5000));
	return 0;
}

static int my_touch_ts_remove(struct i2c_client *client){
	MY_DEBUG("locat");
	return 0;
}

static const struct of_device_id my_touch_of_match[] = {
	{.compatible = "my,touch",},
	{/* sentinel */}
}
MODULE_DEVICE_TABLE(of,my_touch_of_match);

static struct i2c_driver my_touch_ts_driver = {
	.probe = my_touch_ts_probe,
	.remove = my_touch_ts_remove,
	.driver = {
		.name = "my-touch",
		.of_match_table = of_match_ptr(my_touch_of_match),
	},
};

static int __init my_ts__init(void){
	MY_DEBUG("locat");
	return i2c_add_driver(&my_touch_ts_driver);
}

static void __exit my_ts_exit(void){
	MY_DEBUG("locat");
	i2c_del_driver(&my_touch_ts_driver);
}

module_init(my_ts_init);
module_exit(my_ts_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My touch driver");
MODULE_AUTHOR("jialongfei  longfei_jia@163.com");

问题:程序里面的__init和__exit是什么


__init:这个宏通常用于标记一个函数在模块初始化时执行特定的任务。在模块加载到内核时,带有`__init`标记的函数会被执行一次,以完成模块的初始化工作。

__exit:这个宏用于标记一个函数在模块卸载时执行清理工作。当模块从内核中卸载时,带有`__exit`标记的函数会被执行,以释放模块在运行期间占用的资源,进行清理和恢复操作。

驱动开发步骤

  1. 头文件包含

    包含必要的内核头文件是去哦的那个开发的第一步。这些头文件提供了访问内核功能的接口,如设备管理、中断处理、I2C通信、输入设备处理等

  • #include <linux/kernel.h>:提供了内核相关的基本功能,如打印函数printk
  • #include <linux/i2c.h>:用于 I2C 设备驱动开发,包括 I2C 通信相关的结构体和函数。
  • #include <linux/input.h>:用于处理输入设备相关的功能,如触摸输入设备的创建和事件报告。
  • 还有其他头文件,如linux/hrtimer.h(高精度定时器相关)、linux/module.h(用于模块管理)等,每个头文件都为驱动的不同功能部分提供支持。
  1. 调试宏定义

    • 定义调试宏可以方便地在开发过程中输出调试信息,并且可以通过简单的条件编译来控制调试信息的打印与否。
  • 在这个驱动中:
    • #if 1#else用于条件编译。当#if 1条件成立时,MY_DEBUG(fmt,arg...)会被定义为printk("MY_TOUCH:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);,这样就可以在代码中输出带有函数名和行号的调试信息。当#if 0时,MY_DEBUG宏为空,不输出调试信息。
  1. 设备驱动结构体和变量定义

    定义与设备相关的结构体和全局变量,这些结构体和变量将用于存储设备的状态信息、配置参数等。

struct input_dev *input_dev; //定义了一个输入设备结构体指针,用于后续创建和管理触摸输入设备
static struct timer_list my_timer; //定义了要给定时器结构体,用于模拟触摸事件的定时触发
  1. 探测函数(probe)定义

probe函数是设备驱动的核心部分之一。当内核检测到与驱动匹配的设备时,会调用这个函数进行设备的初始化和配置。

在触摸驱动中,my_touch_ts_probe函数中:

  • 先分配输入设备对象,使用devm_input_allocate_device函数。如果分配失败,返回错误。
  • 设置输入设备的名称和总线类型
  • 设置触摸坐标的最大值
  • 初始化多带你触摸设备的槽位
  • 注册输入设备,使用input_register_device函数。如果注册失败,返回错误
  • 初始化定时器,使用timer_setup函数设置定时器回调函数,再用mod_timer函数设置定时器在5s后第一次触发。
  1. 移除函数(remove)定义
  2. 设备匹配表和驱动结构体定义
  • 定义设备匹配表,用于告诉内核驱动支持的设备。
static const struct of_device_id my_touch_of_match[]是一个设备匹配表,其中包含了一个.compatible属性为“my,touch”的匹配项。这用于在设备树中查找与之匹配的设备。
  • MODULE_DEVICE_TABLE(of, my_touch_of_match);将设备匹配表注册到内核,使得内核能够根据设备树中的信息找到对应的驱动。
  • static struct i2c_driver my_touch_ts_driver是一个 I2C 驱动结构体,它包含了proberemove函数指针,以及驱动的名称和设备匹配表等信息。这个结构体用于将驱动注册到内核的 I2C 驱动框架中。
  1. 模块初始化和退出函数定义
  2. 模块许可证,描述和作者信息声明
```c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>

// 定义字符设备结构体
struct my_char_device {
    dev_t devno;         // 设备号
    struct cdev cdev;    // 字符设备结构
    int data;            // 设备的私有数据,可以用于存储设备相关的状态或其他信息
};

// 定义设备文件操作结构体
struct file_operations my_fops = {
  .owner = THIS_MODULE,
  .read = my_char_device_read,
  .write = my_char_device_write,
  .open = my_char_device_open,
  .release = my_char_device_release,
};

// 打开设备函数
static int my_char_device_open(struct inode *inode, struct file *filp)
{
    // 可以在这里进行设备打开时的初始化操作,例如获取设备的互斥锁等
    // 目前简单打印一条消息表示设备已打开
    printk(KERN_INFO "My char device opened.\n");
    return 0;
}

// 释放设备函数
static int my_char_device_release(struct inode *inode, struct file *filp)
{
    // 进行设备关闭时的清理操作,如释放资源、释放互斥锁等
    // 这里简单打印一条消息表示设备已关闭
    printk(KERN_INFO "My char device closed.\n");
    return 0;
}

// 读设备函数
static ssize_t my_char_device_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_char_device *dev = container_of(filp->private_data, struct my_char_device, cdev);
    int data_to_read = dev->data;

    // 将数据复制到用户空间
    if (copy_to_user(buf, &data_to_read, sizeof(int))) {
        return -EFAULT;
    }

    printk(KERN_INFO "Read from my char device: %d\n", data_to_read);
    return sizeof(int);
}

// 写设备函数
static ssize_t my_char_device_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_char_device *dev = container_of(filp->private_data, struct my_char_device, cdev);
    int data_to_write;

    // 从用户空间复制数据到内核空间
    if (copy_from_user(&data_to_write, buf, sizeof(int))) {
        return -EFAULT;
    }

    dev->data = data_to_write;
    printk(KERN_INFO "Written to my char device: %d\n", data_to_write);
    return sizeof(int);
}

// 初始化字符设备函数
static int __init my_char_device_init(void)
{
    int result;
    dev_t devno;

    // 动态分配设备号
    result = alloc_chrdev_region(&devno, 0, 1, "my_char_device");
    if (result < 0) {
        printk(KERN_ERR "Failed to allocate device number.\n");
        return result;
    }

    // 初始化字符设备结构体
    struct my_char_device *dev = kmalloc(sizeof(struct my_char_device), GFP_KERNEL);
    if (!dev) {
        printk(KERN_ERR "Failed to allocate memory for device structure.\n");
        unregister_chrdev_region(devno, 1);
        return -ENOMEM;
    }
    memset(dev, 0, sizeof(struct my_char_device));

    dev->devno = devno;
    cdev_init(&dev->cdev, &my_fops);
    dev->cdev.owner = THIS_MODULE;
    result = cdev_add(&dev->cdev, devno, 1);
    if (result < 0) {
        printk(KERN_ERR "Failed to add character device.\n");
        kfree(dev);
        unregister_chrdev_region(devno, 1);
        return result;
    }

    dev->data = 42; // 设置初始数据值

    printk(KERN_INFO "My char device driver initialized successfully.\n");
    return 0;
}

// 卸载字符设备函数
static void __exit my_char_device_exit(void)
{
    dev_t devno = MKDEV(major(MAJOR), 0);
    struct my_char_device *dev = container_of(cdev_find(devno, 0), struct my_char_device, cdev);

    cdev_del(&dev->cdev);
    kfree(dev);
    unregister_chrdev_region(devno, 1);

    printk(KERN_INFO "My char device driver unloaded successfully.\n");
}

module_init(my_char_device_init);
module_exit(my_char_device_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple character device driver");
MODULE_AUTHOR("Your Name");

驱动有了,就要修改设备树,加入对应的节点,属性,对应的设备树文件也要添加到对应目录下的Makefile中编译成dtb文件。

在kernel目录下执行编译命令:

make ARCH=arm64 -C . M=./drivers/input/touchscreen/my_touch

扩展板主要由接口转换电路、背光电路、音频电路构成。

接口转换电路:因为3.1寸屏幕的mipi接口和泰山派的mipi和触摸接口都不一样,所以需要设计扩展板使信号线能够对应。

3.1寸屏幕的分辨率为480x800,使用的是MIPI DSI接口,屏幕排线为24个引脚,其中4、5、9脚为空不需要接。

泰山派上的MIPI_DSI_VCC_LED+和MIPI_DSI_VCC_LED-是背光引脚,由泰山派上的板载背光电路输出,它的输出电流是110mA,而3.1寸屏幕最大能承受的驱动电路是25mA,所以不适合直接接到3.1寸屏幕的FPC上。

3.1寸触摸屏接口使用的是i2C协议与泰山派进行通讯,除了i2C外还有两个比较重要的引脚分别是触摸复位引脚和触摸中断触发引脚。

背光电路主要由背光选择电路、背光驱动电路以及背光调节电路构成。

背光选择电路分为两路:第一路是由泰山派输出;第二路是由板载的背光驱动输出,通过4个0欧姆电阻进行选择。
![[Pasted image 20241030231101.png]]

背光驱动电路由SY7201ABC实现。SY7201ABC是一款DC/DC升压转换器,主要用于为LED提供精确的恒定电流。SY7201ABC是一款高效率的LED驱动器,主要用于控制和调节LED灯的亮度。SY7201ABC通过提供恒定电流来确保LED发光的一致性和稳定性,从而提高了LED使用的寿命和效能。

电感电流不能突变,与电容电压不能突变

背光调节电路

泰山派没有PWM引脚引到3.1寸扩展板,但触摸接口由I2C1引到3.1寸扩展屏幕上,I2C可以挂载多个设备,所以为了能够实现背光调节功能,通过GP7101一个I2C转PWM的芯片来实现PWM的调节。GP7101和触摸一起挂到I2C1下。

音频接口

通过两个弹簧顶针(POGO PIN)与泰山派SPKP和SPKN连接,音频驱动电路由泰山派上的RK809-5实现。

通过一个弹簧顶针(POGO PIN)与泰山派MIC连接,MIC相关的驱动电路集成在了泰山派上。

叠层阻抗设计

做叠层阻抗匹配主要的目的是让信号完整性更好,一般当我们设计的板子上有高速信号存在的时候我们就需要去考虑信号的完整性。
我们3.1寸扩展屏幕上面有MIPI信号,MIPI属于高速信号但又因为我们这个屏幕尺寸很小速率也不高其实不做也没有关系。

单端6.16mil,mipi差分4.88mil,线间距8mil

线宽设置为15mil给到所有的电源网络使用

主设备号高12位,次设备号低20位

of_property_read_u32 是Linux内核中用于从设备树(Device Tree)中读取32位无符号整数属性值的函数。、

devm_backlight_device_register是Linux内核用于动态注册背光设备的一个函数。前缀带devm的一般都会在设备被销毁时自动释放相关资源,无需手动调用 backlight_device_unregister。

gp7101_backlight_set函数

这就是我们更新背光的核心函数了,每次背光被改动的时候系统都会回调这个函数,在函数中我们通过I2C1去写GP7101实现修改背光。 GP7101两种操作方法第一种是8位PWM,第二种是16位数PWM,刚好我们背光是从0~255所以,我们就选择8位PWM,八位PWM模式需要写寄存器0x03。

移植FreeRTOS:

源码下载;将source复制到工程文件夹下;source文件夹下包含了:
![[Pasted image 20241031113136.png]]

在poertable文件夹中保留KEIL,MemMang、和RVDS,其他删除。KEIL就是我们使用的编辑器,RVDS包含了各种处理器相关的文件夹,MemMang文件夹下存放跟内存管理相关的源文件。

添加heap4.c的内存管理方式

从demo中复制FreeRTOSConfig.h文件到工程头文件夹下

信号量

二进制信号量;计数信号量

互斥锁

事件组 xEventGroupCreate()函数创建事件组,xEventGroupSetBits()用于设置事件组中的位,xEventGroupWaitBits()用于等待事件组中的特定位组合。任务可以通过等待事件组中的位来实现同步,并且可以指定等待的方式。

队列

直接任务通知

MMU:内存管理单元

  • 完成虚拟空间到物理空间的映射
  • 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性

物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap

ioremap: 获取指定物理地址空间对应的虚拟地址空间
iounmap: 释放掉 ioremap 函数做的映射

项目总结

设备树目录地址:

SDK/kernel/arch/arm64/boot/dts/rockchip$

该目录涵盖了所有瑞芯微处理器和开发板的设备树,按照后续归类下来也就 dts、dtsi、dtb、Makefile 四类文件下面我们一一解读 DTSDTSIDTBMakefile。它们各有不同的作用,并且之间存在一定的关系。

  • DTS文件:是设备树的源文件,以 ".dts" 为后缀。它使用一种特定的语法,用于描述硬件设备及其相关信息,比如设备名称、类型、地址、中断等。DTS文件是人可读的文本文件,可以被处理器编译成DTB文件,它相当于C语言中的.c文件。

  • DTSI文件:是设备树源文件的包含文件,以".dtsi"为后缀。它用于存储一些常用的设备树片段,这些片段可以在多个DTS文件中重复使用,避免了代码重复。DTSI文件可以在DTS文件中进行包含(类似C语言的#include),以便重用其中定义的设备树片段。

  • DTB文件:是设备树的二进制文件,以".dtb"为后缀。它是通过将DTS文件编译而来的,使用了特定的编译器工具,比如dtc(Device Tree Compiler)。DTB文件是机器可读的二进制格式,可以被操作系统加载和解析,以用于设备驱动的配置和初始化。它相当于C语言中的.bin文件。

  • Makefile:是一个用于构建和编译项目的脚本文件。在设备树的上下文中,Makefile用于自动化编译和构建DTS/DTSI文件,生成对应的DTB文件。

具体如何加载设备树文件的?

  1. 引导加载程序阶段(以 U - Boot 为例)

    • U - Boot 的设备树支持机制:U - Boot 是一个广泛用于嵌入式系统的开源引导加载程序。它在启动过程中有对设备树文件的加载和处理功能。
    • 设备树文件的位置指定:在 U - Boot 的配置文件(通常是board/[board - name]/configs/[board - config - file].h)或者环境变量中,可以指定设备树文件的名称和存储位置。例如,通过设置环境变量fdtfile来指定设备树文件名,如setenv fdtfile rk3566 - my - board.dtb,这里假设设备树文件名为rk3566 - my - board.dtb,存储在 U - Boot 可以访问的存储设备(如 eMMC、SD 卡等)中。
    • 加载过程:当 U - Boot 启动时,它会根据配置或环境变量的设置,从指定的存储位置读取设备树文件。一般步骤如下:
      • 首先,U - Boot 会初始化存储设备的驱动,以便能够访问存储设备。例如,对于 eMMC 设备,它会初始化 eMMC 控制器,使其能够读取 eMMC 芯片中的数据。
      • 然后,通过文件系统驱动(如果设备树文件存储在文件系统中)或者直接存储设备访问(如果设备树文件存储在裸分区)找到设备树文件,并将其读取到内存中。这个内存区域通常是 U - Boot 预留的用于设备树加载的区域,一般在 DDR 内存中。
      • 最后,U - Boot 会对设备树文件进行一些初步的处理,例如检查文件格式是否正确,解析一些基本的节点和属性信息,为将设备树文件传递给内核做准备。
  2. 内核启动阶段

    • 内核参数传递:U - Boot 在启动内核时,会将设备树文件的内存地址作为内核启动参数之一传递给内核。这个参数通常是通过bootargs环境变量来设置的。例如,在 U - Boot 的命令行或者配置文件中,可以设置bootargs=earlyprintk console=ttyS0,115200 root=/dev/mmcblk0p2 rw rootwait fdtaddr=0x [device - tree - memory - address],其中fdtaddr参数指定了设备树文件在内存中的地址。
    • 内核的设备树初始化
      • 当内核启动后,在其初始化的早期阶段,会解析接收到的设备树文件。这个过程主要是由内核中的设备树解析代码完成的。内核会根据设备树文件中的信息构建内部的数据结构,用于表示硬件设备的拓扑结构和属性。
      • 例如,内核会解析设备树中的compatible属性来匹配相应的设备驱动程序。对于每个设备树节点,内核会检查其compatible属性与已注册的驱动程序的compatible列表是否匹配。如果匹配成功,内核会调用驱动程序的初始化函数,将设备树中该节点的相关属性传递给驱动程序,以便驱动程序可以根据这些属性来初始化硬件设备。
      • 同时,内核还会利用设备树中的资源信息(如寄存器地址、中断号等)来配置系统资源。例如,对于一个需要内存映射寄存器的设备,内核会根据设备树中的reg属性来设置内存映射,使得驱动程序可以访问设备的寄存器。
  3. 运行时动态加载(可选)

    • 动态加载机制概述:在某些情况下,可能需要在系统运行过程中动态加载设备树文件。这通常是用于热插拔设备或者更新硬件配置等场景。
    • 使用of_fdt接口:Linux 内核提供了of_fdt(Open Firmware Device Tree)相关的接口用于动态加载和处理设备树文件。例如,通过of_fdt_load()函数可以从指定的内存位置加载一个新的设备树文件,然后使用of_fdt_install()函数将其安装到内核的设备树数据结构中。不过,这种动态加载操作比较复杂,需要对内核的设备树机制有深入的了解,并且要谨慎处理,因为不当的操作可能会导致系统崩溃或者设备异常。

屏幕序列格式

瑞芯微屏幕序列格式如下:
【包类型】【发送延时】【数据长度】【MIPI 屏初始化数据 n 个】
39 00 06 FF 77 01 00 00 11

  • 39 是 DCS Long Write 命令的操作码,表示这是一个长写命令。
  • 00 通常表示发送延时,这里设置为 0,表示没有额外的延时。
  • 06 表示后面跟随的参数个数,这里是 6 个字节。
  • FF 77 01 00 00 11 是实际的参数数据。
    /
    屏厂给的参数:
[地址]   [长度]   [数据]
{0xFF,     3,  {0x98,0x81,0x03}},
{0x01,     1,  {0x00}},

[延时类型]        [延时时间]    [空值]
{REGFLAG_DELAY,     120,       {}},

转换前
{0xFF,3,{0x98,0x81,0x03}},//PAGE3

[包类型]:转换前为3个参数长度的数据所以我们需要用长写命令0x39

[发送延时]:下一行没有任何延时所以时间就是00ms

[数据长度]:地址+数据一起总共有4个数据所以我们长度为4

[MIPI屏初始化数据*n个]:数据内容就是地址和数据一起

转换后
39 00 04 FF 98 81 03

转换前
{0x01,1,{0x00}},

[包类型]:转换前为1个参数长度的数据所以我们需要用短写命令0x15

[发送延时]:下一行没有任何延时所以时间就是00ms

[数据长度]:地址+数据一起总共有4个数据所以我们长度为2

[MIPI屏初始化数据*n个]:数据内容就是地址和数据一起

转换后

15 00 02 01 00

触摸驱动包括了 GPIO 驱动、中断驱动、IIC 驱动、input 驱动等。

屏幕背光电流计算:
IOUT = 0.2 V/R
R = (R95xR96)/(R95+R96)

IOUT = 0.2V/1.8≈110mA

SY7201ABC到底输出多大的电流是由芯片的FB脚来决定的
8寸的mipi屏幕普遍是在130mA左右

触摸驱动的位置:

SDK/kernel/drivers/input/touchscreen

触摸 IC 是汇顶 GT 9271

汇顶相关驱动位于

SDK/kernel/drivers/input/touchscreen/gt9xx

Makefile 配置文件用来组织编译,gt 9 xx 目录下的 Makefile 如下:

#SPDX-License-Identifier: GPL-2.0# 使用 GPL-2.0 许可证声明

#将 goodix_gt9xx.o 目标文件添加到编译选项 obj-y 中
obj-y += goodix_gt9xx.o

#将 gt9xx.o 目标文件添加到 goodix_gt9xx-y 目标列表中,用于编译链接
goodix_gt9xx-y += gt9xx.o

#将 gt9xx_update.o 目标文件也添加到 goodix_gt9xx-y 目标列表中,用于编译链接
goodix_gt9xx-y += gt9xx_update.o

-y 编译进内核,-m 编译成模块

上层 touchscreen/下的 Makefile

#省略
obj-$(CONFIG_TOUCHSCREEN_GT9XX)     += gt9xx/
#省略

这里我们看到一个变量 CONFIG_TOUCHSCREEN_GT 9 XX,Makefile 中通过这个变量值来判定是否编译 gt 9 xx 的这个变量的值有三个 y、m、n 分别对应配置到内核,模块,不打开。

在同 Makefile 同目录下还有一个 Kconfig,有了这个就可以使用 menuconfig 工具来配置。

config TOUCHSCREEN_GT9XX
    tristate "Goodix gt9xx support for rockchip platform"
    depends on I2C && ARCH_ROCKCHIP
    help
      Say Y here if you have a touchscreen interface using the gt9xx
      on Rockchip platform, and your board-specific initialization
      code includes that in its table of IIC devices.
      If unsure, say N.

menuconfig

kernel 目录下运行

make ARCH=arm64 menuconfig

配置未生效原因:
保存的是. config 每次编译的时候脚本都会去组合生成. config,所以配置就会被覆盖掉。正确的方法是生成 defconfig,然后覆盖到之前的 arch/arm 64/configs/下代替使用的 defconfig

make ARCH=arm64 savedefconfig

配置设备树

泰山派触摸相关的设备树

SDK\kernel\arch\arm64\boot\dts\rockchip\tspi-rk3566-dsi-v10.dtsi
根据GT9xx_Driver_for_Android_V2.4_2014112801dtsi\goodix-gt9xx.dtsi参考修改

&i2c1 {
    status = "okay";
    ts@5d {
        compatible = "goodix,gt9xx";
        reg = <0x5d>;
        tp-size = <89>;
        max-x = <1280>;
        max-y = <800>;
        touch-gpio = <&gpio1 RK_PA0 IRQ_TYPE_LEVEL_LOW>;
        reset-gpio = <&gpio1 RK_PA1 GPIO_ACTIVE_LOW>;
    };
};

I2C1

//i2c1 节点追加
&i2c1 {
    status = "okay";
    /*加触摸*/
};

触摸节点

&i2c1 {
    status = "okay";
    ts@5d { //触摸子节点
        compatible = "goodix,gt9xx"; //这个非常重要,就是靠这个来匹配的驱动
        reg = <0x5d>;//触摸屏地址
        tp-size = <89>; //触摸大小 
        max-x = <1280>; //屏幕最大值 
        max-y = <800>; //屏幕最小值                    
    };
};

因为一个i2c下可以挂载多个从设备,既然可以挂那么多设备我们要怎么和单个设备通讯呢,地址的作用就来了,我想和谁通讯我就叫谁的地址。

IIC 寻址字节由七位地址位和一位方向位组成。

gt9xx. c 触摸部分代码

static struct of_device_id goodix_ts_dt_ids[] = {
    { .compatible = "goodix,gt9xx" },
    { }
};

static struct i2c_driver goodix_ts_driver = {
    .probe      = goodix_ts_probe,
    .remove     = goodix_ts_remove,
    .id_table   = goodix_ts_id,
    .driver = {
        .name     = GTP_I2C_NAME,
     .of_match_table = of_match_ptr(goodix_ts_dt_ids),
    },
};

读取数据方式采用中断方式读取。触摸屏提供了一个中断引脚,当屏幕被按下 INT 脚就会输出中断信号,泰山派配置了中断就可以实现中断来了以后再去读取触摸数据,提高了效率。

中断引脚使用的是 GPIO1_A0,对应&gpio1 RK_PA0,低电平触发 IRQ_TYPE_LEVEL_LOW
复位引脚使用的是 GPIO1_A1,对应&gpio1 RK_PA1,低电平有效 GPIO_ACTIVE_LOW

probe 函数是一个非常重要的回调函数,它在驱动加载时被内核调用,用于初始化设备驱动。

触摸驱动框架

/*
 * 有些同学会好奇我怎么知道包含哪些头文件,其实我也不是一个个去找到的,
 * 我直接把gt9xx那里复制过来的,后面缺少什么就在加什么
 * 这里是驱动所依赖的头文件,它们提供了驱动编写所需的各种函数和宏定义。
 */
#include <linux/kernel.h>       // 内核常用宏和函数
#include <linux/hrtimer.h>      // 高精度定时器
#include <linux/i2c.h>          // I2C总线支持
#include <linux/input.h>        // 输入设备支持
#include <linux/module.h>       // 模块支持
#include <linux/delay.h>        // 延时函数
#include <linux/proc_fs.h>      // /proc文件系统支持
#include <linux/string.h>       // 字符串操作
#include <linux/uaccess.h>      // 用户空间访问支持
#include <linux/vmalloc.h>      // 虚拟内存分配
#include <linux/interrupt.h>    // 中断处理
#include <linux/io.h>           // IO操作
#include <linux/of_gpio.h>      // Open Firmware GPIO支持
#include <linux/gpio.h>         // GPIO操作
#include <linux/slab.h>         // 内存分配(如kmalloc和kfree)

/*
 * 这里我简单的封装了一个打印函数,回打印对应的函数和行号,方便定位消息
 * MY_DEBUG宏用于调试,当#if 1为真时,它将使用printk打印调试信息,包括函数名和行号。
 */
#if 1
#define MY_DEBUG(fmt,arg...)  printk("MY_TOUCH:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);
#else
#define MY_DEBUG(fmt,arg...)  // 当#if 0时,MY_DEBUG不执行任何操作
#endif

/*
 * 探针函数
 * 这是I2C驱动的核心函数之一,当I2C总线上有新的设备被识别时,这个函数会被调用。
 */
static int my_touch_ts_probe(struct i2c_client *client,
            const struct i2c_device_id *id)
{
    MY_DEBUG("probe");  // 打印调试信息
    return 0;           // 返回0表示成功
}

/*
 * 移除函数
 * 当I2C设备从总线上移除时,这个函数会被调用。
 */
static int my_touch_ts_remove(struct i2c_client *client)
{
    MY_DEBUG("remove"); // 打印调试信息
    return 0;           // 返回0表示成功
}

/*
 * 设备匹配表
 * 用于匹配设备树中的设备节点。
 */
static const struct of_device_id my_touch_of_match[] = {
    { .compatible = "my,touch", },  // 兼容的设备树节点名称
    { /* sentinel */ }              // 列表结束标志
};
MODULE_DEVICE_TABLE(of, my_touch_of_match); // 注册设备匹配表

/*
 * I2C驱动结构体
 * 定义了I2C驱动的基本信息。
 */
static struct i2c_driver my_touch_ts_driver = {
    .probe      = my_touch_ts_probe,   // 探针函数  ,用于探测和初始化I2C设备
    .remove     = my_touch_ts_remove,  // 移除函数 ,用于清理和卸载I2C设备
    .driver = {
        .name     = "my-touch",         // 驱动名称
        .of_match_table = of_match_ptr(my_touch_of_match), // 设备树匹配表
    },
};

/*
 * 模块初始化函数
 * 当模块加载时,这个函数会被调用。
 */
static int __init my_ts_init(void)
{
    MY_DEBUG("init"); // 打印调试信息
    return i2c_add_driver(&my_touch_ts_driver); // 注册I2C驱动
}

/*
 * 模块退出函数
 * 当模块卸载时,这个函数会被调用。
 */
static void __exit my_ts_exit(void)
{
    MY_DEBUG("exit"); // 打印调试信息
    i2c_del_driver(&my_touch_ts_driver); // 注销I2C驱动
}

/*
 * 使用module_init和module_exit宏来指定模块的初始化和退出函数。
 * 当模块被加载时,my_ts_init函数会被调用;当模块被卸载时,my_ts_exit函数会被调用。
 */
module_init(my_ts_init);     // 模块初始化时调用my_ts_init函数
module_exit(my_ts_exit);     // 模块退出时调用my_ts_exit函数

/*
 * 模块的许可证、描述和作者信息
 * 这些信息会被内核用来标识和管理模块。
 */
MODULE_LICENSE("GPL");       // 模块使用GNU通用公共许可证
MODULE_DESCRIPTION("My touch driver");  // 模块描述
MODULE_AUTHOR("wucaicheng@qq.com");    // 模块作者

初始化触摸

上面框架我们搞定了接下来我们来模拟一个触摸上报触摸事件,其实这里涉及到input子系统但是没有关系,大家理解就是是调用api就行,套路就是申请驱动、配置参数、调用相关api上报数据,就和我们stm32的外设初始一样一样的。

分配输入设备

要用触摸不可能凭空就用吧,我们需要先进调用devm_input_allocate_device分配输入设备

shell

    input_dev = devm_input_allocate_device(&client->dev);
    if (!input_dev) {
        dev_err(&client->dev, "Failed to allocate input device.\n");
        return -ENOMEM;
    }

指定名字与设备类型
刚分配出来的没有名字,所以我们是不是要给取个名字,并指定一下设备类型

    input_dev->name = "my touch screen";
    input_dev->id.bustype = BUS_I2C;

在泰山派系统中可以通用 cat /proc/bus/input/devices 或者 getevent(安卓)等查看到 "my touch screen"

设置屏幕参数

设置一下我们屏幕的x和y的最大值和最小值

  • input_devstruct input_dev 结构的指针,表示要设置参数的输入设备。struct input_dev 包含了输入设备的各种属性和状态信息。
  • axis:表示要设置参数的轴。在你的示例中,ABS_MT_POSITION_X 表示设置 X 轴的绝对坐标参数。
  • min:表示轴的最小值,设置了 X 轴的最小值为0。
  • max:表示轴的最大值,设置了 X 轴的最大值为1280。
  • fuzz:表示轴的模糊度。模糊度用于指定轴值的偏差范围,设置了 X 轴的模糊度为0,表示轴值的偏差范围为0。
  • flat:表示轴的平坦度。平坦度用于指定轴值的线性度,设置了 X 轴的平坦度为0,表示轴值的线性度为0。

初始多点触摸

gt9xx这个款屏幕是支持多个点同时触摸的,大家平时手机截屏不要是几个手指头一起按下的这就是多点触摸,所以我们这里在通过 input_mt_init_slots 初始多点触摸。

ret = input_mt_init_slots(input_dev, 5, INPUT_MT_DIRECT);
    if (ret) {
        dev_err(&client->dev, "Input mt init error\n");
        return ret;
    }
  • input_dev: struct input_dev 结构的指针,表示要设置参数的输入设备。struct input_dev 包含了输入设备的各种属性和状态信息。
  • num_slots:这里设置成5个点,
  • flags:INPUT_MT_DIRECT为触摸设备,当然还可选其他值比如INPUT_MT_POINTER表示指针设备

注册输入设备

ret = input_register_device(input_dev);
    if (ret)
        return ret;

注销输入设备

static int my_touch_ts_remove(struct i2c_client *client)
{
    MY_DEBUG("locat");
    input_unregister_device(input_dev);
    return 0;
}

状态
接下来我们要告诉系统,我们是按下触摸还是抬起触摸。

//按下
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, true);
//松开
input_mt_report_slot_state(input_dev, MT_TOOL_FINGER, false);
  • input_devstruct input_dev 结构的指针,表示要设置参数的输入设备。struct input_dev 包含了输入设备的各种属性和状态信息。
  • tool_type:表示触摸点的工具类型。MT_TOOL_FINGER 这里表示手指头。
  • active:这是一个布尔值参数,用于指示触摸点的状态。如果 active 为 true,表示触摸点处于活跃状态;如果 active 为 false,表示触摸点处于非活跃状态。

坐标

如果是按下我们就上传坐标信息

    input_report_abs(input_dev, ABS_MT_POSITION_X, x);
    input_report_abs(input_dev, ABS_MT_POSITION_Y, y);

启用指针仿真

该模式允许将多点触控事件转换为鼠标或指针事件

input_mt_report_pointer_emulation(input_dev, true);

同步事件

确保之前通过input_report_absinput_mt_report_slot_state等函数报告的所有事件都被同步到输入子系统,并且作为一个完整的事件集进行处理。没有调用input_sync之前,这些事件是挂起的,不会被系统或用户空间的应用程序看到。

input_sync(input_dev);

定时器初始化

//定时器回调函数
void my_timer_callback(struct timer_list *timer){
    //为了实现重复
    mod_timer(timer, jiffies + msecs_to_jiffies(200));
}

// 初始化定时器
timer_setup(&my_timer, my_timer_callback, 0);
//修改定时触发时间这里是5s后会触发并调用回调函数
// 设置定时器,5 秒后第一次触发
mod_timer(&my_timer, jiffies + msecs_to_jiffies(5000));

//删除
del_timer_sync(&mmy_timer);

实现自己的触摸驱动

#include "linux/stddef.h"
#include <linux/kernel.h>
#include <linux/hrtimer.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/input/mt.h>
#include <linux/random.h>

#define MY_SWAP(x, y)                 do{\
                                         typeof(x) z = x;\
                                         x = y;\
                                         y = z;\
                                       }while (0)

#if 1
#define MY_DEBUG(fmt,arg...)  printk("MY_TOUCH:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);
#else
#define MY_DEBUG(fmt,arg...)
#endif

struct my_touch_dev {
    struct i2c_client *client;
    struct input_dev *input_dev;
    int rst_pin;
    int irq_pin;
    u32 abs_x_max;
    u32 abs_y_max;
    int irq;
};

s32 my_touch_i2c_read(struct i2c_client *client,u8 *addr,u8 addr_len, u8 *buf, s32 len)
{
    struct i2c_msg msgs[2];
    s32 ret=-1;
    msgs[0].flags = !I2C_M_RD;
    msgs[0].addr  = client->addr;
    msgs[0].len   = addr_len;
    msgs[0].buf   = &addr[0];
    msgs[1].flags = I2C_M_RD;
    msgs[1].addr  = client->addr;
    msgs[1].len   = len;
    msgs[1].buf   = &buf[0];

    ret = i2c_transfer(client->adapter, msgs, 2);
    if(ret == 2)return 0;

    if(addr_len == 2){
        MY_DEBUG("I2C Read: 0x%04X, %d bytes failed, errcode: %d! Process reset.", (((u16)(addr[0] << 8)) | addr[1]), len, ret);
    }else {
        MY_DEBUG("I2C Read: 0x%02X, %d bytes failed, errcode: %d! Process reset.", addr[0], len, ret);
    }

    return -1;
}

s32 my_touch_i2c_write(struct i2c_client *client, u8 *addr, u8 addr_len, u8 *buf,s32 len)
{
    struct i2c_msg msg;
    s32 ret = -1;
    u8 *temp_buf;

    msg.flags = !I2C_M_RD;
    msg.addr  = client->addr;
    msg.len   = len+addr_len;

    temp_buf= kzalloc(msg.len, GFP_KERNEL);
    if (!temp_buf){
        goto error;
    }

    // 装填地址
    memcpy(temp_buf, addr, addr_len);
    // 装填数据
    memcpy(temp_buf + addr_len, buf, len);
    msg.buf = temp_buf;

    ret = i2c_transfer(client->adapter, &msg, 1);
    if (ret == 1) {
        kfree(temp_buf);
        return 0;
    }

error:
    if(addr_len == 2){
        MY_DEBUG("I2C Read: 0x%04X, %d bytes failed, errcode: %d! Process reset.", (((u16)(addr[0] << 8)) | addr[1]), len, ret);
    }else {
        MY_DEBUG("I2C Read: 0x%02X, %d bytes failed, errcode: %d! Process reset.", addr[0], len, ret);
    }
    if (temp_buf)
        kfree(temp_buf);
    return -1;
}

static irqreturn_t my_touch_irq_handler(int irq, void *dev_id)
{
    s32 ret = -1;
    struct my_touch_dev *ts = dev_id;
    u8 addr[2] = {0x81,0x4E};
    u8 clear_buf[1] = {0};
    u8 point_data[1+8*1]={0};//1个状态位置+10个触摸点,一个点是8个数据组成
    u8 touch_num = 0;
    u8 buf_stats = 0;
    u8 *coor_data;
    int id,input_x,input_y,input_w;

    MY_DEBUG("irq");

    ret = my_touch_i2c_read(ts->client, addr,sizeof(addr), point_data, sizeof(point_data));
    if (ret < 0){
        MY_DEBUG("I2C write end_cmd error!");
    }

    touch_num = point_data[0]&0x0f;
    buf_stats = point_data[0]&0x80>>7;

    MY_DEBUG("0x814E=:%0x,touch_num:%d,buf_stats:%d",point_data[0],touch_num,buf_stats);
    //获取
    if (touch_num){
        coor_data = &point_data[1];

        id = coor_data[0] & 0x0F;
        input_x  = coor_data[1] | (coor_data[2] << 8);
        input_y  = coor_data[3] | (coor_data[4] << 8);
        input_w  = coor_data[5] | (coor_data[6] << 8);
        MY_DEBUG("id:%d,x:%d,y:%d,w:%d",id,input_x,input_y,input_w);
        //     // 设定输入设备的触摸槽位
        input_mt_slot(ts->input_dev, 0);

        // 报告输入设备的触摸槽位状态,MT_TOOL_FINGER 表示手指状态,isDown 表示是否按下
        input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, true);

        // 翻转 isDown 的值模仿手抬起和按下
        MY_SWAP(input_x, input_y);
        // 报告输入设备的绝对位置信息:x、y 坐标,触摸面积,触摸宽度
        input_report_abs(ts->input_dev, ABS_MT_POSITION_X, 800-input_x);
        input_report_abs(ts->input_dev, ABS_MT_POSITION_Y, input_y);

    }else {
        input_mt_report_slot_state(ts->input_dev, MT_TOOL_FINGER, false);
    }

    // 清除寄存器,要不然回反复触发
    ret = my_touch_i2c_write(ts->client,  addr,sizeof(addr), clear_buf, sizeof(clear_buf));
    if (ret < 0){
        MY_DEBUG("I2C write end_cmd error!");
    }

    // 报告输入设备的指针仿真信息
    input_mt_report_pointer_emulation(ts->input_dev, true);

    // 同步输入事件
    input_sync(ts->input_dev);

    return IRQ_HANDLED;
}

s32 gt9271_read_version(struct i2c_client *client)
{
    s32 ret = -1;
    u8 addr[2] = {0x81,0x40};
    u8 buf[6] = {0};

    ret = my_touch_i2c_read(client, addr,sizeof(addr), buf, sizeof(buf));
    if (ret < 0){
        MY_DEBUG("GTP read version failed");
        return ret;
    }

    if (buf[5] == 0x00){
        MY_DEBUG("IC Version: %c%c%c_%02x%02x", buf[0], buf[1], buf[2], buf[5], buf[4]);
    }
    else{
        MY_DEBUG("IC Version: %c%c%c%c_%02x%02x", buf[0], buf[1], buf[2], buf[3], buf[5], buf[4]);
    }
    return ret;
}

static int my_touch_ts_probe(struct i2c_client *client,
            const struct i2c_device_id *id)
{
    int ret;
    struct my_touch_dev *ts;
    struct device_node *np = client->dev.of_node;
    // 打印调试信息
    MY_DEBUG("locat");

    // ts = kzalloc(sizeof(*ts), GFP_KERNEL);
    ts = devm_kzalloc(&client->dev, sizeof(*ts), GFP_KERNEL);
    if (ts == NULL){
        dev_err(&client->dev, "Alloc GFP_KERNEL memory failed.");
        return -ENOMEM;
    }
    ts->client = client;
    i2c_set_clientdata(client, ts);

    if (of_property_read_u32(np, "max-x", &ts->abs_x_max)) {
        dev_err(&client->dev, "no max-x defined\n");
        return -EINVAL;
    }
    MY_DEBUG("abs_x_max:%d",ts->abs_x_max);

    if (of_property_read_u32(np, "max-y", &ts->abs_y_max)) {
        dev_err(&client->dev, "no max-y defined\n");
        return -EINVAL;
    }
    MY_DEBUG("abs_x_max:%d",ts->abs_y_max);

    //找复位gpio
    ts->rst_pin = of_get_named_gpio(np, "reset-gpio", 0);
    //申请复位gpio
    ret = devm_gpio_request(&client->dev,ts->rst_pin,"my touch touch gpio");
    if (ret < 0){
        dev_err(&client->dev, "gpio request failed.");
        return -ENOMEM;
    }

    //找中断引进
    ts->irq_pin = of_get_named_gpio(np, "touch-gpio", 0);
    /* 申请使用管脚 */
    ret = devm_gpio_request_one(&client->dev, ts->irq_pin,
                GPIOF_IN, "my touch touch gpio");
    if (ret < 0)
        return ret;

    gpio_direction_output(ts->rst_pin,0);
    msleep(20);
    gpio_direction_output(ts->irq_pin,0);
    msleep(2);
    gpio_direction_output(ts->rst_pin,1);
    msleep(6);
    gpio_direction_output(ts->irq_pin, 0);
    gpio_direction_output(ts->irq_pin, 0);
    msleep(50);

    //申请中断
    ts->irq = gpio_to_irq(ts->irq_pin);
    if(ts->irq){
        ret = devm_request_threaded_irq(&(client->dev), ts->irq, NULL,
            my_touch_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT ,
            client->name, ts);
        if (ret != 0) {
            MY_DEBUG("Cannot allocate ts INT!ERRNO:%d\n", ret);
            return ret;
        }
    }

    // 分配输入设备对象
    ts->input_dev = devm_input_allocate_device(&client->dev);
    if (!ts->input_dev) {
        dev_err(&client->dev, "Failed to allocate input device.\n");
        return -ENOMEM;
    }

    // 设置输入设备的名称和总线类型
    ts->input_dev->name = "my touch screen";
    ts->input_dev->id.bustype = BUS_I2C;

    /*设置触摸 x 和 y 的最大值*/
    // 设置输入设备的绝对位置参数
    input_set_abs_params(ts->input_dev, ABS_MT_POSITION_X, 0, 800, 0, 0);
    input_set_abs_params(ts->input_dev, ABS_MT_POSITION_Y, 0, 1280, 0, 0);

    // 初始化多点触摸设备的槽位
    ret = input_mt_init_slots(ts->input_dev, 5, INPUT_MT_DIRECT);
    if (ret) {
        dev_err(&client->dev, "Input mt init error\n");
        return ret;
    }

    // 注册输入设备
    ret = input_register_device(ts->input_dev);
    if (ret)
        return ret;

    gt9271_read_version(client);


    return 0;
}

static int my_touch_ts_remove(struct i2c_client *client)
{
    struct my_touch_dev *ts = i2c_get_clientdata(client);
    MY_DEBUG("locat");
    input_unregister_device(ts->input_dev);
    return 0;
}

static const struct of_device_id my_touch_of_match[] = {
    { .compatible = "my,touch", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_touch_of_match);

static struct i2c_driver my_touch_ts_driver = {
    .probe      = my_touch_ts_probe,
    .remove     = my_touch_ts_remove,
    .driver = {
        .name     = "my-touch",
     .of_match_table = of_match_ptr(my_touch_of_match),
    },
};

static int __init my_ts_init(void)
{
    MY_DEBUG("locat");
    return i2c_add_driver(&my_touch_ts_driver);
}

static void __exit my_ts_exit(void)
{
    MY_DEBUG("locat");
    i2c_del_driver(&my_touch_ts_driver);
}

module_init(my_ts_init);
module_exit(my_ts_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My touch driver");
MODULE_AUTHOR("wucaicheng@qq.com");

驱动开发设计到触摸驱动和 GP7101 背光驱动

再tspi-rk3566-dsi-v10. dtsi 中添加 GP7101 相关设备树驱动,首先引用 I2C1 并往设备树I2C1节点中添加GP7101子节点并指定I2C地址、最大背光,默认背光等。

&i2c1 {              // 引用名为i2c1的节点
    status = "okay"; // 状态为"okay",表示此节点是可用和配置正确的
    GP7101@58 {      // 定义一个子节点,名字为GP7101,地址为58
        compatible = "gp7101-backlight";   // 该节点与"gp7101-backlight"兼容,
        reg = <0x58>;                      // GP7101地址0x58
        max-brightness-levels = <255>;     // 背光亮度的最大级别是255
        default-brightness-level = <100>;  // 默认的背光亮度级别是100
    };
};

背光驱动都放在/kernel/drivers/video/backlight 目录下,创建 my_gp7101 目录来存放 Makefile 和 gp7101_bl. c 文件

目前下 Makefile:

obj -y += gp7101_bl.o

上一级 Makefile 添加

obj -y += my_gp7101_bl/

驱动程序如下:

#include "linux/stddef.h"
#include <linux/kernel.h>
#include <linux/hrtimer.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/input/mt.h>
#include <linux/random.h>

#if 1
#define MY_DEBUG(fmt,arg...)  printk("gp7101_bl:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);
#else
#define MY_DEBUG(fmt,arg...)
#endif

#define BACKLIGHT_NAME "gp7101-backlight"

static int gp7101_bl_probe(struct i2c_client *client,
            const struct i2c_device_id *id)
{
    MY_DEBUG("locat");
    return 0;
}

static int gp7101_bl_remove(struct i2c_client *client)
{
    MY_DEBUG("locat");
    return 0;
}

static const struct of_device_id gp7101_bl_of_match[] = {
    { .compatible = BACKLIGHT_NAME, },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gp7101_bl_of_match);

static struct i2c_driver gp7101_bl_driver = {
    .probe      = gp7101_bl_probe,
    .remove     = gp7101_bl_remove,
    .driver = {
        .name     = BACKLIGHT_NAME,
     .of_match_table = of_match_ptr(gp7101_bl_of_match),
    },
};

static int __init my_init(void)
{
    MY_DEBUG("locat");
    return i2c_add_driver(&gp7101_bl_driver);
}

static void __exit my_exit(void)
{
    MY_DEBUG("locat");
    i2c_del_driver(&gp7101_bl_driver);
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My touch driver");
MODULE_AUTHOR("wucaicheng@qq.com");

驱动过程中会有很多参数,不可能创建全局变量去保存它们,在 Linux 驱动中一般都是通过创建一个结构体来保存驱动相关的参数。

/* 背光控制器设备数据结构 */
struct gp7101_backlight_data {
    /* 指向一个i2c_client结构体的指针*/
    struct i2c_client *client;
    /*......其他成员后面有用到再添加........*/
};

当驱动中 of_match_table = of_match_ptr (gp7101_bl_of_match) 和设备树匹配成功以后会执行探针函数,探针函数中我们会去初始化驱动。

// gp7101_bl_probe - 探测函数,当I2C总线上的设备与驱动匹配时会被调用
static int gp7101_bl_probe(struct i2c_client *client,
            const struct i2c_device_id *id)
{
    struct backlight_device *bl; // backlight_device结构用于表示背光设备
    struct gp7101_backlight_data *data; // 自定义的背光数据结构
    struct backlight_properties props; // 背光设备的属性
    struct device_node *np = client->dev.of_node; // 设备树中的节点

    MY_DEBUG("locat"); // 打印调试信息

    // 为背光数据结构动态分配内存
    data = devm_kzalloc(&client->dev, sizeof(struct gp7101_backlight_data), GFP_KERNEL);
    if (data == NULL){
        dev_err(&client->dev, "Alloc GFP_KERNEL memory failed."); // 内存分配失败,打印错误信息
        return -ENOMEM; // 返回内存分配错误码
    }

    // 初始化背光属性结构
    memset(&props, 0, sizeof(props));
    props.type = BACKLIGHT_RAW; // 设置背光类型为原始类型
    props.max_brightness = 255; // 设置最大亮度为255

    // 从设备树中读取最大亮度级别
    of_property_read_u32(np, "max-brightness-levels", &props.max_brightness);

    // 从设备树中读取默认亮度级别
    of_property_read_u32(np, "default-brightness-level", &props.brightness);

    // 确保亮度值在有效范围内
    if(props.max_brightness>255 || props.max_brightness<0){
        props.max_brightness = 255;
    }
    if(props.brightness>props.max_brightness || props.brightness<0){
        props.brightness = props.max_brightness;
    }

    // 注册背光设备
    bl = devm_backlight_device_register(&client->dev, "backlight", &client->dev, data, &gp7101_backlight_ops,&props);
    if (IS_ERR(bl)) {
        dev_err(&client->dev, "failed to register backlight device\n"); // 注册失败,打印错误信息
        return PTR_ERR(bl); // 返回错误码
    }
    data->client = client; // 保存i2c_client指针
    i2c_set_clientdata(client, data); // 设置i2c_client的客户端数据

    MY_DEBUG("max_brightness:%d brightness:%d",props.max_brightness, props.brightness); // 打印最大亮度和当前亮度
    backlight_update_status(bl); // 更新背光设备的状态

    return 0; // 返回成功
}

devm_backlight_device_register,是 Linux 内核中用于动态注册背光设备的一个函数。前缀带 devm 的一般都会在设备被销毁时自动释放相关资源,无需手动调用 backlight_device_unregister。这个函数的主要作用是创建并注册一个 backlight_device 实例,这个实例代表了系统中的一个背光设备。背光设备通常用于控制显示屏的亮度。函数原型如下:

struct backlight_device *devm_backlight_device_register(
    struct device *dev, const char *name, struct device *parent,
    void *devdata, const struct backlight_ops *ops,
    const struct backlight_properties *props);

参数说明:

  • dev:指向父设备的指针,通常是一个 struct i2c_client 或 struct platform_device
  • name:背光设备的名称。
  • parent:背光设备的父设备,通常与 dev 参数相同。
  • devdata:私有数据,会被传递给背光操作函数。
  • ops:指向 backlight_ops 结构的指针,这个结构定义了背光设备的行为,包括设置亮度、获取亮度等操作。
  • props:指向 backlight_properties 结构的指针,这个结构包含了背光设备的属性,如最大亮度、当前亮度等。

gp7101_backlight_ops结构体

ops参数非常重要,因为我们就是通过这个参数指向的结构成员中的函数去实现获取背光更新背光的。函数的原型如下:

struct backlight_ops {
    unsigned int options;

#define BL_CORE_SUSPENDRESUME   (1 << 0)

    /* Notify the backlight driver some property has changed */
    int (*update_status)(struct backlight_device *);
    /* Return the current backlight brightness (accounting for power,
       fb_blank etc.) */
    int (*get_brightness)(struct backlight_device *);
    /* Check if given framebuffer device is the one bound to this backlight;
       return 0 if not, !=0 if it is. If NULL, backlight always matches the fb. */
    int (*check_fb)(struct backlight_device *, struct fb_info *);
};

通过 backlight_ops 定义了一个名为 gp7101_backlight_opsbacklight_ops 结构体实例,并且只初始化了 .update_status 成员,它指向了一个名为 gp7101_backlight_set 的函数,这个函数负责更新背光设备的亮度状态。

static struct backlight_ops gp7101_backlight_ops = {
    .update_status = gp7101_backlight_set,
};

gp7101_backlight_set 函数

更新背光的核心函数了,每次背光被改动的时候系统都会回调这个函数,在函数中我们通过I2C1去写GP7101实现修改背光。 GP7101两种操作方法第一种是8位PWM,第二种是16位数PWM,刚好我们背光是从0~255所以,我们就选择8位PWM,八位PWM模式需要写寄存器0x03。

![[Pasted image 20241129185512.png]]

/* I2C 背光控制器寄存器定义 */
#define BACKLIGHT_REG_CTRL_8  0x03
#define BACKLIGHT_REG_CTRL_16 0x02
/* 设置背光亮度 */
static int gp7101_backlight_set(struct backlight_device *bl)
{
    struct gp7101_backlight_data *data = bl_get_data(bl);  // 获取背光数据结构指针
    struct i2c_client *client = data->client;  // 获取I2C设备指针
    u8 addr[1] = {BACKLIGHT_REG_CTRL_8};  // 定义I2C地址数组
    u8 buf[1] = {bl->props.brightness};  // 定义数据缓冲区,用于存储背光亮度值

    MY_DEBUG("pwm:%d", bl->props.brightness);  // 输出背光亮度值

    // 将背光亮度值写入设备
    i2c_write(client, addr, sizeof(addr), buf, sizeof(buf));

    return 0;  // 返回成功
}

屏幕参数调试

  1. 修改 lanes 数
    3.1寸屏幕硬件上只用了2lanes的差分对,设备树中默认配置的是4lanes所以我们需要把lanes修改为2

  2. 配置初始化序列
    初始化序列是参考3.1寸屏幕厂商给的修改过来的

  3. 配置屏幕时序
    屏幕时序一般根据数据手册和厂家给的参考得来,以下是针对3.1寸屏幕修改好的参数屏幕时序一般根据数据手册和厂家给的参考得来,以下是针对3.1寸屏幕修改好的参数

初始化序列如下:

//3.1寸
panel-init-sequence = [
    // init code
    05 78 01 01
    05 78 01 11
    39 00 06 FF 77 01 00 00 11
    15 00 02 D1 11
    15 00 02 55 B0 // 80 90 b0
    39 00 06 FF 77 01 00 00 10
    39 00 03 C0 63 00
    39 00 03 C1 09 02
    39 00 03 C2 37 08
    15 00 02 C7 00 // x-dir rotate 0:0x00,rotate 180:0x04
    15 00 02 CC 38
    39 00 11 B0 00 11 19 0C 10 06 07 0A 09 22 04 10 0E 28 30 1C
    39 00 11 B1 00 12 19 0D 10 04 06 07 08 23 04 12 11 28 30 1C
    39 00 06 FF 77 01 00 00 11 // enable  bk fun of  command 2  BK1
    15 00 02 B0 4D
    15 00 02 B1 60 // 0x56  0x4a  0x5b
    15 00 02 B2 07
    15 00 02 B3 80
    15 00 02 B5 47
    15 00 02 B7 8A
    15 00 02 B8 21
    15 00 02 C1 78
    15 00 02 C2 78
    15 64 02 D0 88
    39 00 04 E0 00 00 02
    39 00 0C E1 01 A0 03 A0 02 A0 04 A0 00 44 44
    39 00 0D E2 00 00 00 00 00 00 00 00 00 00 00 00
    39 00 05 E3 00 00 33 33
    39 00 03 E4 44 44
    39 00 11 E5 01 26 A0 A0 03 28 A0 A0 05 2A A0 A0 07 2C A0 A0
    39 00 05 E6 00 00 33 33
    39 00 03 E7 44 44
    39 00 11 E8 02 26 A0 A0 04 28 A0 A0 06 2A A0 A0 08 2C A0 A0
    39 00 08 EB 00 01 E4 E4 44 00 40
    39 00 11 ED FF F7 65 4F 0B A1 CF FF FF FC 1A B0 F4 56 7F FF
    39 00 06 FF 77 01 00 00 00
    15 00 02 36 00 //U&D  Y-DIR rotate 0:0x00,rotate 180:0x10
    15 00 02 3A 55
    05 78 01 11
    05 14 01 29
];

屏幕时序如下:

disp_timings1: display-timings {
    native-mode = <&dsi1_timing0>;
    dsi1_timing0: timing0 {
        clock-frequency = <27000000>;
        hactive = <480>;       //与 LCDTiming.LCDH 对应
        vactive = <800>;       //与 LCDTiming.LCDV 对应
        hfront-porch = <32>;   //与 LCDTiming.HFPD 对应
        hsync-len = <4>;       //与 LCDTiming.HSPW 对应
        hback-porch = <32>;    //与 LCDTiming.HBPD 对应
        vfront-porch = <9>;    //与 LCDTiming.VEPD 对应
        vsync-len = <4>;       //与 LCDTiming.VsPW 对应
        vback-porch = <3>;     //与 LCDTiming.VBPD 对应
        hsync-active = <0>;
        vsync-active = <0>;
        de-active = <0>;
        pixelclk-active = <0>;
    };
};

嵌入式linux驱动-LCD液晶屏驱动

HBP、HFP、VBP 和 VFP 导致了黑边。
因为 RGB LCD屏幕内部是有一个 IC 的,发送一行或者一帧数据给 IC, IC 是需要反应时间的。

![[Pasted image 20241129190513.png]]

裸机可以随意的分配内存。Linux 系统—内存管理很严格,显存是需要申请的。

如何管理这个 LCD 设备?

fb 机制:Framebuffer (帧缓冲),将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一个 fb 设备。会生成一个名为 /dev/fbX(X=0~n) 的设备,应用程序通过访问 /dev/fbX 这个设备就可以访问 LCD。

Linux 内核中已经写好了关于 Framebuffer 的 LCD 驱动,我们主要是修改设备树。

裸机开发应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片等信息。在裸机中我们可以随意的分配显存,但是在 Linux 系统中内存的管理很严格,显存是需要申请的,不是你想用就能用的。而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存要是同一片物理内存。

为了解决上述问题,Framebuffer 诞生了, Framebuffer 翻译过来就是帧缓冲,简称 fb,因此大家在以后的 Linux 学习中见到“Framebuffer”或者“fb”的话第一反应应该想到 RGBLCD 或者显示设备。

fb 是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一 个 fb 设备,当我们编写好 LCD 驱动以后会生成一个名为/dev/fbX(X=0~n)的设备,应用程序通 过访问/dev/fbX 这个设备就可以访问 LCD。

posted @ 2025-03-13 17:18  longfei_jia  阅读(261)  评论(0)    收藏  举报