文章目录
- 一、SPI协议简介
- 1.1 SPI引脚
- 1.2 时钟极性和时钟相位
- 1.3 主从模式
- 二、Flash(M25P16)
- 2.1 Flash简介
- 2.2 M25P16芯片分析
- 2.3 项目所用指令时序
- 2.3.1 WREN(06h)
- 2.3.2 RDID(9Fh)
- 2.3.3 READ(03h)
- 2.3.4 PP(02h)
- 2.3.5 SE(D8h)
- 三、状态机
- 四、项目源码
本项目所用FPGA芯片型号为:EP4CE6F17C8
厂家:Altera
Flash芯片型号:M25P16
厂家:海力士
一、SPI协议简介
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是美国摩托罗拉公司(Motorola)最先推出的一种同步串行传输规范,也是一种单片机外设芯片串行扩展接口,是一种高速、全双工、同步通信总线,所以可以在同一时间发送和接收数据,SPI是一种事实标准因此并没有定义速度限制,本次项目所用的Flash芯片支持的最高速率达到了50Mb/s(页编程,扇区擦除等操作)。同时SPI协议支持一主多从,采用片选信号选择从机,具体的主从机制会在后文介绍。
SPI 设备间的数据传输之所以又被称为数据交换,是因为 SPI 协议规定一个 SPI 设备不能在数据通信过程中仅仅只充当一个 “发送者(Transmitter)” 或者 “接收者(Receiver)”。在每个 Clock 周期内,SPI 设备都会发送并接收一个 bit 大小的数据(不管主设备好还是从设备),相当于该设备有一个 bit 大小的数据被交换了。一个 Slave 设备要想能够接收到 Master 发过来的控制信号,必须在此之前能够被 Master 设备进行访问 (Access)。所以,Master 设备必须首先通过 SS/CS pin 对 Slave 设备进行片选, 把想要访问的 Slave 设备选上。 在数据传输的过程中,每次接收到的数据必须在下一次数据传输之前被采样。如果之前接收到的数据没有被读取,那么这些已经接收完成的数据将有可能会被丢弃,导致 SPI 物理模块最终失效。因此,在程序中一般都会在 SPI 传输完数据后,去读取 SPI 设备里的数据, 即使这些数据(Dummy Data)在我们的程序里是无用的(虽然发送后紧接着的读取是无意义的,但仍然需要从寄存器中读出来)。这一点后续会在代码的滤除无效数据以及发送指令后发送无效数据占用CS片选信号线中体现出来。
1.1 SPI引脚
SPI协议共有四根线,分别为:SCLK(时钟线)、MISO(主机输入,从机输出)、MOSI(主机输出、从机输入)、CS(片选信号)。
- SCLK:主要的作用是 Master(主)设备往 Slave(从)设备传输时钟信号, 控制数据交换的时机以及速率
- CS:用于 Master(主)设备片选 Slave (从)设备,使被选中的 Slave(从)设备能够被 Master(主)设备所访问
- MISO:在 Master(主)上面也被称为 Rx-Channel,作为数据的入口,主要用于SPI 设备接收数据
- MOSI:在 Master(主)上面也被称为 Tx-Channel,作为数据的出口,主要用于 SPI 设备发送数据
1.2 时钟极性和时钟相位
时钟极性(CPOL):
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据。
CPOL可以配置为1或0。这意味着你可以根据需要将时钟的默认状态(IDLE)设置为高或低。
- CKP = 0:时钟空闲IDLE为低电平 0
- CKP = 1:时钟空闲IDLE为高电平1
时钟相位(CPHA):
根据硬件制造商的不同,时钟相位通常写为CKE或CPHA。顾名思义,时钟相位/边沿,也就是采集数据时是在时钟信号的具体相位或者边沿。
- CPHA = 0:在时钟信号SCK的第一个跳变沿采样
- CPHA = 1:在时钟信号SCK的第二个跳变沿采样
从以上介绍我们可以看出,SPI一共有四种传输模式,具体如下:
-
CPOL=0,CPHA=0:此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿。
-
CPOL=0,CPHA=1:此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿。
-
CPOL=1,CPHA=0:此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿。
-
CPOL=1,CPHA=1:此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿。
1.3 主从模式
SPI协议通过主机主动拉低拉低CS片选信号选择从机,也正因如此分出如下两种情况:
多根CS片选信号线:
通常,每个从机都需要一条单独的CS线。
如果要和特定的从机进行通讯,可以将相应的CS信号线拉低,并保持其他CS信号线的状态为高电平;如果同时将两个CSS信号线拉低,则可能会出现乱码,因为从机可能都试图在同一条MISO线上传输数据,最终导致接收数据乱码。
具体情况如下图(NSS即片选信号线CS):
菊花链(只有一根CS片选信号线):
在数字通信世界中,在设备信号(总线信号或中断信号)以串行的方式从一 个设备依次传到下一个设备,不断循环直到数据到达目标设备的方式被称为菊花链。
菊花链的最大缺点是因为是信号串行传输,所以一旦数据链路中的某设备发生故障的时候,它下面优先级较低的设备就不可能得到服务了。
另一方面,距离主机越远的从机,获得服务的优先级越低,所以需要安排好从机的优先级,并且设置总线检测器,如果某个从机超时,则对该从机进行短路,防止单个从机损坏造成整个链路崩溃的情况。
具体情况如下图:
- SCK为时钟信号,8clks表示8个边沿信号
- 其中D为数据,X为无效数据
参考资料:https://blog.csdn.net/u010632165/article/details/109460814
二、Flash(M25P16)
2.1 Flash简介
Flash 存储器(FLASH EEPROM)又称闪存,快闪。它是EEPROM的一种。它结合了ROM和RAM的长处。不仅具备电可擦除可编程(EEPROM)的特点,还不会断电丢失数据同时可以快速读取数据。Flash和EEPROM之间的主要区别在于擦除方式和工作原理。传统的EEPROM可以按字节或页进行擦除和编程,而Flash通常以扇区为单位进行擦除。此外,Flash的擦写寿命较短,需要注意控制擦写次数,而EEPROM没有这个限制。
2.2 M25P16芯片分析
该芯片的特性总结如上图:
- 16Mbit存储空间(32个扇区 每个扇区256页 每页256字节)
- 支持SPI接口
- 数据可保存20年
由上图可以看出该芯片仅支持SPI模式0或模式3,同时该芯片传输方式为MSB,高字节先发。
由上图可以看出:
- 在发出页编程指令前需要先发送写使能(WREN)指令
- 页编程只能将1重置为0,因此在页编程之前需要先发送一次扇区擦除指令,将存储数据全部置为1
- 同时在后续编程中,在发出扇区擦除指令前同样需要发送一次写使能指令
- 该芯片有一个状态寄存器,我们可以通过读取状态寄存器的WIP位获取芯片是否处于忙状态,WEL位获取芯片是否处于写锁存。通过读状态寄存器我们可以在后续页编程和扇区擦除时不必强行等待手册所要求的操作最大时间。为了简写代码,本人未用到读状态寄存器的命令,而是在SE和PP指令发送后强行等待了手册所规定的最大等待时间。状态寄存器格式如下图:
芯片相关指令如下图:
可以看出 页编程(PP)、扇区擦除(SE)、读(READ)指令均需要三个字节的地址数据(扇区地址+页地址+数据地址)
一些重要时间:
- 由上图可以看出M25P16芯片要求片选信号要求在数据到来之前提前拉低至少5ns,同时在数据传输结束后至少继续拉低100ns才能被拉高
- 页编程指令发送后等待5ms
- 扇区擦除指令发送后等待3s
时钟频率选择
由上图可以看出,除了读数据指令时钟最大速度为20MHz外其余指令均能达到50MHz,为了便于代码编写,本次项目在开发板50MHz的基础上进行了四分频,12.5MHz的时钟能够满足所用的所有指令。
2.3 项目所用指令时序
2.3.1 WREN(06h)
在执行PP,SE,BE,RESR指令前需要执行写使能操作。片选信号拉低后,开始执行写使能指令,接着传输一字节指令。指令发送完后,片选信号置为高电平.
- 发送1字节指令
2.3.2 RDID(9Fh)
读ID指令一共会返回三个字节的数据,第一个数据为厂商ID(如下图20h代表的是意法半导体的厂商ID),后两个数据可以理解为器件ID。
- 发送1字节指令
- 接收3字节数据
2.3.3 READ(03h)
- 发送1字节的指令
- 发送3字节的地址
- 接收1字节的数据
2.3.4 PP(02h)
2.3.5 SE(D8h)
三、状态机
接口模块:
SPI接口模块实际就是切割SPI时序图拆解出不同的状态,再在不同的状态中实现SPI时序,以写使能时序图为例:
我们可以拆分出四个状态:CS片选信号在数据传输前需要提前拉低的一段时间DELAY1(最少tSHSH 5ns),发送指令状态DATA,指令发送结束后片选信号延迟一段时间DELAY2,以及手册要求的片选恢复tSHSL(100ns)恢复状态HOLD。
控制模块:
该模块思路可以天马行空,博主就不给出状态机了,可以参考博主后续源码。
四、项目源码
控制模块:
/**************************************功能介绍***********************************
Date :
Author : majiko
Version :
Description:
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module spi_control(
input wire clk ,
input wire rst_n ,
input wire read_req,
input wire rdid_req,
input wire wen_req ,
input wire ready ,
input wire [23:0] read_addr, //需要读的数据地址
input wire [7:0] read_num, //需要读出的数据0~255
input wire [23:0] wen_adder,//需要写入的数据地址
input wire [7:0] wen_num , //需要写入的数据数量
input wire [7:0] data_in, //需要写入的数据
input wire data_in_vld,
output wire [7:0] data_out, //返回的数据
output wire data_out_vld,
//spi接口
input wire miso ,
output wire cs_n ,
output wire sclk ,
output wire mosi //主机输出给从机
);
//---------<参数定义>---------------------------------------------------------
localparam IDLE = 10'b0000000001,
RDID = 10'b0000000010,
READ = 10'b0000000100,
WEN = 10'b0000001000,
WAIT = 10'b0000010000,
SE = 10'b0000100000,
TSE = 10'b0001000000,
PP = 10'b0010000000,
TPP = 10'b0100000000,
HOLD = 10'b1000000000;
parameter MAX_TXE = 28'd150_000_000,//3s
MAX_TPP = 18'd150_000;//3ms
//---------<内部信号定义>-----------------------------------------------------
reg [9:0] cstate ;//现态
reg [9:0] nstate ;//次态
wire idle2rdid;
wire idle2read;
wire idle2wen ;
wire rdid2hold;
wire read2hold;
wire wen2wait;
wire wait2se ;
wire se2tse ;
wire tse2wen ;
wire wait2pp ;
wire pp2tpp ;
wire tpp2idle ;
wire hold2idle;
reg [2:0] cnt_bit ;
wire add_cnt_bit ;
wire end_cnt_bit ;
reg [3:0] cnt_byte ;
wire add_cnt_byte ;
wire end_cnt_byte ;
reg [3:0] num ;
//子模块例化参数
reg [7:0] master_data_in ;
wire master_data_in_vld;
wire done ;
wire [7:0] master_data_out ;
wire master_data_out_vld;
reg [23:0] id_data ;
wire master_ready ;
//数据接收
reg rdid_flag;
wire [7:0] read_data;
wire read_data_vld;
reg read_flag;
reg [2:0] cnt_read ;
wire add_cnt_read ;
wire end_cnt_read ;
//tse延时
reg [27:0] cnt_tse ;
wire add_cnt_tse ;
wire end_cnt_tse ;
//tpp延时
reg [17:0] cnt_tpp ;
wire add_cnt_tpp ;
wire end_cnt_tpp ;
//wen跳转使能
reg wen_flag;
//数据寄存
reg data_in_r;
//****************************************************************
// 状态机
//****************************************************************
//第一段:时序逻辑描述状态转移
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cstate <= IDLE;
end
else begin
cstate <= nstate;
end
end
//第二段:组合逻辑描述状态转移规律和状态转移条件
always @(*) begin
case(cstate)
IDLE : begin
if (idle2rdid) begin
nstate = RDID;
end
else if (idle2read) begin
nstate = READ;
end
else if (idle2wen) begin
nstate = WEN;
end
else begin
nstate = cstate;
end
end
RDID : begin
if (rdid2hold) begin
nstate = HOLD;
end
else begin
nstate = cstate;
end
end
READ : begin
if (read2hold) begin
nstate = HOLD;
end
else begin
nstate = cstate;
end
end
WEN : begin
if (wen2wait) begin
nstate = WAIT;
end
else begin
nstate = cstate;
end
end
WAIT : begin
if (wait2se) begin
nstate = SE;
end
else if (wait2pp) begin
nstate = PP;
end
else begin
nstate = cstate;
end
end
SE : begin
if (se2tse) begin
nstate = TSE;
end
else begin
nstate = cstate;
end
end
TSE : begin
if (tse2wen) begin
nstate = WEN;
end
else begin
nstate = cstate;
end
end
PP : begin
if (pp2tpp) begin
nstate = TPP;
end
else begin
nstate = cstate;
end
end
TPP : begin
if (tpp2idle) begin
nstate = IDLE;
end
else begin
nstate = cstate;
end
end
HOLD : begin
if (hold2idle) begin
nstate = IDLE;
end
else begin
nstate = cstate;
end
end
default : nstate = IDLE;
endcase
end
assign idle2rdid = cstate == IDLE && rdid_req;
assign idle2read = cstate == IDLE && read_req;
assign idle2wen = cstate == IDLE && wen_req ;
assign rdid2hold = cstate == RDID && end_cnt_byte;
assign read2hold = cstate == READ && end_cnt_byte;
assign wen2wait = cstate == WEN && end_cnt_byte;
assign wait2se = cstate == WAIT && done && ~wen_flag;
assign se2tse = cstate == SE && end_cnt_byte;
assign tse2wen = cstate == TSE && end_cnt_tse;
assign wait2pp = cstate == WAIT && done && wen_flag;
assign pp2tpp = cstate == PP && end_cnt_byte;
assign tpp2idle = cstate == TPP && end_cnt_tpp;
assign hold2idle = cstate == HOLD && done;
//第三段:描述输出,时序逻辑或组合逻辑皆可
//****************************************************************
// byte计数器
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 3'd0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 3'd0;
end
else begin
cnt_bit <= cnt_bit + 1'b1;
end
end
end
assign add_cnt_bit = ((cstate == RDID)||(cstate == READ)||(cstate == WEN)||(cstate == SE)||(cstate == PP)) && master_ready;
assign end_cnt_bit = add_cnt_bit && cnt_bit == 7;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_byte <= 4'd0;
end
else if(add_cnt_byte)begin
if(end_cnt_byte)begin
cnt_byte <= 4'd0;
end
else begin
cnt_byte <= cnt_byte + 1'b1;
end
end
end
assign add_cnt_byte = end_cnt_bit;
assign end_cnt_byte = add_cnt_byte && cnt_byte == num-1;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
num <= 1;
end
else if (cstate == RDID) begin
num <= 4;
end
else if (cstate == READ) begin
num <= 4 + read_num;
end
else if (cstate == WEN) begin
num <= 1;
end
else if (cstate == SE) begin
num <= 4;
end
else if (cstate == PP) begin
num <= 5;
end
else begin
num <= 1;
end
end
//****************************************************************
// 指令发送
//****************************************************************
always @(*) begin
case (cstate)
RDID : case (cnt_byte)
0 : master_data_in = 8'h9f;
1 : master_data_in = 8'hff;
2 : master_data_in = 8'hff;
3 : master_data_in = 8'hff;
default: master_data_in = 8'hff;
endcase
READ : case (cnt_byte)
0 : master_data_in = 8'h03;
1 : master_data_in = read_addr[23:16];
2 : master_data_in = read_addr[15:8];
3 : master_data_in = read_addr[7:0];
default: master_data_in = 8'hff;
endcase
WEN : case (cnt_byte)
0 : master_data_in = 8'h06;
default: master_data_in = 8'hff;
endcase
SE : case (cnt_byte)
0 : master_data_in = 8'hd8;
1 : master_data_in = read_addr[23:16];
2 : master_data_in = read_addr[15:8];
3 : master_data_in = read_addr[7:0];
default: master_data_in = 8'hff;
endcase
PP : case (cnt_byte)
0 : master_data_in = 8'h02;
1 : master_data_in = read_addr[23:16];
2 : master_data_in = read_addr[15:8];
3 : master_data_in = read_addr[7:0];
4 : master_data_in = 8'hF1;
default: master_data_in = 8'hff;
endcase
endcase
end
assign master_data_in_vld = add_cnt_byte;
//****************************************************************
// 数据寄存
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_in_r <= 0;
end
else if (data_in_vld) begin
data_in_r <= data_in;
end
else begin
data_in_r <= data_in_r;
end
end
//****************************************************************
// 滤除无用数据
//****************************************************************
reg [3:0] cnt_out ;
wire add_cnt_out ;
wire end_cnt_out ;
reg [3:0] num_out ;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_out <= 'd0;
end
else if (hold2idle || tpp2idle) begin
cnt_out <= 'd0;
end
else if(add_cnt_out)begin
if(end_cnt_out)begin
cnt_out <= 'd0;
end
else begin
cnt_out <= cnt_out + 1'b1;
end
end
end
assign add_cnt_out = master_data_out_vld;
assign end_cnt_out = add_cnt_out && cnt_out == num_out - 1;
always @(*) begin
if (!rst_n) begin
num_out = 0;
end
else if (cstate == RDID) begin
num_out = 1;
end
else if (cstate == READ) begin
num_out = 4;
end
end
//****************************************************************
// 记录操作
//****************************************************************
reg [1:0] op_flag;
`define OP_RDID 1
`define OP_READ 2
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
op_flag <= 0;
end
else if (idle2rdid) begin
op_flag <= `OP_RDID;
end
else if (idle2read) begin
op_flag <= `OP_READ;
end
else if (hold2idle) begin
op_flag <= 0;
end
end
//****************************************************************
// 记录数据
//****************************************************************
//读id使能
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rdid_flag <= 0;
end
else if (end_cnt_out && op_flag == `OP_RDID) begin
rdid_flag <= 1;
end
else if (hold2idle) begin
rdid_flag <= 0;
end
end
//读read使能
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
read_flag <= 0;
end
else if (end_cnt_out && op_flag == `OP_READ) begin
read_flag <= 1;
end
else if (hold2idle) begin
read_flag <= 0;
end
end
assign data_out = master_data_out;
assign data_out_vld = (rdid_flag || read_flag) && master_data_out_vld;
//****************************************************************
// WEN跳转使能
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wen_flag <= 0;
end
else if (tse2wen) begin
wen_flag <= 1;
end
else if (tpp2idle) begin
wen_flag <= 0;
end
end
//****************************************************************
// 延时开始使能
//****************************************************************
reg delay_flag;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
delay_flag <= 0;
end
else if ((cstate == TSE) && done) begin
delay_flag <= 1;
end
else if ((cstate == TPP) && done) begin
delay_flag <= 1;
end
else if (end_cnt_tpp || end_cnt_tse) begin
delay_flag <= 0;
end
end
//****************************************************************
// TSE延时
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_tse <= 'd0;
end
else if(add_cnt_tse)begin
if(end_cnt_tse)begin
cnt_tse <= 'd0;
end
else begin
cnt_tse <= cnt_tse + 1'b1;
end
end
end
assign add_cnt_tse = (cstate == TSE) && delay_flag;
assign end_cnt_tse = add_cnt_tse && cnt_tse == MAX_TXE - 1;
//****************************************************************
// TPP延时
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_tpp <= 'd0;
end
else if(add_cnt_tpp)begin
if(end_cnt_tpp)begin
cnt_tpp <= 'd0;
end
else begin
cnt_tpp <= cnt_tpp + 1'b1;
end
end
end
assign add_cnt_tpp = (cstate == TPP) && delay_flag;
assign end_cnt_tpp = add_cnt_tpp && cnt_tpp == MAX_TPP - 1;
//****************************************************************
// 子模块例化
//****************************************************************
spi_master spi_master_inst(
/*input wire */ .clk (clk),
/*input wire */ .rst_n (rst_n),
/*input wire [7:0] */ .data_in (master_data_in),
/*input wire */ .data_in_vld (master_data_in_vld),
/*input wire */ .miso (miso),
/*output wire */ .master_ready (master_ready),
/*output reg [7:0] */ .data_out (master_data_out),
/*output wire */ .data_out_vld (master_data_out_vld),
/*output wire */ .done (done),
/*output reg */ .cs_n (cs_n),
/*output reg */ .sclk (sclk),
/*output reg */ .mosi (mosi)//主机输出给从机
);
endmodule
接口模块:
/**************************************功能介绍***********************************
Date :
Author : majiko
Version :
Description:
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module spi_master(
input wire clk ,
input wire rst_n ,
input wire [7:0] data_in ,
input wire data_in_vld,
input wire miso ,
output wire master_ready,
output reg [7:0] data_out,
output wire data_out_vld,
output wire done ,
output reg cs_n ,
output reg sclk ,
output reg mosi //主机输出给从机
);
//---------<参数定义>---------------------------------------------------------
parameter DIV = 4;//时钟分频倍数
parameter MAX100 = 5;//100ns
localparam IDLE = 5'b00001,//
DELAY_ONE = 5'b00010,//
DATA = 5'b00100,//
DELAY_TWO = 5'b01000,//
HOLD = 5'B10000;
//---------<内部信号定义>-----------------------------------------------------
//fifo数据缓存
wire fifo_empty;
wire fifo_full;
wire [7:0] fifo_data;
wire fifo_rd_req;
//时钟分频
reg [1:0] cnt_div ;
wire add_cnt_div ;
wire end_cnt_div ;
//状态机参数定义
reg [4:0] cstate ;//现态
reg [4:0] nstate ;//次态
wire idle2delay_one;
wire delay_one2data;
wire data2delay_two;
wire delay_two2hold;
wire hold2idle ;
//bit计数器参数
reg [2:0] cnt_bit ;
wire add_cnt_bit ;
wire end_cnt_bit ;
//延时计数器参数
reg [2:0] cnt_delay ;
wire add_cnt_delay ;
wire end_cnt_delay ;
//****************************************************************
// 寄存数据缓存
//****************************************************************
fifo_rx fifo_rx_inst (
.aclr ( ~rst_n ),
.clock ( clk ),
.data ( data_in ),
.q ( fifo_data ),
.rdreq ( fifo_rd_req ),
.wrreq ( data_in_vld ),
.empty ( fifo_empty ),
.full ( fifo_full )
);
// assign fifo_rd_req = !fifo_empty && data2delay_two;//因为采用了浅显模式,如果使用delay_one2data会导致第一个数据丢失
//提前一点读取新数据,让fifo的empty信号早点更新
//(根据仿真调整的,仿真体现出来的结果是,fifo的empty信号晚了一个时钟周期,导致在DATA状态多待了一个时钟周期1
//从而导致SCLK信号有毛刺,以及div计数器未归0)
assign fifo_rd_req = !fifo_empty && (cnt_bit == 7) &&(cnt_div == DIV - 2) ;//为了使fifo连续读取数据
//****************************************************************
// 状态机
//****************************************************************
//第一段:时序逻辑描述状态转移
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cstate <= IDLE;
end
else begin
cstate <= nstate;
end
end
//第二段:组合逻辑描述状态转移规律和状态转移条件
always @(*) begin
case(cstate)
IDLE : begin
if (idle2delay_one) begin
nstate = DELAY_ONE;
end
else begin
nstate = cstate;
end
end
DELAY_ONE : begin
if (delay_one2data) begin
nstate = DATA;
end
else begin
nstate = cstate;
end
end
DATA : begin
if (data2delay_two) begin
nstate = DELAY_TWO;
end
else begin
nstate = cstate;
end
end
DELAY_TWO : begin
if (delay_two2hold) begin
nstate = HOLD;
end
else begin
nstate = cstate;
end
end
HOLD : begin
if (hold2idle) begin
nstate = IDLE;
end
else begin
nstate = cstate;
end
end
default : nstate = IDLE;
endcase
end
assign idle2delay_one = cstate == IDLE && !fifo_empty;
assign delay_one2data = cstate == DELAY_ONE && 1;
assign data2delay_two = cstate == DATA && fifo_empty;
assign delay_two2hold = cstate == DELAY_TWO && 1;
assign hold2idle = cstate == HOLD && end_cnt_delay;
//第三段:描述输出,时序逻辑或组合逻辑皆可
//****************************************************************
// 时钟分频
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_div <= 'd0;
end
else if(add_cnt_div)begin
if(end_cnt_div)begin
cnt_div <= 'd0;
end
else begin
cnt_div <= cnt_div + 1'b1;
end
end
end
assign add_cnt_div = cstate == DATA;
assign end_cnt_div = add_cnt_div && cnt_div == DIV - 1;
//产生时钟sclk
always @(*) begin
if (!rst_n) begin
sclk = 1;
end
else if (cstate == DATA && (cnt_div < (DIV >> 1))) begin
sclk = 0;
end
else begin
sclk = 1;
end
end
//****************************************************************
// bit计数器
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_bit <= 'd0;
end
else if(add_cnt_bit)begin
if(end_cnt_bit)begin
cnt_bit <= 'd0;
end
else begin
cnt_bit <= cnt_bit + 1'b1;
end
end
end
assign add_cnt_bit = end_cnt_div;
assign end_cnt_bit = add_cnt_bit && cnt_bit == 7;
//****************************************************************
// mosi数据传输
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mosi <= 1;
end
else if (cstate == DATA && (cnt_div == 0)) begin
mosi <= fifo_data[7-cnt_bit];
end
else begin
mosi <= mosi;
end
end
//****************************************************************
// csn产生
//****************************************************************
always @(*) begin
if (!rst_n) begin
cs_n = 1;
end
else if (cstate == IDLE || cstate == HOLD) begin
cs_n = 1;
end
else begin
cs_n = 0;
end
end
//****************************************************************
// 保持延迟
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_delay <= 'd0;
end
else if(add_cnt_delay)begin
if(end_cnt_delay)begin
cnt_delay <= 'd0;
end
else begin
cnt_delay <= cnt_delay + 1'b1;
end
end
end
assign add_cnt_delay = cstate == HOLD;
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX100 - 1;
//****************************************************************
// 数据接收 MISO
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_out <= 0;
end
else if (cstate == DATA && (cnt_div == DIV >> 1)) begin
data_out <= {data_out[6:0],miso};
end
end
assign data_out_vld = end_cnt_bit;
assign done = hold2idle;
assign master_ready = ~fifo_full;
endmodule
串口输出模块:
/**************************************功能介绍***********************************
Date :
Author : majiko
Version :
Description: 串口发送模块
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module uart_tx#
(
parameter BPS = 115200,
parameter CLK_FRE = 50_000_000,
parameter CHECK_BIT = "NONE"//NONE 不校验 DDO奇数 EVEN偶数
)
(
input wire clk ,
input wire rst_n ,
input wire tx_data_vld,//数据发送标志
input wire [7:0] tx_data ,//发送数据
output reg tx ,
output wire ready //准备好发送
);
//---------<参数定义>---------------------------------------------------------
//状态机参数定义
localparam IDLE = 5'b00001,//空闲状态
START = 5'b00010,//发送开始位
DATA = 5'b00100,//发送8bit数据
CHECK = 5'b01000,//发送校验位
STOP = 5'b10000;//发送结束位
parameter MAX_1bit = CLK_FRE/BPS;//传输1bit数据所需时间
//---------<内部信号定义>-----------------------------------------------------
//状态机参数定义
reg [4:0] cstate ;//现态
reg [4:0] nstate ;//次态
wire idle2start;
wire start2data;
wire data2check ;
wire data2stop;
wire check2stop;
wire stop2idle ;
wire temp;//奇偶校验位
//计数器参数定义
reg [3:0] num ;
reg [8:0] cnt_start ;
wire add_cnt_start ;
wire end_cnt_start ;
reg [11:0] cnt_data ;
wire add_cnt_data ;
wire end_cnt_data ;
reg [2:0] cnt_num ;
wire add_cnt_num ;
wire end_cnt_num ;
reg [8:0] cnt_stop ;
wire add_cnt_stop ;
wire end_cnt_stop ;
reg [7:0] tx_data_r ;
//****************************************************************
// 发送模块状态机
//****************************************************************
//第一段:时序逻辑描述状态转移
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cstate <= IDLE;
end
else begin
cstate <= nstate;
end
end
//第二段:组合逻辑描述状态转移规律和状态转移条件
always @(*) begin
case(cstate)
IDLE : begin
if (idle2start) begin
nstate = START;
end
else begin
nstate = cstate;
end
end
START : begin
if (start2data) begin
nstate = DATA;
end
else begin
nstate = cstate;
end
end
DATA : begin
if (data2check) begin
nstate = CHECK;
end
else if (data2stop) begin
nstate = STOP;
end
else begin
nstate = cstate;
end
end
CHECK : begin
if (check2stop) begin
nstate = STOP;
end
else begin
nstate = cstate;
end
end
STOP : begin
if (stop2idle) begin
nstate = IDLE;
end
else begin
nstate = cstate;
end
end
default : ;
endcase
end
assign idle2start = cstate == IDLE && tx_data_vld ; //接收到vld信号开始发送数据
assign start2data = cstate == START && end_cnt_num;//计时发送1bit后跳转
assign data2check = cstate == DATA && end_cnt_num && CHECK_BIT != "NONE";//计时发送8bit数据后,需要校验位
assign data2stop = cstate == DATA && end_cnt_num && CHECK_BIT == "NONE";//不需要校验位
assign check2stop = cstate == CHECK && end_cnt_num;//发送1bit校验位
assign stop2idle = cstate == STOP && end_cnt_num;//计时发送1bit数据后跳转
//第三段:描述输出,时序逻辑或组合逻辑皆可
always @(*) begin
if (!rst_n) begin
tx <= 1;
end
else case (cstate)
IDLE : tx = 1;//拉高
START: tx = 0;//拉低发送开始位
DATA: tx = tx_data_r[cnt_num];//发送数据
CHECK: tx = temp;//奇偶校验位
STOP : tx = 1;//拉高发送结束位
default: tx = 1;
endcase
end
assign ready = cstate == IDLE;
//****************************************************************
// 数据寄存
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_data_r <= 8'd0;
end
else if (tx_data_vld) begin
tx_data_r <= tx_data;
end
end
//****************************************************************
// 奇偶校验位
//****************************************************************
assign temp = (CHECK_BIT == "DDO")? ~^tx_data_r:^tx_data_r;
//****************************************************************
// 1bit数据
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_data <= 12'd0;
end
else if(add_cnt_data)begin
if(end_cnt_data)begin
cnt_data <= 12'd0;
end
else begin
cnt_data <= cnt_data + 1'b1;
end
end
end
assign add_cnt_data = cstate != IDLE;
assign end_cnt_data = add_cnt_data && cnt_data == MAX_1bit-1;
//****************************************************************
// 选择数据位计数器
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_num <= 3'd0;
end
else if(add_cnt_num)begin
if(end_cnt_num)begin
cnt_num <= 3'd0;
end
else begin
cnt_num <= cnt_num + 1'b1;
end
end
end
assign add_cnt_num = end_cnt_data;
assign end_cnt_num = add_cnt_num && cnt_num == num - 1;
//****************************************************************
// 数据控制
//****************************************************************
always @(*) begin
case (cstate)
IDLE : num = 1;
START: num = 1;
DATA : num = 8;
CHECK: num = 1;
STOP : num = 1;
default: num = 1;
endcase
end
endmodule
按键消抖模块:
/**************************************功能介绍***********************************
Date :
Author : majiko
Version :
Description: 按键消抖的参数化模块(高电平有效)
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module key_debounce
#(parameter N = 4)
(
input wire clk ,
input wire rst_n ,
input wire [N-1:0] key_in ,//N个按键输入
output reg [N-1:0] key_out //N个按键输出
);
//---------<参数定义>---------------------------------------------------------
parameter MAX_20MS = 20'd999_999;
//---------<内部信号定义>-----------------------------------------------------
//延时计数器参数
reg [19:0] cnt_delay ;
wire add_cnt_delay ;
wire end_cnt_delay ;
//边沿检测参数
reg [N-1:0] key_in_r0;
reg [N-1:0] key_in_r1;
reg [N-1:0] key_in_r2;
wire nedge ;
wire podge ;
reg flag;//flag驱动使能
//****************************************************************
// 计数器延迟20ms
//****************************************************************
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt_delay <= 20'd0;
end
else if(add_cnt_delay)begin
if(end_cnt_delay)begin
cnt_delay <= 20'd0;
end
else begin
cnt_delay <= cnt_delay + 1'b1;
end
end
end
assign add_cnt_delay = flag;
assign end_cnt_delay = add_cnt_delay && cnt_delay == MAX_20MS;
//****************************************************************
// 下降沿检测
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_in_r0 <= {N{1'b1}};
key_in_r1 <= {N{1'b1}};
key_in_r2 <= {N{1'b1}};
end
else begin
key_in_r0 <= key_in;
key_in_r1 <= key_in_r0;
key_in_r2 <= key_in_r1;
end
end
assign nedge = |(~key_in_r1&key_in_r2);
assign podge = |(key_in_r1&~key_in_r2);
//****************************************************************
// flag使能
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
flag <= 0;
end
else if (nedge) begin
flag <= 1'b1;
end
else if (end_cnt_delay) begin
flag <= 0;
end
else begin
flag <= flag;
end
end
//****************************************************************
// 赋值输出
//****************************************************************
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
key_out <= {N{1'b0}};
end
else if (end_cnt_delay) begin
key_out <= ~key_in_r1;
end
else begin
key_out <= {N{1'b0}};
end
end
endmodule
顶层模块:
/**************************************功能介绍***********************************
Date :
Author : majiko
Version :
Description:
*********************************************************************************/
//---------<模块及端口声名>------------------------------------------------------
module top(
input wire clk ,
input wire rst_n ,
input wire [2:0] key_in ,
input wire miso ,
output wire cs_n ,
output wire sclk ,
output wire mosi ,
output wire tx
);
//---------<参数定义>---------------------------------------------------------
wire [2:0] key_debounce;
wire [7:0] data_out ;
wire data_out_vld;
wire [7:0] tx_data ;
wire tx_data_vld ;
wire ready ;
wire fifo_empty ;
wire fifo_full ;
//---------<内部信号定义>-----------------------------------------------------
key_debounce
#(.N(3))
key_debounce_inst
(
/*input wire */ .clk (clk),
/*input wire */ .rst_n (rst_n),
/*input wire [N-1:0] */ .key_in (key_in),//N个按键输入
/*output reg [N-1:0] */ .key_out (key_debounce)//N个按键输出
);
spi_control spi_control_inst(
/*input wire */ .clk (clk),
/*input wire */ .rst_n (rst_n),
/*input wire */ .read_req (key_debounce[1]),
/*input wire */ .rdid_req (key_debounce[0]),
/*input wire */ .wen_req (key_debounce[2]),
/*input wire */ .ready (),
/*input wire [23:0]*/ .read_addr (24'h000000), //需要读的数据地址
/*input wire [7:0] */ .read_num (3), //需要读出的数据0~255
/*input wire [7:0] */ .data_in (8'h11),
/*input wire */ .data_in_vld (1),
/*output wire [7:0] */ .data_out (data_out), //返回的数据
/*output wire */ .data_out_vld (data_out_vld),
/*//spi接口*/
/*input wire */ .miso (miso),
/*output wire */ .cs_n (cs_n),
/*output wire */ .sclk (sclk),
/*output wire */ .mosi (mosi) //主机输出给从机
);
uart_tx#
(
.BPS (115200) ,
.CLK_FRE (50_000_000) ,
.CHECK_BIT ("NONE") //NONE 不校验 DDO奇数 EVEN偶数
)uart_tx_inst
(
/*input wire */ .clk (clk),
/*input wire */ .rst_n (rst_n),
/*input wire */ .tx_data_vld (tx_data_vld),
/*input wire [7:0] */ .tx_data (tx_data),
/*output reg */ .tx (tx),
/*output wire */ .ready (ready)
);
fifo_tx fifo_tx_inst (
.aclr ( ~rst_n ),
.clock ( clk ),
.data ( data_out ),
.wrreq ( data_out_vld ),
.q ( tx_data ),
.rdreq ( tx_data_vld ),
.empty ( fifo_empty ),
.full ( fifo_full )
);
assign tx_data_vld = ready && ~fifo_empty;
endmodule