目录
一.背景
SPI 是英语 Serial Peripheral interface(串行外设接口) 的缩写,顾名思义就是串行外围设备接口。是 Motorola首先在其 MC68HCXX 系列处理器上定义的。SPI 接口主要应用在 EEPROM,FLASH,实时时钟,AD 转换器,还有数字信号处理器和数字信号解码器之间。
SPI 是一种高速的,全双工,同步的串行通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议。
标准SPI采用四线组网方式,如下图:


SCLK:时钟信号线。与I2C类似的,所有设备都都需要时钟信号线进行同步,该信号由主机负责提供;
➢ MOSI:主出从入,相当于主机的数据发送线;
➢ MISO:主入从出,相当于主机的数据接收线;
➢ CS:片选信号,与I2C不同,SPI设备没有所谓的从机地址,连接在总线上的设备通过CS片选信号选择。在同一时刻,总线上只有一个外设被选择,CS信号线通常是低电平有效的。
以上的四线方式构成了SPI全双工串行同步通信方式。需要注意的是,对于MOSI和MISO来说不是必须存在的,可以只有MOSI或者只有MISO。当然这样的话就只能实现单工通信方式了,这样的SPI成为三线SPI。至于到底采用三线方式还是四线方式取决于外设本身。例如,OLCD只需要主机发送数据而不需要从机发送数据,就可以使用三线SPI的连接方式。
SPI一主机多从机,通过拉低CS片选信号选择设备。
SPI的数据传输的时序:要理解SPI的通信时序。需要搞清楚SPI的两个重要概念。时钟极性(Clock Polarity 简称CPOL)和时钟相位(Clock Phase简称CPHA)。注意这两个概念都是跟时钟信号线相关的。
- CPOL:规定了时钟信号线在空闲时的状态,CPOL=0表示时钟信号线在空闲时为低电平;CPOL=1表示时钟信号线在空闲是为高电平;
- CPHA:与I2C不同,SPI的数据接收方并不是在时钟高或者低电平时采样的,而是在时钟信号处于沿时,至于是在上升沿还是下降沿取决于CPHA。CPHA=0表示是在时钟信号变化的第一个沿时采样
CPHA=1表示是在时钟信号变化的第二个沿时采样。
因此不难理解,这样的排列组合总共能够组合 出四种方式,具体使用哪种方式是由外设厂家规定的,因此在使用SPI之前就不得不读懂外设的说明文档。一下就是四种方式的时序图,来自《IMX6ULL参考手册.pdf》P794页。
SPI四种工作模式

CPOL=1, CPHA=0:空闲时SCLK为高电平,SPI在下降沿采样
CPOL= 1,CPHA=1:空闲时SCLK为高电平,SPI在上升沿采样
CPOL=0, CPHA=0:空闲时SCLK为低电平,SPI在上升沿采样
CPOL=0, CPHA=1:空闲时SCLK为低电平,SPI在下降沿采样
无论哪种工作方式,SPI这种串行通信方式都是MSB先行的。
三种协议对比
| UART | I2C | SPI |
|---|---|---|
| 全双工、异步、串行 | 半双工、同步、串行 | 全双工、同步、串行 |
| 两根线 TXD RXD | 两根线 SCL SDA | 四根线 SCLK MOSI MISO CS |
| < 5MHZ | 100KHZ - 3.4MHZ | 1M - 100MHZ |
| 点对点 | 多主多从 | 一主多从 |
| 低位先行 | 高位先行 | 高位先行 |
ADXL345传感器
测量X,Y,Z三轴的重力加速度


写时寄存器地址最高位为低电平,读时为高电平。
INT1/2中断引脚,里面有寄存器可以设置XYZ三轴的阈值,当超过阈值触发中断
SDO->MISO
SDA->MOSI
CS->片选

CS接17号,SCL接18号,MISO/SDO接19号,MOSI/SDA接20号引脚
二.寄存器
con


将60MHZ分频为1MHZ

四位,将哪一位置1就是将该通道设置为主机模式

三种启动方式:
1.切换通道
2.片选线拉低
3.SMC位,向发送缓冲区写数据自动开启
EN:在配置其他寄存器之前,将此位置1
config

SCLK_CTL置1,默认高电平

SS_POL:片选极性,(极性即默认电平的高低),上面引脚接线图将CS接到GPIO引脚,所以可通过GPIO设置,此处默认即可。

SS_CTL:置0对应上面reg寄存器的发送数据就开始通信,置1说明片选线拉低才通信。
SCLK_POL:表示空闲时为高还是低电平。
SCLK_PHA即CPHA,表示第几个沿发送数据,时钟极性和相位都应置1.
start

