⑰tiny4412 Linux驱动开发之SPI子系统(spi LCD)驱动程序

本次说一下SPI子系统,目前代码效果还不是很理想,但是还是可以看到有数据成功交互了,所以,先贴出来暂时先用着.

SPI 协议简介  
SPI协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在ADC 、LCD 等设备与MCU 间,要求通讯速率较高的场合,SPI 通讯设备之间的常用连接方式见图


SPI通讯使用3条总线及片选线,3条总线分别为SCK、MOSI、MISO,片选线为/SS,它们的作用介绍如下:

(1)SS(Slave Select):从设备选择信号线,常称为片选信号线,也称为NSS 、CS,以下用NSS表示。

当有多个SPI从设备与SPI主机相连时,设备的其它信号线SCK、MOSI及MISO同时并联到相同的SPI总线上,即无论有多少个从设备,都共同只使用这3条总线;而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。I2C协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用NSS 信号线来寻址,当主机要选择从设备时,把该从设备的NSS信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行SPI通讯。所以SPI通讯以NSS线置低电平为开始信号,以NSS线被拉高作为结束信号.

(2)SCK(Serial Clock):时钟信号线,用于通讯数据同步。
它由通信主机产生,决定了通讯的速率,不同的设备支持的最高时钟频率不一样,如STM32的SPI时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备.

(3)  MOSI (Master Output ,  Slave Input) :主设备输出/从设备输入引脚。
主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。

(4)MISO(Master Input,Slave Output):主设备输入/从设备输出引脚。
主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机.

SPI基本通信过程

这是一个主机的通信时序。NSS 、SCK、MOSI 信号都由主机控制产生,而MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI与MISO的信号只在NSS为低电平的时候才有效,在SCK的每个时钟周期MOSI和MISO传输一位数据。

1).通信的起始和停止信号

在上图的标号①处,NSS信号线由高变低,是SPI通信的起始信号。NSS是每个从机各自独占的信号线,当从机在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通信。在图中的标号⑥处,NSS信号由低变高,是SPI通信的停止信号,表示本次通信结束,从机的选中状态被取消.

2).数据有效性  
SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行并没有作硬性规定,但要保证两个SPI通讯设备之间使用同样的协定,一般都会采用上图中的MSB先行模式。观察图中的标号处,MOSI及MISO的数据在SCK的上升沿期间变化输出,在SCK的下降沿时被采样。即在SCK 的下降沿时刻,MOSI及MISO的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI及MISO为下一次表示数据做准备。SPI 每次数据传输可以8 位或16 位为单位,每次传输的单位数不受限制。

3).CPOL/CPHA 及通讯模式  
上面讲述的上图中的时序只是SPI中的其中一种通讯模式,SPI一共有四种通讯模式,它们的主要区别是总线空闲时SCK的时钟状态以及数据采样时刻。为方便说明,在此引入“时钟极性CPOL”和“时钟相位CPHA”的概念。时钟极性CPOL是指SPI通讯设备处于空闲状态时,SCK信号线的电平信号(即SPI通讯开始前、NSS线为高电平时SCK的状态)。CPOL=0时,SCK在空闲状态时为低电平,CPOL=1时,则相反。
时钟相位CPHA 是指数据的采样的时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在SCK时钟线的“奇数边沿”被采样。当CPHA=1时,数据线在SCK的“偶数边沿”采样。

我们来分析这个CPHA=0的时序图。首先,根据SCK在空闲状态时的电平,分为两种情况。SCK信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。无论CPOL=0还是=1,因为我们配置的时钟相位CPHA=0,在图中可以看到,采样时刻都是在SCK的奇数边沿。注意当 CPOL=0的时候,时钟的奇数边沿是上升沿,而CPOL=1的时候,时钟的奇数边沿是下降沿。所以SPI的采样时刻不是由上升/下降沿决定的。MOSI和MISO数据线的有效信号在SCK的奇数边沿保持不变,数据信号将在SCK奇数边沿时被采样,在非采样时刻,MOSI和MISO的有效信号才发生切换。类似地,当CPHA=1时,不受CPOL的影响,数据信号在SCK的偶数边沿被采样,见下图:

由CPOL及CPHA的不同状态,SPI分成了四种模式,见下表,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式0”与“模式3”。

下面是4种模式的波形图:

    

SPI驱动框架

    

