移植准备(实验使用的是GD32)

  1. STM32F407VET6模板工程(Keil)
  2. u8g2源码
  3. STM32f407vet6开发板+4线IIC驱动的0.96寸oled屏

U8G2源码下载

gitee仓库
github仓库

  1. git工具克隆仓库:
1
2
3
git clone https://gitee.com/penjun71/u8g2.git  #gitee

git clone https://github.com/olikraus/u8g2.git #github
  1. 通过仓库下载压缩包。
    文件目录:

U8G2移植

1、将源码加入模板工程(KEIL)

(1)将文件夹中csrc文件夹复制至工程中,改名为u8g2

(2)剔除多余驱动文件:u8x8_d_xxxx.c 仅保留 u8x8_d_ssd1306_128x64_noname.c

(3)创建 Group u8g2,并将剔除后文件夹中C文件全部加入此组中

(4)将u8g2文件夹加入头文件搜索路径

(5)勾选Keil-option C99 支持(由于库中包含有未在函数开始定义的所以需要打开C99支持,如果不打开,编译后对应的错误点可以自行修改为函数开头定义变量。)(注意:这个很重要-其他文章没说明,一直报错,到这里才发现讲得详细些!)

2、编写驱动接口

补充:可以使用后边提供的方法(快捷键 ctrl+A 全选以后,直接把全部文件注释了),先全部注释,然后取消注释头文件,并且找到你屏幕驱动对应的那个函数取消注释!

(1)修改u8g2_d_setup.c仅仅保留函数(对应自己驱动以及ram空间保留自己对应的启动函数)

1
void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb)

(2)修改u8g2_d_memery.c仅保留

1
uint8_t *u8g2_m_16_8_f(uint8_t *page_cnt)

(3)编写启动所需的回调函数(即u8g2_d_setup.c中保留的函数的最后一个参数)并调用启动函数

1
2
3
4
5
void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2,
const u8g2_cb_t *rotation,
u8x8_msg_cb byte_cb,
u8x8_msg_cb gpio_and_delay_cb)

u8g2_t *u8g2:u8g2 句柄结构体指针.

const u8g2_cb_t *rotation:定义屏幕方向.

1
2
3
4
5
6
7
//u8g2_cb_t *rotation可选参数↓
#define U8G2_R0 (&u8g2_cb_r0)
#define U8G2_R1 (&u8g2_cb_r1)
#define U8G2_R2 (&u8g2_cb_r2)
#define U8G2_R3 (&u8g2_cb_r3)
#define U8G2_MIRROR (&u8g2_cb_mirror)
#define U8G2_MIRROR_VERTICAL (&u8g2_cb_mirror_vertical)

u8x8_msg_cb byte_cb:确定读写接口.

1
//可选函数位于u8g2_byte.c

u8x8_msg_cb gpio_and_delay_cb:对接IO初始化、IO读写、延时函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//u8x8_msg_cb 是一个函数指针编写此函数的状态:
uint8_t u8g2_gpio_and_delay_stm32_soft_iic(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr)
{
switch(msg){
//初始化GPIO
case U8X8_MSG_GPIO_AND_DELAY_INIT: //此状态下初始化IO以及延时函数
//本人用HAL库进行配置未使用到这里配置io与延时
break;
case U8X8_MSG_DELAY_MILLI: //此状态下对接ms延时函数,延时arg_int ms
bsp_delay_ms(arg_int);
break;
case U8X8_MSG_DELAY_10MICRO: //此状态下对接10usus延时函数
bsp_delay_us(10);
break;
case U8X8_MSG_DELAY_100NANO: //此状态下对接100ns延时函数
__NOP();
break;
case U8X8_MSG_DELAY_I2C: //此状态下对接IIC延时函数
bsp_delay_us(1);
break;
case U8X8_MSG_GPIO_I2C_DATA: //此状态下修改IIC SDA信号线电平
if (arg_int) HAL_GPIO_WritePin(LCD_SDA_GPIO_Port, LCD_SDA_Pin, GPIO_PIN_SET);
else HAL_GPIO_WritePin(LCD_SDA_GPIO_Port, LCD_SDA_Pin, GPIO_PIN_RESET);
break;
case U8X8_MSG_GPIO_I2C_CLOCK: //此状态下修改IIC SCL信号线电平
if (arg_int) HAL_GPIO_WritePin(LCD_SCL_GPIO_Port, LCD_SCL_Pin, GPIO_PIN_SET);
else HAL_GPIO_WritePin(LCD_SCL_GPIO_Port, LCD_SCL_Pin, GPIO_PIN_RESET);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}

return 1; // command processed successfully.
}

