STM32 + I2C OLED SSD1306 + u8glib + CubeMX

UPDATE: u8glib is outdated now and there is new library u8g2, with full community support. Here you can find tutorial about usage u8g2 library with stm32 MCU.

If you think, that title of this post consists of all possible random words, you are wrong:) This is what I've been trying to make work correctly during the last days! Actually, the task is not too hard, but only when you've got rid of all possible misunderstanding with datasheets and manuals. And now -tadadam - you are fortunate fellows and you will achieve a good portion of really working code!

Frankly speaking, first thing, which thrown sand into my wheels was this blog. It was good starting point, but it does not work for me. I'll explain why a little after.
Ok, now I'll tell a bit about what I'm talking about. U8blib - is cool library to manage with various monochrome displays. It could take the responsibility of middle-level interface with display. For example, draw lines, place text, another primitives - everything this library can. But you need to provide low-level interface to it - such as byte send, reset device, delays etc. SSD1306 OLED display - this is the guy, like this:

I've bought it on aliexpress, and selected I2C connected display. Also, I have SPI version. But today we'll talk about I2C only.

Ok, let's start. First - download latest version of u8glib library here:
https://bintray.com/olikraus/u8glib/ARM/view

Unpack archive, we'll see folders  src and two folders for lpc controllers. But we are cool guys and we need code for cool controllers:) Thus - we delete folders for LPC controllers, and we'll make folder inc. Into inc folder place files u8g_arm.h (need to be created manually), and u8g.h (already existed in src folder).

u8g_arm.h contains:

 #ifndef _U8G_ARM_H  
 #define _U8G_ARM_H  
   
   
 #include "u8g.h"  
 #include "stm32f4xx_hal.h"  
   
   
 #define DATA_BUFFER_SIZE 1000  
 #define I2C_TIMEOUT 10000  
 #define DEVICE_ADDRESS 0x78 //device address is written on back side of your display  
 #define I2C_HANDLER hi2c3  
   
 extern I2C_HandleTypeDef hi2c3; // use your i2c handler  
   
   
 uint8_t u8g_com_hw_i2c_fn(u8g_t *u8g, uint8_t msg, uint8_t arg_val, void *arg_ptr);  
   
 #endif  


u8g_arm.c:

 #include "u8g_arm.h"  
   
 static uint8_t control = 0;  
 void u8g_Delay(uint16_t val)  
 {  
   
  HAL_Delay(val);  
 }  
   
 void u8g_MicroDelay(void)  
 {  
  int i;  
  for (i = 0; i < 1000; i++);  
 }  
   
 void u8g_10MicroDelay(void)  
 {  
      int i;  
      for (i = 0; i < 10000; i++);  
 }  
   
   
 uint8_t u8g_com_hw_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:           /* define cmd (arg_val = 0) or data mode (arg_val = 1) */  
    u8g_10MicroDelay();  
    if (arg_val == 0)  
    {  
         control = 0;  
    }  
    else  
    {  
         control = 0x40;  
    }  
    break;  
   
   case U8G_COM_MSG_WRITE_BYTE:  
   {  
        uint8_t buffer[2];  
        buffer[0] = control;  
        buffer[1] = arg_val;  
        HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t*) buffer, 2, I2C_TIMEOUT);  
   }  
        break;  
   
   case U8G_COM_MSG_WRITE_SEQ:  
   case U8G_COM_MSG_WRITE_SEQ_P:  
   {  
        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);  
   }  
   
    break;  
  }  
  return 1;  
 }  



I'll remind you: place u8g_arm.c file into src folder, make inc folder and place there u8g.h file (without changes) and our prepared file u8g_arm.h.

Why we did this all?
U8glib must have access to low-level functions to work with display. It needs delay functions and function which could perform such operations: write byte to display, write sequence of bytes, init display etc.
Our OLED display works a little bit tricky: it has two modes of sending data. First - send command and second - send data. And how it recognises them? Before every byte, which you'll send, you need to send "instruction" to display, to told display, what type of data you'll send: data or command. If you intend to send command, you need to send [0x00 0xyour_command] array. If you want to send data, you need to send [0x40 0x_your_data] array. By the way, that blog, which I have mentioned before, made this operations incorrectly, which leads to wrong operation.

Ok, now we're ready to start CubeMX and prepare our project. I'll skip steps, where I just pick up my discoveryF4 board.






Clock configuration stay unchanged, also I've did not changed any I2C configuration:




Ok, let's generate project for system workbench, and try to compile it + flash your controller. If you have not achieved success with this, problem is not in display:)
Now, we'll place our previously prepared folders "src" and "inc" into folder "your_project_folder\Drivers\u8glib\". I do not know why, but eclipse-based SystemWorkbench tool does not see our folder, and we need to link it manually to our project. Right-click on folder "Drivers" in project explorer, New->;Folder, then button "Advanced", there - "Link to alternate location"(huh, difficult, maybe someone knows how to do this simpler?).
Then - right-click on project, there - properties. There we need to add our folder to includes.

Also, I've changed c language dialect, to be able write like this:
for(int i = 0; i < 10; i++)
because older standards allows only like this:
int i;
for (i = 0; i < 10; i++)

and turn off code optimization.


Also, there is another issue. U8glib has A LOT of fonts for writing strings on your display. But they has large size. So, you need to select only fonts, which you need. You can watch them here: https://github.com/olikraus/u8glib/wiki/fontsize. And then, I'll select only two by deleting unnecessary fonts in file u8g_font_data.c. Or, you can just comment. Idea is to left only this per font:




 #include "u8g.h"  
 const u8g_fntpgm_uint8_t u8g_font_profont10[2560] U8G_FONT_SECTION("u8g_font_profont10") = {  
 /*you font, i've not placed this*/  
  };  
    


Ok now we'll open our main.c
 and modify it like this:
  /* Includes ------------------------------------------------------------------*/  
 #include "stm32f4xx_hal.h"  
   
 /* USER CODE BEGIN Includes */  
 #include "u8g_arm.h"  
 /* USER CODE END Includes */  
   
 /* Private variables ---------------------------------------------------------*/  
 I2C_HandleTypeDef hi2c3; /*this is our handler, you need to place it in your u8g_arm.h file!!!!!  
   
 /* USER CODE BEGIN PV */  
 /* Private variables ---------------------------------------------------------*/  
 static u8g_t u8g;  
 /* USER CODE END PV */  
   
 /* Private function prototypes -----------------------------------------------*/  
 void SystemClock_Config(void);  
 static void MX_GPIO_Init(void);  
 static void MX_I2C3_Init(void);  
   
 /* USER CODE BEGIN PFP */  
 /* Private function prototypes -----------------------------------------------*/  
   
 /* USER CODE END PFP */  
   
 /* USER CODE BEGIN 0 */  
 /*  
 Function which responds for drawing  
 */  
 void draw(void)  
 {  
      u8g_SetFont(&u8g,u8g_font_profont10);//set current font  
      u8g_DrawStr(&u8g, 2, 12, "Hello!");//write string - you set coordinates and string  
      u8g_DrawBox(&u8g, 30, 30, 35, 35);//draw some box  
 }  
 /* USER CODE END 0 */  
   
 int main(void)  
 {  
   
  /* USER CODE BEGIN 1 */  
   
  /* USER CODE END 1 */  
   
  /* MCU Configuration----------------------------------------------------------*/  
   
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */  
  HAL_Init();  
   
  /* Configure the system clock */  
  SystemClock_Config();  
   
  /* Initialize all configured peripherals */  
  MX_GPIO_Init();  
  MX_I2C3_Init();  
   
  /* USER CODE BEGIN 2 */  
  u8g_InitComFn(&u8g, &u8g_dev_ssd1306_128x64_i2c, u8g_com_hw_i2c_fn); //here we init our u8glib driver  
  /* USER CODE END 2 */  
   
  /* Infinite loop */  
  /* USER CODE BEGIN WHILE */  
  while (1)  
  {  
  /* USER CODE END WHILE */  
   
  /* USER CODE BEGIN 3 */  
  //this loop correspond of drawing  
       u8g_FirstPage(&u8g);  
           do  
           {  
                draw();  
                } while ( u8g_NextPage(&u8g) );  
                u8g_Delay(10);  
  }  
  /* USER CODE END 3 */  
   
 }    
 /* UNCHANGED PART OF YOUR CODE */  


 Do not forget to place your i2c handler to u8g_arm.h file!
u8g_InitComFn(&u8g, &u8g_dev_ssd1306_128x64_i2c, u8g_com_hw_i2c_fn); - here we init our driver and tell it, what function if responsible for low-level communication with our display.

Everything you want to draw you place to draw() function.
Code here: https://github.com/sincoon/SSD1306_I2C

Tadam! Start debug, and we see wonderful picture with square and little text:)
The only issue, which I've found - there is some garbage at last pixel in row, and now I do not know how to fix this. I hope, I'll found solution, and tell you. Or, you'll find and tell me:)


