SPI总线

SPI 简介

SPI 的全称是"Serial Peripheral Interface",意为串行外围接口,是Motorola 首先在其 MC68HCXX 系列处理器上定义的。SPI 接口主要应用在 EEPROM、
FLASH、实时时钟、AD 转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,比如 STM32 系列芯片。下面我们看下 SPI 内部结构简易图,如图 39.1.1.1 所示:

SPI 物理层

SPI 通讯设备之间的常用连接方式见图 25-1。

  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):主设备输入/从设备输出引脚。主机从这条信号线
读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。

协议层

与 I2C 的类似,SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环节。
1. SPI 基本通讯过程
  先看看 SPI 通讯的通讯时序,见图 25-2。

这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。以上通讯流程中包含的各个信号分解如下: 
2. 通讯的起始和停止信号
  在图 25-2 中的标号1处,NSS 信号线由高变低,是 SPI 通讯的起始信号。NSS 是每个从机各自独占的信号线,当从机在自己的 NSS 线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。在图中的标号6处,NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。 
3. 数据有效性
  SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数据传输时,MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用同样的协定,一般都会采用图 25-2 中的 MSB 先行模式。观察图中的2、3、4、5标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出,在 SCK 的下降沿时被采样即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO为下一次表示数据做准备。SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。 
4. CPOL/CPHA 及通讯模式
  上面讲述的图 25-2 中的时序只是 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 的“偶数边沿”采样。见图 25-3 及图 25-4。

  我们来分析这个 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 的有效信号才发生切换。 
 

  由 CPOL 及 CPHA 的不同状态,SPI 分成了四种模式,见表 25-1,主机与从机需要工作在相同的模式下才可以正常通讯,实际中采用较多的是“模式 0”与“模式 3”。

STM32 的 SPI 特性及架构
  与 I2C 外设一样,STM32 芯片也集成了专门用于 SPI 协议通讯的外设。 
STM32 的 SPI 外设简介
  STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2(STM32F103 型号的芯片默认 fpclk1为 72MHz,fpclk2为 36MHz),完全支持 SPI 协议的 4 种模式,数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双线全双工(前面小节说明的都是这种模式)、双线单向以及单线模式。其中双线单向模式可以同时使用 MOSI 及 MISO 数据线向一个方向传输数据,可以加快一倍的传输速度。而单线模式则可以减少硬件接线,当然这样速率会受到影响。我们只讲解双线全双工模式。 
STM32 的 SPI 架构剖析

1.通讯引脚
  SPI 的所有硬件架构都从图 25-5 中左侧 MOSI、MISO、SCK 及 NSS 线展开的。STM32 芯片有多个 SPI 外设,它们的 SPI 通讯信号引出到不同的 GPIO 引脚上,使用时必须配置到这些指定的引脚,见表 25-2。

  其中 SPI1 是 APB2 上的设备,最高通信速率达 36Mbtis/s,SPI2、SPI3 是 APB1 上的设备,最高通信速率为 18Mbits/s。除了通讯速率,在其它功能上没有差异。其中 SPI3 用到了下载接口的引脚,这几个引脚默认功能是下载,第二功能才是 IO 口,如果想使用 SPI3接口,则程序上必须先禁用掉这几个 IO 口的下载功能。一般在资源不是十分紧张的情况下,这几个 IO 口是专门用于下载和调试程序,不会复用为 SPI3。 
2. 时钟控制逻辑
  SCK 线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2]位控制,该位是对 fpclk时钟的分频因子,对 fpclk的分频结果就是 SCK 引脚的输出时钟频率,计算方法见表 25-3。

  其中的 fpclk频率是指 SPI 所在的 APB 总线频率,APB1 为 fpclk1,APB2 为 fpckl2。通过配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 设置成前面分析的 4 种 SPI 模式。
3. 数据控制逻辑
  SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标接收、发送缓冲区以及 MISO、MOSI 线。
当向外发送数据的时候,数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;
当从外部接收数据的时候,数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。
通过写 SPI的“数据寄存器 DR”把数据填充到发送 F 缓冲区中,通讯读“数据寄存器 DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可选择 MSB 先行还是 LSB 先行。
4. 整体控制逻辑
  整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB先行、主从模式、单双向模式等等。
  在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制NSS 信号线。
  实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。

通讯过程

STM32 使用 SPI 外设通讯时,在通讯的不同阶段它会对“状态寄存器 SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。 
图 25-6 中的是“主模式”流程,即 STM32 作为 SPI 通讯的主机端时的数据收发过程。

主模式收发流程及事件说明如下:
(1) 控制 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”中的数据。 

SPI 初始化结构体详解

