Learn how to design characters using an image editor and display them on an LCD controlled by an EFM8 microcontroller.


A previous article explored using the SPI functionality of an EFM8 microcontroller to display a scrolling horizontal line on a 128-by-128-pixel LCD. In this article, we will use that same low-level interface to update the LCD display with all the uppercase letters of the alphabet; even better, you will learn techniques to efficiently design and display any character or symbol that can be adequately represented with 10 vertical pixels and 8 horizontal pixels.     


Previous Articles in This Series


Required Hardware/Software


Project Overview

The objective is to design and display all the uppercase letters on a 128-by-128-pixel LCD. The text is printed to the LCD screen in the typical word processor style: a cursor tracks the position where the next character will be displayed, and you simply “type” a new character by calling a function called InsertCharacter().

The font is a custom-designed fixed width (and fixed height) character set created with the techniques described in the next section. The fixed character size is 10 vertical pixels by 8 horizontal pixels (though the character is actually restricted to 9 by 7 pixels because a blank row and blank column are included to provide separation between adjacent characters). This size was chosen for a few different reasons. The LCD is 128 pixels wide, so 8 horizontal pixels ensures that an even number of characters will fit across the screen. We use 10 vertical pixels instead of 8 because the resulting thinner letters are somewhat more pleasing to the eye. These dimensions also make letters that are large enough to be easily legible yet small enough so that the LCD can display a meaningful number of characters at one time. Yet perhaps the most important reason is the following: the code for copying the pixel data from the character array to the LCD data buffer is much simpler with an 8-pixel width, because the bits representing the pixel states are aligned with byte boundaries. This means that we can properly update the LCD data using clean byte operations instead of awkward (and less efficient) bit operations in which the bits from one byte must be copied to part of one byte and part of another byte.


Designing Characters

Translating visual symbols into LCD-compatible data is a seriously nonintuitive process: who could possibly draw the letter G by assigning a series of hexadecimal values to a byte array? So we need a convenient way to visually design a character that translate this visual information into data bytes that can be incorporated into our microcontroller code and transferred to the LCD module. The process begins with image editing software; here we use Paint.NET, a powerful yet free image and photo editing application.

Get a blank canvas and resize it to 10 pixels high by 8 pixels wide, and zoom in all the way. Use the pencil tool to fill in individual pixels until the letter looks about right, then save the image as a grayscale .bmp file.

Now it is time for Scilab, which you can download here. Scilab is free numerical computation software similar to MATLAB. As with MATLAB, Scilab is useful for much more than crunching numbers. In this project we will use it to analyze the character images and convert them into C code that we can copy and paste into our EFM8 source files. In addition to Scilab itself, you will need to install the Image Processing Design Toolbox; this is easily accomplished via Scilab’s ATOMS module manager:

The most basic way of using Scilab is to type individual commands into the console window. Things get exciting, though, when you start combining and organizing these commands using the SciNotes text editor, which allows you to incorporate Scilab instructions into a coherent source-code file that executes like an application written in a high-level programming language. Here is some of the code from the SciNotes script (LCD_image_handler.sce) used for this project:



                    ImageFilename = uigetfile(["*.bmp"]);
    if(length(ImageFilename) == 0)
    CurrentImage = ReadImage(ImageFilename);
    CurrentImage = SegmentByThreshold(CurrentImage, 100);
    CurrentImage = flipdim(CurrentImage, 2);
    PixelData = uint8(zeros(10,1));
    for row = 1:10
        for column = 1:8
            if (CurrentImage(row,column) == %T)
               PixelData(row) = bitset(PixelData(row), column, 1);
                PixelData(row) = bitset(PixelData(row), column, 0);

  Download Code  

LCD_image_handler.sce executes as follows: First it opens a file dialog; select the .bmp file that you want to process.

The image is converted to black and white (in case any pixel values are not exactly 0 or 255), and each bit in a 10-byte array is set or cleared based on whether the corresponding pixel is black or white. The 10-byte array represents the size of one character (i.e., 10 vertical pixels by 8 horizontal pixels): one byte provides the 8 horizontal pixel values, and thus a 10-byte array contains 10 lines, each with 8 horizontal pixels. Finally, the array name (which is taken from the file name) and the pixel data are output to the Scilab console using printf() routines. This process repeats until you click “cancel” in the file dialog.

These variable definitions can be copied and pasted directly into source files in our EFM8 project. The script also generates code that is used to declare these variables in the project’s header file, so the final output looks like this:

