版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

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设备驱动非常相似,可对比学习。

3.8.ecspi001

如框架图所示,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. 硬件连接说明

3.8.硬件接口

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,正常情况下显示屏会显示设定的内容,如下所示。

3.8.lingyun