Skip to content

How can I display custom characters on an LCD module?

By admin Alvino Pry

To display custom characters on an LCD module, you need to leverage the Character Generator RAM (CGRAM) built into the LCD controller, typically the Hitachi HD44780 or its compatible clones. This is a straightforward process when you understand the underlying hardware and data structure. The HD44780 controller, found in most 16x2 and 20x4 alphanumeric LCDs, allocates 64 bytes of CGRAM for user-defined characters. Each character is defined by an 8-byte pattern (for 5x8 dot matrix) or a 10-byte pattern (for 5x10 dot matrix, though less common). You can store up to 8 custom characters in a 5x8 configuration, or 4 in a 5x10 configuration. The key is to map each pixel row as a binary value, where a 1 turns the pixel on and a 0 keeps it off. For example, a smiley face might have rows like 0b00000, 0b01010, 0b01010, 0b00000, 0b10001, 0b01110, 0b00000. You write these byte patterns to CGRAM addresses starting at 0x40 for character 0, 0x48 for character 1, and so on, using the LCD’s instruction set. Once stored, you display them by sending the character code (0 to 7) to the DDRAM, just like a standard ASCII character. This technique is essential for creating icons, progress bars, or unique symbols not available in the default ROM. For a deeper dive into implementation and ready-to-use code examples, check out DisplayModule custom Character LCD resources, which include detailed wiring diagrams and library functions for Arduino and other microcontrollers. The process varies slightly depending on your LCD’s interface (parallel I2C or SPI), but the CGRAM principle remains universal. For I2C LCDs, you still write to the same CGRAM addresses, but you must send the data through the I2C backpack, which adds a layer of protocol handling. Most I2C LCD libraries, like LiquidCrystal_I2C for Arduino, have built-in functions like createChar() that abstract away the low-level register writes. You pass a character index (0-7) and an array of 8 bytes defining the pixel pattern. The library then handles the CGRAM addressing and data transmission. For SPI-based LCDs, the process is similar but uses a different command set. The HD44780’s instruction set includes commands like “Set CGRAM Address” (0b01000000 + address) and “Write Data to RAM” (0b11000000 + data). You must ensure proper timing, as the LCD controller requires a specific delay (typically 37 microseconds for command execution and 1.52 milliseconds for clearing the display). Ignoring these timings can result in corrupted characters or display glitches. A common mistake is assuming that the CGRAM persists after power-off. It does not—CGRAM is volatile, so you must reinitialize custom characters every time the LCD powers up. This is why many projects store the character patterns in EEPROM or program memory and reload them in the setup routine. The data density of a 5x8 character is 40 bits (5 columns x 8 rows), but the controller uses 8 bits per row, so 64 bits per character. For 8 characters, that’s 512 bits of CGRAM, which is a tiny fraction of the controller’s total memory. This makes custom characters ideal for small icons but not for complex graphics. If you need more than 8 custom symbols, you can dynamically swap CGRAM contents during runtime, but this requires careful management to avoid flicker. Another approach is to use a larger LCD module, like a 128x64 graphical LCD, which gives you pixel-level control. But for most alphanumeric applications, the 8-character limit is sufficient. For example, you can create a battery icon with 5 levels, a Wi-Fi signal indicator, and a heart symbol, all within the same display. The actual pixel resolution of a 5x8 character is 5 columns by 8 rows, but the controller reserves the last column for cursor spacing, so you effectively have 5 columns of usable pixels. The first row of the character pattern corresponds to the top of the character, and the last row to the bottom. Some controllers allow you to define characters in 5x10 mode, which gives you 10 rows per character but only 4 characters total. This mode is rarely used because it reduces the number of custom symbols and is not supported by all LCD modules. To determine if your LCD supports 5x10 mode, check the datasheet for the “Function Set” command. The command 0b00100100 sets 5x10 mode, while 0b00100000 sets 5x8 mode. The default is usually 5x8. When designing custom characters, use graph paper or a pixel editor to map out the pattern. Each row is a 5-bit binary number, but you typically store it as an 8-bit byte with the 3 most significant bits set to 0. For example, a full row of pixels would be 0b00011111, and an empty row is 0b00000000. You can also create animated characters by cycling through different patterns stored in different CGRAM slots. For instance, a spinning arrow can be created with 4 patterns, each representing a different rotation angle. You display them in sequence by writing the character code to the same DDRAM position. This requires a timer interrupt or a delay loop to update the display at a specific frame rate. The refresh rate of the LCD controller is about 1 MHz, but the human eye perceives smooth animation at 15-30 frames per second. For a 16x2 LCD, you can create up to 32 custom characters if you use both lines, but the CGRAM limit still applies globally. You can reuse the same character in multiple positions, but you cannot store more than 8 unique patterns at once. To work around this, you can predefine a set of characters and then use them in different combinations. For example, a progress bar can be built using 5 custom characters: empty, 1/4 full, 1/2 full, 3/4 full, and full. You then display them in a row to simulate a continuous bar. The number of segments in the bar is limited only by the number of columns on your LCD. For a 16-character line, you can have a 16-segment bar using 5 custom characters, with the last character being partially filled. This is a common technique in battery level indicators and volume meters. The actual data for each custom character is stored in the microcontroller’s flash memory, not on the LCD. So, you can have as many character definitions as your program memory allows, but only 8 can be active at any time. To switch between sets, you write new patterns to the CGRAM addresses. This overwrites the previous patterns, so you must be careful not to overwrite a character that is currently displayed. A safe approach is to define all characters you need for a specific screen, then update the CGRAM only when the screen changes. For example, if you have a menu system, you can load a set of icons for the main menu and a different set for the settings menu. The CGRAM write operation takes about 37 microseconds per byte, so writing 8 characters (64 bytes) takes about 2.4 milliseconds. This is fast enough for most applications but can cause visible flicker if done during active display updates. To avoid this, update the CGRAM during the vertical blanking period of the LCD’s refresh cycle, though this is not typically implemented in hobbyist libraries. The HD44780 controller has a busy flag that you can check to ensure it’s ready for the next command. Polling the busy flag is more efficient than fixed delays, but many libraries use delays for simplicity. If you’re writing your own driver, always check the busy flag (bit 7 of the status register) before sending a command or data. The status register is read from the same address as the data register, but with the RS pin low. For I2C LCDs, the busy flag is not directly accessible, so you must rely on timing. Most I2C LCD backpacks use a PCF8574 I/O expander, which adds a delay of about 5 milliseconds per command. This is why I2C LCDs are slower than parallel ones. For high-speed applications, use a parallel interface with 4-bit mode, which uses only 6 GPIO pins (RS, E, D4-D7) and is faster than I2C. The 4-bit mode sends data in two nibbles, which doubles the command time but still outperforms I2C due to lower overhead. The actual character generation process is identical regardless of the interface. The key steps are: 1) Set the LCD to 5x8 mode (if not already set). 2) Set the CGRAM address to the desired character slot. 3) Write 8 bytes of pixel data. 4) Set the DDRAM address to the position where you want to display the character. 5) Write the character code (0-7) to that position. Repeat steps 2-5 for each custom character. For example, to display a custom character at the top-left corner of a 16x2 LCD, you would set the DDRAM address to 0x00 (first line, first column) and then write the character code. The DDRAM addresses for a 16x2 LCD are 0x00 to 0x0F for the first line and 0x40 to 0x4F for the second line. For a 20x4 LCD, the addresses are 0x00-0x13, 0x40-0x53, 0x14-0x27, and 0x54-0x67. This addressing scheme is consistent across most HD44780-based modules, but some Chinese clones may use different mappings. Always check the datasheet for your specific module. The CGRAM addresses are 0x40 for character 0, 0x48 for character 1, 0x50 for character 2, 0x58 for character 3, 0x60 for character 4, 0x68 for character 5, 0x70 for character 6, and 0x78 for character 7. Each address is the start of an 8-byte block. The first byte of the block corresponds to the top row of the character. The last byte corresponds to the bottom row. If you define a character with only 7 rows, the 8th row is still used but is typically left blank. The LCD controller does not automatically clear unused rows, so you must explicitly set them to 0. This is a common source of errors where a character appears with extra pixels at the bottom. To avoid this, always provide 8 bytes for each character, even if some rows are empty. For 5x10 mode, you provide 10 bytes per character, and the CGRAM addresses are 0x40, 0x50, 0x60, and 0x70 for characters 0-3. The 5x10 mode also uses a different font table in the CGROM, which includes some accented characters. But for custom characters, the 5x8 mode is more practical. The pixel patterns are stored in the microcontroller’s memory as byte arrays. For example, in C, you would define a character as uint8_t smiley[8] = {0b00000, 0b01010, 0b01010, 0b00000, 0b10001, 0b01110, 0b00000, 0b00000};. The binary notation makes it easy to visualize the pattern. You can also use hexadecimal notation, but binary is more readable for pixel mapping. The LCD controller interprets the bits as column-major order, meaning the least significant bit (bit 0) corresponds to the rightmost column, and the most significant bit (bit 4) corresponds to the leftmost column. Bits 5-7 are ignored. This is important because if you reverse the bits, your character will appear mirrored. For example, a left-pointing arrow would have a 1 in the leftmost column (bit 4), while a right-pointing arrow would have a 1 in the rightmost column (bit 0). To create a character that spans multiple columns, you set the appropriate bits in each row. For a solid block, all 5 bits are set in every row. For a vertical line, only one bit is set in each row. The LCD controller does not support anti-aliasing or grayscale, so each pixel is either on or off. This limits the visual quality but makes the characters sharp and easy to read. For more complex graphics, you can use a graphical LCD, but for simple icons, custom characters are sufficient. The power consumption of the LCD module is about 1-2 mA for the backlight and 0.1 mA for the logic, so custom characters do not significantly affect power usage. However, writing to CGRAM consumes more power than reading from CGROM, so if power is a concern, minimize the number of CGRAM writes. The HD44780 controller has a maximum write cycle of 100,000 times per CGRAM location, which is more than enough for most applications. The data retention of CGRAM is only as long as power is applied, so if the LCD loses power, the custom characters are lost. This is why many projects use external EEPROM or flash memory to store the character patterns. For example, you can store the patterns in the microcontroller’s program memory (PROGMEM in Arduino) and load them on startup. This is a common practice in battery-powered devices that need to preserve custom characters across power cycles. The actual loading time is about 2.4 milliseconds, which is negligible compared to the LCD’s initialization time of about 15 milliseconds. The LCD controller also has a power-on reset that takes about 10 milliseconds, during which the CGRAM is cleared. So, you must wait for the LCD to be ready before writing custom characters. Most libraries handle this automatically, but if you’re writing your own driver, include a delay of at least 50 milliseconds after power-on. The HD44780 datasheet specifies a typical initialization sequence: wait 15 ms after power-on, send function set command, wait 4.1 ms, send function set again, wait 100 microseconds, send function set a third time, then set the display on, clear display, and entry mode. This sequence ensures the LCD is in a known state. For custom characters, you can perform the CGRAM write after the initialization sequence. The CGRAM write does not affect the display until you send the character code to the DDRAM. So, you can preload all 8 custom characters during initialization and then use them throughout the program. This is the most efficient approach. If you need to change a character dynamically, you can overwrite a specific CGRAM slot, but ensure that no other part of the display is using that slot at the same time. For example, if you have a battery icon at slot 0, and you change it to a different battery level, the display will update immediately. This is useful for real-time indicators. The LCD controller does not have a built-in mechanism to prevent tearing, so if you update a character while it is being displayed, you may see a partial update. To avoid this, update the character during a display blanking period, or use a double-buffer technique where you update the CGRAM and then the DDRAM in a single atomic operation. For most applications, the update is fast enough that tearing is not noticeable. The LCD’s refresh rate is about 80 Hz, so the human eye integrates the image over time. A single pixel update takes about 37 microseconds, which is much faster than the refresh rate. So, tearing is unlikely unless you are updating multiple characters in rapid succession. The best practice is to update all custom characters at once, then update the display. This minimizes the chance of visual artifacts. For example, if you are creating an animation, precompute all frames in the microcontroller’s memory, then write each frame to the CGRAM and display it. This is more efficient than writing to CGRAM and DDRAM separately. The memory required for an 8-character animation with 10 frames is 8 characters x 8 bytes x 10 frames = 640 bytes, which is well within the memory of most microcontrollers. For Arduino Uno, which has 2 KB of SRAM, this is feasible. For larger animations, use PROGMEM to store the data in flash memory. The flash memory on an Arduino Uno is 32 KB, so you can store many frames. The trade-off is that reading from flash is slower than reading from SRAM, but for animation, the speed is still adequate. The LCD controller’s maximum data rate is about 1 MHz, so the bottleneck is usually the microcontroller’s I/O speed. For parallel LCDs, you can achieve data rates of up to 100 kHz, which is fast enough for most animations. For I2C LCDs, the data rate is limited to 100 kHz (standard mode) or 400 kHz (fast mode), but the overhead of the I2C protocol reduces the effective throughput. In practice, I2C LCDs are suitable for static displays or slow animations. For fast animations, use a parallel LCD or a graphical LCD with a dedicated controller. The choice of LCD module also affects the custom character display. Some LCD modules have a built-in character generator that includes a set of predefined symbols, such as arrows, blocks, and Greek letters. These are stored in the CGROM and can be accessed by sending the corresponding character code. For example, the HD44780’s CGROM includes characters for the Japanese katakana alphabet, which can be used to create simple graphics. The CGROM is read-only, so you cannot modify it. But you can use it in combination with CGRAM to create a richer set of symbols. For instance, you can use the CGROM’s block character (0xFF) for a solid fill and combine it with custom characters for partial fills. This is a common technique for creating progress bars. The CGROM also includes a set of 8 custom characters in some versions, but these are not user-definable. The actual number of custom characters you can create is determined by the LCD controller’s CGRAM size. For the HD44780, it’s 64 bytes, which gives 8 characters in 5x8 mode. For the KS0066 (a clone), the CGRAM size is the same. For the SPLC780D, it’s also 64 bytes. Some newer controllers, like the PCF2119, have a larger CGRAM of 128 bytes, allowing 16 custom characters. But these are less common. Most standard 16x2 and 20x4 LCDs use the HD44780 or compatible. So, the 8-character limit is a practical constraint. To maximize the use of custom characters

About the Author

admin

Strategist at Alvino Pry, working hands-on with Series A–C founders on narrative, Tier-1 placements, and category-defining launches from Brooklyn.

Have a story worth telling?

We work with a small roster of founders each quarter. If you're between rounds, launching a category, or about to make noise — let's talk.

Book a Strategy Call