基于FPGA与MCU通信的SPI协议设计


typora-copy-images-to: typora_picture

基于FPGA与MCU通信的SPI协议设计

1. SPI总线协议介绍及硬件设计

1.1 SPI总线协议介绍及硬件设计

SPI总线是一种同步串行外设接口。(Serial Peripheral Interface,串行外设接口)

PIN 定义
NSS 从机选择线(低电平有效)
SCK 串行时钟线
MOSI 主机输出 / 从机输入线
MISO 主机输入 / 从机输出线

有的SPI接口芯片带有中断信号INT,有的SPI接口芯片只能作为从机使用,因而只有MISO(主机 》 从机)。

SPI特点:

  1. 可以同时发出和接收串行数据
  2. 作主机或从机
  3. 提供时钟(频率可以调整)
  4. 发送中断结束标志
  5. 保护:写冲突保护,总线竞争保护
SPI总线工作方式
SP0(使用广泛):
SP1
SP2
SP3(使用广泛):

1.2 STM8 的硬件电路设计

STM8 作为8位单片机, 具有3级流水线哈佛结构,8K存储.

STM8 优点:

  1. 支持3.3V电平, 可以与FPGA直接连接
  2. 内置16Mhz晶振
  3. 支持串行配置
  4. 最小 TSSOP20 封装, 减少硬件设计及PCB空间

本实验MCU型号: STM8S103F3P6

FPGA--------跳线连接--------MCU

STM8 核心板设计:

STM8 原理图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UpSp7vXS-1627885304808)(D:\intelFPGA\typora_picture\微信图片_20210730173530.jpg)]

STM8 引脚 对应SPI引脚
PA3 SPI_NSS
PC5 SPI_SCK
PC6 SPI_MOSI
PC7 SPI_MISO

STM8 的SPI接口关于NSS片选有2种模式:

​ 软件模式: 支持多个设备, 按需要选择软件模式, 认为输出NSS.( 为了日后对许多从机的支持, 这里选择PD2 作为 SPI_NSS )

​ 硬件模式: 只能用PA3输出, 只能支持一个设备.

为了便于在调试过程中查看, 还设计了4位 led 指示灯.

1.3 SPI总线协议时序分析

分析STM8的 4 种 SPI 时序:

​ STM8 自带SPI硬核, 调用SPI 模块非常方便, 而FPGA 中所有的逻辑电路都需要自己设计.

​ FPGA----MCU 的系统而言, 要确定一个主机一个从机, 由于C语言线程控制的优越性, 而FPGA作为并行运算, 具备硬件加速的特点.

​ 所以, 一般有MCU作为主机, FPGA作为从机.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XXQlQEl6-1627885304810)(D:\intelFPGA\typora_picture\SPI协议设计-主从SPI通信框图.png)]

下面研究 STM8 作为主机时的SPI时序图:

STM8 主函数

void main(){

unsigned char i = 0, rxd_char = 0;		//i=0; rxd_char=0;
CLK_Configuration();					//时钟配置
SPI_Init();								//SPI初始化
LED_Init();								//LED初始化

while(1){
	for(i=0; i<=0xff; i++){				//0 ~ 255循环
		rxd_char = SPI_SendByte(i);		//接收字符串 = SPI发送的字符串
		LED_Data(rxd_char);				//LED校验函数
		Delay_ms(500);					//延时500ms
	}
}

}

参数 0 1
CPOL SCK空闲时为低电平,有效时从低到高有8个时钟 SCK空闲时为高电平,有效时从高到低有8个时钟
CPHA 下降沿采样 上升沿采样

通过排列组合,有SP0,SP1,SP2,SP3 共4种方式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ThdrTP5-1627885304811)(D:\intelFPGA\typora_picture\图9 CPOL=CPHA=0的时序图.png)]

时序图分析:

(1)CS低电平有效

(2)主机MOSI在 CS有效后开始输出。从机在CLK上升沿稳态捕获

(3)MISO在 CS 有效后,在SCK 上升沿稳态捕获。

2. SPI总线协议的通信实现

2.1 STM8 的SPI总线收发设计

  1. SPI 接口的初始化
/*****************SPI初始化****************/
#define SPI_CS_LOW()	PD_ODR_ODR2 = 0
#define SPI_CS_HIGH()	PD_ODR_ODR2 = 1