如上图(网络摘图),可以看到SPI的框架流程和I2C的框架是类似的,我们只要能够理解I2C的驱动架构,那么SPI的驱动架构也就大概可以知道了,同样,Linux内核或者芯片原厂已经把SPI适配器层已经提供,我们要开发的就是SPI控制层,也就是上图中的SPI通用设备驱动程序,我们主要以实际应用的角度去开发,本系列之前的一些博客同样也是,至于框架,整体驱动和理论,以后,有机会再说吧.

我们对SPI通用控制层主要的操作就是响应用户数据请求,包括读/写,主要是把上层传递来的数据,封装成约定的形式,调用SPI控制器驱动程序提供的数据操作接口去完成相应的任务,下面,我们来看一下数据交互API的一个流程图(网络摘图):

SPI主要API

我们写SPI通用驱动程序,主要是围绕读写相关的函数来写的,下面介绍一下这些API:

(以下6个API来自:https://www.cnblogs.com/sky-heaven/p/5736638.html)

1).spi_write(): SPI发送函数,数据放在buf中,然后把要发送的数据放在工作队列中

/**  
 * spi_write - SPI synchronous write  
 * @spi: device to which data will be written  
 * @buf: data buffer  
 * @len: data buffer size  
 * Context: can sleep  
 *  
 * This writes the buffer and returns zero or a negative error code.  
 * Callable only from contexts that can sleep.  
 */
static inline int
spi_write(struct spi_device *spi, const void *buf, size_t len)
{
    struct spi_transfer t = {
        .tx_buf = buf,
        .len    = len,
    };
    struct spi_message m;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    
    return spi_sync(spi, &m);
}

2).spi_read(): SPI接收函数,数据放在buf中,然后把要发送的数据放在工作队列中,发送出去

/**  
 * spi_read - SPI synchronous read  
 * @spi: device from which data will be read  
 * @buf: data buffer  
 * @len: data buffer size  
 * Context: can sleep  
 *  
 * This reads the buffer and returns zero or a negative error code.  
 * Callable only from contexts that can sleep.  
 */
static inline int
spi_read(struct spi_device *spi, void *buf, size_t len)
{
    struct spi_transfer t = {
        .rx_buf = buf,
        .len    = len,
    };
    struct spi_message m;

    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    
    return spi_sync(spi, &m);
}

3).spi_w8r8()

/* this copies txbuf and rxbuf data; for small transfers only! */
extern int spi_write_then_read(struct spi_device *spi,
                               const void *txbuf, unsigned n_tx,
                               void *rxbuf, unsigned n_rx);
/**  
 * spi_w8r8 - SPI synchronous 8 bit write followed by 8 bit read  
 * @spi: device with which data will be exchanged  
 * @cmd: command to be written before data is read back  
 * Context: can sleep  
 *  
 * This returns the (unsigned) eight bit number returned by the  
 * device, or else a negative error code.  Callable only from  
 * contexts that can sleep.  
 */
static inline ssize_t spi_w8r8(struct spi_device *spi, u8 cmd)
{
    ssize_t status;
    u8 result;
    status = spi_write_then_read(spi, &cmd, 1, &result, 1);

    /* return negative errno or unsigned value */
    return (status < 0) ? status : result;
}

4).spi_w8r16()

/**  
 * spi_w8r16 - SPI synchronous 8 bit write followed by 16 bit read  
 * @spi: device with which data will be exchanged  
 * @cmd: command to be written before data is read back  
 * Context: can sleep  
 *  
 * This returns the (unsigned) sixteen bit number returned by the  
 * device, or else a negative error code.  Callable only from  
 * contexts that can sleep.  
 *  
 * The number is returned in wire-order, which is at least sometimes  
 * big-endian.  
 */
static inline ssize_t spi_w8r16(struct spi_device *spi, u8 cmd)
{
    ssize_t status;
    u16 result;

    status = spi_write_then_read(spi, &cmd, 1, (u8 *)&result, 2);

    /* return negative errno or unsigned value */
    return (status < 0) ? status : result;
}

5).spi_write_then_read()

