一、概述
在我们进行日常开发时,不管是进行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 ,
//IIC
input [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)begin
state_c <=IDLE ;
end
else begin
state_c <= state_n;
end
end
//第二段:组合逻辑判断状态转移条件,描述状态转移规律
always @(*) begin
case(state_c)
IDLE : begin
if(idle2start)
state_n = START;
else
state_n = state_c;
end
START : begin
if(start2trans)
state_n = TRANS;
else
state_n = state_c;
end
TRANS : begin
if(trans2ack)
state_n = ACK;
else
state_n = state_c;
end
ACK : begin
if(ack2stop)
state_n = STOP;
else
state_n = state_c;
end
STOP : begin
if(stop2idle)
state_n = IDLE;
else
state_n = state_c;
end
default : ;
endcase
end
assign 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)begin
scl_cnt <= 'd0;
end
else if(add_scl_cnt)begin
if(end_scl_cnt)begin
scl_cnt <= 'd0;
end
else 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;
else
iic_scl <=1'b1;
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 =(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)begin
din_r <= 'd0;
end
else if(send_en)begin
din_r <= din;
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
iic_sda_en <= 'd0;
iic_sda_out <= 'd1;
end
else begin
case (state_c)
IDLE :begin//修改
iic_sda_en <= 'd0;
iic_sda_out <= 1'b1;
end
START :begin
iic_sda_en <= 1'b1;
if((iic_scl==1) && (scl_cnt>=(CNT_MAX/2+CNT_MAX/4)))
iic_sda_out <= 1'b0;
else
iic_sda_out <= 1'b1;
end
TRANS :begin
iic_sda_en <= 1'b1;
if((iic_scl==0) && (scl_cnt <=CNT_MAX>>2))
iic_sda_out <= din_r[7-cnt_bit];
end
ACK :begin
iic_sda_en <= 1'b0;
iic_sda_out <= 1'bz;
end
STOP :begin
iic_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;
end
default:;
endcase
end
end
endmodule
2、从机模块代码的编写
这里和主机模块一样也是新建一个iic_slave.v文件,如下:
//---------<模块及端口声名>-------------------------------------------
module iic_slave(
input clk ,
input rst_n ,
//IIC
input iic_scl ,
inout iic_sda ,
//uart_tx
output 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)begin
state_c <= IDLE;
end
else begin
state_c <= state_n;
end
end
//第二段:组合逻辑判断状态转移条件,描述状态转移规律
always @(*) begin
case(state_c)
IDLE : begin
if(idle2start)
state_n<=START ;
else
state_n<= state_c;
end
START : begin
if(start2trans)
state_n<=TRANS ;
else
state_n<= state_c;
end
TRANS : begin
if(trans2ack)
state_n<=ACK ;
else
state_n<= state_c;
end
ACK : begin
if(ack2stop)
state_n<=STOP ;
else
state_n<= state_c;
end
STOP : begin
if(stop2idle)
state_n<=IDLE ;
else
state_n<= state_c;
end
default : 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)begin
iic_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)begin
iic_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)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 =(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)begin
iic_sda_en <= 'd0;
iic_sda_out <= 'd1;
end
else if(state_c==ACK)begin
iic_sda_en <= 1'b1;
iic_sda_out <= 1'b0;
end
else begin
iic_sda_en <= 'd0;
iic_sda_out <= 'd0;
end
end
//数据类型转换,串转并
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dout_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)begin
dout <= 'd0;
end
else if(state_c==STOP)begin
dout <=dout_r;
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
dout_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/1ps
module 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)begin
data_in = {$random}%256;
send_en = 1'b1;
#CLOCK_CYCLE;
send_en = 1'b0;
wait(iic_master_inst.stop2idle);
#500;
end
#2000;
$stop;
end
endmodule
5、波形仿真
通过观察IIC仿真波形图中的输入和输出数据结果可知,两者是一样的,说明我们设计的IIC主机和从机模块没有问题。
6、下板验证
通过串口调试助手我们可以看到数据正常交互。设计完成。