IIC接口

例程链接:https://pan.baidu.com/s/1s1XwqDFkO8fK4SRSTKsNhA?pwd=mshk 
提取码:mshk

上一章我们讲解了IIC的通信流程以及通信代码,这一章就以市面上常见的IIC接口模块——OLED屏为例教学一下IIC接口的驱动怎么写。

第一步当然是搞清楚自己使用的OLED屏幕用的是什么驱动,说是屏幕,实际上就是密集LED点阵,所以必定有用于控制大量LED灯的驱动器,本教学使用的OLED驱动是SSD1306,该驱动器有多种通信接口,这里使用IIC接口(具体使用什么接口,数据手册上会有详细介绍)

根据SSD1306数据手册的描述,该设备的从机地址取决于SA0的电平,但是我翻遍了商家给我的资料也没找到整个模块的硬件原理图(也有可能遗漏了),无奈只能打开例程查看从机地址是0x78。 

1.png

数据手册详细描述了1306的IIC接口规则,7bit的地址位+1bit的读写位,数据线和时钟线描述的就是标准的IIC协议,不必过多纠结。

下面的内容是重点,需要注意的是,编写任何一个外设的驱动,基本上都逃不开指令和数据,驱动方式可能多种多样,但最多也就是指令与数据的排列组合,稍微复杂一些的会加上寄存器操作(也就是用单片机1号通过某种通信接口控制单片机二号),抓住本质之后,思路就能打开。

先来看看数据手册是怎么描述的:

2.png

对阅读比较吃力的小伙伴,我捡重点说一说:

第2条描述的就是从机地址的设置,这在前面以及提到了;

第3条说的是IIC读写位的定义;

第4条说的是IIC协议中应答信号的规则,这里说明了1306会对一切IIC数据(包括地址|读写字节)作出回应;

第5条说的是一旦主机和1306建立通讯(发送地址|读写字节之后),后续发送到IIC总线上的数据就会被识别成“控制字节”或“数据字节”,具体什么是控制字节和数据字节,后文会详细说明;

第6条说的是每个控制字节和数据字节都会被回应;

第7条说的是IIC协议停止位的规则;

现在来说明一下什么是控制字节和数据字节。对于屏幕,我们的操作他的目的是在屏幕上面显示内容,“显示”是一个动作、行为,也可以叫他命令,“内容”就是一个数据。所以操控设备的时候,至少会包含一个指令和一个数据,但是指令和数据在各种通信协议中,都只是一个8bit的数,如何区分他们就成了一个很关键的点。

1306使用这么一种方式来区分他们:一旦通过IIC接口建立通讯,随后接收的第0个字节(byte0)一定是用来说明下一个字节(byte1)的类型的,它用2个字节来表达一个完整的数据传输。数据手册中贴出了1306使用IIC通信的帧结构图:

3.png

控制字节区分数据字节类型的方式,就是通过其最高的2个bit位:Co和D/C#。先说DC位,D就是data,C就是command,这个bit位为0就代表紧跟着的下一个字节是命令,如果bit位是1,那下一个紧跟着的字节就是数据。实际上只要有这么一个bit位就已经可以完成对1306的控制了,那么现在思考一个问题,如果我需要连续写入大量的命令(比如100个),为了完成这100个命令的传输,我需要传输200个字节,因为每个命令都需要绑定一个控制字节。为了提高传输效率,Co位应运而生,如果Co位是0,且DC位也是0,那么1306就会把该控制字节之后传入的所有数据都认定为命令,这样一来如果要写入100个命令,实际上只需要发送101个字节就能实现目的,效率几乎是翻倍了。对于传输数据,也是一样的,只需要把Co位设置为0,DC位设置为1,后续传入的字节就全是数据了。如果没有这种连续写入大量同类型数据(命令/数据,括号里的数据是对屏幕显示而言的,这个括号之前的数据是对IIC总线而言的)的需求,也可以把Co位置1以采用 “控制字节+数据字节(DC byte)”的方式实现功能。

下面就是代码的编写,我们使用联合体直接列出需要发送的帧结构,需要发送时只需要赋值对应的位再发送value这个数组就可以了,这么写就不需要在发送的时候进行位操作,大幅度提高代码可读性:

4.png

再贴一个发指令的函数,这个函数使用的是单次写入的方式,效率不高但是方便使用,需要注意的是这个函数不具备建立IIC通信的能力,他只负责在已建立通信的情况下发出一个完整的指令。

5.png

现在我们拥有了建立IIC通信和发送指令的函数,实际上就已经可以用这些函数看看效果了,1306有一个指令是A5H,他的作用是强制点亮屏幕上所有的像素点,正确初始化OLED之后,再发送A5H即可。

6.png

关于OLED的初始化,模块的资料提供了一套完整的初始化指令,简单来说就是上电之后需要先把这一堆指令发给OLED驱动,之后屏幕才会正常工作,具体到每个指令的详细功能,还请读者查看1306数据手册的指令表章节,或者直接搜索1306指令的相关资料。