补充:这里主要是设置IIC脚位的高低电平跟他有点不同,以下是使用于自己的脚位模式-zmlong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// IIC_SCL 
#define SCL_GPIOClock RCC_APB2Periph_GPIOB
#define SCL_GPIO GPIOB
#define SCL_GPIO_Pin GPIO_Pin_4
//IIC_SDA
#define SDA_GPIOClock RCC_APB2Periph_GPIOB
#define SDA_GPIO GPIOB
#define SDA_GPIO_Pin GPIO_Pin_5

...
if (arg_int) GPIO_SetBits(SDA_GPIO,SDA_GPIO_Pin);
else GPIO_ResetBits(SDA_GPIO,SDA_GPIO_Pin);

break;
case U8X8_MSG_GPIO_I2C_CLOCK: //此状态下修改IIC SCL信号线电平
// if (arg_int) HAL_GPIO_WritePin(LCD_SCL_GPIO_Port, LCD_SCL_Pin, GPIO_PIN_SET);
// else HAL_GPIO_WritePin(LCD_SCL_GPIO_Port, LCD_SCL_Pin, GPIO_PIN_RESET);

if (arg_int) GPIO_SetBits(SCL_GPIO,SCL_GPIO_Pin);
else GPIO_ResetBits(SCL_GPIO,SCL_GPIO_Pin);

break;
...

3、u8g2初始化

完成后调用函数进行启动:

1
2
3
4
5
6
7
8
9
u8g2_t u8g2;
//初始化u8g2
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8g2_gpio_and_delay_stm32_soft_iic);
//屏幕初始化
u8g2_InitDisplay(&u8g2);
//开启电源
u8g2_SetPowerSave(&u8g2, 0);
//清除显存内容
u8g2_ClearBuffer(&u8g2);

4、编写测试程序 和 结果展示

1
2
3
u8g2_DrawCircle(&u8g2,64,32,10,U8G2_DRAW_ALL);  //在62,32位置画一个半径为10的圆
u8g2_SendBuffer(&u8g2);//发送显存到屏幕上

  1. 测试2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    while(1) 
    {
    LED=~LED;
    // delay_ms(100);
    // u8g2_DrawCircle(&u8g2,64,32,10,U8G2_DRAW_ALL); //在62,32位置画一个半径为10的圆
    // u8g2_SendBuffer(&u8g2);//发送显存到屏幕上
    delay_ms(500);
    // u8g2_ClearBuffer(&u8g2);

    u8g2_ClearBuffer(&u8g2);
    if(++t >= 32) t = 1;
    u8g2_DrawCircle(&u8g2,64,32,t,U8G2_DRAW_ALL);
    u8g2_DrawCircle(&u8g2,32,32,t,U8G2_DRAW_ALL);
    u8g2_DrawCircle(&u8g2,96,32,t,U8G2_DRAW_ALL);
    u8g2_SendBuffer(&u8g2);
    }

以下为其他移植补充参考

上面的方法其实已经很完美契合模拟IIC驱动OLED实现U8G2图形库驱动显示啦,后边是收集补充部分,大部分使用的是SPI方式,效果应该好一些,以后有时间可以研究硬件IIC跟DMA方式。

一、stm32移植U8G2图像库指南

U8g2:用于单色显示的库,版本 2