void SPI_Init(void){
    SPI_CR1 = (0<<7) | (0<<6) | (0<<3) | (1<<2) | (1<<1) | (1<<0);	//MSB在前,SPI数据发送频率为8MHz
    SPI_CR2 = (0<<7) | (0<<5) | (0<<4) | (0<<2) | (1<<1) | (1<<0);	//全双工, 使能软件主模式
    SPI_ICR = (0<<7) | (0<<6) | (0<<5) | (0<<4);	//禁止所有中断
    SPI_CR1 |= (1<<6);							  //开启SPI模块
    
    PD_DDR_DDR2 = 1;	//PD2 设置为输出引脚-----输出数据
    PD_CR1_C12 = 1;		//设置推挽输出
    PD_CR1_C12 = 1;		//设置输出频率 1 为10Mhz
    
    SPI_CS_HIGH();		//CS信号高位( 关闭 slave )
}
  1. SPI 收发函数的实现
/*****************SPI 发送,接收函数****************/
unsigned char SPI_SendByte(unsigned char byte){
    SPI_CS_LOW();		//CS信号置0,开始
    while( !(SPI_SR & 0x02) );	//等待 发送寄存器 为空
    SPI_DR = byte;			   //将发送的数据写到 ---> 数据寄存器
    while( !(SPI_SR & 0x01) );	//等待 接收寄存器 满
    
    SPI_CS_HIGH();		//CS 信号置1,结束
    
    return SPI_DR;		//读 数据寄存器
}
  1. STM8 主函数
/******************STM8 主函数******************/
void main(){
    unsigned char i=0, rxd_char=0;
    CLK_Configuration();					 //时钟配置
	SPI_Init();								//SPI初始化
	LED_Init();								//LED初始化

    while(1){
        for(i=0; i<=0xff; i++){				//0 ~ 255循环
            rxd_char = SPI_SendByte(i);		 //接收字符串 = SPI发送的字符串
            LED_Data(rxd_char);				//LED校验函数
            Delay_ms(500);					//延时500ms
	}
}

2.2 边沿检测电路的FPGA实现

  1. 边沿检测电路: 检测输入信号 或 PFGA内部逻辑信号的跳变 ( 实现上升沿或下降沿的检测, 捕获得到边沿使能, 来作为时序逻辑的触发信号 )

通过边沿检测实现使能时钟, 提高FPGA时序逻辑稳定性.

  1. 边沿检测电路的实现:
Trigger_r Trigger 边沿检测
上升沿
下降沿

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqrmW48t-1627885304814)(D:\intelFPGA\typora_picture\一级寄存实现的----捕获电路.png)]

一级的 D触发器寄存 在比较时, 当前时刻从外部输入的信号, 与前一时刻的信号不在同一时钟域( FPGA整体逻辑电路的 时钟域 )

二级D触发器作为信号的寄存 ----- 同步时钟设计, 以提高系统可靠性.

  1. 边沿检测电路的Verilog HDL 实现电路
//---------------------------------------------
// MCU ----(data sync)----> FPGA
reg	spi_sck_r0, spi_sck_rl;			//spi dafault 1;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
    	spi_sck_r0 <= 0;
        spi_sck_r1 <= 0;			//data transfer clock
    	end
    else begin 
    	spi_sck_r0 <= spi_sck;
        spi_sck_r1 <= spi_sck_r0;
    	end
    end
wire mcu_read_flag = ( ~spi_sck_r1 & spi_sck_r0 )? 1'b1 : 1'b0;		//posedge of sck

上一级信号是 : 2级D触发器输出的信号 ----- spi_sck_r1

当前信号是 : D触发器输出的信号 ----- spi_sck_r0

通过两个信号的对比, 可检测出 spi_sck 上升沿 还是 下降沿, 并且输出-------读取使能信号 mcu_read_flag

总结 : 边沿检测, 使能输出.

2.3 SPI通信的数据接收模块设计

  1. spi 数据接收模块的verilog HDL 实现

新建 : 06_MCU2FPGA_SPI_Test 文件夹

建立 : MCU2FPGA_SPI_Test 工程, 移植全局时钟管理模块( system_index ), LED显示模块( led_index 相关 )

(1) 新建 spi_receiver.v, 信号列表如下

