在剖析了《深入浅出玩转FPGA》的串口代码和IIC控制器代码、xilinx官方的xilinx的iic控制器(参见书《FPGACPLD设计工具──Xilinx ISE使用详解》)、《片上系统设计思想与源代码分析》一书中带有wishbone接口的iic控制器后,本文尝试对以上做一些总结,并分析不同的iic控制器的实现区别。
上一讲,我们分析了串口代码的实现以及testbench的设计,这一讲,我们开始分析不同的iic控制器的实现并总结。 下一讲将继续另2种带有总线接口和更充分实现iic协议的例子,敬请期待。
2、IIC控制器
2.1 特权的iic控制器
关于IIC的协议就不再赘述了,可以看看网上的一些资料,主要是scl和sda信号线的高低电平的变化的要求和一些约定。实际上的iic是支持共享总线,但是任何时候只能由一个主设备,这样就需要仲裁。但是特权的例子只是模拟了iic的时序接口,做了一个最基本的控制,这点需要明确。他的例程实现的效果是按下键1,iic控制器写入EEPROM芯片AT24CXX,按下键2,读出存储的值并显示在数码管上。
2个文件和一个顶层文件:
module iic_top( clk,rst_n, sw1,sw2, scl,sda, sm_cs1_n,sm_cs2_n,sm_db //数码管片选 ); input clk; // 50MHz input rst_n; //复位信号,低有效 input sw1,sw2; //按键1、2,(1按下执行写入操作,2按下执行读操作) output scl; // 24C02的时钟端口 inout sda; // 24C02的数据端口 output sm_cs1_n,sm_cs2_n; //数码管片选信号,低有效 output[6:0] sm_db; //7段数码管(不包括小数点) wire[7:0] dis_data; //在数码管上显示的16进制数 iic_com iic_com( .clk(clk), .rst_n(rst_n), .sw1(sw1), .sw2(sw2), .scl(scl), .sda(sda), .dis_data(dis_data) ); led_seg7 led_seg7( .clk(clk), .rst_n(rst_n), .dis_data(dis_data), .sm_cs1_n(sm_cs1_n), .sm_cs2_n(sm_cs2_n), .sm_db(sm_db) ); endmodule
改顶层模块比较好理解。注意,scl这里仅为out型,即只能做主设备。
先看看iic_com实现
输入输出接口:
input clk; // 50MHz input rst_n; //复位信号,低有效 input sw1,sw2; //按键1、2,(1按下执行写入操作,2按下执行读操作) output scl; // 24C02的时钟端口,很明显只是作为主机,并不能接收时钟 inout sda; // 24C02的数据端口 output[7:0] dis_data; //数码管显示的数据输出8位给数码管显示
按键检测:
//-------------------------------------------- //按键检测 reg sw1_r,sw2_r; //键值锁存寄存器,每20ms检测一次键值 reg[19:0] cnt_20ms; //20ms计数寄存器 //这个的习惯是,计数与判断分成2个always来写 always @ (posedge clk or negedge rst_n) if(!rst_n) cnt_20ms <= 20'd0; else cnt_20ms <= cnt_20ms+1'b1; //不断计数 always @ (posedge clk or negedge rst_n) if(!rst_n) begin sw1_r <= 1'b1; //键值寄存器复位,没有键盘按下时键值都为1 sw2_r <= 1'b1; end else if(cnt_20ms == 20'hfffff) begin //50mhz,这个时间为10的6次方乘以20ns即为20ms sw1_r <= sw1; //按键1值锁存 sw2_r <= sw2; //按键2值锁存 end //这个按键锁存并不到位,并没有与的操作这里并没有像特权按键检测之前那样对键值做一些与的操作,防止被检测到触发了多次,所以并没有什么效果,单此处也并不在意触发的次数的多少。具体的按键消抖可以看特权专门的那一章,其思路是收到的键值效果与20ms前比较。
iic支持的传输速度有多种,此处设置为100k,下面即是对时钟分频的处理,需在产生这么时钟,并区分出上升沿,下降沿等。
//--------------------------------------------- //分频部分 reg[2:0] cnt; // cnt=0:scl上升沿,cnt=1:scl高电平中间,cnt=2:scl下降沿,cnt=3:scl低电平中间 reg[8:0] cnt_delay; //500循环计数,产生iic所需要的时钟 reg scl_r; //时钟脉冲寄存器 always @ (posedge clk or negedge rst_n) if(!rst_n) cnt_delay <= 9'd0; else if(cnt_delay == 9'd499) cnt_delay <= 9'd0; //计数到10us为scl的周期,即100KHz,50MHZ else cnt_delay <= cnt_delay+1'b1; //时钟计数 always @ (posedge clk or negedge rst_n) begin if(!rst_n) cnt <= 3'd5; else begin case (cnt_delay) //将一个50%的时钟clk划分为4个时间点,画图可知 9'd124: cnt <= 3'd1; //cnt=1:scl高电平中间,用于数据采样 9'd249: cnt <= 3'd2; //cnt=2:scl下降沿 9'd374: cnt <= 3'd3; //cnt=3:scl低电平中间,用于数据变化 9'd499: cnt <= 3'd0; //cnt=0:scl上升沿 default: cnt <= 3'd5; endcase end end `define SCL_POS (cnt==3'd0) //cnt=0:scl上升沿 `define SCL_HIG (cnt==3'd1) //cnt=1:scl高电平中间,用于数据采样 `define SCL_NEG (cnt==3'd2) //cnt=2:scl下降沿 `define SCL_LOW (cnt==3'd3) //cnt=3:scl低电平中间,用于数据变化 always @ (posedge clk or negedge rst_n) if(!rst_n)
scl_r <= 1'b0; else if(cnt==3'd0)
scl_r <= 1'b1; //scl信号上升沿 else if(cnt==3'd2)
scl_r <= 1'b0; //scl信号下降沿 assign scl = scl_r; //产生iic所需要的时钟500对应50M的时钟正好是100khz,这里的思路是吧一个时钟周期分成4个间距相等的关键的时间节点,自己画图就知道了,后续这些宏定义即代表应该是电平变化的那个阶段。scl根据阶段输出0或1,很好理解。
//--------------------------------------------- //需要写入24C02的地址和数据 `define DEVICE_READ 8'b1010_0001 //被寻址器件地址(读操作) `define DEVICE_WRITE 8'b1010_0000 //被寻址器件地址(写操作) `define WRITE_DATA 8'b0001_0001 //写入EEPROM的数据 `define BYTE_ADDR 8'b0000_0011 //写入/读出EEPROM的地址寄存器 reg[7:0] db_r; //在IIC上传送的数据寄存器 reg[7:0] read_data; //读出EEPROM的数据寄存器
这里是对开发板上的at24c02的实际情况的设定,根据该芯片手册,读写的地址如上。
接下来就是状态机了:
//--------------------------------------------- //读、写时序 parameter IDLE = 4'd0; parameter START1 = 4'd1; //scl为高电平时的sda表示启动 parameter ADD1 = 4'd2; //add1表示的写地址位,会延续8个 parameter ACK1 = 4'd3; //等待应答 parameter ADD2 = 4'd4; parameter ACK2 = 4'd5; parameter START2 = 4'd6; parameter ADD3 = 4'd7; parameter ACK3 = 4'd8; parameter DATA = 4'd9; parameter ACK4 = 4'd10; parameter STOP1 = 4'd11; parameter STOP2 = 4'd12; reg[3:0] cstate; //状态寄存器 reg sda_r; //输出数据指示寄存器 reg sda_link; //输出数据sda信号inout方向控制位,1表示从发数据到ee中 reg[3:0] num; //计数8位 always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin cstate <= IDLE; sda_r <= 1'b1; sda_link <= 1'b0; num <= 4'd0; read_data <= 8'b0000_0000; end else case (cstate) IDLE: begin sda_link <= 1'b1; //数据线sda为output sda_r <= 1'b1; if(!sw1_r || !sw2_r) begin //SW1,SW2键有一个被按下 db_r <= `DEVICE_WRITE; //送器件地址(写操作) cstate <= START1; end else cstate <= IDLE; //没有任何键被按下 end START1: begin if(`SCL_HIG) begin //scl为高电平期间 sda_link <= 1'b1; //数据线sda为output sda_r <= 1'b0; //拉低数据线sda,产生起始位信号,364行assign输出该信号 cstate <= ADD1; num <= 4'd0; //num计数清零 end else cstate <= START1; //等待scl高电平中间位置到来 end ADD1: begin if(`SCL_LOW) begin //scl为低电平期间才可以变化数据 if(num == 4'd8) begin //发完8个,不再发了,准备接收应答 num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK1; end else begin cstate <= ADD1; num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; //高位在前 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送器件地址,从高位开始 end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD1; end ACK1: begin if(/*!sda*/`SCL_NEG) begin //注:24C01/02/04/08/16器件可以不考虑应答位,下降沿表示应答 cstate <= ADD2; //从机响应信号 db_r <= `BYTE_ADDR; // 要写或读的地址 end else cstate <= ACK1; //等待从机响应 //一直等,没有冗余机制 end ADD2: begin if(`SCL_LOW) begin //下降沿开始数据变化,发送地址 if(num==4'd8) begin num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK2; end else begin sda_link <= 1'b1; //sda作为output num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始) cstate <= ADD2; end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD2; end ACK2: begin if(/*!sda*/`SCL_NEG) begin //从机响应信号 if(!sw1_r) begin //这个建是写 cstate <= DATA; //写操作,进入data状态 db_r <= `WRITE_DATA; //写入的数据 end else if(!sw2_r) begin //读 db_r <= `DEVICE_READ; //送器件地址(读操作),特定地址读需要执行该步骤以下操作 cstate <= START2; //读操作 end end else cstate <= ACK2; //等待从机响应,一直等着,知道案件按下 end START2: begin //读操作起始位 if(`SCL_LOW) begin //为低的时候等高电平 sda_link <= 1'b1; //sda作为output sda_r <= 1'b1; //拉高数据线sda cstate <= START2; end else if(`SCL_HIG) begin //scl为高电平中间 sda_r <= 1'b0; //拉低数据线sda,产生起始位信号 cstate <= ADD3; end else cstate <= START2; end ADD3: begin //送读操作地址 if(`SCL_LOW) begin if(num==4'd8) begin num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK3; end else begin num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; //读器件地址 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始) cstate <= ADD3; //等到满足送出8位再来 end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD3; end ACK3: begin //拉低表示应答,其实应该是从机拉低表示应答,这里全部是主机自己根据时间来掐时间认为正确 if(/*!sda*/`SCL_NEG) begin cstate <= DATA; //从机响应信号 sda_link <= 1'b0; //数据线为z,表示准备接受数据 end else cstate <= ACK3; //等待从机响应 end DATA: begin if(!sw2_r) begin //读操作 if(num<=4'd7) begin cstate <= DATA; if(`SCL_HIG) begin num <= num+1'b1; case (num) 4'd0: read_data[7] <= sda; //先接受高位,eeprom中读出的数据 4'd1: read_data[6] <= sda; 4'd2: read_data[5] <= sda; 4'd3: read_data[4] <= sda; 4'd4: read_data[3] <= sda; 4'd5: read_data[2] <= sda; 4'd6: read_data[1] <= sda; 4'd7: read_data[0] <= sda; default: ; endcase // read_data[4'd7-num] <= sda; //读数据(高bit开始) end // else if(`SCL_NEG) read_data <= {read_data[6:0],read_data[7]}; //数据循环右移 end else if((`SCL_LOW) && (num==4'd8)) begin num <= 4'd0; //num计数清零 cstate <= ACK4; end else cstate <= DATA; end else if(!sw1_r) begin //写操作 sda_link <= 1'b1; if(num<=4'd7) begin cstate <= DATA; if(`SCL_LOW) begin sda_link <= 1'b1; //数据线sda作为output num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //写入数据(高bit开始) end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //写入数据左移1bit end else if((`SCL_LOW) && (num==4'd8)) begin num <= 4'd0; sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态 cstate <= ACK4; end else cstate <= DATA; end end ACK4: begin if(/*!sda*/`SCL_NEG) begin // sda_r <= 1'b1; cstate <= STOP1; end else cstate <= ACK4; end STOP1: begin if(`SCL_LOW) begin sda_link <= 1'b1; sda_r <= 1'b0; cstate <= STOP1; end else if(`SCL_HIG) begin sda_r <= 1'b1; //scl为高时,sda产生上升沿(结束信号) cstate <= STOP2; end else cstate <= STOP1; end STOP2: begin if(`SCL_LOW) sda_r <= 1'b1; else if(cnt_20ms==20'hffff0) cstate <= IDLE; //防止死循环,又跳到了idle状态 else cstate <= STOP2; end default: cstate <= IDLE; endcase end
assign sda = sda_link ? sda_r:1'bz;<span style="white-space:pre"> </span>//准备接收数据时为z状态,表示悬空的三态 assign dis_data = read_data;<span style="white-space:pre"> </span>//eeprom中读出的数据给输出给数码管显示状态机还是很清晰的,idle状态下等待按键进入状态循环。根据时序,start信号应该是产生一个下降沿,即sda_link(这个是控制输出的)为1,sda_r为0,进入add1状态,否则一直等到那个分频后的iic的clock的高电平然后该控制器主动发出一个起始信号。add1中等到低电平期间发送器件地址(协议规定sda只能在scl为低时变化),直至8位发完,回到高阻态。记住前一个状态中sda_link为1,所以还是对外输出。ack1状态是等待从机应答,协议规定是从机拉低sda(因为sda是被上拉的,如果从机释放后会被上拉电阻拉到1,但是从机有意继续拉低一个时钟周期,则主机可以判断是从机再应答)产生应答,这里我们不考虑从机应答,设想一切正常,则我们只需等到那个时间点,自动跳转到下一个状态。add2则是要写的地址了,与前面类似,等待发完。ack2再次接受应答,还是一样的处理,这个时候要根据按键与否来判断下面进入哪个状态了,1键是写,db_r为要写入的值,2键是读,得从新发器件地址(影响中iic对读有一些与写有不一样的地方)。按照写的流程则进入data状态。按照读的流程,则再来一次start2,add3依旧是器件地址,接下来ack3,这样才进入data状态。读和写2种方式进入的data状态,得判断。若读操作,从sda读数据到寄存器中,注意是每个时钟高电平期间(SCL_HIG),因为iic协议规定高电平期间锁存数据,先读的是高位,ack3中sda_link已经为0了,表示接受数据,为高阻态。若写,先发高位,注意是低电平SCL_LOW,低电平才可数据变化,sda_link需设为1。等待ack4的响应,stop1为产生了一个停止位。延时一段时间20ms,stop2中状态又回到idle。
所以整个的思路还是很清晰的,但是并没有接受实践的从机的应答,也没有冗余机制,通信失败了怎么办,而是主机一直在哪个地方等,可能会卡死,所以这个仅仅算一个iic时序模拟器吧,有点这种感觉。
最后数据传给dis_data到数码管显示。
简单说下数码管显示:
module led_seg7( clk,rst_n, dis_data, sm_cs1_n,sm_cs2_n,sm_db ); input clk; // 50MHz input rst_n; // 复位信号,低有效 input[7:0] dis_data; //显示数据 output sm_cs1_n,sm_cs2_n; //数码管片选信号,低有效 output[6:0] sm_db; //7段数码管(不包括小数点) reg[7:0] cnt; always @ (posedge clk or negedge rst_n) if(!rst_n) cnt <= 8'd0; else cnt <= cnt+1'b1; //------------------------------------------------------------------------------- /* 共阴极 :不带小数点 ;0, 1, 2, 3, 4, 5, 6, 7, db 3fh,06h,5bh,4fh,66h,6dh,7dh,07h ;8, 9, a, b, c, d, e, f , 灭 db 7fh,6fh,77h,7ch,39h,5eh,79h,71h,00h */
parameter seg0 = 7'h3f, seg1 = 7'h06, seg2 = 7'h5b, seg3 = 7'h4f, seg4 = 7'h66, seg5 = 7'h6d, seg6 = 7'h7d, seg7 = 7'h07, seg8 = 7'h7f, seg9 = 7'h6f, sega = 7'h77, segb = 7'h7c, segc = 7'h39, segd = 7'h5e, sege = 7'h79, segf = 7'h71; reg[6:0] sm_dbr; //7段数码管(不包括小数点) wire[3:0] num; //显示数据 assign num = cnt[7] ? dis_data[7:4] : dis_data[3:0]; //用2个数码管,每个数码管为4位 assign sm_cs1_n = cnt[7]; //数码管1常开 //一下只显示一个数码管,开一个就关另一个,50%到50%的显示比例 assign sm_cs2_n = ~cnt[7]; //数码管2常开 always @ (posedge clk) case (num) //NUM值显示在两个数码管上 4'h0: sm_dbr <= seg0; 4'h1: sm_dbr <= seg1; 4'h2: sm_dbr <= seg2; 4'h3: sm_dbr <= seg3; 4'h4: sm_dbr <= seg4; 4'h5: sm_dbr <= seg5; 4'h6: sm_dbr <= seg6; 4'h7: sm_dbr <= seg7; 4'h8: sm_dbr <= seg8; 4'h9: sm_dbr <= seg9; 4'ha: sm_dbr <= sega; 4'hb: sm_dbr <= segb; 4'hc: sm_dbr <= segc; 4'hd: sm_dbr <= segd; 4'he: sm_dbr <= sege; 4'hf: sm_dbr <= segf; default: ; endcase assign sm_db = sm_dbr; endmodule
总的来说,思路还是比较清晰的,这是一个状态机的例子,与上一讲的串口的实现方式不一样,一个是状态跳转,一个是多个always块中的信号传递来实现控制。这里面也有作者的很多个人的习惯,比如cnt‘的always作者习惯分成2部分来写等等。
但是这只是一个非常粗陋的iic控制器,不接受从机的应答,相对于完整的iic协议来说,非常不完整,并且移植性也不好。我们在学单片机时,知道有的单片机有iic,我们只需在写51代码时,操纵寄存器即可,那么那个部件是怎么实现,显然跟我们这里讲的这种方式是不一样的。
下一讲就来讲讲xilinx官方的一个完整的iic的控制器和or1200中带有wishbone总线接口的iic控制器的实现,相比于这种方式完整得多。