Communicate with the 128x32 monochrome OLED display
published: 10 October 2020 / updated 10 October 2020
Transmission to the SSD1306 OLED display
To test the examples in this article, you must first compile the contents of the file i2c-new.txt:
I2C interface management for FlashForth
For the rest, we will only need to transmit commands and data to the OLED display via the I2C bus. We won't need to recover any data.
As a reminder, the address of the OLED display on the I2C bus is $3c.
This is the address indicated by the bus test by executing i2c.detect
. /p>
Here is the brief description of a command frame:
- 8 bits slave address on 7 bits from b1 to b7
- 8 bits command / data selector b0 to b7
- $00 = orders
- $40 = data
- n x 8 bits commands or data
Written like that, it looks simple. In reality, when we analyze the different source codes available online, it is far from obvious. Here is a sample code C translates to FORTH:
: set.col.start ( end start --- ) 0 ssd.command \ Command stream $21 ssd.command \ Set display start line 0 ssd.command ssd.command \ Set column start address 0 ssd.command ssd.command \ Set column end address i2c.stop ;
This code works ...
But each command is transmitted in a frame. Word
ssd.command
passes the address, the command and makes a
end of frame. For a single command, no less than four bytes are transmitted.
While it is perfectly possible to transmit a group of commands in a single frame.
It is this transmission of commands or data by packet that we will detail.
Construction of a command or data frame
The idea is to store commands or data in frames, kind of like this:
create frame01 $3c c, \ SSD1306 address $00 c, \ code commands \ set of commands: $xx c, ..... $yy c,
If we run frame01
, we will have the address on the data stack
of the first byte stored in our frame. But we don't know the number of bytes
stored in this frame.
Here's how to store the number of bytes to transmit:
create displayON 3 c, \ number of following bytes $3c c, \ SSD1306 address $00 c, \ code commands $af c, \ DISPLAYON
If we run displayON
, we will have the address on the data stack
of the byte containing the number of bytes to be transmitted. We will automate this
creation of frames:
\ do nothing - default action for stream: : nothing ( ---) ; defer stream.action \ default action for stream: \ define a command or data stream for SSD1306 : stream: \ set nothing as execute action by default ['] nothing is stream.action create here \ leave current dictionnary pointer on stack 0 c, \ initial lenght data is 0 does> stream.action ; \ store ataddr length of datas compiled beetween \and here : ;stream ( addr-var len ---) dup 1+ here swap - \ calculate cdata length \ store c in first byte of word defined by stream: swap c! ;
To rewrite our definition of displayON
, we do:
stream: displayON $3c c, \ SSD1306 address $00 c, \ code commands $af c, \ DISPLAYON ;stream
The word stream:
has the same effect as create
The word ;stream
closes the frame. It resolves the number of commands or data
stored in the first byte of the frame created by stream:
At runtime, our word displayON
only drops the address
the first byte, the one containing the number of commands or data stored
in the frame.
Note that before compiling the word displayON
in the dictionary,
the word stream:
placed in the vectorized execution word stream.action
the word nothing
.
Focusing the frames
On crée ces deux mots:
\ get real addr2 and u length form addr1 : count ( addr1 --- addr2 u) dup c@ \ push real length swap 1+ swap \ push start address of datas ; \ used for debugging streams \ for use: \ ' disp.stream is stream.action : disp.stream ( stream-addr ---) count for c@+ . next drop ;
We will assign the execution code of the word disp.stream
in the word
vectorized execution word stream.action
' disp.stream is stream.action
From this point on, stream.action
will execute disp.stream
.
As a reminder, stream.action
is defined after does>
in the definition
fromstream:
We now execute displayON
:
hex
displayON
\ display: 3c 00 af
This allows us to debug the frames and check their content before transmission. This is particularly interesting if we use constants. Example:
$3c constant addrSSD1306 \ i2c device address \ control: $00 for commands \ $40 for datas $00 constant CTRL_COMMANDS $40 constant CTRL_DATAS stream: displayON addrSSD1306 c, \ SSD1306 address CTRL_COMMANDS c, \ code commands $af c, \ DISPLAYON ;stream
Sending frames on the I2C bus
Now that we know how to easily construct frames, let's see how to transmit their content on the I2C bus.
$3c constant addrSSD1306 \ i2c device address \ send stream of datas or commands to SSD1306 : i2c.stream.tx ( stream-addr ---) addrSSD1306 i2c.addr.write drop \ send SSD1306 address count \ fetch real addr and length of datas to send for c@+ i2c.tx \ send commands or datas next drop i2c.stop ;
This word i2c.stream.tx
is simply a variant of our
previous word disp.stream
. In the for..next loop,
instead of displaying a byte, it is transmitted on the I2C bus with the word
i2c.tx
.
To use this word i2c.stream.tx
, we will modify the word
vectorized execution stream.action:
' i2c.stream.tx is stream.action
We can criticize FORTH for its conciseness which partly affects the readability of the code.
I remind you that we can execute a lot of words directly from the terminal in interpreted mode, which C language does not allow.
While C ++ is an extremely powerful object-oriented language, it remains an extremely heavy machine with regard to the possibilities of focus in interactive in situ mode, i.e. directly on the ARDUINO card.
I intentionally heavier the FORTH code by using the word
stream.action
which is only needed for the debugging phase.
We see that by manipulating the vectorized execution words very simply, we can
very easily modify the behavior of the words which depend on it, here the
word displayON
. The C language cannot do that so easily!
SSD1306 OLED display initialization
Here is how to initialize the OLED display:
flash stream: disp.setup CTRL_COMMANDS c, $ae c, ( DISP_SLEEP ) $d5 c, ( SET_DISP_CLOCK ) $80 c, $a8 c, ( SET_MULTIPLEX_RATIO ) $3f c, $d3 c, ( SET_VERTICAL_OFFSET ) $00 c, $40 c, ( SET_DISP_START_LINE ) $8d c, ( CHARGE_PUMP_REGULATOR ) $14 c, ( CHARGE_PUMP_ON ) $20 c, ( MEM_ADDRESSING ) $00 c, $a0 c, ( SET_SEG_REMAP_0 ) $c0 c, ( SET_COM_SCAN_NORMAL ) $da c, ( SETCOMPINS ) $02 c, \ or $12 ??? $db c, ( SET_VCOM_DESELECT_LEVEL ) $40 c, $a4 c, ( RESUME_TO_RAM_CONTENT ) $a6 c, ( NORMALDISPLAY ) $af c, ( DISP_ON ) ;stream stream: disp.reset CTRL_COMMANDS c, $21 c, $00 c, $7f c, $22 c, $00 c, $07 c, ;stream ram : disp.init ( -- ) disp.setup disp.reset ; ' i2c.stream.tx is stream.action
The OLED display is initialized by typing in the terminal i2c.init disp.init
And the OLED screen activates:
In the next article we are going to see how to display text on this SSD1306 128x32 OLED screen.