Recently, I have come to a conclusion that it would be good to print the state of my quadrocopter on a display. The question was what kind of display should I use. I decided to give a try to a small 0.96″ OLED display with I2C interface. It is based on SSD1603 driver. You can see this display on the image above. However, there was an issue regarding the screen controller. But it was soon solved. I have come across the U8glib which is a graphic library, quite popular among Arduino users. As soon as I started to read about the library I realized that it does not support STM32, not mentioning the HAL library.
Well, I have solved that 😉
Some time ago I have described the HAL library from STMicroelectronics along with SPL and other solutions. I have found that other people have already started some work regarding the U8glib for STM32. This blog post describes a general rule how a communication interface should be ported. However, the Author is using the SPL. The approach can be generalized and used to write a driver for STM32 HAL.
How does the U8glib works?
What is the most important thing about the U8glib is that there are abstraction layers. You can divide the code roughly in four parts:
- Physical layer (COM),
- Device layer (DEV),
- Drawing routines,
- Miscellaneous.
This makes porting drivers to different platforms a lot easier. The Author of the U8glib separated device routines, the driver itself, from the hardware layers. This allows a developer to focus on the part on which he or she wants to. If you are not interested in what you should send to the display to initialize it and instead you would like to work closer to the hardware you can focus only on the hardware abstraction layer (HAL, what a coincidence :)).
To create an instance, device with communication interface union, you have to write a line of code like below.
u8g_InitComFn(&u8g, &u8g_dev_ssd1306_128x64_i2c, u8g_com_arm_stm32_ssd_i2c_fn);
This binds the device with the communication layer. Say, if you want to draw a line and you know you have to send 0x12 0x00 0x00 0x2F 0x1F to the display you do not have to worry about it. The device layer sends it to the physical layer and that’s all. Now, it is the physical layer’s task to actually transmits the data to the display.
Physical layer interface
The function responsible for handling communication has a template like this one below
uint8_t u8g_com_arm_stm32_ssd_i2c_fn(u8g_t *u8g, uint8_t msg, uint8_t arg_val, void *arg_ptr) { switch(msg) { case U8G_COM_MSG_STOP: break; case U8G_COM_MSG_INIT: break; case U8G_COM_MSG_ADDRESS: break; case U8G_COM_MSG_CHIP_SELECT: break; case U8G_COM_MSG_RESET: break; case U8G_COM_MSG_WRITE_BYTE: break; case U8G_COM_MSG_WRITE_SEQ: break; case U8G_COM_MSG_WRITE_SEQ_P: break; } return 1; }
- U8G_COM_MSG_STOP: stop the device,
- U8G_COM_MSG_INIT: initialize the device,
- U8G_COM_MSG_ADDRESS: defines data or command mode (command mode for arg_val = 0),
- U8G_COM_MSG_CHIP_SELECT: chip select (valid for SPI interface),
- U8G_COM_MSG_RESET: reset the device (valid for devices with reset port),
- U8G_COM_MSG_WRITE_BYTE: write single byte,
- U8G_COM_MSG_WRITE_SEQ: write byte sequence to device,
- U8G_COM_MSG_WRITE_SEQ_P: write byte sequence to device from flash (program memory).
Solution to the problem
Soon I have found a different attempt to port the driver for the OLED display. The Author is clearly using the same template I have found in the first article. However, there are some issues, mainly two. The first one is how memory is handled. The Author is coping the data that is going to be send to a buffer with below routine:
uint8_t buffer[DATA_BUFFER_SIZE];
uint8_t *ptr = arg_ptr;
buffer[0] = control;
for (int i = 1; i <= arg_val; i++)
{
buffer[i] = *(ptr++);
}
HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t *)buffer, arg_val, I2C_TIMEOUT);
It is not very efficient way to send data, or copy the date on 32-bit MCU. Firstly, you lose time to copy the data and secondly you waste memory for it. Of course, now the microcontrollers have more and more resource but if you can save them than why not? The second issue is referred to the amount of data you want to send. It should be actually arg_val + 1 because you send one extra byte.
Having all this covered I prepared my own implementation of the driver for SSD1603 screen controller.
#include "u8g_com_arm_stm32.h" #if defined USE_HAL_DRIVER #include "i2c.h" # if defined STM32F407xx # include "stm32f4xx_hal.h" # endif #endif #if defined USE_HAL_DRIVER #define STM32_HAL_I2C_HANDLER hi2c1 #define STM32_HAL_I2C_TIMEOUT 2000 # if defined(STM32F407xx) uint8_t control = 0; void u8g_Delay(uint16_t val) { HAL_Delay(val); } void u8g_xMicroDelay(uint16_t val) { static uint32_t i, j; static uint32_t freq; freq = HAL_RCC_GetSysClockFreq() / 1000000; for (i = 0; i < val;) { for (j = 0; j < freq; ++j) { ++j; } ++i; } } void u8g_MicroDelay(void) { u8g_xMicroDelay(1); } void u8g_10MicroDelay(void) { u8g_xMicroDelay(10); } uint8_t u8g_com_arm_stm32_ssd_i2c_fn(u8g_t *u8g, uint8_t msg, uint8_t arg_val, void *arg_ptr) { switch (msg) { case U8G_COM_MSG_STOP: break; case U8G_COM_MSG_INIT: u8g_MicroDelay(); break; case U8G_COM_MSG_ADDRESS: if (arg_val == 0) { control = 0; } else { control = 0x40; } u8g_10MicroDelay(); break; case U8G_COM_MSG_WRITE_BYTE: { HAL_I2C_Mem_Write(&STM32_HAL_I2C_HANDLER, SSD1306_I2C_ADDRESS, control, 1, &arg_val, 1, STM32_HAL_I2C_TIMEOUT); } break; case U8G_COM_MSG_WRITE_SEQ: case U8G_COM_MSG_WRITE_SEQ_P: { HAL_I2C_Mem_Write(&STM32_HAL_I2C_HANDLER, SSD1306_I2C_ADDRESS, control, 1, arg_ptr, arg_val, STM32_HAL_I2C_TIMEOUT); } break; } return 1; } # endif #endif
And the header file:
#ifndef U8G_COM_ARM_STM32_SSD_I2C_HEADER #define U8G_COM_ARM_STM32_SSD_I2C_HEADER #include "u8g.h" #define SSD1306_I2C_ADDRESS 0x78 uint8_t u8g_com_arm_stm32_ssd_i2c_fn(u8g_t *u8g, uint8_t msg, uint8_t arg_val, void *arg_ptr); #endif
As you can see I am using HAL_I2C_Mem_Write() function instead of HAL_I2C_Master_Transmit(). This allows you to send an extra byte/bytes followed by actual data. In this way you do not have to copy it manually to a buffer. Basically, HAL_I2C_Mem_Write() plainly concatenates two chunks of data by sending one after another. This is commonly used i.e. for data transfer with sensors like accelerometers, magnetometers, etc. Firstly, you have to point a register address to which you want to write to or read from and after that the data should follow.
How to use the driver?
To use the driver you have to do a few simple steps. Firstly, prepare your project. You can do this with STM32CubeMX. Create a *.c file, mine source file is u8g_com_arm_stm32.c and the header file is u8g_com_arm_stm32.h. Put the code listed above to the files and add them to your project. This is pretty much it. You can use below sample code to display “Hello World!” text on the screen:
u8g_InitComFn(&u8g, &u8g_dev_ssd1306_128x64_i2c, u8g_com_arm_stm32_ssd_i2c_fn); u8g_Begin(&u8g); while (1) { u8g_FirstPage(&u8g); do { u8g_SetFont(&u8g, u8g_font_profont12); u8g_DrawStr(&u8g, 0, 12, "Hello World!"); } while (u8g_NextPage(&u8g)); HAL_Delay(100); }
Files to download:
The driver is available at Github:
Also there is an example running on STM32F103C8T6 development board:
call u8g_Begin(&u8g) function is unnecessary because the initialization function u8g_InitComFn call it when return.
Yes, you are right. It is called at the return statement of u8g_InitComFn().
Thanks for pointing this out!
Hello. I have a STM32F103C8T6 board and a same display as you. From the start I will like to say that I’m a noob with the STM32F103 board. First of all can you tell me how to wire the board to the display. Second can you tell me what files need to go were and can you give me a simple sketch example for the Arduino IDE that I can use to display a simple text line? Thanks and have a nice day.
I do not know about the Arduino IDE but I can prepare a simple example in SW4STM32. Actually, I had this in my plans all along 🙂 I will upload this example to my github so you will be able to download it.
Ok. After you upload the example ca you post a link to it?
Yes, of course.
The example is available at github. I have tested the driver and is fully working on STM32F103C8T6 development board.
The link to the driver itself and to the example can be found at the bottom of the post.
Are you sure with the display model? May be SSD1306?
Yes, you might be right. Although if I remember correctly it was refereed to as SSD1603. However mine displays look as the SSD1306 you are refereeing to.
You initialize u8glib with u8g_dev_ssd1306_128x64_i2c handler. U8Glib has no 1603 display. According with source code U8Glib supports SSD1306 SSD1309 SSD1322 SSD1325 SSD1327 SSD1351 I think it’s just a typing mistake
After giving it a bit of thought I think you are right. Thanks for pointing this out! If I find some time I will make appropriate changes to the repos and the post. Thanks!