1.IIC概述 IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。 2.IIC物理层只要求两条总线线路,一条是串行数据线SDA,一条是串行时钟线SCL,IIC是半双工,而不是全双工。每个连接到总线的器件都可以通过唯一的地址和其它器件通信,主机/从机角色和地址可配置,主机可以作为主机发送器和主机接收器。IIC是真正的多主机总线,可以在通讯过程中改变主机,如果两个或更多的主机同时请求总线,可以通过冲突检测和仲裁防止总线数据被破坏。 连接到总线的IIC数量只是受到总线的最大负载电容400pf限制。 IIC总线通过上拉电阻接正电源。当总线空闲时,两根线均为高电平。连到总线上的任何一个器件输出的低电平,都将使总线的信号变低,即各器件的SDA和SCL都是线“与”的关系。 3.IIC协议层3-1.数据有效性在时钟的高电平周期内,SDA线上的数据必须保持稳定,数据线仅可以在时钟SCL为低电平时改变。 3-2.起始信号和终止信号SCL线为高电平期间,SDA线由高到低变化表示起始。 SCL线为高电平期间,SDA线由低到高变化表示终止。 要注意起始和终止信号都是由主机发出的,连接到IIC总线上的器件,若具有IIC总线的硬件接口,则很容易检测到起始和终止信号。总线在起始条件之后,视为忙状态,在停止条件之后被视为空闲状态。 3-3.应答每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据,从机应答主机所需要的时钟仍是主机提供的,应答出现在每一次主机完成8个数据位传输后紧跟着的时钟周期,0表示应答,1表示非应答。 3-4.数据帧格式IIC总线上传输的数据信息是广义的,既包括地址信号,又包括真正的数据信号。起始信号后必须传送一个从机的地址(7位),第八位是数据的传送方向位(R/T),用“0”表示主机发送数据“T”,“1”表示主句接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。 IIC传输时时从MSB开始传输到LSB结束。MSB是Most Significant Bit的缩写,最高有效位。在二进制数中,MSB是最高加权位。与十进制数字中最左边的一位类似。通常,MSB位于二进制数的最左侧,LSB位于二进制数的最右侧。LSB,英文 least significant bit,中文义最低有效位。 IIC写时序 - 产生start位
- 传送器件地址ID_Address,器件地址的最后一位为数据的传输方向位,R/W,低电平0表示主机往从机写数据(W),1表示主机从从机读数据(R)。ACK应答,应答是从机发送给主机的应答,这里不用管。
- 传送写入器件寄存器地址,即数据要写入的位置。同样ACK应答不用管。
- 传送要写入的数据。ACK应答不用管。
- 产生stop信号。
IIC读时序 - 产生start信号
- 传送器件地址(写ID_Address),ACK。
- 传送字地址(写REG_Address),ACK。
- 再次产生start信号
- 再传送一次器件地址,ACK。
- 读取一个字节的数据,读数据最后结束前无应答ACK信号。
- 产生stop信号。
从时序图上可以看出,IIC读时序要写两次器件地址,刚开始接触的时候我也很疑惑 dummy write。我个人这样理解这里,首先传送器件地址到总线上找到器件,然后写入寄存器地址,也就是word address找到需要读取数据的地址,但并不是真正的写入数据所以叫做dummy wirte(假写)。然后再传输一次器件地址后开始读数据。 IIC协议在读写数据时,总是要发送器件地址,这里需要注意的是,不是主机给从机发送地址,而是主机给地址总线上发送地址,挂IIC总线上的所有从机都能收到地址,如果发过来的地址和自己的地址匹配上了,从机就会给主机一个应答,这样就建立起来了一个通讯。所以我在想,如果从机的器件是完全一样的,那么IIC协议就可以同时给多个从机,即对多个器件进行配置。这种理论上是可行的,但其实是不行的,IIC协议就是通过地址不同来判断给哪个器件传送数据的,如果两个器件的地址完全一样,器件会产生应答,那么两个器件就通过竞争判断给谁通信了,有随机性。即IIC协议一次只能和一个设备/器件进行通讯。 二、模拟IIC信号控制0.96寸OLED屏(SSD1306)1.OLED屏基本规格1-1.显示规格- 显示方式:无源矩阵
- 显示颜色:单色(蓝色)
- 驱动占空比:1/64 Duty
1-2.机械规格- 像素数:128 × 64
- 面板尺寸:6.70 × 19.26 × 1.4 (mm)
- 有效面积:21.744 × 10.864 (mm)
- 像素间距:0.17 × 0.17 (mm)
- 像素尺寸:0.154 × 0.154 (mm)
- 重量:1.54 (g)
1-3.内存映射和像素构造1-4.引脚定义GND——逻辑电路接地,接电源负极 VCC—— SCL——串行时钟输入 SDA—— 2.IIC信号的模拟2-1.起始信号、停止信号和获取应答信号89C51系列单片机不带IIC总线接口,但是可以利用软件实现IIC总线的数据传送,即软件与硬件结合的信号模拟。(即使是含有IIC硬件的STM32一般也会模拟IIC的时序)。为了保证数据传送的可靠性,标准的I2C总线的数据传送有严格的时序要求。I2C总线的起始信号、终止信号、发送“0”及发送“1”的模拟时序如下: 具体代码如下: //起始信号void IIC_start(){ SDA = 1; //把数据线拉高 SCL = 1; //把时钟线拉高 Delay6us(); //延时6微秒,要求大于4.7微秒 SDA = 0; //把数据线拉低,产生下降沿 Delay6us(); //延时6微秒 SCL = 0; //把时钟线拉低,因为只有时钟线拉低的时候才允许数据变化。}//停止信号 void IIC_stop(){ SDA = 0; //把数据线拉低 SCL = 1; //把时钟线拉高 Delay6us(); //延时6微秒,要求大于4微秒 SDA = 1; //把数据线拉高,产生上升沿 Delay6us(); //延时6微秒}//主机获取应答信号ACKchar IIC_ACK(){ char flag; //定义一个读取ACK的字符 SDA = 1; //先拉高数据线 Delay6us(); //延时 SCL = 1; //拉高时钟线 Delay6us(); //延时,要求大于4微妙 flag = SDA; //读取从机的ACK信号 Delay6us(); //延时 SCL = 0; //拉低时钟线 Delay6us(); return flag;}
2-2.数据发送的时序查询手册,C51单片机应使用如下时序图: 代码实现: void IIC_send_byte(char send_data){ int i; for(i=0;i<8;i++) { SCL = 0; //把时钟线拉低,做好发送数据的准备,在时钟线为低电平时,才能修改数据 SDA = send_data & 0x80; //数据发送由高位先发送,与上0X80获取高位数据 Delay6us(); //延时 SCL = 1; //时钟线拉高电平,发送数据 Delay6us(); SCL = 0; //发送完拉低时钟线 Delay6us(); send_data <<= 1; //左移数据位 }}
2-3.IIC写数据以本次OLED屏(SD1306)为例,写入数据的数据帧如下: - 发送一个起始信号
- 发送7位从站地址+1位的读写信号,这里由于只对OLED写入数据,所以用b01111000,也就是0x78。
- 等待ACK
- 发送一个control byte,control byte有两个类型,如果是0x00就是写入命令,如果是0x40就是写入数据(显示数据)。
- 等待ACK
- 写入指令或数据,由第4步决定
- 等待ACK
- 发送一个停止信号
//写入指令void oled_cmd(char cmd){ IIC_start(); IIC_send_byte(0x78); IIC_ACK(); IIC_send_byte(0x00); IIC_ACK(); IIC_send_byte(cmd); IIC_ACK(); IIC_stop();}//写入数据void oled_data(char Data){ IIC_start(); IIC_send_byte(0x78); IIC_ACK(); IIC_send_byte(0x40); IIC_ACK(); IIC_send_byte(Data); IIC_ACK(); IIC_stop();}
2-4.OLED初始化手册中关于OLED的流程图 根据流程图,编写以下初始化代码 void oled_init(){ oled_cmd(0xAE); oled_cmd(0x00); oled_cmd(0x10); oled_cmd(0x40); oled_cmd(0xB0); oled_cmd(0x81); oled_cmd(0xFF); oled_cmd(0xA1); oled_cmd(0xA6); oled_cmd(0xA8); oled_cmd(0x3F); oled_cmd(0xC8); oled_cmd(0xD3); oled_cmd(0x00); oled_cmd(0xD5); oled_cmd(0x80); oled_cmd(0xD9); oled_cmd(0xF1); oled_cmd(0xDA); oled_cmd(0x12); oled_cmd(0xDB); oled_cmd(0x30); oled_cmd(0x8D); oled_cmd(0x14); oled_cmd(0xAF);}
3.OLED的显示管理手册原文如下: 整个屏幕由上至下被分为8个区域,也就是有8个页,每个页有128×8个点阵,把每个点看作一个位,可以把每个页看作由128个字节控制的区域。 由此可以看出,每个页共有128列,1列对应1个字节,每列由上至下8个位对应,写入数据的低位到高位。例如往第0页的第0列写入0x01,那么点亮的就是整个屏幕最左上角的点。 4.寻址模式4-1.页寻址模式(默认值,也是最常用的模式)在页寻址模式下,读取/写入显示RAM后,列地址指针增加自动加1。如果列地址指针到达列结束地址,则列地址指针为重置为列起始地址而页地址指针不会改变。用户必须设置新页面和列地址,以便访问下一页RAM的内容。 4-2.水平寻址模式在水平寻址模式下,读取/写入显示RAM后,列地址指针增加自动加1。如果列地址指针到达列结束地址,则列地址指针为重置为列起始地址和页地址指针增加1。列和页地址指针都到达结束地址,指针被重置为列起始地址和页起始地址。 4-3.垂直寻址模式在垂直寻址模式下,读取/写入显示RAM后,页面地址指针增加自动加1。如果页面地址指针到达页面结束地址,页面地址指针将被重置页起始地址和列地址指针加1。当列和页地址指针到达结束地址,指针被重置为列起始地址和页起始地址 4-4.设置内存寻址模式三种寻址模式分别对应: - 页寻址模式:0x02(默认值,这里的RESET指的就是默认值)
- 水平寻址模式:0x00
- 垂直寻址模式:0x01
先发送命令0x20,再发送需要设置的寻址模式。(如果是页寻址模式,两条指令都可以省略) 4-5.在页寻址模式下设置GDDRAM访问指针的示例在正常的显示数据RAM读写和页寻址模式下,需要执行以下步骤 定义RAM访问指针的起始位置: - 设置页面的起始地址命令B0h~B7h目标显示位置。
- 通过命令00h~0Fh设置指针的低起始列地址。
- 通过命令10h~1Fh设置指针的高起始列地址。
例如设置页面地址为B2h,下列地址为03h,上列地址为10h,那么开始列是PAGE2的SEG3,输入数据字节将被写入第3列的RAM位置(手册原文有误,翻译时更正了)。 页地址的设置比较简单,发送命令0xB0~0xB7分别表示页指针指向0~7页。 列的设置稍微麻烦了一点,需要分别设置列的低地址和高地址,当列指针指向第0列时,列的低地址和高地址分别为0x00和0x10。 例如,当前列指针指向第15列,那么列的低地址为0x0F,高地址为0x10。当列指针指向第16列时,列的低地址加1,即0x0F+1=0x10,但是低地址的高4位不能操作,因此当列的低地址等于0x10时就会溢出,重置为0x00,然后把高4位里的1传递给列的高地址,列的高地址就变成了0x11了。所以当列指针指向第16列时,列的低地址为0x00,高地址为0x11。可以这么理解,只要低地址的低4位溢出,溢出值就会给到高地址,当高地址的低4位也溢出时,就会重置为0x10,也就是列指针指向第0列。 5.实现OLED显示在OLED显示一个点,整合前面的代码,可得下面的代码: #include "reg52.h"#include <intrins.h>sbit SCL = P1^4;sbit SDA = P1^3;void Delay6us() //@11.0592MHz{ _nop_();}void IIC_start(){ SDA = 1; SCL = 1; Delay6us(); SDA = 0; Delay6us(); SCL = 0;}void IIC_stop(){ SDA = 0; SCL = 1; Delay6us(); SDA = 1; Delay6us();}char IIC_ACK(){ char flag; SDA = 1; Delay6us(); SCL = 1; Delay6us(); flag = SDA; Delay6us(); SCL = 0; Delay6us(); return flag;}void IIC_send_byte(char send_data){ int i; for(i=0;i<8;i++) { SCL = 0; SDA = send_data & 0x80; Delay6us(); SCL = 1; Delay6us(); SCL = 0; Delay6us(); send_data <<= 1; }}void oled_cmd(char cmd){ IIC_start(); IIC_send_byte(0x78); IIC_ACK(); IIC_send_byte(0x00); IIC_ACK(); IIC_send_byte(cmd); IIC_ACK(); IIC_stop();}void oled_data(char Data){ IIC_start(); IIC_send_byte(0x78); IIC_ACK(); IIC_send_byte(0x40); IIC_ACK(); IIC_send_byte(Data); IIC_ACK(); IIC_stop();}void oled_clear() //清屏程序,给每页每列都写入0{ unsigned char i,j; for(i=0;i<8;i++) //遍历每个页 { oled_cmd(0xB0+i); oled_cmd(0x00); oled_cmd(0x10); for(j=0;j<128;j++) //遍历每个列 { oled_data(0); //写入0x00 } }}void oled_init(){ oled_cmd(0xAE); oled_cmd(0x00); oled_cmd(0x10); oled_cmd(0x40); oled_cmd(0xB0); oled_cmd(0x81); oled_cmd(0xFF); oled_cmd(0xA1); oled_cmd(0xA6); oled_cmd(0xA8); oled_cmd(0x3F); oled_cmd(0xC8); oled_cmd(0xD3); oled_cmd(0x00); oled_cmd(0xD5); oled_cmd(0x80); oled_cmd(0xD9); oled_cmd(0xF1); oled_cmd(0xDA); oled_cmd(0x12); oled_cmd(0xDB); oled_cmd(0x30); oled_cmd(0x8D); oled_cmd(0x14); oled_cmd(0xAF);}void main(){ oled_init(); oled_clear(); //清屏 oled_cmd(0x20); oled_cmd(0x02); //页寻址模式 oled_cmd(0xB0); //选择第0页 oled_data(0x08); //在第0页的第0列写入b00001000 while(1);}
如果要显示一条直线,可以参考上面的oled_clear()这个函数,把main函数里面的代码改成如下代码: void main(){ unsigned char j; oled_init(); oled_clear(); oled_cmd(0x20); oled_cmd(0x02); oled_cmd(0xB0); //选择第0页 for(j=0;j<128;j++) { oled_data(0x80); } while(1);}
|