
这个可真的挖了一个坑,把我埋进去了
,原本是用硬件I2C的,毕竟esp32 自带了两个I2C控制器,这样比较方便,但是我发现我手中,有一款oled,使用esp32,无法查找到对应设备地址,但是,使用商家给的stm32代码,是可以直接驱动的,说明屏幕没有问题;于是我就用另一款oled,接入esp32,是可以查找到对应的地址,这就很尴尬了,(这难道就是硬件I2C的缺点? 因为有的设备可以通过写,然后查找到对用的设备地址,有的需要先读再写,才能读到设备地址。参考链接:https://blog.csdn.net/lovemengx/article/details/127252061),所以打算开始写软件的I2C驱动
I2C协议的编写
正常I2C总线的数据是:Start + I2C devece id + R/W + ACK + Data(first byte)+ ACK + ... + Data(n)+ ACK + Stop
Ps:数据在传输的时候,SCL为高电平,此时对应的SDA的值,即为传输的数据,在SCL为低电平的时候,SDA值才能改变

起始信号
在SCL为高电平,SDA由高电平转低电平

停止信号
在SCL为高电平的时候,SDA由低电平转高电平

响应(从机应答)
在主机发送一个字节的数据,都会等待从机应答,也就是一个字节结束后的第一个SCL高电平对应的SDA电平即应答,应答有效:SDA = 0 ,无效则:SDA = 1;
此时发送的是 从机地址 0x78,

完整过程



坑
这个在网上就有现成的,可以不用轮子,但是,在移植的时候需要注意。没想到,是真没想到,esp32在发送地址的时候,做了一个移位操作,我在使用硬件I2C的时候,又看到这个东西,但是,我想着,这个可能是硬件做的操作,软件I2C不需要做移位操作,结果,搞了老半天,都不行,我都emo了,最后用逻辑分析仪才发现,传输出来的地址,是有问题的,(原本硬件I2C是直接在传入地址的时候,我还做过向右的移位操作,结果初始化失败,就直接传入地址,反而可以用的)

参考代码
#include "I2C_SOFT.H" /*=========================================================================== 这个是关于eps32 软件I2C 的学习 SDA:18 SCL:19 ============================================================================*/ #define OLED_CMD 0 #define OLED_DATA 1 #define u8 uint8_t #define mydelay_us 10 #define I2C_SDA (gpio_num_t)18 #define I2C_SCL (gpio_num_t)19 #define SDA_Clear() gpio_set_level(I2C_SDA,0) #define SCL_Clear() gpio_set_level(I2C_SCL,0) #define SDA_Set() gpio_set_level(I2C_SDA,1) #define SCL_Set() gpio_set_level(I2C_SCL,1) u8 OLED_GRAM[144][8]; int usleep(useconds_t us); void oled_init() { gpio_config_t my_gpio; my_gpio.intr_type = GPIO_INTR_DISABLE; //下降沿触发 my_gpio.mode = GPIO_MODE_OUTPUT; //设置引脚为输入输出模式,如果是检测外部电平,则需要选择带有输入的模式 my_gpio.pin_bit_mask = ( 1ull << GPIO_NUM_19); //注意:引脚掩码是64位 my_gpio.pull_up_en = 1; //使能上拉,注意:使用软件的上拉,其带负载能力非常的差,这个上拉和下拉是针对输出高电平时,如果外部没有上拉电阻,采用内部的上拉电阻,那么同样是输出3.3v,内部的供电电流小,外部上拉采用的是外部的3.3v,所以电流比较大,带负载能力比较强 my_gpio.pull_down_en = 0; //下拉,应该是没什么区别的 gpio_config( &my_gpio ); my_gpio.intr_type = GPIO_INTR_DISABLE; //下降沿触发 my_gpio.mode = GPIO_MODE_OUTPUT; //设置引脚为输入输出模式,如果是检测外部电平,则需要选择带有输入的模式 my_gpio.pin_bit_mask = ( 1ull << GPIO_NUM_18 ); //注意:引脚掩码是64位 my_gpio.pull_up_en = 1; //使能上拉,注意:使用软件的上拉,其带负载能力非常的差,这个上拉和下拉是针对输出高电平时,如果外部没有上拉电阻,采用内部的上拉电阻,那么同样是输出3.3v,内部的供电电流小,外部上拉采用的是外部的3.3v,所以电流比较大,带负载能力比较强 my_gpio.pull_down_en = 0; //下拉,应该是没什么区别的 gpio_config( &my_gpio ); } void IIC_delay(void) { usleep(5); } //起始信号 void I2C_Start(void) { SDA_Set(); SCL_Set(); IIC_delay(); SDA_Clear(); IIC_delay(); SCL_Clear(); IIC_delay(); } //结束信号 void I2C_Stop(void) { SDA_Clear(); SCL_Set(); IIC_delay(); SDA_Set(); } //等待信号响应 :传输完一个字节的数据就要等待应答 void I2C_WaitAck(void) //测数据信号的电平 { //也就是在时钟线在高电平的时候,将硬件间高电平的SDA拉低 SDA_Set(); IIC_delay(); SCL_Set(); IIC_delay(); SCL_Clear(); //时钟线只有在拉低的时候,才能切换数据线,也就是换一个数据 IIC_delay(); } //写入一个字节 void Send_Byte(u8 dat) { u8 i; for(i=0;i<8;i++) { if(dat&0x80)//将dat的8位从最高位依次写入 { SDA_Set(); } else { SDA_Clear(); } IIC_delay(); SCL_Set(); //在数据传输的时候,必须要保证时钟脉冲在高电平期间的稳定(保持高电平) IIC_delay(); SCL_Clear(); //将时钟信号设置为低电平 dat<<=1; } } //发送一个字节 //mode:数据/命令标志 0,表示命令;1,表示数据; void OLED_WR_Byte(u8 dat,u8 mode) { I2C_Start(); Send_Byte(0x78); I2C_WaitAck(); if(mode){Send_Byte(0x40);} //写入寄存器的值 else{Send_Byte(0x00);} I2C_WaitAck(); //每次写完一个字节,就要等待确认信号 Send_Byte(dat); I2C_WaitAck(); I2C_Stop(); } void oled_soft_init() { oled_init(); OLED_WR_Byte(0xAE, OLED_CMD); //--display off OLED_WR_Byte(0x00, OLED_CMD); //---set low column address OLED_WR_Byte(0x10, OLED_CMD); //---set high column address OLED_WR_Byte(0x40, OLED_CMD); //--set start line address OLED_WR_Byte(0xB0, OLED_CMD); //--set page address OLED_WR_Byte(0x81, OLED_CMD); // contract control OLED_WR_Byte(0xFF, OLED_CMD); //--128 OLED_WR_Byte(0xA1, OLED_CMD); // set segment remap OLED_WR_Byte(0xA6, OLED_CMD); //--normal / reverse OLED_WR_Byte(0xA8, OLED_CMD); //--set multiplex ratio(1 to 64) OLED_WR_Byte(0x3F, OLED_CMD); //--1/32 duty OLED_WR_Byte(0xC8, OLED_CMD); // Com scan direction OLED_WR_Byte(0xD3, OLED_CMD); //-set display offset OLED_WR_Byte(0x00, OLED_CMD); // OLED_WR_Byte(0xD5, OLED_CMD); // set osc division OLED_WR_Byte(0x80, OLED_CMD); // OLED_WR_Byte(0xD8, OLED_CMD); // set area color mode off OLED_WR_Byte(0x05, OLED_CMD); // OLED_WR_Byte(0xD9, OLED_CMD); // Set Pre-Charge Period OLED_WR_Byte(0xF1, OLED_CMD); // OLED_WR_Byte(0xDA, OLED_CMD); // set com pin configuartion OLED_WR_Byte(0x12, OLED_CMD); // OLED_WR_Byte(0xDB, OLED_CMD); // set Vcomh OLED_WR_Byte(0x30, OLED_CMD); // OLED_WR_Byte(0x8D, OLED_CMD); // set charge pump enable OLED_WR_Byte(0x14, OLED_CMD); // OLED_WR_Byte(0xAF, OLED_CMD); //--turn on oled panel } //更新显存到OLED void OLED_Refresh(void) { u8 i,n; for(i=0;i<8;i++) { OLED_WR_Byte(0xb0+i,OLED_CMD); //设置行起始地址 也就是第几页 ,分成了8页(8行) OLED_WR_Byte(0x00,OLED_CMD); //设置低列起始地址 也就是每一页中都是有8行 128列的像素点 8位数据高位为0代表列(0~127),高位为1代表行 OLED_WR_Byte(0x10,OLED_CMD); //设置高列起始地址 I2C_Start(); Send_Byte(0x3c); //设置从机地址,最低为为1 为读模式;为0,为写模式 I2C_WaitAck(); Send_Byte(0x40); //定义写入的是数据还是指令 ,位6为1 为数据,为0为数据, I2C_WaitAck(); for(n=0;n<128;n++) { Send_Byte(OLED_GRAM[n][i]); I2C_WaitAck(); } I2C_Stop(); } } void OLED_Soft_On(void) { // u8 i, n; // for (i = 0; i < 8; i++) // { // OLED_WR_Byte(0xb0 + i, OLED_CMD); // 设置页地址(0~7) // OLED_WR_Byte(0x00, OLED_CMD); // 设置显示位置—列低地址 // OLED_WR_Byte(0x10, OLED_CMD); // 设置显示位置—列高地址 // for (n = 0; n < 128; n++) // OLED_WR_Byte(1, OLED_DATA); // } u8 i,n; for(i=0;i<8;i++) { for(n=0;n<128;n++) { OLED_GRAM[n][i]=1; } } // 更新显示 OLED_Refresh(); } // 清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!! void OLED_Soft_Clear(void) { // u8 i, n; // for (i = 0; i < 8; i++) // { // OLED_WR_Byte(0xb0 + i, OLED_CMD); // 设置页地址(0~7) // OLED_WR_Byte(0x00, OLED_CMD); // 设置显示位置—列低地址 // OLED_WR_Byte(0x10, OLED_CMD); // 设置显示位置—列高地址 // for (n = 0; n < 128; n++) // OLED_WR_Byte(0, OLED_DATA); // } u8 i,n; for(i=0;i<8;i++) { for(n=0;n<128;n++) { OLED_GRAM[n][i]=0;//清除所有数据 } } // 更新显示 OLED_Refresh(); }参考资料