- Description
- Prerequisites
- Getting started
- SSD1306 commands
- Communicating with the SSD1306
- Embedded font in libSSD1306
libSSD1306
is a library meant for programming OLEDs driven by an
SSD1306
controller. There are other IC's that mimic the instruction set of
the SSD1306
. The most prominent clone out there is the SSH1106
. Getting
this library to work with the SSH1106
shouldn't require too much work.
meson
,ninja
, and ac99
compliant compiler for building the librarycurl
orwget
(optional, for downloading theSSD1306
's datasheet)git
(optional,libSSD1306
can be downloaded as a zip archive)doxygen
(optional, to build the documentation)make
(optional, provides aliases to quicken various aspects of developing/building the library)
To begin using this library, clone the repo.
$ git clone https://github.com/maybe-one-day-ubermensch/libSSD1306
Once you have cloned the repo, download the SSD1306's datasheet like so:
$ ./extra/dl_ssd1306_datasheet.sh
The script assumes you have either curl
or wget
in your $PATH
. If you
don't have any of the two, install one of them. They are both pretty useful!
Alternatively, you can copy the datasheet's url from the script, paste it into
your browser, and download it from there.
First, configure the build by calling meson
:
$ BUILD_DIR='build'
$
$ # meson defaults to building a shared library
$ meson "$BUILD_DIR"
or
$ # Tell meson to build a static library instead
$ meson -Ddefault_library=static "$BUILD_DIR"
or
$ # Tell meson to build both types of libraries
$ meson -Ddefault_library=both "$BUILD_DIR"
Finally, build the library:
$ ninja -C "$BUILD_DIR"
One thing to note is that meson
defaults to building static libraries as
position independent. If you do not want to build static libraries as
position independent, then supply the -Db_staticpic=false
argument when
intially configuring the build.
This method is quite simple for native builds, especially for single board computers that run some linux distribution like the raspberry pi but what about cross builds?
There is an
example
repository that includes a complete and working example on how to cross compile
libSSD1306
. Head over to the machine-files
directory and read the
README.md
. The README.md
talks about how cross compiling libSSD1306
with
meson
looks like. In short, you independently describe each aspect of your
build in machine files and, come
build time, layer the relavent machine files on top of one another.
In order to build and view the documentation, run the following command:
$ make view-doxygen
This will open the generated html
documentation defined in the BROWSER
environment variable. If BROWSER
is empty, then it will default to firefox
.
The view-doxygen
targets always builds the documentation before opening it in
the browser.
However, if you only want to build the documentation, run the following command:
$ make doxygen
All of the commands present in the datasheet are defined as enum
s and grouped
according to the table they are in. For example, Table 9-1
's title is
Fundamental Command
. Its corresponding type declaration is
enum ssd1306_fundamental_command
. All of the enum
definitions can be found
in ./include/ssd1306/ssd1306.h
There are 3 "classes" of commands.
- Commands that do not accept arguments
- Commands that accept arguments
- Commands that accept arguments but the arguments are OR'd with the command
As far as the controller is concerned, there are no optional arguments, all commands that accept arguments require them.
The last class of commands are annotated in the generated documentation as
OR'd arguments
. The annotations describe what the arguments are and their
position in the command. The format of the doxygen alias to generate said
documentation is as follows:
@cmdarg_or [@ref lib_defined_type] argument_name[x:y]
The @ref lib_defined_type
is a reference link to a datatype defined by
libSSD1306
, if one exists. Otherwise, leave it blank. Here is an example from
include/ssd1306/ssd1306.h
.
# 2:0 = a 3 bit value going from bit 2 to bit 0
# That 3 bit value is of type ssd1306_page
@cmdarg_or @ref ssd1306_page start_page[2:0]
Here is an another example but without the type.
# 3:0 = a 4 bit value going from bit 3 to bit 0
@cmdarg_or upper_nybble[3:0]
There is no universal way to configure all I2C
, SPI
, etc.. peripherals in
the world. There are some HAL's provided by manufacturers or RTOSes that enable
cross-platform development, but maintaining all of these configurations by
myself is error-prone. Instead, the user will have to implement an interface
that performs all the platform dependent I/O. As I see it, there are 2 ways of
implementing said interface.
The library's header files would contain the function prototypes of the interface
that needs to be implemented. The user would then have to write their glue code
within libSSD1306
's codebase.
- Function calls to the glue code can be inlined by the compiler.
-
Stuck with only one way of communicating with the
SSD1306
. -
Can complicate the build-system setup if more than one microcontroller is used within the same codebase.
-
The glue code must be compiled with the library and violates the open-closed principle. I could declare the functions as weak symbols to get around this, but that requires a compiler attribute which is not supported by standard C.
The library's header file would declare a struct whose fields would be function pointers to the platform dependent I/O. The user would then pass a pointer to this struct to the library's functions.
-
The user can change how the mcu can communicate with the
SSD1306
within their own code. -
The user could choose to switch which functions are used to communicate with the
SSD1306
. -
libSSD1306
can be compiled into a static library, separate from the glue code, then linked with the user's main application. As a result, this follows the open-closed principle.
-
The compiler may not inline calls to the functions as a result of using function pointers. However, these functions aren't meant to be doing much anyway so it may not matter too much.
const
'ing the struct or some of the fields of the struct may fix the inlining issue. todo(a simple example on godbolt.com didn't show a difference) todo(between calling a function from a const/non-const pointer.) todo(I need to figure out what's going on...) -
Extra
NULL
checking must be done by the functions to ensure the struct is valid. I can setup a compile-time switch to disable the checks with something like-DNDEBUG
to denote a release build. -
Might require a bit more memory as a result of storing
x
amount of function pointers, wherex
is the number of platform dependent functions declared. It's honestly not much but it might be on a very memory constrained platform.
Option 2!!
Option 2 is more portable across c compilers and, I feel, is the cleanest. This
option doesn't require altering the libSSD1306
codebase. The code the user
writes is independent of the library's, as it should be.
All of the code that pertains to platform dependent operations can be found in
./include/ssd1306/platform.h
. In it, there are two typedef
s and a struct
that are responsible for configuring the I/O of the library:
typedef enum ssd1306_err (*ssd1306_send_cmd_cb)(struct ssd1306_ctx *ctx,
uint8_t cmd);
typedef enum ssd1306_err (*ssd1306_write_data_cb)(struct ssd1306_ctx *ctx,
uint8_t data);
struct ssd1306_ctx {
/**
* User supplied callback that sends a command to the SSD1306.
*/
ssd1306_send_cmd_cb send_cmd;
/**
* User supplied callback that writes data to the SSD1306's memory.
*/
ssd1306_write_data_cb write_data;
/**
* Custom data that a user might want available in their supplied callbacks.
*/
void *user_ctx;
...
};
As mentioned in option #2, you will pass
in an instance of struct ssd1306_ctx *
to all library functions. The struct's
ssd1306_ctx::send_cmd
and ssd1306_ctx::write_data
fields are function pointers to the platform
dependent I/O.
libopencm3
provides a template repository for one to start their projects off
of. It is from that template that I built this
example
repository. It provides a complete and working example to start you off.
libSSD1306
bundles an 8x8 font that contains all printable, ascii characters.
The embedded font is a slightly modified version of
font8x8_basic.h. The original font is
released in the public domain. The modified font is generated by
./extra/rotate.c
and printed to stdout
. You should redirect (>
) or append
(>>
) the output of ./extra/rotate.c
to the file of your of your choice.
All of the modifications done to the original font are listed in
./src/font.c
. However, the most important one to note is the reason why
./extra/rotate.c
is necessary in the first place.
font8x8_basic
encodes each character row-wise and in reverse.
By row-wise, I mean that each element n
of the array that encodes a character
corresponds to row n
within the display matrix.
By reverse, I mean that the least significant bit of a row in a character's encoded data corresponds to the most significant bit of its corresponding row in the displayed matrix.
For example, let's take a look at the data for the character 9
.
The first row of the encoded data for 9
is 0x1E
. 0x1E
in binary is
0001 1110
. However, since the data is encoded in reverse, the matrix should
display it like 0111 1000
.
Let's see how the SSD1306
displays the character 9
using the original
encoding.
Since the 9
is encoded row-wise and the SSD1306
expects data to be encoded
column-wise, the 9
is rotated -90 degrees.
./extra/rotate.c
already encodes the data the way the SSD1306
expects it.
Let's change which bit we are extracting when rotating the font so that we
start extracting the most significant bit first, as opposed to the least
significant bit first. You'll see why in just a second.
diff --git a/extra/rotate.c b/extra/rotate.c
index cecbb74..f61b9de 100644
--- a/extra/rotate.c
+++ b/extra/rotate.c
@@ -78,7 +78,7 @@ rotate_8x8_bit_plus_90(const uint8_t *original, uint8_t *output)
for (int j = FONT_WIDTH - 1; j >= 0; j--) {
uint8_t current_val = original[j];
- uint8_t bit = extract_bit(current_val, i);
+ uint8_t bit = extract_bit(current_val, FONT_WIDTH - i);
tmp |= (bit << j);
}
As you can see, the 9
is now rotated correctly, but it's flipped. Since the
characters were originally encoded in reverse, we need to start extracting the
least significant bit first. To reflect this change in ./extra/rotate.c
,
simply undo the previous change.
That's more like it!