上一章我们讲解了IIC的通信流程以及通信代码,这一章就以市面上常见的IIC接口模块——OLED屏为例教学一下IIC接口的驱动怎么写。
第一步当然是搞清楚自己使用的OLED屏幕用的是什么驱动,说是屏幕,实际上就是密集LED点阵,所以必定有用于控制大量LED灯的驱动器,本教学使用的OLED驱动是SSD1306,该驱动器有多种通信接口,这里使用IIC接口(具体使用什么接口,数据手册上会有详细介绍)
根据SSD1306数据手册的描述,该设备的从机地址取决于SA0的电平,但是我翻遍了商家给我的资料也没找到整个模块的硬件原理图(也有可能遗漏了),无奈只能打开例程查看从机地址是0x78。
数据手册详细描述了1306的IIC接口规则,7bit的地址位+1bit的读写位,数据线和时钟线描述的就是标准的IIC协议,不必过多纠结。
下面的内容是重点,需要注意的是,编写任何一个外设的驱动,基本上都逃不开指令和数据,驱动方式可能多种多样,但最多也就是指令与数据的排列组合,稍微复杂一些的会加上寄存器操作(也就是用单片机1号通过某种通信接口控制单片机二号),抓住本质之后,思路就能打开。
先来看看数据手册是怎么描述的:
对阅读比较吃力的小伙伴,我捡重点说一说:
第2条描述的就是从机地址的设置,这在前面以及提到了;
第3条说的是IIC读写位的定义;
第4条说的是IIC协议中应答信号的规则,这里说明了1306会对一切IIC数据(包括地址|读写字节)作出回应;
第5条说的是一旦主机和1306建立通讯(发送地址|读写字节之后),后续发送到IIC总线上的数据就会被识别成“控制字节”或“数据字节”,具体什么是控制字节和数据字节,后文会详细说明;
第6条说的是每个控制字节和数据字节都会被回应;
第7条说的是IIC协议停止位的规则;
现在来说明一下什么是控制字节和数据字节。对于屏幕,我们的操作他的目的是在屏幕上面显示内容,“显示”是一个动作、行为,也可以叫他命令,“内容”就是一个数据。所以操控设备的时候,至少会包含一个指令和一个数据,但是指令和数据在各种通信协议中,都只是一个8bit的数,如何区分他们就成了一个很关键的点。
1306使用这么一种方式来区分他们:一旦通过IIC接口建立通讯,随后接收的第0个字节(byte0)一定是用来说明下一个字节(byte1)的类型的,它用2个字节来表达一个完整的数据传输。数据手册中贴出了1306使用IIC通信的帧结构图:
控制字节区分数据字节类型的方式,就是通过其最高的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这个数组就可以了,这么写就不需要在发送的时候进行位操作,大幅度提高代码可读性:
再贴一个发指令的函数,这个函数使用的是单次写入的方式,效率不高但是方便使用,需要注意的是这个函数不具备建立IIC通信的能力,他只负责在已建立通信的情况下发出一个完整的指令。
现在我们拥有了建立IIC通信和发送指令的函数,实际上就已经可以用这些函数看看效果了,1306有一个指令是A5H,他的作用是强制点亮屏幕上所有的像素点,正确初始化OLED之后,再发送A5H即可。
关于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,所以我们可以以此写一个确定光标位置的函数,这个函数可以在我们需要写字的时候锚定一个正确的显示位置。
前文提到字模,其实就是一批8bit数据,再结合刚刚说明的屏幕显示原理,就不得不再次思考一个问题,像素得精细到什么程度才能看起来像一个字,在像的情况下,还要符合OLED这种一页8行像素的特点(因为这样会更好操作)。答案是使用8n个像素宽度的正方形来显示字符,目前来看16*16大小的字符正好符合要求,这也是大部分小屏显示会选择的大小。如此一来想要显示一个字符就需要写2个page的若干列数据,于是就有了写字函数,具体代码如下图:
该函数首先建立IIC通信,与从机建立通信后设置显示字符的坐标,随后直接按顺序发出上半部分和下半部分的像素数据即可,这个函数可以独立完成对字符的显示,后续演示代码中显示字符串的函数基于此函数实现,虽然对于字符串的显示,最佳方案是建立一次通信就完成所有数据的传输,但那样的代码会把各种功能杂糅在一起,层次不够分明,这里这么规划也是为了内容更好理解,关于IIC与OLED的代码文件会附在文章最后。
所有用于像素显示的数据都会被存到Graphic Display Data RAM(GGDRAM)中,既然是RAM,理论上在上电的时候,其存储的数据应该都是0才对,但为了避免不必要的干扰可能造成的影响,我们还需要一个清屏函数,该函数其实就是对所有page的所有数据进行置0操作。
具备所有的前提条件,我们就可以在main函数中显示内容了,在设备初始化中加入IIC初始化和OLED初始化,再加上字符串显示就大功告成了。
最后贴一个图来看看成品效果
文章末尾说一些题外话,互联网上有很多软件模拟IIC和OLED驱动的相关资料,除去写字部分的应用层代码,数据链路层部分的代码建议还是自己写,这些开源代码的IIC总线效率实际上很低,而且容易造成误解,编者在研究商家给的例程时,一直不理解为什么例程发0x00作为控制字节的时候能初始化成功,而我却不行,后来仔细思考了一番是因为他们的IIC,每次建立通讯都只会发送2个字节的内容,也就是说,如果要发送20个命令,就需要建立20次IIC通信,每次都要重新发送从机地址,发送这20个命令实际上要发送60个字节(包括IIC地址字节的话),功能当然可以实现,但是效率很低,而且这种代码注释并不详细(甚至是挪用代码还不改注释),如果作为学习使用但不加说明的话很容易造成误解(至少我被误解了),读者如果真的有学习需求而不是单纯的挪用需求,最好还是以手册描述的内容为准。
|