国产FPGA测评【六】FPGA实现I2C从机与上位机通信
[复制链接]
声明:1.本帖如有对引用其他网站资源,均附上了网址,针对本帖中可能出现的侵权行为,请及时联系本人修改或删除。
2.未经本人允许,请勿转载。若本帖存在错误或不足之处,烦请指正,本人会及时修改。
0.说明
上个帖子介绍了FPGA作为I2C主机遍历EEPROM的相关知识。我们通常使用的开发板,FPGA往往作为主器件,因此一般都是使用的I2C MASTER模块作为I2C驱动,与其他模块进行交互。
对于另一种常见的使用场景,主板上往往还有ARM芯片、X86芯片或者BMC,FPGA可能只是作为辅助器件承担主板的一部分工作,这时候FPGA往往作为I2C从机与CPU、BMC等进行数据、命令交互。本帖介绍I2C从模块,分为两个部分。第一部分介绍紫光同创的I2C硬核,以及紫光FPGA作为从机通过I2C在线升级固件的方法;第二部分将介绍I2C SLAVE驱动源码,结合I2C时序对其进行分析,方便大家对类似模块的开发和运用。
1.紫光I2C硬核及在线升级固件方法
下图为紫光COMPACT系列器件,不同型号的FPGA均有两个I2C硬核。
如果我们需要使用这个I2C硬核,需要在紫光的软件中设置FEATURE VALUE,使能其I2C功能,而不能作为GPIO去使用。下图介绍了设置FEATURE VALUE使能I2C硬核的方法。
如果需要使用I2C硬核,记得选择Dedicated IO,并设置好I2C地址。当我们选择不同的类型时,FEATURE VALUE也是会跟着改变。我在使用COMPACT系列的FPGA时,一般将其用作普通GPIO,并且将FEATURE设置为200389c3。
设置使能I2C硬核并设置好I2C地址后(FPGA硬核默认实现的作为从机使用),根据紫光官方提供的文档《Compact系列CPLD器件从I2C接口配置和编程应用指南》,大家可以学习如何通过I2C硬核对FPGA在线升级固件。
简单来说,就是按照手册配置好FPGA参数后,I2C主器件(CPU、PCH、BMC、ARM芯片)等需要调用紫光提供的.c驱动文件,将生成的.sbit通过I2C接口向FPGA内部的FLASH更新并固化固件。
2.I2C SLAVE驱动及I2C协议
本帖默认大家理解了I2C协议原理,直接对I2C SLAVE驱动代码进行介绍。
下面是I2C通信的几个关键信号:
起始信号:SCL为高时SDA为下降沿;
重复起始信号:SCL为高时SDA为下降沿;
停止信号:SCL为高时SDA为上升沿。
// start: SCL ~~~~~~~~~~\____
// SDA ~~~~~~~~\______
// x | A | B | C | D | i
// repstart SCL ____/~~~~\___
// SDA __/~~~\______
// x | A | B | C | D | i
// stop SCL ____/~~~~~~~~
// SDA ==\____/~~~~~
// x | A | B | C | D | i
//- write SCL ____/~~~~\____
// SDA ==X=========X=
// x | A | B | C | D | i
//- read SCL ____/~~~~\____
// SDA XXXX=====XXXX
// x | A | B | C | D | i
本帖I2C SLAVE模块作者为德国的Yiğit Süoğlu 于22年所写,该模块结代码简练,思路清晰,质量非常高。本文将对其写的I2C SLAVE驱动进行详细介绍。
2.1 模块端口声明
module i2c_slave(
input clk,
input rst,
//Config & Control
output busy,
output data_available, //New data avaible
output data_request,
//Data interface
input [6:0] addr, //I2C address for slave
input [7:0] data_i, //Data in
output reg [7:0] data_o, //Data Out
//I2C pins
input SCL,
inout SDA/* synthesis keep = 1 */);
各端口的含义如下:
其中,rst为高电平有效,在我们一般设计中多使用低电平有效,程序移植时注意处理;
addr为器件地址,当I2C SLAVE模块接收到地址后,可以与我们设置的器件地址进行比较,如果相同我们可以对主机进行响应,并控制状态机进行读写操作等;
data_i为我们接收到I2C MASTER的单字节数据;
data_o为我们将发给I2C MASTER的单字节数据;
SCL和SDA是FPGA与外部IC的I2C通信引脚,作者Yiğit Süoğlu 已经验证过当SCL为100k时通信是正常的。,
2.2状态机和寄存器变量定义
localparam IDLE = 3'b000,
ADDRS = 3'b001,
ADDRS_ACK = 3'b011,
WRITE = 3'b110,
WRITE_ACK = 3'b010,
READ = 3'b111,
READ_ACK = 3'b101,
WAIT_STOP = 3'b100;
reg [7:0] send_buffer, receive_buffer;
reg SDA_d, SCL_d, in_READ_d;
reg [2:0] state;
reg [2:0] counter;
wire counterDONE;
reg start_condition_reg;
作者使用的状态机编码方式为格雷码,每次只变化一个bit,降低了程序出错的风险,建议大家再运用状态机时也使用格雷码。
定义的send_buffer, receive_buffer;比较容易理解,将接收到的数据和准备发送的数据寄存起来;
state为状态机变量,大家平时见到的可能都是current state和next state,这种两个变量还需要进行相互赋值,使用一个state作为状态机变量,程序更直观,不易出错;
counterDONE为计数完成信号,我们常传输一个字节信号,当SCL上升沿进行采样数据后,连续8次就会将这个信号置位,以便让我们知道已经接收或者发送了一个字节的数据;
2.3信号同步和状态定义
//I2C signal edges into system clock domain
always@(posedge clk) begin
SDA_d <= SDA;
SCL_d <= SCL;
in_READ_d <= in_READ;
end
wire SDA_negedge = SDA_d & ~SDA;
wire SDA_posedge = ~SDA_d & SDA;
wire SCL_negedge = SCL_d & ~SCL;
wire SCL_posedge = ~SCL_d & SCL;
wire in_READ_pulse = ~in_READ_d & in_READ;
//Conditions
wire start_condition = SCL & SDA_negedge;
wire stop_condition = SCL & SDA_posedge;
always@(posedge clk or posedge rst) begin
if(rst)begin
start_condition_reg <= 1'b0;
end else begin
case(start_condition_reg)
1'b0: start_condition_reg <= start_condition;
1'b1: start_condition_reg <= in_IDLE;
endcase
end
end
SCL和SDA信号需要同步到FPGA的系统时钟,避免竞争和冒险问题;
SCL和SDA的上升沿和下降沿是我们经常用到的,因此需要定义出来;
状态定义可以看出I2C起始和结束标志信号都已经根据I2C协议定义好了;
上面的always语句是为了在FPGA复位时给状态机一个初始状态。
2.4设定标志位
//Decode states
wire in_IDLE = (state == IDLE);
wire in_ADDRS_ACK = (state == ADDRS_ACK);
wire in_ADDRS = (state == ADDRS);
wire in_WRITE = (state == WRITE);
wire in_WRITE_ACK = (state == WRITE_ACK);
wire in_READ = (state == READ);
wire in_READ_ACK = (state == READ_ACK);
wire in_Get_Data = in_ADDRS | in_WRITE;
assign busy = ~in_IDLE;
//Flags
wire addressed = (addr == receive_buffer[7:1]);
wire read_nwrite = receive_buffer[0];
assign data_available = in_WRITE_ACK;
assign data_request = (in_ADDRS_ACK & addressed) | (in_READ_ACK & ~SDA);
上述代码根据状态机当前所处的状态,定义了wire变量,告诉我们当前状态机处于什么状态;
addressed
read_nwrite
data_available
data_request
上述标志位分别定义了:接收地址中、读写标志位、一个字节数据接收完成和地址匹配后请求主机发送数据过来。
2.5状态机跳转
状态机跳转就比较简单了,根据I2C的读写时序,写出状态跳转逻辑即可。
//State transactions
always@(posedge clk or posedge rst) begin
if(rst) begin
state <= IDLE;
end else begin
case(state)
WAIT_STOP: begin
state <= (stop_condition) ? IDLE : state;
end
IDLE: begin
state <= (start_condition_reg & SCL_negedge) ? ADDRS : state;
end
ADDRS: begin
state <= (counterDONE & SCL_negedge) ? ADDRS_ACK : state;
end
ADDRS_ACK: begin
state <= (SCL_negedge) ? ((addressed) ? ((read_nwrite) ? READ : WRITE) : WAIT_STOP) : state;
end
READ: begin
state <= (counterDONE & SCL_negedge) ? READ_ACK : state;
end
READ_ACK: begin
state <= (SCL_negedge) ? READ : (((SDA_posedge & SDA)? WAIT_STOP : state));
end
WRITE: begin
state <= (stop_condition) ? IDLE : ((counterDONE & SCL_negedge) ? WRITE_ACK : state);
end
WRITE_ACK: begin
state <= (SCL_negedge) ? WRITE : state;
end
default: begin
state <= IDLE;
end
endcase
end
end
2.6 其他逻辑
//Data line handling
wire SDA_Claim = in_READ | (in_ADDRS_ACK & addressed) | in_WRITE_ACK;
wire SDA_Write = (in_READ) ? send_buffer[7] : 1'b0;
assign SDA = (SDA_Claim) ? SDA_Write : 1'bZ;
//sample at posedge of SCL
always@(posedge SCL) begin
receive_buffer <= (in_Get_Data) ? {receive_buffer[6:0], SDA} : receive_buffer;
end
//Data out buffer
always@(posedge clk) begin
data_o <= (in_WRITE_ACK) ? receive_buffer : data_o;
end
//send_buffer
always@(negedge SCL or posedge in_READ_pulse) begin
if(in_READ_pulse) begin
send_buffer <= data_i;
end else begin
send_buffer <= (in_READ) ? {send_buffer[6:0],1'b1} : send_buffer;
end
end
//Count posedges
assign counterDONE = &counter;
always@(posedge SCL or posedge start_condition) begin
if(start_condition) begin
counter <= 3'b111;
end else begin
counter <= counter + {2'd0, (in_Get_Data | in_READ)};
end
end
介绍下counter计数器,理解稍微难一些:
counter <= counter + {2'd0, (in_Get_Data | in_READ)};该语句表示当状态机处于READ、WRITE或者ADDRESS状态时,计数器就会+1,直到8bit传输完成,计数器就会清0.
3.实物仿真
写本帖之前,是打算购买一个USB转I2C模块的,该模块使用的是CH340G芯片,淘宝可以搜到。但是问了多个店铺,均没有win11的驱动,也就是说无法向串口助手那样调试给FPGA发数据调试I2C。
如果大家手里有DSP或者ARM的开发板,也可以将DSP或者ARM的开发板作为主机,通过I2C与FPGA开发板进行通信,由于手边没有其他开发板,工作也较忙,实物仿真暂且搁置了。
|