移植u8g2单色图形库驱动OLED

本小节教你将 u8g2 单色图形库移植到 STM32 单片机上,用于驱动 0.96 OLED 液晶屏显示模块。

大型的 GUI 解决方案并不适合像 0.96 OLED(128x64 像素,基于 SSD1306)这种资源紧缺型的显示模组使用,而网络上随处可见的代码资源都只是简单地实现一个字符输出功能,达不到预期的目的。幸好,在一个月黑风高的晚上,我在查阅资料中无意中发现了 u8g2,通过学习后发现该显示库支持很多种字体 fonts(英文和数字),而且具有完整的驱动函数库(直线、圆形、斜线、字符旋转镜像反白、bitmap 一应俱全)和丰富的演示 Demo,特别适合应用在嵌入式 MCU 上面。于是,我把它移植到了 STM32 上面,并写下了本章节作为记录。

u8g2是什么

u8g2 是目前在 Arduino 平台上使用最广泛的单色屏驱动库,在 Github 上超过 1.3K Star,1800 Commit。

u8g2的优势

为什么要使用 u8g2 库?也就是说,u8g2 库能带给我们的开发带来什么便利:

  • u8g2 库支持市面上的大部分 OLED 液晶屏显示模块;
  • u8g2 库功能非常丰富,而且编写有完整的驱动函数库,直接调用即可。
  • u8g2 库移植简单,容易使用。

0.96 OLED

0.96 OLED
Image 5.19.1 - 0.96 OLED

我们使用的 0.96 OLED 是由 SSD1306 OLED 驱动芯片进行驱动点亮,使用 SPI 接口进行通信。

0.96 OLED 与 STM32 的引脚连接原理图:

引脚连接原理图
Image 5.19.2 - 引脚连接原理图
0.96 OLED 引脚 功能描述 对应 STM32 引脚
GND 电源地 GND
VCC 3.3V 3.3V
SCL CLK时钟 PB15
SDA MOSI数据 PB14
RST 复位 PB13
D/C 数据/命令 PC13

需要注意的是,STM32 跟 0.96 OLED 的通信是只需要 STM32 发送指令和数据到 OLED,OLED 并不需要反馈任何数据给 STM32。因此,你可以看到 OLED 模块是只有 MOSI(SDA)引脚,没有 MISO 引脚的。

坐标系

OLED 其实就是一个 M * N 的像素点阵,想显示什么就得把具体位置的像素点亮起来。

坐标系如下图所示,左上角是原点,向右是 X 轴,向下是 Y 轴。

0.96 OLED 的坐标系要牢记清楚。

移植步骤

这里没有使用 STM32 的硬件 SPI 接口,而是使用 GPIO 口 模拟 SPI 进行通信。

进入我们上一小节实验的 MiaowLabs-Demo 文件夹,找到 MiaowLabs-Demo.ioc 工程文件,双击,打开工程。

配置相关引脚
Image 5.19.3 - 配置相关引脚

配置完毕,点击生成代码按钮,重新生成代码。

相关链接:u8g2

先从上面相关链接,打开 Github,下载 u8g2 的代码压缩包。你可以把压缩包保持到桌面。

下载u8g2代码
Image 5.19.4 - 下载u8g2代码

对压缩包解压,可以看到 u8g2-master 文件夹里有好多文件和文件夹。

解压
Image 5.19.5 - 解压

我们在移植过程总主要用的是 csrc 文件夹里面的文件,其他文件夹和文件可以等有空再去探究。

在 csrc 文件夹里,点击按类型排序,可以看到有两个 .h 头文件(u8g2.h 和 u8x8.h),其他的都是 .c 源文件。其中,u8x8_d_xxx.c 之类的文件是对应屏幕驱动的文件,我们在这里使用 u8x8_d_ssd1306_128x64_noname.c。 复制 csrc 文件夹里面的两个 .h 头文件到小车项目工程里的 Inc 文件夹,复制其他的所有文件到 Src 文件夹(注意, u8x8_d_xxx.c 这类文件只保留 u8x8_d_ssd1306_128x64_noname.c)。

