Monitor and keyboard controllers
Controlled by low-cost ATMEL "Mega" processors
- Project 1: PAL or NTSC multi-mode text and graphics display with I2C, 4 or 8 bit interface
- Project 2: PC keyboard-->serial and serial-->display interface
- Use both together to allow any project that uses serial I/O to have a screen and keyboard

(actual picture of screen)

Work on here, unless credited otherwise, is copyright Grant Searle. You are not allowed to publish this elsewhere without my permission. You may use any of this for your personal use, but commercial use is prohibited.

by Grant Searle

For news and updates, follow me on Twitter:
Follow @zx80nut

Last update: 2nd January 2017

Major changes log:
* 21st July 2013 - Original (ATmega32) keyboard/display interface taken offline temporarily (sorry) while I get the redesigned interface tested. ...update - back online!
* 22nd July 2013 - Version 2 (ATmega328) released.
* 20th August 2013 - Correction to the "set row" display processor code. New source and HEX in the zip file.
* 21st August 2013 - the ATmega32 version is now back online and can be seen here.
* 7th September 2013 - Version 2.1 - 4 bit data bus implemented to free up pins on the host if required. 8 bit bus still available.
* 18th September 2013 - Version 2.2 - I2C / two wire interface added as an option. There is an I2C issue if data stream not left open - to be resolved.
* 27th September 2013 - Version 3.0 - MAJOR rewrite/update - now supports multi font types plus medium resolution graphics

* 2nd January 2017 - Keyboard/serial interface can now use either the ATmega88 or ATmega328. Sourcecode updated to comile without warning on AtmelStudio 7


Interface coding
Processing and circuit description
Interface hardware details
Source code
Special characters / control codes
Font attributes
Character set
My other pages


This page shows construction details for two cheap and simple interfaces that can be used individually or together:
1. A complete very fast video controller
2. An interface that allows a PC keyboard and the video controller to be connected to a computer system via a serial port. The interface runs at 115200 baud (can be altered within the software) giving a very fast display update.

Video controller specification:

Very easy to interface to, fast and requires no host/external memory.
Character set: Full "CGA" (8x8) character definitions as used on the IBM PC (DOS).
Screen size: Text - 80x25, 40x25, 80x12, 40x12, Graphics 160x100 or ANY mix (defined per 1 of 25 rows)
Resolution: 640x200 for 80 char text, 160x100 for graphics
Font sizes are defined for each line. All chars on the same line have the same font size and style.
Graphics: Any part of the active screen, up to 160x100 pixels. Each pixel individually addressable. Defined on any of the 25 screen lines.
Video output: Standard composite monochrome video, PAL or NTSC timing, non-interlaced so no flicker.
Display memory: 2000 chars internal + 25 line attributes - no host memory needed.
Very fast update and scrolling whichever interface is used.
Interface: 8 bit data bus (requires 10 I/O pins), 4 bit data bus (requires 6 I/O pins) or two-wire (I2C) that uses only 2 pins.
I2C (
thanks to Dave Curran for his guidance and code on this) supports low (100KHz) and high (400KHz) speed clocks or higher (1MHz or more easily achievable).
Hardware: Very low cost - two chips - ATmega328 and 74HCT166

Keyboard and serial controller specification:

Serial interface: Fully buffered 115200 baud, handshake on receive, TTL levels.
Keyboard interface: PC PS/2 standard keyboard connector, accepts standard keyboards made for PCs. Keyboard LED illumination implemented, and allows for caps lock and num lock.
Hardware: Very low cost - one chip - ATmega88/168/328

Using a separate processor with it's own memory allows the host processor to have as much memory available as possible. I have written a minimal-ANSI interpreter within the interface processor to handle escape sequences so that software can be set to use an ANSI terminal setting.
So, in effect, using the ATmega processors, I have created a small minimum-ANSI compatible terminal.

The video controller was created using an ATmega328 processor (or an ATmega32 processor, here). This was based on a design and code produced by Daryl Rictor (he produced a 40x25 display using an ATmega8 and three other ICs). I adapted the code to use the larger memory of the ATmega328 (originally on an ATmega32) and modified it significantly to make an 80 char display and also to support graphics. I modified control code processing and handshaking that avoided two additional ICs. The 2-wire handshaking between the display processor and the IO processor allows correct synchronisation between the processors when transferring serial data to the display processor.

