之前操作OLED屏幕都是用GPIO模拟IIC去驱动,最近打算用硬件IIC去驱动,于是写下这个demo,在这个过程中遇到一点小坑,记录一下,本文章非小白教程,所以只突出踩到的坑点,文章中涉及到的OLED也是网上资料写烂的,所以不懂的同学可以万能的百度。话不多说开始。
目标
使用硬件IIC驱动OLED屏,显示英文字符串“I am Rio”
完整项目工程链接: stm32F103C8T6驱动OLED屏显示字符
同时附上一篇网上找的介绍这个OLED屏的文章
0.96寸OLED(SSD1306)屏幕显示(一)——基础功能介绍
硬件
MCU: stm32f103c8t6
屏幕: 0.96寸OLED(SSD1306)
本次使用的是 gpio 的PB6,PB7脚,这两个脚位可以复用硬件I2C1。
驱动程序
代码中已加入详细注释,放心食用
GPIO和IIC初始化配置
这里遇到第一个坑,就是在配置IO的模式时,一开始设置成推挽输出GPIO_MODE_OUTPUT_PP,因为想着就接一个IIC设备,推挽输出或者开漏输出,都影响不大,结果还说遇到坑了,配置成推挽输出之后,程序会出现卡死现象,debug之后发现程序卡死在函数HAL_I2C_Init(&hi2c1)里; 然后进入到错误HardFault_Handler()中,怀疑是HAL_I2C_Init中的某些设置与设置成推挽输出GPIO_MODE_OUTPUT_PP出现冲突导致硬件错误,具体还未深入研究,欢迎大佬补充。
将GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;后不再出现卡死现象。
I2C_HandleTypeDef hi2c1;
uint16_t slaveAddr = 0x78; //OLED显示屏的IIC设备地址,改地址为写地址
void oled_gpio_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {
0};
__HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB口时钟
/**I2C1 GPIO Configuration
PB6 ------> I2C1_SCL
PB7 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
//当时就是这句导致程序卡死,用HAL库实现硬件IIC时还是乖乖用开漏模式好
// GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; //配置为开漏模式
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* I2C1 clock enable */
__HAL_RCC_I2C1_CLK_ENABLE();
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 设置I2C时钟速度为100kHz(可以根据需要调整)
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比(通常不需要修改)
hi2c1.Init.OwnAddress1 = 0; // 主设备通常不需要设置自己的地址
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位地址模式
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; // 双地址模式禁用
hi2c1.Init.OwnAddress2 = 0; // 不使用第二个地址
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; // 通用调用模式禁用
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 时钟延伸模式禁用
HAL_I2C_Init(&hi2c1);
}
IIC的读和写
这里由于没有理解透HAL库的函数踩到第二个坑,用的这一款OLED在写入命令或者数据时,时序是与从机建立IIC通讯开始信号后,就连续写入control byte + data byte; 比如写数据为 0x40 + data; 写命令是0x00 + cmd;
而HAL库提供的IIC读写函数有好几种,如:
HAL_I2C_Master_Receive(&hi2c1, DevAddress, pData, Size, Timeout);
HAL_I2C_Master_Transmit(&hi2c1, DevAddress, pData, Size, Timeout);
这一组是用来作为主机时,对从机进行读写操作,而我一开始在往从机写数据的时候,使用的就是HAL_I2C_Master_Transmit函数,将control byte + data byte;分开两次来发送,结果就写失败了。原因是分开两次写的话,HAL_I2C_Master_Transmit每次写完一个byte数据之后,就结束该次通讯,这就相当于没有发送完整的control byte + data byte时序;而是
第一次发送control byte结束;第二次发送data byte结束;所以两次没有一次是完整的组合时序。正确的是应该在一次通讯中发送两个byte数据才行;
所以我将要发送的时序进行组合,然后一次发送两个数据即可
uint8_t dataArr[2] = {0x40, data};
I2C_SendData(slaveAddr, dataArr, 2, 1000);
//对HAL_I2C_Master_Transmit函数进行封装
HAL_StatusTypeDef I2C_SendData(uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout)
{
return HAL_I2C_Master_Transmit(&hi2c1, DevAddress, pData, Size, Timeout);
}
//对HAL_I2C_Master_Receive函数进行封装
HAL_StatusTypeDef I2C_ReceiveData(uint16_t DevAddress, uint8_t* pData, uint16_t Size, uint32_t Timeout)
{
return HAL_I2C_Master_Receive(&hi2c1, DevAddress, pData, Size, Timeout);
}
//封装函数,实现对往OLED中写入一个字节数据
void oled_write_data(uint8_t data