跳到主要内容

9.4 实验原理

9.4.1 I2C总线介绍

I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。

alt text
I2C总线连接

主器件用于启动总线传送数据,并产生时钟以开放传送的器件,此时任何被寻址的器件均被认为是从器件.在总线上主和从、发和收的关系不是恒定的,而取决于此时数据传送方向。如果主机要发送数据给从器件,则主机首先寻址从器件,然后主动发送数据至从器件,最后由主机终止数据传送;如果主机要接收从器件的数据,首先由主器件寻址从器件.然后主机接收从器件发送的数据,最后由主机终止接收过程。在这种情况下.主机负责产生定时时钟和终止数据传送。

alt text
I2C通信时序

字节格式

发送到SDA 线上的每个字节必须为8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB),如果从机要完成一些其他功能后(例如一个内部中断服务程序)才能接收或发送下一个完整的数据字节,可以使时钟线SCL 保持低电平,迫使主机进入等待状态,当从机准备好接收下一个数据字节并释放时钟线SCL 后数据传输继续。

alt text
I2C字节格式

启动和停止

在时钟线SCL保持高电平期间,数据线SDA上的电平被拉低(即负跳变),定义为I2C总线总线的启动信号,它标志着一次数据传输的开始。启动信号是一种电平跳变时序信号,而不是一个电平信号。启动信号是由主控器主动建立的,在建立该信号之前I2C总线必须处于空闲状态。

在时钟线SCL保持高电平期间,数据线SDA被释放,使得SDA返回高电平(即正跳变),称为I2C总线的停止信号,它标志着一次数据传输的终止。停止信号也是一种电平跳变时序信号,而不是一个电平信号,停止信号也是由主控器主动建立的,建立该信号之后,I2C总线将返回空闲状态。

alt text
启动和停止格式

应答响应

数据传输必须带响应,相关的响应时钟脉冲由主机产生。在响应的时钟脉冲期间,发送器释放SDA 线(上拉电阻拉高),接收器必须将SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平,这种情况下是应答,如果在这个时钟脉冲的高电平期间SDA线没有被拉低则表示没有应答。通常被寻址的接收器在接收到的每个字节后,必须产生一个应答。当从机接收器不应答时,主机产生一个停止或重复起始条件。

alt text
应答响应格式

通信速率

常见的I²C总线依传输速率的不同而有不同的模式:标准模式(100 Kbit/s)、低速模式(10 Kbit/s),但时钟频率可被允许下降至零,这代表可以暂停通信。而新一代的I2C总线可以和更多的节点(支持10比特长度的地址空间)以更快的速率通信:快速模式(400 Kbit/s)、高速模式(3.4 Mbit/s)。

9.4.2 RPS-0521RS模块连接

STEP BaseBoard V4.0底板上的接近光传感器RPR-0521RS模块电路图如下(上拉电阻未显示):

alt text
RPR-0521RS模块电路

上图为接近光传感器RPR-0521RS模块电路,与FPGA硬件接口有I2C总线(SCL、SDA)和中断信号INT,RPR-0521RS是罗姆公司的集成数字环境光和接近传感器,具有体积小、低功耗等优点,被大量应用于手机、笔记本、相机、液晶显示器等电子产品上,环境光ALS可以根据外部环境调节设备屏幕显示亮度,接近距离传感器可以根据应用场景实现产品对应应用,例如接听电话时控制手机关闭显示等,接口采用I2C总线能够支持400KHz的I2C快速模式。

alt text
RPR-0521RS框图

9.4.3 双向端口设计

可综合Verilog模块设计中必须有端口存在,端口有输入input,输出output,双向inout,对于输入和输出型端口我们很好理解,我们来了解一下双向端口信号的处理。

在芯片中为了管脚复用,很多管脚都是双向的,既可以输入也可以输出。在Verilog中即为inout型端口。Inout端口的实现是使用三态门,三态门的第三个状态是高阻态Z。在实际电路中高阻态意味着响应的管脚悬空、断开。

alt text
FPGA引脚输入输出模型

当inout用作输出时,就像平常一样。当inout用作输入时,需要设为高阻态,这样其电平就可以由外部输入信号决定了(这是高阻态的特性)。

双向端口应用案例:

module bid
(
input out_en,
input a,
inout b,
output c
);

assign b = out_en? a : 1'bz;
assign c = b;

endmodule

9.4.4 RPR-0521RS驱动设计

通过前面的了解,我们对于整个I2C总线的驱动原理有了一定的了解,接下来我们根据APDS-9901的芯片手册了解其驱动方法及参数要点。

