乐趣区

Linux内核调用SPI驱动实现OLED显示功能

0. 导语

进入 Linux 的世界,发现真的是无比的有趣,也发现搞 Linux 驱动从底层嵌入式搞起真的是很有益处。我们在单片机、DSP 这些无操作系统的裸机中学习了这些最基本的驱动,然后用过 GPIO 时序去模拟、然后用那个芯片平台的外设去配置参数,到 Linux 的世界,对于底层的时序心中有数,做起来就容易很多。学习的过程就是不断的给自己出难题,然后去解决他,在未来工程里面遇到这个问题,就瞬间可以解决了,这就是经验的积累吧。

Linux 驱动目录,包含了底层写好的 SPI 驱动,我们需要想办法调用人家写好的 SPI 驱动,就不需要写 IO 口模拟 SPI 时序了。在网络上,对于 SPI 应用级的驱动倒是很多,平台级驱动很少,而我们想把平台级驱动二次包装在我们的字符设备驱动中,对于用户,无需考虑 SPI 通信写协议还是写命令,只需要使用 read 和 write 函数写显示的内容就好了。

基于这样的想法,我们找了一个使用 SPI 协议的从器件来实现,我手里面有 OLED 设备,是支持 SPI 协议在 OLED 显示面板上显示字符的。所以搭建一个实验平台,做一个 OLED 的 demo,未来所有的从 SPI 设备都遵循这个框架(而且我们在这个驱动中加入 了内核机制的驱动的自旋锁、互斥体的内核操作)。

实验平台如下:

  • ARM 板子: 友善之臂 Nano-T3(CortexA53 架构, Samsung s5c6818)
  • ARM 的 Linux 系统:Ubuntu 16.04.2 LTS
  • 编译调试 Linux:Ubuntu 16.04.3 LTS amd64 版本
  • 编译器:arm-cortexa9-linux-gnueabihf-gcc(64 位版本)
  • 从设备:OLED(SPI 模式)

1. 驱动架构模型

总体驱动架构模型如图所示,对于 OLED 驱动的表述,主要包含两个方面,一个是 OLED 这个传感器的抽象;一个是,misc 字符驱动的注册,里面有 read 和 write 函数,供用户接口调用,(在 read 和 write 函数里面使用 OLED 设备表述里面的 master 控制 oled 的行为就好了,比如显示,清除,复位之类的)。

oled 设备表述,为 OLED 设备的抽象,里面包含对硬件的描述和 SPI 的描述,还有对于写时序的时候使用自旋锁和互斥体对时序进行的保护,master 为对 oled 设备的基本操作,包含复位,写字节等等。

在本博客中最重要的就是 SPI 平台驱动的使用,问题也非常的清晰,我们如何使用 linux 内核驱动里面写好的 spi,参考 Linux SPI API 文档里面,那么复杂的结构体,哪些是在驱动中要使用,哪些是在应用级程序中使用的。网络上的资料大部分都是应用级的,没有讲述在字符驱动中二级注册 spi 驱动的,而我们对于 OLED 这样的 SPI 设备,则需要在驱动中调用,让用户无需关心任何 SPI 的调用。

在驱动模型中,master 操作结构体里面,oled_write_byte 这样的函数里面则需要调用系统级 SPI,问题就非常明确,就在写 byte 的时候使用 SPI。

那么我们就需要在注册完字符设备的时候,向内核注册 spi,然后我们使用该 SPI 对 OLED 操作。

2. linux SPI 驱动的注册

Linux Drivers 目录具备一定的通用性也具备各个架构区别不同,在包含头文件的时候,要包含

  1. 通用性的 linux spi 文件 #include <linux/spi/spi.h>
  2. mach 级特性文件#include <mach/slsi-spi.h>

同时也要关注:

  1. plat 级的 device.c 文件,里面包含了 spi_board 信息的模板,用这个可以省去了很多麻烦。

我们使用的 oled_hw_t,图上的结构(OLED->hw)的具体定义,里面定义了 io 口的编号和 spi 的各种机制,注意谁是指针,谁是实体。

struct oled_hw_t 
{
    unsigned int res_io_num;
    unsigned int dc_io_num;
    struct spi_transfer        spi_trans;
    struct spi_message        spi_msg;
    struct spi_driver        *spi_drv;
    struct spi_device        *spi_dev;
    struct spi_master        *spi_master_bus;
};

