Full ASCII Character Font for SSD1306 with 08M2 (and above)

AllyCat

Senior Member
Hi,

The program below was devised to meet the challenge of using an 08M2 to implement a full (96) ASCII characters set for bit-mapped I2C displays such as the SSD1306. The limitations of the 08M2 largely defined the architecture, but the solution is quite efficient and may be equally useful for the larger PICaxe chips. The program was intended as a "core" or "preamble" for further development, to fit within the forum's 10,000 character limit, per post; but the forum software thinks otherwise, so it must be split over two posts. The code size is about 1400 bytes, so there is space for around another 500 bytes, even with an 08M2; or some/most of the code might be removed in a final application.

The smallest practical font uses 5 x 7 pixels characters, fitted within a 6 x 8 character cell. Often the eighth pixels are used for "descenders" (lower-case: g, j, p, q, y) or for an an underline/cursor. The SSD1306 organises the pixel bytes in columns, so 5 bytes are sufficient for each character, or 480 for a normal 96 (displayable) ASCII character set. This fits neatly into the Table memory of the larger M2 PICaxes, but the 08M2 has no Table memory, its EEPROM is only 256 bytes and can be better-used for other purposes, or left empty (as the bytes are "borrowed" from the program memory space in the 08M2). Therefore, it appears that the only space available for the character font storage is the Program memory itself.

The LOOKUP and POKE commands are slow and inefficient, but embedding the data directly into specific HI2COUT commands seems both relatively fast and compact. This is partially because the original PICaxe Basic program "tokens" were optimised for low-valued bytes (particularly 0 and 1) to use less than a byte of space, so "narrow" characters and punctuation are stored particularly efficiently. An issue is that the program needs to select (and execute) one from at least 96 different instructions (implemented as subroutines) for each character to be displayed. The IF .. THEN and SELECT .. CASE structures are rather slow and inefficient, but the ON .. GOTO command is about 5 times faster. However, it does still use a "list" of sequential tests (not a direct index) so the later labels in each instruction are executed with more delay. The program uses a two-stage "tree", first with 8 branches, each then splitting into 16 sub-branches. It's not worthwhile adding a third level to split the 16 into 4 x 4, because each command has an inherent "overhead" and the calculation of the additional "index" takes a significant time. Actually, since the M2s don't have any specific "shift" (<< or >>) operations, there is little advantage in dividing by binary (power) numbers, so 10 branches with 10 sub-branches might be marginally faster, on average. However, the sub-branch calculation can use an AND operator rather than an // (which is slower), and it's more sensible to retain the binary structure of the ASCII character set.

A disadvantage of embedding the data directly within the HI2COUT command is that the program cannot itself read the data. This prevents any direct processing of the font such as inversion (white/black or flip/mirror/rotation), double-height/width, or even adding an underline/cursor. Unfortunately, the SSD1306 doesn't support the reading back of data (via the I2C bus), but we can send the data to "another" memory. Therefore, all the HI2COUT instructions don't use a "hard-wired" address/register value but a Word Variable. In association with the appropriate I2CSETUP command this means that the data can be sent to almost any I2C device with either register (I2CBYTE) , RAM, or full (E)EPROM (I2CWORD) addressing. In particular, some Real Time Clock modules include a small amount of (battery-backed) RAM, which the program can write and read single characters "on the fly", without "wearing it out". Or, the overall system may include sufficient EEPROM to store the character font(s) either "permanently" or using a circular buffer to "level" the wear over an acceptable time period. It may even be worthwhile to add an external I2C serial EEPROM for "pre-prepared" fonts such as double/quadruple height, higher resolution or reversed characters, etc., with practically unlimited text / menu storage.

Thus, the program is structured with facilities to also send the font data to RAM or EEPROM and then read it back either immediately or at any later time (for the case of EEPROM or NVRAM). The bytes may be close-packed to suit smaller memories (or spaced for easier address calculation), but are arranged to not cross any 16-byte-block boundaries, which exist in some EEPROMS. 64-byte "page" blocks are more common now, but wouldn't accommodate any more characters (in both cases 60 bytes = 12 characters). The program then includes a "double height" algorithm, sending individual characters to either the OLED screen or back to another region of external memory. Simply doubling the height of the (7 x 5) characters makes them rather "tall and thin", but doubling also their width would reduce the displayed rows to only 10 characters each, which may be rather restricting. However, increasing the inter-character gap to 3 pixels (i.e. to a 16 x 8 character cell) gives a pleasing result with a "standard" format of 16 x 2 characters, compared with the original 20 (or strictly 21) x 4 character cells. For Large "High Quality" characters, a "Character Rounding" (or diagonal interpolation) routine has also been developed, to fill a 16 x 12 pixel character cell from the same 7 x 5 data (on-the-fly if necessary) and may be posted soon.

