iic可以有字节级命令传输实现和命令级实现,字节级实现就是如何通过VERILOG编写驱动,实现单个字节iic一些的传输。
单字节iic的传输可以看下面文章
现在讲的是多字节(命令级)传输实现
所谓命令级传输实现,是指在进行iic协议通信时,往往不是只进行单个字节进行传输,字节传输的上层就是命令集,就是给字节传输模块分配每次传输的字节。既i2c_control.v文件
模块端口如下
module i2c_control
#(
parameter SYS_CLOCK = 50_000_000,//系统时钟
parameter SCL_CLOCK = 400_000,//iic通信速率
parameter SLAVE_ADDR = 7'h00//iic器件的地址
)
(
clk,
rst_n,
wrreg_req,
rdreg_req,
w_len,
r_len,
wrdata,
rddata,
// device_id,
RW_Done,
ack,
i2c_sclk,
i2c_sdat
);
端口主要包括一些参数的以及端口的定义
主要端口包括
clk, //时钟
rst_n, 复位
wrreg_req, 写数据请求
rdreg_req, 读数据请求
w_len, 写数据的长度
r_len, 读数据的长度
wrdata, //要写入的数据,当前代码最多支持8字节
rddata,//读出的数据
RW_Done, //读写操作完成信号
ack, //应答信号
i2c_sclk, //iic器件的scl
i2c_sdat //iic器件的sda
同样使用状态机完成设计
状态定义如下
localparam
IDLE = 7'b0000001, //空闲状态
WR_REG = 7'b0000010, //写寄存器状态
WAIT_WR_DONE = 7'b0000100, //等待写数据完成
WR_REG_DONE = 7'b0001000, //写数据完成
RD_REG = 7'b0010000, //读寄存器数据
WAIT_RD_DONE = 7'b0100000, //等待数据读完
RD_REG_DONE = 7'b1000000; //读数据完成
状态转移如下
IDLE:
begin
cnt <= 0;
ack <= 0;
RW_Done <= 1'b0;
if(wrreg_req)//空闲状态下,有写请求信号,去写寄存器状态
begin
state <= WR_REG;
wrdata_r <= wrdata;
end
else if(rdreg_req)//空闲状态下,有读请求信号,去读寄存器状态
begin
state <= RD_REG;
rddata <= 0;
end
else
state <= IDLE;
end
空闲状态下等待读写请求输入
写寄存器状态
WR_REG:
begin
state <= WAIT_WR_DONE;//配置本次传输的字节
if(cnt==0)
write_byte(WR | STA, (SLAVE_ADDR<<1|1'b0));
else if(cnt==w_len)
write_byte(WR | STO, wrdata_r[63:56]);
else
write_byte(WR, wrdata_r[63:56]);
end
在写寄存器状态,利用一个周期分配本次数据传输的字节,同时设置一个计数器,传输完每个字节,计数器加一,然后根据计数值的值,分配为数据,并且生成对应的操作指令。
等待写完状态
WAIT_WR_DONE:
begin
Go <= 1'b0;
if(Trans_Done)begin
ack <= ack | ack_o;
if(cnt>0)
wrdata_r <= wrdata_r << 8;
if(cnt==w_len)
state <= WR_REG_DONE; //写数据到达最大值,进入写数据完成状态
else
begin cnt <= cnt+1; state <= WR_REG; end//未到最大值,继续写数据
end
end
在字节传输结束信号来临后,将数据写入寄存器wrdata_r左移8位,这样在写数据时,就可以只写最高的8位
写数据完成
WR_REG_DONE:
begin
RW_Done <= 1'b1;
state <= IDLE;
end
这个状态比较简单,就是命令结束信号,告知上层模快进行下一步操作
读数据状态,代码最大支持6字节数据读取
RD_REG:
begin
state <= WAIT_RD_DONE;
case(cnt)
0:write_byte(WR | STA, (SLAVE_ADDR<<1|1'b1));
1,2,3,4,5:read_byte(RD | ACK);
6:read_byte(RD | NACK | STO);
default:;
endcase
end
在读数据时,我们首先会传送起始位和器件地址,并且用1'b1表示本次时读操作,计数器剩下的都在从从机读出数据
等待数据读完状态
WAIT_RD_DONE:
begin
Go <= 1'b0;
if(Trans_Done)begin
if(cnt <= 5)
ack <= ack | ack_o;
if(cnt==0)
cnt <= 4'd7-r_len;
else
cnt <= cnt + 1'b1;
if(cnt==6)
state <= RD_REG_DONE;//读到预定的值,进入读完状态
else
state <= RD_REG; //否则继续读数据
if(cnt>0)
rddata <= {rddata[39:0],Rx_DATA};//每次读数据将数据缓存,cnt=0时传输地址,没有读出数据
end
end
if(cnt==0)
cnt <= 4'd7-r_len;
else
cnt <= cnt + 1'b1;
在cnt=0时,将cnt设定一个合适的值,以便再读到6字节数据时,可以配合读数据状态中cnt=6时不应答,停止位生成,
比如读3个数据,当cnt=0是,cnt会被置成4,这样等待读到6时,正好经历了4,5,6.在cnt=6生成不应答和停止位的同时实际上只读了3字节数据
数据读完状态
RD_REG_DONE:
begin
RW_Done <= 1'b1;
state <= IDLE;
end
比较简单,就是生成了读写状态完成信号
该模块利用了任务的写法,这是verilog的标准语法,格式如下
task task_name;
logic...
endtask
模块中具体用到的语句是
task read_byte;
input [5:0]Ctrl_Cmd;
begin
Cmd <= Ctrl_Cmd;
Go <= 1'b1;
end
endtask
task write_byte;
input [5:0]Ctrl_Cmd;
input [7:0]Wr_Byte_Data;
begin
Cmd <= Ctrl_Cmd;
Tx_DATA <= Wr_Byte_Data;
Go <= 1'b1;
end
endtask
大致的意思就是一个数据配置的任务,就是给什么变量配置什么值,其实这些内容直接写在状态机的逻辑中,也是完全可以的。
上面就是verilog实现iic底层通信的具体实现了,可以在驱动上层再封装一个模块作为具体的器件适配模块。