引文:本文将由iic介绍、iic模拟应用顺序描述。首次接触关于硬件相关知识,相关知识也是在实操中获取,如有错误,望指正。
IIC——串行通讯总线
IIC是由 PHILIPS 公司开发发两线式串行总线,用于连接微处理器和外部IIC设备。起初专用于音频和视频设备,现在各种电子设备中都有广泛应用。
IIC特性
IIC总线有两条总线线路,一条是串行数据线(SDA),一条是串行时钟线(SCL)。SDA负责数据传输,SCL负责数据传输的时钟同步。IIC设备通过这两条总线连接到处理器的IIC总线控制器上。一种典型设备连接如图所示:

- 每连接到IIC总线上的设备都有着唯一的设备地址。
- 串行的8位双向数据传输,位速率在标准模式下为100kb/s;快速模式下可达到400kb/s;高速模式下可以达到3.4Mb/s。
- IIC总线上在某一时刻只能有一个主机,可以有多个从机,在进行通信前,只能由主机发起,由主机结束。
IIC标准通信过程
IIC总线状态大致上有:起始态、传输态、应答态、停止态;在端口上来看其实就是输入输出电平。
在这里以实际设备为例,我所用的从设备是rn6752,严格按照数据手册。

上图为写操作的时序,上面的为写一个8位数据操作,时期罗列就是如下:
- 主机对总线作起始状态,表示将要通信;
- 主机向从机发送从机地址并附上一个0;
从机地址:这里从机地址是7位,在图中可以看到从机地址在高7位,所以需要将从机地址左移一位
slave addr << 1,因为是写操作,这里不需要其他操作,若是读,则需要对其或1,也就是(write addr | 1)。
- 从机发送应答信号,我们作为主机,对应答进行接收,当然不接受也行;
- 主机向从机发送某一寄存器地址(8位);
- 从机发送应答信号;
- 主机发送一个8位的数据到从机指定的寄存器中。
- 从机发送应答信号;
- 主机对总线作停止状态,表示结束通信;

上图为读操作的时序,上面的为读一个8位数据操作,时期罗列就是如下:
- 主机对总线作起始状态;
- 主机向从机发送从机地址并附上一个0;
- 从机发送应答信号;
- 主机向从机发送某一寄存器地址(8位);
- 从机发送应答信号;
- 主机对总线作停止状态;
- 主机向从机发送从机地址并附上一个1;
- 从机发送应答信号;
- 主机读取一个八位数据;
- 主机回复一个no ack 应答,表明不再接收;
- 主机对总线作停止状态;
上述部分步骤可省略,作为初学者严格按手册步骤编写,避免出现其他问题;
程序编写
将上述各个阶段,按照IIC时序图来实现。

