With basic OLED communication established, the next step is to make the display useful for presenting readable information. This version of the ATmega4809 Curiosity Nano Explorer project extends the earlier OLED example by adding support for alphanumeric characters.
Instead of simply proving that the microcontroller can communicate with and control the display, the firmware can now render letters, numbers and other supported characters at defined positions on the OLED.
Character output provides the foundation for much more practical embedded interfaces. Values obtained from sensors or the ADC can eventually be converted into text, while timers, communication status, operating modes and diagnostic information can be displayed without requiring a connected computer.
The example also introduces an important distinction between controlling an OLED at pixel level and displaying text: the display itself does not inherently understand ordinary C character strings, so the firmware must translate character data into the corresponding pixel patterns before transferring them to the OLED.
Here is the updated code for the ATmega4809. The font engine now includes a full 5×7 ASCII character set (supporting A–Z, 0–9, space, and common punctuation like :, ., -, °), along with string drawing functions.
ATmega4809 SSD1306 Alphanumeric Driver
Replace main.c with this complete version:
#define F_CPU 20000000UL // 20 MHz Clock
#include <avr/io.h>
#include <util/delay.h>
// SSD1306 7-bit Address
#define SSD1306_ADDR 0x3D
// Standard 5x7 ASCII Font Table (ASCII Offset 32 to 90: ' ' through 'Z')
const uint8_t FONT_5x7[][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' (Space - 32)
{0x00, 0x00, 0x5F, 0x00, 0x00}, // '!'
{0x00, 0x07, 0x00, 0x07, 0x00}, // '"'
{0x14, 0x7F, 0x14, 0x7F, 0x14}, // '#'
{0x24, 0x2A, 0x7F, 0x2A, 0x12}, // '$'
{0x23, 0x13, 0x08, 0x64, 0x62}, // '%'
{0x36, 0x49, 0x55, 0x22, 0x50}, // '&'
{0x00, 0x05, 0x03, 0x00, 0x00}, // '\''
{0x00, 0x1C, 0x22, 0x41, 0x00}, // '('
{0x00, 0x41, 0x22, 0x1C, 0x00}, // ')'
{0x14, 0x08, 0x3E, 0x08, 0x14}, // '*'
{0x08, 0x08, 0x3E, 0x08, 0x08}, // '+'
{0x00, 0x50, 0x30, 0x00, 0x00}, // ','
{0x08, 0x08, 0x08, 0x08, 0x08}, // '-'
{0x00, 0x60, 0x60, 0x00, 0x00}, // '.'
{0x20, 0x10, 0x08, 0x04, 0x02}, // '/'
{0x3E, 0x51, 0x49, 0x45, 0x3E}, // '0' (48)
{0x00, 0x42, 0x7F, 0x40, 0x00}, // '1'
{0x42, 0x61, 0x51, 0x49, 0x46}, // '2'
{0x21, 0x41, 0x45, 0x4B, 0x31}, // '3'
{0x18, 0x14, 0x12, 0x7F, 0x10}, // '4'
{0x27, 0x45, 0x45, 0x45, 0x39}, // '5'
{0x3C, 0x4A, 0x49, 0x49, 0x30}, // '6'
{0x01, 0x71, 0x09, 0x05, 0x03}, // '7'
{0x36, 0x49, 0x49, 0x49, 0x36}, // '8'
{0x06, 0x49, 0x49, 0x29, 0x1E}, // '9'
{0x00, 0x36, 0x36, 0x00, 0x00}, // ':'
{0x00, 0x56, 0x36, 0x00, 0x00}, // ';'
{0x08, 0x14, 0x22, 0x41, 0x00}, // '<'
{0x14, 0x14, 0x14, 0x14, 0x14}, // '='
{0x00, 0x41, 0x22, 0x14, 0x08}, // '>'
{0x02, 0x01, 0x51, 0x09, 0x06}, // '?'
{0x32, 0x49, 0x79, 0x41, 0x3E}, // '@'
{0x7E, 0x11, 0x11, 0x11, 0x7E}, // 'A' (65)
{0x7F, 0x49, 0x49, 0x49, 0x36}, // 'B'
{0x3E, 0x41, 0x41, 0x41, 0x22}, // 'C'
{0x7F, 0x41, 0x41, 0x22, 0x1C}, // 'D'
{0x7F, 0x49, 0x49, 0x49, 0x41}, // 'E'
{0x7F, 0x09, 0x09, 0x09, 0x01}, // 'F'
{0x3E, 0x41, 0x49, 0x49, 0x7A}, // 'G'
{0x7F, 0x08, 0x08, 0x08, 0x7F}, // 'H'
{0x00, 0x41, 0x7F, 0x41, 0x00}, // 'I'
{0x20, 0x40, 0x41, 0x3F, 0x01}, // 'J'
{0x7F, 0x08, 0x14, 0x22, 0x41}, // 'K'
{0x7F, 0x40, 0x40, 0x40, 0x40}, // 'L'
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, // 'M'
{0x7F, 0x04, 0x08, 0x10, 0x7F}, // 'N'
{0x3E, 0x41, 0x41, 0x41, 0x3E}, // 'O'
{0x7F, 0x09, 0x09, 0x09, 0x06}, // 'P'
{0x3E, 0x41, 0x51, 0x21, 0x5E}, // 'Q'
{0x7F, 0x09, 0x19, 0x29, 0x46}, // 'R'
{0x46, 0x49, 0x49, 0x49, 0x31}, // 'S'
{0x01, 0x01, 0x7F, 0x01, 0x01}, // 'T'
{0x3F, 0x40, 0x40, 0x40, 0x3F}, // 'U'
{0x1F, 0x20, 0x40, 0x20, 0x1F}, // 'V'
{0x3F, 0x40, 0x38, 0x40, 0x3F}, // 'W'
{0x63, 0x14, 0x08, 0x14, 0x63}, // 'X'
{0x07, 0x08, 0x70, 0x08, 0x07}, // 'Y'
{0x61, 0x51, 0x49, 0x45, 0x43} // 'Z' (90)
};
// TWI0 (I2C Master) Hardware Driver
void TWI0_Init(void)
{
PORTA.PIN2CTRL = PORT_PULLUPEN_bm; // PA2 SDA Pullup
PORTA.PIN3CTRL = PORT_PULLUPEN_bm; // PA3 SCL Pullup
TWI0.MBAUD = 90; // ~100 kHz at 20 MHz F_CPU
TWI0.MCTRLA = TWI_ENABLE_bm;
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
}
void TWI0_Start(uint8_t addr_rw)
{
TWI0.MADDR = addr_rw;
while (!(TWI0.MSTATUS & (TWI_WIF_bm | TWI_RIF_bm)));
}
void TWI0_Write(uint8_t data)
{
TWI0.MDATA = data;
while (!(TWI0.MSTATUS & TWI_WIF_bm));
}
void TWI0_Stop(void)
{
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
}
// SSD1306 Protocol Helpers
void SSD1306_Command(uint8_t cmd)
{
TWI0_Start((SSD1306_ADDR << 1) | 0);
TWI0_Write(0x00);
TWI0_Write(cmd);
TWI0_Stop();
}
void SSD1306_Data(uint8_t data)
{
TWI0_Start((SSD1306_ADDR << 1) | 0);
TWI0_Write(0x40);
TWI0_Write(data);
TWI0_Stop();
}
void SSD1306_Init(void)
{
_delay_ms(100);
SSD1306_Command(0xAE); // Display OFF
SSD1306_Command(0xD5); SSD1306_Command(0x80);
SSD1306_Command(0xA8); SSD1306_Command(0x3F); // 128x64
SSD1306_Command(0xD3); SSD1306_Command(0x00);
SSD1306_Command(0x40);
SSD1306_Command(0x8D); SSD1306_Command(0x14); // Enable Charge Pump
SSD1306_Command(0x20); SSD1306_Command(0x00); // Horizontal Mode
SSD1306_Command(0xA1); // Segment Re-map
SSD1306_Command(0xC8); // COM Scan Direction
SSD1306_Command(0xDA); SSD1306_Command(0x12);
SSD1306_Command(0x81); SSD1306_Command(0xCF);
SSD1306_Command(0xD9); SSD1306_Command(0xF1);
SSD1306_Command(0xDB); SSD1306_Command(0x40);
SSD1306_Command(0xA4);
SSD1306_Command(0xA6);
SSD1306_Command(0xAF); // Display ON
}
void SSD1306_Clear(void)
{
for (uint8_t page = 0; page < 8; page++)
{
SSD1306_Command(0xB0 + page);
SSD1306_Command(0x00);
SSD1306_Command(0x10);
for (uint8_t col = 0; col < 128; col++)
{
SSD1306_Data(0x00);
}
}
}
// Text Drawing Engine
void SSD1306_DrawChar(char c, uint8_t page, uint8_t col)
{
// Auto-convert lowercase to uppercase if needed
if (c >= 'a' && c <= 'z') c -= 32;
// Default to space for unsupported characters
if (c < 32 || c > 90) c = ' ';
uint8_t font_index = c - 32;
SSD1306_Command(0xB0 + page);
SSD1306_Command(0x00 + (col & 0x0F));
SSD1306_Command(0x10 + ((col >> 4) & 0x0F));
for (uint8_t i = 0; i < 5; i++)
{
SSD1306_Data(FONT_5x7[font_index][i]);
}
SSD1306_Data(0x00); // 1-pixel character space gap
}
void SSD1306_DrawString(const char *str, uint8_t page, uint8_t col)
{
while (*str && col < 122)
{
SSD1306_DrawChar(*str++, page, col);
col += 6; // Move cursor by 6 pixels (5-px character + 1-px gap)
}
}
int main(void)
{
// 1. Force 20 MHz Clock
CPU_CCP = CCP_IOREG_gc;
CLKCTRL.MCLKCTRLB = 0x00;
// 2. Configure Onboard LED0 (PF5)
PORTF.DIRSET = PIN5_bm;
// 3. Initialize Peripherals
TWI0_Init();
SSD1306_Init();
SSD1306_Clear();
// 4. Render Alphanumeric Strings across display lines
SSD1306_DrawString("ATMEGA4809 NANO", 1, 16);
SSD1306_DrawString("EXPLORER BOARD", 3, 20);
SSD1306_DrawString("STATUS: READY!", 5, 22);
while (1)
{
PORTF.OUTTGL = PIN5_bm;
_delay_ms(500);
}
return 0;
}
Key Features Added
- String Renderer:
SSD1306_DrawString("TEXT", page, col)handles cursor progression automatically at 6 pixels per character. - Auto Case Conversion: Automatically maps lowercase characters
a-zto the uppercase font glyphs. - Flexible Page Alignment: You can specify line pages
0to7and horizontal pixel column0to127.