int spi_write_then_read(struct spi_device *spi,
                        const void *txbuf, unsigned n_tx,
                        void *rxbuf, unsigned n_rx)
{
    static DEFINE_MUTEX(lock);

    int status;
    struct spi_message message;
    struct spi_transfer x[2];
    u8 *local_buf;

    /* Use preallocated DMA-safe buffer.  We can't avoid copying here,  
        * (as a pure convenience thing), but we can keep heap costs  
        * out of the hot path ...  
        */
    if ((n_tx + n_rx) > SPI_BUFSIZ)
        return -EINVAL;

    spi_message_init(&message);
    memset(x, 0, sizeof x);
    if (n_tx)
    {
        x[0].len = n_tx;
        spi_message_add_tail(&x[0], &message);
    }
    if (n_rx)
    {
        x[1].len = n_rx;
        spi_message_add_tail(&x[1], &message);
    }

    /* ... unless someone else is using the pre-allocated buffer */
    if (!mutex_trylock(&lock))
    {
        local_buf = kmalloc(SPI_BUFSIZ, GFP_KERNEL);
        if (!local_buf)
            return -ENOMEM;
    }
    else
        local_buf = buf;

    memcpy(local_buf, txbuf, n_tx);
    x[0].tx_buf = local_buf;
    x[1].rx_buf = local_buf + n_tx;
    /* do the i/o */
    status = spi_sync(spi, &message);
    if (status == 0)
        memcpy(rxbuf, x[1].rx_buf, n_rx);

    if (x[0].tx_buf == buf)
        mutex_unlock(&lock);
    else
        kfree(local_buf);

    return status;
}

6).spi_sync(): 读写都会调用到spi_sync

int spi_sync(struct spi_device *spi, struct spi_message *message)  
{  
    return __spi_sync(spi, message, 0);  
}

下面,我来说一下为什么读写都会用到spi_sync().

SPI工作原理

①硬件上为4根线。
②主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。
③串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。
④外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。

需要注意的是,摩托罗拉公司的风格就是大端模式,不要小看摩托罗拉公司,当年可以和英特尔平分秋色的,可以说是两大阵营,摩托罗拉主导大端模式,英特尔主导小端模式,然后现在发展的情况就是网络上普遍使用大端,处理器上普遍使用小端,ARM处理器大小端都支持,但是默认使用的是小端,可以通过配置寄存器的方式改变,回到正题,SPI标准是摩托罗拉公司提出的,所以也采用的是大端,发数据是先发高位,后发地位,与此对应,接收数据是先收地位,后收高位,由于寄存器结构的原因,当然,不排除有的IC厂商改变这一规则,只要能拿到正确的数据,底层实现就算改了,不是也没影响吗!哈哈.

下面我们就围绕具体硬件写一个SPI驱动程序,驱动的是一块SPI屏,因为屏幕只需要接收数据并显示即可,不需要回传数据,所以,这个屏没有引出MISO引脚,只有CS, CLK, MOSI三条,下面是SPI屏的引脚定义:

从引脚定义可以看出,除了传输SPI需要的3条线之外,还需要两条线分别控制RS和RST两个端口,其中RS是命令/数据选择线,高电平发送数据,低电平发送命令,RST则为复位引脚,查看tiny4412的原理图可知,板子上引出了SPI0,如下图:

然后IO口方面,我们采用了如下两个:

通过查看核心板原理图可知,两个GPIO口是GPX3_2和GPX3_3,如下图:

下面是完整的SPI LCD驱动程序:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/spi/spi.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/gpio.h>


#define GPIO_RS         EXYNOS4_GPX3(2)     // 0:cmd, 1:data
#define GPIO_RESET      EXYNOS4_GPX3(3)

#if 0
#define X_MAX_PIXEL     176
#define Y_MAX_PIXEL     220
#else
#define X_MAX_PIXEL     220
#define Y_MAX_PIXEL     176
#endif