As you can see, this process is efficient and completely flexible. With this SciNotes script, you can quickly create LCD-compatible pixel data for any symbol that you can draw into a 10-row-by-8-column image.


Port I/O

The port I/O configuration is identical to what we used in the previous SPI project.

The SPI signals are mapped to the appropriate port pins, except for the chip select signal, which we drive manually via P0.1.


Peripherals and Interrupts

The peripheral and interrupt setup is similar to what we used previously: SPI is configured for communication with the LCD module, Timer2 interrupts govern the frame rate, and Timer4 is used for short delays. The only difference is that Timer2 is configured for an interrupt frequency of 5 Hz instead of 60 Hz. In this project, a new letter is printed to the LCD screen every time Timer2 overflows, and a slower update frequency makes it easier to observe the characters before they get overwritten.



In the previous project, we used the LCD’s single-line update mode: after driving chip select to logic high, the EFM8 sends the mode select byte, then the line address, then the 128 bits of pixel data. The transfer is completed with two dummy bytes, and chip select returns to logic low. This mode is fine for unambitious applications, but the new firmware is different. The pixel values for the entire LCD are stored in a single two-dimensional array, and all these data bits are transferred to the LCD whenever the Timer2 interrupt tells the firmware to update the display. In other words, we are sending much more data to the LCD. (Note: In this particular implementation, the microcontroller controls only the first 60 LCD lines, rather than all 128, because only 1024 bytes of on-chip RAM are available to the application; an array large enough for the entire LCD would require 2048 bytes. This limitation is easily remedied in a custom design by choosing one of the code-compatible EFM8 devices with 4352 bytes of RAM.)

With all this extra data going to the LCD, it makes sense to use “multiple-line update mode;” as the name implies, this mode allows us to update the pixel values for multiple lines during one SPI transfer. This means that we need a new state machine in the SPI interrupt routine:

The other important addition to the firmware is the function in which the pixel data from the Scilab-generated arrays is inserted into the two-dimensional array that holds the pixel data for the entire LCD:



                    void InsertCharacter(unsigned char *LCD_Character) //the input to this function is a pointer to a pixel data array
	unsigned char n;
	unsigned char row_pixel;
	unsigned char column_byte;

        //rows are handled on a pixel-by-pixel basis
	row_pixel = LCDCursor[ROW];
        //columns are handled on a byte-by-byte basis, because one character width is 8 bits
	column_byte = LCDCursor[COL];

	/*each byte from the pixel data array is copied to LCDDisplayData,
	starting with the current cursor position*/
	for(n = 0; n < CHAR_HEIGHT; n++)
		LCDDisplayData[row_pixel][column_byte] = *LCD_Character;
		LCD_Character++;  //point to the next byte in the array
		row_pixel++;  //the next byte corresponds to pixel data for the next line

	LCDCursor[COL]++;  //move the cursor one character width to the right

	/*if the cursor has reached the end of the line,
	 return the cursor to the far left and move it
	 down by one character height*/
		LCDCursor[COL] = 0;
		LCDCursor[ROW] = LCDCursor[ROW] + CHAR_HEIGHT;
		if(LCDCursor[ROW] == NUM_LINES)	//if the cursor has reached the end of the display area,
			LCDCursor[ROW] = 0;	//return the cursor to the top line

	while(UPDATE_LCD == FALSE);	//wait here until Timer2 initiates an LCD update



  Download Code  

If you inspect the above code and read the comments, you should be able to get a good idea of how this works. Note that this routine both updates the LCD pixel data array and manages the cursor position.

The overall functionality of this project is to repeatedly print all the uppercase letters, from A to Z then A to Z again and so forth. The basic program flow is the following:

  • The microcontroller clears the LCD.
  • Timer2 is enabled.
  • The program enters an infinite loop and uses the InsertCharacter() function to print A, then B, then C, and so on to Z, then back to A again.
  • The InsertCharacter() function (see the above code excerpt) updates the LCD pixel data array, manages the cursor, waits until Timer2 sets the flag for an LCD update, then initiates an update via the UpdateAllLCDLines() function.
  • The Timer2 ISR sets the update flag only if the SPI state variable indicates that the SPI interface is idle. This ensures that the InsertCharacter() function does not attempt to initiate a new SPI transfer before the previous transfer is complete. 



Next Article in Series: Communicating with an EFM8 Microcontroller via USB