emmmmm,一下子跳到了SPI通信,跨度有点大,刚好学到这里,OK少废话。
相信学过ARM的同学对SPI通信也有一定的认识,很多模块都需要用到SPI通信。我就直接用黑金开发板AX301的SPI_Flash例程里面的SPI_master给大家讲解一下。够良心的啦,黑金开发板的资料都没有给出相应的SPI知识,这让学过ARM但基础知识不扎实的同学怎么办(说的好像就是我。。。。。。。)来吧来吧,哥给你普及一下知识吧。(讲得不好体谅体谅,博主水平有限,欢迎给修改意见)
特地去原子哥里面找到SPI的时序图,放两张重要的:
两张图中可以看出,SPI通信是通过CPHA和CPOL组合成四种方式进行通信的,CPOL=1时,时钟在空闲状态下始终是高电平,反之则为低电平,而CPHA=0时捕捉的是第一个边缘,否则为第二个时钟边缘。
MISO和MOSI又是什么意思呢?M:master;I:input;S:slaver;O:output;意思就是主从设备的输出和输入,那么他们是通过什么样的原理传输数据的呢?很开心继续为大家呈上一张图:
如图,SPI是通过主从设备不断移位实现数据的传输的。移位的概念懂不懂?10111000向右移一位就是01011100,这就不详细解释了,闲的时候多翻翻数电书,哈哈哈哈哈。
OK,说了那么多,给大家直接呈上代码了:
module spi_master
(
input sys_clk,
input rst,
output nCS, //chip select (SPI mode)
output DCLK, //spi clock
output MOSI, //spi master data output
input MISO, //spi master input
input CPOL,
input CPHA,
input nCS_ctrl,
input[15:0] clk_div,
input wr_req,
output wr_ack,
input[7:0] data_in,
output[7:0] data_out
);
localparam IDLE = 0;
localparam DCLK_EDGE = 1;
localparam DCLK_IDLE = 2;
localparam ACK = 3;
localparam LAST_HALF_CYCLE = 4;
localparam ACK_WAIT = 5;
reg DCLK_reg;
reg[7:0] MOSI_shift;
reg[7:0] MISO_shift;
reg[2:0] state;
reg[2:0] next_state;
reg [15:0] clk_cnt;
reg[4:0] clk_edge_cnt;
assign MOSI = MOSI_shift[7];
assign DCLK = DCLK_reg;
assign data_out = MISO_shift;
assign wr_ack = (state == ACK);
assign nCS = nCS_ctrl;
/*************这个就是状态机的定义**************/
always@(posedge sys_clk or posedge rst)
begin
if(rst)
state <= IDLE;
else
state <= next_state;
end
/****************end*************************/
/****************状态机的具体过程*************/
always@(*)
begin
case(state)
IDLE:
if(wr_req == 1'b1)
next_state <= DCLK_IDLE;
else
next_state <= IDLE;
DCLK_IDLE:
//half a SPI clock cycle produces a clock edge
if(clk_cnt == clk_div)
next_state <= DCLK_EDGE;
else
next_state <= DCLK_IDLE;
DCLK_EDGE:
//a SPI byte with a total of 16 clock edges
if(clk_edge_cnt == 5'd15)
next_state <= LAST_HALF_CYCLE;
else
next_state <= DCLK_IDLE;
//this is the last data edge
LAST_HALF_CYCLE:
if(clk_cnt == clk_div)
next_state <= ACK;
else
next_state <= LAST_HALF_CYCLE;
//send one byte complete
ACK:
next_state <= ACK_WAIT;
//wait for one clock cycle, to ensure that the cancel request signal
ACK_WAIT:
next_state <= IDLE;
default:
next_state <= IDLE;
endcase
end
/*************详情见图一**************************/
/****************时钟翻转************************/
always@(posedge sys_clk or posedge rst)
begin
if(rst) /*在空闲状态之前,SCK一直保持CPOL的极性*/
DCLK_reg <= 1'b0;
else if(state == IDLE)
DCLK_reg <= CPOL;
else if(state == DCLK_EDGE) /*边缘检测时,反转SCK*/
DCLK_reg <= ~DCLK_reg;//SPI clock edge
end
/****************end*****************************/
//SPI clock wait counter /*一个计数器*/
always@(posedge sys_clk or posedge rst)
begin
if(rst)
clk_cnt <= 16'd0;
else if(state == DCLK_IDLE || state == LAST_HALF_CYCLE)
clk_cnt <= clk_cnt + 16'd1;
else
clk_cnt <= 16'd0;
end
//SPI clock edge counter
always@(posedge sys_clk or posedge rst)
begin
if(rst)
clk_edge_cnt <= 5'd0;
else if(state == DCLK_EDGE)
clk_edge_cnt <= clk_edge_cnt + 5'd1;
else if(state == IDLE)
clk_edge_cnt <= 5'd0;
end
//SPI data output /*这里就是SPI输出的移位方式*/
always@(posedge sys_clk or posedge rst)
begin
if(rst)
MOSI_shift <= 8'd0;
else if(state == IDLE && wr_req)
MOSI_shift <= data_in;
else if(state == DCLK_EDGE)
if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b1) /*两种方式,取决于CPHA*/
MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]}; /*常见的移位语句,大家要敏感*/
else if(CPHA == 1'b1 && (clk_edge_cnt != 5'd0 && clk_edge_cnt[0] == 1'b0))
MOSI_shift <= {MOSI_shift[6:0],MOSI_shift[7]};
end
//SPI data input
always@(posedge sys_clk or posedge rst)
begin
if(rst)
MISO_shift <= 8'd0;
else if(state == IDLE && wr_req)
MISO_shift <= 8'h00;
else if(state == DCLK_EDGE)
if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0)
MISO_shift <= {MISO_shift[6:0],MISO}; /*MISO输入,然后进行移位*/
else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1))
MISO_shift <= {MISO_shift[6:0],MISO};
end
endmodule
(好长,害怕,其实我不是很想码字,哎,不过算了,大家记得给我点赞,谢谢。)
自己用visio画的状态机,够良心吧,参见注释说的图一:
我觉得这段代码有很多值得学习的地方。状态机、计数器这些常见的代码我就忽略不说了,我来讲讲MISO和MOSI 的过程。
先讲input吧:
else if(state == DCLK_EDGE)
if(CPHA == 1'b0 && clk_edge_cnt[0] == 1'b0)
MISO_shift <= {MISO_shift[6:0],MISO}; /*MISO输入,然后进行移位*/
else if(CPHA == 1'b1 && (clk_edge_cnt[0] == 1'b1))
MISO_shift <= {MISO_shift[6:0],MISO};
可以从我画的状态机图清晰地看到clk_edge_cnt是在DCLK_EDGE状态开始计数,当CPHA=1时,捕捉的是第一个边缘,因为从0开始数,所以只要判断clk_edge_cnt[0] 也就是clk_edge_cnt的末位为0时,则是第一个时钟边缘。(佩服这句代码真的很严谨)然后输入的MISO作为MISO_shift的末位进行移位。那么else if也是同样的理解方法。
那么output也是同样的道理。
哇写完博客觉得自己理解得好透彻哈哈哈哈。虽然还不知道这段代码怎么用,那就期待下一篇吧。下一篇详细去讲SPI+FLASH的实例。
--------------------------------分割线--------------------------------------
新建了一个请求群,希望和大家一起交流FPGA