An Arduino shield and library for the video controller part is also being developed by Dave. Check out his blog here.


Real-timed performance figures are shown here (80 char x 25 line screen - very similar speeds for all modes)

Test case PAL
8 bit interface
4 bit interface
2 wire
8 bit interface
4 bit interface
2 wire
150,000 characters sent to the display
+ 1000 "cursor homes" (to prevent scrolling)
4.7 seconds
31915 char/sec
6.3 seconds
23809 char/sec
8.3 seconds
18072 char/sec
6.7 seconds
22388 char/sec
8.9 seconds
16853 char/sec
11.0 seconds
13636 char/sec
Scroll screen 1000 lines
(graphics and/or text)
2.5 seconds
400 lines/sec
2.5 seconds
400 lines/sec
2.5 seconds
400 lines/sec
3.6 seconds
277 lines/sec
3.6 seconds
277 lines/sec
3.6 seconds
277 lines/sec
Clear screen 1000 times 2.5 seconds
400 clears/sec
2.5 seconds
400 clears/sec
2.5 seconds
400 clears/sec
3.6 seconds
277 clears/sec
3.6 seconds
277 clears/sec
3.6 seconds
277 clears/sec
Using supplied control codes to set 160000 pixels and reset 160000 pixels covering the complete screen (ie. 320,000 pixel operations)
26.5 seconds
12075 pixels/sec
37.1 seconds
8625 pixels/sec
51.6 seconds
6201 pixels/sec

727KHz 2 wire:
35.7 seconds
8956 pixels/sec

37.6 seconds
8510 pixels/sec
52.4 seconds
6102 pixels/sec
68.6 seconds
4664 pixels/sec

727KHz 2 wire:
49.3 seconds
6488 pixels/sec

2 wire performance is for 400KHz clock unless stated. This can be improved significantly by increasing the 2 wire clock speed.
PAL performance is higher than NTSC because a larger proportion of the time is free for PAL than NTSC, due to screen refresh rates.
Each graphics pixel set/reset requires 3 bytes to be sent, which is why 8 bit transfer noticeably faster than 2 wire.

Test conditions:
Host controller: ATmega88 running at 16MHz (identical performance to Arduino Uno or Nano etc.)
2 wire interface: Standard 400KHz clock speed unless specified
Timing: Externally electronically timed using a Black*Star Apollo 100 Universal counter-timer.


For further details, see the processing and circuit description later on in this page. There are THREE options to interface to the display controller:

4 Bit interface

Hardware requirements

The controller requires 6 pins available:
D0 - D3 - Outputs
AVAIL - Output
ACK - Input

Ensure R6 is connected on the display controller to enable 4-bit transfers.

Controller initialisation

Set D0, D1, D2, D3 and AVAIL to be outputs.
Set ACK to be an input.
Set AVAIL pin to low (0).

Sending a character to the display

Ensure AVAIL is low. Should already be low, but if not, set it low.
Wait until ACK pin input is low. Once it is, the display processor is ready.
Place high 4 bits of the character onto D0..D3 output pins.
Set AVAIL to high, to indicate to the display that the high bits are ready to be read.
Wait until ACK pin input is high. Once it is, the display processor has read the high bits and is ready.
Place low 4 bits of the character onto D0..D3 output pins.
Set AVAIL to low, to indicate to the display that the low bits are ready to be read.

8 Bit interface

Hardware requirements

The controller requires 10 pins available:
D0 - D7 - Outputs
AVAIL - Output
ACK - Input

Ensure R6 is NOT connected on the display controller to enable 8-bit transfers.

Controller initialisation

Set D0, D1, D2, D3, D4, D5, D6, D7 and AVAIL to be outputs.
Set ACK to be an input.
AVAIL can be high or low.

Sending a character to the display

Wait until ACK pin input is the same value as the AVAIL pin. Once it is, the display processor is ready.
Place the character onto D0..D7 output pins.
Flip the AVAIL pin to indicate to the display that the data is ready to be read and processed.

I2C (two wire) interface

Only 2 pins needed - SDA and SCL.

The display is at address 01 (can be changed in code).
Use as for any other I2C device, and send 8-bit character codes.