跟其它外设一样,STM32 标准库提供了 SPI 初始化结构体及初始化函数来配置 SPI 外设。初始化结构体及函数定义在库文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对 SPI 外设运用自如了,代码如下。

这些结构体成员说明如下,其中括号内的文字是对应参数在 STM32 标准库中定义的宏:
(1) SPI_Direction
本成员设置 SPI 的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex),双线只接收(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。
(2) SPI_Mode
本成员设置 SPI 工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这两个模式的最大区别为 SPI 的 SCK 信号线的时序,SCK 的时序是由通讯中的主机产生的。若被配置为从机模式,STM32 的 SPI 外设将接受外来的 SCK 信号。
(3) SPI_DataSize
本成员可以选择 SPI 通讯的数据帧大小是为 8 位(SPI_DataSize_8b)还是 16 位(SPI_DataSize_16b)。
(4) SPI_CPOL 和 SPI_CPHA
这两个成员配置 SPI 的时钟极性 CPOL 和时钟相位 CPHA,这两个配置影响到 SPI 的通讯模式,关于 CPOL 和 CPHA 的说明参考前面“通讯模式”小节。
时钟极性 CPOL 成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。
时钟相位 CPHA 则可以设置为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。 
(5) SPI_NSS
本成员配置 NSS 引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。
(6) SPI_BaudRatePrescaler
本成员设置波特率分频因子,分频后的时钟即为 SPI 的 SCK 信号线的时钟频率。这个成员参数可设置为 fpclk 的 2、4、6、8、16、32、64、128、256 分频。
(7) SPI_FirstBit
所有串行的通讯协议都会有 MSB 先行(高位数据在前)还是 LSB 先行(低位数据在前)的问题,而 STM32 的 SPI 模块可以通过这个结构体成员,对这个特性编程控制。
(8) SPI_CRCPolynomial
这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值。
  配置完这些结构体成员后,我们要调用 SPI_Init 函数把这些参数写入到寄存器中,实现 SPI 的初始化,然后调用 SPI_Cmd 来使能 SPI 外设。

SPI 配置步骤

(1)使能 SPI 及对应 GPIO 端口时钟并配置引脚的复用功能
  要使用 SPI 就必须使能它的时钟,前面介绍框图时,我们知道 SPI1 是挂接在 APB2 总线上,而 SPI2 和 SPI3 挂接在 APB1 总线上。而且 SPI 总线接口对应不同的 STM32 引脚,所以还需使能对应引脚的端口时钟,同时配置为复用功能。
(2)初始化 SPI,包括数据帧长度、传输模式、MSB 和 LSB 顺序等

(3)使能(开启)SPI

(4)SPI 数据传输
  通过上面几个步骤的配置,SPI 已经可以开始通信了,在通信的过程中肯定会有数据的发送和接收,固件库也提供了 SPI 的发送和接收函数。
SPI 发送数据函数原型为:
  void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);这个函数很好理解,往 SPIx 数据寄存器写入数据 Data,从而实现发送。
SPI 接收数据函数原型为:
  uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);此函数非常简单,从 SPIx 数据寄存器中读取接收到的数据。
(5)查看 SPI 传输状态
  在 SPI 传输过程中,我们经常要判断数据是否传输完成,发送区是否为空等状态,这是通过函数 SPI_I2S_GetFlagStatus 实现的,此函数原型为:
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
此函数非常简单,第二个参数是用来选择 SPI 传输过程中判断的标志,对应的标志可在 stm32f10x_spi.h 文件中查找到,使用较多的是发送完成标志(SPI_I2S_FLAG_TXE)和接收完成标志(SPI_I2S_FLAG_RXNE)。
判断发送是否完成的方法是:
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE);
将以上几步配置好后,我们就可以使用 STM32F1 的 SPI 和外部 FLASH(EN25QXX)通信了。

spi配置代码(只配置了spi1)

 1 #ifndef _spi_H
2 #define _spi_H
3
4 #include "system.h"
5
6 void SPI1_Init(void); //初始化SPI1口
7 void SPI1_SetSpeed(u8 SpeedSet); //设置SPI1速度
8 u8 SPI1_ReadWriteByte(u8 TxData);//SPI1总线读写一个字节
9
10 //void SPI2_Init(void); //初始化SPI2口
11 //void SPI2_SetSpeed(u8 SpeedSet); //设置SPI2速度
12 //u8 SPI2_ReadWriteByte(u8 TxData);//SPI2总线读写一个字节
13
14 #endif
 1 #include "spi.h"