RR:置1表示超过一位字节数据在接收缓冲区
TE同理。
三.实现
1.初始化
cpp
// spi3_channel0初始化
void spi3_channel0_init(void)
{
// 1. spi引脚复用功能
IOMUXC_SetPinMux(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0); // CS
IOMUXC_SetPinMux(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0); // SCLK
IOMUXC_SetPinMux(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0); // MISO
IOMUXC_SetPinMux(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0); // MOSI
// 2. spi引脚电气属性
IOMUXC_SetPinConfig(IOMUXC_UART2_TX_DATA_GPIO1_IO20, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_RX_DATA_ECSPI3_SCLK, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_RTS_B_ECSPI3_MISO, 0x10B1);
IOMUXC_SetPinConfig(IOMUXC_UART2_CTS_B_ECSPI3_MOSI, 0x10B1);
// 3. 通信前,将CS拉高
GPIO_Type_t t =
{
.direction = gpio_output,
.default_value = 1
};
gpio_init(GPIO1, 20, &t);
// 4. SPI寄存器
ECSPI3->CONREG = 0;
ECSPI3->CONREG |= (7 << 20) | (0x0E << 12) | (2 << 8) | (1 << 4) | (1 << 3) | (1 << 0);
ECSPI3->CONFIGREG = 0;
ECSPI3->CONFIGREG |= (1 << 20) | (1 << 4) | (1 << 0);
};
电气属性更改高压摆率即可,其余和点灯功能相同。
2.读写
原理

SPI是全双工移位通信:主从两侧各有一个移位寄存器。每个时钟沿到来时,主机通过 MOSI 往外"移出"1位,同时从 MISO "移入"1位。也就是------每"写"一位的同时必然"读"到一位。
写即是读:你往总线"写"一个字节(移出8位),同一过程里也会"读"回从机在MISO上的8(可能是无效/占位字节)。
读即是写:你想从从机"读"数据,必须提供时钟,所以主机必须同时"写"出一个占位字节(通常是0xFF或0x00),用这些时钟把从机的数据"移入"。
cpp
// spi读写
unsigned char spi3_read_write(unsigned char data)
{
while ((ECSPI3->STATREG & (1 << 0)) == 0);
ECSPI3->TXDATA = data;
while ((ECSPI3->STATREG & (1 << 3)) == 0);
return ECSPI3->RXDATA;
}
ADC调用
spi 写:
主机发送寄存器地址 -> 主机发送要写入的数据
spi 读:
主机发送寄存器地址 -> 发送占位字节 Dummy(0xFF 0x00) -> 从机回复数据
cpp
// adxl345写
void adxl345_write(unsigned char reg_addr, unsigned char data)
{
// 拉低CS
gpio_set_value(GPIO1, 20, 0);
// 发送寄存器地址
spi3_read_write(reg_addr);//发完会回复一个字节,无用不管
// 发送数据
spi3_read_write(data);
// 拉高CS
gpio_set_value(GPIO1, 20, 1);
}
// adxl345读
unsigned char adxl345_read(unsigned char reg_addr)
{
unsigned char ret = 0;
// 拉低CS
gpio_set_value(GPIO1, 20, 0);
// 发送寄存器地址
spi3_read_write(reg_addr | 0x80);
// 发送Dummy
ret = spi3_read_write(0xFF);
// 拉高CS
gpio_set_value(GPIO1, 20, 1);
return ret;
}


cpp
struct ADXL345_Data
{
short x;
short y;
short z;
float xg;
float yg;
float zg;
};
// adxl345初始化
void adxl345_init(void)
{
// 关闭中断
adxl345_write(0x2E, 0x08);
// 设置满分辨率 + 设置满量程
adxl345_write(0x31, 0x0B);
// 低功耗模式 + 速率
adxl345_write(0x2C, 0x08);
// 配置测量模式 + 唤醒模式
adxl345_write(0x2D, 0x0B);
}
// 获得adxl345三轴加速度
struct ADXL345_Data adxl345_collect(void)
{
struct ADXL345_Data ret;
// 读取x轴数据
ret.x = adxl345_read(0x32);
ret.x |= adxl345_read(0x33) << 8;
// 读取y轴数据
ret.y = adxl345_read(0x34);
ret.y |= adxl345_read(0x35) << 8;
// 读取z轴数据
ret.z = adxl345_read(0x36);
ret.z |= adxl345_read(0x37) << 8;
// 获得x y z三轴加速度
ret.xg = ret.x * 0.0039;
ret.yg = ret.y * 0.0039;
ret.zg = ret.z * 0.0039;
return ret;
}