VERSION 2.0 - Now uses an ATmega328 processor instead of the older ATmega32. The ATmega328 is a cheaper, newer and smaller processor (28 pin instead of 40 pin). The ATmega32 design (8 bit interface only - frozen) is still available and can be seen here.
VERSION 2.1 - Now allows a 4 bit or 8 bit display interface. NOTE: Display NTSC selection moved from PC5 to PD7.
VERSION 2.2 - Two-wire also supported. NOTE: Display shift/load moved from PC5 to PC3, AVAIL moved to PC5, /RTS moved to PC3.

VERSION 3.0 - Graphics and multi-font support use the same pinouts as for 2.2 but now have additional configuration resistors (if needed, can be omitted) - see note 6 below.

This consists of two modules and when used together produces a generic ANSI terminal (also supporting graphics) using a TTL compatible serial interface running at 115200 baud (can be changed in software) so can be used for any computer project that has a serial I/O and needs a keyboard and display.

This uses an ATmega88 (or 168 pr 328) for the keyboard and serial buffer and an ATmega328 for the display processor.
Since the display is independent of the host (eg Z80) processor, the host has no processing overhead so when connected to my CP/M system this system runs much faster than many of the old systems.

The circuit is in two distinct parts, as shown below. If only a display is needed then only the circuit to the left side of the line is needed.


Note 1: Not all power supply pins are not shown. These must be connected to the appropriate power rail.
Note 2: Include R3 for NTSC display only, otherwise do not make any connection between PD7 and ground.
Note 3: Include R6 and also connect PB1 on the interface to ground for 4-bit data ONLY. Do not connect any green  wires when using the 4-bit interface.
Note 4: Include R7 and also connect PB2 on the interface to ground for two-wire ONLY. Do not connect any red or green wires.
Note 5: If a specific display start-up configuration is always used, the code can be altered (see below) and all configuration resistors omitted.
Note 6: Default font will be 80 char, single-height, bold. To change it, pull the following display lines low with 10K resistors:
        Pin 6 (PD4) - 40 chars per line if pulled low via a 10K resistor
        Pin 5 (PD3) - Double height if pulled low via a 10K resistor
        Pin 4 (PD2) - Non-bold if pulled low via a 10K resistor

To illustrate the wiring for the three interfacing options, please see below...

8-bit (suitable for connecting the display to microcontrollers or microprocessors using 10 pins):

4-bit (suitable for connecting the display to microcontrollers or microprocessors using 6 pins):

Two wire (suitable for connecting the display to microcontrollers supporting I2C):

The code for the serial/keyboard controller is written in C, and the code for the display controller is assembler, significantly modified from Daryl Rictor's original (full credit to Daryl for the original!)

I use the free Atmel AVR studio 7 to compile/assemble the two sets of code.

When using both modules together, ensure the data bus width of the display processor matches the data bus width of the keyboard/serial interface otherwise it will not work.


The two parts of the above schematic (8 bit interface, PAL mode) are constructed as shown here. A resistor links each reset to the power because I program the chips in-circuit.

Note: this picture is for the schematic version 2.1. Minor wiring changes for version 2.2

The system that required the interface (my CP/M design) is shown to the right. The TX, RX, /RTS and power connections are made between the interface and the host computer.
The data bus wiring between the display and the interface parts of the circuit can be seen in blue, handshake between them in orange.

To interface the display to a microcontroller without the keyboard or serial interface requires only the left hand part. The controller needs 4 or 8 output pins for the data, one output pin for the "data available signal" and one input pin for the "acknowledge" pin.


The serial/keyboard controller runs at 11.0592MHz (to coincide with the 115200 Baud serial interface) and the display processor runs at 16MHz.

The serialisation of the video data is done using a 74HCT166 parallel to serial converter.

The sync and video signals are merged using a resistor network which works fine for me and terminates in a standard 75 ohm video input on the monitor.

The keyboard used is a standard "PC" keyboard with a PS/2 connector (NOT USB). Caps lock and num lock works and it updates the keyboard LEDs as needed. Non-ASCII keys return codes, but this isn't finalised yet and is easily changed in the code.

Here is a block diagram of the display and keyboard interface being used together as a serial-driven I/O for a microcomputer system:


ATmega88/168/328 processor (Serial and keyboard I/O)
This buffers the serial input and stores it in a circular buffer 800 chars in size. The RTS is cleared when less than 32 character spaces remain in the buffer and is set when there are less than 32 char spaces used. This is to allow overrun from the sending system without losing characters.
When a character or command is to be sent to the display processor, the code is placed on the output port pins.
Some ANSI commands are interpreted and converted to the relevant video processor commands.

ATmega328 processor (video)
This has 2K RAM so can accommodate an 80x25 char display with some bytes remaining for workspace. The main processing is transferring data from the memory to the output serial shifter, and producing the HSYNC and VSYNC signals. During the inactive time of the display, the input port/status bits can be read and the incoming data interpreted either as a command or as a character that is inserted into the display. Scrolling and clearing of the screen is handled within this processor (very quickly!)
It is mainly Daryl's code in there, but I have adapted it for the ATmega328 processor which has 2K RAM, added some routines of my own and altered the row/col cursor positioning commands to take a parameter byte instead of using a set of character spaces and changed the method to allow the display of control-code characters. This allowed me to modify it to produce an 80x25 display. I also changed the code so that only one timer is needed instead of two. Additionally the handshaking/data read code was changed to allow me to interface to the keyboard/serial controller without needing a latch or the other chips that he used. The dot clock runs at 16MHz to allow an 80 column display to be shown on a standard video monitor/TV.

An  ATmega328 pin (see schematic) determines whether PAL (50Hz) or NTSC (60Hz) display is required. Connect via a resistor to ground for NTSC.


Data transfer between the host (or interface) and display controllers (applicable to 4 bit or 8 bit mode).
The two processors must be able to communicate with each other and must wait for the other processor as needed.
Data transfer is via a 4 bit or 8 bit parallel port.
Handshaking between the processors is achieved with two lines - the "Data Available" line and the "Acknowledge" line.

The serial/keyboard processor uses the two handshake lines to see if data can be sent to the display.
The display processor uses the two handshake lines to see if data has been sent.

The handshake is the same for 4 or 8 bit transfers, although the meaning is slightly different...

8 bit transfers

Host controller:
The host (serial/keyboard processor) waits until the  "Acknowledge" line matches the "Data Available" line. Once it does, the data is placed on the output port and the "Data Available" line is flipped (ie. 0 to 1 or 1 to 0).

Display processor:
If the "Data Available" does NOT match the "Acknowledge" line then data has been sent so the data input is read and the "Acknowledge" line is flipped ONCE DATA HAS BEEN READ. Therefore once data transfer is complete, the "Acknowledge" and "Data Available" lines will have the same value.
This data is then processed.

4 bit transfers

Host controller:
(The host (serial/keyboard processor) has AVAIL set as low when initialised. This will remain low when idle in 4 bit mode)
It waits until the  "Acknowledge" line matches the "Data Available" line (LOW). Once it does, the HIGH 4 BITS of the data is placed on the output port and the "Data Available" line is set HIGH.
It waits until the  "Acknowledge" line matches the "Data Available" line (HIGH). Once it does, the LOW 4 BITS of the data is placed on the output port and the "Data Available" line is set LOW.

Display processor:
If the "Data Available" does NOT match the "Acknowledge" line then data has been sent so the data input is read and the "Acknowledge" line is flipped ONCE DATA IS READ. If the AVAIL is high then the 4 bits read are stored in the high 4 bits of a buffer byte but not processed (because data transfer is not yet complete). If the AVAIL is low then the 4 bits read are stored in the low 4 bits of a buffer byte which then completes the byte transfer. This complete 8-bit value is then processed.

To demonstrate this, please refer to the following:

I2C (two wire) transfers

Standard I2C interfacing at high speeds (more than 1MHz if needed). The display processor is set in code to be I2C address 01. Change the code if different address is needed.

Refer to the microcontroller datasheet that the display is connecting to for more information.


Note - 18th September 2013 - Code updated to utilise swapped pins for the AVAIL, Shift/load and /RTS.
27th September - additional config pins for start-up font.
If using latest software here, you MUST ensure you are using the connections as shown in the schematic above.

Coding based on the original code produced by Daryl Rictor

