"Character Rounding" for Double-Sized characters on bitmapped displays such as SSD1306


Senior Member

PICaxe is not well-suited to displaying characters on bitmapped displays because it is relatively slow and has limited memory storage for the character fonts. However, the tiny OLED displays which use the SSD1306 have an appeal because they are small and can run on 3.3. volts with low power. The display is organised in rows of pixels one byte (8 bits) high, so it is most convenient to use a font 8 bits high by 5 bits (plus a space bit) wide. These characters are rather small so an obvious solution is to double their height and/or width by duplicating pixels vertically and/or horizontally, thus avoiding the need for a larger memory.

Doubling the width is very easy, by just transmitting each byte to the display twice, but double-height is more difficult because the bytes must be increased to words with the bits interleaved within themselves. Numerous methods, some quite sophisticated, have been discussed but, at least for a PICaxe, a simple lookup table method appears to be the most efficient, as shown in post #139 of this long thread. For quadruple-height characters, the "mathematical" method needs 32-bit words and the simple "bit-swapping" would take twice as long, so it seems the benefits of the lookup method actually increase.

Some of the doubled characters can look rather "blocky", but there is another defect when using diagonal lines that are (effectively) only one pixel wide. Consider a square of 100 unit-sized pixels : A horizontal or vertical line contains 10 pixels and is 10 units long, whilst a diagonal also contains 10 pixels but is almost 15 units long (i.e. multiplied by the squatre root of 2), so it looks less bright (photos below). Horizontal and vertical lines 2 pixels wide would contain 20 pixels, but if the width of diagonals can be increased to 3 pixels (horizontally and/or vertically), they can contain 30 pixels, giving a more uniform brightness (along their greater length). Furthermore, if the "edge" pixels of the diagonal line are suitably staggered then the line has a smoother appearance. Such methods were developed for the Teletext (TV) character displays in the 1980s, but they used primarily a hardware method.

The principle of the Teletext "character rounding" (or diagonal smoothing) system can be explained by considering a square of 9 pixels (i.e. 3 x 3). The centre square should be set as a "1" if two opposite corners are set to "1" and the other two to "0". This occurs in two cases of the 16 possibilities (i.e. all permutations of the four corner pixels) which could be determined with a lookup table and also gives us a "clue" that the algorithm should implement a 1 in 8 selection. But collecting together the 4 pixel bits into a single nibble (to access the table) would be time-consuming and calculates only one pixel at a time. Ideally, we want to calculate a full byte (or even word) of individual bits in parallel,.

Testing for the equality of two diagonal corners seems the obvious method, but is not efficient; it needs an AND of the pixels (to detect two 1s), a NOR (for two 0s) and then an OR logic operation. This must be done for each diagonal and finally their inequality verified. Much better is to test for the the inequality of adjacent (horizontal or vertical) corner pixels, which can be done with a simple EXCLUSIVE-OR function. Furthermore, the two pairs of horizontally-separated pixels can be compared in a single XOR operation, then two vertical inequalities tested by simple shift and XOR operations, and finally an AND. Shifting (in an M2) can use division or multiplication, but multiplication (or adding to itself, if a single bit-shift is required) is slightly faster.

The algorithm coded below is for pixels (stored in Words) which have already been height-doubled. This implies that adjacent pairs of bits are identical, which may be less efficient, but it is easier to visualise the half- (source) pixel offset implicit in the rounding or smoothing technique. Note that pixels are also added to the source words, not just to the newly-calculated intermediate words. Ultimately it may be worthwhile to adapt the algorithm to use individual (not paired) bits within the words, i.e. before the vertical stretching process. This could be particularly beneficial for quadruple-height characters, but also gives the possibility of processing two pairs of source columns within a single word. In principle, this requires only two passes per character, because the outer verticals do not have pixels added. so only 4 internal bytes (for double-height) need to be calculated before stretching.

The program takes character bytes indirectly from the "high" area of RAM, restores the processed bytes to an adjacent area and shows the resulting character via SERTXD commands. It avoids the use of @BPTR {INC} because this may be dedicated to the interrupt routine for a character-receiving buffer. Note that some care is needed in the design of the font: The algorithm (and similar ones) has a "flaw" that a hollow diamond (i.e. lit pixels to the N, S, E and W of an unlit pixel) becomes filled at its centre.
; Character Rounding algorithm for double-height/width column-scanned 5x7 characters (12 x 16 cell)
; This version assumes height is already doubled (i.e. word data). AllyCat March 2020.
#picaxe 08M2

symbol CHARST = 100                                ; First (blank) column of character
symbol CHAREND = 110                                ; Last (blank) column of character
    call loadpix                                        ; Load the source character pixels into upper (indirect) RAM
symbol source = b19                                ; Source pointer
symbol dest = b20                                    ; Destination pointer
    dest = 80                                        ; Set first Destination 

    for source = CHARST to CHAREND step 2    ; 6 columns of Word values
        peek source, b2,b3,b4,b5                 ; Get two adjacent input columns

        w0 = w1 xor w2                                ; W1 and W2 contain source pixels of adjacent columns
        w0 = w0 * 4 and w0                        ; Require both top and bottom corner squares to be different
        w0 = w2 * 4 xor w2 and w0 / 2         ; Require also upper and lower RHS corner pixels to be different
        w2 = w2 or w0                                ; Add extra pixel(s) to RH column
        w0 = w1 or w0                                ; Add extra pixel(s) to intermediate column (algorithm = 25 bytes)

        poke dest, b0,b1,b4,b5                    ; Write the middle and RHS columns to memory
        dest = dest + 4                            ; Advance pointer
        call show                                    ; Intermediate column in W0
        w0 = w2                                        ; Copy RHS column
        call show
    next                                                ; Next pair of columns

    call show2            ; High byte
    b1 = b0                ; Load low bye and fall into show2

w0 = 0                                 : poke 100 , word w0    ; Poke of a WORD constant is a syntax error
w0 = %11111111111111     : poke 102 , word w0
w0 = %00000011000000     : poke 104 , word w0             ; "K"
w0 = %00001100110000     : poke 106 , word w0
w0 = %00110000001100     : poke 108 , word w0
w0 = %11000000000011     : poke 110 , word w0
w0 = 0                                  : poke 112 , word w0


Cheers, Alan.