================================
第6章 SPI接口及其应用

SPI(Serial Peripheral Interface),即串行外设接口。SPI最早由Motolora半导体部门提出,最初出现在M68系列单片机上用于连接片外的EEPROM、ADC和DAC等外设。
SPI是一种伪共享的全双工/半双工的同步串行通讯接口,仅支持单主多从的系统结构。伪共享的总线结构是受到并行总线的影响,要求主机必须为每一个从机提供一个片选信号(CS),
当某个从机的片选信号被主机置为有效电平时,主机和被选中的从机之间实现全双工/半双工的同步串行通讯。同步串行通讯,意味着SPI接口具有专用的同步时钟信号。
全双工,意味着SPI接口拥有独立的“主机写-从机读”和“主机读-从机写”串行数据传输线。半双工只需要一根串行数据线双向传输数据。

SPI接口经历多次改进,目前不仅支持单个串行数据信号,也支持2位和4位宽度的串行数据信号,在同样的时钟频率条件下串行数据线越多数据吞吐量越大。
QSPI(Quad SPI)接口的数据信号达4个,假设同步时钟信号的频率为32MHz,QSPI实际的位时钟频率达128MHz。高速的QSPI接口已经用于嵌入式系统内的大容量FlashROM、
伪静态RAM等存储器扩展,借助于MCU片上Cache单元甚至可以直接从QSPI接口的FlashROM中执行(XIP)程序。显然,SPI接口是嵌入式系统内十分重要的一种扩展接口,
与I2C接口一样,SPI也是绝大多数MCU标配的片上功能单元。

本章将了解SPI接口的信号、时序和协议规范,以及MCU片上SPI功能单元的主机模式和从机模式,并以SPI接口的显示器和协处理器、QSPI接口的FlashROM等为例讨论SPI接口的结构组成、接口电路和软件编程控制。

===========================
6.1 SPI通讯接口

SPI通讯接口采用主从模式的结构,仅支持一个主机和一个或多个从机。标准的SPI通讯接口是4线的,包括同步时钟信号SCK、主输出从输入信号MOSI、主输入从输出信号MISO、
片选信号NSS(Slave Select)。一对SPI通讯接口的主机和从机的内部结构如图6.1所示。

SPI2021-08-04-20-14-21

图6.1 SPI通讯接口的主机和从机内部结构

这里再次看到移位寄存器,他是SPI通讯接口的核心部组件。根据现代MCU的存储器映射规则,SPI通讯接口的接收和发送数据缓冲器都是MCU内部存储单元。
当SPI通讯接口软件将待发送的数据写入发送数据缓冲器并启动数据发送过程(片选信号NSS被主机置为有效电平),该数据将被自动装载到移位寄存器,
并以最高位(MSB)先发送的规则随着同步时钟SCK顺序地将数据逐位从MOSI发出,同时从机SPI接口将随着同步时钟SCK逐位地将数据位移入移位寄存器。
当主机需要从从机读取数据时,从机首先将待发送的数据写入发送数据缓冲器,当主机将片选信号NSS置为有效电平时自动将数据加载到从机的移位寄存器,
随着同步时钟信号SCK仍遵循MSB先发送的规则将数据顺序地逐位从MISO发出,同时主机SPI接口将随着同步时钟SCK逐位地将数据移入移位寄存器,
所有数据位移入完毕后,主机移位寄存器的数据自动被加载到接收数据缓冲器。

这两个方向的移位过程是可以同时进行,而且不会有接收和发送数据位重叠,两个移位寄存器被两个独立的串行数据线首尾串联成环形,
譬如一个字节(8位)数据从主机移入从机的同时从机上的一个字节数据也正好移入主机。
很显然,标准的SPI通讯接口支持全双工数据传输,即主机向从机写入数据的同时可以读取从机上的数据。
事实上,为了提高数据传输效率,绝大多数现代SPI通讯接口的接收和发送数据缓冲器都采用FIFO(先进先出)结构、接收和发送完毕的中断机制。

有些SPI通讯接口的应用场景无需全双工的数据传输,譬如只需要半双工或单工,我们可以简化接口以减少信号线。图6.2给出全双工的和半双工的SPI通讯接口的对比。

SPI2021-08-04-20-14-33

图6.2 全双工的和半双工的SPI通讯接口

很多资料中提到4线的和3线的SPI接口正是上图所示的两种情况,4线的是标准SPI通讯接口,3线的是半双工的SPI通讯接口。在半双工的数据传输模式,
主机的MOSI和从机的MISO相连,而且主从双方的这个接口信号都是双向的。为了防止信号名称的混淆,在上图中的主机侧仍使用NSS、SCK、MOSI和MISO等4个名称,
而从机侧则使用SDI(从机数据输入信号)代替MOSI,SDO(从机数据输出信号或数据输入信号)代替MISO。其中,半双工模式,从机的SDO信号是双向的,
主机的MOSI信号也是双向的。

半双工的SPI通讯接口节约一个接口信号连线,但并不是所有的SPI接口都支持半双工的模式。单工的SPI接口也可以节约一个接口信号,
根据数据传输的方向需要确定去掉MOSI或MISO。譬如显示器是一种典型的输出外设,显示器的SPI接口可以采用单工的,仅需要NSS、SCK和MOSI三个信号即可。

前面讨论的都是一主一从的SPI通讯接口,多从机时的SPI接口是什么样的结构呢?如图6.3所示。

SPI2021-08-04-20-14-44

图6.3 一主多从的SPI通讯接口

上图中给出两种拓扑结构的一主多从的SPI通讯接口。图中(a)是常规的拓扑结构,SCK、MOSI和MISO等三个信号是SPI通讯接口的共享总线信号,
所有的主机和从机使用这些共享总线连接在一起,但是每一个从机必须独占一个片选信号NSS,随着从机个数的增加,主机将开销更多的I/O引脚用作片选信号。
很显然,图中(b)的菊花链拓扑结构中所需要主机的I/O引脚始终是4个,不受从机个数影响。对比两种拓扑,虽然菊花链结构节约I/O引脚但数据传输需要经过更多次移位,
即消耗更多个同步时钟周期,意味着更低的数据通讯速率。此外,两种拓扑结构的接口软件区别较大,菊花链结构的某个从机与主机之间传输数据所耗费的时钟个数必须根据从机个数和顺序号来确定。

后面所用的SPI通讯接口都默认为常规的拓扑,除非特别说明。与I2C通讯接口使用的惟一从机地址的寻址方法完全不同,任一SPI从机是否被选中与SPI主机通讯,
仅由其片选信号NSS的状态所决定。通常,SPI主机需要访问某个从机时,只需要将该从机的片选信号NSS置为有效电平,同时其他从机的片选信号都被置为无效电平,
仅有一个从机被选中与主机通讯。这种通过惟一的片选信号选中某个从机的方法与传统的三总线(数据总线、地址总线和控制总线)的片选信号选中某个外设的方法几乎完全一致,
传统的三总线是并行总线,现在仅适合MCU或MPU片上组件之间互联,主CPU通过控制地址译码器将某个组件的片选信号置为有效电平,此时主CPU与被选中的组件之间独占完整总线进行数据传输,
期间其他组件(即未被选中的组件)处于空闲状态。当某个SPI从机的片选信号被主机置为有效电平时,SPI主机与被选中的从机之间独占SPI接口总线进行数据传输,
其他未被选中的SPI从机处于空闲状态,忽略SPI总线的输入信号(SCK和MOSI)并释放输出信号(MISO)。

从SPI通讯接口的选中和非选中的访问方法看,任何时候仅有被选中的SPI从机与主机之间一对一通讯,因此SPI通讯接口的时序比I2C简单很多。如图6.4所示,
SPI通讯接口仅以8位(字节)及其整数倍的二进制位对齐的移位操作,没有I2C通讯接口的START和STOP等特殊时序。

SPI2021-08-04-20-15-17

图6.4 SPI通讯接口的时序

上图中,SPI通讯接口的同步时钟信号SCK在总线空闲时的状态是低电平,并在SCK的第偶数次跳变沿对MOSI和MISO信号采样。事实上,标准的SPI接口规范中,
总线空闲时SCK信号的状态CPOL(Clock POLarity)、数据线的采样时刻CPHA(Clock PHAse)、位序MSBFIRST(先发送MSB)和SCK信号频率等都是可配置的。
这些配置也都是SPI软件接口的基本参数,详见第6.2节。关于CPOL和CPHA两个参数之间的关系如图6.5所示。

SPI2021-08-04-20-15-26

图6.5 SPI通讯接口时序的4种配置

根据CPOL和CPHA两个参数,SPI通讯接口时序共有4种不同的配置模式。MODE0,SPI总线空闲时SCK保持低电平,当NSS信号有效期间,
SCK信号的第奇数次跳变沿采样MOSI和MISO信号,即SCK的上升沿时刻采样数据线。MODE2,SPI总线空闲时SCK保持高电平,当NSS信号有效期间,
SCK信号的第奇数次跳变沿采样MOSI和MISO信号,即SCK的下降沿时刻采样数据线。根据上图所示,MODE1和MODE3的两种配置无须赘述。

必须注意,数据线被采样的时刻必须确保数据线状态是稳定的,即不允许信号驱动端改变数据线状态,SPI通讯接口的每一种时序配置的数据线切换时刻也是确定的,
对于MODE0和MODE3的配置,允许SCK位低电平时改变MOSI和MISO的状态。

