(基于三星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类函数也可以实现该效果。