U8g2 是一个用于嵌入式设备的单色图形库。U8g2支持单色OLED和LCD,包括以下控制器:SSD1305, SSD1306, SSD1309, SSD1316, SSD1320, SSD1322, SSD1325, SSD1327, SSD1329, SSD1606, SSD1607,895, SH11607,84D1607,SH11607,84D1607,94D1607,94D1607,84D1607,4D1607,4D1607,4D1607,84D1807 ,PCF8812,HX1230,UC1601,UC1604,UC1608,UC1610,UC1611,UC1617,UC1638,UC1701,ST7511,ST7528,ST7565,ST7567,ST7571,ST7586,ST7588,ST75256,ST75320,NT7534,ST7920,IST3020,IST7920,LD7032,KS0108 、KS0713、HD44102、T7932、SED1520、SBN1661、IL3820、MAX7219(完整列表见此处)。

Arduino 库 U8g2 可以从 Arduino IDE 的库管理器安装。U8g2 还包括 U8x8 库:

  1. U8g2:
  • 包括所有图形程序(线/框/圆绘制)。
  • 支持多种字体。(几乎)对字体高度没有限制。
  • 需要微控制器中的一些内存来呈现显示。
  1. U8x8:
  • 仅文本输出(字符)设备。
  • 仅允许适合 8x8 像素网格的字体。
  • 直接写入显示器。微控制器中不需要缓冲器。

移植准备工作

移植U8G2图像库需要准备好,U8G2的源码
U8g2下载地址: https://github.com/olikraus/u8g2
准备一份KEIL工程,我这里使用了一份我自己写的时间片轮询程序(唯一的要求,带屏幕的初始化就行)

开始移植文件

U8G2源码中,这个csrc是c语言版的源码,直接把他复制到keil的工程里。

我将csrc文件复制到keil工程中的,HARDWARE文件夹内,改名为OLED_U8G2。(名字和位置,可以根据实际情况更改)

修改keil配置

添加一个分组,把U8G2的文件全部添加进去(注:可以先在外面整理好后再添加好一些)

但是,里面有好多u8g2_d_xxx.c格式的文件,里面有三个文件的必要的。其中一个需要根据你OLED屏幕的驱动芯片和像素(如这里选择的文件,就是ssd1306,这个驱动芯片。128*64的像素)

还有这两个文件也是必要的,除了这三个文件以后的u8g2_d_xxx的文件全部删除。

这四个文件也删除了

然后,添加一下环境配置中的c/c++,让keil可以找到这些文件。

修改U8G2源码

重点就是修改,u8g2_d_memory.c 与 u8g2_d_setup.c 这两个文件

u8g2_d_setup.c修改

快捷键 ctrl+A 全选以后,直接把全部文件注释了

然后,取消注释头文件,并且找到你屏幕驱动对应的那个函数取消注释

这里找到了,跟我ssd1306 128*64对应的函数,取消注释。最后一个英文字母f代表的是内存。就是一次传输多少字节的数据。f为1024个字节,2为256个字节,1为128个字节。这个主要是根据单片机的性能来选择的。比如是stmf103c8t6这种小容量的芯片,就选择后缀为1的函数。
u8g2_d_memory.c修改

同样的操作,ctrl+A把全部文件注释了以后,取消对头文件的注释。

然后,在u8g2_d_memory.c文件中,查找下图红框框里的函数,把对应的函数,取消注释。

取消注释

注意

  • 上面的所有操作,需要根据屏幕的驱动芯片
  • f 代表的是一次刷新屏幕的字节数(根据芯片选择 f 、 2 、1)

U8G2接口程序

u8g2_Setup_ssd1306_128x64_noname_f就是刚刚选择的屏幕驱动函数。
下面两句就是开打屏幕的显示。

1
2
3
4
u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_4wire_sw_spi, u8x8_stm32_gpio_and_delay);  
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2,0);

