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 ; <--
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)
Cheers, Alan.
 
Top