2
3 //以下是SPI模块的初始化代码,配置成主机模式
4 //SPI口初始化
5 //这里针是对SPI1的初始化
6 void SPI1_Init(void)
7 {
8 GPIO_InitTypeDef GPIO_InitStructure;
9 SPI_InitTypeDef SPI_InitStructure;
10
11 /* SPI的IO口和SPI外设打开时钟 */
12 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
13 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
14
15 /* SPI的IO口设置 */
16 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
18 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
19 GPIO_Init(GPIOA, &GPIO_InitStructure);
20
21 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
22 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI
23 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //设置SPI的数据大小:SPI发送接收8位帧结构
24 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
25 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
26 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
27 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
28 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
29 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
30 SPI_Init(SPI1, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
31
32 SPI_Cmd(SPI1, ENABLE); //使能SPI外设
33
34 SPI1_ReadWriteByte(0xff);//启动传输
35 }
36
37 //SPI1速度设置函数
38 //SPI速度=fAPB2/分频系数
39 //@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256
40 //fAPB2时钟一般为84Mhz:
41 void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler)
42 {
43 SPI1->CR1&=0XFFC7;//位3-5清零,用来设置波特率
44 SPI1->CR1|=SPI_BaudRatePrescaler; //设置SPI1速度
45 SPI_Cmd(SPI1,ENABLE); //使能SPI1
46 }
47
48 //SPI1 读写一个字节
49 //TxData:要写入的字节
50 //返回值:读取到的字节
51 u8 SPI1_ReadWriteByte(u8 TxData)
52 {
53
54 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);//等待发送区空
55
56 SPI_I2S_SendData(SPI1, TxData); //通过外设SPIx发送一个byte 数据
57
58 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一个byte
59
60 return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据
61
62 }

上述程序中的一个奇怪的地方

在复用SPI总线时,必须先设置总线端口。读取其他ARM芯片(如NXP)一般很容易看出芯片的设置是否正确。不过对于STM32就容易让人迷惑了。就像上述程序中,我们在使用SPI总线进行通信时,可以这样设置:

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7 
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;     // 复用的推挽输出

其他端口如时钟端口以及MOSI端口都是stm32向外输出,引脚设置成推挽输出没问题, 但是大家对MISO端口的设置就会产生疑惑了,MISO不是应该设置成为输入端口(GPIO_Mode_IN_FLOATING)才行的吗?

答题是肯定的,对于STM32的这一类管脚来说(如USART_RX)即可以设置成为输入模式,也可以设置成为复用的推挽输出。其工作都是正常的,不过建议大家还是设置成为输入端口的好,容易理解。

具体产生这一问题的原因是:从功能上来说,MISO应该配置为输入模式才对,但为什么也可以配置为GPIO_Mode_AF_PP?请看下面的GPIO复用功能配置框图。当一个GPIO端口配置为GPIO_Mode_AF_PP是,这个端口的内部结构框图如下:

  图中可以看到,片上外设的复用功能输出信号会连接到输出控制电路,然后在端口上产生输出信号。但是在芯片内部,MISO是SPI模块的输入引脚,而不是输出引脚,也就是说图中的"复用功能输出信号"根本不存在(MISO不会产生输出信号),因此"输出控制电路"不能对外产生输出信号。

  而另一方面看,即使在GPIO_Mode_AF_PP模式下,复用功能输入信号却与外部引脚之间相互连接,既MISO得到了外部信号的电平,实现了输入的功能(可以4-5-6-7路线输入数据,复用的情况下就是4-5-复用功能路线)。

FLASH介绍

控制 FLASH 的指令

  搞定 SPI 的基本收发单元后,还需要了解如何对 FLASH 芯片进行读写。FLASH 芯片自定义了很多指令,我们通过控制 STM32 利用 SPI 总线向 FLASH 芯片发送指令,FLASH芯片收到后就会执行相应的操作。而这些指令,对主机端(STM32)来说,只是它遵守最基本的 SPI 通讯协议发送出的数据,但在设备端(FLASH 芯片)把这些数据解释成不同的意义,所以才成为指令。查看FLASH 芯片的数据手册《W25Q64》,可了解各种它定义的各种指令的功能及指令格式,见表 25-4。 
 

  该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23”指 FLASH 芯片内部存储器组织的地址;“M0~M7”为厂商号(MANUFACTURER ID);“ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7”为 FLASH 内部存储矩阵的内容。 
  在 FLSAH 芯片内部,存储有固定的厂商编号(M7-M0)和不同类型 FLASH 芯片独有的编号(ID15-ID0),见表 25-5。

  通过指令表中的读 ID 指令“JEDEC ID”可以获取这两个编号,该指令编码为“9Fh”,其中“9F h”是指 16 进制数“9F” (相当于 C 语言中的 0x9F)。紧跟指令编码的三个字节分别为 FLASH 芯片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。此处我们以该指令为例,配合其指令时序图进行讲解,见图 25-8。 

  主机首先通过 MOSI 线向 FLASH 芯片发送第一个字节数据为“9F h”,当 FLASH 芯片收到该数据后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响应:通过 MISO 线把它的厂商 ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指令响应后可进行校验。常见的应用是主机端通过读取设备 ID 来测试硬件是否连接正常,或用于识别设备。对于 FLASH 芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应包含更多的数据。
  实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么通讯协议。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那么我们就先根据它的通讯协议,选择好 STM32 的硬件模块,并进行相应的 I2C 或 SPI 模块初始化。接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而 FLASH 则定义了更多的指令,有写指令,读指令,读 ID 指令等等。最后,我们根据这些指令的格式要求,使用通讯协议向设备发送指令,达到控制设备的目标