重点是u8x8_stm32_gpio_and_delay这个函数,这个回调函数,需要由我们使用者来写。他主要就是给U8G2提供的延时函数和通信接口。
这里使用的是4线的spi通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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_DELAY_100NANO: // delay arg_int * 100 nano seconds
__NOP();
break;
case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
for (uint16_t n = 0; n < 320; n++)
{
__NOP();
}
break;
case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
delay_ms(1);
break;
case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
delay_us(5);
break; // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
case U8X8_MSG_GPIO_SPI_DATA:
if(arg_int == 1) // arg_int=1: Input dir with pullup high for I2C clock pin
GPIO_SetBits(GPIOA,GPIO_Pin_1);
else if(arg_int == 0)
GPIO_ResetBits(GPIOA,GPIO_Pin_1);
break;
case U8X8_MSG_GPIO_SPI_CLOCK:
if(arg_int == 1) // arg_int=1: Input dir with pullup high for I2C clock pin
GPIO_SetBits(GPIOA,GPIO_Pin_0);
else if(arg_int == 0)
GPIO_ResetBits(GPIOA,GPIO_Pin_0);
break;
case U8X8_MSG_GPIO_CS:
if(arg_int == 1) // arg_int=1: Input dir with pullup high for I2C clock pin
GPIO_SetBits(GPIOA,GPIO_Pin_4);
else if(arg_int == 0)
GPIO_ResetBits(GPIOA,GPIO_Pin_4);
break;
case U8X8_MSG_GPIO_DC:
if(arg_int == 1) // arg_int=1: Input dir with pullup high for I2C clock pin
GPIO_SetBits(GPIOA,GPIO_Pin_3);
else if(arg_int == 0)
GPIO_ResetBits(GPIOA,GPIO_Pin_3);
break;
case U8X8_MSG_GPIO_RESET:
if(arg_int == 1) // arg_int=1: Input dir with pullup high for I2C clock pin
GPIO_SetBits(GPIOA,GPIO_Pin_2);
else if(arg_int == 0)
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
break;
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}
return 1;
}

最终效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int t = 0;
u8g2_t u8g2;
delay_init(168);
LED_Init(); //初始化LED
OLED_Init(); //初始化OLED
LED0=0;
u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_4wire_sw_spi, u8x8_stm32_gpio_and_delay);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2,0);
while(1)
{
LED0=~LED0;
delay_ms(100);
u8g2_ClearBuffer(&u8g2);
if(++t >= 32) t = 1;
u8g2_DrawCircle(&u8g2,64,32,t,U8G2_DRAW_ALL);
u8g2_DrawCircle(&u8g2,32,32,t,U8G2_DRAW_ALL);
u8g2_DrawCircle(&u8g2,96,32,t,U8G2_DRAW_ALL);
u8g2_SendBuffer(&u8g2);
}

在这里插入图片描述

硬件IIC

前面的准备工作一致

硬件 I2C 效率上比软件 I2C 快了非常多,因此特别适合 U8g2 这种大型 UI 框架。下面基于标准库介绍硬件 I2C 的移植方式。

如果使用硬件 I2C ,需要在调用该函数(或类似函数)时,使用自己的硬件读写函数:

1
void u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2_t *u8g2, const u8g2_cb_t *rotation, u8x8_msg_cb byte_cb, u8x8_msg_cb gpio_and_delay_cb);

首先还是需要编写一个 gpio_and_delay() 回调函数。不过由于这里是使用硬件 I2C ,因此不再需要提供 GPIO 和时序操作的支持,只需要提供一个毫秒级的延时即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
uint8_t u8x8_gpio_and_delay_hw(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
switch (msg) {
case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
break;
case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
break;
case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
Delay_ms(1);
break;
case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
break; // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
break; // arg_int=1: Input dir with pullup high for I2C clock pin
case U8X8_MSG_GPIO_I2C_DATA: // arg_int=0: Output low at I2C data pin
break; // arg_int=1: Input dir with pullup high for I2C data pin
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}
return 1;
}