同步时钟信号SCK的频率是SPI通讯接口的波特率(Baudrate),即二进制位的传输频率(或传输一个二进制位所消耗的时间)。当我们在配置SPI通讯接口的参数时,
必须考虑SPI从机的能力,包括SCK信号支持的/允许的最大频率、模式、位序等。换个角度来看待这些可配置参数,他们都是为了适应SPI从机的目的,
尤其是SPI从机是不可配置或不可编程的情况。

本质上,SPI通讯接口仅仅是一种同步串行数据移位操作的物理层接口,可配置接口参数的高灵活性和开源性使得SPI接口拥有很多种变化版本(Variant)。譬如,
当前被广泛使用于FlashROM(主要是NOR结构闪存)接口的2位(Dual)/4位(Quad)宽度的串行数据线版本分别称作DSPI和QSPI,接口时序的读写操作示例如图6.6所示。

SPI2021-08-04-20-15-41

图6.6 DSPI和QSPI通讯接口的读写时序

上图中的SPI通讯接口配置参数采用MODE0。可以看出,为兼容标准4线SPI通讯接口,传输命令期间仍使用标准4线SPI接口时序,其后的地址和数据传输采用2位或4位宽度的串行数据线发送数据。
很显然,DSPI和QSPI的波特率分别是标准SPI通讯接口波特率的2倍和4倍。

上图©是先写命令和地址信息然后再顺序地连续读取若干地址单元的数据的时序,该时序的写入和读出操作之间有4个SCK周期的Dummy(占位),
允许SPI从机在这个期间加载数据到发送缓冲区。DSPI和QSPI通讯接口的主机和从机信号如图6.7所示。

SPI2021-08-04-20-15-54

图6.7 DSPI和QSPI通讯接口的主从信号

DSPI和QSPI通讯接口及其应用详见 [2]_ 页面的介绍。

SD卡是一种NAND结构的大容量闪存,TF卡是外型尺寸更小的SD卡(即micro SD)。SD卡接口不仅兼容标准SPI接口,也有专用的SD卡接口规范。
当SD卡的读写速度要求较低的场合,尤其嵌入式系统中可以使用标准SPI接口访问SD卡。即使SD卡的高速读写系统,SD卡上电后的初始化阶段,
主机使用标准SPI接口向SD卡发送配置命令,然后SD卡根据配置命令进入SD卡接口模式实现高带宽的数据读写操作。有关SD卡的SPI模式详见文档 [1]_
的第7章。SD卡、TF卡接口信号与SPI接口信号之间的关系如图6.8所示。

SPI2021-08-04-20-16-05

图6.8 SD卡、TF卡接口信号与SPI接口信号之间关系

SD卡模式使用6个信号,分别为CMD、SCK、DAT0~3。4位宽度的串行数据线DAT0~3是双向的,与QSPI相同。CMD是传输主机命令和SD卡应答信息的专用信号线,
SD卡操作总是以命令帧(由1个字节命令、4个字节命令参数和1个字节CRC7校验和组成)开始,譬如CMD17和CMD24分别是单个数据块的读和写命令。

SDIO(Secure Digital Input and Output)接口是从SD接口衍生出来的一种高吞吐量的外设接口,向下兼容SD卡接口规范和标准SPI接口。
SDIO接口不仅用于可插拔的存储卡,还用于WiFi无线网卡、蓝牙卡、摄像头、GPS等外设接口。SDIO的具体应用和规范可在页面 [3]_ 找到。

