嵌入式IIC总线

  这篇详细介绍嵌入式的IIC总线,方便以后写该总线的器件驱动。

一、IIC总线讲解

  IIC的硬件管脚为 VCC,GND,SDA,SCL。IIC的主要构成只有两个双向的信号线,一个是数据线SDA,一个是时钟线SCL。IIC总线有主从之分。

  IIC总线实现的方式分为两种:硬件IIC & 软件模拟IIC。硬件IIC有主从之分;当然,软件I2C也是标准的I2C协议,当然有分主从,但一般情况下,软件IIC为主机模式,即发送请求接收响应信息。为什么要写从机呢?mcu 对 mcu ?)。由于每种MCU的硬件IIC总线配置各不相同,且有些芯片的IIC有Bug(Stm32),故接下来只讲 软件IIC。

  • 由于实际应用上,IIC总线通讯速率本身就不高,因此,硬件IIC总线 & 软件IIC总线
    本身速率方面就没多差大差距,不需要考虑IIC总线切换为软、硬件实现方式会给程序带来隐患。
  • 无论是 硬件IIC 还是 软件IIC ,两种方式只是提供最基础的桥梁——提供了读、写1字节方式。如何调用IIC从器件,还是得查对应IIC从器件的datasheet。IIC总线好比中文的拼音,具体要怎么说话、说什么话,还是得看datasheet。

二、 (软件)IIC总线

2.1 基本知识

  软件IIC,也能更好让我们了解IIC总线协议的实现方式。

  I2C总线通过上拉电阻接正电源。即当总线空闲时,两根线均为高电平。如此,连在总线上的任一器件输出的低电平,都可以使得总线的信号变低,也就是说各器件的SDA和SCL都是线”与”关系。

数据位(1\0)有效性规定:I2C总线进行数据传送时,时钟信号为高电平期间,SDA线上的数据必须保持稳定;只有在SCL线的信号为低电平器件,SDA线的才可进行高低电平状态变化。

起始信号、终止信号、应答信号

  • 起始信号:SCL线为高电平期间,SDA线由高电平向低电平跳变(下降沿)—-是一种电平跳变时序信号
  • 终止信号:SCL线为高电平期间,SDA线由低电平向高电平跳变(上升沿)—-是一种电平跳变的时序信号
  • 应答信号:在接收数据的IC(接收器)在接收到8bit数据后,向发送数据的IC(发送器)发出特定的低电平脉冲,表示已收到数据。即发送器在时钟脉冲9期间释放数据线,这样接收器就可以反馈一个应答信号。ACK(低电平)—-规定为有效应答位,NACK(高电平),规定为非应答位,表示接收器接收该字节咩有成功。

