草庐IT

STM32之SPI详细解析

Swiler 2023-07-15 原文

SPI介绍

SPI协议,用来传输数据的一种标准化协议。

SPI包括这些独特的特点:

  • 主模式和从模式

  • 双向模式

  • 从模式选择输出

  • 模式故障错误标志与CPU中断能力

  • 双缓冲数据寄存器

  • 具有可编程极性和相位的串行时钟

  • 在等待模式下对SPI操作的控制

引脚描述:

​ MOSI:此引脚用于在配置为主主模块时从SPI模块中传输数据,并在配置为从主模块时接收数据。(主出从入)

​ MISO:在配置为SPI模块时从SPI模块中传输数据,在配置为主模块时接收数据。(主入从出)

​ SS:(低有效)用于将选择信号从SPI模块输出到另一个外设,当其配置为主控时进行数据传输,当SPI配置为从控时作为输入来接收从选择信号。
​ 该引脚相当于片选。

​ SCK:此引脚用于输出SPI传输数据或接收从属时钟的时钟。

时序分析

首先要了解两个概念:CPHA(Clock Phase,时钟相位)和CPOL(Clock Polarity,时钟极性)

  1. 时钟极性:是指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。

  2. 时钟相位:是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的”奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。

因此SPI关于时钟的配置,就有了以下4种情况:

模式CLK
CPOL = 0;CPHA = 0SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
CPOL = 1;CPHA = 0SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的奇数边沿被采样
CPOL = 0;CPHA = 1SCK 在空闲状态时为低电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样
CPOL = 1;CPHA = 1SCK 在空闲状态时为高电平,数据线上的信号在 SCK 时钟线的偶数边沿被采样

时序1:CPHA = 0

从时序图可以看出,当 CPHA = 0 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在奇数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。

时序2:CPHA = 1

从时序图可以看出,当 CPHA = 1 的时候,无论CPOL等于多少,在SAMPLE那一栏即采样项(橙色方框处),都是在偶数边沿进行采样,即采样边沿仅受CPHA的影响。并且采样开始前要先拉低SS,即进行片选。

综上所述,可以发现SPI的协议自由度是比IIC要高一些的,给了开发者更多的自由搭配的空间。

比如在写OLED的spi的时候,可能就不用加上MISO引脚,但是要额外搭配DC(Data/Command)引脚,区别发送的是数据还是命令。

接下来我们分别看看硬件SPI和软件模拟SPI

STM32的硬件SPI

接下来我们看看STM32中的硬件SPI,这里以STM32F103RCT6为例。

功能框图

  1. MOSI、MISO、SCK、NSS与前文说过的一样,四根引脚。
  2. 波特率发生器:由框图可以看出,波特率发生器链接的是SCK,那么可想而知,这是用来产生时钟信号的,既然用来产生时钟信号,那么肯定和STM32的时钟有关,并且也能够进行分频之类的操作。并且框图也指出,寄存器SPI_CR1的BR[2,0] 位指向波特率发生器,由数据手册得知,该位是对 fpclk时钟的分频因子,对 fpclk的分频结果就是 SCK 引脚的输出时钟频率。其中的 fpclk频率是指 SPI 所在的 APB 总线频率。
    计算结果如下:
BR[0:2]分频结果(SCK 频率)
000fpclk/2
001fpclk/4
010fpclk/8
011fpclk/16
100fpclk/32
101fpclk/64
110fpclk/128
111fpclk/256
  1. 数据控制单元:该部分包含接收缓冲区、发送缓冲区、数据移位寄存器。
    发送数据的时候,数据移位寄存器将发送缓冲区内的数据一位一位发出去;接收数据的时候,数据移位寄存器则把把接收缓冲区内的数据一位一位读进来。并且每个数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式。配置“LSBFIRST 位”可选择高位先行(MSB)还是低位先行(LSB)。

    对于数据寄存器(DR),通过写 SPI的数据寄存器可以把数据填入发送缓冲区,通过读SPI的数据寄存器可以获取接收缓冲区中的内容。

  2. 剩下的部分则是整体配置SPI相关的控制部分。SPI的运行模式,则随着我们这部分的配置的不同而不同。除了基本的SPI相关参数的配置,还包括SPI的中断信号、DMA请求、NSS信号线配置等。

从选择(NSS)脚管理,即SS引脚,进行片选

有2种NSS模式:

● 软件NSS模式:可以通过设置SPI_CR1寄存器的SSM位来使能这种模式。内部NSS信号电平可以通过写SPI_CR1的SSI位来驱动,就可以将NSS引脚用作别的功能。

● 硬件NSS模式,分两种情况:

─ NSS输出被使能:当STM32作为主机,并且NSS输出已经通过SPI_CR2寄存器的SSOE位使能,这时NSS引脚被拉低,所有NSS引脚与这个主SPI的NSS引脚相连并配置为硬件NSS的SPI设备,将自动变成从机。当一个SPI设备需要发送广播数据,它必须拉低NSS信号,以通知所有其它设备它是主机;如果它不能拉低NSS,这意味着总线上有另外一个主设备在通信,这时将产生一个硬件失败错误(Hard Fault)。

─ NSS输出被关闭:允许操作于多主机环境。

通讯过程

(来自野火的《零死角玩转STM32》)

主模式收发流程及事件说明如下:

(1) 控制 NSS 信号线,产生起始信号(图中没有画出),即先将NSS拉低;

(2) 把要发送的数据写入到“数据寄存器 DR”中,该数据会被存储到发送缓冲区;

(3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去;MISO 则把数据一位一位地存储进接收缓冲区中;

(4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE标志位”会被置 1,表示传输完一帧,接收缓冲区非空;这里的两个标志位要由软件清零。

(5) 等待到 “TXE 标志位” 为 1 时,若还要继续发送数据,则再次往 “数据寄存器DR” 写入数据即可;等待到 “RXNE标志位” 为 1 时,通过读取“数据寄存器DR”可以获取接收缓冲区中的内容。假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。

初始化结构体

typedef struct
{
    uint16_t SPI_Direction; 			/*设置 SPI 的单双向模式 */
    uint16_t SPI_Mode; 					/*设置 SPI 的主/从机端模式 */
    uint16_t SPI_DataSize; 				/*设置 SPI 的数据帧长度,可选 8/16 位 */
    uint16_t SPI_CPOL; 					/*设置时钟极性 CPOL,可选高/低电平*/
    uint16_t SPI_CPHA; 					/*设置时钟相位,可选奇/偶数边沿采样 */
    uint16_t SPI_NSS; 					/*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/
    uint16_t SPI_BaudRatePrescaler; 	/*设置时钟分频因子,fpclk/分频数=fSCK */
    uint16_t SPI_FirstBit; 				/*设置 MSB/LSB 先行 */
    uint16_t SPI_CRCPolynomial; 		/*设置 CRC 校验的表达式 */
} SPI_InitTypeDef;

在与其他SPI从机搭配使用时,有时还要参考从机的数据手册;这类从机一般分为两类:

  1. 仅收发数据,比如FLASH芯片中的W25Q64;
  2. 收发数据和指令,这类从机额外使用DC引脚来区别SPI上发送的数据是单纯的数据还是指令,比如OLED等。

软件SPI

软件SPI就是用普通IO口模拟SPI的时序和通讯方法。这里我用SPI通讯方式的LCD搭配STM32CubeMX来做介绍

GPIO配置

其中DC用来区分写数据还是写指令

BLK调节LCD背光

CS即片选

SCK即时钟

SDA即MOSI信号线

需要注意的是,这里的SCK配置的是上拉,即使SCK时钟在空闲时为高电平,即CPOL = 1

void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOH_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(BLK_GPIO_Port, BLK_Pin, GPIO_PIN_SET);

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOA, DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
                          |RES_Pin, GPIO_PIN_SET);

  /*Configure GPIO pin : PtPin */
  GPIO_InitStruct.Pin = BLK_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(BLK_GPIO_Port, &GPIO_InitStruct);

  /*Configure GPIO pins : PAPin PAPin PAPin PAPin
                           PAPin */
  GPIO_InitStruct.Pin = DC_Pin|CS_Pin|SPI_SCK_Pin|SPI_SDA_Pin
                          |RES_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

模拟时序

很典型的一个写8bit数据的函数,入口数据dat,首先选中LCD,进入循环,对dat拆出高位bit分析是1还是0,再决定MOSI输出的是1还是0,并且这个过程是在一个时钟脉冲内完成的。一次循环结束后,dat左移一位,将低一位的bit推向高位,为下一次循环做好准备。在8次循环过后,就完成了一个8bit数据的发送。其实LCD的使用中,SPI相关的只有以下函数,其他的都是在此之上进行的扩展。(没有明显区分采样时刻是奇数还是偶数边沿)

void LCD_Writ_Bus(u8 dat) 
{	
	u8 i;
	LCD_CS_Clr();
	for(i=0;i<8;i++)
	{			  
		LCD_SCLK_Clr();
		if(dat&0x80)
		{
		   LCD_MOSI_1();
		}
		else
		{
		   LCD_MOSI_0();
		}
		LCD_SCLK_Set();
		dat<<=1;
	}	
  LCD_CS_Set();	
}

当然读取一个数据也可由上面推导出来,但是,LCD上并没有MISO引脚,所以各位看看就好。用不上。

uint8_t LCD_Read_Bus(void)
{
    uint8_t i;
    uint8_t value = 0;
    
	LCD_CS_Clr();
	for(i=0;i<8;i++)
	{			  
		LCD_SCLK_Clr();
        value <<= 1;
        if(LCD_MISO_READ() == 1)
        {
            value = value + 1;
        }
		LCD_SCLK_Set();
	}	
    LCD_CS_Set();	
    return value;
}

比如写一个16位的数据

void LCD_WR_DATA(u16 dat)
{
	LCD_Writ_Bus(dat>>8);
	LCD_Writ_Bus(dat);
}

比如LCD写命令

void LCD_WR_REG(u8 dat)
{
	LCD_DC_Clr();//写命令
	LCD_Writ_Bus(dat);
	LCD_DC_Set();//写数据
}
//因为大多数情况下是写数据,所以这里配置完写命令后要及时切换回写数据

总结

单单从SPI的基本协议来看,SPI可能比IIC更简单一些,只是分出了四种模式,但是单纯的SPI根据不同的从机要对协议进行不一样的扩展,这就提升了编程的难度,但是也加大了协议本身的自由度。速率方面的话,同一个芯片的硬件SPI的速率是绝对远超软件SPI的。

有关STM32之SPI详细解析的更多相关文章

  1. Ruby 解析字符串 - 2

    我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  4. ruby-on-rails - 我更新了 ruby​​ gems,现在到处都收到解析树错误和弃用警告! - 2

    简而言之错误:NOTE:Gem::SourceIndex#add_specisdeprecated,useSpecification.add_spec.Itwillberemovedonorafter2011-11-01.Gem::SourceIndex#add_speccalledfrom/opt/local/lib/ruby/site_ruby/1.8/rubygems/source_index.rb:91./opt/local/lib/ruby/gems/1.8/gems/rails-2.3.8/lib/rails/gem_dependency.rb:275:in`==':und

  5. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  6. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  7. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  8. ruby - 用 YAML.load 解析 json 安全吗? - 2

    我正在使用ruby2.1.0我有一个json文件。例如:test.json{"item":[{"apple":1},{"banana":2}]}用YAML.load加载这个文件安全吗?YAML.load(File.read('test.json'))我正在尝试加载一个json或yaml格式的文件。 最佳答案 YAML可以加载JSONYAML.load('{"something":"test","other":4}')=>{"something"=>"test","other"=>4}JSON将无法加载YAML。JSON.load("

  9. ruby - 如何使用 Nokogiri 解析纯 HTML 表格? - 2

    我想用Nokogiri解析HTML页面。页面的一部分有一个表,它没有使用任何特定的ID。是否可以提取如下内容:Today,3,455,34Today,1,1300,3664Today,10,100000,3444,Yesterday,3454,5656,3Yesterday,3545,1000,10Yesterday,3411,36223,15来自这个HTML:TodayYesterdayQntySizeLengthLengthSizeQnty345534345456563113003664354510001010100000344434113622315

  10. python - 帮我找到合适的 ruby​​/python 解析器生成器 - 2

    我使用的第一个解析器生成器是Parse::RecDescent,它的指南/教程很棒,但它最有用的功能是它的调试工具,特别是tracing功能(通过将$RD_TRACE设置为1来激活)。我正在寻找可以帮助您调试其规则的解析器生成器。问题是,它必须用python或ruby​​编写,并且具有详细模式/跟踪模式或非常有用的调试技术。有人知道这样的解析器生成器吗?编辑:当我说调试时,我并不是指调试python或ruby​​。我指的是调试解析器生成器,查看它在每一步都在做什么,查看它正在读取的每个字符,它试图匹配的规则。希望你明白这一点。赏金编辑:要赢得赏金,请展示一个解析器生成器框架,并说明它的

随机推荐