tvText

This library allows you to generate text display on a PAL TV using an ATmega168 running at 20MHz and two resistors to form a 2-bit DAC. The video signal generation is interrupt-driven, leaving some CPU time over for your own program.

The current specifications are as follows:

The current version of the library, accompanying documentation and the source code for the demo program shown above can be downloaded from here.

Developer Manual

tvText is a library for use with Atmel AVR microcontrollers to provide text output on a television set using nothing more than two resistors and a 20MHz resonator.

It has been primarily developed for use with the ATmega168, but should be portable to other AVRs.

It outputs the picture signal on the most-significant bit of a user-defined I/O port. The remaining bits in the PORT register will be effectively filled with random data, but can be used as input pins (watch out for the internal pull-up resistors being enabled!) The sync signal is output on a single pin on one other I/O port – the rest of that port can be used as normal.

The source code is made available under the MIT licence. In short: do with it what you will.

Hardware

Two resistors are used to form a 2-bit DAC to generate the voltages required for composite video – 0V for sync, 0.3V for black and 1V for white. Assuming a 75Ω resistance in the television, the ideal resistor values are 450Ω and 900Ω – however, 470Ω and 1KΩ work just as well. The AVR needs to be run at 20MHz, so use an external 20MHz resonator and suitable load capacitors. Remember to set the fuses to use an external resonator and disable CKDIV8!

tvText circuit diagram

A simplified circuit diagram is shown above, showing the resonator and resistor DAC connections. The smaller resistor (450Ω) needs to be connected to the most significant pin on the TVTEXT_PICTURE_PORT (PD7 by default). The larger resistor (900Ω) needs to be connected to the pin defined as the sync pin by the TVTEXT_SYNC_PORT and TVTEXT_SYNC_BIT settings (PB0 by default).

Consult the data sheet for your AVR for more detailed instructions.

Software

This package contains the following files:

The various configuration options, functions and variables are discussed below.

Configuration

The config.h file can be modified to change the pins that are used to output the video signal.

For performance reasons, an entire I/O port is used to output the picture data (a 1 bit for white pixels and a 0 bit for black pixels). The picture data is always output on the most significant bit of this port. You can change this port by changing the TVTEXT_PICTURE_PORT and TVTEXT_PICTURE_DDR definitions (both must refer to the same port).

The pin used to generate synchronisation pulses can have any pin number within the port (configured with TVTEXT_SYNC_BIT), and may be any port other than the one used for picture data (configured with TVTEXT_SYNC_PORT and TVTEXT_SYNC_DDR).

Every scanline (row) of the generated video signal is output twice. Enabling the TVTEXT_SKIP_ALTERNATE_ROWS configuration switch will cause the driver to skip alternate rows (outputting a plain black scanline instead), only outputting each scanline once. This greatly reduces the amount of time the driver needs to spend outputting the video signal, at the expense of picture quality.

The width of characters can be controlled with the TVTEXT_CHARACTER_WIDTH setting. This can be 6 (for 6×8 characters in a 32×16 grid) or 8 (for 8×8 characters in a 24×16 grid). This setting will control the value of TVTEXT_BUFFER_WIDTH, which represents the width of the text buffer in characters.

Set TVTEXT_FONT to the name of a font file if you'd like to replace the default. This should be a file path in "quotation marks", relative to the tvtext directory.

Some TVs will crop some pixels from the left and right hand side of the image. You can avoid this by enabling TVTEXT_SQUASHED_HORIZONTALLY, which reduces the width of the fourth and seventh pixel (if applicable) column in every character to 4/5 the usual width. This isn't too noticable with a full screen of text (less noticable than text getting cropped from the sides of the display!) but can show up in certain graphical designs. For this reason, it is recommended that you enable it if you intend on using full screens of text, but leaving it disabled if you're using a more graphical output (e.g. a game) and keep away from these edges. In both instances, it is recommended that you provide the user with some way to set tvtext_offset_x to fine-tune the horizontal position of the display.

Variables

The functions of the variables are summarised below.