上图为网上iic时序图,这里参考作为状态编写;
在写每一个阶段之前,明确之前的状态;
起始
SDA、SCL均为高电平,由SDA拉低后SCL再拉低;
static void i2c_start(void)
{
SCL_OUTH;
SDA_OUTH;
DELAY_TIME;
SDA_OUTL;
DELAY_TIME;
SCL_OUTL;
}
停止
SDA低电平、SCL高电平,将SDA拉高;
static void i2c_stop(void)
{
SDA_OUTL;
DELAY_TIME;
SCL_OUTH;
DELAY_TIME;
SDA_OUTH;
}
主机等待应答
主机发送完数据后,从机会将SDA拉低,当检测到SDA拉低,则说明从机应答;
static uint8_t I2C_WaitToAck(void)
{
uint8_t time= 0;
uint8_t redata = 1;
SDA_IN; //因为主机状态改为接收,故将SDA端口设置为输入模式;
DELAY_TIME;
SCL_OUTH;
DELAY_TIME;
//这里从机将对SDA做出改变(响应)
while(time++ < 250)
{
if(!gpio_get_value(SDA))// 返回:
{
redata = 0;//应答(ack低电平)低-0/NULL
break;
}
DELAY_ACK;
}
SCL_OUTL;
DELAY_TIME;
return redata;
}
主机发送应答
当主机接收完八位数据,还需要接收,则接收完八位后会主动拉低SDA总线;
static void send_ack(void)
{
SDA_OUTL;//主动拉低
DELAY_TIME;
//一个时钟周期
SCL_OUTH;
DELAY_TIME;
SCL_OUTL;
DELAY_TIME;
SDA_OUTH; //释放总线
}
主机发送非应答
当主机接受完一个八位数据后,不再接收数据,则会主动拉高SDA总线;
static void send_no_ack(void)
{
SDA_OUTH;//主动拉高
DELAY_TIME;
SCL_OUTH;
DELAY_TIME;
SCL_OUTL;
}
读数据(8位)
static u8 i2c_read_data(void)
{
SDA_IN; //输入模式
uint8_t value = 0;
uint8_t i = 0;
//先读对高位
for(;i < 8; i++)
{
//从机发送数据,收到数据进行左移
value <<= 1;
SCL_OUTH;
DELAY_TIME;
if( gpio_get_value(SDA))//为0/NULL-低电平
value |= 0x01;
SCL_OUTL;
DELAY_TIME;
}
return value;
}
写操作(8位)
static u8 i2c_send_data(u8 Byte)
{
u8 i;
/* 先发送高位字节*/
for(i = 0;i < 8; i ++)
{
if(Byte &0x80)
SDA_OUTH;
else
SDA_OUTL;
DELAY_TIME; //Z_LOW
SCL_OUTH;
DELAY_TIME; // Z_HIGH
SCL_OUTL;
if(i == 7)
SDA_OUTH; /*释放SDA总线*/
Byte <<= 1;
}
return 0;
}
上述就是各个时期的函数,通过对指定的IO口进行电平变化,以此来实现IIC通信,上述部分语句为宏定义,阅读者可根据实际情况去实现对端口电平变换,因为是模拟IIC,在引用端口时,需要对指定端口设置成IO模式。
实际操作:
参考设备使用的I2C,查询芯片手册指定I2C对应的GPIO端口,同时查询这个端口对应的模式寄存器,这里置0为GPIO模式,找到模式寄存器的地址,这个地址为基地址+偏移地址构成,查询数据手册,对寄存器对应i2c口的指定位进行赋值(0);
u32 reg; reg = readl(PINCTL_REG_BASE + 0xcc); reg &= ~((0x3<<4) | (0x3<<6));//第4、5、6、7位 置0 writel(reg, PINCTL_REG_BASE + 0xcc);上述表示获取这个寄存器初值,因为这里SDA、SCL为寄存器的4、5、6、7号位,采用位操作进行赋值,其他位不变,再对寄存器进行写值;具体的readl、writel函数参考实际情况调用;
一次读时序
严格按照文章前部分,参考rn6752手册;
u8 i2c_read_reg(u8 I2cSlaveAddr, u8 I2cRegAddr)
{
u8 data;
i2c_start();
i2c_send_data(I2cSlaveAddr<<1);//写 |0
if(I2C_WaitToAck())
{
printf("send addr_w no ack\n");
goto ack;
}
i2c_send_data(I2cRegAddr);
if(I2C_WaitToAck())
{
printf("send reg no ack\n");
goto ack;
}
i2c_stop();
i2c_start();
i2c_send_data((I2cSlaveAddr<<1)|1);//读 |1
if(I2C_WaitToAck())
{
printf("send addr_r no ack\n");
goto ack;
}
data = i2c_read_data();
send_no_ack();
i2c_stop();
return data;
ack:
return NULL;
}
一次写时序
u8 i2c_send_reg(u8 I2cSlaveAddr, u8 I2cRegAddr, u8 *data)
{
i2c_start();
i2c_send_data(I2cSlaveAddr<<1);
if(I2C_WaitToAck())
{
printf("send addr_w no ack\n");
goto ack;
}
i2c_send_data(I2cRegAddr);
if(I2C_WaitToAck())
{
printf("send reg no ack\n");
goto ack;
}
i2c_send_data(*data);
if(I2C_WaitToAck())
{
printf("send data no ack\n");
goto ack;
}
i2c_stop();
printf("transfer success\n");
return 0;
ack:
return NULL;
}
具体实验
#define RN6752_SLAVE_ADDR (0x2C)
#define RN6752_REG_ADDR (0X04)
实验一
获取rn6752任意一寄存器的默认值。参考数据手册,我这里选用了rn6752的id寄存器;

printf("read_data = %x\n",i2c_read_reg(RN6752_SLAVE_ADDR, RN6752_REG_ADDR));
当实验结果为1时,则表明读时序无误;
实验二
参考实验一,再对寄存器进行写值,换一个可读写的寄存器,这里选用了色调寄存器首先读取寄存器值,然后在对其进行写值,接着再次进行读取;