So here is the core program with the complete "tree" subroutine to be included in the subsequent post. A sample SSD1306 initialisation routine is included for completeness, but is not intended to be a comprehensive setup. The program has already been extended to include a RTC / Day / Date / Temperature facilities, complete with monitoring of the battery voltage and processor loading (utilisation) percentage, which gives an acceptable update rate even with a 4 MHz clock rate (around 60 characters / second). I plan to post more details either here or elsewhere in due course.
Code:
; Full ASCII character set for SSD1306 using an 08M2
; AllyCat, May 2020
#picaxe 08m2
;#no_data                    ; After initial load
;#define STOREDH            ; Else send to OLED
#define COMPACT                ; Fit 96 characters into 2 pages (< 512 bytes)

symbol PULLUPS = %1100        ; %1100 for 08M2 ; %11000 for 14M2    ; %10100000 For 20M2
symbol I2CSPEED = I2CFAST
symbol WRITEDELAY = 5        ; ms at 4 MHz
symbol DHOFFSET = $200        ; Start with Double-Height character 32 in page $400 (16 bytes/char)
symbol tempb = b1
symbol tempw = w1
symbol addr = w2            ; Word needed for external EEPROM address
symbol col = b6
symbol row = b7
symbol index = b8
symbol char = b9
symbol subchar = b10
symbol source = b11
symbol dest = b12
symbol EPROMSAD = $A0        ; Slave ADdress
symbol OLEDSAD = $78
data 0,(0,3,12,15,48,51,60,63,$C0,$C3,$CC,$CF,$F0,$F3,$FC,$FF)        ; Lookup for vertical stretch
    pullup PULLUPS
    pause 1000
    hi2csetup i2cmaster,OLEDSAD,I2CSPEED,i2cbyte        ; For SSD1306 32 lines OLED display
    hi2cout 0,($AE,$D5,$80,$A8,$1F,$D3,0,$40,$8D,$14,$20,0,$A1,$C8,$DA,2)
    hi2cout 0,($81,$8F,$D9,$F1,$DB,$40,$A4,$A6,$21,0,127,$22,0,3,$AF)
    addr = $40            ; OLED data
    tempb = 0            ; Address increment
    tempw = 32            ; Repetitions
    call CLEARMEM        ; Clear screen
    hi2cout 0,($21,1,126,$22,0,3)    ; Set screen area (21 x 4 chars)
; WRITE FONT TO OLED:
    for index = 32 to 127
        char = index
        call tree
        hi2cout addr,(0)        ; Inter-character gap
    next
    pause 5000
; COPY FONT TO EXTERNAL EEPROM/RAM:
    hi2csetup i2cmaster,EPROMSAD,I2CSPEED,i2cword
#ifdef COMPACT
symbol FWID = 16            ; Width (active pixels) of 3 chars
symbol MEMOFFSET = $FFE0    ; -32 = Char 32 starts at base of page 000
    tempw = 64                ; 2 pages = 512 bytes
    addr = 0
#else
symbol FWID = 24
symbol MEMOFFSET = 0        ; Char 32 starts at base of page 100
    tempw = 96                ; 3 Pages
    addr = $100   
#endif
;    call CLEARMEM            ; (* OPTIONAL) Pre-Clear Memory
    col = 0    : row = 1
    for index =  32 to 127
        hi2csetup i2cmaster,EPROMSAD,I2CSPEED,i2cword
        addr = index + MEMOFFSET * FWID / 3        ; Fit within 16 byte page boundaries (e.g. 24LC04)
        call STOREFONT    ; (* OPTIONAL after first use)
        bptr = 100
        hi2cin addr,(@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptr)        ; Read back
; CREATE DOUBLE-HEIGHT CHARACTER:
stretch:
        dest = 109        ; End of Second row
        do
            subchar = @bptr / 16
            char = @bptr AND 15
            read char,@bptrdec        ; Reload top cell
            read subchar,subchar
            poke dest,subchar        ; Second row
            dec dest
        loop until bptr = 99
        inc bptr
#ifdef STOREDH
        addr = index * 16 + DHOFFSET
#else
        addr = $40
        hi2csetup i2cmaster,OLEDSAD,I2CSPEED,i2cbyte
        dest = row + 1 : tempb = col + 7
        hi2cout $0,($21,col,tempb,$22,row,dest)
#endif
        hi2cout addr,(0,@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,0,0,_
            0,@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,0,0)
        col = col + 8 AND 127
    next
stop

CLEARMEM:    ; Clear Memory/screen
do
    hi2cout addr,(0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0)            ; Clear 16 byte cell
    addr = addr + tempb
    pause WRITEDELAY
    dec tempw
loop until tempw = 0
return

STOREFONT:
    char = index
    call tree
    pause WRITEDELAY        ; EEPROM write time
return

tree:        ; Send pixel bytes for ASCII "char" via I2C

; ** ADD SUBROUTINE FROM THE FOLLOWING POST **

return
IMG_3666 (2).JPG

Cheers, Alan.
 