其中 u8x8_d_ssd1306_128x64_noname.c 是随 oled 屏的驱动芯片来选择的(这里 OLED 的驱动芯片是 ssd1306 )

复制头文件到Inc文件夹中
Image 5.19.6 - 复制头文件到Inc文件夹中
复制源文件到Src文件夹中
Image 5.19.7 - 复制源文件到Src文件夹中

使用 MDK-ARM 打开工程,把复制到 Src 文件夹中的源文件都加入到工程的 Application/User 文件夹中去。

把相关文件全部加入到MDK-ARM工程中
Image 5.19.8 - 把相关文件全部加入到MDK-ARM工程中

在 main.c 中添加两个 .h 头文件:

在main.c中添加h头文件
Image 5.19.9 - 在main.c中添加h头文件

新建一个文件,保存到 Inc 文件夹,命名为 oled.h,敲入以下代码:

#ifndef __OLED_H_
#define __OLED_H_

#include "u8g2.h"
#include "u8x8.h"

void OLED_Init(void);
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);

#endif

新建一个文件,保存到 Src 文件夹,命名为 oled.c。

然后在再加入以下代码,即创建一个回调函数(可根据相关的宏来知其意,比如U8X8_MSG_GPIO_SPI_DATA就是表示软件模拟spi的数据管脚,那个arg_int表示是将当前管脚置高还是复位,其中的OLED_Init()是OLED初始化函数):


#include "gpio.h"
#include "oled.h"


#define OLED_RST_Clr() HAL_GPIO_WritePin(GPIOB,OLED_RST_Pin,GPIO_PIN_RESET)
#define OLED_RST_Set() HAL_GPIO_WritePin(GPIOB,OLED_RST_Pin,GPIO_PIN_SET)

#define OLED_DC_Clr() HAL_GPIO_WritePin(GPIOC,OLED_DC_Pin,GPIO_PIN_RESET)
#define OLED_DC_Set() HAL_GPIO_WritePin(GPIOC,OLED_DC_Pin,GPIO_PIN_SET)

#define OLED_SCLK_Clr() HAL_GPIO_WritePin(GPIOB,OLED_SCL_Pin,GPIO_PIN_RESET)
#define OLED_SCLK_Set() HAL_GPIO_WritePin(GPIOB,OLED_SCL_Pin,GPIO_PIN_SET)

#define OLED_SDIN_Clr() HAL_GPIO_WritePin(GPIOB,OLED_SDA_Pin,GPIO_PIN_RESET)
#define OLED_SDIN_Set() HAL_GPIO_WritePin(GPIOB,OLED_SDA_Pin,GPIO_PIN_SET)

void OLED_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIOB/GPIOC clock enable */
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE(); 

  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(OLED_DC_GPIO_Port, OLED_DC_Pin, GPIO_PIN_RESET);
  /*Configure GPIO pin Output Level */
  HAL_GPIO_WritePin(GPIOB, LED_Pin|OLED_RST_Pin|OLED_SDA_Pin|OLED_SCL_Pin, GPIO_PIN_RESET);
  /*Configure GPIO pin : PtPin */
  GPIO_InitStruct.Pin = OLED_DC_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(OLED_DC_GPIO_Port, &GPIO_InitStruct);
  /*Configure GPIO pins : PBPin PBPin PBPin PBPin 
                           PBPin */
  GPIO_InitStruct.Pin = OLED_RST_Pin|OLED_SDA_Pin|OLED_SCL_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);    

}

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:
            OLED_Init();                    
        break;
        case U8X8_MSG_GPIO_SPI_DATA:
            if(arg_int)OLED_SDIN_Set();
            else OLED_SDIN_Clr();
        break;
        case U8X8_MSG_GPIO_SPI_CLOCK:
            if(arg_int)OLED_SCLK_Set();
            else OLED_SCLK_Clr();
        break;        
        case U8X8_MSG_GPIO_CS:
            //CS默认接地
        case U8X8_MSG_GPIO_DC:
            if(arg_int)OLED_DC_Set();
            else OLED_DC_Clr();
        break;
        case U8X8_MSG_GPIO_RESET:
            if(arg_int)OLED_RST_Set();
            else OLED_RST_Clr();
        break;
        //Function which delays 100ns  
        case U8X8_MSG_DELAY_100NANO:  
            __NOP();  
        break;  
        case U8X8_MSG_DELAY_MILLI:
            HAL_Delay(arg_int);
        break;
        default:
            return 0;//A message was received which is not implemented, return 0 to indicate an error
  }
  return 1;
}