我需要定义以下机制:

  • spi_driver

    spi_driver 会向内核申请总线处理的权限,当我们加载驱动的时候,在 ARM 机器的 linux 上的 /sys/bus/spi/drivers 目录下会看到申请 SPI 驱动内核的名字。

    static const struct spi_device_id oled_spi_id[] =
    {{“oledspi”, 1},
            {},};
    static struct spi_driver sp6818_spi_driver = 
    {
            .driver             =     
            {
                    .name        =    "oled_spi",
                    .bus        =    &spi_bus_type,
                    .owner      =     THIS_MODULE,
            },
            .probe                =    oled_bus_spi_probe,
            .remove             =     __devexit_p(oled_bus_spi_remove),
            .suspend             =     oled_bus_spi_suspend,
            .id_table            =    oled_spi_id,
    };
    MODULE_DEVICE_TABLE(spi, oled_spi_id);

    按照 spi_driver 驱动的格式进行,补充好 probe 和 remove,suspend 函数,但是这里存在一个问题,当我们应该 spi_register_driver 的时候,正常应该执行 probe 函数里面的内容,但是这个不执行,怀疑是因为二级包装问题,我们的主调还是使用 misc 驱动的字符设备 __init 标示在 misc 的初始化函数上,而导致不进入 spi_driver 的 probe 函数。

  • spi_device

    spi_device 和 spi_driver 是成对出现的,在 spi_driver 注册完之后,则需要对 spi_deivce 进行配置,我们首先要声明一个 spi_device,一会儿借助 linux 的 drivers 里面的 platform 级的 deivce.c 文件中的 spi_board 来注册我们的 spi_device。

    定义 spi_device 驱动,这里面的配置信息可以瞎填,我们使用 spi_board 中的配置信息会覆盖这些信息。

    static struct spi_device sp6818_spi_device = 
    {
            .mode                =    SPI_MODE_3,
            .bits_per_word        =    16,
            .chip_select        =    SPI_CS_HIGH,
            .max_speed_hz        =    100000,
    };

    然后现在的工作就是如何 spi_device 和我们刚才 spi_driver 进行绑定了。

    定义下面的信息:

    static struct s3c64xx_spi_csinfo sp6818_csi = 
    {
            .line               =     OLED_CS_IO,
            .set_level          =     gpio_set_value,
            .fb_delay           =     0x2,
    };
            
    struct spi_board_info sp6818_board_info = 
    {
            .modalias           =     "oled",
            .platform_data      =     NULL,
            .max_speed_hz       =     10 * 1000 * 1000,
            .bus_num            =     0,
            .chip_select        =     2,
            .mode               =     SPI_MODE_3,
            .controller_data    =     &sp6818_csi,
    };

    这个模板就定义在 platform 级文件夹的 device.c 里面,我们按照模板的定义方式在我们的驱动文件里面也定义一个,在 s3c64xx_spi_csinfo sp6818_csi 中定义的是片选信号的 IO 口,这个 IO 口根据硬件原理图来的,然后定义 spi_board_info 结构体,这些都是为 spi_device 做准备的,spi 的配置信息也由此写入。

    按照这个顺序进行:程序就如同下面的参考,后面会给出完成程序。

    static void oled_module_hw_init(OLED *self)
    {
        int ret,i;
        struct spi_master *master;
        struct spi_device *spi;
    
        self->hw.res_io_num = OLED_RES_IO;
        self->hw.dc_io_num    = OLED_DC_IO;
        printk(DRV_NAME "\tregister spi driver...\n");
        self->hw.spi_drv = &sp6818_spi_driver;
        ret = spi_register_driver(self->hw.spi_drv);
        if (ret < 0) {printk( DRV_NAME "\terror: spi driver register failed");
        }
        printk(DRV_NAME "\tmaster blind spi bus.\n");
        master = spi_busnum_to_master(0);
        master->num_chipselect = 4;
        if (!master) {printk( DRV_NAME "\terror: master blind spi bus.\n");
            ret = -ENODEV;
            return ret;
        }
        printk(DRV_NAME "\tnew spi device...\n");
        spi =    spi_new_device(master, &sp6818_board_info);
        if (!spi) {printk( DRV_NAME "\terror: spi occupy.\n");        
            return -EBUSY;
        }
        self->hw.spi_master_bus    = master;
        self->hw.spi_dev = spi;
        printk(DRV_NAME "\thw init succussful...\n");
    }

到此,完成,spi 的注册。

spi_device 的注册里面,会在 ARM 上面的 Linux 的 /sys/bus/spi/devices 下面出现我们注册的 device 设备,如图:

spi0.2 就是我们所注册的 device 设备,这个命名就和我们的 spi_board_info 有关系了,

如果,bus_num = 5, chip_select = 20,那么注册的 device 就是 spi5.20 了。这里还有个坑,就是片选信号的数值大小和 master 里面的片选 num 的问题,linux 的 spi api 要求,master 的 num-chipselect 必须大于 spi_board_info 里面 chip_select 的数值。你也看到上面初始化程序,为什么master->num_chipselect = 4; 这个语句了

3. SPI 的使用

在驱动里面对于 spi 的使用就非常简单了。例如我们 oled 的 write_byte 函数:

static void oled_module_write_byte( OLED* self,                \ 
                                    unsigned int dat,         \
                                    enum data_type_t type)
{
    int status;
    unsigned int write_buffer[1];

    if (type == ENUM_WRITE_TYPE_CMD) 
        self->master->set_dc_low(self);
    else 
        self->master->set_dc_high(self);
    write_buffer[0] = dat;
    write_buffer[1] = 0xFF;
    status = spi_write(self->hw.spi_dev, write_buffer, 1);
    if (status)
        dev_err(&self->hw.spi_dev->dev, "%s error %d\n", __FUNCTION__, status);
}

使用 spi_write 函数就好了。

4. 结语

探索 Linux SPI 真是很费劲,这些花了好多时间,经历了无数次的实验,因为是驱动,经常在调试过程中出现暴栈、指针乱指,这些对于 Linux 内核都是毁灭性的错误,只能重启 ARM Linux。光重启 Linux 就好几百次。不过总算是有成果,对于 Linux 驱动的学习还在进行,下次可能要实验 I2C 的平台驱动,找到规律和不同,再加上一些内核的操作,比如并发和 IO 等,在学习中成长。

源代码

Github 地址:https://github.com/lifimlt/ca…

见 oled.c oled.h 和 oledfont.h 三个文件

参考文献:

[1] Linux org, Serial Peripheral Interface (SPI),

[2] 郝过, Linux 设备驱动模型 SPI 之二, 2016 年 2 月 28 日

[3] invo-tronics , SPI Driver for Linux Based Embedded System, 2014 年 9 月 30 日

[4] Linux 学习之路, spi 驱动框架全面分析,从 master 驱动到设备驱动, 2016 年 6 月 22 日

退出移动版