struct regdata_t {
    u8  reg;
    u16 data;
    int delay_ms;
} regdatas[] = {
    {0x10, 0x0000, 0}, {0x11, 0x0000, 0}, {0x12, 0x0000, 0},
    {0x13, 0x0000, 0}, {0x14, 0x0000, 40},

    {0x11, 0x0018, 0}, {0x12, 0x1121, 0}, {0x13, 0x0063, 0},
    {0x14, 0x3961, 0}, {0x10, 0x0800, 10}, {0x11, 0x1038, 30},

    {0x02, 0x0100, 0}, {0x01, 0x001c, 0}, {0x03, 0x1038, 0},

    {0x07, 0x0000, 0}, {0x08, 0x0808, 0}, {0x0b, 0x1100, 0},
    {0x0c, 0x0000, 0}, {0x0f, 0x0501, 0}, {0x15, 0x0020, 0},
    {0x20, 0x0000, 0}, {0x21, 0x0000, 0},

    {0x30, 0x0000, 0}, {0x31, 0x00db, 0}, {0x32, 0x0000, 0}, {0x33, 0x0000, 0},
    {0x34, 0x00db, 0}, {0x35, 0x0000, 0}, {0x36, 0x00af, 0}, {0x37, 0x0000, 0},
    {0x38, 0x00db, 0}, {0x39, 0x0000, 0},

    {0x50, 0x0603, 0}, {0x51, 0x080d, 0}, {0x52, 0x0d0c, 0}, {0x53, 0x0205, 0},
    {0x54, 0x040a, 0}, {0x55, 0x0703, 0}, {0x56, 0x0300, 0}, {0x57, 0x0400, 0},
    {0x58, 0x0b00, 0}, {0x59, 0x0017, 0},

    {0x0f, 0x0701, 0}, {0x07, 0x0012, 50}, {0x07, 0x1017, 0},
};

static inline int
spi_write1(struct spi_device *spi, const void *buf, size_t len)
{
    struct spi_transfer t = {
	.tx_buf = buf,
	.len	= len,
    };

    struct spi_message m;
    
    spi_message_init(&m);
    spi_message_add_tail(&t, &m);

    return spi_sync(spi, &m);
}

void
write_command(struct spi_device *spi, u8 cmd)
{
    gpio_direction_output(GPIO_RS, 0);  // 低电平

    spi_write1(spi, &cmd, 1);
}

void
write_data(struct spi_device *spi, u8 data, int len)
{
    gpio_direction_output(GPIO_RS, 1);  // 高电平

    //while(len > 0)
	//spi_write1(spi, data + (--len), 1);
    spi_write1(spi, &data, 1);
}

void
write_regdata(struct spi_device *spi, u8 cmd, unsigned short data)
{
    write_command(spi, cmd);
    //write_data(spi, (u8 *)&data, 2);
    write_data(spi, (data >> 8) & 0xff, 1);
    write_data(spi, data & 0xff, 1);
}

void
Lcd_SetRegion(struct spi_device *spi, u8 xStart, u8 yStart, u8 xEnd, u8 yEnd)
{
    write_regdata(spi, 0x38, xEnd);
    write_regdata(spi, 0x39, xStart);
    write_regdata(spi, 0x36, yEnd);
    write_regdata(spi, 0x37, yStart);
    write_regdata(spi, 0x21, xStart);
    write_regdata(spi, 0x20, yStart);

    write_command(spi, 0x22);
}

int
exynos_spi_probe(struct spi_device *spi)
{
    int i, j;
    u16 color = 0x001f; // blue

    // lcd reset
    gpio_request(GPIO_RESET, "GPIO_RS");
    
    gpio_direction_output(GPIO_RESET, 0);   // 低电平
    mdelay(100);
    gpio_direction_output(GPIO_RESET, 1);   // 高电平
    mdelay(50);

    gpio_free(GPIO_RESET);
    gpio_request(GPIO_RS, "GPIO_RS");
    
    // lcd init
    for(i = 0; i < ARRAY_SIZE(regdatas); i++){
        write_regdata(spi, regdatas[i].reg, regdatas[i].data);
        if(regdatas[i].delay_ms)
            mdelay(regdatas[i].delay_ms);
    }

    // lcd display
    Lcd_SetRegion(spi, 0, 0, X_MAX_PIXEL - 1, Y_MAX_PIXEL - 1);     // 设置显示区域

#define rgb(r, g, b)    ((((r >> 3) & 0x1f) << 11) | (((r >> 2) & 0x3f) << 5) | ((r >> 3) & 0x1f))
#if 1
    for(i = 0; i < Y_MAX_PIXEL / 2; i++){
        color = rgb(0, 0, 255);
        for(j = 0; j < X_MAX_PIXEL / 2; j++){
            write_data(spi, (u8)(color >> 8), 1);
            write_data(spi, (u8)color, 1);
	}

        color = rgb(255, 0, 0);
        for(j = X_MAX_PIXEL / 2; j < X_MAX_PIXEL; j++){
            write_data(spi, (u8)(color >> 8), 1);
            write_data(spi, (u8)color, 1);
	}
    }

    for(i = Y_MAX_PIXEL / 2; i < Y_MAX_PIXEL; i++){
        color = rgb(0, 255, 0);
        for(j = 0; j < X_MAX_PIXEL / 2; j++){
            write_data(spi, (u8)(color >> 8), 1);
            write_data(spi, (u8)color, 1);
	}

        color = rgb(255, 255, 0);
        for(j = X_MAX_PIXEL / 2; j < X_MAX_PIXEL; j++){
            write_data(spi, (u8)(color >> 8), 1);
            write_data(spi, (u8)color, 1);
	}
    }
#else
    color = rgb(0, 0, 0);
    for(i = 0; i < X_MAX_PIXEL; i++){
	for(j = 0; j < Y_MAX_PIXEL; j++){
	    write_data(spi, (u8)((color >> 8) & 0xff), 1);
	    write_data(spi, (u8)(color & 0xff), 1);
	}
	mdelay(2);
    }
#endif
    
    gpio_free(GPIO_RS);
    
    return 0;
}

