这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【换取逻辑分析仪】+以I2C方式驱动OLED

共7条 1/1 1 跳转至

【换取逻辑分析仪】+以I2C方式驱动OLED

专家
2024-07-03 19:49:59   被打赏 40 分(兑奖)     打赏

I2C方式驱动OLED

之前给开发板加载显示部件时,我一般都是用模拟I2C方式,使用I2COLED。因为占用IO口线少,程序移植方便。移植的时候,只需要调整I2C的脉冲周期匹配就行。一般也不会有太大的问题,实在不好解决,可以通过示波器查看脉冲宽度,做调整,都能解决。直到在我使用其它I2C设别的时候,遇到了了无法解决的问题。

这个问题是关于GXHT30的温湿度传感器的。这个传感器首先在Arduino环境下,利用大佬的库,实现了温湿度的检测。然后把程序移植到雅特力开发板,经过调整,也正常通过。但当我把这个传感器连接到恩智浦单片机开发板的时候,出了问题。无论怎么调整程序,都无法正常获得数据。程序已经根据单片机做了调整,用示波器看,发出的测量指令也正常,就是得不到正常数据。调了几天都没有调通。于是决定换个思路,不再使用模拟I2C方式,而是使用单片机的I2C设备来完成通讯。这样,IC的通讯过程完全由I2C设备完成,不会受到其他影响(比如中断导致的额I2C时序出问题、时钟周期不匹配等)。

但是在这之前,准备先用I2C设备,实现OLED的驱动。因为这个在测试上可以直接通过显示,来确认是否正常。这次试验以芯源的开发板做。为此看I2C的例程,读芯片的数据手册、用手册等。然后在理解了例程的基础上,进行改造。我手里的开发板上芯片,与例程中的GPIO设置是不一样的,根据芯片和开发板的特点,修改成合适的I2C外设,

首先是I2CGPIO初始化:


void GPIO_Configuration(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    // GPIOB_AFRL : GPIOB 复用功能寄存器低段, 实现输入输出端口的复用功能 3=0x11->AF3 147页说明
    //PB00_AFx_I2C2SCL();
    PA01_AFx_I2C2SCL();            // #define PA01_AFx_I2C2SCL()      (CW_GPIOA->AFRL_f.AFR1  = 3)
    // 3=0x11->AF3 147页说明
    //PB01_AFx_I2C2SDA();               
    PA02_AFx_I2C2SDA();            //  #define PA02_AFx_I2C2SDA()     (CW_GPIOA->AFRL_f.AFR2  = 3)
    // 初始化SCL和SDA所在GPIO口
    GPIO_InitStructure.Pins = I2C2_SCL_GPIO_PIN | I2C2_SDA_GPIO_PIN;
    // 复用推挽输出
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    // 高速
    GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
    // 执行初始化
    GPIO_Init(I2C2_SCL_GPIO_PORT, &GPIO_InitStructure);
}


例程中使用PB0PB1作为SCLSDA,而我手里的开发板上的芯片,没有PB1这个输出口。根据手册,

图片1.png 

功能复用用到的设置:

 图片2.png

因为复用PA口的GPIO口,所以要使能PA口的时钟。

/**
  * @brief  Configures the different system clocks.
  * @param  None
  * @retval None
  */
void RCC_Configuration(void) {
    // 使能GPIOB 端口配置时钟及工作时钟使能控制
    CW_SYSCTRL->AHBEN_f.GPIOB  = 1;
    CW_SYSCTRL->AHBEN_f.GPIOA  = 1;
    
    // 使能I2C2 模块配置时钟及工作时钟使能控制
    CW_SYSCTRL->APBEN1_f.I2C2 = 1U;    //
}


CW_SYSCTRL->AHBEN_f.GPIOA  = 1;这行代码就是使能PA口的时钟。

PA口的设置结束了,接着看下PA口上复用的I2C2设备的初始化I2C_Master_Init(CW_I2C2, &I2C_InitStruct);

 

/**
 * @brief I2C:MASTER初始化函数
 *
 * @param I2Cx      : I2C1  I2C2
 * @param I2C_InitStruct
 */
void I2C_Master_Init(I2C_TypeDef *I2Cx, I2C_InitTypeDef *I2C_InitStruct) {
    I2C_SetBaud(I2Cx, I2C_InitStruct->I2C_Baud);                    // 配置波特率计数器
    I2C_BaudGeneratorEnable(I2Cx, I2C_InitStruct->I2C_BaudEn);       // 使能波特率计数器
    I2C_FilterConfig(I2Cx, I2C_InitStruct->I2C_FLT);                   //  配置FLT
    I2C_AcknowledgeConfig(I2Cx, I2C_InitStruct->I2C_AA);             //  配置ACK
    if (I2C_InitStruct->I2C_Baud <= 9)  {
        I2C_FilterConfig(I2Cx, ENABLE);
    }
}