AllyCat

Senior Member
Hi,

And here is the "tree" subroutine which sends the pixel bytes for any character referenced in "char" via I2C to a configured slave device (Display, RAM, EEPROM, etc.).

Code:
tree:        ; Send pixel bytes for ASCII "char" via I2C
    subchar = char AND 15
    char = char / 16        ; Character group
    on char goto udef,cons,syms,nums,ucus,ucxs,lcus,lcxs
udef:        ; User-definable characters (0 - 7) in upper RAM
    bptr = subchar AND 7 * 5 + 28        ; Up to location 67
    hi2cout addr,(@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc)
    return
cons:    ; User-definable characters (16 - 31) in EPROM
    char = subchar * 5 + 16                ; 16 chars to location 95
    subchar = char + 4                    ; End of cell
    for char = char to subchar
        read char,tempb
        hi2cout addr,(tempb)             ; 5 columns
    next
return

syms:
on subchar goto sym0,sym1,sym2,sym3,sym4,sym5,sym6,sym7,sym8,sym9,symA,symB,symC,symD,symE
symF: hi2cout addr,($20, $10, $08, $04, $02) : return ; /
sym0: hi2cout addr,($00, $00, $00, $00, $00) : return ; Sp
sym1: hi2cout addr,($00, $00, $2f, $00, $00) : return ; !
sym2: hi2cout addr,($00, $07, $00, $07, $00) : return ; "
sym3: hi2cout addr,($14, $7f, $14, $7f, $14) : return ; #
sym4: hi2cout addr,($24, $2a, $7f, $2a, $12) : return ; $
sym5: hi2cout addr,($23, $13, $08, $64, $62) : return ; %
sym6: hi2cout addr,($36, $49, $55, $22, $50) : return ; &
sym7: hi2cout addr,($00, $05, $03, $00, $00) : return ; '
sym8: hi2cout addr,($00, $1c, $22, $41, $00) : return ; (
sym9: hi2cout addr,($00, $41, $22, $1c, $00) : return ; )
symA: hi2cout addr,($14, $08, $3E, $08, $14) : return ; *
symB: hi2cout addr,($08, $08, $3E, $08, $08) : return ; +
symC: hi2cout addr,($00, $00, $50, $30, $00) : return ; ,
symD: hi2cout addr,($08, $08, $08, $08, $08) : return ; -
symE: hi2cout addr,($00, $60, $60, $00, $00) : return ; .
nums:
on subchar goto num0,num1,num2,num3,num4,num5,num6,num7,num8,num9,numA,numB,numC,numD,numE
numF: hi2cout addr,($02, $01, $51, $09, $06) : return ; ?
num0: hi2cout addr,($3E, $41, $49, $41, $3E) : return ; 0
num1: hi2cout addr,($00, $42, $7F, $40, $00) : return ; 1
num2: hi2cout addr,($42, $61, $51, $49, $46) : return ; 2
num3: hi2cout addr,($21, $41, $45, $4B, $31) : return ; 3
num4: hi2cout addr,($18, $14, $12, $7F, $10) : return ; 4
num5: hi2cout addr,($27, $45, $45, $45, $39) : return ; 5
num6: hi2cout addr,($3C, $4A, $49, $49, $30) : return ; 6
num7: hi2cout addr,($01, $71, $09, $05, $03) : return ; 7
num8: hi2cout addr,($36, $49, $49, $49, $36) : return ; 8
num9: hi2cout addr,($06, $49, $49, $29, $1E) : return ; 9
numA: hi2cout addr,($00, $36, $36, $00, $00) : return ; :
numB: hi2cout addr,($00, $56, $36, $00, $00) : return ; ;
numC: hi2cout addr,($08, $14, $22, $41, $00) : return ; <
numD: hi2cout addr,($14, $14, $14, $14, $14) : return ; =
numE: hi2cout addr,($41, $22, $14, $08, $00) : return ; >
ucus:
on subchar goto ucu0,ucu1,ucu2,ucu3,ucu4,ucu5,ucu6,ucu7,ucu8,ucu9,ucuA,ucuB,ucuC,ucuD,ucuE
ucuF: hi2cout addr,($3E, $41, $41, $41, $3E) : return ; O
ucu0: hi2cout addr,($32, $49, $79, $41, $3E) : return ; @ 
ucu1: hi2cout addr,($7E, $11, $11, $11, $7E) : return ; A
ucu2: hi2cout addr,($7F, $49, $49, $49, $36) : return ; B
ucu3: hi2cout addr,($3E, $41, $41, $41, $22) : return ; C
ucu4: hi2cout addr,($7F, $41, $41, $22, $1C) : return ; D
ucu5: hi2cout addr,($7F, $49, $49, $49, $41) : return ; E
ucu6: hi2cout addr,($7F, $09, $09, $09, $01) : return ; F
ucu7: hi2cout addr,($3E, $41, $49, $49, $7A) : return ; G
ucu8: hi2cout addr,($7F, $08, $08, $08, $7F) : return ; H
ucu9: hi2cout addr,($00, $41, $7F, $41, $00) : return ; I
ucuA: hi2cout addr,($20, $40, $41, $3F, $01) : return ; J
ucuB: hi2cout addr,($7F, $08, $14, $22, $41) : return ; K
ucuC: hi2cout addr,($7F, $40, $40, $40, $40) : return ; L
ucuD: hi2cout addr,($7F, $02, $0C, $02, $7F) : return ; M
ucuE: hi2cout addr,($7F, $04, $08, $10, $7F) : return ; N
ucxs:
on subchar goto ucx0,ucx1,ucx2,ucx3,ucx4,ucx5,ucx6,ucx7,ucx8,ucx9,ucxA,ucxB,ucxC,ucxD,ucxE
ucxF: hi2cout addr,($40, $40, $40, $40, $40) : return ; _
ucx0: hi2cout addr,($7F, $09, $09, $09, $06) : return ; P
ucx1: hi2cout addr,($3E, $41, $51, $21, $5E) : return ; Q
ucx2: hi2cout addr,($7F, $09, $19, $29, $46) : return ; R
ucx3: hi2cout addr,($46, $49, $49, $49, $31) : return ; S
ucx4: hi2cout addr,($01, $01, $7F, $01, $01) : return ; T
ucx5: hi2cout addr,($3F, $40, $40, $40, $3F) : return ; U
ucx6: hi2cout addr,($1F, $20, $40, $20, $1F) : return ; V
ucx7: hi2cout addr,($3F, $40, $38, $40, $3F) : return ; W
ucx8: hi2cout addr,($63, $14, $08, $14, $63) : return ; X
ucx9: hi2cout addr,($07, $08, $70, $08, $07) : return ; Y
ucxA: hi2cout addr,($61, $51, $49, $45, $43) : return ; Z
ucxB: hi2cout addr,($00, $7F, $41, $41, $00) : return ; [
ucxC: hi2cout addr,($02, $04, $08, $10, $20) : return ; \ 
ucxD: hi2cout addr,($00, $41, $41, $7F, $00) : return ; ]
ucxE: hi2cout addr,($04, $02, $01, $02, $04) : return ; ^
lcus:
on subchar goto lcu0,lcu1,lcu2,lcu3,lcu4,lcu5,lcu6,lcu7,lcu8,lcu9,lcuA,lcuB,lcuC,lcuD,lcuE
lcuF: hi2cout addr,($38, $44, $44, $44, $38) : return ; o
lcu0: hi2cout addr,($00, $01, $02, $04, $00) : return ; `
lcu1: hi2cout addr,($20, $54, $54, $54, $78) : return ; a
lcu2: hi2cout addr,($7F, $48, $44, $44, $38) : return ; b
lcu3: hi2cout addr,($38, $44, $44, $44, $20) : return ; c
lcu4: hi2cout addr,($38, $44, $44, $48, $7F) : return ; d
lcu5: hi2cout addr,($38, $54, $54, $54, $18) : return ; e
lcu6: hi2cout addr,($08, $7E, $09, $01, $02) : return ; f
lcu7: hi2cout addr,($18, $A4, $A4, $A4, $7C) : return ; g
lcu8: hi2cout addr,($7F, $08, $04, $04, $78) : return ; h
lcu9: hi2cout addr,($00, $48, $7A, $40, $00) : return ; i
lcuA: hi2cout addr,($40, $80, $88, $7A, $00) : return ; j
lcuB: hi2cout addr,($7F, $10, $28, $44, $00) : return ; k
lcuC: hi2cout addr,($00, $41, $7F, $40, $00) : return ; l
lcuD: hi2cout addr,($7C, $04, $18, $04, $78) : return ; m
lcuE: hi2cout addr,($7C, $08, $04, $04, $78) : return ; n
lcxs:
on subchar goto lcx0,lcx1,lcx2,lcx3,lcx4,lcx5,lcx6,lcx7,lcx8,lcx9,lcxA,lcxB,lcxC,lcxD,lcxE
lcxF: hi2cout addr,($08, $1C, $2A, $08, $08) : return ; <--   (arrow)
lcx0: hi2cout addr,($FC, $24, $24, $24, $18) : return ; p
lcx1: hi2cout addr,($18, $24, $24, $28, $FC) : return ; q
lcx2: hi2cout addr,($7C, $08, $04, $04, $08) : return ; r
lcx3: hi2cout addr,($48, $54, $54, $54, $20) : return ; s
lcx4: hi2cout addr,($04, $3F, $44, $40, $20) : return ; t
lcx5: hi2cout addr,($3C, $40, $40, $20, $7C) : return ; u
lcx6: hi2cout addr,($1C, $20, $40, $20, $1C) : return ; v
lcx7: hi2cout addr,($3C, $40, $30, $40, $3C) : return ; w
lcx8: hi2cout addr,($44, $28, $10, $28, $44) : return ; x
lcx9: hi2cout addr,($1C, $A0, $A0, $A0, $7C) : return ; y
lcxA: hi2cout addr,($44, $64, $54, $4C, $44) : return ; z
lcxB: hi2cout addr,($00, $08, $36, $41, $00) : return ; {
lcxC: hi2cout addr,($00, $00, $7F, $00, $00) : return ; |
lcxD: hi2cout addr,($00, $41, $36, $08, $00) : return ; }
lcxE: hi2cout addr,($08, $08, $2A, $1C, $08) : return ; --> (arrow)
ADDENDUM: Additional Program Code for "Character Rounding" the font (to 16 x 12 Pixel cells) can be found HERE.

Cheers, Alan.
 
Last edited:

julianE

Senior Member
Alan,

I would like to use the OLED to just display large numbers no need for text. I'm thinking by simplifying it to just display numbers it would all fit in an 08M2 without having to use an external EEPROM. Would that be doable? I have a long way to go to understanding how to make it all work, been looking at a spreadsheet for designing fonts. If it's not too much of an ask do you have an example that you can post of just displaying a single large number. Thanks in advance.
 

AllyCat

Senior Member
Hi,

Yes it's definitely possible. Have you looked at the "HERE" link addendum at the foot of my post #2 above? That shows Double-Height characters, also with Double-Width achieved automatically with a "Character Rounding" (or interpolation) Algorithm that I've described elsewhere on the forum. The program listed above has the option of sending the character font (pixel bytes) to an external EEPROM, but the HI2COUT commands (to EEPROM or OLED) could be modified to send the data (bytes) to the PE Terminal (for example). Copy that data to create a file to build subsequent DATA or I2COUT commands to generate larger characters. If you only want to create (new) character fonts, then you may be able to use the Simulator rather than keep programming a target PICaxe.

I did also create a Quad-Height font using the same methods, but I don't think I actually posted that on the forum, but here's the proof that it worked!

QuadHeightDigits.png


In principle you could just replace the pixel data for say A,B,C,D,E,F,G and H with the Quad-Height, Double-Width Character pixel bytes for "0", etc., but you would need to "print" "AB" in Row 1 , "CD" in Row 2 .. and "GH" in Row 4. In practice it's better to set up a "window", say 10 or 12 pixels wide by 4 rows high (positioned by Row and Column parameters), then you can print "ABCDEFGH" directly, and/or create larger HI2COUT .... commands (with up to 48 pixel-bytes). The following is NOT a full program listing, but a test subroutine that I used, I believe to expand Double-Height into Quadruple-Height characters for the photo above. The commands may give an indication of how it worked:
Code:
data 0,(0,3,12,15,48,51,60,63,$C0,$C3,$CC,$CF,$F0,$F3,$FC,$FF)        ; For vertical stretching
quadheight: 
        call rounded                ; Convert 5 x 8 to 10 x 16 pixels (or might be fetched directly from pre-prepared EEPROM)
        addr = $40                         ;  OLED mode
        row = 0
        hi2csetup i2cmaster,OLEDSAD,I2CSPEED,i2cbyte            ; Select the OLED
        tempb = col + 11                                       ; Move to next character position
        hi2cout $0,($21,col,tempb, $22,0,3)                     ; Select cells (10 x 32) from first row
        bptr = 100                                                      ; Start of first row
        dest = 110                                                      ; End of first row (post inc)
        do                                                                    ; Two passes
            do                                                                ; 10 columns/char (+space)
                char = @bptr AND 15                              ; Four LSBs (Top row)            
                subchar = @bptr / 16                          ; Four MSBs
                read subchar,@bptrinc                         ; Reload Upper half of cell
                read char,char                                     ; Upper cell pixels
                hi2cout addr,(char)                           ; Transmit to avoid storing (need pause if stored)
            loop until bptr = dest                  
            bptr = bptr - 10                                     ; Back to start of row
            hi2cout addr,(0,0,@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,_
            @bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,0,0)    ; Second row (1st zeros for top row)
            dest = 120                            
        loop until bptr = 120                                    ; Two passes
        col = col + 12                                       ; Move to end of character
return
Cheers, Alan.
 

julianE

Senior Member
Thank you very much, very kind of you. I missed the HERE section of your post, will give it all a try.
I also found a demo code by Hemi345 that has large characters and have had some success deciphering it.

all the best.
 

WhiteSpace

Well-known member
@julianE here is the final version of the code from this thread: https://picaxeforum.co.uk/threads/ds18b20-displaying-on-128-x-32-ssd1306-oled-with-08m2.32048/, using an 08m2 to drive a SSD1306, showing (double height) numbers 0 to 9, plus degree symbol, decimal point and C. It's an unnecessarily clunky implementation, because I couldn't understand @AllyCat's very patient and careful explanation of how to do rounding with the output from the ds18b20, but it has the advantage of including code for all of the digits, and quite a lot of commentary in the program that may give you some pointers. You may also find "OLED driver v.4" useful. It's intended to show battery voltage and motor current, running on a 28x2, so it uses scratchpad, but it similarly includes all of the digits, and also moves the start of the next character closer where the previous character is a decimal point or a space, which looks a bit better. The code was work in progress and has been superseded, so it doesn't necessarily work in full, but the main principles of displaying characters were working very well.

Please feel free to ask any questions.
 

Attachments

julianE

Senior Member
@WhiteSpace , very generous of you to upload your samples. I'm finally getting a handle of the OLED and how to generate fonts and so on. I like the elegant setup procedure of paraglider_nut Oled and have been trying to make sense of it and integrate it with the larger fonts instead of a 4 line display, it's a much shorter setup and does all I need. I worked at making a large 10x16 font, gave up and am making a plain 7 segment for that classic look and it's so much simpler.

all the best.
 

AllyCat

Senior Member
Hi,
Code:
tree:        ; Send pixel bytes for ASCII "char" via I2C
    subchar = char AND 15
    char = char / 16        ; Character group
    on char goto udef,cons,syms,nums,ucus,ucxs,lcus,lcxs
udef:        ; User-definable characters (0 - 7) in upper RAM
    bptr = subchar AND 7 * 5 + 28        ; Up to location 67
    hi2cout addr,(@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc)
    return
cons:    ; User-definable characters (16 - 31) in EPROM
    char = subchar * 5 + 16                ; 16 chars to location 95
    subchar = char + 4                    ; End of cell
    for char = char to subchar
        read char,tempb
        hi2cout addr,(tempb)             ; 5 columns
    next
return
syms:
on subchar goto sym0,sym1,sym2,sym3,sym4,sym5,sym6,sym7,sym8,sym9,symA,symB,symC,symD,symE
symF: hi2cout addr,($20, $10, $08, $04, $02) : return ; /            .... etc...
A year now since my original post, with some responses, so it's time for an update, particularly concerning the "tree" subroutine method shown in post #2 above. Not only is it fast (for a PICaxe), but it's also flexible, as shown above, pulling data almost transparently (to the user) from RAM, {TABLE} EPROM and/or Program Memory.

The fastest method for a PICaxe to write data bytes to the OLED display is from pre-prepared HI2COUT command strings, either directly within the Program Memory or via a string of @BPTRINCs from the RAM/Register Memory. Initially, I considered the area of RAM accessed by "udef:" only as a method to allow the User (programmer) to add a few "special" characters, overlooking that it can be (also) a useful area of "Scratchpad" memory, which can be used "On the Fly" during normal operation. The nominally allocated 40 bytes can store 8 "Standard" (5x8 pixels) characters, 4 "Double Height" (5x16), 2 "Large" (10x16) or one "Quad-Height" (10x32) character, but could be extended further if both User-Defined and Scratchpad character facilities were required at the same time.

So the question arises: How Best (i.e. Fastest) to write data into this RAM? The perhaps surprising answer is "From an HI2CIN command", i.e. NOT from any of the PICaxe's internal memory. Therefore, particularly for the 08M2, I've considered the use of an External serial EEPROM such as the DIL8 packaged 24LC16, to provide a fast and potentially enormous amount of "font" (and text) storage. Since only 4 pins are "functionally active", its pins can be bent and linked to form a 4 (or 5) pin "SIL" (Single-In-Line) vertically-mounted package, taking up very little board space next to the PICaxe or the OLED's SDA and SCL pins.

However, this ignores that even the 08M2 may have around 240 bytes of "spare" EEPROM space which can store the pixels slightly more efficiently than the program memory (assuming that the related program code is already present). Also, the Larger M2s (including 14M2) have this EEPROM plus 512 bytes of TABLE memory, plus a further 512 bytes of Table and up to 2048 bytes of Program Memory in a second "Slot". The X2s also have Table Memory, a larger Program Memory and (except 20X2) more Slots.

Above, I did provide for the use of EEPROM/TABLE memory in the "cons:" section of code, but it's not ideal (i.e. "fastest"). It uses a program loop with individual HI2COUT (byte) instructions, so is potentially rather slow, and a Scratchpad "should" be faster. However, the obvious READ{TABLE} charpointer , @BPTRINC, @BPTRINC, @BPTRINC, .... structure is NOT particularly fast, because it's converted by an Internal Macro created by the Program Editor / Compiler (which needs to restore the value of "charpointer" after advancing it during execution). Thus, the rather clunky version that I will show below is faster and uses less Program bytes than this "indexed list". It could be made neater by using a Macro (in PE6), but I prefer to show code compatible with PE5 and AxePad when possible.

Since the Program now has direct access to the Pixel Data, we can add the facility for Double-Height characters, etc., which I had assumed would use Conditional Compilation and/or "Character Attribute" flags. But so far, only 96 (or up to 128) characters have been defined, therefore another 128 "processed" characters can be added, selected by the "char" byte alone. Of course that only adds one attribute type (e.g. Double Height) to the complete character set, but particularly with the 08M2, only some of the characters might be stored in EPROM. For example the "Numbers" and a few special characters could be assigned to separate areas of the character map, depending on the pixel-processing applied. Generally, as here, I would organise and process the characters in ASCII-partitioned groups of 16, but in principle the tree structure permits individual characters to be assigned arbitrarily, simply by adjusting each jump to its "subroutine" label.

Double-Width (alone) characters are easy to create but IMHO not very useful (4 or 8 rows of 10 characters), so my primary "enlarged" font is Double-Height. Only half the number of characters is then required to fill the screen, but the routine might be used as a preamble to further font enlargement, so speed is still quite critical. Therefore, I considered using variables b23 - b27 (of the M2 family) as part of the Scratchpad and "In-Line" code, which can be about twice as fast as a more conventional "indexed loop", but that uses three times more codespace (around 75 bytes versus 25) and precious RAM variables, so reluctantly I chose a more conservative version.

"Large" characters can be created by subsequently Doubling their Width by simple horizontal duplication, or with a "Character Rounding" (interpolation) algorithm to give smoother characters (at the expense of additional time and complexity). The Rounding routine (linked from #2 above) is about 50% slower, so again for my example code the simpler approach has been adopted. These enlarged Characters are written completely to Scratchpad cells (i.e. up to 4), giving an option for further expansion. However, for "Quadruple Height" (i.e. Double-Height Large) it seems better to send half of the (expanded) cell data directly to the display as it is created (i.e. a row at a time), rather than maintaining duplicate pointers to store the pairs of rows. Each of these characters now fills 8 basic cells, but still the overall code is starting to look rather "slow", so directly coded pixel-maps do have some merit, particularly if received via the I2C bus, if the memory space is available.

For single-row-high fonts, the characters can be simply sent sequentially to the display, but for larger fonts and/or if overall control of character positions on the screen is required (TABs, etc.), then it's necessary to set up a "window" to contain the present character pixels. This "cursor" position is defined by its top-left and bottom-right bytes' X and Y co-ordinates, which then need to be updated when each character is written. Since there might be 8 , 10 , 16 or 20/21 , etc., characters per row (selected either globally or contextually), the X-coordinate uses single-pixel units (i.e. 0 to 127 or 131), but the Y-coordinate needs to define only one of 8 rows in memory (of which 4 or 8 are displayed, depending on the OLED panel size). Combining the two coordinates into a single Word (i.e. HighByte : LowByte) is a "trick" to slightly reduce the code size (e.g. RowCol = $yxx or RowCol = RowCol + $yxx , etc.).

Since the spacing ("Gap" Pixels) between characters might be variable (e.g. depending on the font size), I considered defining a separate "Gap Window", but this seems an unnecessary complication. Not only is the HI2COUT .. {byte list} instruction fast, it is also quite compact (hardly more than one Program Byte for each Data Byte transmitted) so it can be used as a "Template" for the whole character cell, i.e. with the "background" pixels (normally zero) embedded into the list. Then this HI2COUT Template usually can follow directly on from the "Character Processing" (Sub-) Routine, without any further Jumps or Calls. Or, by putting any "Gap" code (and advancing the character coordinates) before the Template, it's usually possible to use the RETURN to jump directly back into the main program. This also permits another "trick", that to transmit two rows of pixels, it's possible to first CALL a single-row subroutine and then to "fall into" it, without the delays of a second CALL and RETURN.

Yet again, embedding the program code In-Line would take this post well above the 10000 character forum limit, so my demonstration code will follow in a subsequent post. Perhaps also with examples to show "Continuous Graphics" (i.e. Lines / Blocks with no inter-character Gaps), using the same character-cell-based structure. But for now, here is a sample data block for the 16 "numbers" characters in a format suitable for (or easy conversion to) PICaxe's EEPROM or TABLE memories.

Code:
data 0,(0,3,12,15,48,51,60,63,$C0,$C3,$CC,$CF,$F0,$F3,$FC,$FF)        ; Lookup Table for vertical stretching
;  EEPROM Address 16
data 16,($02, $01, $51, $09, $06) ; ?                ; Or can be TABLE with most M2 and X2s
data 21,($3E, $41, $49, $41, $3E) ; 0                ; Address numbers are optional. May be omitted for data flexibilty 
data 26,($00, $42, $7F, $40, $00) ; 1
data 31,($42, $61, $51, $49, $46) ; 2
data 36,($21, $41, $45, $4B, $31) ; 3
data 41,($18, $14, $12, $7F, $10) ; 4
data 46,($27, $45, $45, $45, $39) ; 5
data 51,($3C, $4A, $49, $49, $30) ; 6
data 56,($01, $71, $09, $05, $03) ; 7
data 61,($36, $49, $49, $49, $36) ; 8
data 66,($06, $49, $49, $29, $1E) ; 9
data 71,($00, $36, $36, $00, $00) ; :
data 76,($00, $56, $36, $00, $00) ; ;
data 81,($08, $14, $22, $41, $00) ; <
data 86,($14, $14, $14, $14, $14) ; =
data 91,($41, $22, $14, $08, $00) ; >
Cheers, Alan.
 

AllyCat

Senior Member
Hi,

Looking back to this thread, I see that I didn't post the promised "demonstration code". This was perhaps because I "discovered" the SH1106 variation of the SSD1306 driver IC, described in detail in this thread , which includes a demonstration of "normal sized" characters. Thus, the code that I had been developing for this thread needs some revision, which I'm not planning at the moment. Therefore, I'll post the version that I had been editing here, in case it is of any use.
Code:
tree:        ; Send pixel bytes for ASCII "char" via I2C
    subchar = char AND 15
    char = char / 16        ; Character group
    on char goto udef,NUMS;,syms,nums,ucus,ucxs,lcus,lcxs          ; ,NUMS,NUMS
nums:        ; Takes about 8ms to send each character 
    index = subchar - "0" * 5 + 16
    bptr = SPRAM
    read index,@bptrinc : inc index            ; Can be READTABLE for larger M2s and X2s
    read index,@bptrinc : inc index          
    read index,@bptrinc : inc index
    read index,@bptrinc : inc index
    read index,@bptr                                ; Total ~8ms @ 4MHz (code size 40 bytes)
    bptr = SPRAM  
    IF char <> NORM then show1                    ; Jump to send single-sized cell
DHT:         ; 12ms  (per cell)                       ; Double-Height
    cellend = rowcol + DHCELL  
    hi2cout $0,($21,col,colend,$22,row,rowend)        ; Set cell area (5 x 16 pix)  9 bytes
    w1 = @bptr * 16                                    ; b3 = High nibble
    @bptr = @bptr AND 15                            ; Low nibble
    read @bptr,@bptr                                ; Store in Upper row
    bptr = bptr + CHARWID                            ; To lower row
    read b3,@bptrinc                                ; Store in Lower row
    bptr =  bptr - CHARWID                            ; Back to upper row
    if bptr => SPC2 then DHT                        ; Loop ~5ms * 5 = 25ms ie 12ms/cell (27 bytes)
        bptr = SPRAM  
    IF char <> DBLHT then DWID
    call show1                                        ; Then fall-through for the second row
show1:
    hi2cout addr,(@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,0)
    return
DWID:        ; 8ms
    cellend = rowcol + DWCELL                        ; 7 bytes  
    hi2cout $0,($21,col,colend,$22,row,rowend)        ; Set cell area (10 x 16 pix)  9 bytes
    bptr = 47                                        ; Last byte of lower row
    tempb = 37                        ; Last byte of narrow character
dwlp:      
    peek tempb,@bptrdec
    peek tempb,@bptrdec  
    dec tempb
    if bptr > 27 then dwlp        ; 1200 = 3000 * 10 = 30ms for 4 cells ie 8ms/cell
    bptr = 28
    IF char = LRGE then showLGE
    tempb = 38
    call quadht
    tempb = 48
QUADHT:   ; 15ms/cell                       ; ~40 bytes  Total 50ms/char*80 = 4s @ 4MHz
    cellend = rowcol + QHCELL                                ; 7 bytes  
    hi2cout $0,($21,col,colend,$22,row,rowend)        ; Set cell area (10 x 32 pix)  9 bytes
    w1 = @bptr * 16                                    ; b3 = high nibble (lower row)
    @bptr = @bptr AND 15                ; Low nibble (upper row)
    read @bptr,b2
    hi2cout addr,(b2)                    ; Send upper row character
    read b3,@bptrinc                    ; Store lower row character
    if bptr < tempb then quadht            ; 1200 = 5400 * 10 = 54ms
SHOWLGE:
    bptr = bptr - 10                    ; Back to start of (next) row
    hi2cout addr,(@bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc,_
        @bptrinc,@bptrinc,@bptrinc,@bptrinc,@bptrinc)                ; Send lower row 3ms (14 bytes)
    return                                ; 2200                ;~120ms/8 = 15ms/cell
Cheers, Alan.
 

mortifyu

New Member
Hi Alan,

Just having a play around with a few OLED's. Once again ""THANK YOU!!!"", you are the man.

Your posts on this forum are absolutely priceless. Words cannot express how grateful I am personally of your contributions in particular.

If ever you come to Brisbane, Australia, look me up, you're welcome at my camp fire anytime, for as long as you like!

Although, be pre-warned, you WILL be driven up the wall by me drawing as much knowledge from your brain as I can possibly acquire. 😁


Regards,
Mort.
 
Top