int
exynos_spi_remove(struct spi_device *spi)
{
    return 0;
}

// 设备树式查找自定义数据
struct of_device_id spi_ids[] = {
    {.compatible = "myspilcd"},
    {},
};

struct spi_driver exynos_spilcd_drv = {
    .driver = {
        .name = "spidev",
        //.of_match_table = spi_ids,
    },
    .probe  = exynos_spi_probe,
    .remove = __devexit_p(exynos_spi_remove),
    // .id_table = exynos_id_table,
};

module_spi_driver(exynos_spilcd_drv);

MODULE_LICENSE("GPL");

然后是makefile:

#指定内核源码路径
KERNEL_DIR = /home/george/1702/exynos/linux-3.5

#ָ指定当前路径
CUR_DIR = $(shell pwd)


#MYAPP = dht11_app
#MODULE = spi_flash
MODULE = spi_lcd

all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
#	arm-none-linux-gnueabi-gcc -o $(MYAPP) $(MYAPP).c
clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
	$(RM) $(MYAPP)
install:
	cp -raf *.ko $(MYAPP) /home/george/1702/exynos/filesystem/1702

#ָ指定当前项目编译的目标
obj-m = $(MODULE).o

上面的驱动程序中,我们没有指定SPI是哪个,原因是这样的:

我们在menuconfig中已经配置成如下:

Device Drivers  --->
	[*] SPI support  ---> 
		<*>   Samsung S3C64XX series type SPI
			[*]     Samsung S3C64XX Channel 0 Support.
			[ ]     Samsung S3C64XX Channel 2 Support.

可以看到,值配置了SPI0,所以,能用到的只有SPI0.另外,我们通过mach-tiny4412.c可以看出如下代码:

#ifdef CONFIG_S3C64XX_DEV_SPI0
static struct s3c64xx_spi_csinfo spi0_csi[] = {
	[0] = {
		.line = EXYNOS4_GPB(1),
		.fb_delay = 0x0,
	},
};

static struct spi_board_info spi0_board_info[] __initdata = {
	{
		.modalias = "spidev",
		.platform_data = NULL,
		.max_speed_hz = 10*1000*1000,
		.bus_num = 0,
		.chip_select = 0,
		.mode = SPI_MODE_0,
		.controller_data = &spi0_csi[0],
	}
};
#endif

#ifdef CONFIG_S3C64XX_DEV_SPI1
static struct s3c64xx_spi_csinfo spi1_csi[] = {
	[0] = {
		.line = EXYNOS4_GPB(5),
		.fb_delay = 0x0,
	},
};

static struct spi_board_info spi1_board_info[] __initdata = {
	{
		.modalias = "spidev",
		.platform_data = NULL,
		.max_speed_hz = 10*1000*1000,
		.bus_num = 1,
		.chip_select = 0,
		.mode = SPI_MODE_0,
		.controller_data = &spi1_csi[0],
	}
};
#endif

...

它们匹配的平台总线都是spidev,区别就在于bus_num这一项,我们可以在写数据时指定用哪一个SPI口.

就说这么多吧.

猜你喜欢

转载自blog.csdn.net/qq_23922117/article/details/81637295