当前位置: 首页 > article >正文

【硬件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);
    }
}

http://www.kler.cn/a/443722.html

相关文章:

  • webrtc音频模块(三) windows Core Audio API及声音的播放
  • 010 Qt_输入类控件(LineEdit、TextEdit、ComboBox、SpinBox、DateTimeEdit、Dial、Slider)
  • 在Visual Studio 2022中配置C++计算机视觉库Opencv
  • 【机器学习】探索机器学习与人工智能:驱动未来创新的关键技术
  • C++----类与对象(下篇)
  • 【从零开始入门unity游戏开发之——C#篇21】C#面向对象的封装——`this`扩展方法、运算符重载、内部类、`partial` 定义分部类
  • C05S09-Keepalive服务架设
  • pytest 小技巧:非测试方法如何使用 pytest fixture
  • 笔记--(Shell脚本04)、循环语句与函数
  • 多功能护照阅读器港澳通行证阅读机RS232串口主动输出协议,支持和单片机/Linux对接使用
  • Midjourney教程之生成同一角色的不同姿势和服装
  • Docker 容器网络问题排查与最佳实践 - PushGateway 部署案例分析
  • 详细分析:AG32 MCU与STM32/GD32的区别
  • Android 折叠屏问题解决 - 展开后布局未撑开
  • 【图像配准】方法总结
  • HarmonyOS NEXT 应用开发实战:音乐播放器的完整实现
  • Vue|scoped样式
  • mapboxGL中室内地图的实现
  • PowerMILL 客制化宏 - 命令关键字
  • 应用如何借用manifestxml追加gid权限
  • PostgreSql-学习06-libpq之同步命令处理
  • go 自己写序列化函数不转义
  • UE5 角色换新枪械的偷懒办法
  • UE5 Lyra项目源码分析-角色配置说明
  • WebRTC服务质量(04)- 重传机制(01) RTX NACK概述
  • 简易记事本开发-(SSM+Vue)