这部分的代码,不需要改动,直接使用例程的就行。

IIC的初始化等配置动作是在主程序里执行的。

int32_t main(void) {
    uint16_t tempcnt = 0 ;
    I2C_InitTypeDef I2C_InitStruct;
    
    //时钟初始化
    RCC_Configuration();
    
    //IO口初始化
    GPIO_Configuration();
    
    //初始化I2C外设,设置通讯参数
    I2C_InitStruct.I2C_BaudEn = ENABLE;                         // 使能波特率计数器
    I2C_InitStruct.I2C_Baud = 0x01;//500K<-(8000000/(8*(1+1))   // 设置通讯波特率:fSCL = fPCLK / 8 / ( BRR + 1 )
    I2C_InitStruct.I2C_FLT = DISABLE;                           // FLT配置
    I2C_InitStruct.I2C_AA = DISABLE;                            // ACK配置
 
    I2C2_DeInit();                                              // 关闭初始化
    I2C_Master_Init(CW_I2C2, &I2C_InitStruct); //初始化模块
    I2C_Cmd(CW_I2C2, ENABLE); //模块使能
 
    OLED_Init();
    SYSTEM_Init();
 
    while (1) {
        
    }
}


OLED的初始化,因为要是用I2C方式发送数据,所以要改造发送字节的处理,改造后的代码,

//发送一个字节

//SSD1306写入一个字节。

//mode:数据/命令标志 0,表示命令;1,表示数据;

void OLED_WR_Byte(uint8_t dat,uint8_t mode) {
    uint8_t u8i = 0, u8State;
    
    // 发出开始信号
    I2C_GenerateSTART(CW_I2C2, ENABLE);
while (1) {
    // 获取中断标志位,等待发生在中断(发送START后产生的)
        while (0 == I2C_GetIrq(CW_I2C2))
        {;}
        // 获取当前状态值
        u8State = I2C_GetState(CW_I2C2);
        switch (u8State) {
            case 0x08:   // 正常发送完START信号后,产生的状态值
                I2C_GenerateSTART(CW_I2C2, DISABLE);            // 中间不再需要发送START信号了,所以禁止
                I2C_Send7bitAddress(CW_I2C2, OLED_ADDR, 0X00);  // 从设备地址发送
                break;
            case 0x18:   //正常发送完写地址SLA+W信号,ACK已收到时的状态值:0x18
                // 根据数据的模式(是指令,还是数据),决定发送的数据:指令=0x00,数据=0x40   
                if(mode){
                    // 数据
                    I2C_SendData(CW_I2C2,0x40);
                } else {
                    // 指令
                    I2C_SendData(CW_I2C2,0x00);
                }
                break;
            
            case 0x28:   // 正常发送完数据(包括OLED指令)后产生的状态值
                // 如果发送的数据很多,那么这个状态会一直持续,直到发送完你想要发的数据
// 这里使用u8i作为数据发送的计数变量,全部发送是否完成了,在后面判断
                I2C_SendData(CW_I2C2, dat);
                u8i++;
                break;
            
            case 0x20:   //发送完SLA+W后从机返回NACK
                
            case 0x38:   //主机在发送 SLA+W 阶段或者发送数据阶段丢失仲裁 或者 主机在发送 SLA+R 阶段或者回应 NACK 阶段丢失仲裁
                I2C_GenerateSTART(CW_I2C2, ENABLE);
                break;
            
            case 0x30:   // 发送完一个数据字节后从机返回NACK
                I2C_GenerateSTOP(CW_I2C2, ENABLE);
                break;
            
            default:
                break;
        }
        // 是不是已经发送完全部数据量,是的话,发送STOP信号,结束发送
        if (u8i > 1) {
            I2C_GenerateSTOP(CW_I2C2, ENABLE);// 发出停止信号
            I2C_ClearIrq(CW_I2C2);
            break;
        }
        // 清除中断标志位
        I2C_ClearIrq(CW_I2C2);
    }
}


 

这个发送过程是因为通过I2C实现的,因此把握I2C的通讯过程也很重要。在用户手册中,对主机发送的处理,有如下流程,

主机发送模式

图片3.png 

图片4.png

一定要注意在发送数据过程中,每个阶段的返回状态值。就是0x180x20),0x280x30),括号里的值是针对NACK方式的返回值,没有括起来的是针对ACK方式的。看懂了流程,再看代码就会理解了。

OLED的初始化程序,利用这个函数,发送初始化的指令和数据:

void OLED_Init(void) {
DelayMs(100);
OLED_WR_Byte(0xAE,OLED_CMD);//--turn off oled panel
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  Set Mapping RAM Display Start Line (0x00~0x3F)
OLED_WR_Byte(0x81,OLED_CMD);//--set contrast control register
OLED_WR_Byte(0xCF,OLED_CMD);// Set SEG Output Current Brightness
OLED_WR_Byte(0xA1,OLED_CMD);//--Set SEG/Column Mapping     0xa0左右反置 0xa1正常
OLED_WR_Byte(0xC8,OLED_CMD);//Set COM/Row Scan Direction   0xc0上下反置 0xc8正常
OLED_WR_Byte(0xA6,OLED_CMD);//--set normal display
OLED_WR_Byte(0xA8,OLED_CMD);//--set multiplex ratio(1 to 64)
OLED_WR_Byte(0x3f,OLED_CMD);//--1/64 duty
OLED_WR_Byte(0xD3,OLED_CMD);//-set display offset Shift Mapping RAM Counter (0x00~0x3F)
OLED_WR_Byte(0x00,OLED_CMD);//-not offset
OLED_WR_Byte(0xd5,OLED_CMD);//--set display clock divide ratio/oscillator frequency
OLED_WR_Byte(0x80,OLED_CMD);//--set divide ratio, Set Clock as 100 Frames/Sec
OLED_WR_Byte(0xD9,OLED_CMD);//--set pre-charge period
OLED_WR_Byte(0xF1,OLED_CMD);//Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
OLED_WR_Byte(0xDA,OLED_CMD);//--set com pins hardware configuration
OLED_WR_Byte(0x12,OLED_CMD);
OLED_WR_Byte(0xDB,OLED_CMD);//--set vcomh
OLED_WR_Byte(0x40,OLED_CMD);//Set VCOM Deselect Level
OLED_WR_Byte(0x20,OLED_CMD);//-Set Page Addressing Mode (0x00/0x01/0x02)
OLED_WR_Byte(0x02,OLED_CMD);//
OLED_WR_Byte(0x8D,OLED_CMD);//--set Charge Pump enable/disable
OLED_WR_Byte(0x14,OLED_CMD);//--set(0x10) disable
OLED_WR_Byte(0xA4,OLED_CMD);// Disable Entire Display On (0xa4/0xa5)
OLED_WR_Byte(0xA6,OLED_CMD);// Disable Inverse Display On (0xa6/a7) 
OLED_WR_Byte(0xAF,OLED_CMD);
OLED_DisPlay_Off();
OLED_Refresh();
OLED_Clear();
OLED_DisPlay_On();
}


在主程序中,使用SYSTEM_Init();,在OLED上输出字符串,

void SYSTEM_Init(void) {
OLED_ShowChinese(  0,  0, 5,16);
OLED_ShowChinese( 16,  0, 6,16);
OLED_ShowChinese( 32,  0, 7,16);
OLED_ShowChinese( 48,  0, 8,16);
OLED_ShowChinese( 64,  0, 9,16);
OLED_ShowChinese( 80,  0, 9,16);
OLED_Refresh();
}


根据点阵设置

