目录
在黑金ax301开发板上使用IIC读取EEPROM 24LC04的数据。
fpga型号:EP4CE6F17C8
开发工具:Quartus ll 13.0 + Modelsim 10.1c
系统时钟:50MHZ
IIC时钟:250KHZ
两个模块:IIC驱动模块和IIC顶层模块
使用的ip核:pll
单次随机读时序图如下:
过程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2 字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;
(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(8) 主机接收到从机回传的应答信号后,开始接收从机传回的单字节数据;
(9) 数据接收完成后,主机产生一个时钟的高电平无应答信号;
(10) 主机向从机发送停止信号,单字节读操作完成。
IIC驱动模块:该模块的主要功能是通过IIC读取EEPROM中的数据。
EEPROM器件地址为AOH,该芯片的字节地址为8位。
module i2c_driver (
//系统接口
input rst_n, //复位信号,低电平有效
input i2c_clk, //i2c系统时钟,250khz
//i2c物理接口
output reg i2c_scl, //串行时钟信号
inout i2c_sda, //串行数据信号
//用户接口
input [7:0] pi_data, //写入i2c的数据
input i2c_start, //i2c开始信号
input [15:0] word_addr, //字节地址
input i2c_num, //1表示字节地址为16位,0表示字节地址为8位
output reg i2c_end, //i2c结束信号
output reg [7:0] po_data //接收的数据
);
parameter DEVICE_ADDR = 7'b1_010_000;//器件地址
localparam IDLE = 0, //空闲状态
START = 1, //开始状态
SEND_ADDR_1 = 2,//发送器件地址+写信号
ACK_1 = 3, //从机响应
SEND_ADDR_H = 4,//发送高八位字节地址
ACK_2 = 5, //从机响应
SEND_ADDR_L = 6,//发送低八位字节地址
ACK_3 = 7, //从机响应
START_2 = 8, //第二个开始状态
SEND_ADDR_R = 9,//发送器件地址+读信号
ACK_4 = 10, //从机响应
READ_DATA = 11, //读数据状态
NACK = 12, //主机非应答状态
STOP = 13; //停止状态
reg [1:0] i2c_clk_cnt;//分频计数器
reg i2c_clk_cnt_en; //i2c系统时钟分频允许位
reg [2:0] cnt_data; //数据位计数
reg sda_en; //三态门开关
reg sda_out; //sda输出
wire sda_in; //sda输入
reg ack_flag; //响应标志信号
reg [7:0] po_data_re;
//状态机
reg [3:0] cur_state; //当前状态
reg [3:0] next_state; //下一个状态
wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //7位设备地址+1位写标志位
wire [7:0] addr_r = {DEVICE_ADDR,1'b1}; //7位设备地址+1位读标志位
//i2c_clk_cnt
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_clk_cnt <= 0;
else if(i2c_clk_cnt_en) //当i2c系统时钟分频允许位为高电平时,i2c_clk_cnt自加1
i2c_clk_cnt <= i2c_clk_cnt + 1'b1;
else
i2c_clk_cnt <= 0;
end
//i2c_clk_cnt_en
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_clk_cnt_en <= 0;
else if(i2c_start)//i2c开始信号来后拉高
i2c_clk_cnt_en <= 1;
else if((cur_state == STOP && cnt_data == 3'd3 && i2c_clk_cnt == 2'd3) || (cur_state == IDLE && !i2c_start))//当STOP状态结束或者空闲状态没有出现i2c开始信号时拉低i2c_clk_cnt_en信号
i2c_clk_cnt_en <= 0;
else
i2c_clk_cnt_en <= i2c_clk_cnt_en;
end
//i2c_sda
assign sda_in = i2c_sda; //i2c_sda作为输入
assign i2c_sda = sda_en ? (sda_out ? 1'bz : 1'b0) : 1'bz; //i2c_sda作为输出
//三段式状态机第一段同步时序描述状态转移
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= IDLE;
else
cur_state <= next_state;
end
//三段式状态机第二段组合逻辑判断状态转移条件,描述状态转移规律
always@(*)begin
case(cur_state)
IDLE:
if(i2c_start)
next_state = START;
else
next_state = IDLE;
START: //开始状态,4个i2c系统时钟周期
if(i2c_clk_cnt == 2'd3)
next_state = SEND_ADDR_1;
else
next_state = START;
SEND_ADDR_1: //发送器件地址+写信号
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_1;
else
next_state = SEND_ADDR_1;
ACK_1: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)begin
if(i2c_num) //判断字节地址位16位还是8位
next_state = SEND_ADDR_H; //字节地位为16位,先发高8位字节地址
else
next_state = SEND_ADDR_L; //字节地址位8位,跳转到发低8位状态
end
else if(i2c_clk_cnt == 2'd3) //从机未响应返回空闲状态
next_state <= IDLE;
else
next_state = ACK_1;
SEND_ADDR_H: //发送高八位字节地址
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_2;
else
next_state = SEND_ADDR_H;
ACK_2: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state = SEND_ADDR_L; //从机响应,转移到发送低8位字节地址状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_2;
SEND_ADDR_L: //发送低八位地址
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_3;
else
next_state = SEND_ADDR_L;
ACK_3:
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state = START_2; //从机响应,转移到开始状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_3;
START_2: //第二个开始状态
if(i2c_clk_cnt == 2'd3)
next_state <= SEND_ADDR_R;
else
next_state <= START_2;
SEND_ADDR_R: //发送器件地址+读信号
if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd7)
next_state <= ACK_4;
else
next_state <= SEND_ADDR_R;
ACK_4: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state <= READ_DATA;
else if(i2c_clk_cnt == 2'd3)
next_state <= IDLE;
else
next_state <= ACK_4;
READ_DATA: //读数据
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = NACK;
else
next_state = READ_DATA;
NACK:
if(i2c_clk_cnt == 2'd3)
next_state = STOP; //主机非应答状态,转移停止状态
else
next_state = NACK;
STOP: //停止状态
if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd3)
next_state <= IDLE;
else
next_state = STOP;
default:next_state <= IDLE;
endcase
end
//三段式状态机第三段
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)begin //初始状态,i2c_sda为输出态,sda_en为高电平,sda_out为高电平
sda_en <= 1'b1;
sda_out <= 1'b1;
cnt_data <= 3'd0;
i2c_end <= 1'b0;
po_data_re <= 8'd0;
po_data <= 8'd0;
end
else begin
i2c_end <= 1'b0;
case(cur_state)
IDLE:begin //空闲状态
sda_en <= 1'b1; //sda位输出状态
sda_out <= 1'b1; //总线拉高
end
START: begin
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= addr_w[7]; //此时sda_scl为下降沿,改变sda
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0; //sda在scl高电平是出现下降沿,i2c开始
end
end
SEND_ADDR_1:begin //发送器件地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //8位数据发送完毕
cnt_data <= 3'd0;
sda_en <= 1'b0; //8位数据发送完毕,拉低sda_en,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data + 1'b1;
sda_out <= addr_w[6 - cnt_data];
sda_en <= 1'b1;
end
end
end
ACK_1:begin
if(i2c_clk_cnt == 2'd3)begin
if(i2c_num == 1)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= word_addr[15]; //i2c_num为1,字节位为16位
end
else begin
sda_en <= 1;
sda_out <= word_addr[7]; //i2c_num为0,字节为8位
end
end
end
SEND_ADDR_H:begin //发送高8位字节地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //8位数据发送完毕
cnt_data <= 3'd0;
sda_en <= 1'b0; //8位数据发送完毕,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data +1'b1;
sda_out <= word_addr[14 - cnt_data];
sda_en <= 1'b1;
end
end
end
ACK_2:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= word_addr[7];
end
SEND_ADDR_L: //发送低8位字节地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //低8位字节地址发送完毕
cnt_data <= 1'b0;
sda_en <= 1'b0; //8位数据发送完毕,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data + 1'b1;
sda_out <= word_addr[6 - cnt_data];
sda_en <= 1'b1;
end
end
ACK_3:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= 1;
end
START_2:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1;
sda_out <= addr_r[7];
end
else if(i2c_clk_cnt == 2'd1)begin //注意需要在scl为高电平时,sda出现下降沿
sda_en <= 1;
sda_out <= 0;
end
SEND_ADDR_R:
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin
cnt_data <= 0;
sda_en <= 0; //数据发送完成,拉低en,等待从机响应
end
else begin
cnt_data <= cnt_data + 1'b1;
sda_out <= addr_r[6 - cnt_data];
sda_en <= 1;
end
end
ACK_4:
sda_en <= 0; //下一个状态是读数据状态,因此拉低en
READ_DATA:begin
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //8位数据读取完毕,拉高en
cnt_data <= 1'b0;
sda_en <= 1;
sda_out <= 1;
po_data <= po_data_re;
end
else
cnt_data <= cnt_data + 1'b1;
end
else if(i2c_clk_cnt == 2'd1)
po_data_re[7-cnt_data] <= sda_in;
end
NACK:begin //主机非应答状态,sda_out拉低,方便停止状态出现上升沿
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1;
sda_out <= 0;
end
end
STOP:begin
if(i2c_clk_cnt == 2'd2 && cnt_data == 3'd0)begin//拉高信号作为终止信号
sda_en <= 1;
sda_out <= 1;
end
else if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd3)begin //送完了终止信号且延时一段时间发送I2C结束信号
i2c_end <= 1'b1;
cnt_data <= 0;
end
else
cnt_data <= cnt_data + 1'b1;
end
end
default:;
endcase
end
end
//生成i2c_scl
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_scl <= 1; //空闲状态scl为高电平
else if(cur_state != STOP)begin
if(i2c_clk_cnt == 2'd2)
i2c_scl <= 0;
else if(i2c_clk_cnt == 2'd0)
i2c_scl <= 1;
end
else
i2c_scl <= 1;
end
//生成从机响应信号
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
ack_flag <= 0;
else
case(cur_state)
ACK_1,ACK_2,ACK_3,ACK_4:
if(i2c_clk_cnt == 2'd1 && !sda_in)
//if(i2c_clk_cnt == 2'd1) //仿真时默认从机响应
ack_flag <= 1'b1;
else if(i2c_clk_cnt == 2'd3)
ack_flag <= 1'b0;
default:ack_flag <= 1'b0;
endcase
end
endmodule
IIC顶层模块:该模块功能是生成IIC开始信号和字节地址(0006H)
module i2c_read(
input clk,
input rst_n,
output i2c_scl, //串行时钟信号
inout i2c_sda //串行数据信号
);
wire i2c_clk;
wire i2c_end;
wire [7:0] po_data;
pll_250k pll_250k_inst (
.inclk0 ( clk ),
.c0 ( i2c_clk )
);
reg [15:0] cnt;
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 0;
else if (cnt < 1000)
cnt <= cnt + 1'b1;
else
cnt <= cnt;
wire i2c_start = (cnt >50 && cnt < 300) ? 1'b1 : 1'b0;
i2c_driver i2c_driver(
//系统接口
.rst_n(rst_n), //复位信号,低电平有效
.i2c_clk(i2c_clk), //i2c系统时钟,250khz
//i2c物理接口
.i2c_scl(i2c_scl), //串行时钟信号
.i2c_sda(i2c_sda), //串行数据信号
//用户接口
.pi_data(8'hab), //写入i2c的数据
.i2c_start(i2c_start), //i2c开始信号
.word_addr(16'h0006),
.i2c_num(0), //1表示字节地址为16位,0表示字节地址为8位
.i2c_end(i2c_end), //i2c结束信号
.po_data(po_data)
);
endmodule
????????由于我没有找到IIC的仿真模型,因此在等待从机响应时默认从机响应(在i2c_driver模块的代码中把那个注释去掉),读取的数据是高阻态。
仿真代码:
`timescale 1ns/1ns
module i2c_tb;
reg clk;
reg rst_n;
wire i2c_scl;
wire i2c_sda;
i2c_read i2c_read(
.clk,
.rst_n,
.i2c_scl, //串行时钟信号
.i2c_sda //串行数据信号
);
initial clk = 0;
always #10 clk = !clk;
initial begin
rst_n = 0;
#65;
rst_n = 1;
#1_000_000;
$stop;
end
endmodule
?波形图:
????????EEPROM芯片中已经提前在06H上写入数据56H,我们可以从图中看到读取的数据为56H,因此上板成功。
在黑金ax301开发板上使用IIC顺序读取EEPROM 24LC04的数据。并通过uart232串口把读取的数据发给串口调试助手。(中间将把EEPROM中读取的数据存到fifo中,然后通过处理后从fifo中读取数据传给uart232发送模块)
fpga型号:EP4CE6F17C8
开发工具:Quartus ll 13.0 + Modelsim 10.1c+串口调试助手
系统时钟:50MHZ
IIC时钟:250KHZ
uart232接收波特率:115200
四个模块:IIC驱动模块、uart控制模块、uart发送数据模块和顶层模块。
使用的ip核:pll和fifo
时序图如下:
过程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写 入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2 字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,主机再次向从机发送一个起始信号;
(7) 主机向从机发送控制命令,读写控制位设置为高电平,表示对从机进行数据读操作;
(8) 主机接收到从机回传的应答信号后,开始接收从机传回的第一个单字节数据;
(9) 数据接收完成后,主机产生应答信号回传给从机,从机接收到应答信号开始下一字节数据的传输,若数据接收完成,执行下一操作步骤;若数据接收未完成,在此执行步骤(9);
(10) 主机产生一个时钟的高电平无应答信号;
(11) 主机向从机发送停止信号,顺序读操作完成。
IIC驱动模块:该模块使用三段式段式状态机,主要功能是通过i2c顺序从EEPROM 24LC04(器件地址为A0H)中读取数据(数据多少可以指定)。
module i2c_driver (
//系统接口
input rst_n, //复位信号,低电平有效
input i2c_clk, //i2c系统时钟,250khz
//i2c物理接口
output reg i2c_scl, //串行时钟信号
inout i2c_sda, //串行数据信号
//用户接口
input i2c_start, //i2c开始信号
input [15:0] word_addr, //字节地址
input [7:0] i2c_num, //1表示字节地址为16位,0表示字节地址为8位
output reg i2c_end, //i2c结束信号
output reg [7:0] po_data, //接收的数据
output reg po_data_flag //数据标志位
);
parameter DEVICE_ADDR = 7'b1_010_000,//器件地址
DATA_NUM = 8'd10; //读取的字节数据的个数
localparam IDLE = 0, //空闲状态
START = 1, //开始状态
SEND_ADDR_1 = 2,//发送器件地址+写信号
ACK_1 = 3, //从机响应
SEND_ADDR_H = 4,//发送高八位字节地址
ACK_2 = 5, //从机响应
SEND_ADDR_L = 6,//发送低八位字节地址
ACK_3 = 7, //从机响应
START_2 = 8, //第二个开始状态
SEND_ADDR_R = 9,//发送器件地址+读信号
ACK_4 = 10, //从机响应
READ_DATA = 11, //读数据状态
ACK_5 = 12, //主机响应状态
NACK = 13, //主机非应答状态
STOP = 14; //停止状态
reg [1:0] i2c_clk_cnt;//分频计数器
reg i2c_clk_cnt_en; //i2c系统时钟分频允许位
reg [2:0] cnt_data; //数据位计数
reg sda_en; //三态门开关
reg sda_out; //sda输出
wire sda_in; //sda输入
reg ack_flag; //响应标志信号
reg [7:0] po_data_re; //输出数据寄存
reg [7:0] data_num_cnt; //读取数据个数计数
//状态机
reg [3:0] cur_state; //当前状态
reg [3:0] next_state; //下一个状态
wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //7位设备地址+1位写标志位
wire [7:0] addr_r = {DEVICE_ADDR,1'b1}; //7位设备地址+1位读标志位
//i2c_clk_cnt
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_clk_cnt <= 0;
else if(i2c_clk_cnt_en) //当i2c系统时钟分频允许位为高电平时,i2c_clk_cnt自加1
i2c_clk_cnt <= i2c_clk_cnt + 1'b1;
else
i2c_clk_cnt <= 0;
end
//i2c_clk_cnt_en
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_clk_cnt_en <= 0;
else if(i2c_start)//i2c开始信号来后拉高
i2c_clk_cnt_en <= 1;
else if((cur_state == STOP && cnt_data == 3'd3 && i2c_clk_cnt == 2'd3) || (cur_state == IDLE && !i2c_start))//当STOP状态结束或者空闲状态没有出现i2c开始信号时拉低i2c_clk_cnt_en信号
i2c_clk_cnt_en <= 0;
else
i2c_clk_cnt_en <= i2c_clk_cnt_en;
end
//i2c_sda
assign sda_in = i2c_sda; //i2c_sda作为输入
assign i2c_sda = sda_en ? (sda_out ? 1'bz : 1'b0) : 1'bz; //i2c_sda作为输出
//三段式状态机第一段同步时序描述状态转移
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= IDLE;
else
cur_state <= next_state;
end
//三段式状态机第二段组合逻辑判断状态转移条件,描述状态转移规律
always@(*)begin
case(cur_state)
IDLE:
if(i2c_start) //开始信号来的时候,状态跳到开始状态
next_state = START;
else
next_state = IDLE;
START: //开始状态,4个i2c系统时钟周期
if(i2c_clk_cnt == 2'd3)
next_state = SEND_ADDR_1;
else
next_state = START;
SEND_ADDR_1: //发送器件地址+写信号
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_1;
else
next_state = SEND_ADDR_1;
ACK_1: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)begin
if(i2c_num) //判断字节地址位是16位还是8位
next_state = SEND_ADDR_H; //字节地位为16位,跳转到发高8位字节地址状态
else
next_state = SEND_ADDR_L; //字节地址位8位,跳转到发低8位字节地址状态
end
else if(i2c_clk_cnt == 2'd3) //从机未响应返回空闲状态
next_state <= IDLE;
else
next_state = ACK_1;
SEND_ADDR_H: //发送高八位字节地址
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_2;
else
next_state = SEND_ADDR_H;
ACK_2: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state = SEND_ADDR_L; //从机响应,转移到发送低8位字节地址状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_2;
SEND_ADDR_L: //发送低八位地址
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_3;
else
next_state = SEND_ADDR_L;
ACK_3:
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state = START_2; //从机响应,转移到开始状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_3;
START_2: //第二个开始状态
if(i2c_clk_cnt == 2'd3)
next_state <= SEND_ADDR_R;
else
next_state <= START_2;
SEND_ADDR_R: //发送器件地址+读信号
if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd7)
next_state <= ACK_4;
else
next_state <= SEND_ADDR_R;
ACK_4: //从机响应
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state <= READ_DATA;
else if(i2c_clk_cnt == 2'd3)
next_state <= IDLE;
else
next_state <= ACK_4;
READ_DATA: //读数据
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)begin
if(data_num_cnt == DATA_NUM - 1)
next_state <= NACK; //数据全部读取完,转移到主机非应答状态
else
next_state <= ACK_5; //数据并未全部读取完,转移至主机响应状态
end
else
next_state = READ_DATA;
ACK_5: //主句响应状态
if(i2c_clk_cnt == 2'd3)
next_state <= READ_DATA;
else
next_state <= ACK_5;
NACK:
if(i2c_clk_cnt == 2'd3)
next_state = STOP; //主机非应答状态,转移停止状态
else
next_state = NACK;
STOP: //停止状态
if(i2c_clk_cnt == 2'd3 && cnt_data == 3'd3)
next_state <= IDLE;
else
next_state = STOP;
default:next_state <= IDLE;
endcase
end
//三段式状态机第三段
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)begin //初始状态,i2c_sda为输出态,sda_en为高电平,sda_out为高电平
sda_en <= 1'b1;
sda_out <= 1'b1;
cnt_data <= 3'd0;
i2c_end <= 1'b0;
po_data_re <= 8'd0;
po_data <= 8'd0;
data_num_cnt <= 8'd0;
po_data_flag <= 1'd0;
end
else begin
i2c_end <= 1'b0;
case(cur_state)
IDLE:begin //空闲状态
sda_en <= 1'b1; //sda位输出状态
sda_out <= 1'b1; //总线拉高
end
START: begin
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1'b1;
sda_out <= addr_w[7]; //此时sda_scl为下降沿,改变sda
end
else begin
sda_en <= 1'b1;
sda_out <= 1'b0; //sda在scl高电平是出现下降沿,i2c开始
end
end
SEND_ADDR_1:begin //发送器件地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //8位数据发送完毕
cnt_data <= 3'd0;
sda_en <= 1'b0; //8位数据发送完毕,拉低sda_en,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data + 1'b1;
sda_out <= addr_w[6 - cnt_data];
sda_en <= 1'b1;
end
end
end
ACK_1:begin
if(i2c_clk_cnt == 2'd3)begin
if(i2c_num == 1)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= word_addr[15]; //i2c_num为1,字节位为16位
end
else begin
sda_en <= 1;
sda_out <= word_addr[7]; //i2c_num为0,字节为8位
end
end
end
SEND_ADDR_H:begin //发送高8位字节地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //8位数据发送完毕
cnt_data <= 3'd0;
sda_en <= 1'b0; //8位数据发送完毕,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data +1'b1;
sda_out <= word_addr[14 - cnt_data];
sda_en <= 1'b1;
end
end
end
ACK_2:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= word_addr[7];
end
SEND_ADDR_L: //发送低8位字节地址
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //低8位字节地址发送完毕
cnt_data <= 1'b0;
sda_en <= 1'b0; //8位数据发送完毕,等待从机响应
end
else begin //发送完一位数据
cnt_data <= cnt_data + 1'b1;
sda_out <= word_addr[6 - cnt_data];
sda_en <= 1'b1;
end
end
ACK_3:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= 1;
end
START_2:
if(i2c_clk_cnt == 2'd3)begin
sda_en <= 1;
sda_out <= addr_r[7];
end
else if(i2c_clk_cnt == 2'd1)begin //注意需要在scl为高电平时,sda出现下降沿
sda_en <= 1;
sda_out <= 0;
end
SEND_ADDR_R:
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin
cnt_data <= 0;
sda_en <= 0; //数据发送完成,拉低en,等待从机响应
end
else begin
cnt_data <= cnt_data + 1'b1;
sda_out <= addr_r[6 - cnt_data];
sda_en <= 1;
end
end
ACK_4:
sda_en <= 0; //下一个状态是读数据状态,因此拉低en
READ_DATA:begin
if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd7)begin //1个8位数据读取完毕
if(data_num_cnt < DATA_NUM - 1)begin //字节数据并未全部读取完,跳转至主机应答状态,sda_en拉高,sda_out拉低
cnt_data <= 1'b0;
sda_en <= 1;
sda_out <= 0;
po_data <= po_data_re;
po_data_flag <= 1;
end
else begin //字节数据全部读取完成,跳转至主机非应答状态,sda_en拉高,sda_out拉高
cnt_data <= 1'b0;
sda_en <= 1;
sda_out <= 1;
po_data <= po_data_re;
po_data_flag <= 1;
end
end
else
cnt_data <= cnt_data + 1'b1;
end
else if(i2c_clk_cnt == 2'd1)
po_data_re[7-cnt_data] <= sda_in;
end
ACK_5:begin //主机响应状态,sda_out为低电平
po_data_flag <= 0;
po_data_re <= 8'd0;
if(i2c_clk_cnt == 2'd3)begin
data_num_cnt <= data_num_cnt + 1'b1;
sda_en <= 0;
sda_out <= 0;
end
end
NACK:begin //主机非应答状态,sda_out拉低,方便停止状态出现上升沿
po_data_flag <= 0;
sda_en <= 1;
sda_out <= 1;
end
STOP:begin
if(i2c_clk_cnt == 2'd2 && cnt_data == 3'd0)begin//拉高信号作为终止信号
sda_en <= 1;
sda_out <= 1;
end
else if(i2c_clk_cnt == 2'd3)begin
if(cnt_data == 3'd3)begin //送完了终止信号且延时一段时间发送I2C结束信号
i2c_end <= 1'b1;
cnt_data <= 0;
end
else
cnt_data <= cnt_data + 1'b1;
end
end
default:;
endcase
end
end
//生成i2c_scl
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
i2c_scl <= 1; //空闲状态scl为高电平
else if(cur_state != STOP)begin
if(i2c_clk_cnt == 2'd2)
i2c_scl <= 0;
else if(i2c_clk_cnt == 2'd0)
i2c_scl <= 1;
end
else
i2c_scl <= 1;
end
//生成从机响应信号
always @(posedge i2c_clk or negedge rst_n) begin
if(!rst_n)
ack_flag <= 0;
else
case(cur_state)
ACK_1,ACK_2,ACK_3,ACK_4,ACK_5:
if(i2c_clk_cnt == 2'd1 && !sda_in)
//if(i2c_clk_cnt == 2'd1) //仿真时默认从机响应
ack_flag <= 1'b1;
else if(i2c_clk_cnt == 2'd3)
ack_flag <= 1'b0;
default:ack_flag <= 1'b0;
endcase
end
endmodule
uart控制模块:该模块主要功能是把从EEPROM中读取的数据存到fifo中,然后在把数据从fifo中读取出来传给uart232发送模块
module uart_ctrl(
input clk, //50mhz
input rst_n, //复位信号
input i2c_end, //i2c结束信号
input [7:0] po_data, //i2c读取的数据
input po_data_flag, //i2c数据读取标志位
output [7:0] uart_data, //处理后传入232串口的数据
output reg uart_data_flag //uart_data数据标志位
);
localparam cnt_time_max = 8680*10/20-1;//波特率为115200读取1个字节的时间
reg [7:0] q; //计数器,po_data_flag来的时候计数
reg wr_en; //fifo写允许信号
reg [12:0] cnt_time; //fifo读允许信号标志位
wire rd_en; //fifo读允许信号
reg i2c_end_re; //i2c结束信号寄存
reg [7:0] cnt; //fifo中的数据个数计数
//fifo IP核例化
fifo fifo_inst (
.clock ( clk ),
.data ( po_data ),
.rdreq ( rd_en ),
.wrreq ( wr_en ),
.q ( uart_data )
);
//po_data_flag持续一个i2c周期的高电平(200个系统时钟周期)
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
q <= 0;
else if(po_data_flag)
q <= q + 1'b1;
else
q <= 0;
end
//wr_en
always@(posedge clk or negedge rst_n)
if(!rst_n)
wr_en <= 0;
else if(q == 8'b1)
wr_en <= 1;
else
wr_en <= 0;
//i2c_end_re
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
i2c_end_re <= 0;
else if(i2c_end)
i2c_end_re <= 1;
end
//i2c结束后,每隔(8680*10/20)s,读取fifo中的数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt_time <= 0;
else if(cnt > 0)begin
if(i2c_end_re)begin
if(cnt_time == cnt_time_max)
cnt_time <= 0;
else
cnt_time <= cnt_time + 1'b1;
end
end
end
//rd_en
assign rd_en = (cnt_time == cnt_time_max) ? 1 : 0;
//寄存rd_en
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
uart_data_flag <= 0;
else
uart_data_flag <= rd_en;
end
//cnt,wr_en信号来的时候加1,rd_en信号来的时候减1
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
cnt <= 0;
else if(wr_en)
cnt <= cnt + 1'b1;
else if(rd_en)
cnt <= cnt - 1'b1;
else
cnt <= cnt;
end
endmodule
uart发送模块:波特率为115200
module uart_tx(
input clk,
input rst_n,
input [7:0] pi_data,
input pi_flag,
output reg tx,
output reg tx_done //结束信号
);
localparam BAUD_RATE = 115200;
localparam CNT_BAUD_MAX = 50_000_000 / BAUD_RATE - 1;
wire done;
// pi_data寄存,由于pi_data随着tx模块接收数据变动,所以在pi_flag时刻将pi_data复制一份
reg [7:0] pi_data_r;
always @(posedge clk, negedge rst_n) begin
if(!rst_n)
pi_data_r <= 8'd0;
else if(pi_flag)
pi_data_r <= pi_data;
end
// 计数器使能,在pi_flag来后开始计数,传输完毕结束计数
reg cnt_ena;
always @(posedge clk) begin
if(pi_flag)
cnt_ena <= 1'b1;
else if(done)
cnt_ena <= 1'b0;
end
// 波特率计数器
reg [12:0] cnt_baud;
wire cnt_baud_done = (cnt_baud == CNT_BAUD_MAX);
always @(posedge clk, negedge rst_n) begin
if(!rst_n)
cnt_baud <= 13'd0;
else if(cnt_ena) begin
if(cnt_baud_done)
cnt_baud <= 13'd0;
else
cnt_baud <= cnt_baud + 13'd1;
end
end
localparam
IDLE = 0,
START = 1,
D0 = 2,
D1 = 3,
D2 = 4,
D3 = 5,
D4 = 6,
D5 = 7,
D6 = 8,
D7 = 9,
STOP = 10;
reg [3:0] state, next;
// stop状态只持续一个时钟周期,因为IDLE状态和STOP状态tx都是高电平,避免错过下一个pi_flag
always @(*) begin
case(state)
IDLE : next = pi_flag ? START : IDLE;
START : next = cnt_baud_done ? D0 : START;
D0 : next = cnt_baud_done ? D1 : D0;
D1 : next = cnt_baud_done ? D2 : D1;
D2 : next = cnt_baud_done ? D3 : D2;
D3 : next = cnt_baud_done ? D4 : D3;
D4 : next = cnt_baud_done ? D5 : D4;
D5 : next = cnt_baud_done ? D6 : D5;
D6 : next = cnt_baud_done ? D7 : D6;
D7 : next = cnt_baud_done ? STOP : D7;
STOP : next = IDLE;
endcase
end
always @(posedge clk, negedge rst_n) begin
if(!rst_n)
state <= IDLE;
else
state <= next;
end
// 传输完毕信号
assign done = (state == STOP);
// tx
always @(*) begin
case(state)
IDLE : tx = 1'b1;
START : tx = 1'b0;
D0 : tx = pi_data_r[0];
D1 : tx = pi_data_r[1];
D2 : tx = pi_data_r[2];
D3 : tx = pi_data_r[3];
D4 : tx = pi_data_r[4];
D5 : tx = pi_data_r[5];
D6 : tx = pi_data_r[6];
D7 : tx = pi_data_r[7];
STOP : tx = 1'b1;
endcase
end
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx_done <= 0;
else if(state == STOP)
tx_done <= 1;
else
tx_done <= 0;
end
endmodule
顶层模块:默认字节地址为01H,从EEPROM中读取十个数据
module i2c_read(
input sys_clk, //50mhz
input sys_n, //复位
output i2c_scl, //串行时钟信号
inout i2c_sda, //串行数据信号
output tx //232串口发送
);
wire clk; //50mhz
wire i2c_clk; //250khz
wire locked; //pll锁存信号
wire rst_n = locked && sys_n; //rst_n
wire i2c_end; //i2c结束信号
wire [7:0] po_data; //i2c读取的数据
wire po_data_flag; //i2c读取的数据标志位
wire [7:0] uart_data; //传入232发送模块的数据
wire uart_data_flag; //传入232发送模块的数据标志位
wire tx_done; //串口传输完一个字节数据结束信号
reg [15:0] cnt; //计数器
//cnt
always@(posedge clk or negedge rst_n)
if(!rst_n)
cnt <= 0;
else if (cnt < 1000)
cnt <= cnt + 1'b1;
else
cnt <= cnt;
wire i2c_start = (cnt >50 && cnt < 300) ? 1'b1 : 1'b0; //上电1000ns后产生i2c开始信号,高电平持续一个i2c时钟周期
//p;;例化
pll_250k pll_250k_inst (
.inclk0 ( sys_clk ),
.c0 ( clk ),
.c1 ( i2c_clk ),
.locked ( locked )
);
//i2c驱动模块例化
i2c_driver i2c_driver(
//系统接口
.rst_n(rst_n), //复位信号,低电平有效
.i2c_clk(i2c_clk), //i2c系统时钟,250khz
//i2c物理接口
.i2c_scl(i2c_scl), //串行时钟信号
.i2c_sda(i2c_sda), //串行数据信号
//用户接口
.i2c_start(i2c_start), //i2c开始信号
.word_addr(16'h0001),//字节地址
.i2c_num(0), //1表示字节地址为16位,0表示字节地址为8位
.i2c_end(i2c_end), //i2c结束信号
.po_data(po_data), //接收的数据
.po_data_flag(po_data_flag) //数据标志位
);
//232串口控制模块例化
uart_ctrl uart_ctrl(
.clk(clk),
.rst_n(rst_n),
.i2c_end(i2c_end), //i2c结束信号
.po_data(po_data), //i2c读取的数据
.po_data_flag(po_data_flag), //i2c数据读取标志位
.uart_data(uart_data), //处理后传入232串口的数据
.uart_data_flag(uart_data_flag) //uart_data数据标志位
);
//uart_tx例化
uart_tx uart_tx(
.clk(clk),
.rst_n(rst_n),
.pi_data(uart_data),
.pi_flag(uart_data_flag),
.tx(tx),
.tx_done(tx_done) //结束信号
);
endmodule
仿真代码:
`timescale 1ns/1ns
module i2c_tb;
reg clk;
reg rst_n;
wire i2c_scl;
wire i2c_sda;
wire tx;
i2c_read i2c_read(
.sys_clk(clk),
.sys_n(rst_n),
.i2c_scl(i2c_scl), //串行时钟信号
.i2c_sda(i2c_sda), //串行数据信号
.tx(tx)
);
initial clk = 0;
always #10 clk = !clk;
initial begin
rst_n = 0;
#65;
rst_n = 1;
#3_000_000;
$stop;
end
endmodule
仿真波形:这里就只给i2c_driver模块的波形了,由于没有EEPROM的仿真模型,因此默认从机响应,读取的数据也都是高阻态。
当i2c_start出现上升沿时开始抓取,在此之前我们已经通过i2c页写入数据0AH,12H,23H,34H,45H,56H,67H,78H,89H,91H到EEPROM芯片中(起始地址是01H)。这里就只展现i2c驱动模块的图了。
我们也可以从串口调试助手中看到收到了数据,如图:
有任何问题都可以在评论区和我交流。
本文参考了大佬孤独的单刀的博客