定义 FLASH 指令编码表

为了方便使用,我们把 FLASH 芯片的常用指令编码使用宏来封装起来,后面需要发送指令编码的时候我们直接使用这些宏即可。
 1 //指令表
2 #define EN25X_WriteEnable 0x06
3 #define EN25X_WriteDisable 0x04
4 #define EN25X_ReadStatusReg 0x05
5 #define EN25X_WriteStatusReg 0x01
6 #define EN25X_ReadData 0x03
7 #define EN25X_FastReadData 0x0B
8 #define EN25X_FastReadDual 0x3B
9 #define EN25X_PageProgram 0x02
10 #define EN25X_BlockErase 0xD8
11 #define EN25X_SectorErase 0x20
12 #define EN25X_ChipErase 0xC7
13 #define EN25X_PowerDown 0xB9
14 #define EN25X_ReleasePowerDown 0xAB
15 #define EN25X_DeviceID 0xAB
16 #define EN25X_ManufactDeviceID 0x90
17 #define EN25X_JedecDeviceID 0x9F

读取 FLASH 芯片 ID

根据“JEDEC”指令的时序,我们把读取 FLASH ID 的过程编写成一个函数。
 1 //读取芯片ID
2 //返回值如下:
3 //0XEF13,表示芯片型号为EN25Q80
4 //0XEF14,表示芯片型号为EN25Q16
5 //0XEF15,表示芯片型号为EN25Q32
6 //0XEF16,表示芯片型号为EN25Q64
7 //0XEF17,表示芯片型号为EN25Q128
8 u16 EN25QXX_ReadID(void)
9 {
10 u16 Temp = 0;
11 EN25QXX_CS=0;
12 SPI2_ReadWriteByte(0x9F);//发送读取ID命令
13 SPI2_ReadWriteByte(0x00);
14 SPI2_ReadWriteByte(0x00);
15 SPI2_ReadWriteByte(0x00);
16 Temp|=SPI2_ReadWriteByte(0xFF)<<8;
17 Temp|=SPI2_ReadWriteByte(0xFF);
18 //EN25QXX_CS=1;
19 return Temp;
20 }
  这段代码利用控制 CS 引脚电平的宏“SPI_FLASH_CS_LOW/HIGH”以及前面编写的单字节收发函数 SPI_FLASH_SendByte,很清晰地实现了“JEDEC ID”指令的时序:发送一个字节的指令编码“W25X_JedecDeviceID”,然后读取 3 个字节,获取 FLASH 芯片对该指令的响应,最后把读取到的这 3 个数据合并到一个变量 Temp 中,然后作为函数返回值,把该返回值与我们定义的宏“sFLASH_ID”对比,即可知道 FLASH 芯片是否正常。 
 

FLASH 写使能以及读取当前状态

在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命令即可写使能。
1 //EN25QXX写使能
2 //将WEL置位
3 void EN25QXX_Write_Enable(void)
4 {
5 EN25QXX_CS=0; //使能器件
6 SPI2_ReadWriteByte(EN25X_WriteEnable); //发送写使能
7 EN25QXX_CS=1; //取消片选
8 }
  
  与 EEPROM 一样,由于 FLASH 芯片向内部存储矩阵写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,见图 25-9

我们只关注这个状态寄存器的第 0 位“BUSY”,当这个位为“1”时,表明 FLASH芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。利用指令表中的“Read Status Register”指令可以获取 FLASH 芯片状态寄存器的内容,其时序见图 25-10。 

  只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最新的状态寄存器内容,直到收到 SPI 通讯的停止信号。据此我们编写了具有等待 FLASH 芯片写入结束功能的函数,见下面代码。 
 1 //读取EN25QXX的状态寄存器
