How to Display an Image on an LCD using an EFM8 Microcontroller

July 24, 2015 by Robert Keim

Use the EFM8’s USB functionality to transfer an image from your PC to an LCD.

Learn how to use the EFM8’s USB functionality to transfer an image from your PC to an LCD.

Recommended Level


Previous Articles in This Series

Required Hardware/Software

Project Overview

In previous projects, we explored implementing the EFM8’s SPI functionality, communicating with an LCD module, formatting and printing 10-by-8-pixel characters, and establishing a USB connection between the EFM8 and SciLab using the VCPXpress library. The current project brings these capabilities together to efficiently and conveniently display a 128-by-128-pixel image on the LCD. The objective is to start with any standard grayscale .bmp image file and use Scilab to process it and then transfer it via USB to the EFM8 microcontroller so that we can display it on a 128-by-128-pixel LCD. This project processes only a single image, but the techniques presented here could readily be adapted to displaying a simple animation composed of a series of similar images.

The process begins with creating the image using Paint.NET or some other image editing application. This image is loaded into Scilab, processed into a format that is compatible with the LCD, converted into a matrix of pixel data, and transmitted via 64-byte USB packets to the EFM8. An updated SPI state machine is then used to transfer this pixel data, four lines at a time, to the LCD module.    

Port I/O

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

The SPI signals are mapped to the appropriate port pins, except for the chip select signal, which we drive manually via P0.1. We do not need to directly configure the port pins for the USB data lines; all USB peripheral initialization is accomplished through the VCPXpress library.

Peripherals and Interrupts

The peripheral and interrupt setup is identical to what we used in the previous article: SPI is configured for communication with the LCD module, and Timer4 is used for short delays. We do not use Timer2 in this project because we have no need for a frame rate. Rather, the LCD is updated sequentially as pixel data packets are received from the PC.


The VCP configuration for this project is identical to what we used in the previous project. There is some additional USB functionality, though: previously the EFM8 only received data from Scilab, whereas now the EFM8 also transmits data.

void USBTxByte(unsigned char BytetoSend)
	Block_Write(&BytetoSend, 1, &USBBytesTransmitted);

As implied by the name of the Block_Write() function above, the VCPXpress library is capable of transmitting an array of bytes with one function call. However, in this project USB transmissions from the EFM8 are used only for flow control: the EFM8 sends one byte to inform Scilab that it is time to send more data. So the USBTxByte() function is simply a convenient way of using Block_Write() to transmit a single byte.

Received USB packets are handled with the following code:

   if (API_InterruptCode & RX_COMPLETE)   // USB read complete
	   if(USBBytesReceived == 1 && USBRxPacket[0] == NEW_IMAGE_FLAG)

		   NextLinetoWrite = 0;

		   //return the new image flag byte to the PC for flow control

		   //continue with the next USB read procedure
		   Block_Read(USBRxPacket, USB_PACKET_SIZE, &USBBytesReceived);

	   else if(USBBytesReceived == USB_PACKET_SIZE)
		   /*this flag tells the while loop in ImagetoLCD_main.c
	     	 to process a received USB pixel data packet*/

		   //continue with the next USB read procedure
		   Block_Read(USBRxPacket, USB_PACKET_SIZE, &USBBytesReceived);

When the Scilab script has finished converting the image file to LCD pixel data, it sends a one-byte packet with a value defined in ImagetoLCD_Defs.h as NEW_IMAGE_FLAG. Thus, if the received packet length is one and the single received byte has a value of NEW_IMAGE_FLAG, the microcontroller knows that a new image is on the way. It clears the LCD, transmits NEW_IMAGE_FLAG to the PC, and loads zero into NextLinetoWrite, which is a variable that holds the first line address to be updated when the microcontroller receives the next pixel data packet. If the received packet length is 64 bytes instead of one byte, the packet is bringing actual pixel data. In this case, we simply set the USB_PACKET_RECEIVED flag to true; the flow control byte will be transmitted after the LCD update is complete.

When the infinite loop in ImagetoLCD_main.c realizes that USB_PACKET_RECEIVED has been set to true, it calls ProcessUSBRxPacket():

void ProcessUSBRxPacket()
	unsigned char n = 0, row, column;

	//copy the received pixel data to the LCD display data array
	for(row = 0; row < LINES_PER_PACKET; row++)
		for(column = 0; column < NUM_LINE_DATA_BYTES; column++)
			LCDDisplayData[row][column] = USBRxPacket[n];

	//wait until the SPI state variable indicates that the bus is available for a new transfer
	while(LCDTxState != IDLE);



Here we transfer the pixel data into the appropriate two-dimensional array. In this project, LCDDisplayData[][] is 4 rows by 16 columns: we still need 16 column bytes to hold the 128 bits of horizontal data, but we need only 4 rows because pixel data is transferred from the PC in 64 byte packets, and 64 bytes divided by 16 bytes per line equals 4 lines. After updating the array, the program waits until the LCD communications interface is idle and then calls UpdateLCDLines().

This project requires some changes to the state machine that governs SPI transfers to the LCD. Previously we had the UpdateAllLCDLines() function, which (as you might guess from the name) initiates a process that updates all the LCD lines in one SPI transfer. But now we update only four lines during one SPI transfer, and two additional tasks are performed at the end of the procedure:


The Scilab script begins with an image processing section:

The input image must be a grayscale, 128-by-128 pixel .bmp file. The SegmentByThreshold() function converts the image from grayscale to black and white, since with our LCD the pixels are either on or off—no gray allowed. A series of bitset() operations converts this image data into pixel data that can be sent to the EFM8 and transferred directly to the LCD. Note that sophisticated computational applications like Scilab are not exactly optimized for the sort of awkward bitwise operations we are using here. In other words, the double for-loop block in the above code takes a long time to execute (e.g., about 23 seconds with a 2.5 GHz processor running Windows 8.1). Thus, if you want to adapt this code for displaying an animation sequence, you will need to convert all the images to LCD pixel format before you start sending data to the EFM8.

The other main section in the Scilab script is the for loop that sends the pixel data to the EFM8 via the VCP connection:

Four rows of pixel data are converted into a one-dimensional array and transmitted as a single 64-byte packet using the slSendArray() function. The script then reads the single confirmation byte from the EFM8 before sending the next four rows of pixel data. Important Note: The calls to slReadByte() in this script have “1” for the second parameter, i.e., response = slReadByte(EFM8Port, 1). This “1” indicates that the function will block, which is to say that Scilab will do nothing until at least one byte arrives. The advantage here is that the script runs as quickly as possible, because execution will continue as soon as the EFM8 sends the confirmation byte. The problem, though, is that if something goes wrong and the byte never comes, Scilab will be in a coma until you close the program and reopen it. Consequently, a better way to do this during the debugging phase is to use the sleep() function to give the EFM8 time to respond then read the byte without blocking, i.e., slReadByte(EFM8Port, 0).

The Scilab script also calls tic() and toc() to measure and display the length of time required to transfer and display one image. With the same 2.5 GHz Windows machine mentioned above, the process takes only about 50 ms, which means that this system should be able to comfortably maintain an animation frame rate of 10 images per second. 

Give this project a try for yourself! Get the BOM.