2.2 软件模拟IIC驱动程序函数编写

  在IIC程序设计中,都是以8bit为基础进行数据的传输

  • IO管教初始化
  • 发出起始信号
  • 发出终止信号
  • 发出应答ACK
    • 功能要求:由于IIC为双向数据通信,当从机发送完数据,主机也需要发送应答信号来说我接收到你的信息了,此时从机才可变为接收状态,接收来自主机的数据。
  • 发出应答NACK
    • 功能要求:当IIC程序运行到主机读取从机数据完成,需要停止此次数据传输时,主机发送一个发出主无应答信号,从机接收到后就停止发送数据,并释放SDA线;之后主机才可发送终止信号,停止此次数据的传输。
  • 发送一个字节数据
    • 基本思路:SCL在为0时,可以进行SDA数据的配置,当SCL为1时,SDA数据一定要锁定。其次为数据的移位,将待发送数据与0x80进行与运算,获得最高位的数据,通过8次循环完成1byte的数据发送。
  • 读取一个字节,并发送ACK或NACK(发送NACK基通知从机发送器结束数据发送,释放SDA线(SDA接口置1)
    • 功能要求:发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号(故读取完需要发送 ACK 或 NACK )
  • 等待ACK应答
    • 功能要求:当IIC主机进行获取数值时,主机需要等待从机的应答信号,以此来判断从机是否完成了数据的接收。从主机方看,为IIC等待ASK函数。
    • 基本思路:通过 延时等待从机的ACK是否发送出来,如果发送出来,则函数返回0,主机可继续发送数据,如果返回1,则从机没有应答,此时需要停止IIC数据传输。防止出现错误数据。

2.3 从具体I2C器件中读写数据

主机写(发送)从机数据

主机读(接收)从机数据

Ps:IIC器件往往是 器件地址+0 为写数据 ,器件地址+1 为读数据

主机读从机的情况分为两种:

  • 读操作之前,都是需要进行一次写操作(写入读地址),表明你要读的是哪个地址的数据,然后在进行一次读操作(故有两个器件地址);(这是一般情况)
  • 直接进行读操作,截取所需数据段

  因为有些IIC器件(从机),例如24C02,当你需要读取它的数据,你要跟它说读取哪个地址数据,故先进行写操作;有些IIC器件,例如SD2403,它的时间日期地址是固定的(寄存器地址:00H-06H),因此读取该器件数据时,直接进行读操作,然后读出来7个数据,截取00H-06H的数据后,停止读取(这是特殊情况)

DEVICEADDRESS(器件地址)

  器件地址的8位地址信息因器件而异;

三、软件IIC实例

3.1 EEPROM_24C02通信基础——IIC协议

  24C02是一个可储存 256(8bit)字节数据的EEPROM,因此他的Word Address为8bit(单字节);而他的器件地址如下:

据2.3.1图所示,主机对从机写操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void AT24CXX_WriteOneByte(u16 WriteAddr, u8 DataToWrite)
{
IIC_Starts(); //发出起始信号
IIC_Send_Byte(0xA0); //写器件地址+写操作
IIC_Wait_Ack();

//IIC_Send_Byte(WriteAddr>>8); //发送高地址,适用于更高容量的EEPROM
//IIC_Wait_Ack();

IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite);
IIC_Wait_Ack();
IIC_Stop();
delay_ms(10); //等AT24C02写数据
}

据2.3.1图所示,主机对从机读操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
IIC_Send_Byte(0XA0); //发送器件地址0XA0,写操作
IIC_Wait_Ack();

//IIC_Send_Byte(ReadAddr/256); //发送高地址,适用于更高容量的EEPROM
//IIC_Wait_Ack();

IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0); //读一个字节数据完成,并发出No_Ack(输入参数:0)
IIC_Stop();
return temp;
}

3.2 24C02程序拓展——datasheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//在AT24CXX里面的指定地址开始写入长度为Len的数据
//该函数用于写入16bit或者32bit的数据.
//WriteAddr :开始写入的地址
//DataToWrite:数据数组首地址
//Len :要写入数据的长度2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{
u8 t;
for(t=0;t<Len;t++)
{
AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
}

//在AT24CXX里面的指定地址开始读出长度为Len的数据
//该函数用于读出16bit或者32bit的数据.
//ReadAddr :开始读出的地址
//返回值 :数据
//Len :要读出数据的长度2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{
u8 t;
u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}
//检查AT24CXX是否正常
//这里用了24XX的最后一个地址(255)来存储标志字.
//如果用其他24C系列,这个地址要修改
//返回1:检测失败
//返回0:检测成功
u8 AT24CXX_Check(void)
{
u8 temp;
temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX
if(temp==0X55)return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255,0X55);
temp=AT24CXX_ReadOneByte(255);
if(temp==0X55)return 0;
}
return 1;
}

//在AT24CXX里面的指定地址开始读出指定个数的数据
//ReadAddr :开始读出的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
//在AT24CXX里面的指定地址开始写入指定个数的数据
//WriteAddr :开始写入的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}

3.3 实时时钟_SD2403——IIC总线

  SD2403是一个实时时钟,他的Word Address为8bit(单字节);而他的器件地址如下:

据2.3图所示,主机对从机写操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
static uint8_t SD2403_WriteOneByte(uint8_t addr,uint8_t data)
{
if(!SD2403_start())return(false);
SD2403_SendByte(0x64);
SD2403_WaitAck();
SD2403_SendByte(addr);
SD2403_WaitAck();
SD2403_SendByte(data);
SD2403_WaitAck();
SD2403_stop();
return(true);
}