2 //BIT7 6 5 4 3 2 1 0
3 //SPR RV TB BP2 BP1 BP0 WEL BUSY
4 //SPR:默认0,状态寄存器保护位,配合WP使用
5 //TB,BP2,BP1,BP0:FLASH区域写保护设置
6 //WEL:写使能锁定
7 //BUSY:忙标记位(1,忙;0,空闲)
8 //默认:0x00
9 u8 EN25QXX_ReadSR(void)
10 {
11 u8 byte=0;
12 EN25QXX_CS=0; //使能器件
13 SPI2_ReadWriteByte(EN25X_ReadStatusReg); //发送读取状态寄存器命令
14 byte=SPI2_ReadWriteByte(0Xff); //读取一个字节
15 EN25QXX_CS=1; //取消片选
16 return byte;
17 }
1 //等待空闲
2 void EN25QXX_Wait_Busy(void)
3 {
4 while((EN25QXX_ReadSR()&0x01)==0x01); // 等待BUSY位清空
5 }

FLASH 扇区擦除

由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的 FLASH 芯
片支持“扇区擦除”、“块擦除”以及“整片擦除”,见表 25-6。 

FLASH 芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含 16 个扇区,其内部存储矩阵分布见图 25-11。

使用扇区擦除指令“Sector Erase”可控制 FLASH 芯片开始擦写,其指令时序见图25-14。 

扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦除指令后,通过读取寄存器状态等待扇区擦除操作完毕,代码如下。
 1 //擦除一个扇区
2 //Dst_Addr:扇区地址 根据实际容量设置
3 //擦除一个山区的最少时间:150ms
4 void EN25QXX_Erase_Sector(u32 Dst_Addr)
5 {
6 //监视falsh擦除情况,测试用
7 printf("fe:%x\r\n",Dst_Addr);
8 Dst_Addr*=4096;
9 EN25QXX_Write_Enable(); //SET WEL
10 EN25QXX_Wait_Busy();
11 EN25QXX_CS=0; //使能器件
12 SPI2_ReadWriteByte(EN25X_SectorErase); //发送扇区擦除指令
13 SPI2_ReadWriteByte((u8)((Dst_Addr)>>16)); //发送24bit地址
14 SPI2_ReadWriteByte((u8)((Dst_Addr)>>8));
15 SPI2_ReadWriteByte((u8)Dst_Addr);
16 EN25QXX_CS=1; //取消片选
17 EN25QXX_Wait_Busy(); //等待擦除完成
18 }
这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用扇区擦除指令时注意输入的地址要对齐到 4KB。
 

FLASH 的页写入

目标扇区被擦除完毕后,就可以向它写入数据了。与 EEPROM 类似,FLASH 芯片也有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256 个字节的数据,我们把这个单位为页大小。FLASH 页写入的时序见图 25-13。 

  从时序图可知,第 1 个字节为“页写入指令”编码,2-4 字节为要写入的“地址 A”,接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始,按顺序写入到 FLASH 的存储矩阵。若发送的数据超出 256 个,则会覆盖前面发送的数据。与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后,发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写入 200 个字节也是没有问题的(小于 256 均可)。 只是在实际应用中由于基本擦除单元是4KB,一般都以扇区为单位进行读写,想深入了解,可学习我们的“FLASH 文件系统”相关的例子。把页写入时序封装成函数,其实现见下列代码。 
 1 //写SPI FLASH
