本次说一下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口.
就说这么多吧.