如果是使用硬件 I2C ,那么需要自行编写硬件驱动函数,向 OLED 写入字节。这个函数的编写可以参考官方提供的软件驱动函数 u8x8_byte_sw_i2c() ,一个编写示例为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
uint8_t* data = (uint8_t*) arg_ptr;
switch(msg) {
case U8X8_MSG_BYTE_SEND:
while( arg_int-- > 0 ) {
I2C_SendData(I2C1, *data++);
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
continue;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
I2C_InitTypeDef I2C_InitStructure = {
.I2C_Mode = I2C_Mode_I2C,
.I2C_DutyCycle = I2C_DutyCycle_2,
.I2C_OwnAddress1 = 0x10,
.I2C_Ack = I2C_Ack_Enable,
.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit,
.I2C_ClockSpeed = 400000
};
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
break;
case U8X8_MSG_BYTE_SET_DC:
/* ignored for i2c */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
continue;
I2C_Send7bitAddress(I2C1, 0x78, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
continue;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
I2C_GenerateSTOP(I2C1, ENABLE);
break;
default:
return 0;
}
return 1;
}

从各个 case 标签可以很明白地看出一个 I2C 的读写过程:U8X8_MSG_BYTE_INIT 标签下需要初始化 I2C 外设,U8X8_MSG_BYTE_START_TRANSFER 标签产生起始信号并发出目标地址,U8X8_MSG_BYTE_SEND 标签开始发送字节,并且发送的字节存储在 *arg_ptr 参数中,arg_int 是字节的总长度( U8g2 库似乎一次不会传输多余 32 字节的信息)。最后,U8X8_MSG_BYTE_END_TRANSFER 标签处产生停止信号。

  • 注意在使用硬件 I2C 时,GPIO 需要设置为复用开漏输出模式 GPIO_Mode_AF_OD

最后一步,用以上编写的硬件函数初始化 U8g2 驱动:

1
u8g2_Setup_ssd1306_i2c_128x64_noname_f(u8g2, U8G2_R0, u8x8_byte_hw_i2c, u8x8_gpio_and_delay_hw);

u8x8_byte_xxxx_xxxx_xxxx函数的编写方法

这个回调函数就是你要用的通信协议啦,如果你想用模拟IIC或者模拟SPI的话,其实官方是有写好给你直接用的,官方是这么说的:

就是说,如果想用软件模拟时序的话,官方已经把模拟时序的相关函数写好了,这些函数里面相关的引脚电平设置函数和延时函数用的就是我们在第八步里面告诉它的。可以直接用上面表格第一列的几个函数作为你的回调函数,这样就不用再写了,因而用软件模拟的话可以跳到下一步也行。

不过本篇主要是介绍硬件IIC的方式啦,那么就需要我们自己来写这个回调函数。这个回调函数是这种形式的:typedef uint8_t (*u8x8_msg_cb)(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)。一样的,函数名字可以随意,但输入参数没错就好了。官方给出了硬件SPI和硬件IIC的模板,分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
extern "C" uint8_t u8x8_byte_arduino_hw_spi(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr) {
uint8_t *data;
uint8_t internal_spi_mode;
switch(msg) {
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 ) {
SPI.transfer((uint8_t)*data);
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_disable_level);
SPI.begin();
break;
case U8X8_MSG_BYTE_SET_DC:
u8x8_gpio_SetDC(u8x8, arg_int);
break;
case U8X8_MSG_BYTE_START_TRANSFER:
/* SPI mode has to be mapped to the mode of the current controller, at least Uno, Due, 101 have different SPI_MODEx values */
internal_spi_mode = 0;
switch(u8x8->display_info->spi_mode) {
case 0: internal_spi_mode = SPI_MODE0; break;
case 1: internal_spi_mode = SPI_MODE1; break;
case 2: internal_spi_mode = SPI_MODE2; break;
case 3: internal_spi_mode = SPI_MODE3; break;
}
SPI.beginTransaction(SPISettings(u8x8->display_info->sck_clock_hz, MSBFIRST, internal_spi_mode));
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_enable_level);
u8x8->gpio_and_delay_cb(u8x8, U8X8_MSG_DELAY_NANO, u8x8->display_info->post_chip_enable_wait_ns, NULL);
break;
case U8X8_MSG_BYTE_END_TRANSFER:
u8x8->gpio_and_delay_cb(u8x8, U8X8_MSG_DELAY_NANO, u8x8->display_info->pre_chip_disable_wait_ns, NULL);
u8x8_gpio_SetCS(u8x8, u8x8->display_info->chip_disable_level);
SPI.endTransaction();
break;
default:
return 0;
}
return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
uint8_t u8x8_byte_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;