2 //在指定地址开始写入指定长度的数据
3 //该函数带擦除操作!
4 //pBuffer:数据存储区
5 //WriteAddr:开始写入的地址(24bit)
6 //NumByteToWrite:要写入的字节数(最大65535)
7 u8 EN25QXX_BUFFER[4096];
8 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
9 {
10 u32 secpos;
11 u16 secoff;
12 u16 secremain;
13 u16 i;
14 u8 * EN25QXX_BUF;
15 EN25QXX_BUF=EN25QXX_BUFFER;
16 secpos=WriteAddr/4096;//扇区地址
17 secoff=WriteAddr%4096;//在扇区内的偏移
18 secremain=4096-secoff;//扇区剩余空间大小
19 //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
20 if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
21 while(1)
22 {
23 EN25QXX_Read(EN25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
24 for(i=0;i<secremain;i++)//校验数据
25 {
26 if(EN25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除
27 }
28 if(i<secremain)//需要擦除
29 {
30 EN25QXX_Erase_Sector(secpos);//擦除这个扇区
31 for(i=0;i<secremain;i++) //复制
32 {
33 EN25QXX_BUF[i+secoff]=pBuffer[i];
34 }
35 EN25QXX_Write_NoCheck(EN25QXX_BUF,secpos*4096,4096);//写入整个扇区
36
37 }else EN25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间.
38 if(NumByteToWrite==secremain)break;//写入结束了
39 else//写入未结束
40 {
41 secpos++;//扇区地址增1
42 secoff=0;//偏移位置为0
43
44 pBuffer+=secremain; //指针偏移
45 WriteAddr+=secremain;//写地址偏移
46 NumByteToWrite-=secremain; //字节数递减
47 if(NumByteToWrite>4096)secremain=4096; //下一个扇区还是写不完
48 else secremain=NumByteToWrite; //下一个扇区可以写完了
49 }
50 }
51 }
  这段代码的内容为:先发送“写使能”命令,接着才开始页写入时序,然后发送指令编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH状态寄存器,等待 FLASH 内部写入结束。
 

从 FLASH 读取数据

相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可,其指令时序见图 25-14。
 

发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。代码如下。
 1 //读取SPI FLASH
2 //在指定地址开始读取指定长度的数据
3 //pBuffer:数据存储区
4 //ReadAddr:开始读取的地址(24bit)
5 //NumByteToRead:要读取的字节数(最大65535)
6 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)
7 {
8 u16 i;
9 EN25QXX_CS=0; //使能器件
10 SPI2_ReadWriteByte(EN25X_ReadData); //发送读取命令
11 SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //发送24bit地址
12 SPI2_ReadWriteByte((u8)((ReadAddr)>>8));
13 SPI2_ReadWriteByte((u8)ReadAddr);
14 for(i=0;i<NumByteToRead;i++)
15 {
16 pBuffer[i]=SPI2_ReadWriteByte(0XFF); //循环读数
17 }
18 EN25QXX_CS=1;
19 }
由于读取的数据量没有限制,所以发送读命令后一直接收 NumByteToRead 个数据到结束即可。 

3. main 函数  

最后我们来编写 main 函数,进行 FLASH 芯片读写校验,代码如下。
 1 #include "system.h"
2 #include "SysTick.h"
3 #include "led.h"
4 #include "usart.h"
5 #include "tftlcd.h"
6 #include "key.h"
7 #include "spi.h"
8 #include "flash.h"
9
10
11 //要写入到25Q64的字符串数组
12 const u8 text_buf[]="www.prechin.net";
13 #define TEXT_LEN sizeof(text_buf)
14 //u16 key3;
15
16 int main()
17 {
18 u8 i=0;
19 u8 key;
20 u8 buf[30];
21
22 SysTick_Init(72);
23 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中断优先级分组 分2组
24 LED_Init();
25 USART1_Init(9600);
26 TFTLCD_Init(); //LCD初始化
27 KEY_Init();
28 EN25QXX_Init();
29
30 FRONT_COLOR=BLACK;
31 LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,16,"PRECHIN STM32F1");
32 LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net");
33 LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,16,"FLASH-SPI Test");
34 LCD_ShowString(10,70,tftlcd_data.width,tftlcd_data.height,16,"K_UP:Write K_DOWN:Read");
35 FRONT_COLOR=RED;
36
37 while(EN25QXX_ReadID()!=EN25Q64) //检测不到EN25Q64
38 //while(1)
39 {
40 //key3 = EN25QXX_ReadID();
41 printf("EN25Q64 Check Failed! \r\n");
42 LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Failed! ");
43 }
44 printf("EN25Q64 Check Success!\r\n");
45 LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Success!");
46
47 LCD_ShowString(10,170,tftlcd_data.width,tftlcd_data.height,16,"Write Data:");
48 LCD_ShowString(10,190,tftlcd_data.width,tftlcd_data.height,16,"Read Data :");
49
50 while(1)
51 {
52 key=KEY_Scan(0);
53 if(key==KEY_UP)
54 {
55 EN25QXX_Write((u8 *)text_buf,0,TEXT_LEN);
56 printf("发送的数据:%s\r\n",text_buf);
57 LCD_ShowString(10+11*8,170,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net");
58 }
59 if(key==KEY_DOWN)
60 {
61 EN25QXX_Read(buf,0,TEXT_LEN);
62 printf("接收的数据:%s\r\n",buf);
63 LCD_ShowString(10+11*8,190,tftlcd_data.width,tftlcd_data.height,16,buf);
64 }
65
66 i++;
67 if(i%20==0)
68 {
69 led1=!led1;
70 }
71
72 delay_ms(10);
73
74 }
75 }
注意:
由于实验板上的 FLASH 芯片默认已经存储了特定用途的数据,如擦除了这些数据会影响到某些程序的运行。所以我们预留了 FLASH 芯片的“第 0 扇区(0-4096 地址)”专用于本实验,如非必要,请勿擦除其它地址的内容。如已擦除,可在配套资料里找到“刷外部 FLASH 内容”程序,根据其说明给 FLASH 重新写入出厂内容。
 
 
 
 
 1 //无检验写SPI FLASH
