本文接着之前的M4系列介绍,对另外一个十分常见的通信总线进行一个介绍,就是IIC总线。
首先,还是找个免费劳动力来做一个官方的介绍,下面这一段话非常全面的介绍了IIC的各个特征,用之前提到的通信特征来总结,IIC是一种串行,同步,半双工,板级有线通信。与SPI对比,其少了一个数据线,只有一个数据线,因此只能实现半双工通信。
第二段是描述了IIC的拓扑结构,IIC一共有两个线,一根时钟线SCL,用来提供主从通信的时钟基准,另外一根是SDA数据线,用来传输数据用;之所以称作为总线,与前面的SPI一样,表明其可以同时挂接多个设备,设备之间可以进行通信。一主多从的物理拓扑图如下图所示:
与上一篇的SPI对比可以发现少了CS片选线,那么主机是怎么选择通信的对象的呢,注意上面的简介中提到了一个地址的概念,IIC的通信就是通过地址来确定通信的对象的,根据原理图和芯片手册获取到对应器件的信息后,主机将地址发送到总线上,从设备接收地址信息进行判断,如果地址与自己的地址一致,则给主机一个反馈信号,进而二者建立通信;而地址不对应的从器件是不会有反馈的。具体的地址信息放到后面做介绍,这里做个了解即可。
除了上面的一主多从模式,IIC还有多主多从的拓扑结构,结构图如下图所示:
这样的连接方式可以实现多个主机操作同一组IIC从设备。
这里对IIC的通信流程做个大致的介绍,如下图所示,假设此时A为主设备,也就是我们的单片机,B设备就是常用的IIC从机AT24cxx。
第一个通信目标是:A将数据写入B
其通信流程如下:
1.微控制器 A 主机 寻址微控制器 B 从机
2.微控制器 A 主机 发送器 发送数据到微控制器 B 从机 接收器
3.微控制器 A 终止传输,总线空闲,两条线路都恢复高电平
第二个通信的目标是: A从B中读取数据,其具体的通信流程如下:这里需要补充一点,在IIC总线中,主机才有对时钟线的操作权利,从机是没有的,因此,从机并不能主动给主机传输数据,必须要接收到主机给从机相应的获取指令后,才可以将数据传输到数据线上,进而被主机接收。因此A从B中获取数据的流程如下:
1.微控制器 A 主机 寻址微控制器 B 从机
2.微控制器 A 主机 接收器 从微控制器 B 从机 发送器 接收数据
3.微控制器 A 终止传输总线空闲,两条线路都恢复高电平
•1. 只要求两条总线线路 一条串行数据线 SDA 一条串行时钟线 SCL
• 2.每个连接到总线的器件都可以通过唯一的地址和一直存在的简单的主机 从机关系软件设定地址 主机可以作为主机发送器或主机接收器
• 3.它是一个真正的多主机总线 如果两个或更多主机同时初始化数据传输可以通过冲突检测和仲裁防止数据被破坏
• 4.串行的 8 位双向数据传输位速率在标准模式下可达 100kbit/s ;快速模式下可达 400kbit/s; 高速模式下可达 3.4Mbit/s,此处可以发现,IIC的最高传输速率是不如SPI的。
• 5.片上的滤波器可以滤去总线数据线上的毛刺波 保证数据完整
• 6. 连接到相同总线的 IC 数量只受到总线的最大电容 400pF 限制
以上描述来自——IIC总线规范
广州周立功单片机发展有限公司 Tel: (020)38730976 38730977 Fax:38730925 http://www.zlgmcu.com
关于IIC的一些更详细的介绍可以参考以下几篇文章,上面的图片也是来自这两篇博客:
关于为什么要加上拉电阻以及电阻阻值的计算在里面也有描述。
一文搞懂I2C通信总线——http://t.csdn.cn/RjJob
IIC原理超详细讲解—值得一看——http://t.csdn.cn/zkq43
关于STM32的IIC通信,与SPI类似,也是有两种方案来解决一种是使用控制器来实现,另一种是配置STM32内部的控制器来实现通信,实际使用过程中常用的反而是IO模拟的方式,STM32内部的IIC控制器数量是3个,数量不算多
虽然每个IIC控制器都对应有两组IO口,但是我们去数据手册的复用映射表查看就知道了,支持IIC控制器服用的IO口,大多都有着其他的复用功能,复用为IIC后其他的功能就需要找其他IO口,而且其他的复用功能,并不能很好地使用IO模拟来实现,而IIC可以通过IO模拟很好地实现,也就是说,只要是GPIO口理论上就可以实现IIC的时序模拟,因此大多是的IIC通信都是使用的IO模拟。
当然,使用控制器的IIC会有更高的传输速率以及稳定性,但是其配置过程也是十分复杂的,一不留神就会配置错误导致通信不上,这里我们后面做个对比即可。
由于常用的是GPIO模拟的IIC,所以本文就重点介绍一下IO模拟的IIC实现过程。
使用GPIO模拟的IIC,参考之前的SPI模拟时序,第一件事就是要看IIC通信的时序图,然后根据时序图来实现IIC的模拟时序。
来看一下IIC通信的完整数据帧,这里用的是7位器件地址,一位读写位,八位数据位,一次传输两个数据的情况为例。
从上图可以看出完整的数据传输包含了1.开始信号;2.器件地址;3.读写位;4.应答信号;5.传输的数据;6.结束信号。而且观察上图可以发现:
SDA 线上的数据在时钟的高电平周期保持稳定; SDA线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变;也就是说,时钟线高电平期间,数据线是只读的不能操作,而时钟线低电平期间,数据线是可写的,可以写入对应的高低电平。
接下来及分别对这里面的各个部分进行介绍。
起始条件一般由主机产生。
在 SCL 线是高电平时 SDA 线从高电平向低电平切换,表示起始信号;
根据这个时序图可以大致知道起始信号的样子,但是还不知道其具体的持续时间,这时就需要打开对应的芯片手册进行查看了,在芯片手册的读写周期范围里面会有具体的时间要求,根据以下表格,可以写出起始信号的伪代码。
起始条件函数
{
时钟线拉低
delay_us(5);//IIC的延时时间 时间只能大于表中的时间,不能小于表中时间
数据线拉高 //为数据线的下降沿做准备
时钟线拉高
delay_us(5);//IIC的延时时间 空闲时间不得少于4.7us这里给个5us
数据线拉低
时钟线拉低 //安全模式 维持完整周期
}
上面的是手册给出的示意图,接下来来看一个实际的示波器抓取的波形,如下图所示:
原图链接——http://www.eepw.com.cn/article/234058.htm
橙色框内的就是起始信号,时钟线和数据线都是先被拉高,然后持续一段时间,随后,数据线拉低,这时一个起始信号就已经完成了,不过为了保证能够正确的接收数据,时钟线也会被拉低,因为只有在时钟线低电平的时候数据线才能被操作。
传输过程中是高位先发的MSB。
在起始条件 S 后,发送了一个从机地址 这个地址共有 7 位(下图中红色框内的器件地址),紧接着的第 8 位是数据方向位 R/ W 0 表示主机对从机写入数据,写 1 表示主机需要从从机中读取数据。
IIC每次传输的都是一个八位数据,起始信号发送完毕后需要紧跟一个8位数据,这个八位数据的高七位是从器件的地址,最低位是读写位,为了方便理解这里还是直接看个实例,假设我们使用AT24C02作为从器件,那么它的器件地址就是如下图所示的,其中前四位是厂商规定的,1010,而后面的三位A0-A2是由硬件电路来确定的。
如下图,硬件连接中A0-A2都是被拉低了,所以此时的7位器件地址位:101 0000(0x60)
而其对其写入数据的时的写地址为:1010 0000 (0xA0)
读地址为1010 0001(0XA1)
同样的,可以来看一下实际的示波器抓取的波形,下图中的器件地址是011 1000 也就是0x38,最后一位的读写位是0,标明是写入数据,因此最终发送的八位数据是0111 0000(0x70)。
通过上面的两个图,可以模拟出发送八位数据的时序,这里还需要再提示一下,前面说过,IIC通信过程中,只有在时钟线拉低的情况下,数据线才能被操作,因此,我们在写入每一位之前都需要保证,时钟线是拉低的状态才可以。伪代码如下:
发送数据:时钟低电平状态发送 发送8次
发送数据(地址)函数(数据)
{
for(u8 i=0;i<8;i++)//发8次
{
时钟线拉低
If(data & 0x80 >>i)//高位先发
{
数据线拉高
}
Else
{
数据线拉低
}
拉高时钟线
}
调用接收应答函数
拉低时钟线 //安全模式
}
在介绍应答信号之前还需要插播一个小知识,注意如果被问到IIC总线可以挂接多少个从器件时,可以参考如下的描述作答。
接下来就是应答信号,关于应答位,首先它只有一位,也只有两个状态,要么是0,要么是1;其中0表示应答成功,1表示应答失败。
数据传输必须带响应;相关的响应时钟脉冲由主机产生,在响应的时钟脉冲期间,发送器(主机)释放 SDA 线(拉高);
在响应的时钟脉冲期间 接收器必须将 SDA 线拉低(0)使它在这个时钟脉冲的高电平期间保持稳定的低电平,此时表示应答成功;否则,也就是数据线没有保持在低电平,说明应答失败。
在使用IO模拟的时候,我们需要获取SDA管脚的高低电平来判断是否应答成功,这里又涉及到了一个知识点,前面发送数据以及起始信号时,SDA一直是作为数据脚的,此时需要判断高低电平,既要做输入又要做输出,这时,根据之前GPIO的介绍,有两种方案,一是发送时配置为推挽输出,到了要接收的时候再配置一遍IO口,将其转换为无上下拉的输入模式,也就是需要我们不断地切换模式;
另外一个方案是配置为开漏输出,此时GPIO的高端MOS被屏蔽了,无法输出高电平,当GPIO的ODR配置为1时,SDA数据线不再受此GPIO的控制,而是有上拉电阻拉成空闲状态,由从机控制SDA的高低电平。此时直接使用IDR进行读取即可。
而且还需要注意,前面提到过,时钟线是交给主机控制的(也有例外,但是大部分是这样),而数据线的写操作,必须在时钟线拉低的情况下才可以,因此,为了从机能够操作数据线,还需要使用主机为其提供一个低电平的时钟信号。
同样的,注意示波器的波形,此时从机也是正常将SDA拉低了,说明从机应答正常,接下来可以开始传输数据了。
伪代码如下:
u8 主机接收从机应答函数
{
//数据线模式 输入模式
拉低时钟线
拉高数据线 //让数据线处于空闲 可以接收数据
拉低时钟线 //让从机发送应答
拉高时钟线 //主机接收应答
If(数据线状态)//检测到高表示应答失败
return 1
else
return 0
拉低时钟线 //安全模式
}
关于数据传输,与前面提到的地址写入是一致的,在操作的时候根据芯片手册的指令表,写入对应功能的数据即可。
接下来就是结束信号了,当数据传输完成后,需要一个结束信号,来告诉从机通信结束。当 SCL 是高电平时,SDA 线由低电平向高电平切换表示停止条件;结束信号的时序图如下图所示:
实际的示波器图形如下图所示:
停止条件函数
{
拉低时钟线
数据线拉低 //为上升沿做准备
拉高时钟线
delay_us(5);
拉高数据线
delay_us(5);
}
至此,关于主机对从机写入数据的时序模拟就结束了,实际使用过程中,根据对应的时序图调用对应的函数即可,例如对于AT24C02写入数据
代码流程如下:
①发送一个起始信号
②器件写地址
③接收应答
④器件的存储上的绝对地址
⑤接收应答
⑥写入数据
⑦接收应答
⑧停止信号
上面介绍了主机对从机写入数据的过程以及所需要函数逻辑,也就是主机 发送器发送到从机 接收器 的传输;方向不会改变,一直是主机发送数据,从机接收数据后做应答,主机检测应答,然后继续发送数据,直到数据发送完成后,主机发送结束信号。
实际使用过程中,对于IIC的从设备,往往是一些传感器和存储器,不仅需要对其写入对应的数据,还需要进行获取数据,获取的具体流程与上面的发送大致一样,但是数据接收和应答这个需要修改一下。
在第一个字节(读地址)后;主机立即读从机 。
此时的第一次响应还是由从机产生,之后就变成了从机发送数据,主机接收数据,每次接受一个数据后主机发送个应答位。直到数据接收完成,最后一个数据接收完成后,主机会发送一个非应答信号,告诉从机接收结束,紧接着主机产生结束信号,接收此次通信。
可以注意到,在读取数据时,起始信号,读地址发送,从机应答,结束信号都是与前面一样的,只是多了一个数据接收以及一个主机应答的操作,根据IIC的通信需求,每次接收完一个八位数据位后接收端就需要返回给发送端一个应答,只有应答信号正常时才会进行下一步操作,前面的数据发送是主机MCU的发送器发送给从机的接收器的,从机接收八位数据后会返回一个应答位,主机检测应答正常后继续发送。而此时的读取数据是从机发送器发送数据,主机的接收器接收数据当主机接收了八位数据后,需要给从机返回一个应答位,以便于从机下一位的数据发送,当接收最后一位数据后,主机需要产生一个非应答信号告诉从机接收完成。
前面提到过,只有在时钟线拉高的情况下才可以进行数据的接收,而且一次接收就需要接收八位,且是高位在前,因此,接收数据的伪代码如下:
u8 接收数据函数
{
u8 data
//数据线切换成输入模式
拉低时钟线
拉高数据线 //让数据线处于空闲 可以接收数据
for(u8 i=0;i<8;i++)
{
拉低时钟线 //从机发送数据
拉高时钟线 //主句可以开始接受数据
data = data << 1; //先进行左移再接收
if(数据线)
{
data |= 1;
}
}
//调用发送应答函数
拉低时钟线 ///安全模式
}
除了接收八位数据以外,就是需要给从机发送应答信号,来告诉它是否传输成功,其实思路与之前的接收应答位差不多,就是在时钟拉低的时候将数据线对应拉高或者拉低就可以了,其中拉低表示应答成功,拉高表示应答失败。
伪代码如下:
发送应答函数(ack)
{
拉低时钟线
delay_us(5); //此时间需要从参考对应的从设备的数据手册
if(ack==0)
拉低数据线 //主机发送应答信号
else
拉高数据线 //主机发送非应答信号
delay_us(1);
拉高时钟线 //主机帮从机拉高时钟线从而 从机获取应答
delay_us(5);
拉低时钟线 //安全模式
}
还是举个例子吧,假设需要从AT24C02中读取一个字节其时序流程如下图所示:
具体的流程如下:
①开始信号
②发送写地址
③接收应答
④发送绝对地址//需要读取的数据的地址
⑤接收应答
⑥开始信号
⑦发送读地址
⑧接收应答
⑨读取一个字节
⑩发送非应答
⑪停止信号
由于笔者手头没有IIC的器件了,所以此处就只留一个IIC的初始化代码了。
根据前面的介绍,一个IIC的模拟时序需要有以下的函数:
1.GPIO口的初始化,SCL初始化为推挽输出,SDA初始化为开漏输出,这里笔者用开漏输出,如果想要推挽输出,然后通信过程中切换GPIO模式的,可以去看一下正点原子的代码,他们的就是使用的推挽输出,然后接收时再配置为输入模式。
2.编写起始信号代码,结束信号代码,发送数据代码,接受数据代码,接受应答的代码,发送应答的代码。
IIC的代码如下:
#include "stm32f4xx.h" // Device header
#include "iic.h"
static void delay_us(u32 us)
{
u32 i = 168 / 4 * us;
while(i)
{
i--;
}
}
/*******************************************
*函数名 :iic_IO_init
*函数功能 :IIC管脚初始化配置
*函数参数 :无
*函数返回值:无
*函数描述 :
SCL-----PB8 通用推挽输出
SDA-----PB9 通用开漏输出
*********************************************/
void iic_IO_init(void)
{
//端口时钟使能
RCC->AHB1ENR |= (1<<1);
//端口模式配置
GPIOB->MODER &= ~((3<<16) | (3<<18)); //清零
GPIOB->MODER |= ((1<<16) | (1<<18)); //通用输出
//端口输出类型配置
GPIOB->OTYPER &= ~(1<<8); //推挽
GPIOB->OTYPER |= (1<<9); //开漏
//端口输出速度配置
GPIOB->OSPEEDR &= ~((3<<16) | (3<<18)); //2M
//IO口保持电平
GPIOB->ODR |= (1<<8);
GPIOB->ODR |= (1<<9);
}
/*******************************************
*函数名 :iic_star
*函数功能 :IIC其实信号函数
*函数参数 :无
*函数返回值:无
*函数描述 :
*********************************************/
void iic_star(void)
{
//时钟线拉低
SCL_L;
//数据线高电平
SDA_OUT_H;
//时钟线拉高
SCL_H;
delay_us(5);
//数据线拉低
SDA_OUT_L;
delay_us(5);
//安全
SCL_L;
}
/*******************************************
*函数名 :iic_stop
*函数功能 :IIC停止信号函数
*函数参数 :无
*函数返回值:无
*函数描述 :
*********************************************/
void iic_stop(void)
{
SCL_L;
SDA_OUT_L;
SCL_H;
delay_us(5);
SDA_OUT_H;
delay_us(5);
}
/*******************************************
*函数名 :iic_send_ack
*函数功能 :IIC发送应答函数
*函数参数 :u8 ack
*函数返回值:无
*函数描述 :
ack 0 应答
ack 1 不应答
主机发送应答表示继续接收数据
主机发送不应答表示不再接收数据
*********************************************/
void iic_send_ack(u8 ack)
{
SCL_L;
delay_us(5);
if(ack == 0)
{
SDA_OUT_L; //应答信号
}
else
{
SDA_OUT_H; //不应答信号
}
delay_us(1);
SCL_H;
delay_us(5);
SCL_L; //安全
}
/*******************************************
*函数名 :iic_get_ack
*函数功能 :主机检测应答函数
*函数参数 :无
*函数返回值:u8
*函数描述 :
应答返回值是0
不应答返回值是1
*********************************************/
u8 iic_get_ack(void)
{
u8 ack = 0;
/*将IO口切换为输入*/
SCL_L;
SDA_OUT_H; //切换输入
/*检测应答*/
SCL_L; //主机帮助从机拉低时钟线从机才自动发送应答信号
delay_us(5);
SCL_H; //主机可以读数据线
delay_us(5);
if(SDA_INT )
{
ack = 1;
}
else
{
ack = 0;
}
//安全
SCL_L;
return ack;
}
/*******************************************
*函数名 :iic_send_byte
*函数功能 :主机通过IIC发送一个字节函数
*函数参数 :u8 data
*函数返回值:无
*函数描述 :
*********************************************/
void iic_send_byte(u8 data)
{
u8 i;
for(i=0;i<8;i++)
{
SCL_L; //可以写数据
delay_us(5);
if(data & 0x80)
SDA_OUT_H;
else
SDA_OUT_L;
delay_us(1);
SCL_H; //主机帮助从机拉高时钟线,从机才可以读这一位数据
delay_us(5);
data = data << 1; //下一位数据
}
//安全
SCL_L;
}
/*******************************************
*函数名 :iic_read_byte
*函数功能 :主机通过IIC接收一个字节函数
*函数参数 :无
*函数返回值:u8
*函数描述 :
*********************************************/
u8 iic_read_byte(void)
{
u8 data;
u8 i;
/*将IO口切换为输入*/
SCL_L;
SDA_OUT_H;
/*读数据*/
for(i=0;i<8;i++)
{
SCL_L; //主机帮助从机拉低时钟线
delay_us(5);
SCL_H; //主机可以读数据
delay_us(5);
data = data << 1;
if(SDA_INT)
data= data | 0x01;
}
//安全
SCL_L;
return data;
}
需要特别提醒的是IIC的延时一定要准确,一定一定要准确,大部分的软件模拟IIC通信异常都是延时的问题。
关于IIC的介绍就先记录到此,关于控制器实现IIC的大家可以自己搜索一下,下面两篇文章中也有详细介绍,这路笔者不再做介绍了,文中如有不足,欢迎大家批评指正,关于IIC和SPI的实际使用,后面有机会再做补充,因为手上的最小系统板实在是外设有限,目前只能介绍这么多了。
STM32硬件I2C与软件模拟I2C超详解http://t.csdn.cn/8ljI9
【STM32】入门(七):I2C硬件控制方式http://t.csdn.cn/1zZHP
1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信
整个M4的介绍基本上结束了,还有一个看门狗和一个位带操作,这个我是打算鸽了,笔者最近正在准备一个板子,因为这个系列主要使用的寄存器来编程控制,而且用的是M4系列的;其实在实际使用过程中M3系列的还是占比比较大的,所以下一个系列可能使用标准库,围绕这个板子进行一个介绍,然后插入一点FreeRTOS或者是用STM32CubeMX配置,用HAL库来进行。当然,前提是这个板子能够正常调通。而且最近立创那边好像也开了一个GD32的小车项目,这个大家有兴趣的也可以去试试水。
欢迎大家提供意见和建议哟,感谢大家的支持和点赞。