Many thanks to Dave Curran for providing me with the two-wire code for the display processor.

Commented source code and HEX files for the serial/keyboard processor and display processor (or previous version for ATmega32) processors are HERE.

There is an I2C issue if data stream not left open - so, if using I2C, don't close the stream otherwise some data may be lost.

When programming the ATMEL processors, ensure the appropriate fuse bits are set (see documentation/programmer details) as these need to use the following settings:

A crystal (XT) clock (must be rail to rail for the display controller)
No watchdog timer
No clock divider
JTAG disabled
OCD disabled
No boot program

Must use XT rail-to-rail clock for the video processor because this also clocks the 74HCT166 (as permitted in the ATMEL datasheet).

Suitable fuse settings for the two controllers are as follows (please refer to datasheets for info)...

For the 328P interface chip:
Low: 0xFF
High: 0xD9
Extended: 0xFF

The 328P display chip must be set to rail-to-rail crystal operation, so the fuse bytes for that would be:
Low: 0xF7 (ie. CKSEL3 = 0)
High: 0xD9
Extended: 0xFF

The source and HEX files provided are for the flexible-startup option where external pull-downs are used to determine the operating mode. To simplify the circuit, these can be omitted and the code altered (minor change) if a specific configuration is always used at start-up.

Within SBCVideo.ASM I have already included the lines to change...

; mov configByte,J

Remove the ";" comment characters from the start of the "ldi" and "mov" lines and set the bits as needed. This will completely override the pull-down resistors so none of them are needed.
CONFIG_PAL = 1 for PAL or 0 for NTSC
CONFIG_N_TWI = 0 for two-wire, otherwise 4/8 bit will be used
CONFIG_8_OR_4_BIT = 1 for 8 bit or 0 for 4 bit (if two-wire has not been selected)
CONFIG_80_CHAR_PER_LINE = 1 for 80 char, 0 for 40 char
CONFIG_SINGLE_HEIGHT = 1 for normal, 0 for double-height


Standard ASCII is implemented for the main character set. Extended ASCII codes are implemented the same as for DOS. Control codes are standard ASCII where applicable - other codes added to allow control of the screen. The full implementation is shown below:

Video display control codes:
Hex (Decimal) and meaning
01 (01) - Cursor home (Standard ASCII)
02 (02) - Define cursor character (2nd byte is the curs character, or 00 to turn off) <--New for 3.0
03 (03) - Cursor blinking
04 (04) - Cursor solid
05 (05) - Set graphics pixel (next two bytes = x,y) <--New for 3.0
06 (06) - Reset graphics pixel (next two bytes = x,y) <--New for 3.0
08 (08) - Backspace (Standard ASCII)
09 (09) - Tab (Standard ASCII)
0A (11) - Linefeed (Standard ASCII)
0C (12) - Clear screen (Standard ASCII)
0D (13) - Carriage return (Standard ASCII)
0E (14) - Set column 0 to 79 (2nd byte is the column number) or 0 to 39 for a 40 char line
0F (16) - Set row 0 to 24 (2nd byte is the row number)
10 (16) - Delete start of line
11 (17) - Delete to end of line
12 (18) - Delete to start of screen
13 (19) - Delete to end of screen
14 (20) - Scroll up
15 (21) - Scroll down
16 (22) - Scroll left
17 (23) - Scroll right
18 (24) - Set font attribute for the current line (see elsewhere on this page for details) <--New for 3.0
1A (26) - Treat next byte as a character (to allow PC DOS char codes 1 to 31 to be displayed on screen)
1B (27) - ESC - reserved for ANSI sequences
1C (28) - Cursor right
1D (29) - Cursor Left
1E (30) - Cursor up
1F (31) - Cursor down
20 (32) to 7E (126) - Standard ASCII codes
7F (127) - Delete
80 (128) to FF (255) - PC (DOS) extended characters


So, to print "Hello" at column 18, row 10 on the monitor in BASIC would be as follows:
PRINT CHR$(14) ; CHR$(17) ; CHR$(15) ; CHR$(9) ; "HELLO"