据2.3图所示,主机对从机读操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint8_t SD2403_ReakOneByte(uint8_t addr)
{
uint8_t res=0;
//先写入要读取的寄存器
if(!SD2403_start()) return false;
SD2403_SendByte(0x64);
if(!SD2403_WaitAck())
{
SD2403_stop();
return false;
}
SD2403_SendByte(addr);
SD2403_WaitAck();

//再读取的寄存器数据
SD2403_start();
SD2403_SendByte(0x65);
SD2403_WaitAck();
res=SD2403_ReceiveByte(0);
SD2403_stop();
return res;
}

3.4 SD2403程序拓展——datasheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
uint8_t SD2403_ReadTimeDate(_strTimeDate *pstrTimeDate)
{
uint8_t i,dat[7];
if(!SD2403_start()) return false;
SD2403_SendByte(0x65);
if(!SD2403_WaitAck())
{
SD2403_stop();
return(false);
}
for(i=0; i<7; i++)
{
dat[i]=SD2403_ReceiveByte();
if(i==2)
dat[2]=BCDTODEC(dat[2]&0x7F);//24小时舍弃最高位(区分12/24小时制)
else
dat[i]=BCDTODEC(dat[i]);
*(&pstrTimeDate->ucSec +i)=dat[i];
if (i!=6) //最后一个数据不应答
{
SD2403_ACK();////ACK 低
}
}
SD2403_No_ACK();//ACK 高结束
SD2403_stop();

return(true);
}
uint8_t SD2403_WriteTimeDate(uint8_t *pstrTimeDate)
{
uint8_t *set_time,i;
set_time=pstrTimeDate;
SD2403_WriteTimeOn();
if(!SD2403_start())return(false);
SD2403_SendByte(0x64);
if(!SD2403_WaitAck())
{
SD2403_stop();
return(false);
}
SD2403_SendByte(0x00);//设置写起始地址
SD2403_WaitAck();
for(i=0; i<7; i++)
{
if(i==2)
SD2403_SendByte(0x80|DECTOBCD(*set_time));//最高位区分12/24小时制(1为24小时制)
else
SD2403_SendByte(DECTOBCD(*set_time));
SD2403_WaitAck();
set_time++;
}
SD2403_stop();
SD2403_WriteTimeOff();
return(true);
}
/******写SD2403允许程序******/
static uint8_t SD2403_WriteTimeOn(void)
{
if(!SD2403_WriteOneByte(0x10,0x80))return(false);
SD2403_WriteOneByte(0x0f,0x84);
return(true);
}
/******写SD2403禁止程序******/
static uint8_t SD2403_WriteTimeOff(void)
{
if(!SD2403_WriteOneByte(0x0f,0))return(false);
SD2403_WriteOneByte(0x10,0);
return(true);
}

四、拓展——软件IIC从机

  要实现IIC从机功能,最核心的部分就是如何精确的抓住IIC_SCL,也就是IIC主机发出来的时钟信号。只有抓住精确的时钟SCL,才能正确的读取到SDA的数据,才能真正模拟出IIC时序。

  但是要抓住SCL信号可不容易,IIC最高速度有400K,最小有效脉宽达到1.4us(数字0/1),最小脉冲是0.8us(应答和STOP信号产生的尖刺),采用中断来识别SCL是不可能的做到的,因为即使在最高主频72MHZ情况下,STM32最小指令周期是1/72(us),从SCL中断发生到STM32进入中断响应,至少要要40个指令周期,也就是40/72(us),加上堆栈操作及变量,很可能已经错过了SCL信号。

因此根据IIC主机的速度,从机实现方法分为两种:

  1. 采用中断方式识别SCL(适用于总线速度较慢)
  2. 采用查询方式识别SCL(适用于总线速度较快)

  反过来言之,当你做成一个(从机)模块,实现软件模拟IIC,实际上还会因为IIC主机的因素来决定你这个模块的实际使用效果。如果是做模块化,还是推荐使用硬件IIC的方式实现。

-------------本文结束感谢您的阅读-------------