u8 data = 0x40;
printf("read_data = %x\n",i2c_read_reg(RN6752_SLAVE_ADDR, RN6752_REG_ADDR));
i2c_send_reg(RN6752_SLAVE_ADDR,RN6752_REG_ADDR, &data);
printf("read_data = %x\n",i2c_read_reg(RN6752_SLAVE_ADDR, RN6752_REG_ADDR));
实验结果:
实验三
改变不同时钟频率,通过示波器观测IIC通信过程;
首先讲一下时钟频率,这里直接理解为传输八位数据时方波周期的倒数。文章前段也介绍了IIC的三种模式速率,在这里由延时时间来进行换算得到,比如当设延时时间为5us时,其周期则为10us,那么时钟频率为1/10us,等于100KHz。相当于就是标准模式下的最高速率;

上图为rn6752在IIC通信时对应的时间频率规则;看最后一栏,规定SCL时钟频率最高为400KHz,那么一个方波周期就是2.5us,现在通过实验去验证当时钟频率超过了400KHz时,还能否继续通信;
按照延时时间来设定,也就是半个时钟周期,分别设20us、5us、3us、2us、1us、无延时,观测其波形;
| 延时时间 | 理论周期时间 | 实际周期时间 | 理论时钟频率 | 实际时钟频率 |
|---|---|---|---|---|
| 20us | 40us | 45us | 25KHz | 22KHz |
| 5us | 10us | 15us | 100KHz | 66KHz |
| 3us | 6us | 10us | 166KHz | 100KHz |
| 2us | 4us | 8us | 250KHz | 125KHz |
| 1us | 2us | 5.5us | 500KHz | 181KHz |
| 0us | 0 | 2us | ∞ | 500KHz |
下图为延时20us——时间时钟频率为25KHz时的波形图,在传输过程中比较稳定,结果无误。

下图为延时5us——时间时钟频率为100KHz时的波形图,在传输过程中比较稳定,结果无误。

下图为无延时时。在代码层面表示,两行代码运行启动时间,半个周期几乎近似于1us——实际的时钟频率为500KHz,这远大于设备定义的最大时钟频率(400Khz),可以看到在高频传输下,I2c通信过程在部分上升沿及下降沿会带有一定的失真,但是也不排除是示波器的原因,在串口打印的结果上,显示的数据无异,这表明,在一定情况下此设备采用iic_GPIO模拟方式通信可达到500KHz的高频。

实验四
在实际应用场景中测试。采用自己模拟IIC去替换工程原始的IIC通信API,显示倒车影像。
1、首先了解rn6752调用IIC的入口函数,在其中了解到,原工程内的IIC实质上将一系列变量与函数封装在adap结构体中,在这其中主要的是alog结构体,而在这个结构体中只有一个函数指针,指向一个函数名,这个函数名可指向两类传输函数,一个是硬件的,一个是模拟的,这就相当于在上层进行了封装,具体调用在上层函数调用时决定。其次adap结构体中,就是一些保护机制和名字,这些按照具体场景理应需要,在我模拟的过程中暂时没有加。
2、弄清楚传输具体过程,在rn6752调用时,其首要就是msg,在工程中定义到msg由四个成员组成首要就是从机地址,然后就是它在结构体内部定义了一些状态标志位用以实时检测通信状况,得到具体的异常情况,再去决定是否继续通信。
3、从上往下看,实际上每调用一次transfer,通信过程中只进行了一次写寄存器操作,也就是说每次调用,只传输了8个字节的data,按照这个逻辑只需要将data、reg_addr这两者改为函数形参即可,故在i2c_send_reg这个函数部分地方修改并在rn6752中调用即可。
直接上结果:

本文详细介绍了IIC串行通讯总线的工作原理、特性,并通过模拟实现IIC通信的各个阶段,包括起始、传输、应答和停止状态。文中还提供了具体的IIC时序函数实现,以及针对rn6752芯片的读写操作示例。通过不同时钟频率的实验,验证了模拟IIC通信在不同速度下的稳定性,并在实际应用中替代原工程IIC API完成倒车影像显示功能。

8268

被折叠的 条评论
为什么被折叠?



