-
-
Notifications
You must be signed in to change notification settings - Fork 21
Getting Started
I wrote OneBitDisplay to be a single library to control all types of displays which have 1-bit per pixel. I noticed that the memory layout of OLEDs and LCDs was the same for most displays, so it made sense to have all of the pixel code in common and just write unique code to initialize the different display types. This grew recently into supporting e-paper displays as well. The common graphics functions can draw text and do the usual types of graphics primitives as well as a few unique ones for using Windows BMP files. OneBitDisplay is not related to LVGL or any GUI-focused API, instead it provides code to talk to a lot of different display types with primitive functions that are common across different displays and target MCUS/Operating systems. What does this mean in real terms? It means that you can write code to display info on your 128x64 OLED connected to your Arduino board and the same code (with minor changes) can draw on an e-paper display connected to a Raspberry Pi. The C code is C99 and free from external dependencies so that even the humblest microcontroller can use it.
After initializing the display (OLED, LCD or e-paper), the OBD library starts in a memoryless state. This means that there is no memory reserved to hold a copy of the display image. In this state, some operations will work and others will not. An attempt to use a function that cannot work in this mode will return OBD_ERROR_NO_MEMORY. An example of operations that will work are drawing text. The text is sent as whole bytes (8 rows or columns) directly to the display's internal memory. Most displays only allow writing into the internal RAM, not reading it back so writing individual pixels isn't really possible - older data occupying the same byte will be overwritten. This memoryless state is useful for when you only need to use a limited set of features or if your microcontroller doesn't have enough RAM to hold the entire display. To use all of the features of OBD, you will need to provide or ask OBD to allocate a display buffer. This is a memory area to hold the pixels of the entire display. For example, a 128x64 OLED has 8192 pixels which occupy 1024 bytes. If you ask OBD to allocate a back buffer, it will try to allocate 1024 bytes of RAM using malloc(). Once you have a back buffer, the drawing functions can draw into this memory or draw directly onto the display memory depending on the render flag. Any drawing done to the OBD RAM copy of the display is not visible until you ask OBD to "dump/display" it on the physical display.
The simplest displays to connect use I2C since they only require 2 wires for communication (clock and data). There are still a number of options that need to be specified to get started. The Arduino C++ class has default values for all of them:
Display type: OLED_128x64 (SSD1306 0.96" 128x64)
I2C pins: The default pins for your Arduino board
I2C address: determined automatically at run time (0x3c or 0x3d)
I2C speed: 400Kbs (the max default of older controllers)
I2C wire mode: hardware I2C (as opposed to bit-banging)
The C init function requires these values to be specified explicitly, but you can use the value -1 to tell OBD to use the same defaults as above:
int obdI2CInit(OBDISP *pOBD, int iType, int iAddr, int bFlip, int bInvert, int bWire, int sda, int scl, int reset, int32_t iSpeed)
pOBD - Pointer to a OBDISP structure. This holds the unique info for your display. The OneBitDisplay library can manage any number of displays simultaneously, so your MCU can control one or more at a time.
iType - The display type defined in OneBitDisplay.h. For I2C, this can be one of the OLED_xxx defines (e.g. OLED_128x64).
iAddr - The I2C address of your display. Most OLEDs are at 0x3C or 0x3D. You can also set this to -1 and it will automatically detect the address of your display (if there is only 1 on the I2C bus).
bFlip - A boolean indicating where the display is in the normal orientation (0) or flipped 180 degrees (1).
bInvert - OLEDs and LCDs have an option to invert the colors. This is implemented in hardware and not done by OneBitDisplay.
.
bWire - This parameter tells the library to use hardware I2C (the Wire library) or software (bit-banging of the signals on GPIO pins). For practical purposes, there's not a big difference between the two. On certain MCUs (e.g. AVR), the Wire library limits the max speed to 400Khz even though the display is capable of handling a much faster signal. Some MCUs like the ESP32 have more flexible pin definitions and can do hardware I2C on various pins. In that case, you can specify the specific pin numbers to use. To use the default I2C pins, specify -1 for both SDA and SCL. When bWire is false (0), you must specify valid GPIO pin numbers for SDA and SCL.
sda - As described above, this specifies the GPIO pin number for the SDA signal. If you set bWire to true (1) and want to use the default I2C pins, you can set this to -1.
scl - As described above, this specifies the GPIO pin number for the SCL signal. If you set bWire to true (1) and want to use the default I2C pins, you can set this to -1.
reset - The GPIO pin used to reset the display. Some OLED displays configured for I2C require this signal, others do the reset automatically and don't need it. Set this to -1 if your display doesn't have a reset pin exposed.
iSpeed - The I2C clock speed. 'normal' I2C speed is 100Khz, 'fast' is 400Khz. All of the OLEDs I've tested can handle much faster signals. On ESP32 you can set this as high as 800Khz, on ARM Cortex-M, the MCUs I've tested allow you to specify almost any speed, but the displays usually stop working above 1.6Mhz depending on the wiring and pull-up resistors used.
So, here's a simple example of initializing a 128x64 OLED connected to the default I2C bus on any MCU:
OBDISP obd;
obdI2CInit(&obd, OLED_128x64, -1, 0, 0, 1, -1, -1, -1, 400000L);
OneBitDisplay comes with a set of built-in fixed fonts and also supports drawing Adafruit_GFX format bitmap fonts. This allows a wide variety of use cases. They can be drawn on any pixel boundary and support partial display (clipping). The built-in fixed fonts can be drawn directly on the display or to a back buffer (or both), but only allow drawing them on byte boundaries (any pixel column, but only on rows in 8 pixel groups). For the fixed fonts, there are some advanced features present such as fine horizontal scrolling and delayed rendering (cacheing of small writes into larger blocks). For C++, use the setFont() and setFreeFont() methods to choose the font, then the print or drawString() methods. For C, here's the fixed font drawing function:
int obdWriteString(OBDISP *pOBD, int iScrollX, int x, int y, char *szMsg, int iSize, int bInvert, int bRender)
pOBD - Pointer to a OBDISP structure
iScrollX - A scroll offset representing the number of pixels to scroll the text to the left. For example, if the 8x8 font is used and this value is 4, then the left half of the first character will not be visible.
x - The column to start drawing the text (starting from 0). On a 128x64 display, this value can be 0-127.
y - The byte row to start drawing the text. On a 128x64 display there are 8 byte rows (64/8), so this value can be 0-7.
szMsg - Zero terminated (C String) containing the text to draw. Text which would run off the right edge of the display will continue to be drawn on the next line of word wrap is enabled (see obdSetTextWrap).
iSize - This specifies one of the enumerated font size values (e.g. FONT_NORMAL).
bInvert - Flag to invert the color of the characters and background.
bRender - Flag indicating if the text will be drawn immediately on the display (true) or just to a backing buffer (false). If no backing buffer is defined, this will be set to true.
example - write the words "Hello World" on the first line of the display with the 8x8 font:
obdWriteString(&obd, 0,0,0, (char *)"Hello World", FONT_NORMAL, 0, 1);
This is the proportional font drawing function and text box measurement function:
int obdWriteStringCustom(OBDISP *pOBD, GFXfont *pFont, int x, int y, char *szMsg, uint8_t ucColor);
void obdGetStringBox(GFXfont *pFont, char *szMsg, int *width, int *top, int *bottom);
Here's an example of writing some text in a custom font:
obdWriteStringCustom(&obd, (GFXfont *)&FreeSerif12pt7b, 0, 16, (char *)"Hello World",1);
Due to the way that these LCDs manage memory, it would require a lot of extra code to support memoryless access. For this reason, all of the drawing functions (e.g. text, circles, lines) will only function if you define a back buffer for your display. The drawing functions will only draw into memory and not update the display. When you are ready to display your content, you must call obdDumpBuffer()
. If your MCU doesn't have enough memory to allocate a back buffer, there is another option. The obdWriteLCDLine()
function allows you to write a single line of pixels at a time without needing an entire back buffer. For the 400x240 LCD you only need to provide 50 bytes (400 horizontal pixels) at a time.