之前操作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 ConfigurationPB6 ------> I2C1_SCLPB7 ------> 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 ConfigurationPB6 ------> I2C1_SCLPB7 ------> 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~0xFFoled_write_cmd(0xA8); //設置多路復用率oled_write_cmd(0x3F); //0x0E~0x3Foled_write_cmd(0xD3); //設置顯示偏移oled_write_cmd(0x00); //0x00~0x7Foled_write_cmd(0x40); //設置顯示開始行,0x40~0x7Foled_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~0xFFoled_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地址是B0oled_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);}
}