版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
3.8. SPI子系统——OLED屏实验
3.8.1. ecspi基本知识
该 oled 设备是通过 spi 协议进行传输,而具体的 spi 协议在单片机的学习中,已经有很详细的介绍,这里主要介绍的是在 linux 下,实现 spi 传输的原理以及涉及到的 api 函数。
ecSPI是Enhanced Configurable SPI 直译为增强可配置SPI,可以理解为是功能更强的SPI接口。
3.8.2. ecspi驱动框架
ecspi设备驱动和i2c设备驱动非常相似,可对比学习。
如框架图所示,ecspi可分为spi总线驱动和spi设备驱动。spi总线驱动已经由芯片厂商提供,我们适当了解其实现机制。 而spi设备驱动由我们自己编写,则需要明白其中的原理。spi设备驱动涉及到字符设备驱动、SPI核心层、SPI主机驱动,具体功能如下。
SPI核心层:提供SPI控制器驱动和设备驱动的注册方法、注销方法、SPI通信硬件无关接口函数。
SPI主机驱动:主要包含SPI硬件体系结构中适配器(spi控制器)的控制,用于产生SPI 读写时序。
SPI设备驱动:通过SPI主机驱动与CPU交换数据。
3.8.2.1. 关键数据结构
这里对整个ecspi驱动框架所涉及的关键数据结构进行整理,可先跳过,后续代码中遇到这些数据结构时再回来看详细定义。
(1)spi_transfer
在spi设备驱动程序中,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;
};
传输结构体的成员较多,需要我们自己设置的很少,这里只介绍我们常用的配置项。
tx_buf: 发送缓冲区,用于指定要发送的数据地址。
rx_buf: 接收缓冲区,用于保存接收得到的数据,如果不接收不用设置或设置为NULL.
len: 要发送和接收的长度,根据SPI特性发送、接收长度相等。
tx_dma、rx_dma: 如果使用了DAM,用于指定tx或rx DMA地址。
bits_per_word: speed_hz,分别用于设置每个字节多少位、发送频率。如果我们不设置这些参数那么会使用默认的配置,也就是我初始化spi是设置的参数。
(2)spi_master
总的来说spi_transfer结构体保存了要发送(或接收)的数据,而在SPI设备驱动中数据是以“消息”的形式发送。 spi_message是消息结构体,我们把它称为消息结构体,发送一个消息分四步, 依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
spi_message结构体定义如下所示:
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
/* 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结构体,这个成员就是用于指定消息来自哪个设备。
3.8.3. ecspi设备驱动
3.8.3.1. 核心函数
(1)ecspi设备的注册和注销函数
ecspi设备的注册和注销函数分别在驱动入口和出口函数调用,这与平台设备驱动、I2C设备驱动相同。
int spi_register_driver(struct spi_driver *sdrv)
static inline void spi_unregister_driver(struct spi_driver *sdrv)
对比i2c设备的注册和注销函数,不难发现把“spi”换成“i2c”就是i2c设备的注册和注销函数了,并且用法相同。
参数:
spi spi_driver类型的结构体(spi设备驱动结构体),一个spi_driver结构体就代表了一个ecspi设备驱动
返回值:
成功: 0
失败: 其他任何值都为错误码
(2) spi_setup()函数
函数设置spi设备的片选信号、传输单位、最大传输速率等,函数中调用spi控制器的成员controller->setup(), 也就是spi_imx->bitbang.master->setup(),在函数spi_imx_probe()中我们将spi_imx_setup赋予该结构体。
int spi_setup(struct spi_device *spi)
参数:
spi spi_device spi设备结构体
返回值:
成功: 0
失败: 其他任何值都为错误码
(3)spi_message_init()函数
初始化spi_message
static inline void spi_message_init(struct spi_message *m)
{
memset(m, 0, sizeof *m);
spi_message_init_no_memset(m);
}
参数:
m spi_message 结构体指针, spi_message是消息结构体,我们把它称为消息结构体,发送一个消息分四步, 依次为定义消息结构体、初始化消息结构体、“绑定”要发送的数据(也就是初始化好的spi_transfer结构)、执行发送。
返回值: 无。
(4)spi_message_add_tail()函数
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队列的末尾。
3.8.3.2. ecspi同步与互斥
(1)SPI同步传输数据
阻塞当前线程进行数据传输,spi_sync()内部调用__spi_sync()函数,mutex_lock()和mutex_unlock()为互斥锁的加锁和解锁。
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;
}
阻塞当前线程,当message发送完成时结束阻塞。
(2) SPI异步传输数据
int spi_async(struct spi_device *spi, struct spi_message *message)
{
...
ret = __spi_async(spi, message);
...
}
在驱动程序中调用async时不会阻塞当前进程,只是把当前message结构体添加到当前spi控制器成员queue的末尾。 然后在内核中新增加一个工作,这个工作的内容就是去处理这个message结构体。
3.8.3.3. spi数据收发流程
该过程涉及到两个重要的结构体两个重要结构体 spi_message 和 spi_transfer
1)申请并初始化 spi_transfer,设置 spi_transfer 的 tx_buf 成员变量, tx_buf 为要发送的数据。然后设置 rx_buf 成员变量, rx_buf 保存着接收到的数据,最后设置 len 成员变量,也就是要进行数据通信的长度。
2)使用 spi_message_init 函数初始化 spi_message。
3)使用spi_message_add_tail函数将前面设置好的 spi_transfer 添加到 spi_message 队列中。
4)使用 spi_sync 函数完成 spi 数据同步传输。
void spi_message_init(struct spi_message *m)
// 初始化 spi_message
void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m)
// 将 spi_transfer 添加到 spi_message 队列中
int spi_sync(struct spi_device *spi, struct spi_message *message)
// 同步传输函数
int spi_async(struct spi_device *spi, struct spi_message *message)
// 异步传输函数
3.8.4. oled屏幕驱动实验
3.8.4.1. 硬件连接说明
oled 显存
oled 本身是没有显存的,它的显存是依赖于 ssd1306 提供的,ssd1306 的显存总共为 128 * 64 bit 大小,oled 不能一次控制一个点阵,只能控制8个点阵,而且是垂直方向扫描控制。
spi oled 的管脚说明
res 管脚:用于 reset 的管脚,置为 1 则有效
dc 管脚:用来决定传输的 spi_message 是数据还是指令,置 1 则为数据,置 0 则为指令
cs 管脚:片选管脚,spi 总线上可以挂载许多 spi 设备,cs 置 0,则主机与该 spi 设备进行通信
scl 管脚:spi 时钟线
sda管脚:spi 数据线
总之,该 oled 屏幕通过 spi 协议进行数据传输,要想实现通信,reset 管脚置为 1 ,cs 管脚即片选管脚置 0(置 0 则与该 oled 进行 spi 通信),dc 管脚在发送数据前,需要对 oled 进行初始化,此时需要对oled 发送命令,即需要将其置为 0,之后传入显存中的图像数据时,则置 1 ,除此之外的 scl 和 sda 管脚,具体实现参照单片机课程即可,在 linux 内核中已经有封装好的函数实现 spi 数据传输。
3.8.4.2. 设备树插件
编写oled的dt overlay文件,在imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays
文件夹向下创建oled_spi.dts,在该文件中引用imux6ul.dts已经定义好的ecspi1并叠加,然后在总线节点上添加spi设备节点,并修改overlays文件夹下的makefile文件,在之后make dtbs时,可以对该dtoverlay文件进行编译。
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include "../imx6ul-pinfunc.h"
&ecspi1 {
fsl,spi-num-chipselects = <1>;
cs-gpios = <&gpio3 26 GPIO_ACTIVE_LOW>;
reset-gpios = <&gpio5 1 GPIO_ACTIVE_LOW>;
dc-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_spi_uart8
&pinctrl_oled_reset
&pinctrl_oled_dc>;
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
spidev0: oled_spi@0 {
reg = <0>;
compatible = "oled_spi";
spi-max-frequency = <8000000>;
};
};
&iomuxc {
pinctrl_spi_uart8: spi_uart8_grp {
fsl,pins = <
MX6UL_PAD_LCD_DATA20__ECSPI1_SCLK 0x10b0
MX6UL_PAD_LCD_DATA22__ECSPI1_MOSI 0x10b0
MX6UL_PAD_LCD_DATA23__ECSPI1_MISO 0x10b0
MX6UL_PAD_LCD_DATA21__GPIO3_IO26 0x10b0
>;
};
pinctrl_oled_reset: oled_reset_grp {
fsl,pins = <
MX6UL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0
>;
};
pinctrl_oled_dc: oled_dc_grp {
fsl,pins = <
MX6UL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0
>;
};
};
这里使用了5个引脚它们与ecspi_oled显示屏引脚对应关系、引脚的功能、以及在开发板上的位置如上表所示。 ecspi_oled显示屏需要一个额外的引脚连接D/C, 用于控制spi发送的是数据还是控制命令(高电平是数据,低电平是控制命令)。
添加完成后,回到imx6ull/bsp/kernel/linux-imx/
文件夹,使用make dtbs命令重新编译设备树,这时在imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays
文件夹下可以看到编译好的oled_spi.dtbo文件。
3.8.4.3. 实验代码解析
ecspi_oled的驱动结构和i2c_sht20完全相同。
3.8.4.3.2. oled驱动实现
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h> //file_operations,用于联系系统调用和驱动程序
#include <linux/errno.h> //ENODEV,ENOMEM存放的头文件
#include <linux/kernel.h> // printk(),内核打印函数
#include <linux/device.h> // 用于自动生成设备节点的函数头文件
#include <linux/gpio.h> // gpio相关函数
#include <linux/gpio/consumer.h>// gpiod相关函数
#include <linux/of_gpio.h> // gpio子系统相关函数
#include <linux/uaccess.h> // copy_to_user函数头文件
#include <linux/timer.h> //定时器相关
#include <linux/spi/spi.h> //spi相关
#include <linux/delay.h> // 延时函数头文件
#include <linux/cdev.h> //cdev相关函数
#include <linux/mm.h> //remap_pfn_range
#include <linux/fb.h>
#include <linux/dma-mapping.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#define DEV_NAME "spi_oled" //最后在/dev路径下的设备名称,应用层open的字符串名
#define DEV_CNT 1
#define PLATDRV_MAGIC 0x16 //魔术字
#define OLED_ON _IO (PLATDRV_MAGIC, 0x18)
unsigned char *baseaddr;
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif
static int dev_major = DEV_MAJOR; /*主设备号*/
int dc_gpio; //片选所使用的gpio编号
int reset_gpio; //复位所使用gpio编号
int cs_gpio; //数据,命令选择线所使用的gpio编号
enum
{
OLED_CMD = 0,
OLED_DATA,
};
/*OLED显示屏列数*/
#define X_WIDTH 128
#define Y_WIDTH 64
/*存放sht20的私有属性*/
struct oled_priv{
struct cdev cdev; /*cdev结构体*/
struct class *dev_class;/*自动创建设备节点的类*/
struct spi_device *spi;
struct device *dev;
};
/*
*向 oled 发送数据
*spi_device,指定oled 设备驱动的spi 结构体
*data, 要发送数据的地址
*lenght,发送的数据长度
*/
static int oled_send(struct spi_device *spi_device, unsigned char *buf, int lenght)
{
int error = 0;
int index = 0;
struct spi_message *message; //定义发送的消息
struct spi_transfer *transfer; //定义传输结构体
/*使能oled,片选拉低*/
gpio_set_value(cs_gpio, 0);
/*申请空间*/
message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
/*每次发送 30字节,循环发送*/
do
{
if (lenght > 30)
{
transfer->tx_buf = buf + index;
transfer->len = 30;
spi_message_init(message);
spi_message_add_tail(transfer, message);
index += 30;
lenght -= 30;
}
else
{
transfer->tx_buf = buf + index;
transfer->len = lenght;
spi_message_init(message);
spi_message_add_tail(transfer, message);
index += lenght;
lenght = 0;
}
error = spi_sync(spi_device, message);
if (error != 0)
{
printk("spi_sync error! %d \n", error);
return -1;
}
} while (lenght > 0);
kfree(message);
kfree(transfer);
return 0;
}
/*OLED write a byte command data or command*/
static void oled_send_one(struct oled_priv *priv, unsigned char data, unsigned short type)
{
if(type)
{
gpio_set_value(dc_gpio, OLED_DATA);
}
else
{
gpio_set_value(dc_gpio, OLED_CMD);
}
oled_send(priv->spi, &data, 1);
gpio_set_value(dc_gpio, OLED_DATA);
//printk("OLED write a byte command data or command ok!\n");
}
/*
* oled display function API
*/
/*set oled display position*/
static int oled_set_pos(struct oled_priv *priv, uint8_t x, uint8_t y)
{
oled_send_one(priv, 0xb0+y, OLED_CMD); //设置行起始地址
oled_send_one(priv, ((x&0xf0)>>4)|0x10, OLED_CMD);//设置低列起始地址
oled_send_one(priv, (x&0x0f)|0X01, OLED_CMD); //设置高列起始地址
return 0;
}
/*
*填充整个OLED显示屏
*oled_priv *priv 私有数据
*/
static void oled_clear(struct oled_priv *priv)
{
uint8_t y, x;
for(y=0; y<8; y++)
{
oled_set_pos(priv, 0, y);
for(x=0; x < X_WIDTH; x++)
{
oled_send_one(priv, 0x00, OLED_DATA);
}
}
}
/*
*向oled发送显示数据, x,y指定显示的起始位置,支持自动换行
*spi_device,指定oled设备驱动的spi结构体
*display_buffer,数据地址
*length,发送长度
*/
static int oled_display_buffer(struct oled_priv *priv, uint8_t *display_buffer, int length)
{
uint8_t y = 0;
uint8_t x = 0;
int index = 0;
int error = 0;
do
{
error += oled_set_pos(priv, x, y);
if (length > (X_WIDTH - x))
{
error += oled_send(priv->spi, display_buffer + index, X_WIDTH - x);
length -= (X_WIDTH - x);
index += (X_WIDTH - x);
x = 0;
y++;
}
else
{
error += oled_send(priv->spi, display_buffer + index, length);
index += length;
// x += length;
length = 0;
}
} while (length > 0);
if (error != 0)
{
/*发送错误*/
printk("oled_display_buffer error! %d \n",error);
return -1;
}
return index;
}
/*oled 初始化函数*/
static void oled_init(struct oled_priv *priv)
{
gpio_set_value(reset_gpio, 1);
mdelay(100);
gpio_set_value(reset_gpio, 0);
mdelay(200);
gpio_set_value(reset_gpio, 1);
oled_send_one(priv, 0xAE, OLED_CMD);//--turn off oled panel
oled_send_one(priv, 0xFD, OLED_CMD);
oled_send_one(priv, 0x12, OLED_CMD);
oled_send_one(priv, 0xd5, OLED_CMD);//--set display clock divide ratio/oscillator frequency
oled_send_one(priv, 0xA0, OLED_CMD); //Set COM Output Scan Direction
oled_send_one(priv, 0xA8, OLED_CMD);//--set multiplex ratio(1 to 64)
oled_send_one(priv, 0x3f, OLED_CMD);//--1/64 duty
oled_send_one(priv, 0xD3, OLED_CMD);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
oled_send_one(priv, 0X00, OLED_CMD);//-not offset
oled_send_one(priv, 0X40, OLED_CMD);//--set start line address Set Mapping RAM Display Start Line (0x00~0x3F)
oled_send_one(priv, 0XA1, OLED_CMD);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常
oled_send_one(priv, 0XC8, OLED_CMD);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常
oled_send_one(priv, 0xDA, OLED_CMD);//--set com pins hardware configuration
oled_send_one(priv, 0x12, OLED_CMD);//
oled_send_one(priv, 0x81, OLED_CMD);//--set contrast control register
oled_send_one(priv, 0xBF, OLED_CMD);// Set SEG Output Current Brightness
oled_send_one(priv, 0xD9, OLED_CMD);//--set pre-charge period
oled_send_one(priv, 0x25, OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
oled_send_one(priv, 0xDB, OLED_CMD);//--set vcomh
oled_send_one(priv, 0x34, OLED_CMD);//Set VCOM Deselect Level
oled_send_one(priv, 0xA4, OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
oled_send_one(priv, 0xA6, OLED_CMD);// Disable Inverse Display On (0xa6/a7)
oled_send_one(priv, 0xAF, OLED_CMD);// Disable Inverse Display On (0xa6/a7)
oled_clear(priv);
}
/*字符设备操作函数,open函数实现*/
static int oled_open(struct inode *inode, struct file *filp)
{
struct oled_priv *priv = NULL;
printk("oled_open \n");
priv = container_of(inode->i_cdev, struct oled_priv, cdev);
filp->private_data = priv;
oled_init(priv);//11
printk("oled_init \n");
return 0;
}
static ssize_t oled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int ret = 0;
ret = copy_to_user(buf, baseaddr, 1024);
return 0;
}
/*字符设备操作函数,.write函数实现*/
static int oled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off)
{
struct oled_priv *priv = filp->private_data;
unsigned char databuf[1024];
int rv = 0;
/*数据发送*/
rv = copy_from_user(databuf, buf, cnt);
if(rv < 0)
{
printk("oled_write fail\r\n");
return -EFAULT;
}
printk("copy_from_user ok:cnt = %d\r\n", cnt);
oled_display_buffer(priv, databuf, cnt);
oled_clear(priv);
return 0;
}
static int oled_mmap(struct file *filp, struct vm_area_struct *vma)
{
// struct oled_priv *priv = filp->private_data;
// vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
// vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
vma->vm_page_prot = pgprot_cached(vma->vm_page_prot);
if(!baseaddr)
{
return -1;
}
if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(baseaddr) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
{
return -ENOBUFS;
}
return 0;
}
static long oled_ioctrl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct oled_priv *priv = filp->private_data;
unsigned char *display_buffer = baseaddr;
switch(cmd)
{
case OLED_ON:
oled_display_buffer(priv, display_buffer, 1024);
break;
}
return 0;
}
static int oled_release(struct inode *inode, struct file *filp)
{
struct oled_priv *priv = filp->private_data;
oled_send_one(priv, 0xAE, OLED_CMD);//关闭显示
return 0;
}
/*字符设备操作函数集*/
static struct file_operations oled_fops =
{
.owner = THIS_MODULE,
.open = oled_open,
.read = oled_read,
.write = oled_write,
.unlocked_ioctl = oled_ioctrl,
.mmap = oled_mmap,
.release = oled_release,
};
/*spi总线设备函数集:.probe函数只需要初始化esdpi,添加、注册一个字符设备即可。 */
static int oled_probe(struct spi_device *spi_dev)
{
struct oled_priv *priv = NULL; //临时存放私有属性的结构体
dev_t devno; //设备的主次设备号
int rv = 0;
struct device_node *np = NULL;
printk("match successed \n");
/*为oled私有属性分配存储空间*/
priv = devm_kzalloc(&spi_dev->dev, sizeof(struct oled_priv), GFP_KERNEL);
if (!priv)
return -EINVAL;
/*获取oled的设备树节点*/
np = of_find_node_by_path("/soc/bus@2000000/spba-bus@2000000/spi@2008000"); //当前设备节点
if(np == NULL)
{
printk("get ecspi_oled_node failed!\n");
}
/*获取 oled的cs引脚并设置为输出,默认高电平*/
cs_gpio = of_get_named_gpio(np, "cs-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(cs_gpio < 0)
{
printk("can't get cs-gpio\n");
return -EINVAL;
}
rv = gpio_request(cs_gpio, "cs-gpio");
rv = gpio_direction_output(cs_gpio, 1);
if(rv < 0)
{
printk("can't set cs_gpio!\r\n");
}
/*获取 oled的reset引脚并设置为输出,默认高电平*/
reset_gpio = of_get_named_gpio(np, "reset-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(reset_gpio < 0)
{
printk("can't get reset-gpio\n");
return -EINVAL;
}
rv = gpio_request(reset_gpio, "reset-gpio");
rv = gpio_direction_output(reset_gpio, 1);
if(rv < 0)
{
printk("can't set reset_gpio!\r\n");
}
/*获取 oled的dc引脚并设置为输出,默认高电平*/
dc_gpio = of_get_named_gpio(np, "dc-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(dc_gpio < 0)
{
printk("can't get dc-gpio\n");
return -ENOMEM;
}
rv = gpio_request(dc_gpio, "dc-gpio");
rv = gpio_direction_output(dc_gpio, 1);
if(rv < 0)
{
printk("can't set dc-gpio!\r\n");
}
printk("cs_gpio=%d, reset_gpio=%d, dc_gpio=%d\n", cs_gpio, reset_gpio, dc_gpio);
/*---------------------注册 字符设备部分-----------------*/
/*1.分配主次设备号,这里即支持静态指定,也至此动态申请*/
if (0 != dev_major) /*static*/
{
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, DEV_CNT, DEV_NAME); /* /proc/devices/DEV_NAME */
}
else
{
rv = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME); /*动态申请字符设备号*/
dev_major = MAJOR(devno); /*获取主设备号*/
}
/*自动分配设备号失败*/
if (rv < 0)
{
printk("%s driver can't get major %d", DEV_NAME, dev_major);
return rv;
}
printk(" %s driver use major %d\n", DEV_NAME, dev_major);
/*2.分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
priv->cdev.owner = THIS_MODULE; /*.owner这表示谁拥有你这个驱动程序*/
cdev_init(&priv->cdev, &oled_fops); /*初始化cdev,把fops添加进去*/
rv = cdev_add(&priv->cdev, devno, DEV_CNT); /*注册给内核,设备数量1个*/
if (0 != rv) {
printk( " %s driver can't register cdev:result=%d\n", DEV_NAME, rv);
goto undo_major;
}
printk( " %s driver can register cdev:result=%d\n", DEV_NAME, rv);
//3.创建类,驱动中进行节点创建
priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(priv->dev_class))
{
printk("%s driver create class failure\n", DEV_NAME);
rv = -ENOMEM;
goto undo_cdev;
}
//4.创建设备
devno = MKDEV(dev_major, 0); //给每一个led设置设备号
priv->dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME); /* /dev/DEV_NAME 注册这个设备节点*/
if(IS_ERR(priv->dev))
{
dev_err(&spi_dev->dev, "fail to create device\n");
rv = -ENOMEM; //返回错误码,应用空间strerror查看
goto undo_class;
}
//5.初始化spi
spi_dev->mode = SPI_MODE_0;//spi设置模式为SPI_MODE_0模式
spi_dev->max_speed_hz = 2000000; //设置最高频率,会覆盖设备树中的设置
spi_setup(spi_dev);//设置spi
priv->spi = spi_dev; //传回spi_device结构体,该结构体一个spi设备
//6. 保存私有数据
spi_set_drvdata(spi_dev, priv);
//oled_init(priv);//11
dev_info(&spi_dev->dev, "oled spi driver probe okay.\n");
return 0;
undo_class:
class_destroy(priv->dev_class);
undo_cdev:
cdev_del(&priv->cdev);
undo_major:
unregister_chrdev_region(devno, DEV_CNT);
return rv;
}
static int oled_remove(struct spi_device *spi_dev)
{
/*删除设备*/
struct oled_priv *priv = spi_get_drvdata(spi_dev);//临时存放私有属性的结构体
dev_t devno = MKDEV(dev_major, 0);
devno = MKDEV(dev_major, 0);
gpio_free(cs_gpio);
gpio_free(reset_gpio);
gpio_free(dc_gpio);
device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
class_destroy(priv->dev_class); //注销类
cdev_del(&priv->cdev); //删除cdev
unregister_chrdev_region(MKDEV(dev_major, 0), DEV_CNT);//释放设备号
devm_kfree(&spi_dev->dev, priv);//释放堆
kfree(baseaddr);
printk("oled driver removed.\n");
return 0;
}
/*定义设备树匹配表*/
static const struct of_device_id oled_of_match_table[] = {
{.compatible = "oled_spi"},
{/* sentinel */}};
/*定义spi总线设备结构体*/
struct spi_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.driver ={
.name = "oled_spi", //无设备树时,用于设备和驱动间匹配
.owner = THIS_MODULE,
.of_match_table = oled_of_match_table,
},
};
/*
* 驱动初始化函数
*/
static int __init oled_driver_init(void)
{
int ret;
printk("oled_driver_init\n");
baseaddr = kmalloc(1024, GFP_KERNEL);
ret = spi_register_driver(&oled_driver); /*添加一个spi设备驱动*/
return ret;
}
/*
* 驱动注销函数
*/
static void __exit oled_driver_exit(void)
{
printk("oled_driver_exit\n");
spi_unregister_driver(&oled_driver); /*删除一个spi设备驱动*/
}
module_init(oled_driver_init);
module_exit(oled_driver_exit);
MODULE_AUTHOR("Zou Ying <1019804140@qq.com>");
MODULE_DESCRIPTION("spi_oled driver on i.MX6ULL platform");
MODULE_LICENSE("GPL");
MODULE_ALIAS("spi:oled");
3.8.4.3.2. oled驱动分析
(1)驱动的入口和出口函数实现
驱动入口和出口函数与i2c_sht20驱动相似,只是把i2c替换为ecspi,源码如下所示。
/*定义设备树匹配表*/
static const struct of_device_id oled_of_match_table[] = {
{.compatible = "oled_spi"},
{/* sentinel */}};
/*定义spi总线设备结构体*/
struct spi_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.driver ={
.name = "oled_spi", //无设备树时,用于设备和驱动间匹配
.owner = THIS_MODULE,
.of_match_table = oled_of_match_table,
},
};
/*
* 驱动初始化函数
*/
static int __init oled_driver_init(void)
{
int ret;
printk("oled_driver_init\n");
display_buffer = kmalloc(1024, GFP_KERNEL);
ret = spi_register_driver(&oled_driver); /*添加一个spi设备驱动*/
return ret;
}
/*
* 驱动注销函数
*/
static void __exit oled_driver_exit(void)
{
printk("oled_driver_exit\n");
spi_unregister_driver(&oled_driver); /*删除一个spi设备驱动*/
}
module_init(oled_driver_init);
module_exit(oled_driver_exit);
(2).prob函数实现
在.prob函数中完成三个主要工作,第一,通过指定设备树节点路径对设备树进行解析,第二,申请一个字符设备,第三,初始化ecspi。
/*spi总线设备函数集:.probe函数只需要初始化esdpi,添加、注册一个字符设备即可。 */
static int oled_probe(struct spi_device *spi_dev)
{
struct oled_priv *priv = NULL; //临时存放私有属性的结构体
dev_t devno; //设备的主次设备号
int rv = 0;
struct device_node *np = NULL;
printk("match successed \n");
/*为oled私有属性分配存储空间*/
priv = devm_kzalloc(&spi_dev->dev, sizeof(struct oled_priv), GFP_KERNEL);
if (!priv)
return -EINVAL;
/*获取oled的设备树节点*/
np = of_find_node_by_path("/soc/bus@2000000/spba-bus@2000000/spi@2008000"); //根据设备树节点路径获取节点。
if(np == NULL)
{
printk("get ecspi_oled_node failed!\n");
}
/*获取 oled的cs引脚并设置为输出,默认高电平*/
cs_gpio = of_get_named_gpio(np, "cs-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(cs_gpio < 0)
{
printk("can't get cs-gpio\n");
return -EINVAL;
}
rv = gpio_request(cs_gpio, "cs-gpio");
rv = gpio_direction_output(cs_gpio, 1);
if(rv < 0)
{
printk("can't set cs_gpio!\r\n");
}
/*获取 oled的reset引脚并设置为输出,默认高电平*/
reset_gpio = of_get_named_gpio(np, "reset-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(reset_gpio < 0)
{
printk("can't get reset-gpio\n");
return -EINVAL;
}
rv = gpio_request(reset_gpio, "reset-gpio");
rv = gpio_direction_output(reset_gpio, 1);
if(rv < 0)
{
printk("can't set reset_gpio!\r\n");
}
/*获取 oled的dc引脚并设置为输出,默认高电平*/
dc_gpio = of_get_named_gpio(np, "dc-gpio", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if(dc_gpio < 0)
{
printk("can't get dc-gpio\n");
return -ENOMEM;
}
rv = gpio_request(dc_gpio, "dc-gpio");
rv = gpio_direction_output(dc_gpio, 1);
if(rv < 0)
{
printk("can't set dc-gpio!\r\n");
}
printk("cs_gpio=%d, reset_gpio=%d, dc_gpio=%d\n", cs_gpio, reset_gpio, dc_gpio);
/*---------------------注册 字符设备部分-----------------*/
/*...部分省略...*/
//5.初始化spi
spi_dev->mode = SPI_MODE_0;//spi设置模式为SPI_MODE_0模式
spi_dev->max_speed_hz = 2000000; //设置最高频率,会覆盖设备树中的设置
spi_setup(spi_dev);
/* .prob函数传回的spi_device结构体,根据之前讲解,该结构体代表了一个ecspi(spi)设备,我们通过它配置SPI,这里设置的内容将会覆盖设树节点中设置的内容。*/
priv->spi = spi_dev;//传回spi_device结构体,该结构体一个spi设备
//6. 保存私有数据
spi_set_drvdata(spi_dev, priv);
dev_info(&spi_dev->dev, "oled spi driver probe okay.\n");
}
(3)字符设备操作函数集实现
该驱动主要使用的是 oled_open,oled_wirte,oled_read,oled_mmap,oled_ioctrl 函数,当应用程序在用户空间调用了 open,write,read,mmap,ioctrl 函数时,驱动程序就在内核空间调用了oled_open,oled_wirte,oled_read,oled_mmap,oled_ioctrl 函数。
.open函数实现
在open函数中完成ecspi_oled的初始化,代码如下:
oled_open 是利用 contain_of 函数,来找到 在 oled_probe 函数中,初始化的 priv 结构体指针,并将这个指针传给 file 结构体中的 private_data 中,这样就获取到了 oled_probe 中初始化的 oled 的 priv结构体,并且在对设备文件的其他操作中,可以利用 filp->private_data 来获取这个 priv 属性结构体。
/*字符设备操作函数,open函数实现*/
static int oled_open(struct inode *inode, struct file *filp)
{
struct oled_priv *priv = NULL;
printk("oled_open \n");
priv = container_of(inode->i_cdev, struct oled_priv, cdev);
filp->private_data = priv;
oled_init(priv);
printk("oled_init \n");
return 0;
}
在oled_init()函数中调用oled_send_one()函数初始化ecspi_oled,然后调用清屏函数。
/*oled 初始化函数*/
static void oled_init(struct oled_priv *priv)
{
gpio_set_value(reset_gpio, 1);
mdelay(100);
gpio_set_value(reset_gpio, 0);
mdelay(200);
gpio_set_value(reset_gpio, 1);
oled_send_one(priv, 0xAE, OLED_CMD);//--turn off oled panel
oled_send_one(priv, 0xFD, OLED_CMD);
oled_send_one(priv, 0x12, OLED_CMD);
oled_send_one(priv, 0xd5, OLED_CMD);//--set display clock divide ratio/oscillator frequency
oled_send_one(priv, 0xA0, OLED_CMD); //Set COM Output Scan Direction
oled_send_one(priv, 0xA8, OLED_CMD);//--set multiplex ratio(1 to 64)
oled_send_one(priv, 0x3f, OLED_CMD);//--1/64 duty
oled_send_one(priv, 0xD3, OLED_CMD);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
oled_send_one(priv, 0X00, OLED_CMD);//-not offset
oled_send_one(priv, 0X40, OLED_CMD);//--set start line address Set Mapping RAM Display Start Line (0x00~0x3F)
oled_send_one(priv, 0XA1, OLED_CMD);//--Set SEG/Column Mapping 0xa0左右反置 0xa1正常
oled_send_one(priv, 0XC8, OLED_CMD);//Set COM/Row Scan Direction 0xc0上下反置 0xc8正常
oled_send_one(priv, 0xDA, OLED_CMD);//--set com pins hardware configuration
oled_send_one(priv, 0x12, OLED_CMD);//
oled_send_one(priv, 0x81, OLED_CMD);//--set contrast control register
oled_send_one(priv, 0xBF, OLED_CMD);// Set SEG Output Current Brightness
oled_send_one(priv, 0xD9, OLED_CMD);//--set pre-charge period
oled_send_one(priv, 0x25, OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
oled_send_one(priv, 0xDB, OLED_CMD);//--set vcomh
oled_send_one(priv, 0x34, OLED_CMD);//Set VCOM Deselect Level
oled_send_one(priv, 0xA4, OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
oled_send_one(priv, 0xA6, OLED_CMD);// Disable Inverse Display On (0xa6/a7)
oled_send_one(priv, 0xAF, OLED_CMD);// Disable Inverse Display On (0xa6/a7)
oled_clear(priv);
}
这里主要讲解oled_send_command()函数
/*OLED write a byte command data or command*/
static void oled_send_one(struct oled_priv *priv, unsigned char data, unsigned short type)
{
if(type)
{
gpio_set_value(dc_gpio, OLED_DATA);
}
else
{
gpio_set_value(dc_gpio, OLED_CMD);
}
oled_send(priv->spi, &data, 1);
gpio_set_value(dc_gpio, OLED_DATA);
//printk("OLED write a byte command data or command ok!\n");
}
设置 D/C引脚为低电平,前面说过,ecspi_oled的D/C引脚用于控制发送的命令或数据,低电平时表示发送的是命令。
/*
*向 oled 发送数据
*spi_device,指定oled 设备驱动的spi 结构体
*data, 要发送数据的地址
*lenght,发送的数据长度
*/
static int oled_send(struct spi_device *spi_device, unsigned char *buf, int lenght)
{
int error = 0;
int index = 0;
struct spi_message *message; //定义发送的消息
struct spi_transfer *transfer; //定义传输结构体
/*使能oled,片选拉低*/
gpio_set_value(cs_gpio, 0);
/*申请空间*/
message = kzalloc(sizeof(struct spi_message), GFP_KERNEL);
transfer = kzalloc(sizeof(struct spi_transfer), GFP_KERNEL);
/*每次发送 30字节,循环发送*/
do
{
if (lenght > 30)
{
transfer->tx_buf = buf + index;
transfer->len = 30;
spi_message_init(message);
spi_message_add_tail(transfer, message);
index += 30;
lenght -= 30;
}
else
{
transfer->tx_buf = buf + index;
transfer->len = lenght;
spi_message_init(message);
spi_message_add_tail(transfer, message);
index += lenght;
lenght = 0;
}
error = spi_sync(spi_device, message);
if (error != 0)
{
printk("spi_sync error! %d \n", error);
return -1;
}
} while (lenght > 0);
kfree(message);
kfree(transfer);
return 0;
}
为节省内核栈空间这里使用kzalloc为它们分配空间,这两个结构体大约占用100字节,推荐这样做。
发送流程依次为初始化spi_transfer结构体指定要发送的数据、初始化消息结构体、将消息结构体添加到队尾部、调用spi_sync函数执行同步发送。
.write函数实现
oled_wirte 则是使用 copy_from_user 将用户空间传入的数据读到内核空间,并进行处理,主要将获取到的 data 数组通过 oled_send()函数,传入到 oled 中,使 oled 可以显示我们所需要的图像。
函数实现如下所示:
/*字符设备操作函数,.write函数实现*/
static int oled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off)
{
struct oled_priv *priv = filp->private_data;
unsigned char databuf[1024];
int rv = 0;
/*数据发送*/
rv = copy_from_user(databuf, buf, cnt);
if(rv < 0)
{
printk("oled_write fail\r\n");
return -EFAULT;
}
printk("copy_from_user ok:cnt = %d\r\n", cnt);
oled_display_buffer(priv, databuf, cnt);
oled_clear(priv);
return 0;
}
/*
*向oled发送显示数据, x,y指定显示的起始位置,支持自动换行
*priv,oled_priv结构体
*display_buffer,数据地址
*length,发送长度
*/
static int oled_display_buffer(struct oled_priv *priv, uint8_t *display_buffer, int length)
oled_clear()清屏函数
/*set oled display position*/
static int oled_set_pos(struct oled_priv *priv, uint8_t x, uint8_t y)
{
oled_send_one(priv, 0xb0+y, OLED_CMD); //设置行起始地址
oled_send_one(priv, ((x&0xf0)>>4)|0x10, OLED_CMD);//设置低列起始地址
oled_send_one(priv, (x&0x0f)|0X01, OLED_CMD); //设置高列起始地址
return 0;
}
/*
*填充整个OLED显示屏
*oled_priv *priv 私有数据
*/
static void oled_clear(struct oled_priv *priv)
{
uint8_t y, x;
for(y=0; y<8; y++)
{
oled_set_pos(priv, 0, y);
for(x=0; x < X_WIDTH; x++)
{
oled_send_one(priv, 0x00, OLED_DATA);
}
}
}
.read函数实现
将虚拟地址baseaddr 的数据传给应用空间空间,便于检查是否和应用控制的buff一致。
static ssize_t oled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int ret = 0;
ret = copy_to_user(buf, baseaddr, 1024);
return 0;
}
.mmap函数实现
![3.8. mmap](images/3.8. mmap.png)
oled_mmap 的作用则是,在应用程序 mmap 后,由于内核中的 oled_mmap 函数的作用,应用空间的虚拟地址和内核中的虚拟地址映射到同一块物理地址(两个虚拟地址分别是内核中的 oled_data 和应用中的 buf),这一过程主要是通过 mmu 实现的。
static int oled_mmap(struct file *filp, struct vm_area_struct *vma)
{
/* 设置适当的标志,无论I/O内存是否存在:*/
// vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
// vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);//禁用缓存.
vma->vm_page_prot = pgprot_cached(vma->vm_page_prot);
if(!baseaddr)
{
return -1;
}
//调用 remap_pfn_range,计算PFN、大小和保护标志。
if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(baseaddr) >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot))
{
return -ENOBUFS;
}
return 0;
}
mmap的内核态函数为:
int (*mmap)(struct file *filp,struct vm_area_struct *vma)
结构体 struct vm_area_struct *vma 是我们在使用 mmap 系统调用的时候内核帮我们找到的虚拟地址区间,它的主要成员是:
vma->vm_start: 映射后的用户态虚拟地址起始地址;
vma->vm_end: 映射后的用户态虚拟地址结束地址;
vma->vm_pgoff: 物理地址所在的页帧号,它的值由用户空间传进来的物理地址右移PAGE_SHIFT位得到,PAGE_SHIFT值为12,那么它右移12位就得到物理地址的页帧号(一页大小为4KB)。
virt_to_phys(baseaddr) >> PAGE_SHIFT//获取与缓冲区偏移位置所在页面的PFN对应的PFN。
vma->vm_end - vma->vm_start;//检查映射大小是否大于我们的缓冲区大小。
.ioctrl函数实现
ioctrl刷新的数据display_buffer是mmap 在应用层 memcpy 获取的
static long oled_ioctrl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct oled_priv *priv = filp->private_data;
unsigned char *display_buffer = baseaddr;
switch(cmd)
{
case OLED_ON:
oled_display_buffer(priv, display_buffer, 1024);
break;
}
return 0;
}
.release函数实现
.release函数功能仅仅是向ecspi_oled显示屏发送关闭显示命令
static int oled_release(struct inode *inode, struct file *filp)
{
struct oled_priv *priv = filp->private_data;
oled_send_one(priv, 0xAE, OLED_CMD);//关闭显示
return 0;
}
3.8.4.3.2. oled测试应用程序实现
测试应用程序主要工作是实现oled显示屏显示图片。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#define W_WIDTH 128 //oled显示屏列数
#define Y_WIDTH 64 //oled显示屏行数
#define DEVNAME_LEN 64 //文件名长度
#define PLATDRV_MAGIC 0x16 //魔术字
#define OLED_ON _IO (PLATDRV_MAGIC, 0x18)
unsigned char *buf;
unsigned char rx_buf[1024];
unsigned char databuf[1024] =
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x0E,0xFE,0xFE,0xFE,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xC0,0xC0,0x80,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xC0,
0xC0,0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x06,0x0F,0x1F,0x3F,0x7E,0xFC,0xF8,0xF0,0xE0,0xC0,
0x00,0x00,0x00,0x00,0x00,0x00,0x1E,0x1E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,
0x0E,0x0E,0x0E,0x0E,0x0E,0xFF,0xFF,0xFF,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,
0x0E,0x0E,0x0E,0x0E,0x1E,0x1E,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x03,0x03,0x03,0x03,
0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,
0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,
0x03,0x03,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x07,0x0F,0x0F,
0x07,0x06,0x3C,0x3C,0x3C,0x3C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x9C,0xDC,0xFC,0xFC,
0xFC,0xDC,0x9C,0x1C,0x1C,0x1F,0x1F,0x1F,0x1C,0x9C,0xDC,0xFC,0xFC,0xFC,0xDC,0xDC,
0x9C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x3C,0x3C,0x3C,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xF8,0xFC,
0xF8,0x70,0x00,0xC0,0xE0,0xE0,0xF0,0xF8,0xF8,0x7C,0x3E,0x1F,0x0F,0x07,0x07,0x83,
0xC1,0xF0,0xF8,0xFE,0x7C,0x3C,0x18,0x08,0x00,0x00,0x01,0x01,0x03,0x03,0x07,0x07,
0x0F,0x1F,0x1F,0x3E,0x7C,0xFC,0xF8,0xF0,0xF0,0x60,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x0F,0x0F,0x0F,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,
0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0xC7,0xF7,0xFF,0xFF,0xFF,0x7F,0x37,
0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x07,
0x07,0x07,0x07,0x07,0x07,0x07,0x07,0x0F,0x0F,0x0F,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xE0,0xF8,0xFF,0xFF,0x1F,0x07,
0x01,0x00,0x00,0x00,0x00,0x01,0x01,0x81,0xC0,0xC0,0xE0,0xF0,0xF8,0xFC,0xFF,0xFF,
0xDF,0x9F,0x1F,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x9C,0xFC,0xFC,0xFC,0xFE,
0x3E,0x1E,0x06,0x00,0x00,0x00,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0xC0,0xE0,0xF0,0xFC,0xFE,0x3F,0x1F,0x0F,0x07,0x01,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x70,0xF0,0xF8,0xF0,0xE0,0xC0,0x80,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x80,0xE0,0xF8,0xFC,0xFF,0x7F,0x0F,0x03,0x00,0x00,0x00,
0x00,0x00,0x00,0x02,0x06,0x0F,0x1F,0x0F,0x0F,0x07,0x03,0x01,0x00,0x01,0x03,0x07,
0x0F,0x1F,0x3E,0x7C,0xF8,0xF0,0xE0,0xE0,0xF8,0xFC,0x7E,0x3F,0x1F,0x07,0x03,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xE0,0xF0,0xF8,
0xFC,0x7E,0x3F,0x1F,0x07,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x07,0x0F,0x1F,0x7F,0xFC,0xF8,0xF0,0xE0,
0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x07,0x07,0x01,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xE0,0xE0,0xE0,0xF0,0xF0,0xF8,0x78,
0x7C,0x3C,0x3E,0x1F,0x1F,0x0F,0x07,0x07,0x07,0x0F,0x0F,0x1F,0x1E,0x3C,0x3C,0x78,
0x78,0xF8,0xF0,0xF0,0xF0,0xE0,0xE0,0xE0,0xE0,0xC0,0xC0,0xC0,0xC0,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x1C,0x7E,0x3F,0x3F,0x3F,0x3F,0x3F,0x3D,
0x3C,0x3C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1C,0x1E,0x1E,0x1E,0x0E,0x0E,0x0E,
0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x0E,0x06,0x07,0x07,0x0F,
0x3F,0x7F,0xFE,0xF8,0x70,0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x01,0x07,0x07,0x03,0x03,0x03,0x03,0x01,0x01,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x01,0x01,0x01,0x03,0x03,0x03,0x03,0x03,0x01,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
int show_bmp(int fd, unsigned char *buffer, unsigned int length);
int oled_clear(int fd);
int main(int argc, char *argv[])
{
int rv = -1;
char dev_name[DEVNAME_LEN];
int fd = 0;
unsigned char *buf;
memset(dev_name, 0, sizeof(dev_name));
snprintf(dev_name, sizeof(dev_name), "/dev/spi_oled");
/*打开文件*/
fd = open(dev_name, O_RDWR, 0755);
if(fd < 0)
{
printf("file %s open failure!\n", dev_name);
return -1;
}
printf("open fd : %s [%d] successfully.\n", dev_name, fd);
buf = mmap(NULL,1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//内存映射,会调用驱动的mmap函数
if(NULL == buf)
{
printf("oled mmap failed!\r\n");
close(fd);
return -1;
}
else
{
printf("oled mmap ok!\r\n");
}
if(NULL == memcpy(buf, databuf, sizeof(databuf)))
{
printf("memcpy fail\n");
return -1;
}
for(int i=0; i<1024; i++)
{
printf("%x ", buf[i]);
}
printf("\n\n");
read(fd, rx_buf, 1024);
for(int i=0; i<1024; i++)
{
printf("%x ", rx_buf[i]);
}
rv = ioctl(fd, OLED_ON, 1);
if(rv == 0)
{
printf("ioctrl ok\n");
}
else
{
printf("ioctrl fail\n");
}
munmap(buf, 1024);//去除映射
sleep(30);
/*关闭文件*/
rv = close(fd);
if(rv < 0)
{
printf("close file error!\n");
}
return 0;
}
/*清屏函数
*fd,打开文件描述符
*/
int oled_clear(int fd)
{
int rv = 0;
int x, y;
char buff[1024];
memset(buff, 0x00, sizeof(buff));
/*执行写入*/
rv = write(fd, buff, sizeof(buff));
if(rv < 0)
{
printf("write to buf err:%d", rv);
return -1;
}
return 0;
}
/*显示图片
*fd,打开文件描述符
*buffer,要显示的数据地址
*length,数据长度
*/
int show_bmp(int fd, unsigned char *buffer, unsigned int length)
{
int rv;
/*执行写入*/
rv = write(fd, buffer, length);
if(rv < 0)
{
printf("show_bmp err, number write :%d \n", rv);
return -2;
}
printf("show_bmp ok, number write :%d \n", rv);
return 0;
}
用户态的mmap函数接口为:
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)
函数参数的意义如下:
addr:指定映射的起始地址,即进程虚拟空间的虚拟地址,人为的指定;通常设置为NULL,让内核帮我们去指定;
len: 要映射的区间大小;
prot: 映射区的保护方式,可以取以下值:
PROC_EXEC:映射区可被执行;
PROC_READ:映射区可被读取;
PROC_WRITE:映射区可写;
PROC_NONE:映射区不能存取。
flags是映射区的特性,可以取以下值:
MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享;
MAP_PRIVATE:对映射区的写入会产生一个映射区的复制(COPY_ON_WRITE),对此映射区的修改不会写入源文件;
fd:由open函数返回的文件描述符;
offset:文件开始处的偏移量,必须是分页大小的整数倍。
函数返回值:映射得到的用户虚拟地址;
3.8.4.3.3. Makefile
KERNAL_DIR := /home/zying/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-
TFTP_DTR := /tftp/zouying
PWD := $(shell pwd)
obj-m := spi_oled.o
modules:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
$(CROSS_COMPILE)gcc spi_app.c -o spi_app
@make clear
cp spi_oled.ko spi_app $(TFTP_DTR) -f
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f *.ko
3.8.4.4. 程序运行结果
3.8.4.4.1. 加载设备树和驱动文件
将之前编译好的oled.dtbo文件传送到开发板中dt overlay文件所在位置,并修改config.txt文件,在该文件中添加对oled的dt overlay支持。
# Enable extra overlays
dtoverlay_extra=oled_spi
重启开发板后可以看到如下的打印信息
Applying DT overlay ==> oled_spi.dtbo
1545 bytes read in 3 ms (502.9 KiB/s)
然后将之前编译好的 spi_oled.ko 文件传送到开发板中,使用 insmod spi_oled.ko 装载该模块文件。
root@igkboard:~/driver/09spi_oled# insmod spi_oled.ko
root@igkboard:~/driver/09spi_oled# ./spi_app
open fd : /dev/spi_oled [3] successfully.
oled mmap ok!
ioctrl ok
^C
root@igkboard:~/driver/09spi_oled# rmmod spi_oled.ko
装载完成后,可以在 /dev 文件夹下看到 oled_spi 设备
root@igkboard:/dev# find -name spi_oled
./spi_oled
3.8.4.4.2. 测试效果
驱动加载成功后直接运行测试应用程序 ./spi_oled,正常情况下显示屏会显示设定的内容,如下所示。