unsigned char Hzk1[32][16]={
{0x00,0x40,0x42,0x44,0x58,0x40,0x40,0x7F,0x40,0x40,0x50,0x48,0xC6,0x00,0x00,0x00},
{0x00,0x40,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0x44,0xFF,0x00,0x00,0x00},/*"当",0*/
 
{0x08,0x08,0xE8,0x29,0x2E,0x28,0xE8,0x08,0x08,0xC8,0x0C,0x0B,0xE8,0x08,0x08,0x00},
{0x00,0x00,0xFF,0x09,0x49,0x89,0x7F,0x00,0x00,0x0F,0x40,0x80,0x7F,0x00,0x00,0x00},/*"前",1*/
 
{0x10,0x60,0x02,0x8C,0x00,0x00,0xFE,0x92,0x92,0x92,0x92,0x92,0xFE,0x00,0x00,0x00},
{0x04,0x04,0x7E,0x01,0x40,0x7E,0x42,0x42,0x7E,0x42,0x7E,0x42,0x42,0x7E,0x40,0x00},/*"温",2*/
 
{0x00,0x00,0xFC,0x24,0x24,0x24,0xFC,0x25,0x26,0x24,0xFC,0x24,0x24,0x24,0x04,0x00},
{0x40,0x30,0x8F,0x80,0x84,0x4C,0x55,0x25,0x25,0x25,0x55,0x4C,0x80,0x80,0x80,0x00},/*"度",3*/
 
{0x00,0x20,0x22,0x2C,0x20,0x20,0xE0,0x3F,0x20,0x20,0x20,0x20,0xE0,0x00,0x00,0x00},
{0x80,0x40,0x20,0x10,0x08,0x06,0x01,0x00,0x01,0x46,0x80,0x40,0x3F,0x00,0x00,0x00},/*"为",4*/
 
 
{0x08,0x08,0x89,0xEA,0x18,0x88,0x00,0x04,0x04,0xFC,0x04,0x04,0x04,0xFC,0x00,0x00},
{0x02,0x01,0x00,0xFF,0x01,0x86,0x40,0x20,0x18,0x07,0x40,0x80,0x40,0x3F,0x00,0x00},/*"初",5*/
 
{0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x40,0xE0,0x58,0x47,0x40,0x50,0x60,0xC0,0x00},
{0x40,0x22,0x15,0x08,0x16,0x21,0x00,0x00,0xFE,0x42,0x42,0x42,0x42,0xFE,0x00,0x00},/*"始",6*/
 
{0x00,0x80,0x60,0xF8,0x07,0x00,0x00,0x00,0xFF,0x40,0x20,0x10,0x08,0x04,0x00,0x00},
{0x01,0x00,0x00,0xFF,0x00,0x04,0x02,0x01,0x3F,0x40,0x40,0x40,0x40,0x40,0x78,0x00},/*"化",7*/
 
{0x00,0x00,0xF0,0x10,0x10,0x10,0x10,0xFF,0x10,0x10,0x10,0x10,0xF0,0x00,0x00,0x00},
{0x00,0x00,0x0F,0x04,0x04,0x04,0x04,0xFF,0x04,0x04,0x04,0x04,0x0F,0x00,0x00,0x00},/*"中",8*/
 
 
{0x00,0xC0,0xC0,0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"…",9*/
 
 
 
{0x10,0x60,0x02,0x8C,0x00,0xFE,0x92,0x92,0x92,0x92,0x92,0x92,0xFE,0x00,0x00,0x00},
{0x04,0x04,0x7E,0x01,0x44,0x48,0x50,0x7F,0x40,0x40,0x7F,0x50,0x48,0x44,0x40,0x00},/*"湿",10*/
 
{0x00,0x00,0xFC,0x24,0x24,0x24,0xFC,0x25,0x26,0x24,0xFC,0x24,0x24,0x24,0x04,0x00},
{0x40,0x30,0x8F,0x80,0x84,0x4C,0x55,0x25,0x25,0x25,0x55,0x4C,0x80,0x80,0x80,0x00},/*"度",11*/
 
{0x00,0x0C,0x12,0x0C,0x00,0xC0,0x70,0x10,0x08,0x08,0x08,0x08,0x10,0x30,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x0F,0x18,0x30,0x20,0x20,0x20,0x20,0x30,0x1C,0x00,0x00},/*"℃",12*/
 
{0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08},
{0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20,0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20},/*"RH",13*/
 
{0x00,0x00,0x00,0x00,0x00,0x00,0xFC,0x04,0xFC,0xFC,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x1C,0x37,0x28,0x2B,0x37,0x0C,0x00,0x00,0x00,0x00,0x00},/*温度.bmp,14*/
 
{0x00,0x00,0x00,0x00,0xC0,0x60,0x10,0x0C,0x0C,0x10,0x60,0xC0,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x0F,0x10,0x20,0x26,0x26,0x20,0x10,0x0F,0x00,0x00,0x00,0x00},/*湿度.bmp,15*/
};


应该显示为:初始化中……

改造完成后,编译、下载、运行,

图片5.png 

可以看到,成功了。

之前用过I2C的方式做过其他操作,需要通过检测中间状态来确定下一部动作,感觉挺麻烦的,不如模拟方式来的容易。现在看看,其实也没那么麻烦,省掉好多模拟用的代码,代码也变得简洁了。而且铜须的具体过程,不需要操心,全都由I2C外设做就行,不用调整时钟周期了,也挺好的。其实还是模拟方式出了问题解决不了而被逼使用的,哈哈哈。后面继续使用I2C方式驱动GXHT30温湿度传感器,这个涉及到主机发送、主机接收的处理,中间需要转换,估计还得花些时间。

 

 



专家
2024-07-04 06:33:29     打赏
2楼

这个操作复杂


院士
2024-07-11 16:35:05     打赏
3楼

有图有真相。

别用模拟的方式了,那个不是一个作品


专家
2024-07-12 09:25:43     打赏
4楼

哈哈哈,您说得有道理,我也认同。但是,以模拟方式移植程序是最快的方式。不同厂家的单片机,在处理I2C的方式上,不尽相同,要做很多工作。


专家
2024-07-12 09:28:37     打赏
5楼

比如使用芯源的是一个样,使用TI的是有一个样子。包括对I2C的初始化上,发送数据时的处理。有的需要判断中间发送状态,而有的,直接把数据教给I2C外设就行,不用管中间状态(比如0x08,0x18,0x28之类的)。判断状态的比较麻烦,需要根据状态值,决定下一步怎么动作。


菜鸟
2024-07-15 08:46:12     打赏
6楼

求助


工程师
2024-07-16 09:18:21     打赏
7楼

+666666


共7条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]