Variable Type Description Default
tvtext_flags uint8_t This variable stores a number of flags that control the appearance of the behaviour of the text console:
  • TVTEXT_VISIBLE – When set, the display is visible as normal. When cleared, the display is blanked to black if TVTEXT_INVERTED is cleared, white if TVTEXT_INVERTED is set.
  • TVTEXT_AUTO_SCROLL – If the cursor runs off the bottom or top of the viewport when this flag is enabled, the viewport scrolls up or down automatically and the console is placed on the bottom or top row of the display as appropriate. If the cursor runs off the bottom or top of the viewport and this flag is not enabled, no scrolling occurs and the cursor wraps back to the top or bottom of the display as appropriate.
  • TVTEXT_CURSOR_ENABLED – When set, a flashing character is displayed at the location of the cursor. When disabled, no cursor is shown.
  • TVTEXT_CURSOR_VISIBLE – This flag stores the current state of the flashing cursor. If it is set, the cursor character is displayed at the cursor location, otherwise the character in the cell under the cursor is displayed. This flag has no effect if the TVTEXT_CURSOR_ENABLED flag is disabled.
  • TVTEXT_INVERTED – Set this flag to invert the output video (black text on a white background). When cleared the default of white text on a black background is used.
To set a flag, use a bitwise OR operation. To unset a flag, use a bitwise AND operation:
  • tvtext_flags |= _BV(TVTEXT_AUTO_SCROLL); // Enables auto-scroll.
  • tvtext_flags &= ~_BV(TVTEXT_CURSOR_ENABLED); // Disables the cursor.
TVTEXT_VISIBLE, TVTEXT_AUTO_SCROLL, TVTEXT_CURSOR_ENABLED and TVTEXT_CURSOR_VISIBLE are set.
tvtext_buffer char[] This array stores every character on the screen in left-to-right, top-to-bottom order. The address of any particular character on the screen can be calculated with row * 32 + column. Filled with the clear character (tvtext_cleared).
tvtext_cursor_column
tvtext_cursor_row
int8_t These variables store the column and row that the text cursor resides in. If you change the cursor position manually, please ensure that it is within the text viewport before calling any of the text output functions – it is usually safer and easier to call tvtext_cursor_move() instead. 0
tvtext_cursor char This is the character used to represent the flashing cursor (if enabled). '_'
tvtext_cleared char This is the character used to clear the viewport when tvtext_clear() is called or to fill in new rows or columns when the viewport is scrolled. ' '
tvtext_viewport_left uint8_t These four variables define the edges of the current text viewport. All coordinates are inclusive – a left edge of 0 and a right edge of 31 results in a viewport 32 characters wide. If changing these values manually, please ensure that the cursor is moved inside the viewport before calling any of the text output functions – it is usually safer and easier to call tvtext_set_viewport() instead. 0
tvtext_viewport_bottom 15
tvtext_viewport_right TVTEXT_BUFFER_WIDTH-1
tvtext_viewport_top 0
tvtext_cursor_flash_period uint8_t The period (in frames) that it takes the flashing cursor to toggle between being shown and being hidden. A value of 50 would show the cursor for 50 frames (one second) then hide it for 50 frames (another second). 16
tvtext_cursor_flash_timer uint8_t This timer counts down every frame. It starts at tvtext_cursor_flash_period, counts down to zero, then inverts the cursor state and restarts. N/A
tvtext_frame_counter int16_t This counter is incremented every time a frame is completed and the rasteriser enters vertical sync period. Feel free to modify it and use it for your own timing purposes. 0
tvtext_offset_x uint8_t This variable can be used to move the displayed text area left or right on the screen to some extent. A larger value moves the display right, a smaller value moves it left.
Each unit corresponds to 3/5 of a pixel's width. You must ensure that this value is greater than or equal to zero, and less than or equal to TVTEXT_OFFSET_X_MAX – if you exceed these values, the library will generate an invalid video signal that your TV will not be able to display correctly.
It is recommended that you provide the user with some way to set this variable, to ensure that the text buffer is centred correctly on their TV and not have pixels from the left or right hand side cropped.
TVTEXT_OFFSET_X_DEFAULT
tvtext_offset_y int16_t This variable can be used to move the displayed text area up and down on the screen. A positive value moves the display down, a negative value moves it up.
The active part of the TV display is 304 scanlines tall. Each row of the 192×128 pixel display is doubled vertically to fill two scanlines, making the displayed text area 256 scanlines tall. 32 scanlines above and 16 scanlines below this text area are left blank.
To move the entire display down one 8 pixel row, you would need to set this variable to 16 due to the line doubling.
0

