基于STM32F103与U8g2库的OLED图形显示

一、 器材与软件环境

  1. 硬件:
    • STM32F103C8T6核心板 x1
    • 0.96寸OLED显示屏 (SSD1306驱动, I2C接口) x1
    • ST-LINK/V2调试下载器 x1
    • 杜邦线 若干
  2. 软件:
    • STM32CubeMX
    • Keil MDK-ARM (Keil 5)

二、 原理简述

  1. I2C协议: 一种由Philips公司开发的两线式串行总线,用于连接微控制器及其外围设备。它由SDA(串行数据线)SCL(串行时钟线) 构成,通过地址寻址方式实现主从设备间的通信。本实验中,STM32作为主设备,OLED屏作为从设备。
  2. OLED显示原理: 0.96寸OLED屏像素为128x64,每个像素点自发光,无需背光。它通过I2C接收STM32发送的命令和数据来控制每个像素点的亮灭。显示汉字或图片的本质是向OLED的GDDRAM(图形显示数据RAM)写入对应的点阵数据。
  3. U8g2库: 一个用于嵌入式设备的单色图形库,支持多种显示器控制器和单片机平台。它封装了底层驱动,提供了大量高层API(如绘制点、线、圆、字符、汉字、位图等),极大简化了图形界面开发。

三、 步骤

第一部分:硬件连接与STM32CubeMX工程创建

步骤 1:硬件连接

将OLED屏与STM32F103C8T6按照下表用杜邦线连接:

OLED屏引脚 STM32F103C8T6引脚 说明
VCC 3.3V 电源正极
GND GND 电源地
SCL PB6 I2C1时钟线
SDA PB7 I2C1数据线

步骤 2:使用STM32CubeMX创建工程并配置时钟和I2C

1.打开STM32CubeMX,点击 File -> New Project

2.在芯片选择器中,在搜索框输入 STM32F103C8,然后在下方列表中选择 STM32F103C8Tx,点击 Start Project

3.配置时钟源:

  • Pinout & Configuration 标签页下,左侧目录找到 RCC
  • High Speed Clock (HSE) 设置为 Crystal/Ceramic Resonator

在这里插入图片描述

4.配置I2C1:

  • 在左侧目录找到 I2C1
  • I2C1 Mode 设置为 I2C
  • 此时,右侧的原理图上,PB6和PB7会自动被配置为I2C1的SCL和SDA。

在这里插入图片描述

5.配置时钟树:

  • 点击上方 Clock Configuration 标签。
  • 为了获得较好的性能,我们将系统时钟配置到最高72MHz。找到HSE,输入8(因为外部晶振通常是8MHz),然后在System Clock Mux处选择PLLCLK,最后在右侧的HCLK输入72,并按下回车。软件会自动完成PLL的倍频配置。

在这里插入图片描述

6.生成工程代码:

  • 点击 Project Manager 标签。
  • Project 子标签下,设置 Project Name
  • Toolchain / IDE 设置为 MDK-ARM V5
  • Code Generator 子标签下,推荐选择 Copy all used libraries into the project folder 以便于代码管理。
  • 点击右上角的 GENERATE CODE 按钮,生成Keil工程文件。

在这里插入图片描述

第二部分:获取并移植U8g2库

步骤 3:下载U8g2库源码

  1. 访问U8g2的官方GitHub仓库:https://github.com/olikraus/u8g2
  2. 点击 Code -> Download ZIP,将整个库下载到本地并解压。
    在这里插入图片描述

在这里插入图片描述