P.S. Dear fellows, I hope that somebody read this article:) And I hope that for someone it will be helpful. Also, I have bad English spelling and styling skills, and if someone want help to improve this article by fixing this text, you are welcome! Just write to me to sincoon@gmail.com. Also, your comments are also very appreciated! Thank you all!


Comments

  1. Is it possible to avoid using "for" operator for delays? (like for (i = 0; i < 1000; i++); in u8g_MicroDelay and u8g_10MicroDelay). I'm afraid that this could be different delay for different chips: you are using STM32F4, but will it work for STM32F1 with the same delay? Also it is not good if you will enable FreeRTOS.

    ReplyDelete
    Replies
    1. Actually, it works fine if you'll set just HAL_Delay(1) for both cases(10 and 1 microseconds). I've not found any timing sensitive features for this display, so, maybe it is simplest way.

      Delete
  2. Thank you for the instructions, i was able to connect oled display to stm32f3discovery. Btw, i've had problem with garbage pixels at end of lines first time, but it was gone when switched to another I2C interface - found out that one of pins of I2C1 on stm32f3discovery was used for SWD. May be you have same problem with conflicts on I2C pins?

    ReplyDelete
    Replies
    1. Thank you for your advice! I'll try another I2C port, and I hope this will help:)

      Delete
    2. Actually, this didn't work. After some time bad dots did appear again. So, i've just redefined display size in the u8glib as 129x64 ("#define WIDTH 129" in u8g_dev_SSD1306_128x64.c). :)

      Delete
    3. Add 1 to the arg_val and change WIDTH back to 128. This should be a proper fix for garbage pixels.

      HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t *)buffer, arg_val + 1, I2C_TIMEOUT);

      Delete
    4. this HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t *)buffer, arg_val + 1, I2C_TIMEOUT);

      don't work for me, I have: HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t*) buffer, 2, I2C_TIMEOUT); in the file ug8_arm.c

      If I change the width to 129 the garbage disappears

      Delete
    5. wait! there is a second line with arg_val instead the '2' I changed for arg_val+1 and now works with width 128 and without grabage!

      Delete
  3. Hi thanks for this nice tutorial
    I have a problem when i trying to write a string there is only half of string visible bottom is not
    i changed font and position but nothnig changed

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Hi, I have a problem:
    make: *** [Drivers/u8glib/src/u8g_arm.o] Error 1
    Any suggestions, what could be the problem?

    ReplyDelete
    Replies
    1. Do you use IAR or System Workbench?

      Delete
    2. I use System Workbench for STM32 downloaded from openstm32.org

      Delete
    3. This comment has been removed by the author.

      Delete
    4. Please help me this problem !!!
      make: *** [Drivers/u8glib/src/u8g_arm.o] Error 1

      contact Mail: sheva_h07@yahoo.com

      Delete
  6. To add a folder to the project should be done next:

    1. Properties - C/C++ General - Path and Symbols - Source Location - Add Folder...

    2. Properties - C/C++ Build - Settings - Tool Settings - MCU GCC Compiller - Includes - Include paths - Add...

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Please help me this problem !!!
      make: *** [Drivers/u8glib/src/u8g_arm.o] Error 1

      contact Mail: sheva_h07@yahoo.com

      Delete
  7. This comment has been removed by a blog administrator.

    ReplyDelete
  8. HI,
    anybody use the tutorial with Visual Studio? I am going mad... I have problems with the linker... i think

    1>Linking ../VisualGDB/Debug/F0-SSD1306...
    1>VisualGDB/Debug/main.o: In function `main':
    1>D:\Development\02_Programming\04_Cortex-M\STM32\F0-SSD1306\Src\main.c(98): error : undefined reference to `u8g_InitComFn'
    1>D:\Development\02_Programming\04_Cortex-M\STM32\F0-SSD1306\Src\main.c(103): error : undefined reference to `u8g_com_hw_i2c_fn'
    1>D:\Development\02_Programming\04_Cortex-M\STM32\F0-SSD1306\Src\main.c(103): error : undefined reference to `u8g_dev_ssd1306_128x64_i2c'
    1>collect2.exe : error : ld returned 1 exit status

    At this point my knowledge leaves me. :/

    ReplyDelete
    Replies
    1. Include path is ok? I mean, have you added u8glib?

      Delete
    2. Hi Artyom,
      i am not sure. I have put all the files under the \driver\u8glib folder and it looks like Visual Studio finds the source files. I think the linker is problem. My knowledge about Visual Studio is still a little underdeveloped and i am not sure how i solve this problem. It is maybe a litte thing.

      I hope some one is a VS Geek and has a solution.... Meanwhile i am digging around a little more.

      If i got it... i post the solution :)

      Delete
    3. Hi Artyom,
      okay i got it. My fault. In Visual Studio i need to add all the files to the project. It is not enough to just add the path.

      So at the end it runs :)

      Thx

      Delete
    4. I have lost hope in visualGDB and moved to sw2stm , got a nice dark theme and now im headache free. I love VS for Visual C# it is unparalleled , but not till a Microsoft integrated Solution for ARM is made will i try it again for ARM

      Delete
  9. -> In function: u8g_com_hw_i2c_fn()
    -> For switch cases: U8G_COM_MSG_WRITE_SEQ and U8G_COM_MSG_WRITE_SEQ_P
    -> Local array is initialized: uint8_t buffer[DATA_BUFFER_SIZE];
    -> Where DATA_BUFFER_SIZE = 1000 bytes
    1. The array buffer[] gets put onto the stack.
    2. Most STM32 Nucleo dev kits have a default stack size of 0x400 (1024).
    3. When u8g_com_hw_i2c_fn is called, it is capable of overflowing the stack.


    ==> Solution: refactor by making buffer[] reside in RAM rather than the stack
    1. At the top of u8g_arm.c, add:
    static uint8_t buffer[DATA_BUFFER_SIZE]
    1. In u8g_com_hw_i2c_fn(), comment out line:
    // uint8_t buffer[DATA_BUFFER_SIZE];

    <== repercussions:
    1. RAM usage is increased by the size of your buffer
    2. The call to u8g_com_hw_i2c_fn() now uses approx 40 bytes rather than DATA_BUFFER_SIZE+40 bytes so lees chance of blowing out the stack



    -> Your example uses the SSD1306 display with 128x64 resolution
    -> 128*64 = 8192 pixels
    -> 8192 pixels pack into 1024 bytes
    -> The code updates 1/2 of the screen at a time so only 512 bytes are required for half-frame buffering
    -> buffer[] includes the bytes for half-frame buffering and some additional params
    -> The example uses DATA_BUFFER_SIZE = 1000

    ==> Solution: for SSD1306 with 128x64 display:
    1. Redefine DATA_BUFFER_SIZE to 520 (I think it can go down to 512 but have not ASSERTed this)

    <== reprecussions:

    1.Decreased space required in RAM for pixel buffering
    2. DATA_BUFFER_SIZE needs to be defined appropriately for the display resolution being used:
    1. 128x64 pixel display requires DATA_BUFFER_SIZE ~ 520
    2. 128x32 pixel display requires DATA_BUFFER_SIZE ~ 260
    3. 92x32 pixel display requires DATA_BUFFER_SIZE ~ 190



    NOTES:
    1. If sufficient RAM exists on the microcontroller, it may be beneficial to implement full frame buffering. Display becomes more responsive because the I2C interface is rather slow
    2. Use FreeRtos and define a task for the display update - or - put the display update in its own thread. If FreeRtos, set priority low. In both scenarios, the display update task/thread is called round-robin during idle. Delay functions can be made simpler and the code becomes non-blocking


    ReplyDelete
    Replies
    1. Note: in previous post, the following
      ==> Solution: for SSD1306 with 128x64 display:
      1. Redefine DATA_BUFFER_SIZE to 520 (I think it can go down to 512 but have not ASSERTed this)

      should have read

      ==> Solution: for SSD1306 with 128x64 display:
      1. Redefine DATA_BUFFER_SIZE to 520 (I think it can go down to 513 but have not ASSERTed this)

      Delete
  10. To fix issue with last column garbage you need send all data in page. In file u8g_arm.c the line HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t *)buffer, arg_val, I2C_TIMEOUT); should have data length = arg_val +1.
    So this line should be:
    HAL_I2C_Master_Transmit(&I2C_HANDLER, DEVICE_ADDRESS, (uint8_t *)buffer, arg_val+1, I2C_TIMEOUT);

    ReplyDelete
  11. Thank you a lot! Got a black pill (f411ce) with a ssd1303 128x32 LPC1114FN28 with your guide without a hassle!

    ReplyDelete
    Replies
    1. Paste fault, forget the LPC1114FN28

      Delete

Post a Comment

Popular posts from this blog

u8g2 library usage with STM32 MCU

Use PCM5102 with STM32

RFFT in CMSIS DSP. Part 1.