2 //必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
3 //具有自动换页功能
4 //在指定地址开始写入指定长度的数据,但是要确保地址不越界!
5 //pBuffer:数据存储区
6 //WriteAddr:开始写入的地址(24bit)
7 //NumByteToWrite:要写入的字节数(最大65535)
8 //CHECK OK
9 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
10 {
11 u16 pageremain;
12 pageremain=256-WriteAddr%256; //单页剩余的字节数
13 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节
14 while(1)
15 {
16 EN25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
17 if(NumByteToWrite==pageremain)break;//写入结束了
18 else //NumByteToWrite>pageremain
19 {
20 pBuffer+=pageremain;
21 WriteAddr+=pageremain;
22
23 NumByteToWrite-=pageremain; //减去已经写入了的字节数
24 if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
25 else pageremain=NumByteToWrite; //不够256个字节了
26 }
27 }
28 }
 
 1 #ifndef _flash_H
2 #define _flash_H
3
4 #include "system.h"
5
6
7 //EN25X系列/Q系列芯片列表
8 //EN25Q80 ID 0XEF13
9 //EN25Q16 ID 0XEF14
10 //EN25Q32 ID 0XEF15
11 //EN25Q64 ID 0XEF16
12 //EN25Q128 ID 0XEF17
13 #define EN25Q80 0XEF13
14 #define EN25Q16 0XEF14
15 #define EN25Q32 0XEF15
16 //#define EN25Q64 0XEF16
17 //#define EN25Q128 0XEF17
18 //#define EN25Q64 0XC816
19 //#define EN25Q64 0X1C16 //GD25QXX
20 //#define EN25Q64 0X2016 //XM25QHXX
21 #define EN25Q64 0Xb16 //MXIC C216
22 #define EN25Q128 0XC817
23
24 extern u16 EN25QXX_TYPE; //定义EN25QXX芯片型号
25
26 #define EN25QXX_CS PGout(13) //EN25QXX的片选信号
27
28
29 //指令表
30 #define EN25X_WriteEnable 0x06
31 #define EN25X_WriteDisable 0x04
32 #define EN25X_ReadStatusReg 0x05
33 #define EN25X_WriteStatusReg 0x01
34 #define EN25X_ReadData 0x03
35 #define EN25X_FastReadData 0x0B
36 #define EN25X_FastReadDual 0x3B
37 #define EN25X_PageProgram 0x02
38 #define EN25X_BlockErase 0xD8
39 #define EN25X_SectorErase 0x20
40 #define EN25X_ChipErase 0xC7
41 #define EN25X_PowerDown 0xB9
42 #define EN25X_ReleasePowerDown 0xAB
43 #define EN25X_DeviceID 0xAB
44 #define EN25X_ManufactDeviceID 0x90
45 #define EN25X_JedecDeviceID 0x9F
46
47 void EN25QXX_Init(void);
48 u16 EN25QXX_ReadID(void); //读取FLASH ID
49 u8 EN25QXX_ReadSR(void); //读取状态寄存器
50 void EN25QXX_Write_SR(u8 sr); //写状态寄存器
51 void EN25QXX_Write_Enable(void); //写使能
52 void EN25QXX_Write_Disable(void); //写保护
53 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);
54 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead); //读取flash
55 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//写入flash
56 void EN25QXX_Erase_Chip(void); //整片擦除
57 void EN25QXX_Erase_Sector(u32 Dst_Addr); //扇区擦除
58 void EN25QXX_Wait_Busy(void); //等待空闲
59 void EN25QXX_PowerDown(void); //进入掉电模式
60 void EN25QXX_WAKEUP(void); //唤醒
61
62
63 #endif
 
 
 
 
 

