Communicate with the 128x32 monochrome OLED display

published: 10 October 2020 / updated 10 October 2020

Lire cette page en français

 

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:

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 at  addr 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.