(基于三星Exynos 4412 / iTop4412精英版开发板)
将Linux内核iTop4412_Kernel_3.0_20180508.tar.gz复制到虚拟机,解压。
进入解压后的文件夹,使用命令cp config_for_linux_scp_elite .config覆盖配置文件。
执行make zImage编译内核。
注意:
此处的编译是必须的。否则在下面仅编译模块的时候会报错。
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控制台日志级别:优先级高于该值的消息将被打印至控制台
默认的消息日志级别:将用该优先级来打印没有优先级的消息
最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)
默认的控制台日志级别:控制台日志级别的缺省值
数值越小,优先级越高
将mini_linux_module.c和Makefile放在同一个目录下,make clean;make进行编译。
生成的文件如下所示:
其中,ko文件是需要加载的内核驱动模块文件。
编译时会自动生成mini_linux_module.mod.c文件。
将ko文件复制到开发板,使用insmod装载驱动,可以看到执行了驱动中的函数:
lsmod查看已装载模块:
rmmod卸载模块:
卸载模块时可能会出现错误,解决方法如图所示,根据提示新建缺少的目录即可。
首先,需要进入内核的设备平台文件(此处为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函数的情况。
杂项设备能够提供一个设备节点(设备文件)供操作系统访问,以实现对设备的操作。
在程序中定义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; }底板原理图上找LCD相关的部分,确定引脚网络标号。
在对应的核心板原理图上查看对应的GPIO口和GPIO口供电电压:
通过GPIO口供电电压为VDDQ_LCD可以找到对应的电压控制引脚是电源芯片的VDDIOAP_18:
搜索可知,VDDIOAP_18的电压由电压芯片的VLDO3决定。
因此,若修改电源芯片的VLDO3电压,就可以实现调整输出电平。
实际测试时,修改Kernel下的相关内容会出错,因此只能使用电平转换芯片74ALVC164245实现1.8V到3.3V的转换。
使用该芯片时,Vccb**必须**要大于Vcca。
GPIO口相关的定义在arch\arm\mach-exynos\include\mach\gpio-exynos4.h下。
各GPIO口的定义对应关系如下:
例如,对应F2_0的GPIO的宏定义是:EXYNOS4_GPF2(0)
要操作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]); }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)Linux中提供了常用的延时函数:(忙等待)
包含头文件:
#include <linux/delay.h>秒级延时:delay(x)
毫秒延时:mdelay(x)
微秒延时:udelay(x)
纳秒级别延时ndelay(x)和平台头文件相关,一般包含在include/asm-???/delay.h中。
除了delay类之外,还有sleep类函数也可以实现该效果。