(stm32学习总结)—SPI-FLASH 实验的更多相关文章

  1. stm32学习总结)—SPI-FLASH 实验 _

    SPI总线 SPI 简介 SPI 的全称是"Serial Peripheral Interface",意为串行外围接口,是Motorola 首先在其 MC68HCXX 系列处理器上 ...

  2. STM32学习笔记——SPI串行通讯(向原子哥学习)

    一.SPI  简介 SPI是 Serial Peripheral interface 的缩写,就是串行外围设备接口.SPI 接口主要应用在  EEPROM, FLASH,实时时钟,AD 转换器,还有数 ...

  3. STM32学习笔记(八) SPI总线(操作外部flash)

    1. SPI总线简介 SPI全称串行外设接口,是一种高速,全双工,同步的外设总线:它工作在主从方式,常规需要至少4根线才能够正常工作.SPI作为基本的外设接口,在FLASH,EPPROM和一些数字通讯 ...

  4. 【iCore、iCore2 双核心板】EPCS 实验(SPI Flash)(基于Verilog语言)

    _____________________________________ 深入交流QQ群: A: 204255896(1000人超级群,可加入) B: 165201798(500人超级群,满员) C ...

  5. STM32学习笔记——OLED屏

    STM32学习笔记--OLED屏 OLED屏的特点: 1.  模块有单色和双色可选,单色为纯蓝色,双色为黄蓝双色(本人选用双色): 2.  显示尺寸为0.96寸 3.  分辨率为128*64 4.   ...

  6. Jlink使用技巧之烧写SPI Flash存储芯片

    前言 大多数玩单片机的人都知道Jlink可以烧写Hex文件,作为ARM仿真调试器,但是知道能烧写SPI Flash的人应该不多,本篇文章将介绍如何使用JLink来烧写或者读取SPI Flash存储器, ...

  7. OpenRisc-32-ORPSoC烧写外部spi flash

    引言 经过前面的分析和介绍,我们对ORPSoC的启动过程(http://blog.csdn.net/rill_zhen/article/details/8855743)和 ORpSoC的debug子系 ...

  8. (电工基地笔记)Vivado固化至SPI Flash

    如果从头开始做SPI Flash固化是有一些麻烦的,要在完成综合之后,打开 synthesized Design (图) (图) 然后在synthesized Design打开状态下,选择Tools- ...

  9. Nand Flash,Nor Flash,CFI Flash,SPI Flash 之间的关系

    前言:    在嵌入式开发中,如uboot的移植,kernel的移植都需要对Flash 有基本的了解.下面细说一下标题中的中Flash中的关系 一,Flash的内存存储结构    flash按照内部存 ...

随机推荐

  1. shell脚本编程练习

    转至:http://www.178linux.com/88406 1.写一个脚本,使用ping命令探测172.16.250.1-172.16.250.254之间的所有主机的在线状态 在线的主机使用绿色 ...

  2. Python:获取某一月的天数

    import calendarcalendar.monthlen(2021,6)30calendar.monthrange(2021,6)(1, 30) calendar.monthrange( ye ...

  3. cpolar——安全的内网穿透工具

    什么是cpolar? cpolar是一种安全的内网穿透云服务,它将内网下的本地服务器通过安全隧道暴露至公网,使得公网用户可以正常访问内网服务. 它能用在哪些场景? 微信公众号开发,实时断点调试微信消息 ...

  4. VS常用的快捷键

    整理代码          Ctrl+k+f 注释                 Ctrl+k+c 取消注释          Ctrl+k+u 帮助文档          F1 无调试启动     ...

  5. 关于Web的一些知识,Web怎么构成?

    前端学习:学习地址:黑马程序员pink老师前端入门教程,零基础必看的h5(html5)+css3+移动,下面这些都是一些学习笔记.临渊羡鱼,不如退而结网!!愿我自己学有所成,也愿每个前端爱好者学有所成 ...

  6. java后端工程师学习路线

    根据自己的经历和见识梳理了一份java后端工程师的学习路线(不含安卓方向),难免有局限性和疏漏,请在评论区反馈意见和建议! 很明显的是我的学习路线过于庞大了[尴尬],你可以认为这些只是我的一家之言,具 ...

  7. phpStudy 升级 MySQL5.7

    最新在开发项目中需要使用到mysql5.7以上版本,但是phpStudy的版本是5.5,所以需要针对MySQL升级一下 步骤  1.备份原本MySQL 备份:原本phpStudy中的MySQL文件夹改 ...

  8. Linux开机自启应用&开机执行脚本&监听端口应用挂掉了执行启动脚本

    linux开机自启 背景 目前要部署一个spring boot框架的jar包,实现开机启动项目或者应用挂掉了 执行启动脚本 在root目录下有一个启动项目的脚本: app_start.sh app_s ...

  9. idea Mybatis mapper配置文件删除SQL语句背景色

    原样式: 看着很不爽 本文 idea 版本为:idea 2020.3.1,以下操作均基于此版本进行 解决办法 1.去除警告 Settings>Editor>Inspections>S ...

  10. 5月9日 python学习总结 外键、表之间的关联关系、修改表、清空表内容、复制表

    一.外键foreign key    外键约束: 1.必须先创建被关联表才能创建关联表 2.插入记录时,必须先插入被关联表的记录,才能插入关联表(要用到被关联表)的记录 3.若不设置同步更新和同步删除 ...