步骤 4:将U8g2文件添加到Keil工程

  1. 打开上一步由CubeMX生成的Keil工程文件 (.uvprojx)。
  2. 在Keil的 Project 窗口中,右键点击 Source Group 1(或您的组名),选择 Add Existing Files to Group 'Source Group 1'...
  3. 在弹出的文件浏览器中,导航到您解压的U8g2库的 csrc 文件夹。这个文件夹包含了所有C源码文件。
  4. 我们需要添加以下关键文件:
    • u8x8_d_ssd1306_128x64_noname.c (可以删除除了这个驱动文件以外的其他u8x8_d_…文件)
    • u8g2_d_setup.c ( u8g2_d_ssd1306_128x64_noname_f 函数所在的文件且文件中可以只留下这个函数)
    • u8g2_d_memory.cu8g2_m_16_8_f函数所在的文件且文件中可以只留下这个函数)
    • u8g2_d_lcd.c
    • u8g2_ll_hvline.c
    • u8g2_buffer.c
    • u8g2_font.c
    • u8g2_hvline.c
    • u8g2_selection_list.c
    • u8g2_circle.c
    • u8g2_line.c
    • u8g2_polygon.c
    • u8g2_bezier.c
    • u8g2_box.c
    • u8g2_cleardisplay.c (这个文件可能不存在,如果没有,请添加 u8g2_clear_display.c)
    • u8g2_setup.c
    • u8g2_fonts.c

在这里插入图片描述

5.添加头文件路径:

  • 在Keil中,点击魔术棒按钮 Options for Target
  • 选择 C/C++ 标签。
  • Include Paths 一栏,点击末尾的 ... 按钮。
  • 点击 New Line 按钮 (文件夹带*号图标),然后添加U8g2库的 csrc 文件夹路径。
    在这里插入图片描述
    在这里插入图片描述

步骤 5:编写硬件层驱动函数 (HAL库适配)

U8g2库需要依赖您实现的底层gpio_and_delay函数。我们需要创建一个新文件来实现它。

  1. 在Keil工程中,右键 Source Group 1 -> Add New Item to Group 'Source Group 1',选择 C File (.c),命名为 u8g2_port.c。用同样的方法添加对应的头文件 u8g2_port.h

  2. u8g2_port.h 中写入以下内容:

    #ifndef U8G2_PORT_H
    #define U8G2_PORT_H
    
    #include "stm32f1xx_hal.h" // 确保包含HAL库头文件
    #include "u8g2.h"         // 包含U8g2库头文件
    
    // 声明一个外部变量,用于I2C句柄,我们将在main.c中定义它
    extern I2C_HandleTypeDef hi2c1;
    
    // 声明U8g2的初始化函数
    uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr);
    uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
    
    #endif
    
  3. u8g2_port.c 中写入以下内容:

    #include "u8g2_port.h"
    
    // GPIO和延时函数
    uint8_t u8x8_stm32_gpio_and_delay(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr)
    {
      switch (msg)
      {
      case U8X8_MSG_GPIO_AND_DELAY_INIT: // 初始化,被u8g2.begin()调用
        // 这里可以放置一些初始化的代码,但HAL库已经通过CubeMX初始化好了,通常为空。
        break;
      case U8X8_MSG_DELAY_MILLI:         // 延时毫秒命令
        HAL_Delay(arg_int);               // 调用HAL库的延时函数
        break;
      case U8X8_MSG_DELAY_10MICRO:       // 延时10微秒命令 (对于I2C通常不需要)
        for (uint16_t n = 0; n < 80; n++) // 粗略的10us延时循环,根据CPU频率调整
        {
          __NOP();
        }
        break;
      case U8X8_MSG_DELAY_100NANO:       // 延时100纳秒命令 (对于I2C通常不需要)
        __NOP(); // 执行一个空操作,大约几十纳秒
        break;
      default:
        return 0; // 消息未知,返回0表示失败
      }
      return 1; // 消息处理成功
    }
    
    // I2C字节传输函数
    uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
    {
      static uint8_t buffer[32]; // U8g2将会分块传输数据,32字节是常见的块大小
      static uint8_t buf_idx;
      uint8_t *data;
    
      switch (msg)
      {
      case U8X8_MSG_BYTE_SEND: // 发送数据,arg_ptr指向要发送的数据,arg_int是数据长度
        data = (uint8_t *)arg_ptr;
        while (arg_int > 0)
        {
          buffer[buf_idx++] = *data;
          data++;
          arg_int--;
        }
        break;
      case U8X8_MSG_BYTE_INIT: // 初始化,这里不需要做什么
        break;
      case U8X8_MSG_BYTE_SET_DC: // 设置数据/命令引脚,对于硬件I2C,这个信息通常包含在地址中,所以这里什么都不做。
        break;
      case U8X8_MSG_BYTE_START_TRANSFER: // 开始传输
        buf_idx = 0;
        break;
      case U8X8_MSG_BYTE_END_TRANSFER: // 结束传输,将缓冲区的数据通过HAL_I2C_Master_Transmit发送出去
        // 0x78 是SSD1306的I2C地址(7位地址左移一位后为0x3C << 1 = 0x78)
        if (HAL_I2C_Master_Transmit(&hi2c1, 0x78, buffer, buf_idx, HAL_MAX_DELAY) != HAL_OK)
        {
          return 0; // 传输失败
        }
        break;
      default:
        return 0;
      }
      return 1;
    }
    
  4. u8g2_port.c 添加到Keil工程的 Source Group 1 中。