Functions

The only function you need to call is tvtext_init() – you can output text by directly writing to the text buffer. Several other functions are provided to help you get text on the display more easily, however.

void tvtext_putc(char c);

This function writes a single character to the text buffer and advances the cursor.

If the cursor moves off the side of the viewport, it is advanced to the next line. If the cursor was previously on the bottom line of the viewport and TVTEXT_AUTO_SCROLL is enabled, the viewport scrolls up and the cursor is placed back on the bottom line. If the cursor was previously on the bottom line of the viewport and TVTEXT_AUTO_SCROLL is disabled, it wraps back around to the top of the viewport.

for (char c = 'A'; c <= 'Z'; ++c) {
    tvtext_putc(c);
}

If the character value is in the range 0~31 or is 127, a special function is carried out. These functions aim to replicate those found on the BBC Micro and in subsequent versions of BBC BASIC.

Code Function
0 Nothing. This value is ignored.
4 Enables auto-scrolling mode (sets the TVTEXT_AUTO_SCROLL bit in tvtext_flags).
When the cursor moves below the bottom of the viewport the viewport is scrolled up and the cursor is moved back to the bottom line.
When the cursor moves above the top of the viewport the viewport is scrolled down and the cursor is moved back to the top line.
5 Disables auto-scrolling mode (clears the TVTEXT_AUTO_SCROLL bit in tvtext_flags).
When the cursor moves below the bottom of the viewport it is moved back to the top line.
When the cursor moves above the top of the viewport it is moved back to the bottom line.
8 Moves the cursor left one character.
If the cursor moved beyond the left edge of the viewport it is moved up a line and to the right edge of the viewport.
9 Moves the cursor right one character.
If the cursor moved beyond the right edge of the viewport it is moved down a line and to the left edge of the viewport.
10 Moves the cursor down one line.
If the cursor moved beyond the bottom edge of the viewport then either the viewport scrolls up and the cursor is moved to the bottom line or the cursor is moved to the top edge of the viewport, depending on the setting of TVTEXT_AUTO_SCROLL.
11 Moves the cursor up one line.
If the cursor moved beyond the top edge of the viewport then either the viewport scrolls down and the cursor is moved to the top line or the cursor is moved to the bottom edge of the viewport, depending on the setting of TVTEXT_AUTO_SCROLL.
12 The viewport is cleared. The character used to clear the viewport is controlled with the tvtext_cleared variable.
13 The text cursor is moved to the left edge of the display.
23 After writing this character, you must write a further nine bytes of data. The action performed depends on the values you have written.
  • Switching the cursor on and off – Firstly, write a 1 to indicate that you wish to change the cursor. Secondly, write a 0 to disable the cursor or a 1 to enable the cursor. Finally, write seven further zeroes to make up the nine bytes that this command requires.
  • Scrolling the display – Firstly, write a 7 to indicate that you wish to scroll the screen. Secondly, write a 0 if you'd like to scroll the current viewport or a 1 if you'd like to scroll the entire screen. Thirdly, write the direction you wish to scroll:
    • 0 – Scroll right one character.
    • 1 – Scroll left one character.
    • 2 – Scroll down one row.
    • 3 – Scroll up one row.
    Finally, write six further zeroes to make up the nine bytes that this command requires.
26 This command resets the viewport to fill the entire screen and moves the cursor to its top left.
27 The character written immediately after this one goes directly to the screen. This allows you to print special characters in the 0~31 or 127 range.
28 The next four bytes written to the screen are interpreted as the new left, bottom, right and top edges of the viewport respectively.
If the cursor is outside the new viewport, it is moved to its top left – otherwise, it is left alone.
30 The cursor is moved to the top left of the current viewport.
31 The next two bytes written to the screen are interpreted as the new column and row of the cursor respectively.
The cursor will be clipped inside the current viewport.
127 The cursor is moved one character left. The character under the new cursor position is then cleared using the tvtext_cleared variable.

