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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