你以为我就直接将spi协议呢? 嘿嘿,百度吧,一搜一大堆,我就不赘诉了。
总的来说就是四根线,mosi,miso,sck和cs,三线模式下为半双工通信(同一时刻只能输入或者输出),四线模式下为全双工通信(同一时刻既能输入,又能输出),连接方式如下图所示:
这是一个一主多从的连接方式,每个设备都会收到时钟和数据,但是决定哪一个从机去响应就由片选信号(cs)决定;
sck : 主机时钟输出
mosi:主机输出,从机输入,与从机的mosi或者单向的sdi连接;
miso:主机输入,从机输出,与从机的miso或者单向的sdo连接;
cs :chip select (片选信号)
spi一共有四种模式:mode0, mode1,mode2, mode3,一般由CPOL,CPHA 控制它的初始时钟电平,相位;需要注意的是mode0和mode2是第一个时钟沿是采样沿,数据在第一个时钟沿到来之前需要准备好mosi的数据。
1.CPOL = 0,CPHA = 0(mode0):此时空闲态时,SCLK处于低电平,数据采样是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在上升沿,数据发送是在下降沿,时序如下:
2.CPOL = 0,CPHA = 1(mode1):此时空闲态时,SCLK处于低电平,数据发送是在第1个边沿,也就是SCLK由低电平到高电平的跳变,所以数据采样是在下降沿,数据发送是在上升沿,时序如下:
ps:粘贴一半发现原版文档值写了前两种模式,崩溃,重新找图
3.CPOL = 1,CPHA = 0(mode2):此时空闲态时,SCLK处于高电平,数据采集是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在下降沿,数据发送是在上升沿,时序如下:
4.CPOL = 1,CPHA = 1(mode3):此时空闲态时,SCLK处于高电平,数据发送是在第1个边沿,也就是SCLK由高电平到低电平的跳变,所以数据采集是在上升沿,数据发送是在下降沿,时序如下:
ps:不好意思,盗图有点东拼西凑,实在是懒得画图
关于三线和四线模式,我看到官方文档里面是mosi和miso都是作为一个双向io口使用的。作为四线模式使用时,是一个全双工模式通信,输入和输出都是单向数据;而作为三线模式时,spi时一个半双工通信,也就是说用的同一根数据线进行输入输出,分时复用,所以我的代码里面用的是将mosi作为一个双向io;而另外一根miso线则是在三线模式中没有使用,我没太懂为啥文档里会把miso也作为一个双向的io。
下面是飞思卡尔的一个spi协议的模块框图:
这个spi模块就划分为了几个大的部分,有控制寄存器,状态寄存器,时钟分频和移位寄存器。
先说一下代码,有很多不规范的地方,不同时钟最好放在不同module里面,等等,欢迎大家指正。
此为spi主设备,可挂外部设备进行通讯,以下为此模块特点:
1、传输帧数据位宽8、16、32位选择
2、对于外部从设备时钟SCLK频率,以PCLK为基础分频,可写入预分频系数进行分频;
3、cs为片选信号
4、支持三线半双工或四线全双工工作模式,二线的情况下通讯MOSI作为双向数据线;
5、可编数据发送顺序,MSB优先或LSB优先
6、SPI通讯忙标志
7、SPI频率计算公式(最小8分频)
8、SPI频率 = PCLK/(DIV * 2);
使用方式就是:先配置好时钟分频和spi模式,然后开启cs,最后开启start;(cs和start的间隔时间具体看从机器件规格书)
这是一个基于apb总线的模块,寄存器本想做一个表格,但是这上面表格里面换行不知道怎么弄就截图了。
这一段是时钟分频,sel0,sel1,sel2,sel3是四种模式,sclk的初始的电平不同:
//clk divide counter
always @(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt <= 0;
else if(div_cnt == div)
div_cnt <= 0;
else if(start_r2)
div_cnt <= div_cnt + 31'b1;
else
div_cnt <= 0;
//spi clk generate
always @(posedge clk or negedge rst_n)
if(!rst_n)
sclk <= 0;
else if(start) begin
if(div_cnt == div)
sclk <= ~sclk;
else ;
end
else begin
if(sel0 || sel1)
sclk <= 0;
else if(sel2 || sel3)
sclk <= 1;
end
这一段是数据的移位,在每一个采样沿移位,给下一次数据做准备;其中ctrl【4:3】就是mode的选择,2‘b00代表sel0,2‘b01代表sel1,2’b10代表sel2,2’b11代表sel3;start_r2是延迟两拍的开始信号,ctrl【0】是开始信号的命令,lsb_en为1表示低位先出,否则为msb(高位先出)。
//shift data
always @(posedge clk or negedge rst_n)
if(!rst_n)
wr_data <= 0;
else if(ctrl[0])
wr_data <= wr_r;
else if(start_r2) begin
if(lsb_en) begin
case (ctrl[4:3])
2'b00:begin
if(sclk_pos || start_pos)
tx_lsb_shift;
else ;
end
2'b01:begin
if(sclk_neg)
tx_lsb_shift;
else ;
end
2'b10:begin
if(sclk_neg || start_pos)
tx_lsb_shift;
else ;
end
2'b11:begin
if(sclk_pos)
tx_lsb_shift;
else ;
end
endcase
end
else begin
case (ctrl[4:3])
2'b00:begin
if(sclk_pos || start_pos)
tx_msb_shift;
else ;
end
2'b01:begin
if(sclk_neg)
tx_msb_shift;
else ;
end
2'b10:begin
if(sclk_neg || start_pos)
tx_msb_shift;
else ;
end
2'b11:begin
if(sclk_pos)
tx_msb_shift;
else ;
end
endcase
end
end
task tx_lsb_shift;
if(send_bits == 6'd8)
wr_data = {1'b0, wr_data[7:1]};
else if(send_bits == 6'd16)
wr_data = {1'b0, wr_data[15:1]};
else if(send_bits == 6'd32)
wr_data = {1'b0, wr_data[31:1]};
endtask
task tx_msb_shift;
if(send_bits == 6'd8)
wr_data = {wr_data[6:0], 1'b0};
else if(send_bits == 6'd16)
wr_data = {wr_data[14:0], 1'b0};
else if(send_bits == 6'd32)
wr_data = {wr_data[30:0], 1'b0};
endtask
这是一个状态机,主要用来生成标志信号,busy,read,write等。bidiroe 为1表示,选择三线模式(半双工模式),需要用读或者写来指定是发送还是接受数据,否则为四线模式(全双工);
//state machine
localparam IDLE = 6'b000_001,
START = 6'b000_010,
WRITE = 6'b000_100,
READ = 6'b001_000,
FULL_WORK = 6'b010_000,
END = 6'b100_000;
reg [5:0]current_state,next_state;
always @(posedge clk or negedge rst_n)
if(!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
always @(*)
case(current_state)
IDLE : begin
if(start)
next_state <= START;
else
next_state <= IDLE;
end
START : begin
if(bidiroe )
if(read_en)
next_state <= READ;
else
next_state <= WRITE;
else
next_state <= FULL_WORK;
end
READ : begin
if(transfer_end)
next_state <= END;
else
next_state <= READ;
end
WRITE : begin
if(transfer_end)
next_state <= END;
else
next_state <= WRITE;
end
FULL_WORK : begin
if(transfer_end)
next_state <= END;
else
next_state <= FULL_WORK;
end
END : begin
next_state <= IDLE;
end
default : next_state <= IDLE;
endcase
always @(posedge clk or negedge rst_n)
if(!rst_n) begin
busy <= 0;
wr_finish <= 0;
rd_finish <= 0;
recv_en <= 0;
write_en <= 0;
sdo_en <= 0;
end
else
case(next_state)
IDLE : begin
busy <= 0;
wr_finish <= 0;
rd_finish <= 0;
write_en <= 0;
recv_en <= 0;
sdo_en <= 0;
end
START : begin
busy <= 1;
sdo_en <= 0;
end
READ : begin
recv_en <= 1;
sdo_en <= 0;
write_en<= 0;
end
WRITE : begin
recv_en <= 0;
write_en<= 1;
sdo_en <= 1;
end
FULL_WORK : begin
recv_en <= 1;
write_en<= 1;
sdo_en <= 1;
end
END : begin
case({recv_en,write_en})
2'b00 :begin
recv_en <= 0;
write_en<= 0;
wr_finish <= 0;
rd_finish <= 0;
end
2'b01 : begin
wr_finish <= 1;
write_en <= 0;
end
2'b10 : begin
rd_finish <= 1;
recv_en <= 0;
end
2'b11 : begin
recv_en <= 0;
write_en<= 0;
wr_finish <= 1;
rd_finish <= 1;
end
endcase
sdo_en <= 0;
busy <= 0;
end
default :begin
busy <= 0;
wr_finish <= 0;
rd_finish <= 0;
recv_en <= 0;
sdo_en <= 0;
end
endcase
这是数据接收部分,分为上升沿接收,和下降沿接收。
//recive data
//posedge sample
always @(posedge sclk or negedge rst_n)
if(!rst_n)
rd_data_pos <= 0;
else if(recv_en)
if(lsb_en) begin
if(send_bits == 6'd8)
rd_data_pos[7:0] <= {sdi, rd_data_pos[7:1]};
else if(send_bits == 6'd16)
rd_data_pos[15:0] <= {sdi, rd_data_pos[15:1]};
else if(send_bits == 6'd32)
rd_data_pos[31:0] <= {sdi, rd_data_pos[31:1]};
else ;
end
else begin
if(send_bits == 6'd8)
rd_data_pos[7:0] <= {rd_data_pos[6:0], sdi};
else if(send_bits == 6'd16)
rd_data_pos[15:0] <= {rd_data_pos[14:0], sdi};
else if(send_bits == 6'd32)
rd_data_pos[31:0] <= {rd_data_pos[30:0], sdi};
else ;
end
//negedge sample
always @(negedge sclk or negedge rst_n)
if(!rst_n)
rd_data_neg <= 0;
else if(recv_en)
if(lsb_en) begin
if(send_bits == 6'd8)
rd_data_neg[7:0] <= {sdi, rd_data_neg[7:1]};
else if(send_bits == 6'd16)
rd_data_neg[15:0] <= {sdi, rd_data_neg[15:1]};
else if(send_bits == 6'd32)
rd_data_neg[31:0] <= {sdi, rd_data_neg[31:1]};
else ;
end
else begin
if(send_bits == 6'd8)
rd_data_neg[7:0] <= {rd_data_neg[6:0], sdi};
else if(send_bits == 6'd16)
rd_data_neg[15:0] <= {rd_data_neg[14:0], sdi};
else if(send_bits == 6'd32)
rd_data_neg[31:0] <= {rd_data_neg[30:0], sdi};
else ;
end
always @(*)
if(sel0 || sel3)
rd_data <= rd_data_pos;
else if(sel1 || sel2)
rd_data <= rd_data_neg;
else
rd_data <= 0;
数据发送部分,mode0和mode2在时钟沿来之前需要提前准备数据,这一段写的有点奇怪,但是找不到更好的描述方法只有这样了。
//send data
//mode0
always @(negedge sclk or posedge start_pos)
if(start_pos) begin
if(lsb_en)
sdo0 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo0 <= wr_data[7];
else if(send_bits == 6'd16)
sdo0 <= wr_data[15];
else if(send_bits == 6'd32)
sdo0 <= wr_data[31];
end
end
else if(sel0) begin
if(lsb_en)
sdo0 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo0 <= wr_data[7];
else if(send_bits == 6'd16)
sdo0 <= wr_data[15];
else if(send_bits == 6'd32)
sdo0 <= wr_data[31];
end
end
else
sdo0 <= 0;
//mode1
always @(posedge sclk)
if(sel1) begin
if(lsb_en)
sdo1 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo1 <= wr_data[7];
else if(send_bits == 6'd16)
sdo1 <= wr_data[15];
else if(send_bits == 6'd32)
sdo1 <= wr_data[31];
end
end
else
sdo1 <= 0;
//mode2
always @(posedge sclk or posedge start_pos)
if(start_pos) begin
if(lsb_en)
sdo2 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo2 <= wr_data[7];
else if(send_bits == 6'd16)
sdo2 <= wr_data[15];
else if(send_bits == 6'd32)
sdo2 <= wr_data[31];
end
end
else if(sel2) begin
if(lsb_en)
sdo2 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo2 <= wr_data[7];
else if(send_bits == 6'd16)
sdo2 <= wr_data[15];
else if(send_bits == 6'd32)
sdo2 <= wr_data[31];
end
end
else
sdo2 <= 0;
//mode3
always @(negedge sclk)
if(sel3) begin
if(lsb_en)
sdo3 <= wr_data[0];
else begin
if(send_bits == 6'd8)
sdo3 <= wr_data[7];
else if(send_bits == 6'd16)
sdo3 <= wr_data[15];
else if(send_bits == 6'd32)
sdo3 <= wr_data[31];
end
end
else
sdo3 <= 0;
always @(*)
case (ctrl[4:3])
2'b00: sdo <= start ? sdo0 : 1'b0;
2'b01: sdo <= start ? sdo1 : 1'b0;
2'b10: sdo <= start ? sdo2 : 1'b0;
2'b11: sdo <= start ? sdo3 : 1'b0;
endcase
综合出来的一个触发器,表示很新鲜。
最后就是一个三态逻辑,一般三态逻辑放到顶层,我原本是放在一个gpio的模块里面。这里给方便大家看,修改了一下,就放到这里了。
assign sdi = bidiroe ? mosi : miso;
assign mosi = bidiroe ? (sdo_en ? sdo : 1'bz): sdo;
最后上仿真,由于是一个从cortexm0控制的,仿真文件是操作的寄存器。仿真文件看了没多大意义,我简单说一下,ctrl[0]是start信号,在拉高一个周期后就会自动清零。下面这个是测试三线模式,半双工模式,输入输出分时复用一个线,sdo_en忘了放上来了,sdo_en为1输出,否则输入。由于找了一下没找到仿真模型,就没挂,所以sdo_en为0的时候一直是高阻。
链接:代码下载地址(注释较少)