FPGA开发——IIC实现简单的串口回环
一、概述
在我们进行日常开发时,不管是进行MCU、单片机、还是FPGA,都会使用到IIC通信协议。采用串行总线可以简化系统硬件结构、减小系统体积、提高系统可靠性。常 用的串行总线有单总线(1-Wire Bus)、IIC(Inter-Integrated Circuit)、SPI(Serial Peripheral Interface)等。 IIC 总线是 Phlips 公司推出的一种同步串行总线,是一种支持多主机多从机 系统、总线仲裁以及高低速器件同步功能的高性能串行总线技术。
在前面学习串口通信的时候,我们实现了数据回环的设计,现在我们学到了IIC通信协议,我们也来进行一下简单的IIC数据回环的设计,所谓的IIC数据回环设计就是在使用我们设计出的串口的基础之上,编写IIC的主机和从机,按照IIC通信协议的要求,进行一个数据的交互,从而实现IIC的数据回环实现。今天这篇文章的目的就是为了让我们熟悉一下IIC的通信协议。
二、框架构造
1、总体系统框架
在框图中可以看到,我们实现的思路就是通过串口接收和发送模块实现与PC机的交互,从而实现数据的回环,而IIC的作用就是通过对于接收模块接收到的数据进行处理,最后把数据传递给发送模块。
2、IIC主机模块
图中我们把SDA分成了三根线,在代码端口定义中我们对于sda_oen,sda_out,sda_in进行了处理,最后只使用了一根线。
3、从机模块
因为我们在主机中对于SDA线进行了处理,所以对与从机来说,只需要一根线接收就行,只不过为了方便观察我们写出来了。
三、工程实现
1、IIC主机模块的代码编写
新建一个iic_master.v文件,如下:这里的输入数据是来自串口接收模块接收到的数据
//---------<模块及端口声名>-------------------------------------------
module iic_master( input clk ,input rst_n ,//IICinput [7:0] din ,input send_en ,//iic_slave从机数据output reg iic_scl ,inout iic_sda
);
//---------<参数定义>------------------------------------------------
parameter CLK =50_000_000 ;
parameter SPEED =100_000;//传输速率
//---------<内部信号定义>--------------------------------------------
parameter IDLE =5'b000_01 ,START =5'b000_10 ,TRANS =5'b001_00 ,ACK =5'b010_00 ,STOP =5'b100_00 ;parameter CNT_MAX =CLK/SPEED;reg iic_sda_out;
reg iic_sda_en ;
wire iic_sda_in ;reg [9:0] scl_cnt;//SCL时钟计数器
wire add_scl_cnt;
wire end_scl_cnt;reg [3:0] cnt_bit ;
wire add_cnt_bit;
wire end_cnt_bit;reg iic_scl_r;
wire nedge;
reg [4:0] state_c ;//现态
reg [4:0] state_n ;//次态wire idle2start ;
wire start2trans ;
wire trans2ack ;
wire ack2stop ;
wire stop2idle ;reg [7:0] din_r;//输入数据缓存//IIC_SDA描述
assign iic_sda =iic_sda_en ? iic_sda_out : 1'bz;
assign iic_sda_in =iic_sda;
//第一段:同步时序描述状态转移
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginstate_c <=IDLE ;end else begin state_c <= state_n;end
end//第二段:组合逻辑判断状态转移条件,描述状态转移规律
always @(*) begincase(state_c)IDLE : beginif(idle2start)state_n = START;elsestate_n = state_c; endSTART : beginif(start2trans)state_n = TRANS;elsestate_n = state_c; endTRANS : beginif(trans2ack)state_n = ACK;elsestate_n = state_c; endACK : beginif(ack2stop)state_n = STOP;elsestate_n = state_c; endSTOP : beginif(stop2idle)state_n = IDLE;elsestate_n = state_c; enddefault : ;endcase
endassign idle2start = state_c == IDLE && send_en;
assign start2trans = state_c == START && end_scl_cnt;
assign trans2ack = state_c == TRANS && end_cnt_bit;
assign ack2stop = state_c == ACK && end_scl_cnt;
assign stop2idle = state_c == STOP && end_scl_cnt;//第三段:描述状态输出(组合逻辑/时序逻辑)
//scl_cnt
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginscl_cnt <= 'd0;end else if(add_scl_cnt)begin if(end_scl_cnt)begin scl_cnt <= 'd0;endelse begin scl_cnt <= scl_cnt + 1'b1;end end
end assign add_scl_cnt = state_c!=IDLE ;
assign end_scl_cnt = add_scl_cnt && scl_cnt ==CNT_MAX-1 ;
//IIC_SCL时钟
always @(posedge clk or negedge rst_n)begin if(!rst_n)iic_scl <=1'b1;else if(state_c !=IDLE && scl_cnt<=CNT_MAX/2-1)iic_scl <= 1'b0;elseiic_scl <=1'b1;
end //bit计数器
always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_bit <= 'd0;end else if(add_cnt_bit)begin if(end_cnt_bit)begin cnt_bit <= 'd0;endelse begin cnt_bit <= cnt_bit + 1'b1;end end
end assign add_cnt_bit =(state_c ==TRANS) && end_scl_cnt ;
assign end_cnt_bit = add_cnt_bit && cnt_bit ==8-1 ;//din_r
always @(posedge clk or negedge rst_n)begin if(!rst_n)begindin_r <= 'd0;end else if(send_en)begin din_r <= din;end
endalways @(posedge clk or negedge rst_n)begin if(!rst_n)beginiic_sda_en <= 'd0;iic_sda_out <= 'd1;end else begincase (state_c)IDLE :begin//修改iic_sda_en <= 'd0;iic_sda_out <= 1'b1;endSTART :beginiic_sda_en <= 1'b1;if((iic_scl==1) && (scl_cnt>=(CNT_MAX/2+CNT_MAX/4)))iic_sda_out <= 1'b0;elseiic_sda_out <= 1'b1;endTRANS :beginiic_sda_en <= 1'b1;if((iic_scl==0) && (scl_cnt <=CNT_MAX>>2))iic_sda_out <= din_r[7-cnt_bit]; endACK :beginiic_sda_en <= 1'b0;iic_sda_out <= 1'bz;endSTOP :beginiic_sda_en <= 1'b1;if(scl_cnt<=CNT_MAX>>2)iic_sda_out <= 1'b0;else if((iic_scl==1) && (scl_cnt>=(CNT_MAX/2+CNT_MAX/4)))iic_sda_out <= 1'b1;enddefault:;endcaseendend
endmodule
2、从机模块代码的编写
这里和主机模块一样也是新建一个iic_slave.v文件,如下:
//---------<模块及端口声名>-------------------------------------------
module iic_slave( input clk ,input rst_n ,//IICinput iic_scl ,inout iic_sda ,//uart_txoutput reg [7:0] dout ,//串口发送模块输入数据output reg dout_vld //串口发送模块输入数据标志位
);
//---------<参数定义>------------------------------------------------
parameter CLK =50_000_000 ;
parameter SPEED =100_000;//传输速率
//---------<内部信号定义>--------------------------------------------
parameter IDLE =5'b000_01 ,START =5'b000_10 ,TRANS =5'b001_00 ,ACK =5'b010_00 ,STOP =5'b100_00 ;parameter CNT_MAX =CLK/SPEED;reg iic_scl_r1;//同步打拍
reg iic_scl_r2;
reg iic_sda_r1;
reg iic_sda_r2;wire sda_nedge;//sda信号检测
wire sda_pedge;wire scl_nedge;//scl信号检测
wire scl_pedge;reg iic_sda_en;//对于sda线进行一个描述(三台门)
reg iic_sda_out;
wire iic_sda_in;reg [3:0] cnt_bit;
wire add_cnt_bit;
wire end_cnt_bit;reg [9:0] scl_cnt;//SCL时钟计数器
wire add_scl_cnt;
wire end_scl_cnt; reg [4:0] state_c ;//现态
reg [4:0] state_n ;//次态wire idle2start ;
wire start2trans ;
wire trans2ack ;
wire ack2stop ;
wire stop2idle ;reg [7:0] dout_r;//输出数据寄存器
//sda信号描述
assign iic_sda = iic_sda_en ? iic_sda_out :1'bz;
assign iic_sda_in = iic_sda;
//第一段:同步时序描述状态转移
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginstate_c <= IDLE;end else begin state_c <= state_n;end
end//第二段:组合逻辑判断状态转移条件,描述状态转移规律
always @(*) begincase(state_c)IDLE : beginif(idle2start)state_n<=START ;elsestate_n<= state_c; endSTART : beginif(start2trans)state_n<=TRANS ;elsestate_n<= state_c; endTRANS : beginif(trans2ack)state_n<=ACK ;elsestate_n<= state_c; endACK : beginif(ack2stop)state_n<=STOP ;elsestate_n<= state_c; endSTOP : beginif(stop2idle)state_n<=IDLE ;elsestate_n<= state_c; enddefault : state_n <=IDLE ;endcase
end
assign idle2start = (state_c == IDLE ) && (iic_scl_r2 && sda_nedge);
assign start2trans = (state_c == START ) && (scl_nedge);
assign trans2ack = (state_c == TRANS ) && (end_cnt_bit);
assign ack2stop = (state_c == ACK ) && (scl_nedge);
assign stop2idle = (state_c == STOP ) && (iic_scl_r2 && sda_pedge);//第三段:描述状态输出(组合逻辑/时序逻辑)
//对于scl输入信号进行打拍
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginiic_scl_r1 <= 1'b1;iic_scl_r2 <= 1'b1;end else begin iic_scl_r1 <= iic_scl;iic_scl_r2 <= iic_scl_r1;end
end
assign scl_nedge= ~iic_scl_r1 && iic_scl_r2 ;
assign scl_pedge= iic_scl_r1 && ~iic_scl_r2 ;
//对于sda输入信号进行打拍
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginiic_sda_r1 <= 1'b1;iic_sda_r2 <= 1'b1;end else begin iic_sda_r1 <= iic_sda;iic_sda_r2 <= iic_sda_r1;end
end
assign sda_nedge= ~iic_sda_r1 && iic_sda_r2 ;
assign sda_pedge= iic_sda_r1 && ~iic_sda_r2 ;//bit计数器
always @(posedge clk or negedge rst_n)begin if(!rst_n)begincnt_bit <= 'd0;end else if(add_cnt_bit)begin if(end_cnt_bit)begin cnt_bit <= 'd0;endelse begin cnt_bit <= cnt_bit + 1'b1;end end
end assign add_cnt_bit =(state_c ==TRANS) && scl_nedge ;
assign end_cnt_bit = add_cnt_bit && cnt_bit ==8-1 ;//sda_en、sda_out
always @(posedge clk or negedge rst_n)begin if(!rst_n)beginiic_sda_en <= 'd0;iic_sda_out <= 'd1;end else if(state_c==ACK)beginiic_sda_en <= 1'b1;iic_sda_out <= 1'b0;endelse beginiic_sda_en <= 'd0;iic_sda_out <= 'd0;end
end //数据类型转换,串转并
always @(posedge clk or negedge rst_n)begin if(!rst_n)begindout_r <= 'd0;end else if(state_c==TRANS && iic_scl_r2)begin dout_r[7-cnt_bit] <=iic_sda_r2;end
end
always @(posedge clk or negedge rst_n)begin if(!rst_n)begindout <= 'd0;end else if(state_c==STOP)begin dout <=dout_r; end
endalways @(posedge clk or negedge rst_n)begin if(!rst_n)begindout_vld <= 'd0;end else begin dout_vld <=stop2idle; end
end
endmodule
关于串口接收和发送模块的代码在前面的文章中已经写过了,如果不知道怎么写的小伙伴直接去前面文章里面找就行。
3、顶层文件 的编写
通过顶层文件将串口接收模块和IIC主从机模块连接起来,从而实现数据回环。
新建top.v文件,如下:
module top (input clk ,input rst_n ,input rx ,output tx
);
wire [7:0] rx_dout;
wire rx_dout_vld;
wire busy;
wire iic_scl;
wire iic_sda;
wire [7:0] tx_din ;
wire tx_din_vld;
uart_rx uart_rx_inst(/*input */ .clk (clk ),/*input */ .rst_n (rst_n ), /*input */ .din_rx (rx ),/*output reg[7:0] */ .dout_data (rx_dout ),/*output reg */ .dout_flag (rx_dout_vld )
);iic_master iic_master_inst( /*input */.clk (clk ),/*input */.rst_n (rst_n ),/*input [7:0] */.din (rx_dout),/*input */.send_en (rx_dout_vld),//开始传输的使能信号/*output reg */.iic_scl (iic_scl),/*inout */.iic_sda (iic_sda)
); iic_slave u_iic_slave( /*input */.clk (clk ),/*input */.rst_n (rst_n ),/*input */.iic_scl (iic_scl),/*inout */.iic_sda (iic_sda),/*output reg [7:0] */.dout (tx_din ),/*output reg */.dout_vld (tx_din_vld)
); uart_tx uart_tx_inst(/*input */ .clk (clk ) ,/*input */ .rst_n (rst_n ) ,/*input [7:0]*/ .tx_din (tx_din ) ,//进入发送模块准备发送的数据/*input */ .tx_din_vld (tx_din_vld && ~busy ) ,//要发送数据的有效信号/*output reg */ .tx_dout (tx ) ,//串行发送出去的数据/*output reg */ .busy (busy ) //发送一字节完成信号
);
endmodule
4、IIC测试文件的编写
因为串口我们前面已经写了并且测试过,所以这里我们只需要进行IIC的主机和从机进行一个仿真就可以得出结果,只要在这里数据能够正常交互,IIC的数据回环就没啥问题。
新建一个iic_tb.v文件,如下:
`timescale 1ns/1psmodule iic_tb();//激励信号定义 reg tb_clk ;reg tb_rst_n ;reg [7:0] data_in ;reg send_en ;reg sda_in ;//内部信号定义 wire iic_scl ;wire iic_sda ;
//输出信号定义wire [7:0] r_data ;wire r_data_vld ;pullup(iic_sda);//时钟周期参数定义 parameter CLOCK_CYCLE = 20; //模块例化iic_master iic_master_inst( /*input */.clk (tb_clk ),/*input */.rst_n (tb_rst_n ),/*input [7:0] */.din (data_in ),/*input */.send_en (send_en ),//开始传输的使能信号/*output reg */.iic_scl (iic_scl ),/*inout */.iic_sda (iic_sda )
); iic_slave iic_slave_inst( /*input */.clk (tb_clk ),/*input */.rst_n (tb_rst_n ),//iic_master()/*input */.iic_scl (iic_scl ),.iic_sda (iic_sda ),//uart_tx()/*output reg */.dout (r_data ),/*output reg */.dout_vld (r_data_vld )
); //产生时钟initial tb_clk = 1'b0;always #(CLOCK_CYCLE/2) tb_clk = ~tb_clk;//产生激励initial begin tb_rst_n = 1'b1;data_in = 0;send_en = 0;sda_in = 1'b1;#(CLOCK_CYCLE*2);tb_rst_n = 1'b0;#(CLOCK_CYCLE*20);tb_rst_n = 1'b1;#3;repeat(10)begindata_in = {$random}%256;send_en = 1'b1;#CLOCK_CYCLE;send_en = 1'b0;wait(iic_master_inst.stop2idle);#500;end#2000;$stop;endendmodule
5、波形仿真
通过观察IIC仿真波形图中的输入和输出数据结果可知,两者是一样的,说明我们设计的IIC主机和从机模块没有问题。
6、下板验证
通过串口调试助手我们可以看到数据正常交互。设计完成。