#申请原创# 本章将介绍CW32的IIC接口,并最终点亮一块OLED屏幕,如果你对如何编写各种模块的驱动代码束手无策,那本系列教程的IIC章节或许能让你受益匪浅。
Inter-Integrated Circuit Bus,集成电路总线,简称IIC总线。这是一种半双工同步总线协议,这个分类很好地概括了IIC总线的特点:
1.作为总线协议,可以在一根“信息主干道”上接入多个通信节点(也就是通信设备);
2.同一时间只能有一个通信节点能够在总线上讲话;
3.同步传输意味着这种传输方式至少需要一根时钟线;
IIC总线使用2根线来传输信号:时钟线和数据线,相比于其他的总线协议,这种传输方式更节省IO资源,由于多数情况下IIC仅用于同一块集成电路板上不同模块之间的通信,所以它并不能传输很长的距离,速度也不是那么快,但硬件布线简单,且同一块电路板都会使用同一个电源的正负极,因此IIC总线相当实用。
本文不会详细介绍IIC总线的时序,这里只对其通信流程进行概括,并着重介绍数据链路层的相关内容。从流程上看,IIC总线协议的通信过程大致如下。
假设有A、B两个设备,他们使用IIC总线协议来传递信息,协议规定至少要有一个设备作为主机,其他设备作为从机,IIC是同步通信,协议规定主机来提供时钟,因此只有主机可以主动发起一次通信。假设现在A需要向B传递一个信息DATA,那过程就是这样:
设备A发出消息“全体目光向我看齐,我宣布个事,我要开始讲话了”;
设备A发出的“公告”会被B看到,B作为从机就会听A接下来要说什么,从A发出“公告”开始,A就会占用总线,此时其他设备都无法在数据线上发出消息。
设备A需要继续喊话,这第二次喊话,A就需要告知总线上的设备自己到底要找谁发消息,是广播给所有人(就像全校演讲那样),还是逮住某个设备“私聊”,IIC总线使用从机地址来区分广播和“私聊”,如果第二次喊话的内容是广播模式专有的“地址码”(0x00),总线上的所有从机都会接收后续发出的数据;如果第二次喊话的内容是设备B专有的“地址码”(一个7bit大小的数字),那这次喊话就是针对设备B的,其他的设备会发现点名私聊没找到自己,也就会放弃对后续数据的接收。
假设设备A用二次喊话找到设备B进行私聊,待B回应一个应答信号(ACK)之后,A就可以开始数据的传输,每当B接收到A传输的数据,B就会给A发出一个应答信号,这个过程会持续到A完成所有数据的传输并发出停止信号为止——“我的话讲完了,你可以挂电话了“。
如果在A不断向B传输数据的过程中,B觉得自己脑子要炸了,数据太多了,需要时间消化,B可以在回发应答信号的时候发一个“我收不了了!“(NACK),A在接收到这个信号之后,就知道B已经接收到极限了,A就不会再发数据。之后A可以选择开始新一轮的数据传输(回到过程1发起一个新的”喊话“)或者发出停止信号来直接关闭这次通信。
我们会发现上述过程存在很多分支选项,整个过程就像上课一样,“老师讲课学生都听着“、”老师点名某个学生回答问题“,下面我们就从具体的格式上来把上述的抽象过程给对应上。
格式上看,任何一次完整的IIC通信需要传输的数据都是如下的结构:
“起始信号+从机地址|读写类型+ACK+数据0+ACK+数据1+ACK+数据2+……+数据N+ACK+停止信号”
起始信号=我要开始喊话了;
从机地址|读写类型=我要找谁讲话|我要传达or索要信息;
ACK=收到!;
数据N=需要传达or索要的信息;
停止信号:我的话讲完了;
肯定有小伙伴想问:“那IIC总线通过什么方式来表示这些信号呢?“,这些内容属于物理层,感兴趣的小伙伴可以自行百度。
前半部分主要讲解了IIC总线的过程,下面介绍具体到代码上,单片机的IIC接口应该如何去使用。
首先登场的还是喜闻乐见的IO和时钟配置,需要注意的是,这里的IO输出模式需要配置为开漏输出,因为IIC接口需要从IO口收发数据,读写都在这一个IO上,开漏输出就能满足同时读写的需求。
对于IIC的配置其实相对来说比较简单,配置好波特率(公式在结构体的注释里面写好了),使能外设,设置好应答规则,最后开启IIC外设即可。
接着就是比较重要的部分了,IIC接口的收发并不是全自动的,因为一个完整的通信不仅包括发数据(地址、数据什么的),还包含收数据(啥也不干也得接收ACK信号),所以IIC通信的每个部分基本上都是收发易位的过程,IIC外设并不会自动完成这个复杂的过程,每个部分的信号是否发送、以及发送的情况都需要开发者自己去查看(开发者:改为手动操作,全部让我来!)。
编者写了几个带自检功能的IIC函数,这几个简单的函数可以满足驱动OLED的需求。
编写这四个函数可以方便我们后续对OLED驱动的开发,现在我要详细说明一下这四个函数的内在逻辑。
首先是起始信号,我们可以看到发起始信号的函数显示打开了一个开关,等待某个标志成立之后又关掉了那个开关,这里结合手册进行说明。IIC协议中,起始信号是“SCL维持高电平,SDA线电平拉低”这一现象,手册中也有详细描述:
又由于CW32的IIC接口并不会在起始信号发出之后自动停发起始信号,因此如果不在监测到起始信号发出之后关闭起始信号的发送,那么数据传输就无法开始,IIC设备会一直发送RESTART信号来占用总线,通信就会失败。
对于总线的状态——“我发的信号到底成功发出去没有呢?”,CW32提供了IIC状态码来指示总线状态,根据IIC设备不同的工作模式,一共会有26种总线状态,我们并不会用到全部的状态,但可能用到的状态都可以放到枚举类型里面,就像这样:
以起始信号发送函数为例,其返回值就是已发送起始信号的状态码(0x08),如果起始信号发送失败,死循环就无法跳出,程序死机(虽然实际上不应该这么写,此处只做演示,编者就小小地偷一下懒)。
视线来到发送从机地址和读写指令的函数,就像本文前半部分讲的一样,喊话宣言之后需要指名道姓自己需要私聊的对象。从机地址本身只有7bit,占据整个字节的高7位,0号bit位表示这一次通信是为了传达信息还是索取信息,0位为0则传达(也就是写),为1则索取(也就是读)。当成功发送从机地址这一个字节之后,IIC状态码也会改变,对比状态码之后即可确认从机地址字节发送成功并收到了从机的ACK信号,这表示从机确认收到了这个字节的消息。
发送数据的函数和发送从机地址的函数很相似,只不过整个字节都表示数据,并没有什么独特的含义。
最后就是发送停止信号的函数,与起始信号不同,停止信号成功发出之后,总线会进入空闲状态,并且停止信号使能位会被硬件自动清零。
捋清逻辑之后,我就要说明一个非常重要的细节,仔细观察会发现,所有的IIC信号发送函数都有一个清除中断标志的操作,这里明明不是中断,为什么要写这个语句呢?因为CW32的IIC接口,其发送数据的触发条件,就是中断标志位被清除。根据手册的描述,只要IIC状态码改变,中断标志位就会被硬件置位,在开启中断的情况下,程序会进入中断服务函数,如果不开启中断,程序的执行顺序不会改变,这个标志位也就只是一个发送开关。
这个发送逻辑某种程度上很反直觉,因为大部分的通信接口,都是拿“数据缓冲区被写入数据”来触发发送行为的,而此处的send函数,均不具备发送功能,与其叫send_data,不如叫set_data更合适,他们的作用只是把数据装载到IIC的数据寄存器,因此如果想要发送,就需要在清除中断标志位之前将数据写入数据寄存器。手册上也详细描述了这一点:
这样一来,IIC通信就具备基本的发送功能了,对于常见的EEPROM读写,CW32的IIC库提供了连续读写的函数,开发者可以直接使用:
个人评价:大部分人在需要使用IIC的时候,都会直接移植软件模拟的IIC接口,但是在更多的地方,我还是推荐使用硬件IIC,尤其是需要使用IIC大量读写数据的场合。而CW32的IIC接口,在不考虑发送触发与中断绑定这一反直觉因素的情况下,其内部的处理逻辑相比其他MCU的IIC接口,还是颇具优势的(读者可以自行对比STM32的IIC接口,STM32的IIC读写逻辑不能完全手动操作,效率不够高),尤其是每次发送之后,不必要立刻进行下一个字节的发送,只要IIC总线还保持在建立状态,开发者可以在之后一段时间内的任意时刻发送下一个字节,这直接省去了等待发送完成的时间(当然本文并没有采取这种写法),提高了程序整体的运行效率。
篇幅有限,下一章将会介绍如何使用IIC接口编写一个简易的OLED驱动程序。
|