裁剪 u8g2_d_setup.c u8g2_d_memory.c 文件中与 ssd1306 无关的代码,减小代码体积,ram用量。

u8g2_d_setup.c 只保留 u8g2_Setup_ssd1306_128x64_noname_f 函数,其他全部删掉。

裁剪u8g2_d_setup.c
Image 5.19.10 - 裁剪u8g2_d_setup.c

u8g2_d_memory.c 只保留 *u8g2_m_16_8_f 函数,其他全部删掉。

裁剪u8g2_d_memory.c
Image 5.19.11 - 裁剪u8g2_d_memory.c

在 main.h 头文件加入 oled.h 头文件。

加入oled.h头文件
Image 5.19.12 - 加入oled.h头文件

在主函数中初始化 u8g2,先创建 u8g2 的句柄,并创建一个临时变量 nTemp:

创建 u8g2 句柄
Image 5.19.13 - 创建 u8g2 句柄

修改 main() 函数,初始化 u8g2 显示库,增加显示代码。

u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,u8g2_R0,u8x8_byte_4wire_sw_spi,u8x8_stm32_gpio_and_delay);//初始化u8g2
  u8g2_InitDisplay(&u8g2);//初始zai化显示器
  u8g2_SetPowerSave(&u8g2,0);//唤醒显示器
初始化u8g2
Image 5.19.14 - 初始化u8g2

在主循环中添加以下代码:

u8g2_ClearBuffer(&u8g2);//清空缓冲区的内容
       if(++nTemp>=32) nTemp=1;
      u8g2_DrawCircle(&u8g2,64,32,nTemp,u8g2_DRAW_ALL);//画圆
      u8g2_DrawCircle(&u8g2,32,32,nTemp,u8g2_DRAW_ALL);//画圆
      u8g2_DrawCircle(&u8g2,96,32,nTemp,u8g2_DRAW_ALL);//画圆
      u8g2_SendBuffer(&u8g2);//绘制缓冲区的内容
修改主循环代码
Image 5.19.15 - 修改主循环代码

重新编译,下载,可以看到三个循环变化圆环不断地变化。

OLED显示三个圆环
Image 5.19.16 - OLED显示三个圆环

如果我们想显示数据,比如显示小车的实时角度,那该怎么办呢?

其实也并不困难。我们先在初始化部分对字体设置函数 u8g2_SetFont 进行初始化。

u8g2_Setup_ssd1306_128x64_noname_f(&u8g2,u8g2_R0,u8x8_byte_4wire_sw_spi,u8x8_stm32_gpio_and_delay);//初始化u8g2
u8g2_InitDisplay(&u8g2);//初始zai化显示器
u8g2_SetPowerSave(&u8g2,0);//唤醒显示器
u8g2_SetFont(&u8g2,u8g2_font_6x12_mr);//设置英文字体

然后,声明一个数组 char cStr[3];

/* USER CODE BEGIN 1 */
u8g2_t u8g2;
char cStr[3];
/* USER CODE END 1 */

再写好功能部分代码。

u8g2_ClearBuffer(&u8g2);//清空缓冲区的内容
u8g2_DrawStr(&u8g2,0,30,"Angle:");//输出固定不变的字符串Angle:
sprintf(cStr,"%5.1f",g_fCarAngle);//将角度数据格式化输出到字符串
u8g2_DrawStr(&u8g2,50,30,cStr);//输出实时变化的角度数据
u8g2_SendBuffer(&u8g2);//绘制缓冲区的内容

值得注意的是,我们要先使用标准库函数 sprintf() 对角度数据(原来的格式为浮点型)格式化输出为字符串,再使用函数 u8g2_DrawStr() 显示到 OLED 显示器上。