void tvtext_puts(const char* s)

This function writes a NUL-terminated string held in SRAM to the display using the tvtext_putc() function.

tvtext_puts("Hello, world!");

void tvtext_puts_P(const char* s)

This function writes a NUL-terminated string stored in program memory to the display using the tvtext_putc() function.

tvtext_puts_P(PSTR("Hello, world!"));

This function is strongly recommended over tvtext_puts() for constant strings as it doesn't require a copy of the string to be stored in SRAM.

void tvtext_cursor_left(void)

Moves the cursor left one character.

If the cursor moved beyond the left edge of the viewport it is moved up a line and to the right edge of the viewport.

void tvtext_cursor_right(void)

Moves the cursor right one character.

If the cursor moved beyond the right edge of the viewport it is moved down a line and to the left edge of the viewport.

void tvtext_cursor_down(void)

Moves the cursor down one line.

If the cursor moved beyond the bottom edge of the viewport then either the viewport scrolls up and the cursor is moved to the bottom line or the cursor is moved to the top edge of the viewport, depending on the setting of TVTEXT_AUTO_SCROLL.

void tvtext_cursor_up(void)

Moves the cursor up one line.

If the cursor moved beyond the top edge of the viewport then either the viewport scrolls down and the cursor is moved to the top line or the cursor is moved to the bottom edge of the viewport, depending on the setting of TVTEXT_AUTO_SCROLL.

void tvtext_cursor_clear(void)

The viewport is cleared. The character used to clear the viewport is controlled with the tvtext_cleared variable.

void tvtext_cursor_scroll_right(void)

Scrolls the viewport contents right one character.

void tvtext_cursor_scroll_left(void)

Scrolls the viewport contents left one character.

void tvtext_cursor_scroll_down(void)

Scrolls the viewport contents down one line.

void tvtext_cursor_scroll_up(void)

Scrolls the viewport contents up one line.

void tvtext_reset_viewport_cursor_home(void)

Resets the viewport to fill the entire screen and moves the cursor to its top left.

void tvtext_set_viewport(int8_t left, int8_t bottom, int8_t right, int8_t top)

Sets the boundaries of the text viewport. If the cursor is outside the new viewport, it is moved to its top left – otherwise, it is left alone.

void tvtext_cursor_home()

The cursor is moved to the top left of the current viewport.

void tvtext_cursor_move(uint8_t column, uint8_t row)

Moves the cursor to a particular location on the screen. The cursor will be clipped inside the current viewport.

void tvtext_wait_vsync(void)

Waits for the rasteriser to enter the vsync period. During this period the library will be generating sequences that indicate that one frame is about to end and the next is about to begin. These vertical synchronisation sequences are fortunately quite simple and so the AVR has more time to spend running your program. Updating the text buffer between frames also reduces visual tearing, producing a smoother display. You may also use this function for timing purposes – at 50Hz, calling this function fifty times in a row will produce a one second delay.

void tvtext_cursor_reset_flash(void)

This function sets TVTEXT_CURSOR_VISIBLE and resets tvtext_cursor_flash_timer to tvtext_cursor_flash_period. It is automatically called whenever the cursor is moved.

uint8_t tvtext_get_font_row(char c, uint8_t row)

This function can be used to retrieve a row of pixels from a particular character in the font.

The returned value stores the first pixel in the most significant bit, the second pixel in the next most significant bit, the third in the next most significant bit after that and so on. A set bit indicates a white ("on") pixel.

tvtext_clear();
// Loop over every pixel in the character.
for (uint8_t row = 0; row < 8; ++row) {
    // Fetch a row of pixels from the font for the '@' sign.
    uint8_t pixels = tvtext_get_font_row('@', row);
    for (uint8_t column = 0; column < 6; ++column) {
        // Is the bit set (on?) If so, draw it on the screen as a '*'.
        if (bit_is_set(pixels, 7 - column)) {
            tvtext_cursor_move(column, row);
            tvtext_putc('*');
        }
    }
}

References

The following links were extremely useful when writing this library.