继APB总线协议之后再来看一下串行总线的I2C传输协议,体验一下串行总线在数据传输的复杂度
I2C总线协议
概述
只有两条同步串行总线:串行数据线(SDA),串行时钟线(SCL)
主器件用于启动总线传输数据,产生时钟以开发传送的器件,此时任何被寻址的器件都被认为是从器件
在总线上主和从,发和收的关系不是恒定的,而是取决于此时数据传送方向
主机发数据给从机:则主机寻址从器件,然后发送数据至从器件,最后由主机终止数据传送
主机接收从器件数据:则主机寻址从器件,然后接收从器件发送的数据,最后由主机终止数据传送
主机负责产生定时时钟和终止数据传送,核心在主机
例如:控制系统就可以作为主机,传感器作为从机
特征
I2C总线是一个真正的多主机总线,如果两个或多个主机同时初始化数据传输,可以通过冲突检测和仲裁防止数据破坏
每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机
数据传输和地址设定由软件设定,非常灵活。总线上的器件增加和删除不影响其他器件正常工作
1 | 假如希望有多个微控制器(MCU)将数据记录到单个存储卡或将文本显示到单个LCD时,这个功能就非常有用。 |
地址
I2C总线上的每一个设备都对应一个唯一地址,主从设备直接的数据传输时建立在地址的基础上,也就是说,主设备在传输有效数据之前要先指定从设备的地址,地址指定的过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的。
不是1次传一个字节吗,为啥是7位的地址?
协议规定再给地址添加一个最低位用来表示接下来数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备读数据。
硬件层
两条数据线需要接上拉电阻
I²C总线(SDA
,SCL
)内部都使用漏极开路驱动器(开漏驱动),因此SDA
和SCL
可以被拉低为低电平,但是不能被驱动为高电平,所以每条线上都要使用一个上拉电阻,默认情况下将其保持在高电平;
- 连接到总线的设备的输出级必须有一个漏极开路或集电极开路来执行“线与“功能。
- 只有单个主设备时,如果总线上没有设备拉伸时钟,则主设备的SCL输出可以是推挽式设计
起始和截止条件
总线数据传输必须以一个起始信号为开始条件,一个结束信号为传输停止条件,这两个信号由主设备产生。
起始和结束信号产生条件:总线在空闲状态时,SCL和SDA都保持高电平
- 当SCL为高电平而SDA由高到低跳变时,表示产生一个起始条件;
- 当SCL为高而SDA由低到高跳变时,产生一个停止条件
在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;停止条件产生后,数据传输的主从设备释放总线,总线再次处于空闲
应答(ACK)&非应答(NACK)
响应应答信号
主设备在SCL线上产生每个时钟脉冲的过程中将在SDA线上传输一个数据位,当一个字节数据位从高到低传输完后,从设备拉低SDA线,即应答位(ACK),表明字节已成功接收,可以发送下一个字节;
主设备产生所有时钟脉冲,包括第九个确认(ACK)时钟脉冲。
在第九个确认脉冲时钟脉冲期间,发送方将释放SDA总线,从而接收方可以把SDA拉低, 而且在脉冲高电平期间,SDA需持续拉低,定义为应答信号(ACK)
非响应应答信号
当SDA在第九个时钟脉冲期间保持为高时,定义为非应答信号(NACK)。
从端没有对该请求做出响应,主服务器可以生成终止传输的停止条件,或者生成启动新传输的重复启动条件。
传输操作
主设备往从设备中写数据
主设备在从设备中读数据
主设备往从设备中写数据,然后重启起始条件,紧接着在从设备中读取数据
主设备在从设备中读数据,然后重启起始条件,紧接着往从设备中写数据
为啥要重启起始条件呢?
因为每次起始的第一个8拍,第8位的读写位就定死了数据的传输方向,只能重启
数据有效性
- SDA线上的数据在SCL高电平必须是稳定的。
- 只有当SCL时钟信号为低时,SDA的数据才可以发生变化。
- 每传输一个数据位产生一个时钟脉冲。
时钟同步和仲裁
同步
两个主设备可以同时在空闲的总线上开始传送数据,必须有一种方法来决定哪一个来控制总线并完成它的数据传送,这是通过时钟同步和仲裁完成的。
时钟同步是连接到SCL总线的I2C接口的“线与”实现的;
当SCL总线从高电平拉低时,主设备将计数他们的低电平时钟,一旦一个主设备时钟拉低,其将把SCL总线也拉低,直到主设备时钟再次拉高;
然而如果另一个主设备的时钟仍旧是拉低的,SCL总线将保持拉低状态;因此,SCL总线被有最长低周期的主设备保持在低电平。在此期间,低周期较短的主设备将进入高电平等待状态。
当所有相关的主设备时钟拉高时,主设备时钟和SCL总线的状态就没有区别了,所有的主时钟开始计算它们的高周期;第一个结束高周期的主设备会将SCL线再次拉低。 这样就产生了一个同步的SCL时钟,它的低周期由低电平最长的主设备时钟确定,而它的高周期由高电平最短的主设备时钟确定。
仲裁
仲裁和同步一样,只有在系统中使用多个主设备时才需要;从设备不参与仲裁程序。
只有在总线空闲的情况下,主设备才可以发起传送。
两个主机可以在启动条件的最小保持时间(tHD;STA)内生成一个启动条件,从而在总线上生成一个有效的启动条件。然后仲裁程序决定哪一个主设备来完成它的传输。
一个主机每发送1个bit数据,在scl处于高电平时,就检查看SDA的电平是否和发送的数据一致,如果不一致,这个主机便知道自己输掉仲裁,然后停止向SDA写数据。也就是说,如果主机一直检查到总线上数据和自己发送的数据一致,则继续传输,这样在仲裁过程中就保证了赢得仲裁的master不会丢失数据。
仲裁输掉的主机在检测到自己输了后也不再产生时钟脉冲,并且在总线空闲时才能重写传输
仲裁的过程可能要经过多个bit的发送和检查,实际上如果两个主机发送的时序和数据完全一致,则两个主机都能正常完成整个数据传输
显示了两个主设备的仲裁过程;
当产生DATA1的主设备数据与SDA线上的实际数据有差异时,DATA1输出被关闭。这并不影响获胜主设备发起的数据传输。
由于I2C总线的控制完全取决于竞争主设备发送的地址和数据,因此没有中央主设备,总线上也没有任何优先级顺序
总线清零
在一些概率极低的情况中,时钟(SCL)会一直卡在低电平,如果I2C设备有硬件复位输入,优先考虑用硬件复位信号重置总线,如果I2C设备没有硬件复位输入,
则激活强制的内部通电复位(POR)电路。
如果数据线(SDA)一直卡在低电平,主设备应该发送9个时钟脉冲。控制总线拉低的设备应该在这9个时钟内释放它。如果没有效果,则使用硬件复位或循环电源来清除总线。
时钟拉伸
时钟拉伸通过保持SCL行较低来暂停事务;期间传输不能继续,直到SCL再次被拉高。
在字节传输级别,设备可能能够以较快的速度接收字节数据,但是需要更多的时间来存储收到的字节或准备传输另一个字节。在字节发送的(ACK)位之后,从设备
可以拉低SCL总线,迫使主设备进入等待状态,直到从设备准备好下一个字节的传输(参见图7)。
在比特级别,诸如微控制器这样的设备,无论是否为i2c总线提供有限的硬件,都可以通过延长每个时钟的低周期来降低总线时钟的速度。
在高速模式下,这个握手功能只能在字节级使用。
保留地址
两组8地址(0000 XXX和1111 XXX)是被保留的为了实现下面的功能
广播寻址(0000 0000)
广播寻址用于同时寻址连接到i2c总线的每个设备;如果设备不需要广播提供的任何数据,它可以NACK来忽略广播地址;如果设备确实需要来自广播的数据,它就会对广播地址进行ACK,并作为一个从接收设备。
如果多个设备都发出了响应,主设备实际上并不知道有多少设备回应;每个能够处理此数据的从接收设备都会对第二个和后面的字节ACK;一般广播地址的含义总是在第二个字节中指定。
有两种情况需要考虑:当最低有效位B为零时。当最低有效位B为1时。
LSB=0
0000 0110 (06h):通过硬件复位和写从设备可编程部分;当接收到这2字节序列时,从设备回应广播地址复位,并接收其地址可编程部分。
必须采取预防措施,以确保设备在施加电源电压后不会拉低SDA或SCL,因为这些低电平会阻塞总线。
0000 0100 (04h):通过硬件编写从地址的可编程部分。行为如上述,但设备不重置。
0000 0000 (00h):此代码不允许用作第二个字节。
LSB=1
当第B位为1时,2字节序列是一个硬件通用调用。
这意味着序列是由硬件主设备传输的,例如键盘扫描器,它可以通过编程传输所需的从地址。
由于硬件主设备事先不知道消息必须传输到哪个设备,因此它只能生成这个硬件通用调用和它自己的地址,并将其标识给系统。
第二个字节中剩下的7位包含硬件主设备地址;这个地址被一个连接到总线的智能设备(例如,一个微控制器)识别,然后总线接受来自硬件设备的信息。
start byte(0000 0001)
存在原因:
并不是每个连接到I2C总线的微控制器都有集成的I2C控制器。这些微控制器必须永久观察I2C线路,以检测I2C传输。这将消耗(主要由轮询完成)大量的CPU时间。为了减少这种CPU功率的浪费,可以用较慢的仲裁方法建立I2C传输。
start byte有何作用
为此,主机发送启动条件,然后是 start byte (’ 00000001 ‘),一个虚拟的应答脉冲和一个重复的启动条件。观察微控制器只能检测SDA上的七个零中的一个来检测I2C传输。这可以通过一个相对缓慢的轮询率来实现。一旦控制器检测到SDA是低的,它就可以切换到一个更高的轮询速率,以便等待重复的启动条件和接下来的I2C传输。
传输结束后,它可以切换回节省cpu功耗的慢轮询速率,以便检测下一次传输。
在开始字节之后产生一个与ACK相关的时钟脉冲,这样做只是为了与总线上使用的字节处理格式相一致,不允许任何设备确认开始字节
Device ID(1111 1xx1)
概念:
设备ID字段是一个可选的3字节只读字(24位),提供以下信息:
- 12位带有制造商名称,每个制造商都是唯一的(例如NXP); 9位带有零件标识,由制造商指定(例如PCA9698) ;3位带有模具修订,由制造商指定(例如RevX)
设备ID是只读的,在设备中硬连接,可以按如下方式访问:
开始条件(S)& 主设备发送预留的设备ID I2C-bus地址,后面跟着设置为0的R/W位(写):11111000。
主设备发送从设备可以识别I2C-bus从地址;LSB是一个Don ‘ t care值;只有一个设备可以识别这个字节(具有i2c总线从地址的那个)。
主设备发送一个重新启动条件(Sr)。主设备发送预留的设备ID I2C-bus地址,后面跟着R/W位,设置为1(读):1111 1001。
接着读设备ID,首先是12制造商比特(第一个字节+第二个字节的高四比特),紧随其后的是九个部分标识位(第二个字节的低四比特+ 第三个字节的高五比特),然后是
三个模具修改部分(第三个字节低四比特)。主设备通过对最后一个字节NACK来结束读取序列,从而重新设置从设备状态并允许主设备发送停止条件
10bit地址
10位地址扩展了地址位数;7位和10位地址的设备可以连接到相同的I2C总线,并且7位和10位地址可以在所有总线速度模式中使用。
10位从地址由开始条件或重复开始条件后的前两个字节组成。第一个字节的前7位是组合1111 0XX,其中最后2位(XX)是10位地址的两个最高有效位(MSB);
第一个字节的第8位是决定传输方向的R/W位。前面描述的7位寻址的所有读/写格式组合都可以用10位寻址实现。
10-bit 地址写
开始条件后跟着10位从地址,每个从设备将从地址(1111 0XX)第一个字节的前7位与它自己的地址进行比较,并检测第八位(R/W方向位)是否为0。
有可能有多个从设备匹配并生成应答信号(A1)。
把以上所有匹配从设备的地址与从地址(XXXX XXXX)第二个字节的8位进行比较,只有一个从设备地址匹配并生成应答信号(A2)。
匹配的从设备地址一直由主地址寻址,直到它接收到一个停止条件(P)或一个重复启动条件(Sr),并且其后面跟着一个不同的从属地址
10-bit 地址读
传输方向在第二个R/W位之后改变;在确认位A2之前,其过程和读相同;重复启动条件(Sr)之后,匹配的从设备会记住它之前被寻址过。
然后这个从设备检查Sr之后的第一个字节的前7位是否与开始条件之后的第一个字节相同,并检测第8位(R/W)是否为1;如果匹配,从设备认为它已经作为一个发送器被寻址并生成确认A3。
从发送设备保持寻址,直到它收到一个停止条件(P)或直到它收到另一个重复启动条件(Sr),且后面跟着一个不同的从设备地址。
VIP开发
接口及功能分析
interface
1 |
|
transcation
考虑总线的特性状态,进行功能特性提取
核心就4个变量:命令cmd,地址adress,data数组,选择是否使用10bit地址
当然还有很多其他的状态:开始和截止位状态,重启状态,响应位状态
base_transcation
1 |
|
master_transaction
总线按bit传输且存在开始、结束、重启、失去仲裁、非响应、时钟拉伸等情况
主端多出的对于事件的可能处理:
主端在发送完一个transcation后是重启还是结束:sr_or_p_gen
主端仲裁:
- 失去仲裁后,是否完成当前transcation发送:abort_if_arb_lost
- 失去仲裁后,要重新尝试几次来完成当前的transcation传输:num_of_retry
- 是否等待别的主端开始,以进入仲裁状态:arbitrate
主端是否发送start byte:send_start_byte
对于非应答做何处理:
是否放弃当前传输:retry_if_nack
尝试几次来完成当前的transcation传输:num_of_retry
1 |
|
slave_transcation
从端决定回复的应答信号以及时钟拉伸
1 |
|
环境搭建
driver
和apb相比,驱动的思想应该用串行的方式来理解,按bit位进行分析
比如apb的一次传输需要用两拍或者更多拍来完成,setup和trans这两个状态就要依次驱动
i2c明显更加复杂,需要考虑地址位,读写位,响应位,结束位等等,但归根结底还是串行的,一样用串行的驱动方式
bfm_common
核心方法:clk_low_offset_gen()
对不同模式驱动的参数进行配置
1 |
|
master_driver_common
核心方法:send_xact(lvc_i2c_master_transaction trans)
1 |
|
start_gen()
- mos管导通,将scl和sda两根线拉高
- 等待1000个外部时钟,将sda置低
- 已经完成启动,但是需要等待sta_hd个外部时钟,将scl置低,因为scl为高时,数据不能变,为低时才真正把数据放到总线上
1 | task lvc_i2c_master_driver_common::start_gen(); |
stop_gen()
等待4个时间
- wait_data_hd_time():数据的hold time,在子类中定义;将sda置为0
- i2c_clk_low个外部时钟,将scl置为1
- sto_su个外部时钟,将sda拉高
- 再等待tbuf_time个外部时钟,释放锁,此时其他master可以通过start_gen来获取锁
1 | task lvc_i2c_master_driver_common::stop_gen(); |
re_start_gen()
和start非常类似,但是需要考虑当前两根线的状态,不能直接置为1
scl在每次传输结束时都置为0,等待i2c_clk_low个外部时钟后,再将其拉高
在低电平期间将sda置1,比scl置1早re_sta_su个外部时钟
1 | task lvc_i2c_master_driver_common::re_start_gen(); |
send_byte
8位bit的发送+判断是否响应
1 | task lvc_i2c_master_driver_common::send_byte(bit[7:0] send_byte); |
接收byte发送应答
和写一样,每次scl为1时采集数据;
8个数据读完后要发送1个应答位
1 | task lvc_i2c_master_driver_common::recv_byte(output bit[7:0] recv_data); |
接收byte发送非应答
1 | task lvc_i2c_master_driver_common::recv_byte_noack(output bit[7:0] recv_data); //the last data received, master will sent out a n-ack to slave |
send_start_byte
1 | task lvc_i2c_master_driver_common::send_start_byte(); |
7bit地址传输
一个bit位的传输逻辑
前面start开启的时候scl已经置0了,数据可以直接放上来;
因为wait_data_hd_time()等待dat_hd个外部时钟,远小于i2c_clk_low。所以在scl低电平期间,数据已经建立;
然后将scl置1,高电平维持i2c_clk_high个外部时钟;
最后将scl置0,一次bit传输结束
可以看到:每次开始,或者1bit传输结束后,scl都置为0,以等待下一个bit的传输
一个byte的传输逻辑
传完7bit地址和1bit的rw,在下一个高电平期间采样,采样sda判断是否为1
为1则有应答,为0则无应答
1 | task lvc_i2c_master_driver_common::rw_slave_7bit_addr(bit[6:0] addr, bit rw); |
10bit地址传输
两个字节,明显要复杂一些,但是串行驱动的本质不变
和7位地址的区别在于:10位地址的读需要重启
写的话类似:
- 先组装第一个字节数组,传第一个字节,然后再传第二个low_address字节
读的话有点意思:
- 第一个字节的读写位同样是0(写),然后再传第二个low_address字节
- 想要修改读写位只能重启re_start_gen(),只需要重传高8位,将读写位改为1
1 | task lvc_i2c_master_driver_common::rw_slave_10bit_addr(bit [9:0] addr, bit rw); |
I2C_WRITE:
- 判断模式是标准还是快速,标准模式直接执行start_gen(),判断是否发送start_byte。快速模式需要先来一套二连,然后执行re_start_gen()
- 选择地址模式并发送地址,处理非应答情况
- 发送transcation中的数据,每个字节都要处理非应答情况
1 | I2C_WRITE: |
I2C_READ:
和I2C_WRITE完全一致
I2C_GEN_CALL:
只需要考虑标准模式:
- 执行start_gen();
- 判断是否发送start_byte
- 发送第一个字节+非应答处理+发送第二个信号+非应答处理
- 循环发送data字节+非应答处理
1 | I2C_GEN_CALL: |
I2C_DEVICE_ID:
1 | I2C_DEVICE_ID: |
master_driver
核心方法是 : run_phase中的consume_from_seq_item_port()
调用的核心驱动方式是子类的:common.send_xact(trans);
没有通过seq_item_port.item_done(rsp)的方式返回rsp,而是调用方法put_response_to_seq_item_port(lvc_i2c_master_transaction xact, int drop = 0);
该方法内部声明了一个带锁线程,当多个master同时驱动时
1 |
|
slave_driver_commom
相对清晰很多
1 | class lvc_i2c_slave_driver_common extends lvc_i2c_bfm_common; |
slave_start()
对总线进行采样,满足开始条件就尝试上锁,由当前slave执行;
1 | task lvc_i2c_slave_driver_common::slave_start(); |
slave_end()
1 | task lvc_i2c_slave_driver_common::slave_end(lvc_i2c_slave_transaction trans); |
data_ana(trans)
从端驱动的核心方法
1 | task lvc_i2c_slave_driver_common::data_ana(lvc_i2c_slave_transaction trans); //check all protocol and collect write/read data to transaction |
8’b 0000_0000(广播)
1 | begin |
8’b1111_1xxx(device_id)
1 | begin |