Some ANSI/VT100 escape sequences have been implemented to allow programs such as Wordstar etc. to run on the CPM machine. The ones implemented are shown below (Esc = character 1B Hex, 27 decimal):
Esc[Line;ColumnH or Esc[Line;Columnf moves cursor to that coordinate
Esc[J=clear from cursor down
Esc[1J=clear from cursor up
Esc[2J=clear complete screen
Esc[K = erase to end of line
= erase to start of line
Esc[L = scroll down
Esc[M = scroll up
Esc- = turn off ANSI interpreter - NEEDED IF USING GRAPHICS otherwise char 1B can't be sent to the display  <--New for 3.0

The above example can also be implemented using escape sequences instead...
PRINT CHR$(27) ; "[18;10H" ; "HELLO"

The character font is not the same as supplied by Daryl. I recreated the complete normal and bold fonts from a bitmap of 8x8 characters for a CGA PC display (using a small VB program). Most appear identical, so I assume we used a similar source. However, I have included all characters, including those normally reserved for control codes.

ASCII codes 00 to 1F (and 7F) are reserved for control. However, the character map also has display characters with these values. These characters can be displayed by sending code 1A (26 decimal) before sending the code for the character. This prefix tells the display processor to store the actual character into the display and not treat it as a control code.
eg. in BASIC, to display the heart symbol (at 03) you can use the following...
PRINT CHR$(26) ; CHR$(3)


Each line can have it's own font defined. Cannot have mixed font types on the same line.
This can be:
40 Character normal
80 Character normal
40 Character bold
80 Character bold
40 Character normal double-height
80 Character normal double-height
40 Character bold double-height
80 Character bold double-height

This is illustrated here:

When "double height" is specified, internally, the current line gets set to "double-height top-half" and the following line gets the same font definition and  "double-height bottom-half".
The screen is always addressed as row 0..24, with double-height covering two rows.

The font attribute byte is defined as follows:

7 6 5 4 3 2 1 0
Graphics Spare Spare Spare Spare Double height Bold 80 chars

So, bold 80 char normal height is 0b0000011, or 0x03, normal 40 char is 0x00000000, or 0x00 etc.

To set it, send 0x18 (see above) followed by the attribute number to the display.

When characters on a line overflow onto the following line OR a linefeed is issued, the following line will also be set to the same font definition - no need to reset at each line.

When a cursor is put on a line (cursor up/down or set row command) the active font becomes whatever was on that line, so no need to reset the font to match the current line.

When a font select is sent, the current line will be set to that font (plus the lines that follow if text flows over the end, as described above).

When a clear screen is issued, the complete screen will take on the current font selection as default.

For double-height lines, the line position used is that line occupied by the top-half of the text.
Repositioning the cursor on to the second line of a double-height line will give strange results, because that will be reset to be top of a double-line.


Full 160x100 bitmapped graphics are supported. Two functions are included to set and reset pixels.

Whenever a set pixel or reset pixel command is issued, the X/Y coordinates determine which display line needs to be used for graphics. This is automatic, so no need to set graphics line attributes. If the line is currently set for text then the line is automatically cleared before setting the pixel.

Pixel coordinates are X = 0..159, Y = 0..100

Part of a screen is shown here:

So, Y=0..3 will occupy the space where the top text line would normally appear, 4..7 the next display line etc. The complete horizontal line becomes graphics mode.

To set a pixel, send 0x05, X, Y as three bytes.
To reset a pixel, send 0x06, X, Y as three bytes.

Internally, graphics are held as character blocks with each bit assigned as follows

0 1
2 3
4 5
6 7

So, instead of set/reset pixel, the cursor can be positioned on the line, the attribute then set to 0x80 (graphics) and individual characters sent as would be done for text lines. eg. a block with pixels 0,4,5 and 7 set would be 0b10110001, or 0xB1

Ensure the cursor is either turned off, or not positioned on a graphics line otherwise pixels will flash where the cursor is positioned.


The full character set (showing PC CGA 80 char bold), exactly as displayed, is shown here:

A close-up of an example display (running on my home-designed CP/M computer) on a green-screen PAL CRT monitor is shown here:

(brightness turned up so that you can see the scanlines - normally black background)



I hope this page has been useful.


To contact me, my current eMail address can be found here. Please note that this address may change to avoid spam.

Note: All information shown here is supplied "as is" with no warranty whatsoever, however, please let me know if there are any errors. All copyrights recognised.