【硬件IIC】stm32单片机利用硬件IIC驱动OLED屏幕
之前操作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)
{
/*错误的一次只写一个数据,时序不完整*/
// uint8_t dataOptionByte = 0x00;
// I2C_SendData(slaveAddr, &dataOptionByte, 1, 1000);
// I2C_SendData(slaveAddr, &dataData, 1, 1000);
/*一次只写入完整的时序即可*/
uint8_t dataArr[2] = {0x40, data};
I2C_SendData(slaveAddr, dataArr, 2, 1000);
/*使用HAL_I2C_Mem_Write函数,也可实现相同效果*/
// uint8_t tmpData = data;
// HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT,
// &tmpData, 1, 0xff);
}
//封装函数,实现对往OLED中写入一个字节命令
void oled_write_cmd(uint8_t cmd)
{
/*错误的一次只写一个数据,时序不完整*/
// uint8_t cmdOptionByte = 0x00;
// I2C_SendData(slaveAddr, &cmdOptionByte, 1, 1000);
// I2C_SendData(slaveAddr, &dataCmd, 1, 1000);
/*一次只写入完整的时序即可*/
uint8_t cmdArr[2] = {0x00, cmd};
I2C_SendData(slaveAddr, cmdArr, 2, 1000);
/*使用HAL_I2C_Mem_Write函数,也可实现相同效果*/
// uint8_t tmpCmd = cmd;
// HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT,
// &tmpCmd, 1, 0xff);
}
完整代码
oled.c
#include "oled.h"
#include "delay.h"
#include "font.h"
I2C_HandleTypeDef hi2c1;
uint16_t slaveAddr = 0x78;
void oled_gpio_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
/**I2C1 GPIO Configuration
PB6 ------> I2C1_SCL
PB7 ------> I2C1_SDA
*/
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
// 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);
}
/*oled_gpio_init()函数中不配置GPIO口和使能时钟,在MSP函数中配置也可以,因为
执行HAL_I2C_Init(&hi2c1);时,会执行HAL_I2C_MspInit函数
*/
//void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
//{
// GPIO_InitTypeDef GPIO_InitStruct = {0};
// if(i2cHandle->Instance==I2C1)
// {
// /* USER CODE BEGIN I2C1_MspInit 0 */
// /* USER CODE END I2C1_MspInit 0 */
// __HAL_RCC_GPIOB_CLK_ENABLE();
// /**I2C1 GPIO Configuration
// PB6 ------> I2C1_SCL
// PB7 ------> I2C1_SDA
// */
// GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
// 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();
// /* USER CODE BEGIN I2C1_MspInit 1 */
// /* USER CODE END I2C1_MspInit 1 */
// }
//}
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_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);
}
void oled_write_data(uint8_t data)
{
// uint8_t dataOptionByte = 0x00;
// I2C_SendData(slaveAddr, &dataOptionByte, 1, 1000);
// I2C_SendData(slaveAddr, &dataData, 1, 1000);
uint8_t dataArr[2] = {0x40, data};
I2C_SendData(slaveAddr, dataArr, 2, 1000);
// uint8_t tmpData = data;
// HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT,
// &tmpData, 1, 0xff);
}
void oled_write_cmd(uint8_t cmd)
{
// uint8_t cmdOptionByte = 0x00;
// I2C_SendData(slaveAddr, &cmdOptionByte, 1, 1000);
// I2C_SendData(slaveAddr, &dataCmd, 1, 1000);
uint8_t cmdArr[2] = {0x00, cmd};
I2C_SendData(slaveAddr, cmdArr, 2, 1000);
// uint8_t tmpCmd = cmd;
// HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT,
// &tmpCmd, 1, 0xff);
}
void oled_init(void)
{
oled_gpio_init();
delay_ms(100);
oled_write_cmd(0xAE); //设置显示开启/关闭,0xAE关闭,0xAF开启
oled_write_cmd(0xD5); //设置显示时钟分频比/振荡器频率
oled_write_cmd(0x80); //0x00~0xFF
oled_write_cmd(0xA8); //设置多路复用率
oled_write_cmd(0x3F); //0x0E~0x3F
oled_write_cmd(0xD3); //设置显示偏移
oled_write_cmd(0x00); //0x00~0x7F
oled_write_cmd(0x40); //设置显示开始行,0x40~0x7F
oled_write_cmd(0xA1); //设置左右方向,0xA1正常,0xA0左右反置
oled_write_cmd(0xC8); //设置上下方向,0xC8正常,0xC0上下反置
oled_write_cmd(0xDA); //设置COM引脚硬件配置
oled_write_cmd(0x12);
oled_write_cmd(0x81); //设置对比度
oled_write_cmd(0xCF); //0x00~0xFF
oled_write_cmd(0xD9); //设置预充电周期
oled_write_cmd(0xF1);
oled_write_cmd(0xDB); //设置VCOMH取消选择级别
oled_write_cmd(0x30);
oled_write_cmd(0xA4); //设置整个显示打开/关闭
oled_write_cmd(0xA6); //设置正常/反色显示,0xA6正常,0xA7反色
oled_write_cmd(0x8D); //设置充电泵
oled_write_cmd(0x14);
oled_write_cmd(0xAF); //开启显示
}
// y的值为page,屏幕总共有8个page
void oled_set_cursor(uint8_t x, uint8_t y)
{
oled_write_cmd(0xB0 + y); //确定在哪一个page, 第一个page地址是B0
oled_write_cmd((x & 0x0F) | 0x00); //取字节的低位
oled_write_cmd(((x & 0xF0) >> 4) | 0x10); //取字节的高位,|0x10是因为oled芯片的要求
}
//清屏函数,每次刷新画面时,需要清屏,防止上一帧数据残留
void oled_fill(uint8_t data)
{
uint8_t i, j;
for( i = 0; i <8 ; i++)
{
oled_set_cursor(0, i);
for(j = 0; j < 128; j++)
{ //page模式,地址每次都自动偏移
oled_write_data(data);
}
}
}
//输入字符的坐标、ASCII码、大小; size一般是宽为高的1/2;
void oled_show_char(uint8_t x, uint8_t y, uint8_t num, uint8_t size)
{
uint8_t i, j, page;
num = num - ' ';
page = size/8;
if (size % 8)
page++;
for(j = 0; j < page; j++ )
{
oled_set_cursor(x, y + j);
for(i = size/2 * j; i < size/2 *(j + 1); i++) //即每一个page写size/2宽的数据,
{
if (size == 12)
oled_write_data(ascii_6X12[num][i]);
else if (size == 16)
oled_write_data(ascii_8X16[num][i]);
else if (size == 24)
oled_write_data(ascii_12X24[num][i]);
}
}
}
//输入字符的坐标、字符指针、大小; size一般是宽为高的1/2;
void oled_show_string(uint8_t x, uint8_t y, char *p, uint8_t size)
{
while( *p !='\0')
{
oled_show_char(x, y, *p, size);
x += size/2;
p++;
}
}
oled.h
#ifndef __OLED_H__
#define __OLED_H__
#include "sys.h"
void oled_gpio_init(void);
void oled_init(void);
void oled_write_cmd(uint8_t cmd);
void oled_write_data(uint8_t data);
void oled_set_cursor(uint8_t x, uint8_t y);
void oled_fill(uint8_t data);
void oled_show_char(uint8_t x, uint8_t y, uint8_t num, uint8_t size);
void oled_show_string(uint8_t x, uint8_t y, char *p, uint8_t size);
#endif
main.c
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "uart1.h"
#include "oled.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* 初始化LED灯 */
uart1_init(115200);
printf("hello world!\r\n");
oled_init();
oled_fill(0x00);
oled_set_cursor(0, 0);
//划线
// oled_write_data(0x80);
// oled_write_data(0x80);
// oled_write_data(0x80);
// oled_write_data(0x80);
// oled_write_data(0x80);
// oled_write_data(0x80);
// oled_show_char(0, 0, 'x', 24);
oled_show_string(0, 2, "I am Rio", 24);
while(1)
{
led1_on();
led2_off();
delay_ms(500);
led1_off();
led2_on();
delay_ms(500);
}
}