话题拉回屏幕显示这一块,我们的目标肯定不只是把整个屏幕擦亮这么简单,屏幕要么拿来画画,要么拿来写字,他至少要能够写字才行。现在已经知道的是,屏幕就是一个LED阵,那只要控制一批像素点按照固定的规则点亮就能显示我们想要的内容,实现这个目的的过程也叫取字模,售卖屏幕的商家打包的资料都会有取字模的软件,如果没有也可以直接去网上下一个。将字模数据预置存储在单片机里面,需要的时候直接发出去就能显示,这种办法简单而有效。

很好,现在我们写字的目标已经转变成“在合适的位置点亮合适的像素点”,那怎么确定位置呢?屏幕有那么多像素点,现在空有数据,却没有位置。这个时候就需要简单说明一下OLED的显示逻辑了,整个屏幕被划分成了多个页,每一页都有128列像素点,屏幕的分辨率是128*64,横向128像素,纵向64像素,我们每次写入的数据都是8bit的,这个8bit数据指示了某个像素页内某一列的像素状态。形象地说就是:8个点排一列,横向排128列就组成了一个页,整个屏幕一共有8个页,这8个页再纵向排列,最终形成了一个128*64的屏幕。

想要在正确的位置显示内容,就得选择正确的页(后文直接称page),page0-page7一共8个,每个page都有自己的物理地址,从B0H到B7H,所以我们可以以此写一个确定光标位置的函数,这个函数可以在我们需要写字的时候锚定一个正确的显示位置。

7.png

前文提到字模,其实就是一批8bit数据,再结合刚刚说明的屏幕显示原理,就不得不再次思考一个问题,像素得精细到什么程度才能看起来像一个字,在像的情况下,还要符合OLED这种一页8行像素的特点(因为这样会更好操作)。答案是使用8n个像素宽度的正方形来显示字符,目前来看16*16大小的字符正好符合要求,这也是大部分小屏显示会选择的大小。如此一来想要显示一个字符就需要写2个page的若干列数据,于是就有了写字函数,具体代码如下图:

8.png

该函数首先建立IIC通信,与从机建立通信后设置显示字符的坐标,随后直接按顺序发出上半部分和下半部分的像素数据即可,这个函数可以独立完成对字符的显示,后续演示代码中显示字符串的函数基于此函数实现,虽然对于字符串的显示,最佳方案是建立一次通信就完成所有数据的传输,但那样的代码会把各种功能杂糅在一起,层次不够分明,这里这么规划也是为了内容更好理解,关于IIC与OLED的代码文件会附在文章最后。

9.png

所有用于像素显示的数据都会被存到Graphic Display Data RAM(GGDRAM)中,既然是RAM,理论上在上电的时候,其存储的数据应该都是0才对,但为了避免不必要的干扰可能造成的影响,我们还需要一个清屏函数,该函数其实就是对所有page的所有数据进行置0操作。

10.png

具备所有的前提条件,我们就可以在main函数中显示内容了,在设备初始化中加入IIC初始化和OLED初始化,再加上字符串显示就大功告成了。

11.png

最后贴一个图来看看成品效果

12.png

文章末尾说一些题外话,互联网上有很多软件模拟IIC和OLED驱动的相关资料,除去写字部分的应用层代码,数据链路层部分的代码建议还是自己写,这些开源代码的IIC总线效率实际上很低,而且容易造成误解,编者在研究商家给的例程时,一直不理解为什么例程发0x00作为控制字节的时候能初始化成功,而我却不行,后来仔细思考了一番是因为他们的IIC,每次建立通讯都只会发送2个字节的内容,也就是说,如果要发送20个命令,就需要建立20次IIC通信,每次都要重新发送从机地址,发送这20个命令实际上要发送60个字节(包括IIC地址字节的话),功能当然可以实现,但是效率很低,而且这种代码注释并不详细(甚至是挪用代码还不改注释),如果作为学习使用但不加说明的话很容易造成误解(至少我被误解了),读者如果真的有学习需求而不是单纯的挪用需求,最好还是以手册描述的内容为准。

来源:CW32生态社区

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 12

例程链接:

https://pan.baidu.com/s/1s1XwqDFkO8fK4SRSTKsNhA?pwd=mshk 

提取码:mshk

本章将介绍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,那过程就是这样:

1.设备A发出消息“全体目光向我看齐,我宣布个事,我要开始讲话了”;

2.设备A发出的“公告”会被B看到,B作为从机就会听A接下来要说什么,从A发出“公告”开始,A就会占用总线,此时其他设备都无法在数据线上发出消息。

3.设备A需要继续喊话,这第二次喊话,A就需要告知总线上的设备自己到底要找谁发消息,是广播给所有人(就像全校演讲那样),还是逮住某个设备“私聊”,IIC总线使用从机地址来区分广播和“私聊”,如果第二次喊话的内容是广播模式专有的“地址码”(0x00),总线上的所有从机都会接收后续发出的数据;如果第二次喊话的内容是设备B专有的“地址码”(一个7bit大小的数字),那这次喊话就是针对设备B的,其他的设备会发现点名私聊没找到自己,也就会放弃对后续数据的接收。

