以I2C方式驱动OLED
之前给开发板加载显示部件时,我一般都是用模拟I2C方式,使用I2C的OLED。因为占用IO口线少,程序移植方便。移植的时候,只需要调整I2C的脉冲周期匹配就行。一般也不会有太大的问题,实在不好解决,可以通过示波器查看脉冲宽度,做调整,都能解决。直到在我使用其它I2C设别的时候,遇到了了无法解决的问题。
这个问题是关于GXHT30的温湿度传感器的。这个传感器首先在Arduino环境下,利用大佬的库,实现了温湿度的检测。然后把程序移植到雅特力开发板,经过调整,也正常通过。但当我把这个传感器连接到恩智浦单片机开发板的时候,出了问题。无论怎么调整程序,都无法正常获得数据。程序已经根据单片机做了调整,用示波器看,发出的测量指令也正常,就是得不到正常数据。调了几天都没有调通。于是决定换个思路,不再使用模拟I2C方式,而是使用单片机的I2C设备来完成通讯。这样,IC的通讯过程完全由I2C设备完成,不会受到其他影响(比如中断导致的额I2C时序出问题、时钟周期不匹配等)。
但是在这之前,准备先用I2C设备,实现OLED的驱动。因为这个在测试上可以直接通过显示,来确认是否正常。这次试验以芯源的开发板做。为此看I2C的例程,读芯片的数据手册、用手册等。然后在理解了例程的基础上,进行改造。我手里的开发板上芯片,与例程中的GPIO设置是不一样的,根据芯片和开发板的特点,修改成合适的I2C外设,
首先是I2C的GPIO初始化:
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); }
例程中使用PB0和PB1作为SCL和SDA,而我手里的开发板上的芯片,没有PB1这个输出口。根据手册,
功能复用用到的设置:
因为复用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的通讯过程也很重要。在用户手册中,对主机发送的处理,有如下流程,
主机发送模式:
一定要注意在发送数据过程中,每个阶段的返回状态值。就是0x18(0x20),0x28(0x30),括号里的值是针对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*/ };
应该显示为:初始化中……
改造完成后,编译、下载、运行,
可以看到,成功了。
之前用过I2C的方式做过其他操作,需要通过检测中间状态来确定下一部动作,感觉挺麻烦的,不如模拟方式来的容易。现在看看,其实也没那么麻烦,省掉好多模拟用的代码,代码也变得简洁了。而且铜须的具体过程,不需要操心,全都由I2C外设做就行,不用调整时钟周期了,也挺好的。其实还是模拟方式出了问题解决不了而被逼使用的,哈哈哈。后面继续使用I2C方式驱动GXHT30温湿度传感器,这个涉及到主机发送、主机接收的处理,中间需要转换,估计还得花些时间。