第三部分:编写主程序,实现显示功能

步骤 6:修改主程序 main.c

  1. main.c/* USER CODE BEGIN Includes */ 区域,包含头文件:

    /* USER CODE BEGIN Includes */
    #include "u8g2.h"
    #include "u8g2_port.h"
    /* USER CODE END Includes */
    
  2. /* USER CODE BEGIN PV */ 区域,定义一个U8g2对象:

    /* USER CODE BEGIN PV */
    u8g2_t u8g2; // 定义一个U8g2结构体
    /* USER CODE END PV */
    
  3. /* USER CODE BEGIN 2 */ 区域,初始化U8g2库:

    /* USER CODE BEGIN 2 */
    // 初始化U8g2库,配置为SSD1306驱动,128x64分辨率,硬件I2C
    u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_stm32_gpio_and_delay);
    u8g2_InitDisplay(&u8g2); // 发送初始化序列到显示器
    u8g2_SetPowerSave(&u8g2, 0); // 唤醒显示器
    u8g2_ClearBuffer(&u8g2);     // 清除内部缓冲区
    
    // 显示欢迎信息或测试图形
    u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr); // 设置字体
    u8g2_DrawStr(&u8g2, 0, 20, "U8g2 Init OK!"); // 在坐标(0,20)绘制字符串
    u8g2_SendBuffer(&u8g2); // 将缓冲区内容发送到显示器
    
    HAL_Delay(2000); // 延时2秒
    /* USER CODE END 2 */
    
  4. /* USER CODE BEGIN 3 */ ,实现显示。

    1:显示qq和id

    /* USER CODE BEGIN 3 */
    u8g2_ClearBuffer(&u8g2); // 清除缓冲区
    u8g2_SetFont(&u8g2, u8g2_font_unifont_t_chinese2);
    u8g2_DrawUTF8(&u8g2, 0, 20, "QQ:361***717");
    u8g2_DrawUTF8(&u8g2, 0, 40, "ID:CYUE18");
    u8g2_SendBuffer(&u8g2);
    HAL_Delay(3000); // 显示3秒
    

    2:上下滑动显示 (使用第一页缓冲技术)

    //2:上下滑动显示
    u8g2_SetFont(&u8g2, u8g2_font_6x10_tf);
    for (int pos = 64; pos >= -64; pos--) { // 从下往上滑动
      u8g2_ClearBuffer(&u8g2);
      u8g2_DrawStr(&u8g2, 0, pos, "QQ:3614789717");
      u8g2_DrawStr(&u8g2, 0, pos+20, "Line 2 of the text.");
      u8g2_SendBuffer(&u8g2);
      HAL_Delay(50); // 控制滑动速度
    }
    HAL_Delay(1000);
    

    任务 3:显示一个简单图案并添加动态效果 (这里是移动的小球)

    // 3:显示动态图案
    int ball_x = 20;
    int ball_y = 20;
    int ball_vx = 2;
    int ball_vy = 2;
    int ball_radius = 5;
    
    for (int i = 0; i < 150; i++) {
      u8g2_ClearBuffer(&u8g2);
    
      // 绘制边框
      u8g2_DrawFrame(&u8g2, 0, 0, 128, 64);
      
      // 绘制背景网格线(可选)
      for (int x = 20; x < 128; x += 20) {
        u8g2_DrawVLine(&u8g2, x, 0, 64);
      }
      for (int y = 16; y < 64; y += 16) {
        u8g2_DrawHLine(&u8g2, 0, y, 128);
      }
      
      // 绘制移动的小球
      u8g2_DrawDisc(&u8g2, ball_x, ball_y, ball_radius, U8G2_DRAW_ALL);
      
      // 在小球中心画个点,方便观察
      u8g2_DrawPixel(&u8g2, ball_x, ball_y);
    
      // 显示简单的帧数信息(不使用snprintf)
      u8g2_SetFont(&u8g2, u8g2_font_5x7_tf);
      
      // 方法1:直接绘制数字(简单可靠)
      u8g2_DrawStr(&u8g2, 100, 10, "F:");
      u8g2_SetCursor(&u8g2, 110, 10);
      u8g2_PrintU8(&u8g2, i % 100);  // U8g2自带的数字打印函数
      
      // 方法2:如果需要显示位置,使用简单条件显示
      if (ball_vx > 0) {
        u8g2_DrawStr(&u8g2, 90, 20, "Right");
      } else {
        u8g2_DrawStr(&u8g2, 90, 20, "Left");
      }
      
      if (ball_vy > 0) {
        u8g2_DrawStr(&u8g2, 90, 30, "Down");
      } else {
        u8g2_DrawStr(&u8g2, 90, 30, "Up");
      }
    
      // 更新小球位置
      ball_x += ball_vx;
      ball_y += ball_vy;
    
      // 改进的边界碰撞检测(考虑小球半径)
      if (ball_x <= ball_radius) {
        ball_vx = -ball_vx;
        ball_x = ball_radius; // 防止卡在左边界
      }
      if (ball_x >= 128 - ball_radius) {
        ball_vx = -ball_vx;
        ball_x = 128 - ball_radius; // 防止卡在右边界
      }
      
      if (ball_y <= ball_radius) {
        ball_vy = -ball_vy;
        ball_y = ball_radius; // 防止卡在上边界
      }
      if (ball_y >= 64 - ball_radius) {
        ball_vy = -ball_vy;
        ball_y = 64 - ball_radius; // 防止卡在下边界
      }
    
      u8g2_SendBuffer(&u8g2);
      HAL_Delay(35); // 控制动画帧率
    }
    

步骤 7:编译与下载

  1. 在Keil中,点击 Rebuild (F7) 按钮编译整个工程。确保0 Errors, 0 Warnings。

  2. 将ST-LINK通过USB连接电脑,并与STM32板子正确连接。

  3. 点击 Load (F8) 按钮将程序下载到STM32中。

  4. 下载完成后,系统会自动复位运行。观察OLED屏幕的显示效果。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1. I2C波形分析

如果使用Keil的逻辑分析仪采集I2C的SDA波形,应该能看到标准的I2C通信时序:

  • 起始条件:SCL高电平时SDA由高变低
  • 设备地址:7位从设备地址(0x3C) + 读写位(0)
  • 数据帧:多个8位数据字节
  • 停止条件:SCL高电平时SDA由低变高
2. 替代分析方法

如果无法采集真实I2C信号,可以采用以下方法:

  • 软件模拟I2C:使用GPIO模拟I2C时序,便于调试和观测
  • 串口打印调试:在关键代码位置添加串口输出,分析程序执行流程
  • 逻辑分析仪:使用外部逻辑分析仪捕获和分析I2C信号
  • 示波器观测:使用数字示波器观察SDA和SCL的波形
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