移植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 是由 SSD1306 OLED 驱动芯片进行驱动点亮,使用 SPI 接口进行通信。
0.96 OLED 与 STM32 的引脚连接原理图:
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 工程文件,双击,打开工程。配置 PB13、PB14、PB15、PC13 为 GPIO_Output 模式,并分别命名为 OLED_RST、OLED_SDA、OLED_SCL、OLED_DC。
配置完毕,点击生成代码按钮,重新生成代码。
相关链接:u8g2
先从上面 u8g2 链接,打开 Github,下载 u8g2 的代码压缩包。你可以把压缩包保持到桌面。
对压缩包解压,可以看到 u8g2-master 文件夹里有好多文件和文件夹。
我们在移植过程总主要用的是 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 )
使用 MDK-ARM 打开工程,把复制到 Src 文件夹中的源文件都加入到工程的 Application/User 文件夹中去。
在 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,并在 MDK 左侧栏双击 User 栏,将该文件加进工程中。
然后在再加入以下代码,即创建一个回调函数(可根据相关的宏来知其意,比如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_memory.c 只保留 *u8g2_m_16_8_f 函数,其他全部删掉,如下图所示。
在 main.h 头文件加入 oled.h 头文件。
在主函数中初始化 u8g2,先创建 u8g2 的句柄,并创建一个临时变量 nTemp:
/* USER CODE BEGIN 1 */
u8g2_t u8g2;
int nTemp;
/* USER CODE END 1 */
修改 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);//初始化显示器
u8g2_SetPowerSave(&u8g2,0);//唤醒显示器
在主循环中添加以下代码:
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);//绘制缓冲区的内容
重新编译,下载,可以看到三个循环变化圆环不断地变化。
如果我们想显示数据,比如显示小车的实时角度,那该怎么办呢?
其实也并不困难。我们先在初始化部分对字体设置函数 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 cStrAngle[3]、 char cStrDistance[6]、char cStrBattery[3]
。
/* USER CODE BEGIN 1 */
u8g2_t u8g2;
char cStrAngle[3];//储存小车倾角
char cStrDistance[6];//储存超声波探测距离
char cStrBattery[3];//储存电池电压
/* USER CODE END 1 */
再写好功能部分代码。
u8g2_ClearBuffer(&u8g2);//清空缓冲区的内容
u8g2_DrawStr(&u8g2,0,15,"Angle:");//输出固定不变的字符串Angle,显示小车实时车体倾角
u8g2_DrawStr(&u8g2,0,30,"Distance:");//输出固定不变的字符串Distance,显示超声波探测距离
u8g2_DrawStr(&u8g2,0,45,"Battery:");//输出固定不变的字符串Battery ,显示小车实时电压
sprintf(cStrAngle,"%5.1f",g_fCarAngle);//将角度数据格式化输出到字符串
u8g2_DrawStr(&u8g2,50,15,cStrAngle);//输出实时变化的角度数据
sprintf(cStrDistance,"%5d",Distance);//将距离数据格式化输出到字符串
u8g2_DrawStr(&u8g2,50,30,cStrDistance);//输出实时变化的角度数据
fTemp = GetBatVoltage();//获取电池电压
sprintf(cStrBattery,"%5.1f",fTemp);//将距离数据格式化输出到字符串
u8g2_DrawStr(&u8g2,50,45,cStrBattery);//输出实时变化的电压数据
u8g2_SendBuffer(&u8g2);//绘制缓冲区的内容
值得注意的是,我们要先使用标准库函数 sprintf()
对角度、距离、电压数据(原来的格式为浮点型、INT型)格式化输出为字符串,再使用函数 u8g2_DrawStr()
显示到 OLED 显示器上。