赞
踩
本章我们以spi 接口的oled 显示屏为例讲解spi 驱动程序的编写。
本章主要分为五部分内容。
STM32MP157 的spi 总线都可以挂载多个设备,spi 支持标准的一主多从,全双工半双工通信等。
其中四根控制线包括:
i2c 通过i2c 设备地址选择通信设备,而spi 通过片选引脚选中要通信的设备。
STM32MP157 的spi 接口支持有多个片选引脚,连接多个SPI 从设备,当然也可以使用外部GPIO扩展SPI 设备的数量,这样一个spi 接口可连接的设备数由片选引脚树决定。
通常情况下无特殊要求我们使用spi 接口提供的片选引脚。
起始信号:NSS 信号线由高变低
停止信号:NSS 信号由低变高
数据传输:在SCK 的每个时钟周期MOSI 和MISO 同时传输一位数据,高/低位传输没有硬性规定
– 传输单位:8 位或16 位
– 单位数量:允许无限长的数据传输
总线空闲时SCK 的时钟状态以及数据采样时刻
时钟极性CPOL:指SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号:
– CPOL=0 时,SCK 在空闲状态时为低电平
– CPOL=1 时,SCK 在空闲状态时为高电平
时钟相位CPHA:数据的采样的时刻:
– CPHA=0 时,数据在SCK 时钟线的“奇数边沿”被采样
– CPHA=1 时,数据在SCK 时钟线的“偶数边沿”被采样
如上图所示:
SCK 信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。CPHA=0,数据在SCK 时钟线的“奇数边沿”被采样,当CPOL=0 的时候,时钟的奇数边沿是上升沿,当CPOL=1的时候,时钟的奇数边沿是下降沿。
spi 设备驱动和i2c 设备驱动非常相似,可对比学习。这一小节主要介绍spi 驱动框架以及主要的结构体。
如框架图所示,spi 可分为spi 总线驱动和spi 设备驱动。spi 总线驱动已经由芯片厂商提供,我们适当了解其实现机制。而spi 设备驱动由我们自己编写,则需要明白其中的原理。spi 设备驱动涉及到字符设备驱动、SPI 核心层、SPI 主机驱动,具体功能如下。
这里对整个spi 驱动框架所涉及的关键数据结构进行整理,可先跳过,后续代码中遇到这些数据结构时再回来看详细定义。
spi_master 会在SPI 主机驱动中使用到。spi_controller 实际是一个宏,指向spi_controller 结构体。
部分成员变量已经被省略,下面是spi_controller 关键成员变量:
列表1: spi_controller 结构体(内核源码/include/linux/spi/spi.h)
struct spi_controller { struct device dev; ... struct list_head list; s16 bus_num; u16 num_chipselect; ... struct spi_message *cur_msg; ... int (*setup)(struct spi_device *spi); int (*transfer)(struct spi_device *spi, struct spi_message *mesg); void (*cleanup)(struct spi_device *spi); struct kthread_worker kworker; struct task_struct *kworker_task; struct kthread_work pump_messages; struct list_head queue; struct spi_message *cur_msg; ... int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi,struct spi_transfer *transfer); int (*prepare_transfer_hardware)(struct spi_controller *ctlr); int (*transfer_one_message)(struct spi_controller *ctlr,struct spi_message *mesg); void (*set_cs)(struct spi_device *spi, bool enable); ... int *cs_gpios; }
spi_controller 中包含了各种函数指针,这些函数指针会在SPI 核心层中被使用。
列表2: spi_driver 结构体(内核源码/include/linux/spi/spi.h)
struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
void (*shutdown)(struct spi_device *spi);
struct device_driver driver;
};
可以看到spi 设备驱动结构体和我们之前讲过的i2c 设备驱动结构体i2c_driver 、平台设备驱动结构体platform_driver 拥有相同的结构,用法也相同。
在spi 驱动中一个spi 设备结构体代表了一个具体的spi 设备,它保存着这个spi 设备的详细信息,也可以说是配置信息。当驱动和设备匹配成功后(例如设备树节点)我们可以从.prob 函数的参数中得到spi_device 结构体。
在spi 设备驱动程序中,spi_transfer 结构体用于指定要发送的数据,后面称为传输结构体:
列表3: spi_transfer 结构体
struct spi_transfer { /* it's ok if tx_buf == rx_buf (right?) * for MicroWire, one buffer must be null * buffers must work with dma_*map_single() calls, unless * spi_message.is_dma_mapped reports a pre-existing mapping */ const void *tx_buf; void *rx_buf; unsigned len; dma_addr_t tx_dma; dma_addr_t rx_dma; struct sg_table tx_sg; struct sg_table rx_sg; unsigned cs_change:1; unsigned tx_nbits:3; unsigned rx_nbits:3; #define SPI_NBITS_SINGLE 0x01 /* 1bit transfer */ #define SPI_NBITS_DUAL 0x02 /* 2bits transfer */ #define SPI_NBITS_QUAD 0x04 /* 4bits transfer */ u8 bits_per_word; u16 delay_usecs; u32 speed_hz; struct list_head transfer_list; };
传输结构体的成员较多,需要我们自己设置的很少,这里只介绍我们常用的配置项。
总的来说spi_transfer 结构体保存了要发送(或接收)的数据,而在SPI 设备驱动中数据是以“消息”的形式发送。spi_message 是消息结构体,我们把它称为消息结构体,发送一个消息分四步,依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
spi_message 结构体定义如下所示:
列表4: spi_message 结构体
struct spi_message { struct list_head transfers; struct spi_device *spi; unsigned is_dma_mapped:1; /* REVISIT: we might want a flag affecting the behavior of the * last transfer ... allowing things like "read 16 bit length L" * immediately followed by "read L bytes". Basically imposing * a specific message scheduling algorithm. * * Some controller drivers (message-at-a-time queue processing) * could provide that as their default scheduling algorithm. But * others (with multi-message pipelines) could need a flag to * tell them about such special cases. */ /* completion is reported through a callback */ void (*complete)(void *context); void *context; unsigned frame_length; unsigned actual_length; int status; /* for optional use by whatever driver currently owns the * spi_message ... between calls to spi_async and then later * complete(), that's the spi_master controller driver. */ struct list_head queue; void *state; };
spi_message 结构体成员我们比较陌生,如果我们不考虑具体的发送细节我们可以不用了解这些成员的含义,因为spi_message 的初始化以及“绑定”spi_transfer 传输结构体都是由内核函数实现。唯一要说明的是第二个成员“spi”,它是一个spi_device 类型的指针,我们讲解spi_device 结构体时说过,一个spi 设备对应一个spi_device 结构体,这个成员就是用于指定消息来自哪个设备。
linux 系统在开机的时候就会执行,自动进行spi 总线注册。
列表5: spi 总线注册(内核源码/drivers/spi/spi.c)
static int __init spi_init(void)
{
int status;
...
status = bus_register(&spi_bus_type);
...
status = class_register(&spi_master_class);
...
}
当总线注册成功之后,会在sys/bus 下面生成一个spi 总线,然后在系统中新增一个设备类,sys/class/目录下会可以找到spi_master 类。
spi_bus_type 总线定义,会在spi 总线注册时使用。
列表6: spi 总线定义
struct bus_type spi_bus_type = {
.name = "spi",
.dev_groups = spi_dev_groups,
.match = spi_match_device,
.uevent = spi_uevent,
};
.match 函数指针,设定了spi 设备和spi 驱动的匹配规则,具体如下spi_match_device。
列表7: spi 总线注册(内核源码/drivers/spi/spi.c)
static int spi_match_device(struct device *dev, struct device_driver *drv) { const struct spi_device *spi = to_spi_device(dev); const struct spi_driver *sdrv = to_spi_driver(drv); /* Attempt an OF style match */ if (of_driver_match_device(dev, drv)) return 1; /* Then try ACPI */ if (acpi_driver_match_device(dev, drv)) return 1; if (sdrv->id_table) return !!spi_match_id(sdrv->id_table, spi); return strcmp(spi->modalias, drv->name) == 0; }
函数提供了四种匹配方式,设备树匹配方式和acpi 匹配方式以及id_table 匹配方式,如果前面三种都没有匹配成功,则通过设备名进行配对。
我们使用的STM32MP157 芯片有6 个spi 控制器,对应的设备树存在6 个节点
列表8: spi5 设备树节点(内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)
spi5: spi@44009000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32h7-spi";
reg = <0x44009000 0x400>;
interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc SPI5_K>;
resets = <&rcc SPI5_R>;
dmas = <&dmamux1 85 0x400 0x01>,
<&dmamux1 86 0x400 0x01>;
dma-names = "rx", "tx";
power-domains = <&pd_core>;
status = "disabled";
};
compatible 属性值与主机驱动匹配; 在STM32MP157 的SPI 主机驱动文件为内核源码
drivers/spi/spi-stm32.c 中可以找到:
列表9: (内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)
static const struct of_device_id stm32_spi_of_match[] = {
{ .compatible = "st,stm32h7-spi", },
{},
};
reg 为spi5 寄存器组相关的起始地址为0x44009000,寄存器长度为0x400,其他属性暂时不需要了解。
对应的控制器驱动源码中, module_platform_driver() 宏
列表10: module_platform_driver() 宏(内核源码/include/linux/platform_device.h)
#define module_platform_driver(__platform_driver) \
module_driver(__platform_driver, platform_driver_register, \
platform_driver_unregister)
module_driver() 展开如下:
列表11: module_driver(内核源码/include/linux/device.h)
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
向函数传入platform_driver 结构体类型的spi_imx_driver 结构体变量,module_
platform_driver(stm32_spi_driver),间接调用platform_driver_register 和platform_driver_unregister,实现平台驱动函数的注册和注销。
stm32_spi_probe() 函数主要有如下功能:获取设备树节点信息,初始化spi 时钟、dma、中断等。
代码如下(部分被省略):
列表12: stm32_spi_probe 函数(内核源码/drivers/spi/spistm32.c)
static int stm32_spi_probe(struct platform_device *pdev) { struct spi_master *master; struct stm32_spi *spi; struct resource *res; int i, ret, num_cs, cs_gpio; master = spi_alloc_master(&pdev->dev, sizeof(struct stm32_spi)); if (!master) { dev_err(&pdev->dev, "spi master allocation failed\n"); return -ENOMEM; } platform_set_drvdata(pdev, master); spi = spi_master_get_devdata(master); spi->dev = &pdev->dev; spi->master = master; ...... spi->irq = platform_get_irq(pdev, 0); if (spi->irq <= 0) { ret = spi->irq; if (ret != -EPROBE_DEFER) dev_err(&pdev->dev, "failed to get irq: %d\n", ret); goto err_master_put; } ret = devm_request_threaded_irq(&pdev->dev, spi->irq, NULL, stm32_spi_irq, IRQF_ONESHOT, pdev->name, master); if (ret) { dev_err(&pdev->dev, "irq%d request failed: %d\n", spi->irq, ret); goto err_master_put; } ...... spi->dma_tx = dma_request_slave_channel(spi->dev, "tx"); if (!spi->dma_tx) dev_warn(&pdev->dev, "failed to request tx dma channel\n"); else master->dma_tx = spi->dma_tx; spi->dma_rx = dma_request_slave_channel(spi->dev, "rx"); if (!spi->dma_rx) dev_warn(&pdev->dev, "failed to request rx dma channel\n"); else master->dma_rx = spi->dma_rx; if (spi->dma_tx || spi->dma_rx) master->can_dma = stm32_spi_can_dma; pm_runtime_set_active(&pdev->dev); pm_runtime_enable(&pdev->dev); ........ ret = spi_register_master(master); if (ret) { dev_err(&pdev->dev, "spi master registration failed: %d\n", ret); goto err_dma_release; } dev_info(&pdev->dev, "driver initialized\n"); return 0; ..... }
spi 总线驱动,由硬件供应商提供,我们只需要了解,学习其原理就行。下面涉及的函数,我们将会在spi 设备驱动中使用。
spi 设备的注册和注销函数分别在驱动的入口和出口函数中调用,这与平台设备驱动、i2c 设备驱动相同,
spi 设备注册和注销函数如下:
列表13: spi 设备注册和注销函数
int spi_register_driver(struct spi_driver *sdrv)
static inline void spi_unregister_driver(struct spi_driver *sdrv)
对比i2c 设备的注册和注销函数,不难发现把“spi”换成“i2c”就是i2c 设备的注册和注销函数
了,并且用法相同。
参数:
返回值:
函数设置spi 设备的片选信号、传输单位、最大传输速率等,函数中调用spi 控制器的成员controller->setup(),也就是master->setup, 在函数stm32_spi_probe() 中我们将stm32_spi_setup 赋予该结构体。
列表14: spi_setup 函数(内核源码/drivers/spi/spi.c)
int spi_setup(struct spi_device *spi)
参数:
返回值:
初始化spi_message
列表15: spi_message_init 函数(内核源码/include/linux/spi/spi.h)
static inline void spi_message_init(struct spi_message *m)
{
memset(m, 0, sizeof *m);
spi_message_init_no_memset(m);
}
参数:
返回值:无。
列表16: spi_message_init 函数(内核源码/include/linux/spi/spi.h)
static inline void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
{
list_add_tail(&t->transfer_list, &m->transfers);
}
这个函数很简单就是将将spi_transfer 结构体添加到spi_message 队列的末尾。
spi_message 通过成员变量queue 将一系列的spi_message 串联起来,第一个spi_message 挂在structlist_head queue 下面spi_message 还有struct list_head transfers 成员变量,transfer 也是被串联起来的,如下图所示。
阻塞当前线程进行数据传输,spi_sync() 内部调用__spi_sync() 函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。
列表17: spi_sync() 函数(内核源码/drivers/spi/spi.c)
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
int ret;
mutex_lock(&spi->controller->bus_lock_mutex);
ret = __spi_sync(spi, message);
mutex_unlock(&spi->controller->bus_lock_mutex);
return ret;
}
__spi_sync() 函数实现如下:
列表18: __spi_sync() 函数(内核源码/drivers/spi/spi.c)
static int __spi_sync(struct spi_device *spi, struct spi_message *message) { int status; struct spi_controller *ctlr = spi->controller; unsigned long flags; status = __spi_validate(spi, message); if (status != 0) return status; message->complete = spi_complete; message->context = &done; message->spi = spi; ... if (ctlr->transfer == spi_queued_transfer) { spin_lock_irqsave(&ctlr->bus_lock_spinlock, flags); trace_spi_message_submit(message); status = __spi_queued_transfer(spi, message, false); spin_unlock_irqrestore(&ctlr->bus_lock_spinlock, flags); } else { status = spi_async_locked(spi, message); } if (status == 0) { ... wait_for_completion(&done); status = message->status; } message->context = NULL; return status; }
列表19: spi_async() 函数(内核源码/drivers/spi/spi.c)
int spi_async(struct spi_device *spi, struct spi_message *message)
{
...
ret = __spi_async(spi, message);
...
}
在驱动程序中调用async 时不会阻塞当前进程,只是把当前message 结构体添加到当前spi 控制器成员queue 的末尾。然后在内核中新增加一个工作,这个工作的内容就是去处理这个message 结构体。
列表20: __spi_async() 函数(内核源码/drivers/spi/spi.c)
static int __spi_async(struct spi_device *spi, struct spi_message *message)
{
struct spi_controller *ctlr = spi->controller;
...
return ctlr->transfer(spi, message);
}
spi_oled 驱动和我们上一节介绍的i2c_mpu6050 设备驱动非常相似,可对比学习,推荐先学习i2c_mpu6050 驱动。
在oled 驱动中我们使用spi5,可以通过STM32MP157 的芯片手册查到,spi 使用到的引脚如下。
在我们实验中spi5 使用原理图的QSPI_IO0-QSPI_IO3 引脚,oled 屏和spi 引脚对应入下。
OLED 显示屏引脚 | 芯片引脚 | 说明 | 开发板(排针丝印) |
---|---|---|---|
MOSI | PF9 | MOSI 引脚 | SPI_IO1 |
未使用 | PF8 | MISO 引脚 | SPI_IO0 |
CLK | PF7 | SPI 时钟引脚 | SPI_IO2 |
D/C | PF6 | 数据、命令控制引脚 | SPI_IO3 |
CS | PB6 | 片选引脚 | SPI_NCS |
GND | 电源- | GND | |
VCC | 电源+ | 3.3V |
设备树插件书写格式不变,我们重点讲解spi_oled 设备节点。
列表21: spi_oled 设备树插件(linux_driver/SPI_OLED/stm-fire-spi5-oled-overlay.dts)
/{ fragment@0{ target=<&spi5>; __overlay__{ #address-cells = <1>; #size-cells = <0>; pinctrl-names = "default", "sleep"; pinctrl-0 = <&spi5_pins_a>; pinctrl-1 = <&spi5_sleep_pins_a>; cs-gpios = <&gpiob 6 0>; status = "okay"; spi_oled@0 { compatible = "fire,spi_oled"; spi-max-frequency = <10000000>; d_c_control_pin = <&gpiof 6 0>; reg = <0>; }; }; }; }
列表22: 设备树pinctrl 描述(内核源码/arch/arm/boot/dts/stm32mp157-pinctrl.dtsi)
/{ ...... spi5_pins_a: spi5-0 { pins1 { pinmux = <STM32_PINMUX('F', 7, AF5)>, /* SPI5_SCK */ <STM32_PINMUX('F', 9, AF5)>; /* SPI5_MOSI */ bias-disable; drive-push-pull; slew-rate = <1>; }; pins2 { pinmux = <STM32_PINMUX('F', 8, AF5)>; /* SPI5_MISO */ bias-disable; }; }; spi5_sleep_pins_a: spi5-sleep-0 { pins { pinmux = <STM32_PINMUX('F', 7, ANALOG)>, /* SPI5_SCK */ <STM32_PINMUX('F', 8, ANALOG)>, /* SPI5_MISO */ <STM32_PINMUX('F', 9, ANALOG)>; /* SPI5_MOSI */ }; }; ........ }
向pinctrl 子系统添加引脚具体内容可参考GPIO 子系统章节,设备树的几个引脚与spi_oled 显示屏引脚对应关系、引脚的功能、以及在开发板上的位置如前面表格所示。需要注意的是spi_oled显示屏没有MISO 引脚,直接空出即可,spi_oled 显示屏需要一个额外的引脚连接D/C, 用于控制spi 发送的是数据还是控制命令(高电平是数据,低电平是控制命令)。
spi_oled 驱动使用设备树插件方式开发, 主要工作包三部分内容。
spi_oled 的驱动结构和上一章的i2c_mpu6050 完全相同。这里不再赘述,直接讲解实现代码。如下所示。
驱动入口和出口函数与I2C_mpu6050 驱动相似,只是把i2c 替换为spi, 源码如下所示。
列表23: 驱动入口函数实现(linux_driver/SPI_OLED/spi_oled.c)
/* 指定ID 匹配表*/ static const struct spi_device_id oled_device_id[] = { {"fire,spi_oled", 0}, {}}; /* 指定设备树匹配表*/ static const struct of_device_id oled_of_match_table[] = { {.compatible = "fire,spi_oled"}, {}}; /*spi 总线设备结构体*/ struct spi_driver oled_driver = { .probe = oled_probe, .remove = oled_remove, .id_table = oled_device_id, .driver = { .name = "spi_oled", .owner = THIS_MODULE, .of_match_table = oled_of_match_table, }, }; /* * 驱动初始化函数 */ static int __init oled_driver_init(void) { int error; pr_info("oled_driver_init\n"); error = spi_register_driver(&oled_driver); return error; } /* * 驱动注销函数 */ static void __exit oled_driver_exit(void) { pr_info("oled_driver_exit\n"); spi_unregister_driver(&oled_driver); }
在.prob 函数中完成两个主要工作,第一,初始化spi,第二,申请一个字符设备。
列表24: .prob 函数实现(linux_driver/SPI_OLED/spi_oled.c)
static int oled_probe(struct spi_device *spi) { printk(KERN_EMERG "\t match successed \n"); /* 获取spi_oled 设备树节点*/ oled_device_node = of_find_node_by_path("/soc/spi@44009000/spi_oled@0"); if (oled_device_node == NULL) { printk(KERN_EMERG "\t get spi_oled@0 failed! \n"); } /* 获取oled 的D/C 控制引脚并设置为输出,默认高电平*/ oled_control_pin_number = of_get_named_gpio(oled_device_node, "d_c_control_pin", 0); gpio_direction_output(oled_control_pin_number, 1); /* 初始化spi*/ oled_spi_device = spi; oled_spi_device->mode = SPI_MODE_0; oled_spi_device->max_speed_hz = 2000000; spi_setup(oled_spi_device); /* 注册字符设备*/ ret = alloc_chrdev_region(&oled_devno, 0, DEV_CNT, DEV_NAME); if (ret < 0) { printk("fail to alloc oled_devno\n"); goto alloc_err; } /* 关联字符设备结构体cdev 与文件操作结构体file_operations*/ oled_chr_dev.owner = THIS_MODULE; cdev_init(&oled_chr_dev, &oled_chr_dev_fops); /* 添加设备至cdev_map 散列表中*/ ret = cdev_add(&oled_chr_dev, oled_devno, DEV_CNT); if (ret < 0) { printk("fail to add cdev\n"); goto add_err; } /* 创建类*/ class_oled = class_create(THIS_MODULE, DEV_NAME); /* 创建设备DEV_NAME 指定设备*/ device_oled = device_create(class_oled, NULL, oled_devno, NULL, DEV_NAME); ... return 0; }
.prob 函数介绍如下:
字符设备操作函数集是驱动对外的接口,我们要在这些函数中实现对spi_oled 的初始化、写入、关闭等等工作。这里共实现三个函数,.open 函数用于实现spi_oled 的初始化,.write 函数用于向spi_oled 写入显示数据,.release 函数用于关闭spi_oled。
.open 函数实现
在open 函数中完成spi_oled 的初始化,代码如下:
列表25: .open 函数实现(linux_driver/SPI_OLED/spi_oled.c)
/* 字符设备操作函数集,open 函数实现*/ static int oled_open(struct inode *inode, struct file *filp) { spi_oled_init(); //初始化显示屏 return 0; } /*oled 初始化函数*/ void spi_oled_init(void) { 11 /* 初始化oled*/ 12 oled_send_commands(oled_spi_device, oled_init_data, sizeof(oled_init_ ,→data)); 13 14 /* 清屏*/ 15 oled_fill(0x00); 16 } static int oled_send_command(struct spi_device *spi_device, u8 *commands, u16 lenght) { int error = 0; struct spi_message *message; //定义发送的消息 struct spi_transfer *transfer; //定义传输结构体 /* 申请空间*/ message = kzalloc(sizeof(struct spi_message), GFP_KERNEL); transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL); /* 设置D/C 引脚为低电平*/ gpio_direction_output(oled_control_pin_number, 0); /* 填充message 和transfer 结构体*/ transfer->tx_buf = commands; transfer->len = lenght; spi_message_init(message); spi_message_add_tail(transfer, message); error = spi_sync(spi_device, message); kfree(message); kfree(transfer); if (error != 0) { printk("spi_sync error! \n"); return -1; } return error; }
如上代码所示,open 函数只调用了自定义spi_oled_init 函数,在spi_oled_init 函数函数最终会调用oled_send_command 函数初始化spi_oled,然后调用清屏函数。这里主要讲解oled_send_command函数:
.write 函数实现
.write 函数用于接收来自应用程序的数据,并显示这些数据。函数实现如下所示:
列表26: .write 函数实现(linux_driver/SPI_OLED/spi_oled.c)
/* 字符设备操作函数集,.write 函数实现*/ static int oled_write(struct file *filp, const char __user *buf, size_t cnt,loff_t *off) { int copy_number=0; /* 申请内存*/ oled_display_struct *write_data; write_data = (oled_display_struct*)kzalloc(cnt, GFP_KERNEL); copy_number = copy_from_user(write_data, buf,cnt); oled_display_buffer(write_data->display_buffer, write_data->x, write_ata->y, write_data->length); /* 释放内存*/ kfree(write_data); return 0; } static int oled_display_buffer(u8 *display_buffer, u8 x, u8 y, u16 length) /* 数据发送结构体*/ typedef struct oled_display_struct { u8 x; u8 y; u32 length; u8 display_buffer[]; }oled_display_struct;
代码介绍如下:
.release 函数实现
.release 函数功能仅仅是向spi_oled 显示屏发送关闭显示命令,源码如下:
列表27: .release 函数实现(linux_driver/SPI_OLED/spi_oled.c)
/* 字符设备操作函数集,.release 函数实现*/
static int oled_release(struct inode *inode, struct file *filp)
{
oled_send_command(oled_spi_device, 0xae);//关闭显示
return 0;
}
测试应用程序主要工作是实现oled 显示屏实现刷屏、显示文字、显示图片。
测试程序需要用到字符以及图片的点阵数据保存在oled_code_table.c 文件,为方便管理我们编写了一个简单makefile 文件方便我们编译程序。
makefile 文件如下所示:
列表28: Makefile(linux_driver/SPI_OLED/test_app/Makefile.c)
out_file_name = "test_app"
all: test_app.c oled_code_table.c
arm-linux-gnueabihf-gcc $^ -o $(out_file_name)
.PHONY: clean
clean:
rm $(out_file_name)
Makefile 很简单,就不加以说明。
下面是我们的测试程序源码。如下所示:
列表29: 测试应用程序Makefile(linux_driver/SPI_OLED/test_app/test_app.c)
/* 点阵数据*/ extern unsigned char F16x16[]; extern unsigned char F6x8[][6]; extern unsigned char F8x16[][16]; extern unsigned char BMP1[]; int main(int argc, char *argv[]) { int error = -1; /* 打开文件*/ int fd = open("/dev/spi_oled", O_RDWR); if (fd < 0) { printf("open file : %s failed !\n", argv[0]); return -1; } while(1) { /* 显示图片*/ show_bmp(fd, 0, 0, BMP1, X_WIDTH*Y_WIDTH/8); sleep(2); oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏 oled_show_F16X16_letter(fd,0, 0, F16x16, 4); //显示汉字 oled_show_F8X16_string(fd,0,2,"F8X16:THIS IS SPI TEST APP"); oled_show_F6X8_string(fd, 0, 6,"F6X8:THIS IS SPI TEST APP"); sleep(2); oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏 oled_show_F8X16_string(fd,0,0,"Testing is completed"); sleep(2); oled_fill(fd, 0, 0, 127, 7, 0x00); //清屏 } /* 关闭文件*/ error = close(fd); if(error < 0) { printf("close file error! \n"); } return 0; }
测试程序很简单,完整代码请参考配套例程,结合代码简单介绍如下:
我们编写的设备树插件在linux_driver/SPI_OLED/stm-fire-spi5-oled-overlay.dts 将其拷贝到内核源码的arch/arm/boot/dts/overlays/下,在内核源码根目录下执行命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
在“内核源码的arch/arm/boot/dts/overlays/”目录下,会生成stm-fire-spi5-oled.dtbo,stm-fire-spi5-oled.dtbo 就是oled 屏的设备树插件
将linux_driver/SPI_OLED/ 拷贝到内核源码同级目录,执行里面的MakeFile,生成spi_oled.ko。
将linux_driver/SPI_OLED/test_app 目录中执行里面的MakeFile,生成test_app。
将前面生成的设备树插件、驱动程序、应用程序通过scp 等方式拷贝到开发板。
将设备树插件拷贝到/usr/lib/linux-image-4.19.94-stm-r1/overlays, 在/boot/uEnv.txt 中添加dtoverlay=/usr/lib/linux-image-4.19.94-stm-r1/overlays/stm-fire-spi5-oled.dtbo
加载驱动陈程序sudo insmod spi_oled.ko。驱动程序打印match successed 和相关spi 信息
驱动加载成功后直接运行测试应用程序./test_app,正常情况下显示屏会显示设定的内容,如下所示。
参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。