`timescale 1ns / 1ns
//-------------------------------------------//
module spi_receiver(
	//global clock
    input	clk,		//时钟
    input	rst_n,		//复位
    
    //mcu spi interface
    input	spi_cs,		//片选
    input	spi_sck,	//数据传输时钟
    input	spi_mosi,
    input	spi_miso,
    
    //user interface
    output	reg			rxd_flag,	//外部输出的rxd_data 的外部使能信号
    output	reg	[7:0]	rxd_data	//mcu发送的 串转并 后的 spi数据
);

(2) spi总线接口的--------数据同步模块设计

SPI的数据由MCU输入,与FPGA不在同一个时钟域,同时MCU输入的数据相对于FPGA而言速率很低

采用 2级D触发器寄存输出同步。

代码:

//-------------------------------------------//
//MCU -----> FPGA
reg	spi_cs_r0,	spi_cs_r1;
reg	spi_sck_r0,	spi_sck_r1;
reg	spi_mosi_r0,	spi_mosi_r1;
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin 
        spi_cs_r0 <= 1,		spi_cs_r1 <= 1;			//片选使能
        spi_sck_r0 <= 0,	spi_sck_r1 <= 0;		//数据传输时钟
        spi_mosi_r0 <= 0,	spi_mosi_r1 <= 0;		//mosi
    	end
    else begin
    	spi_cs_r0 <= spi_cs,	spi_cs_r1 <= spi_cs_r0;			//片选使能
        spi_sck_r0 <= spi_sck,	 spi_sck_r1 <= spi_sck_r0;		 //数据传输时钟
        spi_mosi_r0 <= spi_mosi,	spi_mosi_r1 <= spi_mosi_r0;	 //mosi
    	end
    end

wire	mcu_cs = spi_cs_r1;
wire	mcu_data = spi_mosi_r1;
wire	mcu_read_flag = (~spi_sck_r1 & spi_sck_r0) ? 1'b1 : 1'b0;	//sck上升沿
wire	mcu_read_done = (~spi_cs_r1 & spi_cs_r0) ? 1'b1 : 1'b0;		//cs上升沿

wire 输出的4个信号说明如表:

信号 说明
mcu_cs 2级D触发器同步后的 spi_cs 信号
mcu_data 2级D触发器同步后的 spi_mosi 信号
mcu_read_flag spi_sck同步边沿检测后的上升沿使能信号
mcu_read_done 8位spi数据传输完毕信号

(3) 8位spi数据接收

spi通信协议非常简单,数据的接口可以不用状态机来实现。

设计中主要根据 mcu_read_flag 信号来捕获 SPI 数据,

verilog HDL 代码如下:

//------------------------------------
//sample signal, receive data

reg [3:0] rxd_cnt;
reg [7:0] rxd_data_r;
always @(posedge clk or posedge rst_n) begin
	if (!rst) begin
		// reset
		rxd_cnt 	<= 0;
		rxd_data_r 	<= 0;
	end
    //-------------
	else if (mcu_cs == 1'b0) begin
		if(mcu_read_flag) begin         					//sck上升沿
			rxd_data_r[[3'd7 - rxd_cnt[2:0]] <= mcu_data;
			rxd_cnt <= rxd_cnt + 1'b1;						//计数器加1,0-7-8
		end
		else begin
			rxd_cnt 	<= rxd_cnt;
			rxd_data_r 	<= rxd_data_r;
		end	
	end
    //-------------                   
	else begin
		rxd_cnt 	<= 0;
		rxd_data_r 	<= rxd_data_r; 
	end
end

设计电路时,首先判断 mcu_cs 是否有效,当 mcu_cs 无效,即 mcu_cs = 1, FPGA挂起,

否则根据使能信号 mcu_read_flag 来捕获 8位数据。8位数据的捕获根据 rxt_cnt 的计数来实现。

当mcu_read_flag 无效时,数据保持不变。

(4) 捕获完成后使能信号的输出

如 矩阵键盘检测电路 一样,在捕获到外部数据后, 为了便于后端模块的响应,人为输出一个读取完成后的触发信号,作为rxd_data 读取的激励。

当 mcu_cs 从有效到无效时,即完成一次 SPI 数据通信( 捕获到 mcu_read_down 有效 )。

//------------------------------------
//output_spi_receive_data_and_receive_flag

always @(posedge clk or negedge rst_n) begin
	if (!rst) begin
		// reset
		rxd_flag 	<= 0;
		rxd_data 	<= 0;
	end
    else if (mcu_read_done) begin      //mcu_read_done有效时读取 串转并 后的数据rxd_data_r   	
			rxd_flag <= 1'b1;		//同步输出了并行数据捕获的 rxd_flag, 这2个信号同步输出,作为外部信号的激励。
			rxd_data <= rxd_data_r;		
	end
	else begin
		rxd_flag <= 0;
		rxd_data <= rxd_data;	
	end	
end

  1. modelsim仿真(未完待续。。。)

2.4 SPI通信的数据发送模块设计

上一篇:eclipse中创建的spring-boot项目在启动时指定加载那一个配置文件的设置


下一篇:FPGA 双向口的使用及Verilog实现