更多的SPI通讯接口的变种,请查阅维基百科的SPI说明(https://en.wikipedia.org/wiki/Serial_Peripheral_Interface),如果无法打开该链接,
请点击下面的链接下载PDF格式的文档:

. :download:维基百科(wikipedia.org)对SPI通讯接口的介绍 <../_static/dl_files/bluefi_ch6_1/Serial_Peripheral_Interface_Wikipedia.pdf>

前面已初步了解半双工SPI接口(3线)、全双工的标准SPI接口(4线)、DSPI(4线)、QSPI(6线)、SD(6线)及其衍生的SDIO等通讯接口,
这些接口常用于嵌入式系统主控制器与内部组件之间的总线接口,与I2C相比SPI接口的时序更简单、更容易实现、允许更高的波特率。


BlueFi开源板的主控制器与彩色LCD显示器、WiFi协处理器等都使用SPI通讯接口,并使用QSPI接口扩展片外的2MB闪存用于保存Python库、
Python脚本程序、声音和图片等资源文件。此外,BlueFi开源板的40P金手指扩展接口的P13~P16可作为标准SPI接口。
BlueFi开源板的SPI接口如图6.9所示。

SPI2021-08-04-20-16-20

图6.9 BlueFi开源板上的SPI接口外设

nRF52840具有1个QSPI接口和4个标准SPI接口(分别称作SPI0~3),其中SPI0和SPI1分别与I2C0和I2C1共享存储器映射资源,即使用I2C0时SPI0将无法使用,
使用I2C1时将无法使用SPI1。nRF52840的4个标准SPI接口都可编程作为SPI主机模式,其中3个还可编程作为SPI从机模式。
此外,nRF52840的QSPI接口和4个标准SPI接口的最大波特率都高达32MHz。

根据上图可以看出,BlueFi开源板的彩色LCD显示器使用的是变种的SPI通讯接口,WiFi网络协处理器使用标准SPI接口,我们并未使用SPI支持的共享总线。
考虑到I2C0、I2C1和SPI0、SPI1共享存储器资源的局限性,我们在后续的BlueFi开源板的BSP代码中使用SPI2和SPI3分别连接彩色LCD显示器和WiFi网络协处理器,
前一章中我们已经使用I2C1作为BlueFi板上的温湿度、光学和运动传感器,BlueFi板的40P金手指扩展接口上的I2C和SPI接口分别使用I2C0和SPI0,
意味着任何时候只能选择使用其中的一种接口。

BlueFi开源板的QSPI接口固定用于片外2MB闪存的扩展接口,按照nRF52840的QSPI接口协议,最大支持24位地址宽度,即支持最大16MB片外扩展的QSPI闪存。
当然,根据QSPI接口规范,向下兼容DSPI和标准SPI等低速接口。BlueFi的片外2MB闪存主要用于Python文件系统,我们不再详细赘述

下一节将以BlueFi开源板的彩色LCD显示器的BSP实现为实例来了解SPI主机模式接口及其编程控制。


参考文献:
::

[1] https://www.sdcard.org/downloads/pls/click.php?p=Part1_Physical_Layer_Simplified_Specification_Ver8.00.jpg&f=Part1_Physical_Layer_Simplified_Specification_Ver8.00.pdf&e=EN_SS1_8
[2] https://www.nxp.com/docs/en/application-note/AN4512.pdf
[3] https://www.sdcard.org/chs/index.html

===========================
6.2 SPI主机模式

BlueFi开源板的彩色LCD显示器使用SPI通讯接口与nRF52840主控制器连接,显示器作为一种输出外设,我们将这个接口设计为半双工模式,
仅使用NSS、SCK、MOSI等三个信号,同时引入第4个信号——数据/命令信号D/C,当MOSI输出命令信息时D/C信号为低电平,输出数据时则为高电平。
引入D/C信号的SPI通讯接口的时序示例如图6.10所示。nRF52840的SPI通讯接口的主机模式支持D/C信号,每次发起SPI数据帧传输之前,
通过配置数据帧中的命令字节数和数据字节数,SPI接口自动产生D/C信号的有效电平向SPI从机发送数据/指令标示信号。

SPI2021-08-04-20-16-42

图6.10 带有D/C信号的SPI通讯接口时序示例

BlueFi使用的彩色LCD显示器的驱动器为台湾矽创电子股份公司的ST7789V,支持262K(18位RGB颜色编码)种像素颜色,最大像素数达240x320。
ST7789支持可配置的多种并行与同步串行通讯接口,包括8-/9-/16-/18-位并行接口、3线(无D/C信号)和4线(有D/C信号)SPI接口。
我们使用的彩色LCD显示器由屏幕生产厂商将驱动IC、LCD玻璃屏、背光板等集成在一起,使用COG(Chip On Glass)工艺将驱动IC直接绑定在玻璃上,
同时驱动IC的接口配置也被设定。

BlueFi开源板使用的彩色LCD显示器及其接口电路如图6.11所示。

SPI2021-08-04-20-16-58

图6.11 BlueFi开源板使用的彩色显示器及其接口电路

上图(a)的彩色LCD显示器示意图中浅灰色方形区是有效显示区域,右侧较宽的区域是COG工艺区和外部软排线接口区,实际使用时我们将软排线弯曲后焊在玻璃屏背面。
上图(b)的接口电路采用4线(含D/C信号)的半双工SPI接口,MOSI信号是双向的,这个接口电路中nRF52840的SPI通讯接口是主机模式,SCK和D/C两个信号的方向是输出。
此外,彩色LCD显示器作为一种非主动光源型显示器必须借助外界光源我们才能看到显示内容,背光板是此类显示器的必备组件,上图(b)的接口电路使用一个N型三极管控制背光板,
主控制器不仅能够控制背光板的亮、灭和亮度,还能提高背光板所需的大电流(约10mA)。

简要分析BlueFi开源板的彩色显示器接口电路后,接着开始了解该显示器的软件接口的实现。SPI接口软件也可以使用前一章所掌握的I2C的分层抽象方法,
硬件层仍使用Nordic半导体提供的SPI硬件驱动库,硬件抽象层则是Arduino开源平台的SPI类接口库,BlueFi的彩色LCD显示器接口软件(BSP)是基于硬件抽象层的实现(中间层),
这部分BSP为用户层提供显示器初始化、文本和图形等显示接口。整个显示器接口软件的架构如图6.12所示。

SPI2021-08-04-20-17-13

图6.12 基于SPI接口的彩色LCD显示器的软件架构(兼容Arduino平台)

在Arduino开源平台,对于任一种Arduino官方或第三方开源板系列,SPI通讯接口的硬件层和硬件抽象层的软件实现都是开源软件包的一部分。譬如,
基于Nordic的nRF52系列MCU的开源板,我们在第3.5节安装的nRF52系列MCU的开源软件包中,SPI通讯接口的硬件抽象层的软件实现位于
“…/Arduino15/packages/adafruit/hardware/nrf52/版本号/libraries/SPI/”文件夹,硬件层的软件实现由Nordic半导体提供,
位于“…/Arduino15/packages/adafruit/hardware/nrf52/版本号/cores/nRF5/nordic/nrfx/”文件夹。

Arduino官网列出SPI通讯接口的硬件抽象层的软件接口 [2]_ ,可以看出这个硬件抽象层仅支持SPI主机模式!主要接口包括:

  • SPISettings(clock, borOrder, dataMode),SPI通讯接口的SCK频率、位序和数据线采样模式等参数配置接口
  • setBitOrder(MSBFIRST),SPI通讯接口的位序单独地配置接口:MSBFIRST和LSBFIRST等2种参数
  • setClockDivider(diver),SPI通讯接口的时钟分频器配置接口。该配置将影响SCK时钟频率,CPU内核时钟频率被分频后作为SCK的时钟
  • setDataMode(mode),SPI通讯接口的数据线采样模式单独地配置接口:SPI_MODE0、SPI_MODE1、SPI_MODE2、SPI_MODE3等4种参数
  • begin(),初始化SPI通讯接口,并配置为主机模式,并配置SPI通讯接口的引脚、时钟速度、位序、数据线采样模式等
  • end(),取消SPI通讯接口的初始化,禁用该SPI接口
  • beginTransaction(SPISettings),使用SPISettings接口的参数初始化SPI通讯接口
  • endTransaction(),停用SPI通讯接口
  • transfer(txBuf[], rxBuf[], len),SPI通讯接口的(全双工)数据传输接口。该接口有另外三种形式:transfer(val)、transfer(val16)、transfer(buf[], len)
  • usingInterrupt(numIRQ),指定SPI通讯接口的中断号

除了参数配置和初始化接口之外,全双工数据传输接口“transfer(txBuf[], rxBuf[], len)”是最基本的接口,另外三种形式的“transfer()”(半双工)接口也是基于该接口。
BlueFi开源板的彩色LCD显示器的BSP几乎仅仅使用Arduino开源平台SPI抽象层的“transfer()”接口,并根据该LCD的驱动器IC——ST7789V的接口规范(含命令和数据格式),
首先定义“tft_Write_x(d)”、“pushPixels(colors[], len)”、“pushBlock(color, len)”等3个LCD基本的中间层接口,
分别实现8/16/24/32位数据写操作、连续写入若干个像素点的颜色值、连续填充(写入)若干像素位指定的颜色;基于这些基本的写入操作,接着定义单个像素(指定(x,y)坐标)颜色、
绘制直线、绘制圆弧和圆等基本图形图案的显示操作,以及彩色文本显示(指定位置和字体)等。

基于BSP(中间层)的LCD接口,我们很容易将文本信息显示在BlueFi的彩色LCD显示器上,或者基于直线、圆弧和圆等基本图形的绘制接口实现复杂图案设计与显示。
现在看起来LCD接口的功能较多,编写代码的工作量比较大。实施这些编码工作,我们无须从零开始,从“github”等开源社区的代码托管平台搜索“Arduino SPI ST7789”等关键词,
或许会找到数十甚至上百个相关的开源项目代码,直接将合适的项目代码移植到我们的项目中即可。

我们已经修改“Adafruit TFT eSPI”开源项目的代码用于BlueFi开源板,点击下面链接下载这部分代码的独立压缩包:

. :download:BlueFi_TFT_eSPI开源库 <../_static/dl_files/bluefi_ch6_2/BlueFi_TFT_eSPI.zip>

请将下载后的压缩包文件解压到“…/Documents/Arduino/libraries/”文件夹,你将会看到“BlueFi_TFT_eSPI”子文件夹中的LCD显示器的全部接口。
面向用户层的接口都在“…/Documents/Arduino/libraries/BlueFi_TFT_eSPI/BlueFi_TFT_eSPI.h”文件中,只需要将该文件“include”到BlueFi开源板BSP的“BlueFi.h”文件并添加少许代码即可使用这些接口。
打开“…/Documents/Arduino/libraries/BlueFi/src/”文件夹中的“BlueFi.h”和“BlueFi.cpp”两个文件,将

1
2
#include <BlueFi_TFT_eSPI.h> 
TFT_eSPI Lcd = TFT_eSPI();

两个语句添加到“BlueFi.h”文件中,并将下面的程序语句添加到“BlueFi.cpp”文件中的“void BlueFi::begin(bool LCDEnable, bool SerialEnable)”接口函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

if (LCDEnable) {
Lcd.init();
Lcd.setRotation(1);
Lcd.fillScreen(TFT_BLACK); // clear screen
Lcd.setCursor(6, 108, 4);
Lcd.setTextColor(TFT_RED, TFT_BLACK);
Lcd.print("BlueFi "); // red
Lcd.setTextColor(TFT_GREEN, TFT_BLACK);
Lcd.print(" with "); // green
Lcd.setTextColor(TFT_BLUE, TFT_BLACK);
Lcd.print(" Arduino\n"); // blue, "\n", to skip a line
Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
}

这些代码是对BlueFi开源板的彩色LCD显示器初始化的操作,包括屏幕旋转、清屏和默认的内容显示等。

为了便于测试,请先删除“…/Documents/Arduino/libraries/BlueFi”文件夹中的全部文件,然后下载下面的压缩文件包,
并解压到“…/Documents/Arduino/libraries/BlueFi”文件夹中,

. :download:本节内容所用到的BlueFi的BSP源文件 <../_static/dl_files/bluefi_ch6_2/BlueFi_bsp_ch6_2.zip>

在这个BSP文件压缩包中已包含BlueFi开源板的彩色LCD显示器的BSP(中间层)接口,下面我们使用这些接口使用BlueFi的显示器。
示例1的源程序如下:

(…/examples/TFT_LCD/hello_world.ino)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <BlueFi.h>

void setup() {
bluefi.begin(); // 包含LCD显示器的初始化操作
bluefi.Lcd.fillScreen(TFT_BLACK); // 清屏,清除默认的显示内容

// Set "cursor" at top left corner of display (0,0) and select font 4
bluefi.Lcd.setCursor(0, 0, 4);

// Set the font colour to be white with a black background
bluefi.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
// We can now plot text on screen using the "print" class
bluefi.Lcd.println("Hello, I am BlueFi\n"); // "\n", to skip a line

bluefi.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
bluefi.Lcd.println("this is White text");
bluefi.Lcd.setTextColor(TFT_RED, TFT_BLACK);
bluefi.Lcd.println("this is Red text");
bluefi.Lcd.setTextColor(TFT_GREEN, TFT_BLACK);
bluefi.Lcd.println("this is Green text");
bluefi.Lcd.setTextColor(TFT_BLUE, TFT_BLACK);
bluefi.Lcd.println("this is Blue text");
}

void loop() {
bluefi.redLED.on();
delay(100);
bluefi.redLED.off();
delay(900);
}

这个示例中,首先调用“bluefi.begin()”对BlueFi开源板的相关硬件进行初始化,包括LCD显示器的初始化在内;然后调整显示器的光标位置和所用字体大小,
之后的显示将从当前光标位置开始;接着在屏幕上显示4行彩色文本信息,每一行文字的颜色分别位白色、红色、绿色和蓝色,用这些文本内容和颜色验证显示器的基本配置是否正确。
在主循环中不再更新显示内容,仅仅保持BlueFi开源板的红色LED闪烁,表示我们的程序已经正确地执行。

显然,借助于BlueFi开源板的中间层LCD接口让BlueFi的LCD显示彩色文本,我们并不需要直接访问SPI接口相关的寄存器,也无须直接面对LCD驱动IC——ST7789V的SPI通讯协议。
现在你可以打开“…/Documents/Arduino/libraries/BlueFi_TFT_eSPI/BlueFi_TFT_eSPI.h”文件了解我们的彩色LCD显示器接口的名称、参数等,
基于这些接口,我们可以实现各种显示效果。

下面我们来探索另外一个有趣的示例——康威生命游戏的模拟效果(取消gif格式动画),如图6.13所示。

SPI2021-08-04-20-17-27

图6.13 康威(Conway)生命游戏的模拟

该游戏由英国数学家康威(Conway)于1970年设计的,使用2D网格模拟生物群落的生与死,每一个网格代表一个生命体(或元胞),其生存法则为:

  1. 如果当前网格的元胞是活体,且周围活着的邻居数目(至多8个)为2个或3个时,保持原状态
  2. 如果当前网格的元胞是活体,且周围活着的邻居数目小于2个时,生物群落太小,该元胞死亡
  3. 如果当前网格的元胞是活体,且周围活着的邻居数目大于3个时,生物群落太大,该元胞死亡
  4. 如果当前网格的元胞是死亡的,且周围活着的邻居数目是3个,该元胞变为活体
  5. 如果当前网格的元胞是死亡的,且周围活着的邻居数目不是3个,保持原状态

这些生存法则是经过我们重新编辑的描述,目的是更容易演变成生命游戏的程序算法。
该示例程序主要使用BlueFi的绘制填充颜色的方形图案的接口“fillRect(x, y, w, h, color)”绘制每个“细胞”的生与死状态,
如果某个网格的“细胞”为死亡状态则保持该方形图案的颜色与背景的黑色相同,否则随机选择一种非黑的颜色显示该“细胞”。示例程序的源码如下:

(…/examples/TFT_LCD/game_life.ino)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

// //The Game of Life, also known simply as Life, is a Cellular Automaton
#include <BlueFi.h>
// 2 x 2 pixel cells, array size = 28800 bytes per array, runs fast
#define GRIDX 120
#define GRIDY 120
#define CELLXY 2
// 1 x 1 pixel cells, array size = 20480 bytes per array
//#define GRIDX 240
//#define GRIDY 240
//#define CELLXY 1

#define GEN_DELAY 10 // Set a delay between each generation to slow things down
//Current grid and newgrid arrays are needed
uint8_t grid[GRIDX][GRIDY];
//The new grid for the next generation
uint8_t newgrid[GRIDX][GRIDY];

void setup() {
bluefi.begin();
bluefi.Lcd.fillScreen(TFT_BLACK);
initGrid();
drawGrid();
//Compute generations then show
uint16_t generations = 0;
while ( computeCA() ) {
generations++;
Serial.print("Generations: "); Serial.println(generations);
drawGrid(); // show
for (int16_t x = 1; x < GRIDX-1; x++) {
for (int16_t y = 1; y < GRIDY-1; y++) {
grid[x][y] = newgrid[x][y];
}
}
delay(GEN_DELAY);
}
bluefi.Lcd.setCursor(0, 120, 4);
bluefi.Lcd.setTextColor(TFT_WHITE, TFT_BLACK);
bluefi.Lcd.println("Game over!"); // "\n", to skip a line
}

void loop() {
}

//Draws the grid on the display
void drawGrid(void) {
uint16_t color = TFT_RED;
for (int16_t x = 1; x < GRIDX - 1; x++) {
for (int16_t y = 1; y < GRIDY - 1; y++) {
if ((grid[x][y]) != (newgrid[x][y])) {
if (newgrid[x][y] == 1)
color = random(0xFFFF);
else
color = 0;
bluefi.Lcd.fillRect(CELLXY * x, CELLXY * y, CELLXY, CELLXY, color);
}
}
}
}

//Initialise Grid
void initGrid(void) {
for (int16_t x = 0; x < GRIDX; x++) {
for (int16_t y = 0; y < GRIDY; y++) {
newgrid[x][y] = 0;
if (x == 0 || x == GRIDX - 1 || y == 0 || y == GRIDY - 1) {
grid[x][y] = 0;
} else {
if (random(3) == 1)
grid[x][y] = 1;
else
grid[x][y] = 0;
}
}
}
}

//Compute the CA. Basically everything related to CA starts here
bool computeCA() {
bool changed = false;
for (int16_t x = 1; x < GRIDX; x++) {
for (int16_t y = 1; y < GRIDY; y++) {
uint8_t neighbors = getNumberOfNeighbors(x, y);
if ( grid[x][y] == 1 ) {
if (neighbors != 2 && neighbors != 3 ) {
newgrid[x][y] = 0; //
changed |= true;
}
} else {
if ( neighbors == 3 ) {
newgrid[x][y] = 1; // Invert it (to live)
changed |= true;
}
}
}
}
return changed;
}

// Check the Moore neighborhood
uint8_t getNumberOfNeighbors(int16_t x, int16_t y) {
return grid[x-1][y] + grid[x-1][y-1] + \
grid[x][y-1] + grid[x+1][y-1] + \
grid[x+1][y] + grid[x+1][y+1] + \
grid[x][y+1] + grid[x-1][y+1];
}

这个示例程序仅初始化“setup()”的代码,主循环“loop()”部分无代码(仅仅是一个死循环)。
初始化“setup()”的代码包括,BlueFi相关的接口和硬件初始化,并清除LCD屏幕、调用“initGrid()”和“drawGrid()”两个函数在显示屏上输出第一代“细胞”的模拟效果;
然后设置代表生存代数的变量generations为0,调用函数“computeCA()”根据生存法则计算每一个网格中“细胞”的生与死的状态,如果没有任何“细胞”的状态变化该函数返回false,
否则返回true;如果函数“computeCA()”的返回值为true则将当前的生存代数变量generations增加1并发送到串口控制台,调用函数“drawGrid()”绘制新一代的“细胞”状态,
并将保存此代的状态,延迟若干ms后再次调用函数“computeCA()”;如果函数“computeCA()”的返回值为false则在显示屏上显示“Game Over!”并终止程序。

在BlueFi执行该示例程序前,能猜测出执行效果吗?我们能看到屏幕上显示“Game Over!”?

现在将示例程序编译并下载到BlueFi开源板上,观察该示例程序的运行效果与你所猜测的效果一样?


我们为BlueFi设计的Python解释器默认使用彩色LCD作为字符控制台,用于输出Python解释器的状态,以及执行脚本语句“print()”时的信息输出。
现在双击BlueFi开源板的复位按钮,然后将Python解释器拖放到“BLUEFIBOOT”磁盘,将BlueFi恢复到运行Python脚本的状态。每次BlueFi上电或复位时,
你首先在LCD屏幕上左上角看到CIRCUITPYTHON的Logo——蟒蛇图案,当解释器开始执行“code.py”脚本程序前会在屏幕上显示“code.py output:”提示信息。

换句话说,在Python解释器的状态允许我们直接使用“print(info)”输出数值或文本信息到BlueFi的LCD显示器。如果我们需要在LCD上显示基本图形或其他形式的信息,
那就需要相关的Python库或自建Python代码来实现。我们先让BlueFi的Python解释器运行下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

import time
from hiibot_bluefi.screen import Screen
from hiibot_bluefi.sensors import Sensors
# import Circle, and Circle classes from adafruit_display_shapes
from adafruit_display_shapes.circle import Circle
from adafruit_display_shapes.line import Line
import displayio

# instantiate Screen, and Sensors classes
screen = Screen()
sensors = Sensors()

# define a group of graphic element to draw a backgroud pattern, include 9 elements
bluefi_group = displayio.Group(max_size=9)
# define 9 graphic elements
x_line = Line(0, 120, 240, 120, color=screen.WHITE)
y_line = Line(120, 0, 120, 240, color=screen.WHITE)
outer1_circle = Circle(120, 120, 119, outline=screen.RED)
outer2_circle = Circle(120, 120, 90, outline=screen.YELLOW)
middle_circle = Circle(120, 120, 70, outline=screen.GREEN)
inner2_circle = Circle(120, 120, 50, outline=screen.CYAN)
inner1_circle = Circle(120, 120, 30, outline=screen.BLUE)
inner0_circle = Circle(120, 120, 12, outline=screen.VIOLET)
# append 9 graphic elements into the group of graphic element
bluefi_group.append(x_line)
bluefi_group.append(y_line)
bluefi_group.append(outer1_circle)
bluefi_group.append(outer2_circle)
bluefi_group.append(middle_circle)
bluefi_group.append(inner2_circle)
bluefi_group.append(inner1_circle)
bluefi_group.append(inner0_circle)

# define a group of graphic element to draw a foregroud pattern, a bubble
bubble_group = displayio.Group(max_size=1)
# define this bubble, its x-, and y- coordinate equal to x, and y of sensors.acceleration
x, y, _ = sensors.acceleration
level_bubble = Circle(int(x + 120), int(y + 120), 9, fill=screen.WHITE, outline=screen.WHITE)
# append this bubble into graphic element
bubble_group.append(level_bubble)
# append this graphic element into the group
bluefi_group.append(bubble_group)
# show this group of graphic element on the screen
screen.show(bluefi_group)

while True:
# update the bubble position on the screen according to sensors.acceleration
x, y, _ = sensors.acceleration
bubble_group.y = int(x * 12)
bubble_group.x = int(y * -12)
time.sleep(0.05)

用文本编辑器或MU编辑器,复制上述代码覆盖“/CIRCUITPY/code.py”文件中的全部代码,你将会看到一种水平仪的模拟效果,
显示屏上有多个彩色圆代表水平仪刻度,并用一个白色填充圆代表“气泡”。
倾斜BlueFi板时BlueFi的LCD显示屏上的“气泡”的位置会随之改变,晃动BlueFi板时“气泡”也会随之晃动。

为什么会有这样显示效果呢?尤其是,为什么气泡位置的改变时不会影响其他元素的完整显示呢?
该示例程序的前6行分别导入Screen、Sensors、displayio类模块,以及绘制圆和直线的Circle和Line类模块,
并将Screen和Sensors分别实例化为screen和sensors,使用screen和sensors可以访问BlueFi的显示屏和传感器。
在第14行语句中定义名叫bluefi_group的图形元素组(displayio.Group)且包含9个元素,其后的几行语句分别定义2条白色直线和6个彩色的圆形,
调用“bluefi_group.append(element)”将定义好的这8个图形元素添加到bluefi_group的图形元素组中;然后再绘制一个白色填充圆
代表气泡,这个圆的中心坐标由BlueFi的加速度传感器的x和y分量来确定,最后将图形元素组显示到LCD屏幕上。
主循环程序中,读取加速度传感器的x和y分量,然后更新“气泡”的中心坐标。

这个示例程序中用到的Python库,displayio是BlueFi的Python解释器内建的模块,只需要导入即可使用,使用这个Python模块的接口,
将我们需要在LCD屏幕上显示的若干文本信息、几何图形等分层设计和控制,甚至可以在BlueFi的LCD屏幕上实现动画效果,改变某个显示元素的位置时不会影响其他元素的完整性。譬如,
冒泡排序算法的可视化 [4]_ ,程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

import time
import random
import displayio
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle
from hiibot_bluefi.screen import Screen
screen = Screen()
speed = 0.1 # seconds for changing animation
height = [random.randint(10 , 100) for _ in range(7)]
gol = [0, 1, 2, 3, 4, 5, 6] # list of the index of group elements
x = [26, 58, 90, 122, 154, 186, 218] # list of x-coordinate for each sprite
# creat a group of sprites (5x rects)
group = displayio.Group(max_size=9)
# draw each sprite (5x rects)
s0 = {'x':x[0] , 'y':150-height[0] , 'x2':20 , 'y2':height[0] , 'ot':(0, 52, 255) , 'fl':(0, 26, 255)}
S0 = Rect(s0['x'] , s0['y'] , s0['x2'] , s0['y2'] , outline = s0['ot'] , fill = s0['fl'])
group.append(S0)
s1 = {'x':x[1] , 'y':150-height[1] , 'x2':20 , 'y2':height[1] , 'ot':(255, 0, 0) , 'fl':(255, 0, 0)}
S1 = Rect(s1['x'] , s1['y'] , s1['x2'] , s1['y2'] , outline = s1['ot'] , fill = s1['fl'])
group.append(S1)
s2 = {'x':x[2] , 'y':150-height[2] , 'x2':20 , 'y2':height[2] , 'ot':(212, 255, 0) , 'fl':(212, 255, 0)}
S2 = Rect(s2['x'] , s2['y'] , s2['x2'] , s2['y2'] , outline = s2['ot'] , fill = s2['fl'])
group.append(S2)
s3 = {'x':x[3] , 'y':150-height[3] , 'x2':20 , 'y2':height[3] , 'ot':(63, 255, 0) , 'fl':(63, 255, 0)}
S3 = Rect(s3['x'] , s3['y'] , s3['x2'] , s3['y2'] , outline = s3['ot'] , fill = s3['fl'])
group.append(S3)
s4 = {'x':x[4] , 'y':150-height[4] , 'x2':20 , 'y2':height[4] , 'ot':(0, 216, 255) , 'fl':(0, 216, 255)}
S4 = Rect(s4['x'] , s4['y'] , s4['x2'] , s4['y2'] , outline = s4['ot'] , fill = s4['fl'])
group.append(S4)
s5 = {'x':x[5] , 'y':150-height[5] , 'x2':20 , 'y2':height[5] , 'ot':(255, 0, 255) , 'fl':(255, 0, 255)}
S5 = Rect(s5['x'] , s5['y'] , s5['x2'] , s5['y2'] , outline = s5['ot'] , fill = s5['fl'])
group.append(S5)
s6 = {'x':x[6] , 'y':150-height[6] , 'x2':20 , 'y2':height[6] , 'ot':(255, 216, 0) , 'fl':(255, 216, 0)}
S6 = Rect(s6['x'] , s6['y'] , s6['x2'] , s6['y2'] , outline = s6['ot'] , fill = s6['fl'])
group.append(S6)
# draw a red dot to mark the current minimum
red_dot = Circle( 36, 170, 5, outline=(255,0,0), fill=(255,0,0) )
group.append(red_dot)
white_dot = Circle( 66, 170, 5, outline=(127,127,127), fill=(127,127,127) )
group.append(white_dot)
# show thoese sprites onto BlueFi LCD screen
screen.show(group)

# changing animation
def animation_chg(l, r, steps):
global group
for _ in range( 8 ):
time.sleep(speed)
group[l].x += 4*steps
group[r].x -= 4*steps
#time.sleep(speed)

# no-change animation
def animation_nochg(l, r):
global group
tf = group[l].fill
for _ in range(2):
time.sleep(speed)
group[l].y -= 40
time.sleep(speed)
group[l].y += 40
#time.sleep(speed/4)
group[l].fill = tf

# sort and its animation
for i in range(7):
red_dot.x = x[i]+4
time.sleep(0.1)
for j in range(i+1, 7):
time.sleep(0.1)
white_dot.x = x[j]+4
time.sleep(0.1)
if height[i] > height[j]:
# Exchange their positions, and exchange the index of group elements
c1, c2 = height[j], gol[j]
height[j], gol[j] = height[i], gol[i]
height[i], gol[i] = c1, c2
animation_chg(gol[j], gol[i], j-i)
else:
animation_nochg(gol[j], gol[i])

while True:
pass

这个冒泡排序算法的动画效果显示,将7个彩色方块根据高度升序排列到屏幕上。程序仍使用displayio模块的接口将7个随机高度的方块和2个原点看作是图形元素组中的基本图形元素,
初始状态7个方块的颜色和高度都是随机生成的,高度是无序的,然后使用冒泡算法按他们的高度进行升序排列,当前正在比较和交换的两个方块的下方各用一个圆点来指示,
交换过程的动画由方块的x坐标分量逐渐增加/减小来实现。

通过这个示例,我们不仅掌握如何使用Python语言控制BlueFi的显示屏显示图案和动画,还能帮助我们理解冒泡排序算法本身。你能修改上面代码来改变动画的速度吗?


虽然SPI通讯接口的数据传输速度远高于I2C,而且接口的硬件实现较简单,但SPI通讯接口规范中并没有指定具体的通讯协议,反而允许主机端配置SCK的时钟频率、位序(MSBFIRST/LSBFIRST)、数据线采样模式等,
因此SPI通讯接口协议存在较大差异,每一种SPI接口外设几乎都需要根据其接口协议订制接口软件,本节仅以SPI接口的彩色LCD显示器为例演示SPI主机模式的接口实现和应用示例。


参考文献:
::

[1] https://pdf1.alldatasheetcn.com/datasheet-pdf/view/1132511/SITRONIX/ST7789V.html
[2] https://www.arduino.cc/en/Reference/SPI
[3] https://en.wikipedia.org/wiki/Conway’s_Game_of_Life
[4] https://python4bluefi.readthedocs.io/zh_CN/latest/bluefi_tutorials/advance/bubble_sort_algorithm.html

===========================
6.3 SPI从机模式

MCU片上SPI通讯接口单元工作在从机模式的应用场景往往是双MCU或多MCU系统中,与普通的双核或多核处理器(Multi-Core Processor)组成的系统完全不同。
多核处理器一般是指一颗CPU IC由多个内核组成,多核处理器的内核一般采用对等结构,使用片上高速总线互联,多个对等的内核都是总线的Master,由总线仲裁器管理他们对总线的访问,
多核并行处理同一个任务的指令序列,仅仅是加速事务处理的速度。多MCU系统中,允许多种体系架构CPU内核(或许是多核的结构),不同体系架构的多MCU系统是异构系统,
每一个CPU内核需要单独编程(异构系统内的CPU指令集不同)。

… Note:: 多核处理器和多处理器系统

  • 多核处理器 (Multi-Core Processor),A multi-core processor is a computer processor integrated circuit with two or more separate processing units, called cores, each of which reads and executes program instructions, as if the computer had several processors
  • 多处理器系统 (Multi-Processor system),It is the use of two or more central processing units (CPUs) within a single computer system. The term also refers to the ability of a system to support more than one processor or the ability to allocate tasks between them

主从多处理器系统(Master/Slave Multi-Processor system)是高性能嵌入式系统常用的架构,主MCU作为系统的主控制器,从MCU仅负责系统的特定任务,
譬如网络访问或视频信号处理等任务,允许主从MCU完全不同(包括时钟速度、指令架构等),两个MCU通过共享总线或私有总线、共享内存等方式建立通讯通路。
BlueFi开源板是一种典型的主从多处理器系统,主控制器采用nRF52840,从控制器采用ESP32,主从控制器之间采用SPI通讯接口互联,如图6.14所示。

SPI2021-08-04-20-17-48

图6.14 BlueFi的双MCU结构

两个MCU之间使用标准4线的SPI通讯接口之外还增加2个额外的控制信号(或称握手信号),一个是从MCU的复位控制信号,另一个从MCU的忙/空闲状态信号。
BlueFi开源板使用这种双处理器协作系统,可以将WiFi联网、TCP/IP协议栈等网络事务与其他事务分开,主处理器只需要通过SPI通讯接口向网络协处理器发送网络处理指令,
譬如扫描周围热点(Scan AP)、连接指定热点、连接指定域名的Web服务器等,网络协处理器根据指令及指令参数执行网络事务并通过SPI通讯接口返回执行结果。
这就好比我们使用手机/电脑WiFi访问某个网站的过程,首先打开WiFi配置窗口查看周围热点,连接指定的热点,当WiFi连接到某个AP之后,打开浏览器并输入网址,
即可查看该网页信息。当我们在手机/电脑上使用浏览器打开指定网址的过程中,虽然浏览器首先使用DNS(域名解析系统)获取指定网址的IP地址,
然后使用TCP连接这个IP地址的服务器(还包含默认的TCP端口80),发送HTTP请求,最后接收HTTP报文格式的网页信息并显示在浏览器上,我们所感知的只是打开一个网页,
并不关心DNS、TCP连接和HTTP传输等细节。

下面先用一个示例来演示BlueFi开源板的这种双处理器系统架构的益处。运行示例之前,需要做些准备工作,首先点击下面链接下载本节内容用到的BlueFi的WiFi BSP源文件:

. :download:BlueFi_WiFi_eSPI开源库 <../_static/dl_files/bluefi_ch6_3/Bluefi_WiFi_eSPI.zip>

下载后将压缩文件解压到“…/Documents/Arduino/libraries/”文件夹中,子文件夹“BlueFi_WiFi_eSPI”是主控制器nRF52840通过SPI通讯接口访问网络协处理器的BSP源文件。
本节内容所更新的BlueFi开源板的BSP源文件包的链接如下,请先删除“…/Documents/Arduino/libraries/BlueFi”文件夹中的全部文件,然后下载下面的压缩文件包,
并解压到“…/Documents/Arduino/libraries/”文件夹中,

. :download:本节内容所用到的BlueFi的BSP源文件 <../_static/dl_files/bluefi_ch6_3/BlueFi_bsp_ch6_3.zip>

准备工作并未完毕!以我们使用WiFi联网的经验,必须配置网络协处理器能够连接到某个可用的WiFi热点(AP)。我们的BSP源文件使用一个独立的仅有两行代码的.h文件保存AP名称和密码。
使用文本编辑器修改并保存“…/Documents/Arduino/libraries/BlueFi_WiFi_eSPI/scr/secrets_wifi.h”文件:

1
2
#define SECRET_SSID "your_AP_name"
#define SECRET_PASS "your_AP_password"

第1行双引号内输入你可用的WiFi AP名称代替原来的字符串,第2行双引号内输出这个WiFi AP的密码代替原来的字符串。

然后使用Arduino IDE编辑下面的示例代码,编译并下载到已与你电脑USB相连接BlueFi开源板上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <BlueFi.h>
String hostName = "www.zjut.edu.cn"; // Specify IP address or hostname

void setup() {
bluefi.begin();
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
connectWiFi(true); // see BlueFi_WiFi.h
}

void loop() {
Serial.print("Pinging "); Serial.print(hostName); Serial.print(": ");
int16_t pingResult = WiFi.ping(hostName);
if (pingResult >= 0) {
Serial.print("SUCCESS! RTT = "); Serial.print(pingResult); Serial.println(" ms");
} else {
Serial.print("FAILED! Error code: "); Serial.println(pingResult);
}
delay(5000);
}

当BlueFi执行上面示例期间,点击Arduino IDE菜单栏的“工具–>串口监视器”,打开窗口控制台,将在控制台窗口看到以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13

Check WiFi Coprocessor
WiFi Coprocessor firmware: 1.3.0
Attempting to connect to AP with SSID: your_AP_name
..
Connected to wifi
SSID: your_AP_name
IP Address: 192.168.4.120
signal strength (RSSI):-46 dBm
Pinging www.zjut.edu.cn: SUCCESS! RTT = 10 ms
Pinging www.zjut.edu.cn: SUCCESS! RTT = 0 ms
Pinging www.zjut.edu.cn: SUCCESS! RTT = 10 ms
Pinging www.zjut.edu.cn: SUCCESS! RTT = 10 ms

你或许在其他地方使用过“ping”命令来测试某个网址或网络设备的物理连通性和网络可达性,控制台输出的最后几行信息正是我们的示例程序执行“ping www.zjut.edu.cn”网址的结果,
控制台的前几行提示信息分别是检查网络协处理器及其版本、连接到指定WiFi AP的信息、连接成功后本机IP地址的信息等。

在上面示例程序中,第9行语句“connectWiFi(true)”将“…/Documents/Arduino/libraries/BlueFi_WiFi_eSPI/scr/secrets_wifi.h”文件中的AP名称和密码发送给网络协处理器,
协处理器自动连接指定的AP,并通过控制台给出提示(即前4行的提示信息),一旦连接上之后就给出已连接的AP名称和AP为BlueFi分配的IP地址。
第14行语句“ WiFi.ping(hostName)”将目标网址字符串“hostName”(即“www.zjut.edu.cn”)通过SPI通讯接口发送给网络协处理器,
网络协处理器立即执行“ping www.zjut.edu.cn”命令并返回结果。

“ping”命令是常用的一种网路测试工具,他是基于TCP/IP协议栈的网络层ICMP(Internet Control Message Protocol)协议来实现的。很显然,
上述示例的程序中,主控制器仅仅通过SPI通讯接口将字符串“hostName”和“ping”命令发送给网络协处理器,具体的“ping”命令执行过程则有网络协处理器独立完成,
查看“…/Documents/Arduino/libraries/BlueFi_WiFi_eSPI/scr/”文件夹中的WiFi接口,你会发现这个WiFi的接口源文件中并没有涉及TCP/IP协议栈等。
这里的主控制器所使用的WiFi接口完全兼容Arduino开源平台的WiFi接口库,页面 [1]_ 有这个WiFi接口库的详细说明和参考示例。

当你把前面的的BSP源文件下载并解压到指定文件夹后,我们已经为BlueFi主控制器准备好完整的WiFi联网接口,包括网络配置、TCP/IP应用层的客户端(client)、
服务器端(server)、HTTP和UDP报文收发等接口。基于这些接口,我们只需要通过对主控制器编程即可实现Web访问和应用程序等。

那么,网络协处理器的固件是如何实现的呢?我们的协处理器采用上海乐鑫的Xtensa体系架构的WiFi SoC——ESP32,其固件是从“Arduino NINA-W102 firmware”移植过来的,
固件的源码、编译工具、编译过程等详见 [2]_ 链接及其说明。

在网络协处理器的固件源码中,你将会发现“lwIP”(开源TCP/IP协议栈)和“freeRTOS”(开源RTOS)等被使用。当然这个ESP32的SPI通讯接口工作在从机模式,
我们也能找到该接口的源码实现,即“…/nina-fw/arduino/libraries/SPIS/src/”文件夹的“SPIS.h”和“SPIS.cpp”两个源文件。
在“SPIS.h”和“SPIS.cpp”两个源文件中有该SPI通讯接口所使用的SPI端口号、I/O引脚及其配置等初始化接口“begin()”,
“transfer(uint8_t out[], uint8_t in[], size_t len)”是SPI从模式关键接口,即双向数据传输的实现。
除了第三方开源库(TCP/IP协议栈、RTOS等)、SPI通讯接口(从机模式数据传输接口)之外,协处理器固件的核心工作是SPI通讯接口的命令解析和处理,
即接收、解析主控制器发送过来的命令和参数,然后执行该命令并给以应答。


在Python环境如何使用网络协处理器呢?这需要使用主控制器的WiFi接口的Python模块,包含“/CIRCUITPY/lib/hiibot_bluefi/wifi.py”主接口模块文件,
以及“/CIRCUITPY/lib/adafruit_esp32spi/adafruit_esp32spi.py”模块,请打开页面链接 [3]_ 并根据页面说明下载最新版的BlueFi开源板的Python库文件,
下载并解压后将“…/lib/hiibot_bluefi/”和“…/lib/adafruit_esp32spi/”两个文件夹复制到CIRCUITPY磁盘的“/lib/”文件夹中,
我们就可以正常使用BlueFi开源板的WiFi接口编写Python代码实现网络处理。譬如,下面示例使用WiFi的“scan_networks()”接口扫描周围WiFi热点。
该功能的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

from hiibot_bluefi.wifi import WIFI
wifi = WIFI()

if wifi.esp.status != 0xFF:
print("ESP32 be found and in idle mode")

print("MAC addr:", [hex(i) for i in wifi.esp.MAC_address])

for ap in wifi.esp.scan_networks():
print("\t%s RSSI: %d" % (str(ap["ssid"], "utf-8"), ap["rssi"]))

# turn the power of WIFI to save power! this is very importent
wifi.esp.reset()
print("Program Done!")

复制这些代码并覆盖“/CIRCUITPY/code.py”文件,或者将上述代码复制-粘贴到MU编辑器的代码编辑区,并保存到“/CIRCUITPY/code.py”文件即可,
打开MU编辑器的串口控制台,将会看到以下提示信息:

1
2
3
4
5
6
7
8
9
10
11
12
13

code.py output:
ESP32 be found and in idle mode
MAC addr: ['0x10', '0x7a', '0x83', '0x91', '0x2', '0x50']
myTestAP1 RSSI: -46
1402 RSSI: -64
myTestAP2 RSSI: -65
myPrinter RSSI: -67
AP_40BFE4_4G RSSI: -68
wlKeJiYuan RSSI: -70
alldone1 RSSI: -75
TP-LINK_505F RSSI: -76
Program Done!

具体的WiFi热点名称和信号强度(RSSI)与周边的WiFi环境有关,上述提示仅作参考。
在上面的示例程序中,前两行代码分别是导入WIFI模块及其实例化;第4行和第5行代码分别检查网络协处理器的有效性及错误提示;
第7行代码将网络协处理器WiFi接口的MAC地址打印出来;第9行和第10行首先执行热点扫描(wifi.esp.scan_networks()接口将返回一个AP列表),
然后将AP的名称和信号强度打印到屏幕(或串口控制台);最后两行程序分别将网络协处理器复位(降低系统功耗)和程序终止提示。

接下来我们编写Python代码控制网络协处理器联网,并使用NTP(Network Time Protocol)获取当地的网络时间,然后用BlueFi设计一个简易的电子表功能。
具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

import time, rtc
from hiibot_bluefi.wifi import WIFI
wifi = WIFI()
######### 1. connect to a AP #########
while not wifi.esp.is_connected:
try:
wifi.wifi.connect()
#wifi.esp.connect_AP(b"your_ap_name", b"your_ap_password")
except RuntimeError as e:
print("could not connect to AP, retrying: ", e)
continue
print("Connected to", str(wifi.wifi.ssid, "utf-8"), "\tRSSI: {}".format(wifi.wifi.signal_strength) )
print("My IP address is {}".format(wifi.wifi.ip_address()))
######### 2. Get Local Date&Time with NTP #########
weekDayAbbr = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
TIME_API = "http://worldtimeapi.org/api/ip"
print("get local time from NTP (", TIME_API, ") through WiFi device on the BlueFi")
the_rtc = rtc.RTC()
response = None
while True:
try:
print("Fetching json from", TIME_API)
response = wifi.wifi.get(TIME_API)
break
except (ValueError, RuntimeError) as e:
print("Failed to get data, retrying\n", e)
continue
######### 3. Close WiFi Co-Processor #########
if wifi.esp.is_connected:
wifi.esp.reset()
######### 4. Parse Date&Time from JSON #########
json = response.json()
print(json) # print all message
current_time = json["datetime"]
the_date, the_time = current_time.split("T")
print(the_date)
year, month, mday = [int(x) for x in the_date.split("-")]
the_time = the_time.split(".")[0]
print(the_time)
hours, minutes, seconds = [int(x) for x in the_time.split(":")]
# We can also fill in these extra nice things
year_day = json["day_of_year"]
week_day = json["day_of_week"]
# Daylight Saving Time (夏令时)?
is_dst = json["dst"]
now = time.struct_time(
(year, month, mday, hours, minutes, seconds+1, week_day, year_day, is_dst) )
the_rtc.datetime = now
######### 5. Show Date&Time on the LCD Screen #########
from hiibot_bluefi.screen import Screen
screen = Screen.simple_text_display(
title_scale=3, title_color=Screen.RED, title="bluefi",
text_scale=3, colors=(Screen.WHITE,) )
screen[1].text = "Time"
screen[1].color = Screen.MAGENTA
screen[2].text = "Week"
screen[2].color = Screen.GREEN
screen[3].text = "Date"
screen[3].color = Screen.BLUE
screen.show()
######### 6. Update Date&Time on the LCD Screen #########
while True:
the_date=" {}-{}-{}".format(
the_rtc.datetime.tm_year, the_rtc.datetime.tm_mon, the_rtc.datetime.tm_mday, )
screen[3].text = the_date
the_week=weekDayAbbr[the_rtc.datetime.tm_wday]
screen[2].text = " " +the_week
the_time=" {}:{}:{}".format(
the_rtc.datetime.tm_hour, the_rtc.datetime.tm_min, the_rtc.datetime.tm_sec, )
screen[1].text = the_time

在执行这个示例程序之前,仍需要配置连接指定WiFi热点的名称和密码,与Arduino开源平台的思路一致,这些配置信息保存在一个文本文件中,
即“/CIRCUITPY/secrets.py”,将该文件的“ssid”和“password”两项的值分别修改为你可用的WiFi热点名称和密码并保存。
然后将上面的示例代码保存到“/CIRCUITPY/code.py”文件,BlueFi执行该程序时会提示是否正确地连接到WiFi热点,是否正确滴获取网络时间,
最后在BlueFi的LCD屏幕上显示出日期、时间等信息。

根据注释语句,我们可以清晰地看到整个示例程序分为6个步骤:控制网络协处理器连接到WiFi热点;使用NTP服务获取本地日期和时间信息;
关闭WiFi以节约功耗;解析NTP服务返回的JSON格式信息获取当前的年月日和时分秒信息,并使用这些信息更新本地RTC(日历时钟)单元;
将当前日期和时间信息显示在LCD屏幕指定位置;在主循环中读取本地RTC单元获取最新的日期和时间并更新屏幕显示。

前面的4步仅是为了联网获取本地日期时间并校准本地RTC单元,最后两步才是电子表的设计和实现。这样示例程序在没有使用备用电池的情况下,
每次开机首先联网获取当前时间校准RTC,然后在进入电子表模式。


本节给出一种双处理器系统设计,两个处理器使用SPI通讯接口实现协作事务处理。本节的协处理器是用于处理WiFi联网和网络处理,
网络协处理器的固件需要单独编程,与主机通讯接口的SP单元工作在从机模式,负责从SPI端口接收并解析主控制器发出的命令及其参数,
网络协处理器执行完毕后仍通过SPI通讯接口向主控制器发出应答信息。

SPI通讯接口支持多从机共享通讯总线,根据本节的示例我们很容易实现多处理器系统,只是主控制器需要开销更多个I/O引脚用于从机片选信号、
主从握手信号等。异构型多处理器系统(不同体系架构的MCU组成的系统)能够以较低的成本实现多种事务协作处理,而且具有极高的灵活性,
源于协处理器的可编程特性。


参考文献:
::

[1] https://www.arduino.cc/en/Reference/WiFiNINA
[2] https://github.com/adafruit/nina-fw
[3] https://python4bluefi.readthedocs.io/zh_CN/latest/bluefi_lib/index_lib.html

===========================
6.4 SPI接口应用设计

SPI通讯接口的扩展常用于高速的或大数据容量的功能外设拓展,譬如WiFi、Ethernet、SD/TF卡、大容量高速数据存储器等。与I2C通讯接口相比,
虽然SPI通讯接口的拓扑需要占用更多个I/O引脚用于片选或握手信号,但SPI接口的时钟频率远高于I2C。此外,SPI通讯接口支持全双工通讯,但I2C是半双工的。
我们直到SD/TF卡的存储器容量可以按千兆字节(即GB)来计量,而NOR结构型FlashROM的存储容量仅以MB计量,两者的存取速度相差很大(后者速度更快),
而且这两类存储器都采用SPI或QSPI等接口。大容量存储器不使用I2C通讯接口的另一个原因是,I2C的总线寻址和大容量存储器的地址管理会造成数据存取过程中地址信息的传输将占用大量时间,
数据的存取效率极地。

某些小容量的存储器既有I2C接口的也有SPI的,譬如MRFRAM(Magnetic Relaxor Ferroelectric RAM, 磁性弛豫铁电RAM),FRAM的容量仅有128B~512KB范围。
这种存储器既有普通SRAM一样的存取速度和读写寿命(达万亿次),又有普通FlashROM一样的不挥发性(即断电后数据仍能保持数十年不丢失)。FRAM常用于记录系统运行时的关键数据信息,
即使没有后备电池系统突然断电也不必担心这些数据会丢失,而且在系统运行期间将数据写入FRAM操作的时间与RAM一样地快。譬如航空航天器控制系统内的数据采集和记录单元,
当数据采集和记录的频次较高时(如每秒1K次),使用普通FlashROM记录数据的写入速度无法满足要求,使用普通RAM记录数据如果突遇断电会丢失(未转移的)部分数据,
FRAM能更好地满足此类需求。绝大多数FRAM产品都支持I2C或SPI通讯接口,I2C接口的时钟频率最高可达1MHz,而SPI接口的时钟频率达20MHz。
我们该选择那种接口,需要根据使用FRAM类小容量存储器的目的和数据存取速度、频率来确定。

除了同步时钟频率外,基于SPI通讯接口的功能能外设扩展设计还需要考虑其他一些因素,如外设工作电压、逻辑电平转换、握手信号的有效电平和默认状态等。
当SPI接口的外设工作电压、接口逻辑电平电压与主控制器I/O引脚的逻辑电平向匹配时,这些问题都非常简单,标准SPI通讯接口的SCK、MOSI和MISO都采用推挽型驱动电路,
SPI外设的这些信号引脚直接与总线对应连接即可。当逻辑电平电压不匹配时,电平转换电路单元是接口设计中不可缺的,由于SPI总线的信号方向都是单向的,
单向的和三态的电平转换IC非常多,OnSemi(安森美)、TI、NXP(或安世)等都有电平转换产品系列可选用。接口功能方面,握手信号与SPI接口的片选信号相似,
使用主控制器的低速I/O引脚即可,当然逻辑电平电压的匹配也是需要考虑的,外设的片选信号的默认状态应该设为无效电平,握手信号也应按功能选择合适的默认电平状态,
即系统复位后或接口未激活时的电平状态,一般使用上拉或下拉电阻来设置。


现在我们以硬件TCP/IP协议栈单元的扩展为例来说明SPI接口的通用扩展方法。硬件协议栈,顾名思义就是使用纯数字电路硬件实现TCP/IP协议。对于存储容量小、
计算能力弱的MCU来说,硬件协议栈是实现IoT应用的最佳方案。前一节的双处理器系统的网络协处理器也有相似的作用,WiFi网络协处理器需要单独编程,
使用软件结合WiFi MAC和PHY等硬件单元实现TCP/IP全栈功能。本节使用WIZnet的W5500硬件TCP/IP协议栈 [1]_ ,采用标准SPI通讯接口与BlueFi金手指拓展接口的P13~P16引脚连接,
进而与BlueFi的主控制器——nRF52840实现主从通讯,为BlueFi拓展有线的Ethernet(以太网)功能接口。我们将在后续的内容中使用Ethernet接口,
本节使用SPI通讯接口拓展的硬件TCP/IP协议栈不仅是一种SPI接口设计示例,也是一种Ethernet功能接口拓展方法。

硬件TCP/IP协议栈W5500的内部结构如图6.15所示。

SPI2021-08-04-20-18-49

图6.15 硬件TCP/IP协议栈——W5500的内部结构(来自WIZnet)

TCP/IP协议栈的实现是W5500的核心,包含一个100BT的Ethernet PHY单元,外围只需一个网络隔离变压器和RJ45插座(或者使用内置网络隔离变压器的RJ45插座)即可让系统接入Ethernet网络;
PHY通过标准的MII(介质无关的接口)与MAC层(数据链路层的关键协议,确保数据报文的可靠性和完整性)连接,网络层(包含IP、ARP和PPPoE等)、传输层(TCP和UDP等)等通过内部总线与下层协议单元相连接。
W5500内部有一个32KB的Ethernet接收/发送缓存,主控制器可通过SPI通讯接口发送偏移地址访问这些缓存(即读写TCP/IP协议报文)。
使用W5500扩展Ethernet功能接口的信号连接如图6.16所示。

SPI2021-08-04-20-17-56

图6.16 BlueFi拓展Ethernet功能接口的信号连接

具体的电路原理图如图6.17所示。

SPI2021-08-04-20-18-24

图6.17 使用BlueFi金手指拓展W5500的电路原理图

上图的左半部分是RJ45(内置网络隔离变压器)插座、BlueFi金手指拓展接口插座的电路原理,右半部分则是W5500及其外围的基本电路,
W5500共有5个信号连接到BlueFi金手指拓展接口上,包括4个标准SPI通讯接口信号和一个中断请求信号。根据W5500的数据页(Datasheet)所列的电气接口信息,
建议其工作电压和I/O接口逻辑电压为3.3V,正好与BlueFi的主控制器I/O逻辑电压一致,所以W5500的这些接口信号可以与BlueFi金手指拓展接口信号直连,
并使用BlueFi金手指的3.3V电源输出为其供电,注意W5500的最大消耗电流约150mA。

W5500需要外置的25MHz晶体振荡器为其内部PLL单元提供低频时钟信号,其内部PHY单元的工作模式是通过3个引脚PHY_M2/M1/M0的逻辑电平的组合配置,
上图中使用一个简表列举常用配置,这3个配置引脚内部带有上拉电阻,即默认为自动协商模式。W5500的片选信号和中断请求信号都设置有上拉电阻,
当这个Ethernet功能接口未与主控制器连接时,即使接口信号都为悬空状态,W5500默认是未被选择的状态,即空闲状态。

W5500支持的SPI通讯接口协议如图6.18所示。完全兼容标准SPI通讯接口以字节整数倍来传输数据,且每一次传输数据至少4个字节,包含2字节的地址信息、
1字节的控制字和至少1字节的数据。数据域的传输方向和模式由控制字的低3位来指定。W5500的数据传输模式分为4种,数据域个数可变的模式,或固定为1/2/4个字节的模式。

SPI2021-08-04-20-19-04

图6.18 W5500支持的SPI通讯接口协议

根据这个Ethernet功能接口电路原理图,以及WIZnet提供的接口库,我们可以实现其软件接口和Ethernet应用,如Web(HTTP)、e-mail(SMTP和POP3)、FTP等。
Arduino开源平台的Ethernet库 [2]_ 支持W5500,或者使用其改进版的库 [3]_ ,基于这些开源库代码在Arduino开源平台上使用这里拓展的Ethernet功能接口是非常容易的。

Arduino开源平台的Ethernet库的接口及其应用示例参见 [4]_ 。
参考Arduino官网的编写自定义开源库的向导 [5]_ 可自行实现Ethernet接口库,或者参考 [6]_ 页面的操作向导安装第三方的开源库。


上面基于BlueFi金手指拓展的Ethernet功能接口仍可以使用Python脚本编程来实现网络连接。首先将BlueFi插入电脑USB端口,双击BlueFi复位按钮,
将BlueFi的Python解释器固件拖放到BLUEFIBOOT磁盘,即可使用Python脚本程序控制BlueFi。

使用标准Ethernet网线将BlueFi开源板及其Ethernet功能接口连接到可用的以太网内,打开电源,并将下面的示例程序代码保存到“/CIRCUITPY/code.py”文件,
给BlueFi开源板通电。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import board
import busio
import digitalio
import adafruit_requests as requests
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket

print("Wiznet5k WebClient Test")

TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_URL = "http://api.coindesk.com/v1/bpi/currentprice/USD.json"

cs = digitalio.DigitalInOut(board.P16)
spi_bus = busio.SPI(board.P13, MISO=board.P14, MOSI=board.P15)

# Initialize ethernet interface with DHCP
eth = WIZNET5K(spi_bus, cs)

# Initialize a requests object with a socket and ethernet interface
requests.set_socket(socket, eth)

print("Chip Version:", eth.chip)
print("MAC Address:", [hex(i) for i in eth.mac_address])
print("My IP address is:", eth.pretty_ip(eth.ip_address))
print(
"IP lookup adafruit.com: %s" % eth.pretty_ip(eth.get_host_by_name("adafruit.com"))
)

# eth._debug = True
print("Fetching text from", TEXT_URL)
r = requests.get(TEXT_URL)
print("-" * 40)
print(r.text)
print("-" * 40)
r.close()

print()
print("Fetching json from", JSON_URL)
r = requests.get(JSON_URL)
print("-" * 40)
print(r.json())
print("-" * 40)
r.close()

print("Program Done!")

如果接入的以太网端口与广域网是连通的,我们将会看到从测试网页抓取到的文本信息。
这个示例程序用到3种开源库:requests、WIZNET5K和socket,分别是HTTP请求、W5500的SPI通讯接口和网络套接字,
运行该示例程序前请从BlueFi的Python库软件包中复制这三个库文件到“/CIRCUITPY/lib/”文件夹,否则Python解释器会提示错误。

与WiFi接口不同,Ethernet接口无须特殊配置即可连接到广域网,只要求所连接的网络设备(如路由器)能够连接到广域网。


参考文献:
::

[1] https://www.wiznet.io/product-item/w5500/
[2] https://github.com/arduino-libraries/Ethernet
[3] https://github.com/sstaub/Ethernet3
[4] https://www.arduino.cc/en/Reference/Ethernet
[5] https://www.arduino.cc/en/Hacking/LibraryTutorial
[6] https://www.arduino.cc/en/Guide/Libraries

===========================
6.5 本章总结

SPI是一种高速同步通讯接口,也是现代绝大多数MCU片上的一种基本功能单元,虽然仅支持主从通讯模式,但数据传输速度几乎是I2C的1000倍,
SPI通讯接口已经成为一种最常用的系统内高速外设拓展接口,包括LCD显示器、SD/TF卡、闪存(FlashROM)、pSRAM(伪静态RAM)、网络等外设。

SPI通讯接口是一种伪共享通讯总线,共享总线仅有SCK、MISO和MOSI三个信号,但每一个SPI从机必须有惟一的片选信号和一些必要的握手信号,
使用SPI接口连接多个从外设时需要开销更多个I/O引脚。

SPI通讯接口支持全双工数据传输模式,也支持半双工模式,而且半双工模式可以节约一个I/O引脚资源。SPI通讯接口的硬件仅仅是移位寄存器,
通讯协议/时序仅规定以8位(单字节)的整数倍的数据传输格式和4种数据线采样模式之外,并没有更多的信息格式规定,
这意味着每一种SPI从外设都有自定义的数据格式,因此SPI通讯接口外设没有统一的接口库。

本章中,我们首先了解SPI通讯接口的电路连接和基本时序/协议,包括总线拓扑、数据线的4种采样模式,并了解多种改进的SPI通讯接口。
然后从SPI主机模式和从机模式两种角度了解SPI接口的硬件设计和软件编程,并以SPI接口的LCD显示器和网络协处理器等为例分别说明两种模式的接口。

通过本章学习,我们初步掌握SPI通讯接口的基本原理、接口设计方法、编程控制及应用。


本章总结如下:

  1. SPI通讯接口主机和从机的移位寄存器结构、全双工和半双工连接方式、2种总线拓扑,SPI接口的基本时序、数据线的4种采样模式
  2. 改进的SPI通讯接口,如QSPI、SDIO等
  3. SPI接口主机的接口设计,基于SPI接口的彩色LCD显示器的软硬件设计
  4. SPI接口从机的接口设计,基于SPI接口的双处理器系统的软硬件设计,WiFi网络协处理器的编程应用
  5. SPI接口的系统功能拓展设计及应用,基于SPI通讯接口的硬件TCP/IP协议栈的Ethernet功能拓展

===========================
思考题

  1. 根据图6.1和图6.4,简述SPI通讯接口的主机向从机的0x1234地址单元写入数据0x5678的传输过程。
  2. 根据图6.3所示的两种SPI总线拓扑,分别简述两种总线拓扑的主机访问其中某个从机的过程。
  3. BlueFi的彩色LCD显示器由240*240个像素组成的点阵显示器,每个像素大小和像素间距大小是固定的,像素和间距越小则显示效果越细腻。
    下图是5(列)7(行)点阵字符的示意图,请给出“0”~“9”十个数字字符的字模数据(含字符间隔);当我们需要将这些字符放大2倍(10列14行)、4倍或2n倍显示时,
    请给出字模放大算法。

SPI2021-08-04-20-19-20

  1. 图形LCD显示器不仅可以显示几何图形、字符,也可以显示汉字等象形文字。假设使用16*16点阵(含字间距)显示单个汉字,请给出自己名字的汉字字模数据,
    并使用BlueFi将这些汉字显示在LCD屏幕上。
  2. 请列举多处理器系统的优缺点。
  3. 使用搜索引擎查阅FRAM存储器MB85RS16的Datasheet,并列举其他半导体公司的同类存储器。如果BlueFi需要使用此类存储器保存关键数据,
    请以BlueFi金手指拓展接口设计该存储器的软硬件接口。