本文历程使用ESP8266使用IIC协议驱动OLED显示。
IIC 总线是一种串行数据总线,总共有4跟线,其中有二根信号线,一根是双向的数据线SDA,另一根是时钟线SCL,在IIC总线上可以挂载多个IIC设备。 IIC设备(绝大多数)里有个固化的地址,只有在两条线上传输的值等于IIC设备的固化地址时,其才会作出响应。通常我们为了方便把IIC设备分为主设备和从设备,基本上谁控制时钟线(即控制SCL的电平高低变换)谁就是主设备。
IIC也是一种比较常用的协议,在ESP8266提供的SDK中也有IIC的官方接口,我们可以使用他的接口也可以自己写一个,用起来会比较顺手嘛;本文就官方提供的IIC编写思路,自行编写自己的IIC协议,官方提供的自己去看就行。
很多初学者会觉得这种协议相关的东西会学起来比较晦涩,其实不然,看着时序图,我们可以很容易的发现,IIC的时序动作其实就这几种:起始信号、停止信号、应答、非应答、就完了,然后进行动作的搭配,完整的通讯时序就出来了,根据前几篇博客我们学会了ESP8266的GPIO的操作,那么对于IIC只剩下理解时序了。
IIC协议网上有很多大佬介绍得特别详细,这里做个简单的说明:
个人认为,看懂时序就学会了IIC,对于IIC协议也就只有那个动作嘛,搞懂了就到了搭积木的撸代码过程;
IIC总线进行数据传送时,时钟信号SCL为高电平期间,数据线SDA上的数据必须保持稳定(从机读取主机发送过来的数据,是读取在SCL为高电平时SDA的状态),只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之前必须稳定,这个期间的SDA数据才算有效。
I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。
此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
起始信号:当SCL为高期间,SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。
停止信号:当SCL为高期间,SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。
主机向从机发送数据,从机每接收到一个字节的数据都会向主机回发一个应答,主机通过接收应答信号来判断当前字节是否成功发送与成功接收,但是有一点需要注意的是,从机发送的应答信号所需的始终依然由主机提供,所以应答信号紧跟在一个字节数据的8个时钟脉冲后的第9个时钟脉冲期间;
应答信号一般出现在:
发送端发送了一个字节数据后,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号ACK,接收失败则返回NACK。
如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号
应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功,如上图即为应答,非应答即为高电平。
如图是AT24Cxx数据写入的时序图,图中是完整的一个字节数据的写入,包括了IIC从机设备的地址以及是写命令还是写数据,本问不讲具体实现,只讲最底层软件IIC的编写,只负责一个字节数据的写入部分,方便后续的移植啊通用啥的。
根据前面数据有效性的介绍,SDA在SCL为低电平时可变,在SCL为高到低跳变前保持稳定即可;
void mIIC_Send_Byte(uint8_t byte)
{
int8_t i=0;
IIC_SCL_OUT(0); //拉低时钟线
IIC_SDA_OUT(0); //拉低数据线
i2c_master_wait(i2c_delay_times); //延时5us
for(i=7; i>=0; i--)
{
if(byte & (1<<i)) //根据需要传输的数据,改变数据线电平
IIC_SDA_OUT(1);
else
IIC_SDA_OUT(0);
i2c_master_wait(i2c_delay_times); //稳定5us
IIC_SCL_OUT(1); //拉高时钟线
i2c_master_wait(i2c_delay_times); //稳定5us
IIC_SCL_OUT(0); //时钟线高到低跳变,完成1bit的发送
i2c_master_wait(i2c_delay_times);
}
}
一样只需编写读取一个字节数据的代码即可,同时前面应答信号有说明读取完一个字节后,需要发送一个NACK,告诉发送端停止发送。
uint8_t mIIC_Read_Byte(uint8_t ask) //参数可设置成是否需要发送应答信号
{
int8_t i = 0;
uint8_t byte = 0;
mIIC_SDA_Mode_Change(SDA_MODE_READ); //SDA切换到输入模式
i2c_master_wait(i2c_delay_times);
for(i = 7; i >=0 ; i--)
{
IIC_SCL_OUT(1); //时钟线为高时,采集SDA数据电平
i2c_master_wait(i2c_delay_times);
if(IIC_SDA_IN())
byte |= 1<<i;
IIC_SCL_OUT(0);
i2c_master_wait(i2c_delay_times);
}
mIIC_SDA_Mode_Change(SDA_MODE_WRITE); //SDA切换到输出模式
if(ask)
mIIC_Send_Ack(); //发送应答ACK
else
mIIC_Send_NAck(); //发送非应答NACK
return byte;
}
效果如图:(完成底层的IIC协议后,可以很方便的移植IIC协议的设备,具体代码见下文的连接)
这里有个小坑需要注意
就是SDK中的driver/gpio.h里面,gpio_config()这个函数中,带有日志打印的效果,代码如下:
ESP_LOGI(GPIO_TAG, "GPIO[%d]| InputEn: %d| OutputEn: %d| OpenDrain: %d| Pullup: %d| Pulldown: %d| Intr:%d ", io_num, input_en, output_en, od_en, pu_en, pd_en, gpio_cfg->intr_type);
个人猜测,这段代码执行周期太长,容易导致软件IIC的时序错乱,如果使用自己写的软件IIC,像本文的测试是在OLED,如果不注释会出现雪花,不能正常显示,注释之后就好了。
以上截图来源于AT24CXX的数据手册,用作讲解说明参考
具体完整代码见GITHUB
我的GITHUB
我的个人博客
CSDN