SPI的原理以及Verilog HDL实现

举报
李锐博恩 发表于 2021/07/15 02:42:21 2021/07/15
【摘要】 文章链接:SPI https://www.diangon.com/wenku/rd/danpianji/201501/00017903.html SPI是同步串行通信接口。  SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片...

文章链接:SPI

https://www.diangon.com/wenku/rd/danpianji/201501/00017903.html

SPI是同步串行通信接口。 
SPI是英语Serial Peripheral Interface的缩写,顾名思义就是串行外围设备接口。SPI是一种高速的、全双工、同步通信总线,标准的SPI也仅仅使用4个引脚,常用于单片机和EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信。SPI通信原理比I2C要简单,它主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,标准的SPI是4根线,分别是SSEL(片选,也写作SCS)、SCLK(时钟,也写作SCK)、MOSI(主机输出从机输入Master Output/Slave Input)和MISO(主机输入从机输出Master Input/Slave Output)。

SSEL:从设备片选使能信号。如果从设备是低电平使能的话,当拉低这个引脚后,从设备就会被选中,主机和这个被选中的从机进行通信。 
SCLK:时钟信号,由主机产生,和I2C通信的SCL有点类似。 
MOSI:主机给从机发送指令或者数据的通道。 
MISO:主机读取从机的状态或者数据的通道。

单从设备:

多从设备:

 

在某些情况下,我们也可以用3根线的SPI或者2根线的SPI进行通信。比如主机只给从机发送命令,从机不需要回复数据的时候,那MISO就可以不要;而在主机只读取从机的数据,不需要给从机发送指令的时候,那MOSI可以不要;当一个主机一个从机的时候,从机的片选有时可以固定为有效电平而一直处于使能状态,那么SSEL可以不要;此时如果再加上主机只给从机发送数据,那么SSEL和MISO都可以不要;如果主机只读取从机送来的数据,SSEL和MOSI都可以不要。 3线和2线的SPI大家要知道怎么回事,实际使用也是有应用的,但是当我们提及SPI的时候,一般都是指标准SPI,都是指4根线的这种形式。

SPI通信的主机也是我们的单片机,在读写数据时序的过程中,有四种模式,要了解这四种模式,首先我们得学习一下2个名词。

CPOL:Clock Polarity,就是时钟的极性。 
时钟的极性是什么概念呢?通信的整个过程分为空闲时刻和通信时刻,SCLK在数据发送之前和之后的空闲状态是高电平那么CPOL=1,如果空闲状态SCLK是低电平,那么CPOL=0。 
CPHA:Clock Phase,就是时钟的相位。

主机和从机要交换数据,就牵涉到一个问题,即主机在什么时刻输出数据到MOSI上而从机在什么时刻采样这个数据,或者从机在什么时刻输出数据到MISO上而主机什么时刻采样这个数据。同步通信的一个特点就是所有数据的变化和采样都是伴随着时钟沿进行的,也就是说数据总是在时钟的边沿附近变化或被采样。而一个时钟周期必定包含了一个上升沿和一个下降沿,这是周期的定义所决定的,只是这两个沿的先后并无规定。又因为数据从产生的时刻到它的稳定是需要一定时间的,那么,如果主机在上升沿输出数据到MOSI上,从机就只能在下降沿去采样这个数据了。反之如果一方在下降沿输出数据,那么另一方就必须在上升沿采样这个数据。 
CPHA=1,就表示数据的输出是在一个时钟周期的第一个沿上,至于这个沿是上升沿还是下降沿,这要是CPOL的值而定,CPOL=1那就是下降沿,反之就是上升沿。那么数据的采样自然就是在第二个沿上了。 
CPHA=0,就表示数据的采样是在一个时钟周期的第一个沿上,同样它是什么沿由CPOL决定。那么数据的输出自然就在第二个沿上了。 
仔细想一下,这里会有一个问题:就是当一帧数据开始传输第一bit时,在第一个时钟沿上就采样该数据了,那么它是在什么时候输出来的呢?有两种情况:一是SSEL使能的边沿,二是上一帧数据的最后一个时钟沿,有时两种情况还会同时生效。

我们以CPOL=1/CPHA=1为例,把时序图画出来给大家看一下,如图1所示,。 
 

è¿éåå¾çæè¿°

大家看图1所示,当数据未发送时以及发送完毕后,SCK都是高电平,因此CPOL=1。可以看出,在SCK第一个沿的时候,MOSI和MISO会发生变化,同时SCK第二个沿的时候,数据是稳定的,此刻采样数据是合适的,也就是上升沿即一个时钟周期的后沿锁存读取数据,即CPHA=1。注意最后最隐蔽的SSEL片选,一般情况下,这个引脚通常用来决定是哪个从机和主机进行通信。剩余的三种模式,我把图画出来,简化起见把MOSI和MISO合在一起了,大家仔细对照看看研究一下,把所有的理论过程都弄清楚,有利于你对SPI通信的深刻理解,如图2所示。
 

