Linux Arm上的RGB液晶屏驱动开发遇到的问题总结

xiaoxiao2021-03-01  13

(基于三星Exynos 4412 / iTop4412精英版开发板)

1. 开发前准备和内核编译

将Linux内核iTop4412_Kernel_3.0_20180508.tar.gz复制到虚拟机,解压。

进入解压后的文件夹,使用命令cp config_for_linux_scp_elite .config覆盖配置文件。

执行make zImage编译内核。

注意:

​ 此处的编译是必须的。否则在下面仅编译模块的时候会报错。

2. 第一个最简驱动

2.1 驱动代码

mini_linux_module.c:

#include <linux/init.h> //init.h - 包含初始化宏定义的头文件,代码中的函数module_init和module_exit在此文件中 #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); //本定义必须,声明GPL协议 MODULE_AUTHOR("TOPEET"); //声明代码/驱动作者,懒得改了 static int hello_init(void) { printk(KERN_EMERG "HELLO WORLD enter!\n"); //向内核打印信息,KERN_EMERG为优先级最高的打印信息,LVL:0 //默认级别为4 return 0; } static void hello_exit(void) { printk(KERN_EMERG "HELLO WORLD exit!\n"); } module_init(hello_init); //驱动加载(insmod)时执行的函数,参数为初始化函数的函数指针 module_exit(hello_exit); //驱动卸载(rmmod)时执行的函数,参数为驱动卸载函数的函数指针

可通过修改/proc/sys/kernel/printk来修改printk打印日志的等级。

文件内容及含义如下:

[root@iTOP-4412]# cat /proc/sys/kernel/printk 7 4 1 7

控制台日志级别:优先级高于该值的消息将被打印至控制台

默认的消息日志级别:将用该优先级来打印没有优先级的消息

最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)

默认的控制台日志级别:控制台日志级别的缺省值

数值越小,优先级越高

2.2 Makefile

#!/bin/bash #通知编译器我们要编译模块的哪些源码 #这里是编译itop4412_hello.c这个文件编译成中间文件itop4412_hello.o obj-m += mini_linux_module.o #源码目录变量,这里用户需要根据实际情况选择路径 #注意:这里的源码目录下的源码必须编译过一次! KDIR := /home/clair/iTop4412_Kernel_3.0 #当前目录变量 PWD ?= $(shell pwd) #make命名默认寻找第一个目标 #make -C就是指调用执行的路径 #$(KDIR)Linux源码目录,这里指的是/home/clair/iTop4412_Kernel_3.0 #$(PWD)当前目录变量 #modules要执行的操作 all: make -C $(KDIR) M=$(PWD) modules #make clean执行的操作是删除后缀为o的文件 clean: rm -rf *.o

2.3 编译&加载驱动模块

将mini_linux_module.c和Makefile放在同一个目录下,make clean;make进行编译。

生成的文件如下所示:

其中,ko文件是需要加载的内核驱动模块文件。

编译时会自动生成mini_linux_module.mod.c文件。

将ko文件复制到开发板,使用insmod装载驱动,可以看到执行了驱动中的函数:

lsmod查看已装载模块:

rmmod卸载模块:

卸载模块时可能会出现错误,解决方法如图所示,根据提示新建缺少的目录即可。

3. 设备驱动的注册

首先,需要进入内核的设备平台文件(此处为iTop4412_Kernel_3.0/arch/arm/mach-exynos/mach-itop4412.c)

添加自己的设备驱动的platform_device结构体(这里跳过了KConfig和Menuconfig相关的设置):

struct platform_device s3c_device_gpio_rgb_ctl = { .name = "rgb_gpio_ctl", .id = -1, };

注意:这里的.name与驱动的platform_driver.driver.name必须相同。

之后在同一个文件的结构体static struct platform_device *smdk4x12_devices[] __initdata的定义中添加上述结构体的引用如下:

static struct platform_device *smdk4x12_devices[] __initdata = { ... #ifdef CONFIG_LEDS_CTL &s3c_device_leds_ctl, #endif &s3c_device_gpio_rgb_ctl, //add here #ifdef CONFIG_BUZZER_CTL &s3c_device_buzzer_ctl, #endif }

编译内核,生成zImage并烧录zImage镜像。

同理,在自己编写的驱动中的设备名称也要定义为相同名称:

#define DRIVER_NAME "rgb_gpio_ctl" //这里的name必须和内核中注册的名字一样!!! ... struct platform_driver hello_driver = { .probe = hello_probe, //insmod,设备注册匹配成功后,自动执行该函数 .remove = hello_remove, //rmmod执行后,设备反注册成功后执行 .shutdown = hello_shutdown, .suspend = hello_suspend, .resume = hello_resume, .driver = { .name = DRIVER_NAME, //这里的name必须和内核中注册的名字一样!!! .owner = THIS_MODULE, } };

在驱动的init函数和exit函数里,增加对设备节点的注册和反注册:

static int hello_init(void) { int DriverState; printk(KERN_EMERG "HELLO WORLD enter!\n"); DriverState = platform_driver_register(&hello_driver); //注册设备节点 printk(KERN_EMERG "\tDriverState is %d\n",DriverState); return 0; } static void hello_exit(void) { printk(KERN_EMERG "HELLO WORLD exit!\n"); platform_driver_unregister(&hello_driver); //反注册设备节点 } module_init(hello_init); module_exit(hello_exit);

注册和反注册使用函数platform_driver_register和platform_driver_unregister实现。

编译ko后insmod,可以看到Linux已经把启动匹配成功了,并且执行了驱动中的.probe对应的hello_probe函数:

static int hello_probe(struct platform_device *pdv){ printk(KERN_EMERG "\tinitialized\n"); return 0; }

注意:

​ insmod时,Linux会将要加载的驱动的platform_driver.driver.name和内核设备平台文件中所有注册的platform_device结构体的.name进行匹配,并且仅在匹配成功的情况下,才执行.probe指向的函数。

​ 在本例中,若不修改内核,则insmod之后(准确点说是insmod之后,通过platform_driver_register函数注册驱动时),系统无法匹配设备,会出现成功insmod但是不执行probe函数的情况。

4. Misc device(杂项设备)的设备节点

杂项设备能够提供一个设备节点(设备文件)供操作系统访问,以实现对设备的操作。

在程序中定义file_operations结构体和对应函数,以声明该设备文件对应的操作:

static long hello_ioctl( struct file *files, unsigned int cmd, unsigned long arg){ printk("cmd is %d,arg is %d\n",cmd,arg); return 0; } static int hello_release(struct inode *inode, struct file *file){ printk(KERN_EMERG "hello release\n"); return 0; } static int hello_open(struct inode *inode, struct file *file){ printk(KERN_EMERG "hello open\n"); return 0; } static struct file_operations hello_ops = { .owner = THIS_MODULE, .open = hello_open, //使用open打开设备文件时执行的操作 .release = hello_release, //使用close函数关闭设备文件执行的操作 .unlocked_ioctl = hello_ioctl, //使用ioctl操作设备文件时执行的操作 };

声明结构体后,在probe和remove函数里对节点进行注册(misc_register)和反注册(misc_deregister):

static int hello_probe(struct platform_device *pdv){ printk(KERN_EMERG "\tinitialized\n"); misc_register(&hello_dev); //杂项设备节点注册 return 0; } static int hello_remove(struct platform_device *pdv){ printk(KERN_EMERG "\tremove\n"); misc_deregister(&hello_dev); //杂项设备节点反注册 return 0; }

5. GPIO的操作

5.1 从电路图分析GPIO相关信息

底板原理图上找LCD相关的部分,确定引脚网络标号。

在对应的核心板原理图上查看对应的GPIO口和GPIO口供电电压:

通过GPIO口供电电压为VDDQ_LCD可以找到对应的电压控制引脚是电源芯片的VDDIOAP_18:

搜索可知,VDDIOAP_18的电压由电压芯片的VLDO3决定。

因此,若修改电源芯片的VLDO3电压,就可以实现调整输出电平。

实际测试时,修改Kernel下的相关内容会出错,因此只能使用电平转换芯片74ALVC164245实现1.8V到3.3V的转换。

使用该芯片时,Vccb**必须**要大于Vcca。

5.2 GPIO口相关操作

5.2.1 GPIO口的定义

GPIO口相关的定义在arch\arm\mach-exynos\include\mach\gpio-exynos4.h下。

各GPIO口的定义对应关系如下:

例如,对应F2_0的GPIO的宏定义是:EXYNOS4_GPF2(0)

5.2.2 GPIO请求和释放

要操作GPIO,首先需要在系统中对GPIO进行申请,这样就可以阻止其他驱动重复使用GPIO。

申请的函数如下:

int gpio_request(unsigned gpio, const char *label)

第一个参数是GPIO,第二个参数是给这个占用起的名字。

例如:

static int rgb_lcd_gpios[] = { EXYNOS4_GPF2(0), EXYNOS4_GPF1(7), EXYNOS4_GPF1(3), EXYNOS4_GPF1(2), EXYNOS4_GPF1(1), EXYNOS4_GPF1(0), EXYNOS4_GPF0(2), EXYNOS4_GPF0(1), EXYNOS4_GPF2(1), EXYNOS4_GPF2(2), EXYNOS4_GPF2(7), EXYNOS4_GPF3(0), EXYNOS4_GPF3(1) }; //... gpio_request(rgb_lcd_gpios[i], "LED");

返回值为0则操作成功。

在不使用GPIO时,需要进行释放。

使用函数gpio_free实现:

for(i = 0; i < LED_NUM; i++) { gpio_free(rgb_lcd_gpios[i]); }

5.2.3 GPIO的方向、输出、读取、上下拉

GPIO的输入输出、模式选择使用s3c_gpio_cfgpin设置。

例如,将F2_0设置为输出:s3c_gpio_cfgpin(EXYNOS4_GPF2(0), S3C_GPIO_OUTPUT);

相关的选项定义如下:

#define S3C_GPIO_SPECIAL_MARK (0xfffffff0) #define S3C_GPIO_SPECIAL(x) (S3C_GPIO_SPECIAL_MARK | (x)) /* Defines for generic pin configurations */ #define S3C_GPIO_INPUT (S3C_GPIO_SPECIAL(0)) #define S3C_GPIO_OUTPUT (S3C_GPIO_SPECIAL(1)) #define S3C_GPIO_SFN(x) (S3C_GPIO_SPECIAL(x)) #define s3c_gpio_is_cfg_special(_cfg) \ (((_cfg) & S3C_GPIO_SPECIAL_MARK) == S3C_GPIO_SPECIAL_MARK)

GPIO的输出电平使用函数gpio_set_value控制。

例如:gpio_set_value(EXYNOS4_GPF2(0), 1);输出高电平。

GPIO读取使用函数gpio_get_value实现。

例如:int gpio = gpio_get_value(EXYNOS4_GPF2(0))

GPIO的上下拉配置使用s3c_gpio_setpull实现。

例如设置为无上下拉:s3c_gpio_setpull(DATA_PORT[i],S3C_GPIO_PULL_NONE);

相关定义:

#define S3C_GPIO_PULL_NONE ((__force s3c_gpio_pull_t)0x00) #define S3C_GPIO_PULL_DOWN ((__force s3c_gpio_pull_t)0x01) #define S3C_GPIO_PULL_UP ((__force s3c_gpio_pull_t)0x02)

6. 延时

Linux中提供了常用的延时函数:(忙等待)

包含头文件:

#include <linux/delay.h>

秒级延时:delay(x)

毫秒延时:mdelay(x)

微秒延时:udelay(x)

纳秒级别延时ndelay(x)和平台头文件相关,一般包含在include/asm-???/delay.h中。

除了delay类之外,还有sleep类函数也可以实现该效果。

转载请注明原文地址: https://www.6miu.com/read-3650286.html

最新回复(0)