SPI通信协议详解及SPI驱动编写实例
1.接口定义:
SPI 总线是由前摩托罗拉公司命名的一种工作与全双工模式的同步数据通信标准。SPI 是单主设备(single-master)通信协议,这意味着总线中的只有一支中心设备能发起通信。
当SPI以主从方式工作的,通常有一个主器件和一个或多个从器件,当具有多个设备的时候通过片选信号选中/失能设备,通常 SPI 使用3条通讯总线和1条片选线,定义如下:
MOSI – 主器件数据输出,从器件数据输入
MISO – 主器件数据输入,从器件数据输出
SCLK – 时钟信号,由主器件产生
nSS – 从器件片选/使能信号,由主器件控制,低电平有效
-- 在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。
-- 在多个从器件的系统中,每个从器件需要独立的使能信号,硬件上比 I2C 系统要稍微复杂一些。
注意:SPI也有3线模式。即将 MOSI,MISO合并为 DATA 线,但此时 SPI 就不再能实现高效的全双工通信了。
2.工作模式
SPI 根据时钟极性和时钟相位的不同可以有4种工作模式,通常在编写 SPI 设备驱动程序时需要特别注意从设备的时钟相位与时钟极性,如果与主设备(SOC)不一致时,一般都不能进行正常的通信。
-- 时钟极性(CPOL)指通讯设备处于空闲状态( SPI 开始通讯前、nSS 线无效)时 SCK 的状态。
CPOL = 0:SCK在空闲时为低电平
CPOL = 1:SCK在空闲时为高电平
-- 时钟相位(CPHA)指数据的采样时刻位于 SCK 的偶数边沿采样还是奇数边沿采样。
CPHA = 0:在SCK的奇数边沿采样
CPHA = 1:在SCK的偶数边沿采样
-- 通过时钟极性和时钟相位的不同组合 SPI 总共可以设置为4种工作模式
MODE0 : CPOL = 0 CPHA = 0 SCK空闲为低,SCK的奇数次边沿采样
MODE1 : CPOL = 0 CPHA = 1 SCK空闲为低,SCK的偶数次边沿采样
MODE2 : CPOL = 1 CPHA = 0 SCK空闲为高,SCK的奇数次边沿采样
MODE3 : CPOL = 1 CPHA = 1 SCK空闲为高,SCK的偶数次边沿采样
3.SPI时序分析实例
SPI 通讯时 nSS 、SCK 、MOSI 信号均由主机产生,MISO 信号由从机产生。在 nSS 为低电平(片选选中)的前提下,MOSI 和 MISO 信号才有效。在每个时钟周期 MOSI 和 MISO 传输一位数据。跟I2C通讯类似,SPI通讯也需要通讯的起始/结束信号,有效数据 和 同步时钟。
起始/结束信号:
nSS 信号由高电平变为低电平即为 SPI 通讯的起始信号,反过来, nSS 信号由低电平变为高电平即为SPI通讯的结束信号。
当从机检测到自身的 nSS 引脚被拉低时就知道自己被主机选中,准备和主机进行通讯。
数据同步:
SCK 用于数据同步,MOSI 和 MISO 线上的数据在每个 SCK 时钟周期传输一位数据,数据的输入/输出可以同时进行(双线全双工)。
3.1 ADE7953 SPI 时序分析
现在通过分析一款之前笔者使用过的芯片 ADE7953,来对 SPI 进行跟深入的理解。首先我们查看其用户手册,找到它的 SPI 时序图。
上图即是ADE7953的时序图和时序图中对应的参数表,通常我们分析时序图都是结合参数表一起来分析的。其中作为编程者的角度我们需要理解的东西并不多,下面我们结合部分参数对这个时序图进行简单的分析:
1. 首先主设备拉低CS片选选中该设备。
2. 延时至少t_CS之后,主设备拉低时钟线,等待最多t_SF后,时钟线变为低电平。
3. 在时钟线被拉低后,再经过t_DAV的时间,该设备会在MISO线上准备好数据供主设备读取主设备应在数据保持的时间内读取出来(这里的数据保持时间比较长)
4. 时钟线低电平维持至少t_SL后,开始进入上升沿,等待最多t_SR时间,时钟线变为高
5. 主设备需要时钟线为高之前至少 t_DSU时间内,在MOSI上建立需要数据,因为从设备会在时钟线为高之后的t_DHD时间内,从MOSI线上读取数据同时主机需要在这个时间内一直维持数据有效。
6. 时钟线高电平维持至少t_SH后,拉低时钟线,一个周期结束,开始下一个周期的数据传输
通过以上的分析可以了解到:
从设备会在时钟线拉低之后延时 t_DAV 之后,输出数据,直到时钟线为低,数据仍然保持.
从设备会在上升沿结束后的 t_DHD 时间内读取主设备写入到 MOSI 上的数据.
同时也可以看出,时钟线空闲时为高电平(CPOL = 1),数据采样发生在SCLK的第2个边沿,即偶数边沿(CPHA = 1)所以如果使用硬件SPI需要配置主设备SPI为模式3.
SCLK 周期也反映了 SPI 的通信速率,这里看到最小值是 200ns,也就意味着 SPI 最大的通信速率可以达到 5MHZ,同时每个时钟周期交换一位数据,可以大致算出每秒最大能传输的数据量。
4. IO模拟SPI读写实现
至此就可以大致归纳出模拟该设备SPI通信的编程思路:
1.拉低片选信号
2.延时 至少t_cs 拉低时钟线
3.延时 至少t_sl 后,主设备立即将需要发送的电平信号发送待 MOSI 上
4.再拉高时钟线,主设备读 MISO 的电平信号,时钟高维持 t_sh 时间后周期结束
5.垃低时钟线,重复步骤3、4开始读取下一位的数据
7.最后一位数据读取完成,拉高片选
注意:需要保证交换一位数据的周期时间大于 200ns
通过以上分析,我们就能明确编程思路了,这里我为了节省空间,是将 SPI 的读写函数写到了一起,当然也可以分开写。
1 uint8_t SPI_ExchangeByte(uint8_t data) 2 { 3 uint8_t ret; 4 uint8_t cnt; 5 for(cnt = 0; cnt < 8; cnt++) 6 { 7 SCLK_CTRL(0); /*拉低时钟线*/ 8 delay_ns(200); /*t_sl 至少80ns,这里给200ns*/ 9 if(data&0x80) MOSI_CTRL(1); /*主设备发送电平信号到 MOSI*/ 10 else MOSI_CTRL(0); 11 12 SCLK_CTRL(1); /*拉高时钟线,从设备会在之后的 5ns 内读走 MOSI 上信号*/ 13 if(MISO_READ()) res|=0x01; /*主设备读取 MISO 上的信号*/ 14 else res&=0xfe; 15 data<<=1; 16 ret<<=1; 17 delay_ns(200); /*t_sh 至少80ns,这里给200ns*/ 18 } 19 }
ADE7953的SPI只支持模式3,如果有其他模式的器件,我们也可以举一反三:
如果有设备CPOL = 0,CPHA = 1:只需要将 SCLK_CTRL(0),和 SCLK_CTRL(1) 交换一下位置即可
类似还有CPOL = 1,CPHA = 0:此时数据采样发生在奇数边沿,即拉低SCLK时,
主设备需要在此之前发送电平信号到 MOSI 以供从设备采样,也需要在此之后采样从设备发送到 MISO 上的信号,我们只需要将上面的代码,交换主设备读和主设备写两部分的位置即可。
注意:理解时钟相位只需要明白一点,无论我们的编程对象是主机还是从机只需要记住在采样边沿发生之前,应该准备好我们当前设备需要发送的数据在采样边沿发生之后,再去读取其他设备发送过来的数据。
现在我们可以将上面驱动完善一下,实现多字节寄存器数据的读写,单字节读写类似:
1 void ADE7953_Read_Byte(uint16_t reg, uint8_t *pBuffer, uint16_t nLen) 2 { 3 uint16_t cnt; 4 uint8_t buf[3]; 5 6 buf[0] = (uint8_t)(reg >> 8); /*ADE7953 的寄存器是16位的,这里需要拆成两个字节发送*/ 7 buf[1] = (uint8_t)reg; 8 buf[2] = 0x80; /*ADE7953 读数时还需要发送数据读取标识字节*/ 9 10 CS_CTRL(0); /*片选使能*/ 11 SPI_ExchangeByte(buf[0]); /*发送寄存器地址和读取标识字节*/ 12 SPI_ExchangeByte(buf[1]); 13 SPI_ExchangeByte(buf[2]); 14 for(cnt = 0; cnt < nLen; cnt++) 15 { 16 /*循环读取数据,读数时可以发送空字节(NOP = 0)*/ 17 pBuffer[cnt] = SPI_ExchangeByte(NOP); 18 } 19 CS_CTRL(1); /*片选失能,数据读取完成*/ 20 } 21 22 void ADE7953_Write_NByte(uint16_t reg, uint8_t *pBuffer, uint16_t nLen) 23 { 24 uint16_t cnt; 25 uint8_t buf[3]; 26 27 buf[0] = (uint8_t)(reg >> 8); /*ADE7953 的寄存器是16位的,这里需要拆成两个字节发送*/ 28 buf[1] = (uint8_t) reg; 29 buf[2] = 0x00; /*需要发送写入标识字节*/ 30 31 CS_CTRL(0); /*片选使能*/ 32 SPI_ExchangeByte(buf[0]); /*发送寄存器地址和写入标识字节*/ 33 SPI_ExchangeByte(buf[1]); 34 SPI_ExchangeByte(buf[2]); 35 for(cnt = 0; cnt < nLen; cnt++) 36 { 37 /*将 pBuffer中的数据循环写入*/ 38 SPI_ExchangeByte(pBuffer[cnt]); 39 } 40 CS_CTRL(1); /*片选失能,数据写入完成*/ 41 }
5. STM8硬件SPI及SPI参数配置
5.1 stm8l151c8t6 SPI 初始化函数及参数配置
1 void STM8L15x_SPI2_Init() 2 { 3 CLK_PeripheralClockConfig(CLK_Peripheral_SPI2,ENABLE); 4 SPI_DeInit(SPI2); 5 SPI_Init(SPI2,SPI_FirstBit_MSB, /*高位在前*/ 6 SPI_BaudRatePrescaler_4, /*SPI时钟预分频4*/ 7 SPI_Mode_Master, /*主机模式*/ 8 SPI_CPOL_High, /*SCLK空闲为高*/ 9 SPI_CPHA_2Edge, /*SCLK的偶数次边沿采样*/ 10 SPI_Direction_2Lines_FullDuplex, /*2数据线,全双工*/ 11 SPI_NSS_Soft, /*NSS信号由硬件管理,不需要编程操作*/ 12 7); /*CRC 值计算的多项式*/ 13 SPI_Cmd(SPI2,ENABLE); 14 /*SPI相关IO口配置*/ 15 GPIO_Init(PORT_SPI, PIN_MISO, GPIO_Mode_In_PU_No_IT); // MISO 16 GPIO_Init(PORT_SPI, PIN_SCLK, GPIO_Mode_Out_PP_High_Slow); // SCLK 17 GPIO_Init(PORT_SPI, PIN_MOSI, GPIO_Mode_Out_PP_High_Slow); // MOSI 18 }
5.2 spi 单字节读写函数
1 uint8_t SPI2_ExchangeByte(uint8_t data) 2 { 3 SPI_SendData(SPI2, data); 4 while (RESET == SPI_GetFlagStatus(SPI2, SPI_FLAG_TXE)); // 等待数据传输完成 5 while (RESET == SPI_GetFlagStatus(SPI2, SPI_FLAG_RXNE)); // 等待数据接收完成 6 return (SPI_ReceiveData(SPI2)); 7 }
注意:如果使用 stm8 的内部高速时钟 HSI,需要设置足够大的分频系数,比如 ADE7953 SPI 速率最大为 5MHZ, 如果这里分频系数为 SPI_BaudRatePrescaler_2,则 stm8 的 spi 时钟为 16M/2=8MHZ,超过了 ADE7953 支持的最大速率,很有可能导致通信失败。所以这里需要注意计算 spi 的通信速率。
6. STM32硬件SPI及SPI参数配置
6.1. stm32f407 spi 配置(hal库版本)
1 SPI_HandleTypeDef hspi1; 2 void STM32F4x_SPI_Init(void) 3 { 4 hspi1.Instance = SPI1; 5 hspi1.Init.Mode = SPI_MODE_MASTER; /*SPI模式:主机模式*/ 6 hspi1.Init.Direction = SPI_DIRECTION_2LINES; /*双线双向全双工*/ 7 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; /*SPI发送接收帧:8位*/ 8 hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; /*时钟线空闲时电平:高*/ 9 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; /*数据采样边沿:偶数次边沿*/ 10 hspi1.Init.NSS = SPI_NSS_SOFT; /*NSS信号控制:软件*/ 11 hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; /*数据传输首位:MSB位*/ 12 hspi1.Init.TIMode = SPI_TIMODE_DISABLE; /*SPI TIM MODE:失能*/ 13 hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /*预分频:256*/ 14 hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /*CRC校验计算:禁止*/ 15 hspi1.Init.CRCPolynomial = 7; /*CRC多项式:7*/ 16 HAL_SPI_Init(&hspi1); 17 } 18 19 void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle) 20 { 21 GPIO_InitTypeDef GPIO_InitStruct = {0}; 22 if(spiHandle->Instance==SPI1) 23 { 24 __HAL_RCC_SPI1_CLK_ENABLE(); 25 __HAL_RCC_GPIOA_CLK_ENABLE(); 26 GPIO_InitStruct.Pin = SPI1_SCK_Pin|SPI1_MISO_Pin|SPI1_MOSI_Pin; 27 GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; 28 GPIO_InitStruct.Pull = GPIO_NOPULL; 29 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; 30 GPIO_InitStruct.Alternate = GPIO_AF5_SPI1; 31 HAL_GPIO_Init(SPI1_Port, &GPIO_InitStruct); 32 } 33 } 34 35 uint8_t SPI1_Exchange_Byte(uint8_t data) 36 { 37 uint8_t ret; 38 if(HAL_SPI_TransmitReceive(&hspi1, &data, &ret, 1, 500) != HAL_OK) 39 { 40 Error_Handler(); 41 } 42 return ret; 43 }
注意: 这里除了需要注意 SPI的速率之外还需要注意SPI的引脚复用,关于STM32F4的引脚复用可以查看官方的 Datasheet 第三章 Pinouts and pin description.
6.2. stm32f407 spi 配置(std库版本)
1 void STM32F4x_SPI_Init(void) 2 { 3 GPIO_InitTypeDef GPIO_InitStructure; 4 SPI_InitTypeDef SPI_InitStructure; 5 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //使能 GPIOB 时钟 6 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 使能 SPI1 时钟 7 8 /*GPIOFB3,4,5 初始化设置: 复用功能输出*/ 9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5; 10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能 11 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 12 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //100MHz 13 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉 14 GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化 15 16 /*配置引脚复用映射*/ 17 GPIO_PinAFConfig(GPIOB,GPIO_PinSource3,GPIO_AF_SPI1); //PB3 复用为 SPI1 18 GPIO_PinAFConfig(GPIOB,GPIO_PinSource4,GPIO_AF_SPI1); //PB4 复用为 SPI1 19 GPIO_PinAFConfig(GPIOB,GPIO_PinSource5,GPIO_AF_SPI1); //PB5 复用为 SPI1 20 21 /*这里只针对 SPI 口初始化*/ 22 RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,ENABLE); //复位 SPI1 23 RCC_APB2PeriphResetCmd(RCC_APB2Periph_SPI1,DISABLE); //停止复位 SPI1 24 25 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; /*双线双向全双工*/ 26 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; /*SPI模式:主机模式*/ 27 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; /*SPI发送接收帧:8位*/ 28 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; /*时钟线空闲时电平:高*/ 29 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; /*数据采样边沿:偶数次边沿*/ 30 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; /*NSS信号控制:软件*/ 31 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; /*预分频:256*/ 32 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; /*数据传输首位:MSB位*/ 33 SPI_InitStructure.SPI_CRCPolynomial = 7; /*CRC多项式:7*/ 34 SPI_Init(SPI1, &SPI_InitStructure); 35 SPI_Cmd(SPI1, ENABLE); /*使能 SPI1*/ 36 } 37 38 uint8_t SPI_Exchange_Byte(uint8_t data) 39 { 40 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){} //等待发送区空 41 SPI_I2S_SendData(SPI1, data); //通过外设 SPIx 发送一个 byte 数据 42 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){} //等待接收完 43 return SPI_I2S_ReceiveData(SPI1); //返回通过 SPIx 最近接收的数据 44 }