alt text
RPR-0521RS时序图
alt text
RPR-0521RS时序参数

通过RPR-0521RS时序参数了解,RPR-0521RS支持I2C通信400KHz快速模式同时兼容100KHz的标准模式,还有两种模式下时序中的各种时间参数,本例中我们就采用标准模式完成驱动设计。

首先我们分频得到400KHz的时钟,整个设计都基于该时钟完成,程序实现如下:

//使用计数器分频产生400KHz时钟信号clk_400khz
reg clk_400khz;
reg [9:0] cnt_400khz;
always@(posedge clk or negedge rst_n) begin
if(!rst_n) begin
cnt_400khz <= 10'd0; clk_400khz <= 1'b0;
end else if(cnt_400khz >= CNT_NUM-1) begin
cnt_400khz <= 10'd0; clk_400khz <= ~clk_400khz;
end else begin
cnt_400khz <= cnt_400khz + 1'b1;
end
end

2C时序可以分解成基本单元(启动、停止、发送、接收、发应答、读应答),整个I2C通信都是由这些单元按照不同的顺序组合,我们设计一个状态机,将这些基本单元做成状态,控制状态机的跳转就能实现I2C通信时序。主机每次发送数据都要接收判断从机的响应,每次接收数据也要向从机发送响应,所以发送单元和读应答单元可以合并,接收单元和写应答单元可以合并。

启动时序状态设计程序实现如下:

START:begin //I2C通信时序中的起始START
if(cnt_start >= 3'd5) cnt_start <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_start <= cnt_start + 1'b1;
case(cnt_start)
3'd0: begin sda <= 1'b1; scl <= 1'b1; end //将SCL和SDA拉高,保持4.7us以上
3'd1: begin sda <= 1'b1; scl <= 1'b1; end //每个周期2.5us,需要两个周期
3'd2: begin sda <= 1'b0; end //SDA拉低到SCL拉低,保持4.0us以上
3'd3: begin sda <= 1'b0; end //clk_400khz每个周期2.5us,需要两个周期
3'd4: begin scl <= 1'b0; end //SCL拉低,保持4.7us以上
3'd5: begin scl <= 1'b0; state <= state_back; end //每个周期2.5us,两个周期
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

发送单元和读应答单元合并,时序状态设计程序实现如下:

WRITE:begin //I2C通信时序中的写操作WRITE和相应判断操作ACK
if(cnt <= 3'd6) begin //共需要发送8bit的数据,这里控制循环的次数
if(cnt_write >= 3'd3) begin cnt_write <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end else begin
if(cnt_write >= 3'd7) begin cnt_write <= 1'b0; cnt <= 1'b0; end //复位变量
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end
case(cnt_write)
//按照I2C的时序传输数据
3'd0: begin scl <= 1'b0; sda <= data_wr[7-cnt]; end //SCL拉低,SDA输出
3'd1: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd2: begin scl <= 1'b1; end //clk_400khz每个周期2.5us,需要两个周期
3'd3: begin scl <= 1'b0; end //SCL拉低,准备发送下1bit的数据
//获取从设备的响应信号并判断
3'd4: begin sda <= 1'bz; end //释放SDA线,准备接收从设备的响应信号
3'd5: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd6: begin ack_flag <= i2c_sda; end //获取从设备的响应信号
3'd7: begin scl <= 1'b0;
if(ack_flag)state <= state;
else state <= state_back; end //SCL拉低,如果不应答循环写
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

接收单元和写应答单元合并,时序状态设计程序实现如下:

READ:begin  //I2C通信时序中的读操作READ和返回ACK的操作
if(cnt <= 3'd6) begin //共需要接收8bit的数据,这里控制循环的次数
if(cnt_read >= 3'd3) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end else begin
if(cnt_read >= 3'd7) begin cnt_read <= 1'b0; cnt <= 1'b0; end //复位变量值
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end
case(cnt_read)
//按照I2C的时序接收数据
3'd0: begin scl <= 1'b0; sda <= 1'bz; end //SCL拉低,释放SDA线
3'd1: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd2: begin data_r[7-cnt] <= i2c_sda; end //读取从设备返回的数据
3'd3: begin scl <= 1'b0; end //SCL拉低,准备接收下1bit的数据
//向从设备发送响应信号
3'd4: begin sda <= ack; end //发送响应信号,将前面接收的数据锁存
3'd5: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd6: begin scl <= 1'b1; end //SCL拉高,保持4.0us以上
3'd7: begin scl <= 1'b0; state <= state_back; end //SCL拉低
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

停止时序状态设计程序实现如下:

STOP:begin  //I2C通信时序中的结束STOP
if(cnt_stop >= 3'd5) cnt_stop <= 1'b0; //对STOP中的子状态执行控制cnt_stop
else cnt_stop <= cnt_stop + 1'b1;
case(cnt_stop)
3'd0: begin sda <= 1'b0; end //SDA拉低,准备STOP
3'd1: begin sda <= 1'b0; end //SDA拉低,准备STOP
3'd2: begin scl <= 1'b1; end //SCL提前SDA拉高4.0us
3'd3: begin scl <= 1'b1; end //SCL提前SDA拉高4.0us
3'd4: begin sda <= 1'b1; end //SDA拉高
3'd5: begin sda <= 1'b1; state <= state_back; end //完成STOP操作
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

基本单元都有了,接下来我们需要了解RPR-0521RS驱动的流程,手册上看到RPR-0521RS芯片有很多寄存器,有的配置工作模式,有的配置功能使能,有的返回结果数据,这个需要大家自己查看芯片手册,这里不作讲解。

alt text
RPR-0521RS寄存器布局

RPR-0521RS的一般控制流程是首先配置控制字,包括系统控制字、模式控制字等,也就是寄存器地址40h、41h、42h、43h这几个控制字内容,然后经过一定延时就可以读取ALS和PS数据内容。

根据数据手册,向寄存器写入数据的操作,按照时序

alt text

向reg_addr地址寄存器中写入数据reg_data,程序实现如下

4'd0:   begin state <= START; end   //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end //寄存器地址
4'd3: begin data_wr <= reg_data; state <= WRITE; end //写入数据
4'd4: begin state <= STOP; end //I2C通信时序中的STOP

写操作做成状态机的一个状态,这样重复向寄存器写入数据的操作只需要在这个状态上循环执行就好了,单次写操作状态程序实现如下:

MODE1:begin //单次写操作
if(cnt_mode1 >= 4'd5) cnt_mode1 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode1 <= cnt_mode1 + 1'b1;
state_back <= MODE1;
case(cnt_mode1)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end //寄存器地址
4'd3: begin data_wr <= reg_data; state <= WRITE; end //写入数据
4'd4: begin state <= STOP; end //I2C通信时序中的STOP
4'd5: begin state <= MAIN; end //返回MAIN
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

同理两字节数据连读的操作也做成一个状态,如果连续读写的寄存器地址是连续的,也可以一次操作完成

alt text

程序实现如下

MODE2:begin //两次读操作
if(cnt_mode2 >= 4'd10) cnt_mode2 <= 1'b0; //对START中的子状态执行控制cnt_start
else cnt_mode2 <= cnt_mode2 + 1'b1;
state_back <= MODE2;
case(cnt_mode2)
4'd0: begin state <= START; end //I2C通信时序中的START
4'd1: begin data_wr <= dev_addr<<1; state <= WRITE; end //设备地址
4'd2: begin data_wr <= reg_addr; state <= WRITE; end //寄存器地址
4'd3: begin state <= START; end //I2C通信时序中的START
4'd4: begin data_wr <= (dev_addr<<1)|8'h01; state <= WRITE; end//设备地址
4'd5: begin ack <= ACK; state <= READ; end //读寄存器数据
4'd6: begin dat_l <= data_r; end
4'd7: begin ack <= NACK; state <= READ; end //读寄存器数据
4'd8: begin dat_h <= data_r; end
4'd9: begin state <= STOP; end //I2C通信时序中的STOP
4'd10: begin state <= MAIN; end //返回MAIN
default: state <= IDLE; //如果程序失控,进入IDLE自复位状态
endcase
end

因为用到延时,也设计成一个状态,程序实现如下:

DELAY:begin //延时模块
if(cnt_delay >= num_delay) begin
cnt_delay <= 1'b0;
state <= MAIN;
end else cnt_delay <= cnt_delay + 1'b1;
end

最后我们编程控制状态机按照驱动例程代码中流程运行,程序实现如下:

4'd0:begin dev_addr <= 7'h38; reg_addr <= 8'h40; reg_data <= 8'h0a; state <= MODE1; end  //写入配置
4'd1: begin dev_addr <= 7'h38; reg_addr <= 8'h41; reg_data <= 8'hc6; state <= MODE1; end  //写入配置
4'd2:   begin dev_addr <= 7'h38; reg_addr <= 8'h42; reg_data <= 8'h02; state <= MODE1; end  //写入配置
4'd3:   begin dev_addr <= 7'h38; reg_addr <= 8'h43; reg_data <= 8'h01; state <= MODE1; end  //写入配置
4'd4:   begin state <= DELAY; dat_valid <= 1'b0; end    //12ms延时
4'd5:   begin dev_addr <= 7'h38; reg_addr <= 8'h44;  state <= MODE2; end    //读取配置
4'd6:   begin  prox_dat<= {dat_h,dat_l}; end    //读取数据
4'd7:   begin dev_addr <= 7'h38; reg_addr <= 8'h46;  state <= MODE2; end    //读取配置
4'd8:   begin ch0_dat <= {dat_h,dat_l}; end //读取数据
4'd9:   begin dev_addr <= 7'h38; reg_addr <= 8'h48;  state <= MODE2; end    //读取配置
4'd10:  begin ch1_dat <= {dat_h,dat_l}; end //读取数据
4'd11:  begin dat_valid <= 1'b1; end    //读取数据

9.4.5 系统总体实现

程序中我们做了一个简单的滤波处理,为了保证数据的有效,将瞬间变化太大的采样数据舍弃,程序实现如下:

reg [15:0] prox_dat0,prox_dat1,prox_dat2;
always @(posedge dat_valid) begin
    prox_dat0 <= prox_dat;
    prox_dat1 <= prox_dat0;
    if(((prox_dat1-prox_dat0) >= 16'h800)||((prox_dat1-prox_dat0) >= 16'h800))
        prox_dat2 <= prox_dat2;
    else
        prox_dat2 <= prox_dat0;
end

我们从传感器读取的距离信息有效位数为12位数据,可以设置一个阈值,当采样回来的数据与阈值比较控制手机屏幕的显示与否,本实验要求用能量条的方式显示距离的远近,我们设计一个编码器将0到16‘h3ff的范围控制8个led灯的控制,程序实现如下:

always@(prox_dat2[11:9]) begin
    case (prox_dat2[11:9])
        3'b000: Y_out = 8'b11111110;
        3'b001: Y_out = 8'b11111100;
        3'b010: Y_out = 8'b11111000;
        3'b011: Y_out = 8'b11110000;
        3'b100: Y_out = 8'b11100000;
        3'b101: Y_out = 8'b11000000;
        3'b110: Y_out = 8'b10000000;
        3'b111: Y_out = 8'b00000000;
        default:Y_out = 8'b11111111;
    endcase
end

同时把环境光的传感器数据经过BCD转码后送到数码管显示,在顶层设计中例化这几个模块,将信号连接,程序实现如下:

rpr0521rs_driver u1(
        .clk            (clk            ),  //系统时钟
        .rst_n      (rst_n      ),  //系统复位,低有效
        .i2c_scl        (i2c_scl        ),  //I2C总线SCL
        .i2c_sda        (i2c_sda        ),  //I2C总线SDA
       
        .dat_valid  (dat_valid  ),  //数据有效脉冲
        .ch0_dat        (ch0_dat        ),  //ALS数据
        .ch1_dat        (ch1_dat        ),  //IR数据
        .prox_dat   (prox_dat   )   //Prox数据
    );
decoder u2(
        .rst_n      (rst_n),
        .dat_valid  (dat_valid  ),
        .ch0_dat        (ch0_dat        ),
        .ch1_dat        (ch1_dat        ),
        .prox_dat   (prox_dat   ),
        .lux_data   (lux_data   ),
        .Y_out      (led            )
    );
segment_scan u4(
        .clk(clk),                  //系统时钟 12MHz
        .rst_n(rst_n),              //系统复位 低有效
        .dat_1(lux_data[31:28]  ),  //SEG1 显示的数据输入
        .dat_2(lux_data[27:24]  ),  //SEG2 显示的数据输入
        .dat_3(lux_data[23:20]  ),  //SEG3 显示的数据输入
        .dat_4(lux_data[19:16]  ),  //SEG4 显示的数据输入
        .dat_5(lux_data[15:12]  ),  //SEG5 显示的数据输入
        .dat_6(lux_data[11:08]  ),  //SEG6 显示的数据输入
        .dat_7(lux_data[07:04]  ),  //SEG7 显示的数据输入
        .dat_8(lux_data[03:00]  ),  //SEG8 显示的数据输入
        .dat_en(8'b1111_1111    ),  //数码管数据位显示使能,[MSB~LSB]=[SEG1~SEG8]
        .dot_en(8'b0000_0100    ),  //数码管小数点位显示使能,[MSB~LSB]=[SEG1~SEG8]
        .seg_rck(seg_rck        ),  //74HC595的RCK管脚
        .seg_sck(seg_sck        ),  //74HC595的SCK管脚
        .seg_din(seg_din        )   //74HC595的SER管脚
    );

综合后的设计框图如下:

alt text