目录
1.单次随机写数据
1.1简介
1.2代码
1.3Modelsim仿真
1.4逻辑分析仪上板验证
2.页写
2.1简介
2.1代码
2.3Modelsim仿真
2.4逻辑分析仪上板验证
1.单次随机写数据
1.1简介
在黑金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) 单字节数据写入完成,主机接收到应答信号后,向从机发送停止信号,单字节数据 写入完成。
1.2代码
i2c_driver模块:该模块的主要功能是通过IIC向EEPROM写入数据。器件地址为A0H,字节地址为12H,数据为ABH。在这里iic的时钟周期为250khz,对它进行4分频来作为i2c_scl。
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结束信号
);
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,
SEND_DATA = 8, //发送数据
ACK_4 = 9,
STOP = 10;
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 [3:0] cur_state;
reg [3:0] next_state;
wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //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:
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 = SEND_DATA; //从机响应,转移到发送数据状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_3;
SEND_DATA:
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_4;
else
next_state = SEND_DATA;
ACK_4:
if(ack_flag && i2c_clk_cnt == 2'd3)
next_state = STOP; //从机响应,转移停止状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_4;
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 <= 0;
end
else begin
i2c_end <= 0;
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位数据发送完毕,等待从机响应
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]; //释放总线
end
else begin
sda_en <= 1;
sda_out <= word_addr[7];
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_out <= pi_data[7];
end
SEND_DATA:begin //写入数据
if(i2c_clk_cnt == 2'd3)
if(cnt_data == 3'd7)begin //高8位数据写完
sda_en <= 0; //高8位数据发送完毕,等待从机响应
cnt_data <= 0;
end
else begin
cnt_data <= cnt_data + 1'b1;
sda_out <= pi_data[6 - cnt_data];
sda_en <= 1'b1;
end
end
ACK_4:begin
if(i2c_clk_cnt == 2'd3)
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= 0; //释放总线
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驱动模块提供了iic开始信号、字节地址和数据。
module i2c_write(
input clk,
input rst_n,
output i2c_scl, //串行时钟信号
inout i2c_sda //串行数据信号
);
wire i2c_clk;
wire i2c_end;
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'h0012),
.i2c_num(0), //1表示字节地址为16位,0表示字节地址为8位
.i2c_end(i2c_end) //i2c结束信号
);
endmodule
1.3Modelsim仿真
tb代码:由于没有EPPROM的仿真模型,所以仿真时我们默认从机响应。
`timescale 1ns/1ns
module i2c_tb;
reg clk;
reg rst_n;
wire i2c_scl;
wire i2c_sda;
i2c_write i2c_write(
.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
波形:
1.4逻辑分析仪上板验证
我们以i2c_clk作为抓取时钟,深度设为512,当i2c_start出现上升沿时开始抓取。
2.页写
2.1简介
首先使用串口调试助手通过uart232向开发板发送数据,开发板接收到数据后存到fifo中,然后从fifo中读取数据再通过iic向EEPROM中写入数据。
fpga型号:EP4CE6F17C8
开发工具:Quartus ll 13.0 + Modelsim 10.1c
系统时钟:50MHZ
IIC时钟:250KHZ
四个模块:uart接收模块,i2c控制模块,i2c驱动模块,顶层模块
使用的ip核:pll,fifo
UART波特率:115200
时序图:
过程如下:
(1) 主机产生并发送起始信号到从机,将控制命令写入从机设备,读写控制位设置为低电平,表示对从机进行数据写操作,控制命令的写入高位在前低位在后;
(2) 从机接收到控制指令后,回传应答信号,主机接收到应答信号后开始存储地址的写 入。若为 2 字节地址,顺序执行操作;若为单字节地址跳转到步骤(5);
(3) 先向从机写入高 8 位地址,且高位在前低位在后;
(4) 待接收到从机回传的应答信号,再写入低 8 位地址,且高位在前低位在后,若为 2 字节地址,跳转到步骤(6);
(5) 按高位在前低位在后的顺序写入单字节存储地址;
(6) 地址写入完成,主机接收到从机回传的应答信号后,开始第一个单字节数据的写入;
(7) 数据写入完成,主机接收到应答信号后,开始下一个单字节数据的写入;
(8) 数据写入完成,主机接收到应答信号。若所有数据均写入完成,顺序执行操作 程;若数据尚未完成写入,跳回到步骤(7);
(9) 主机向从机发送停止信号,页写操作完成。
2.1代码
uart_rx模块:该模块的功能是接收串口调试助手传来的数据
module uart_rx(
input rst_n, //复位
input clk, //时钟,50mhz
input rx, //接收信号
input [2:0] bot_set, //波特率设置位
output reg rx_done, //接收完成位
output reg [7:0] data //fpga接收位
);
reg en; //使能位
reg[31:0]time1; //接收十六分之一数据所用时间
reg[1:0]d; //2个D触发器
reg[7:0]counter; //160个状态
reg[8:0]q; //计数器
//wire pdge; //上升沿标准标志
wire ndge; //下降沿标志
reg[3:0]s_begin; //开始位检测
reg[3:0]s_end; //结束位检测
reg[3:0]s[7:0]; //0到7位数据位检测
//波特率
always@(*)
case(bot_set)
0:time1=1000000000/9600/16/20;
1:time1=1000000000/19200/20/16;
2:time1=1000000000/38400/20/16;
3:time1=1000000000/57600/20/16;
4:time1=1000000000/115200/20/16;
default:time1=1000000000/9600/20/16;
endcase
//d触发器,用来检测rx出现上升沿还是下降沿
always@(posedge clk)begin
d[0]<=rx;
d[1]<=d[0];
end
//assign pdge=(d==2'b01); //rx出现上升沿
assign ndge=(d==2'b10); //rx出现下降沿
//en
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
en<=0;
else if(ndge) //当rx出现下降沿时en拉高
en<=1;
else if(rx_done||(s_begin>=4)) //传输结束en拉低
en<=0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
q<=0;
else if(en)begin //当en为高电平时,q开始计数
if(q==time1-1)
q<=0;
else
q<=q+1'b1;
end
else
q<=0;
end
wire flag=(q==time1/2); //中点为采样点,中点到来产生一个时钟周期脉冲
always@(posedge clk or negedge rst_n)
if(!rst_n)
counter<=0;
else if(en)begin
if(flag)begin //中点到来,counter自加1
if(counter==160)
counter<=0;
else
counter<=counter+1'b1;
end
else
counter<=counter;
end
else
counter<=0; //en低电平时,counter保持0
always@(posedge clk or negedge rst_n)
if(!rst_n)begin //复位,检测位清0
s_begin<=0;
s_end<=0;
s[0]<=0;
s[1]<=0;
s[2]<=0;
s[3]<=0;
s[4]<=0;
s[5]<=0;
s[6]<=0;
s[7]<=0;
end
else if(flag)begin
case(counter)
0:begin s_begin<=0;
s_end<=0;
s[0]<=0;
s[1]<=0;
s[2]<=0;
s[3]<=0;
s[4]<=0;
s[5]<=0;
s[6]<=0;
s[7]<=0;
end
5,6,7,8,9,10,11,12:s_begin<=s_begin+rx;
21,22,23,24,25,26,27:s[0]<=s[0]+rx;
37,38,39,40,41,42,43:s[1]<=s[1]+rx;
53,54,55,56,57,58,59:s[2]<=s[2]+rx;
69,70,71,72,73,74,75:s[3]<=s[3]+rx;
85,86,87,88,89,90,91:s[4]<=s[4]+rx;
101,102,103,104,105,106,107:s[5]<=s[5]+rx;
117,118,119,120,121,122,123:s[6]<=s[6]+rx;
133,134,135,136,137,138,139:s[7]<=s[7]+rx;
149,150,151,152,153,154,155:s_end<=s_end+rx;
default: begin s_begin<=s_begin;
s_end<=s_end;
s[0]<=s[0];
s[1]<=s[1];
s[2]<=s[2];
s[3]<=s[3];
s[4]<=s[4];
s[5]<=s[5];
s[6]<=s[6];
s[7]<=s[7];
end
endcase
end
always@(posedge clk or negedge rst_n)
if(!rst_n)
data<=0;
else if(counter==160&&flag)begin
data[0]<=((s[0]>=4)?1'b1:1'b0);
data[1]<=((s[1]>=4)?1'b1:1'b0);
data[2]<=((s[2]>=4)?1'b1:1'b0);
data[3]<=((s[3]>=4)?1'b1:1'b0);
data[4]<=((s[4]>=4)?1'b1:1'b0);
data[5]<=((s[5]>=4)?1'b1:1'b0);
data[6]<=((s[6]>=4)?1'b1:1'b0);
data[7]<=((s[7]>=4)?1'b1:1'b0);
end
always@(posedge clk or negedge rst_n)
if(!rst_n)
rx_done<=0;
else if(counter==160&&(q==(time1-1)/2))
rx_done<=1;
else
rx_done<=0;
endmodule
i2c控制模块:该模块的功能是把232串口接收到的数据存到fifo中,并产生i2c的开始信号和数据。
module i2c_ctrl(
input clk, //50m时钟信号
input rst_n, //复位信号
input [7:0] rx_data, //232串口接收的数据
input rx_done, //232串口接收数据标志位
input [7:0] rx_num, //接收的字节数
input data_flag, //fifo开始读取标志位
output i2c_start, //i2c开始信号
output [7:0] i2c_data //写入i2c的数据
);
reg [7:0] cnt_data; //计数接收的字节数
reg [7:0] q; //232串口结束到i2c开始信号计数
reg rd_en; //fifo读信号
reg [7:0] rd_en_flag; //fifo读信号标志,由于data_flag高电平持续一个i2c时钟周期(200系统时钟周期),因此它为1时rd_en拉高
//fifo IP核例化
fifo fifo_inst (
.clock ( clk ),
.data ( rx_data ),
.rdreq ( rd_en),
.wrreq ( rx_done ),
.q ( i2c_data )
);
//计数接收的字节数
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt_data <= 0;
else if(rx_done)begin //当rx_done来的时候,cnt_data自加1
if(cnt_data == rx_num)
cnt_data <= 0;
else
cnt_data <= cnt_data + 1'b1;
end
end
//q,由于1个i2c时钟周期等于200个系统时钟周期,因此需计数200
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
q <= 0;
else if(cnt_data == rx_num)begin //当数据全部接收完后q开始计数
if(q == 202)
q <= 202;
else
q <= q + 1'b1;
end
end
assign i2c_start = (q > 0 && q <202) ? 1'b1 : 1'b0; //由于i2c的时钟位250khz,因此得让i2c_start保持一个i2c时钟周期的高电平
//data_flag高电平持续200个系统时钟周期
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rd_en_flag <= 0;
else if(data_flag)
rd_en_flag <= rd_en_flag + 1'b1;
else
rd_en_flag <= 0;
end
//rd_en
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rd_en <= 0;
else if(rd_en_flag == 8'd1)
rd_en <= 1;
else
rd_en <= 0;
end
endmodule
i2c驱动模块:该模块的主要功能是通过IIC向EEPROM写入数据。器件地址为A0H,字节地址为01H,数据为fifo中读取的数据。在这里iic的时钟周期为250khz,对它进行4分频来作为i2c_scl。
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位
input [7:0] data_num, //写入的字节数
output reg i2c_end, //i2c结束信号
output reg data_flag //从fifo中开始读取标志位
);
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,
SEND_DATA = 8, //发送数据
ACK_4 = 9,
STOP = 10;
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] write_num; //计数发送的字节数
//状态机
reg [3:0] cur_state;
reg [3:0] next_state;
wire [7:0] addr_w = {DEVICE_ADDR,1'b0}; //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:
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 = SEND_DATA; //从机响应,转移到发送数据状态
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_3;
SEND_DATA:
if(cnt_data == 3'd7 && i2c_clk_cnt == 2'd3)
next_state = ACK_4;
else
next_state = SEND_DATA;
ACK_4:begin
if(ack_flag && i2c_clk_cnt == 2'd3)begin //从机响应
if(write_num == data_num)
next_state = STOP; //数据全部发送完,跳转至停止状态
else
next_state = SEND_DATA; //数据并未发完,跳转至发数据状态
end
else if(i2c_clk_cnt == 2'd3)
next_state = IDLE; //从机未响应返回空闲状态
else
next_state = ACK_4;
end
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 <= 0;
write_num <= 8'b0;
data_flag <= 0;
end
else begin
i2c_end <= 0;
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位数据发送完毕,等待从机响应
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]; //释放总线
end
else begin
sda_en <= 1;
sda_out <= word_addr[7];
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_out <= pi_data[7];
data_flag <= 0;
write_num <= write_num + 1'b1;
end
else if(i2c_clk_cnt == 2'd2) //提前一个i2c时钟周期开始从fifo中读取数据
data_flag <= 1;
SEND_DATA:begin //写入数据
if(i2c_clk_cnt == 2'd3)
if(cnt_data == 3'd7)begin //高8位数据写完
sda_en <= 0; //高8位数据发送完毕,等待从机响应
cnt_data <= 0;
end
else begin
cnt_data <= cnt_data + 1'b1;
sda_out <= pi_data[6 - cnt_data];
sda_en <= 1'b1;
end
end
ACK_4:begin
if(i2c_clk_cnt == 2'd3)begin
data_flag <= 0;
if(write_num == data_num)begin //数据全部写完
write_num <= 8'b0;
sda_en <= 1; //从机响应完成,拉高sda_en
sda_out <= 0; //拉低sda_out,方便在scl高电平时,sda产生上升沿
end
else begin //数据并未全部写完,因此sda_out得为数据的最高位
sda_en <= 1;
sda_out <= pi_data[7];
write_num <= write_num + 1'b1;
end
end
else if(i2c_clk_cnt == 2'd2 && write_num < data_num)
data_flag <= 1; //提前一个i2c时钟周期改变数据
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
顶层模块:
module i2c_write(
//系统接口
input sys_clk,
input sys_n,
//uart_rx
input rx,
//i2c
inout i2c_sda,
output i2c_scl
);
wire clk; //50MHZ
wire i2c_clk; //250KHZ
wire locked;
wire rst_n = locked && sys_n;
wire rx_done;
wire [7:0] rx_data;
wire i2c_end;
wire data_flag;
wire i2c_start;
wire [7:0] i2c_data;
parameter num = 8'd10;//接收的字节数
wire [7:0] rx_num = num;
//pll IP核例化
pll_250k pll_250k_inst (
.inclk0 ( sys_clk ),
.c0 ( clk ),
.c1 ( i2c_clk ),
.locked ( locked )
);
//232接收模块例化
uart_rx uart_rx(
.rst_n(rst_n), //复位
.clk(clk), //时钟,50mhz
.rx(rx), //接收信号
.bot_set(4), //波特率设置位
.rx_done(rx_done), //接收完成位
.data(rx_data) //fpga接收位
);
//i2c控制模块例化
i2c_ctrl i2c_ctrl(
.clk(clk), //50m时钟信号
.rst_n(rst_n), //复位信号
.rx_data(rx_data), //232串口接收的数据
.rx_done(rx_done), //232串口接收数据标志位
.rx_num(rx_num), //接收的字节数
.data_flag(data_flag), //fifo开始读取标志位
.i2c_start(i2c_start), //i2c开始信号
.i2c_data(i2c_data) //写入i2c的数据
);
//iic驱动模块例化
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(i2c_data), //写入i2c的数据
.i2c_start(i2c_start), //i2c开始信号
.word_addr(16'h0001), //字节地址
.i2c_num(1'b0), //1表示字节地址为16位,0表示字节地址为8位
.data_num(rx_num), //写入的字节数
.i2c_end(i2c_end), //i2c结束信号
.data_flag(data_flag) //fifo开始读取标志位
);
endmodule
2.3Modelsim仿真
tb代码:仿真时模拟向开发板发送3给数据:5FH、F5H和12H。
`timescale 1ns/1ns
module i2c_tb;
reg sys_clk;
reg sys_n;
reg rx;
wire i2c_sda;
wire i2c_scl;
i2c_write i2c_write(
//系统接口
.sys_clk(sys_clk),
.sys_n(sys_n),
//uart_rx
.rx(rx),
//i2c
.i2c_sda(i2c_sda),
.i2c_scl(i2c_scl)
);
task f1;
input[7:0]tx_data;
begin
rx=1;
#20
rx=0;
#8680;
rx=tx_data[0];
#8680;
rx=tx_data[1];
#8680;
rx=tx_data[2];
#8680;
rx=tx_data[3];
#8680;
rx=tx_data[4];
#8680;
rx=tx_data[5];
#8680;
rx=tx_data[6];
#8680;
rx=tx_data[7];
#8680;
rx=1;
#8680;
end
endtask
initial sys_clk = 0;
always#10 sys_clk =! sys_clk;
initial begin
sys_n=0;
rx=1;
#201
sys_n=1;
#200;
f1(8'h5f);
#10000;
f1(8'hf5);
#10000;
f1(8'h12);
#2_000_000;
$stop;
end
endmodule
uart接收模块和iic控制模块仿真图:
i2c驱动模块:
2.4逻辑分析仪上板验证
首先我们通过串口调试助手向开发板发送10个数据
然后以i2c_clk作为抓取时钟,深度设为512,当i2c_start出现上升沿时开始抓取。
这样就成功向EEPROM中写入了数据(初始字节地址为(01H),通过前面的文章FPGA实现IIC接口(1)-EEPROM芯片读取数据-CSDN博客我们也可以顺利读出这些数据。文章来源:https://www.toymoban.com/news/detail-815992.html
————————————————
版权声明:本文为CSDN博主「守雲开见月明」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_45742083/article/details/135307355文章来源地址https://www.toymoban.com/news/detail-815992.html
到了这里,关于FPGA实现IIC接口(2)-EEPROM芯片写数据的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!