switch(msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 )
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
break;
case U8X8_MSG_BYTE_SET_DC:
/* ignored for i2c */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
i2c_transfer(u8x8_GetI2CAddress(u8x8) >> 1, buf_idx, buffer);
break;
default:
return 0;
}
return 1;
}

接下来就照猫画虎,跟着硬件IIC的模板稍微改一下就好了。主要是两个地方:

  • 一个是IIC初始化,我们的HAL库自动生成了初始化代码并且在主函数调用了,这里我们不填也行
  • 另外一个是那个i2c_transfer()函数要换成HAL库的。HAL库的IIC写函数有两个,一个是HAL_I2C_Master_Transmit(),另外一个是HAL_I2C_Mem_Write(),后者一般用于器件中还有内存或者寄存器地址的情况,比如EEPROM等。所以我们用前者。
  • 还有一个需要注意的事情,大坑,就是u8x8_GetI2CAddress(u8x8)这里返回来的是已经右移了的器件地址,0.96寸oled一般是0x78,然后HAL库里面的也是要我们填右移了的器件地址,所以不要像模板那样再往左移动一位了(可能别的啥库函数需要左移后的原始地址吧,为啥不统一呢,太坑了!!!)
    over,综上,我们的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;

switch(msg){

case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while( arg_int > 0 ){
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;

case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
break;

case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;

case U8X8_MSG_BYTE_END_TRANSFER:
HAL_I2C_Master_Transmit(&hi2c1,u8x8_GetI2CAddress(u8x8), buffer, buf_idx,1000);
break;

default:
return 0;
}
return 1;
}

初始化

调用第四步剩下的u8g2_Setup_ssd1306_i2c_128x64_noname_f()这个函数来初始化,第一个参数是一个空的结构体地址,第二个参数表示是否旋转,第四个参数是我们写的gpio的那个回调函数名字,第三个参数是我们写的另外一个回调函数名或者我截图表格里那几个模拟时序的函数名。
*代码如下:*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "u8g2.h"

uint8_t u8x8_byte_hw_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr);
uint8_t u8g2_gpio_and_delay_stm32(U8X8_UNUSED u8x8_t *u8x8, U8X8_UNUSED uint8_t msg, U8X8_UNUSED uint8_t arg_int, U8X8_UNUSED void *arg_ptr);

u8g2_t u8g2;

u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2,U8G2_R0,u8x8_byte_hw_i2c,u8g2_gpio_and_delay_stm32);
u8g2_InitDisplay(&u8g2); // send init sequence to the display, display is in sleep mode after this,
u8g2_SetPowerSave(&u8g2, 0); // wake up display
u8g2_ClearDisplay(&u8g2);
u8g2_SetFont(&u8g2, u8g2_font_wqy16_t_chinese1);
u8g2_DrawCircle(&u8g2,60,30,20,U8G2_DRAW_ALL);
u8g2_DrawUTF8(&u8g2,10,50,"你好,world");
u8g2_SendBuffer(&u8g2);
HAL_Delay(3000);

上面的代码记得按照cubemx的要求放到代码中的相应部分,不然一更新用户代码就没了。

要显示中文的话,一定要把编译器的编码格式改成UTF-8,不然可能啥都显示不了。另外好像能显示的中文有限,改成俺的名字就直接编译错误了,应该是字库没有这些字的原因,后面再研究下咋整。

一次可以设置多个图形,这个图形数据会以叠加的方式更新到原有的buf里面,然后调用u8g2_SendBuffer(&u8g2)这个函数把buf一下子全写入到oled里面进行更新。也就是说不调用这个函数做再多操作也只是再更新buf而不会更新oled显示。、


相关链接

  1. (强推)U8G2 软件 IIC 移植至STM32(以STM32F407为例)

  2. stm32移植U8G2图像库指南

  3. u8g2的stm32f103c8t6移植

  4. (这个个人博客也很好)U8g2图形库移植

  5. (有讲硬件IIC-博主也有说明LVGV)U8g2图形库与STM32移植

  6. STM32硬件IIC移植U8g2库教程详解

  7. (这个还没试过-看评论应该可以)u8g2库stm32移植记录(硬件IIC)


=================我是分割线=================

欢迎到公众号来唠嗑: