FPGA开发--I2C读写时序
文章摘要:
本文实现了类似于单片机的外设的I2C读写模块,将I2C各步骤独立出来,可以更清晰的描述I2C时序及工作原理,也使代码具有更发了好的通用性;
重点内容: 三段式状态机,双向IO,I2C时序;
硬件平台:EP4CE6F17C8
开发环境:Quartus II 13.1
时序图:
起始位:SCL高电平时,SDA下跳;
停止位:SCL高电平时,SDA上跳;
数据位:SCL低电平时,SDA可改变状态,SCL上跳时传输;
应答位: 0 - ACK, 1 - NAK(只有主机才允许发送NAK)
I2C模块
/*
* 功能描述:I2C接口模块
*/
module i2c_comm(
clk, // 模块时钟
rst_n, // 模块复位(低电平有效)
scl, // 时钟引脚
sda, // 数据引脚(双向)
start_req, // 发送起始位
wr_req, // 发送请求
rd_req, // 接收请求
stop_req, // 发送停止位
wdata, // 待发数据
rdata, // 接收数据
busy, // 空闲(0)/繁忙(1)
done // 操作完成(脉冲信号)
);
input clk; // 模块时钟
input rst_n; // 模块复位(低电平有效)
output scl; // 时钟引脚
inout sda; // 数据引脚(双向)
input start_req; // 发送直始位
input wr_req; // 发送数据
input rd_req; // 读取数据
input stop_req; // 发送停止位
input [7:0]wdata; // 待发数据
output[7:0]rdata; // 待发数据
output busy; // 操作完成(空闲)
output reg done;
//-----------------------------------------
parameter I2C_COUNT = 20'd500; // 100KHz = 10us
parameter S_IDLE = 4'd0;
parameter S_START = 4'd1;
parameter S_SEND_DATA = 4'd2;
parameter S_RECV_DATA = 4'd3;
parameter S_WAIT_ACK = 4'd4;
parameter S_SEND_ACK = 4'd5;
parameter S_STOP = 4'd6;
//-----------------------------------------
reg [3:0] nstate; // 后续状态
reg [3:0] cstate; // 当前状态
//-----------------------------------------
reg isout;
reg scl_r;
reg sda_r;
reg [7:0]rdata_r;
reg [3:0] bits;
reg[19:0] i2c_cnt;
//-----------------------------------------
assign scl = scl_r;
assign sda = isout?sda_r:1'bz;
assign busy = (cstate == S_IDLE)?1'b0:1'b1;
assign rdata = rdata_r;
//-----------------------------------------
// 传输计时逻辑
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
i2c_cnt <= 0;
end
else if(cstate == S_IDLE) begin
i2c_cnt <= 0;
end
else if(i2c_cnt == I2C_COUNT) begin
i2c_cnt <= 0;
end
else begin
i2c_cnt <= i2c_cnt + 1'b1;
end
end
//-----------------------------------------
// 三段状态机第一段(标准代码)
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
cstate <= S_IDLE;
end
else begin
cstate <= nstate;
end
end
//-----------------------------------------
// 三段状态机第二段(状态转移)
always @(*) begin
case(cstate)
S_IDLE: begin
if(start_req) begin
nstate = S_START; // 启动总线
end
else if(stop_req) begin
nstate = S_STOP; // 停止总线
end
else if(wr_req) begin
nstate = S_SEND_DATA; // 发送数据
end
else if(rd_req) begin
nstate = S_RECV_DATA; // 发送数据
end
else begin
nstate = S_IDLE;
end
end
// 发送起始位
S_START: begin
if(i2c_cnt == I2C_COUNT) begin
nstate = S_SEND_DATA; // 发送完起始位,发送设备地址
end
else begin
nstate = S_START;
end
end
// 发送数据
S_SEND_DATA: begin
if(bits == 4'd8) begin
nstate = S_WAIT_ACK; // 发送完数据,接收ACK
end
else begin
nstate = S_SEND_DATA;
end
end
// 等待设备响应
S_WAIT_ACK: begin
if(i2c_cnt == I2C_COUNT) begin
nstate = S_IDLE; // 接收完ACK,转向空闲
end
else begin
nstate = S_WAIT_ACK;
end
end
// 接收数据
S_RECV_DATA: begin
if(bits == 4'd8) begin
nstate = S_SEND_ACK; // 接收完数据,发送ACK/NAK
end
else begin
nstate = S_RECV_DATA;
end
end
// 主机响应
S_SEND_ACK: begin
if(i2c_cnt == I2C_COUNT) begin
nstate = S_IDLE; // 发送完ACK/NAK,转向空闲
end
else begin
nstate = S_SEND_ACK;
end
end
// 发送停止位
S_STOP: begin
if(i2c_cnt == I2C_COUNT) begin
nstate = S_IDLE; // 发送完停止位,转向空闲
end
else begin
nstate = S_STOP;
end
end
default: begin
nstate = S_IDLE;
end
endcase
end
//-----------------------------------------
// 三段状态机第三段(输出)
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
isout <= 0; // 设置双向端口为输入
sda_r <= 1; // 复位时,设置为高电平
scl_r <= 1; // 复位时,设置为高电平
end
else begin
case(cstate)
S_IDLE: begin
done <= 0;
bits <= 0;
end
// 发送起始位:SCL为高时,SDA下跳
// 前置状态:SCL为高,SDL为高(S_IDLE);
// 后续状态:SCL为低,SDA为低(S_SEND_DATA);
S_START: begin
if(i2c_cnt == 20'd100) begin
scl_r <= 1'b1; // SCL拉高
end
else if(i2c_cnt == 20'd200) begin
sda_r <= 1'b0; // SDA下跳(产生起始位)
end
else if(i2c_cnt == 20'd400) begin
scl_r <= 1'b0; // SCL拉低为次数据做准备
end
else begin
isout <= 1'b1; // 设置为输出
end
end
// 发送数据:SCL低电平准备,SCL上跳送出
// 前置状态:SCL为低(重要)(S_START/S_WAIT_ACK);
// 后续状态:SCL为低,SDAl输入(S_WAIT_ACK)
S_SEND_DATA: begin
if(bits == 4'd8) begin
isout <= 0; // 设置为输入准备接收ACK
sda_r <= 1;
end
else if(i2c_cnt == 20'd100) begin
isout <= 1'b1; // 设置为输出
sda_r <= wdata[7 - bits]; // 低电平时准备SDA数据
end
else if(i2c_cnt == 20'd200) begin
scl_r <= 1'b1; // SCL上跳发送数据
end
else if(i2c_cnt == 20'd400) begin
scl_r <= 1'b0; // SCL拉低准备下次发送
end
else if(i2c_cnt == I2C_COUNT) begin
bits <= bits + 1'b1; // 时序结束时才计数
end
end
// 等待ACK: 由设备回应,设备在SCL拉低之后一段时间内已经准备好了数据;
// 前置状态:SCL为低,SDA输入(S_SEND_DATA)
// 后续状态:SCL为低,SDA输入
S_WAIT_ACK: begin
if(i2c_cnt == 20'd200) begin
isout <= 0; // 设置为输入准备接收ACK
scl_r <= 1'b1; // SCL上跳以接收ACK
end
if(i2c_cnt == 20'd400) begin
scl_r <= 1'b0; // SCL拉低以准备下次发送
// 在此处理ACK
end
else if(i2c_cnt == I2C_COUNT) begin
bits <= 0;
done <= 1;
end
else begin
isout <= 1'b0; // 设置为输入
end
end
// 接收数据:SCL低电平准备
// 前置状态:SCL为低,SDA输入(S_WAIT_ACK)
// 后续状态:SCL为低,SDA输入(S_SEND_ACK)
S_RECV_DATA: begin
if(bits == 4'd8) begin
bits <= 0;
end
else if(i2c_cnt == 20'd100) begin
scl_r <= 1'b0; // SCL先拉低,因为不确定之前的电平
end
else if(i2c_cnt == 20'd200) begin
scl_r <= 1'b1; // SCL上跳接收数据
end
else if(i2c_cnt == 20'd300) begin
rdata_r[7 - bits] <= sda; // 数据稳定后读取数据
end
else if(i2c_cnt == 20'd400) begin
scl_r <= 1'b0; // SCL拉低准备下次发送
end
else if(i2c_cnt == I2C_COUNT) begin
bits <= bits + 1'b1;
end
else begin
isout <= 1'b0; // 设置为输入
end
end
// 发送ACK: ACK(0)/NAK(1)
// 前置状态:SCL为低,SDA输入,
// 后续状态:SCL为高,SDA为低(STOP)
S_SEND_ACK: begin
if(i2c_cnt == 20'd100) begin
isout <= 1; // 设置为输出
sda_r <= 1'b1; // SCL低电平设置ACK(0)/NAK(1)数据
end
else if(i2c_cnt == 20'd200) begin
scl_r <= 1'b1; // SCL上跳发送
end
else if(i2c_cnt == 20'd400) begin
scl_r <= 1'b0; // SCL拉低以准备修改SDA
end
else if(i2c_cnt == I2C_COUNT) begin
sda_r <= 0; // 恢复为低电平(准备上跳停止)
done <= 1;
end
else begin
end
end
// 发送停止位: SCL为高电平时,SDA上跳
// 前置状态:SCL为低,SDA不确定
// 后续状态:SCL为高,SDA为高(准备下次起始位)
S_STOP: begin
if(i2c_cnt == 20'd200) begin
isout <= 1'b1; // 输出
sda_r <= 1'b0; // 设置SDA输出低电平
end
else if(i2c_cnt == 20'd300) begin
scl_r <= 1'b1; // SCL设置为高电平
end
else if(i2c_cnt == 20'd400) begin
sda_r <= 1'b1; // SDA上跳
end
else if(i2c_cnt == I2C_COUNT) begin
done <= 1;
end
else begin
isout <= 1'b1;
end
end
default: begin
done <= 0;
end
endcase
end
end
endmodule