è¿éåå¾çæè¿°

当CPOL= 0以及CPHA = 0时候的Verilog HDL设计为:


  
  1. `timescale 1ns / 1ps
  2. //
  3. // Create Date: 17:02:00 03/15/2019
  4. // Design Name:
  5. // Module Name: spi_master
  6. // Additional Comments:
  7. // CPOL = 0 => SCK在数据发送前后的空闲状态都是低电平;
  8. // CPHA = 0 => 数据采样在第一个时钟周期的第一个沿,数据输出在第二个沿;
  9. // clk = 100MHZ,通过分频产生1MHZ的sck时钟;
  10. //
  11. module spi_master(
  12. input mosi,
  13. input busy,
  14. input rst_n,
  15. input clk,
  16. input spi_send,
  17. input [7:0] spi_data_out,
  18. output reg sck,
  19. output reg miso,
  20. output reg cs,
  21. output reg spi_send_done
  22. );
  23. reg [3:0] count;
  24. //状态机分为四个状态:等待
  25. localparam IDLE = 0,
  26. CS_L = 1,
  27. DATA = 2,
  28. FINISH = 3;
  29. reg [4:0] cur_st,nxt_st;
  30. reg [7:0] reg_data;
  31. reg sck_reg;
  32. reg [8:0] delay_count;
  33. //时钟分频
  34. always@(posedge clk) begin
  35. if(!rst_n) delay_count <= 0;
  36. else if(delay_count == 49) delay_count <= 0;
  37. else delay_count <= delay_count + 1;
  38. end
  39. //产生一个1MHZ的时钟
  40. always@(posedge clk) begin
  41. if(!rst_n) sck_reg <= 0;
  42. else if(delay_count == 49) sck_reg <= !sck_reg;
  43. end
  44. //sck只有在cs拉低时才变化,其他都为低,响应CPOL = 0,sck在数据发送前后的空闲状态都为地
  45. always@(*) begin
  46. if(cs) sck = 0; //cs为高,则没有选中从设备;
  47. else if(cur_st == FINISH) sck = 0;
  48. else if(!cs) sck = sck_reg;
  49. else sck = 1;
  50. end
  51. //状态机部分
  52. always@(posedge sck_reg) begin
  53. if(!rst_n) cur_st <= IDLE;
  54. else cur_st <= nxt_st;
  55. end
  56. always@(*) begin
  57. nxt_st = cur_st;
  58. case(cur_st)
  59. IDLE: if(spi_send) nxt_st = CS_L;
  60. CS_L: nxt_st = DATA;
  61. DATA: if(count == 7) nxt_st = FINISH;
  62. FINISH: if(busy) nxt_st = IDLE;
  63. default: nxt_st = IDLE;
  64. endcase
  65. end
  66. //产生发送结束标志
  67. always@(*) begin
  68. if(!rst_n) spi_send_done = 0;
  69. else if(cur_st == FINISH) spi_send_done = 1;
  70. else spi_send_done = 0;
  71. end
  72. //产生CS
  73. always@(posedge sck_reg) begin
  74. if(!rst_n) cs <= 1;
  75. else if(cur_st == CS_L) cs <= 0;
  76. else if(cur_st == DATA) cs <= 0;
  77. else cs <= 1;
  78. end
  79. //发送数据计数
  80. always@(posedge sck_reg) begin
  81. if(!rst_n) count <= 0;
  82. else if(cur_st == DATA) count <= count + 1;
  83. else if(cur_st == IDLE|cur_st == FINISH) count <= 0;
  84. end
  85. //MISO数据
  86. always@(negedge sck_reg) begin
  87. if(!rst_n) miso <= 0;
  88. else if(cur_st == DATA) begin
  89. reg_data[7:1] <= reg_data[6:0];
  90. miso <= reg_data[7];
  91. end
  92. else if(spi_send) reg_data <= spi_data_out;
  93. end
  94. endmodule

给出输入输出的原理图:

 

有空仿真下看看,这是书上的例子,但还是心存疑惑!



下面再给出一个博文中的Verilog实现的SPI,虽然我不知道意义多大,但还能凑合着看:https://blog.csdn.net/IamSarah/article/details/76269737

(改变了代码的格式,原格式看着真心累!)

以上就是SPI接口的工作原理,下面给出其verilog实现,这里主器件用读命令和写命令来控制数据的输入和输出,并且对于一个字节的数据读和写分别用一个任务实现,如下:


  
  1. module spi(clk,rd,wr,rst,data_in,si,so,sclk,cs,data_out);
  2. parameter bit7=4'd0,bit6=4'd1,bit5=4'd2,bit4=4'd3,bit3=4'd4,bit2=4'd5,bit1=4'd6,bit0=4'd7,bit_end=4'd8;
  3. parameter bit70=4'd0,bit60=4'd1,bit50=4'd2,bit40=4'd3,bit30=4'd4,bit20=4'd5,bit10=4'd6,bit00=4'd7,bit_end0=4'd8;
  4. parameter size=8;
  5. input clk,rst;
  6. input wr,rd;//读写命令
  7. input si;//spi数据输入端
  8. input [size-1:0]data_in;//待发送的数据
  9. output[size-1:0]data_out;//待接收的数据
  10. output sclk;//spi中的时钟
  11. output so;//spi的发送端
  12. output cs;//片选信号
  13. wire [size-1:0]data_out;
  14. reg [size-1:0]dout_buf;
  15. reg FF;
  16. reg sclk;
  17. reg so;
  18. reg cs;
  19. reg [3:0]send_state;//发送状态寄存器
  20. reg [3:0]receive_state;//接收状态寄存器
  21. always@(posedge clk)
  22. begin
  23. if(!rst)
  24. begin
  25. sclk<=0;
  26. cs<=1;
  27. end
  28. else
  29. begin
  30. if(rd|wr)
  31. begin
  32. sclk<=~sclk;//当开始读或者写的时候,需要启动时钟
  33. cs<=0;
  34. end
  35. else
  36. begin
  37. sclk=0;
  38. cs<=1;
  39. end
  40. end
  41. end
  42. always@(posedge sclk)//发送数据
  43. begin
  44. if(wr)
  45. begin
  46. send_state <= bit7;
  47. send_data;
  48. end
  49. end
  50. always@(posedge sclk)//接收数据
  51. begin
  52. if(rd)
  53. begin
  54. receive_state<=bit70;
  55. FF<=0;
  56. receive_data;
  57. end
  58. end
  59. assign data_out=(FF==1)?dout_buf:8'hz;
  60. task send_data;//发送数据任务
  61. begin
  62. case(send_state)
  63. bit7:
  64. begin
  65. so<=data_in[7];
  66. send_state<=bit6;
  67. end
  68. bit6:
  69. begin
  70. so<=data_in[6];
  71. send_state<=bit5;
  72. end
  73. bit5:
  74. begin
  75. so<=data_in[5];
  76. send_state<=bit4;
  77. end
  78. bit4:
  79. begin
  80. so<=data_in[4];
  81. send_state<=bit3;
  82. end
  83. bit3:
  84. begin
  85. so<=data_in[3];
  86. send_state<=bit2;
  87. end
  88. bit2:
  89. begin
  90. so<=data_in[2];
  91. send_state<=bit1;
  92. end
  93. bit1:
  94. begin
  95. so<=data_in[1];
  96. send_state<=bit0;
  97. end
  98. bit0:
  99. begin
  100. so<=data_in[0];
  101. send_state<=bit_end;
  102. end
  103. bit_end:
  104. begin
  105. so=1'bz;
  106. send_state<=bit7;
  107. end
  108. endcase
  109. end
  110. endtask
  111. task receive_data;
  112. begin
  113. case (receive_state)
  114. bit70:
  115. begin
  116. dout_buf[7]<=si;
  117. receive_state<=bit60;
  118. end
  119. bit60:
  120. begin
  121. dout_buf[6]<=si;
  122. receive_state<=bit50;
  123. end
  124. bit50:
  125. begin
  126. dout_buf[5]<=si;
  127. receive_state<=bit40;
  128. end
  129. bit40:
  130. begin
  131. dout_buf[4]<=si;
  132. receive_state<=bit30;
  133. end
  134. bit30:
  135. begin
  136. dout_buf[3]<=si;
  137. receive_state<=bit20;
  138. end
  139. bit20:
  140. begin
  141. dout_buf[2]<=si;
  142. receive_state<=bit10;
  143. end
  144. bit10:
  145. begin
  146. dout_buf[1]<=si;
  147. receive_state<=bit00;
  148. end
  149. bit00:
  150. begin
  151. dout_buf[0]<=si;
  152. receive_state<=bit_end;
  153. FF<=1;
  154. end
  155. bit_end0:
  156. begin
  157. dout_buf<=8'hzz;
  158. receive_state<=bit70;
  159. end
  160. endcase
  161. end
  162. endtask
  163. endmodule

为了看清这段代码,上图更加直观:

 

 

 

 

文章来源: reborn.blog.csdn.net,作者:李锐博恩,版权归原作者所有,如需转载,请联系作者。

原文链接:reborn.blog.csdn.net/article/details/88569954

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。