4.假设设备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上,开漏输出就能满足同时读写的需求。

1.png

2.png

对于IIC的配置其实相对来说比较简单,配置好波特率(公式在结构体的注释里面写好了),使能外设,设置好应答规则,最后开启IIC外设即可。

接着就是比较重要的部分了,IIC接口的收发并不是全自动的,因为一个完整的通信不仅包括发数据(地址、数据什么的),还包含收数据(啥也不干也得接收ACK信号),所以IIC通信的每个部分基本上都是收发易位的过程,IIC外设并不会自动完成这个复杂的过程,每个部分的信号是否发送、以及发送的情况都需要开发者自己去查看(开发者:改为手动操作,全部让我来!)。

编者写了几个带自检功能的IIC函数,这几个简单的函数可以满足驱动OLED的需求。

 3.png

编写这四个函数可以方便我们后续对OLED驱动的开发,现在我要详细说明一下这四个函数的内在逻辑。

4.png

首先是起始信号,我们可以看到发起始信号的函数显示打开了一个开关,等待某个标志成立之后又关掉了那个开关,这里结合手册进行说明。IIC协议中,起始信号是“SCL维持高电平,SDA线电平拉低”这一现象,手册中也有详细描述:

5.png

又由于CW32的IIC接口并不会在起始信号发出之后自动停发起始信号,因此如果不在监测到起始信号发出之后关闭起始信号的发送,那么数据传输就无法开始,IIC设备会一直发送RESTART信号来占用总线,通信就会失败。

对于总线的状态——“我发的信号到底成功发出去没有呢?”,CW32提供了IIC状态码来指示总线状态,根据IIC设备不同的工作模式,一共会有26种总线状态,我们并不会用到全部的状态,但可能用到的状态都可以放到枚举类型里面,就像这样: 

6.png

以起始信号发送函数为例,其返回值就是已发送起始信号的状态码(0x08),如果起始信号发送失败,死循环就无法跳出,程序死机(虽然实际上不应该这么写,此处只做演示,编者就小小地偷一下懒)。

视线来到发送从机地址和读写指令的函数,就像本文前半部分讲的一样,喊话宣言之后需要指名道姓自己需要私聊的对象。从机地址本身只有7bit,占据整个字节的高7位,0号bit位表示这一次通信是为了传达信息还是索取信息,0位为0则传达(也就是写),为1则索取(也就是读)。当成功发送从机地址这一个字节之后,IIC状态码也会改变,对比状态码之后即可确认从机地址字节发送成功并收到了从机的ACK信号,这表示从机确认收到了这个字节的消息。

7.png

8.png

发送数据的函数和发送从机地址的函数很相似,只不过整个字节都表示数据,并没有什么独特的含义。

最后就是发送停止信号的函数,与起始信号不同,停止信号成功发出之后,总线会进入空闲状态,并且停止信号使能位会被硬件自动清零。

9.png

捋清逻辑之后,我就要说明一个非常重要的细节,仔细观察会发现,所有的IIC信号发送函数都有一个清除中断标志的操作,这里明明不是中断,为什么要写这个语句呢?因为CW32的IIC接口,其发送数据的触发条件,就是中断标志位被清除。根据手册的描述,只要IIC状态码改变,中断标志位就会被硬件置位,在开启中断的情况下,程序会进入中断服务函数,如果不开启中断,程序的执行顺序不会改变,这个标志位也就只是一个发送开关。

这个发送逻辑某种程度上很反直觉,因为大部分的通信接口,都是拿“数据缓冲区被写入数据”来触发发送行为的,而此处的send函数,均不具备发送功能,与其叫send_data,不如叫set_data更合适,他们的作用只是把数据装载到IIC的数据寄存器,因此如果想要发送,就需要在清除中断标志位之前将数据写入数据寄存器。手册上也详细描述了这一点:

10.png

这样一来,IIC通信就具备基本的发送功能了,对于常见的EEPROM读写,CW32的IIC库提供了连续读写的函数,开发者可以直接使用:

11.png

个人评价:大部分人在需要使用IIC的时候,都会直接移植软件模拟的IIC接口,但是在更多的地方,我还是推荐使用硬件IIC,尤其是需要使用IIC大量读写数据的场合。而CW32的IIC接口,在不考虑发送触发与中断绑定这一反直觉因素的情况下,其内部的处理逻辑相比其他MCU的IIC接口,还是颇具优势的(读者可以自行对比STM32的IIC接口,STM32的IIC读写逻辑不能完全手动操作,效率不够高),尤其是每次发送之后,不必要立刻进行下一个字节的发送,只要IIC总线还保持在建立状态,开发者可以在之后一段时间内的任意时刻发送下一个字节,这直接省去了等待发送完成的时间(当然本文并没有采取这种写法),提高了程序整体的运行效率。

来源:CW32生态社区

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 6
订阅 RSS - IIC接口