Difference between revisions of "How to implement a character display API"

From Nutwiki
Jump to: navigation, search
(Low Level Graphics API)
 
m (1 revision imported)
 
(No difference)

Latest revision as of 17:02, 27 October 2016

A proposal for a graphics display device access method

This text was started based on the email thread "New displays, chained devices, best way or how to" (Ulrich Prinz, March 5th 2009).

The thread can be found at: http://www.nabble.com/New-displays%2C-chained-devices%2C-best-way-or-how-to-tt22340479.html#a22340479

Authors comment: (Ulrich Prinz) After a lot people ask for having display support in Nut/OS, I'd like to rework this text and go into more detail. This page is under construction, but feel free to add any comment.


Display Classes

Before going to the implementation we should get a quick overview over the important differences of the displays. In the following chapter it is not important of what technology a display is (LCD, OLED...) but how it can be interfaced to the system one refers.

We have two different classes of displays:

  • 'Dumb' Displays that only have row and columnbt drivers.
  • 'Intelligent' Displays that have local RAM for pixels and or characters.

Both of these types is available as monochrome or color display in LCD or OLED technic. As designing and programming a driver board for a dumb display is not an easy thing, we stick to the second type.

Display-Controller Interfaces

Intelligent displays incorporate a controller that refreshes the display constantly, provides the internal voltages and currents and gives us a nice set of commands to control the display or put nice graphics or text on it.

There are quite a lot of different controllers available for any kind of display. Even worse, these controllers often do not exist for more than a few month to years before beeing replaced by newer ones, or beeing discontinued.

  • Parallel controllers, 4..24bit with several control signals.
  • Serial controllers with TWI or SPI

Do not mix up parallel controllers with parallel displays. There are parallel displays, grayscale or color, that have a 24-bit parallel bus. But these displays need a Controller-Board that drives it. It's like LCD-TV or LCD Monitor.

If we stick to smaller displays that are available at low prices everywhere on the market, we find that the integrated controllers often provide several interfaces, selectable by one or two contacts. OLED Drivers like SSD1303 or SSD1325 can be used with 8-bit parallel or 3-wire/SPI Bus.

Parallel Interface

The parallel interface enables full access to the controller of a display. The user can write and read registers and display data. Unfortunately this connection needs 5..10 GPIO lines which are often rare. A second problem might be that the controller does not have a data-bus so reading and writing display data is a lot of byte-banging what slows down the application.

Serial Interface

In opposite to the parallel bus, most µControllers have an SPI interface. Unlike normal SPI-devices, a display often need two control lines. One is used for chipselect like in SPI specified. The second line selects between visual data and commands.

On the first look this interface is first choice for µController users, but it has some traps. The first trap is, that most small displays do not have a MISO line, so one cannot read the display. While that is not a problem for simple text displays or even small graphics, it is a big problem if one needs to implemt some visual effects. In worst case the application controller needs to be the one two classes up, as a local frame buffer needs to keep a copy of the complete display.

Visual Data

There is no rule how graphic display present data that is written to them. Some displays write incoming bytes in a row, som in a column. Some use a bit per pixel others a byte or two. Even these two chips from above, the SSD1303 and the SSD1325 have totally different byte to display representations. A lot of displays can be configured of how to interprete data, align the bytes, flip vertically or horizontally.

I take the SSD1303 and SSD1325 OLED driver chips as an example where I started to implement a driver for Nut/OS.

The SSD1303 can be configured to some display modes, but the normal one is to write bytes that represent 8 vertical pixels. So the display is filled with lines of 8 pixel height. With this alignment it is very easy to write a driver for simple character emulation. A font of fixed width and 5x7 pixels can be used. The data for the characters should be aligned vertical so they can be copied one by one to the display. This works fine at high speed on all platforms.

The SSD1325 display is capable of gray-scale. So several bits of a byte represent one pixel in a column. I don't remember but I think it was 2 bits per pixel, four pixels in one byte, vertically oriented.

Now, there it starts with decisions:
- You can go the fast way, by just doubling the hard coded font to 2 bytes per column. While doubling the footprint of the font in the flash is not an issue with controllers of 128k or more flash, but it is with all smaller ones. A nice sideeffectz of this implementation is, that you can add a filter that 'ands' the actual column byte written to the display with 0b10101010 or 0b01010101 and you have a simple switchable gray scale font.
- The other way is to do some bit-shifting. Font stays small and is double addressed. On the first run you take only the upper 4 bits, expand them to 8 bits and draw them, then you do a second loop. Problem with AVR is, that it only knows a single shift command. Other architectures can do multi-bit-shifts in one cycle. So here it needs some nifty code to be small and fast.
How about abstraction?
Normally systems that are used on different architectures or have different displays, use some unified low level grafix routines. So for a new display you only implement a pixel( x, y, c) and you can do what you want. Even character drawing will use this function.
Problem 1) It's slow as with every drawing of a pixel, you need to calculate the pixel position on the displays memory, shift the pixel to the right position inside the data byte, send two commands of 1..2 bytes + data to address the display memory and then write the byte.
Problem 2) It doesn't work with displays that are not readable, as setting a pixel in a byte where other pixel are in will clear the other pixels. This results in either you need a lot of ram for keeping a copy of the display locally or you have to use another display that allowes read access to its memory or you have to connect it with a parallel bus eating up your few GPIO pins you have.
And unfortunately most of the smaller displays are only readable in parallel mode but not in SPI mode...
An idea might be to divide the display in parts that take care of certain visual effects. In my tests I defined a lot of additional symbols or icons that show the systems status. These are drawn at fixed positions of the display. The main area is text only.
Shortcuts...

Nut/OS already has a small terminal implementation. This is a device where you can printf() to and it keeps the text in a buffer of a size that you define. It also cares about wrap around at line end and scrolling at and of the screen.
A simple LCD/OLED driver must only provide a character emulation and can then be chained to the terminal device. The terminal handles the text in a small buffer and if display activity needs refreshing or scrolling, the terminal driver will redraw the characters. But it only needs a buffer for the chars and not the complete display pixels.



This is the text copied from Haralds original display page. It will be modified as the above text is complete.

Connectivity API

The basic idea:
Accessing a graphics display device may be done by various SW layers:

  • high level graphics and text API (HW independent)
  • low level graphics API (as HW independent as possible)
  • device driver for a specific graphics controller (HW dependent)
  • connectivity API (i.e SPI, parallel I/F, ..)

SPI

An explanation of the SPI Bus Support can be found here: http://www.ethernut.de/en/documents/ntn-6_spi.html

As of 4.7.5 the following modules/functions build the SPI bus API stack:
(The following table may better belong onto a directly SPI related page ...)


  • include\cfg\spi.h (reserved, currently empty)
  • dev\spibus.c
    • NutRegisterSpiDevice()
    • NutSpiBusWait()
    • NutSpiBusSetMode()
    • NutSpiBusSetRate()
    • NutSpiBusSetBits()
BitbangAT91 SPI ctrlAVR SPI ctrl
  • dev\spibus_gpio.c
    • GpioSpiSetup()
  • dev\spibus0gpio.c
    • GpioSpi0ChipSelect()
    • SpiMode0Transfer()
    • SpiMode1Transfer()
    • SpiMode2Transfer()
    • SpiMode3Transfer()
    • GpioSpiBus0Transfer()
    • GpioSpiBus0NodeInit()
    • GpioSpiBus0Select()
    • GpioSpiBus0Deselect()
    • NUTSPIBUS spiBus0Gpio
  • arch\arm\dev\spibus_at91.c
    • At91SpiInterrupt()
    • At91SpiSetup()
    • At91SpiBusNodeInit()
    • At91SpiBusPollTransfer()
    • At91SpiBusDblBufTransfer()
    • At91SpiBusWait()
  • arch\arm\dev\spibus0at91.c and spibus1at91.c
    • At91Spi0ChipSelect()
    • At91SpiBus0Select()
    • At91SpiBus0Deselect()
    • At91SpiBus0Interrupt()
    • At91SpiBus0Transfer()
    • NUTSPIBUS spiBus0At91
  • arch\avr\dev\spibus_avr.c
    • AvrSpiSetup()
  • arch\avr\dev\spibus0avr.c
    • AvrSpi0ChipSelect()
    • AvrSpi0Interrupt()
    • AvrSpiBus0Transfer()
    • AvrSpiBus0Wait()
    • AvrSpiBus0NodeInit()
    • AvrSpiBus0Select()
    • AvrSpiBus0Deselect()
    • NUTSPIBUS spiBus0Avr


Device Driver

spi_oled is a driver that I write and it is optimized for SAM architecture.
Let's take it for an example even it is not available in Nut/OS until it is finished. Lot's of people ask for drivers like this, so I hope this run-through will help them how to connect the things right in Nut/OS.

Overview

In Nut/OS exist standarized device descriptors. These basic descriptors need to be filled so the system can recognize our device. NUTDEVICE is the struc that initally defines a device.
This struct contains a name for the device and 'links' to other structs and functions that are handling the device.

As we write a driver for an OLED connected to a SPI bus, we need to link to a NUTSPINODE struct that defines this hardware connection.

Further we want to use the existing terminal emulation of Nut/OS so we link to the TERMDCB struct.

As our display needs some memory to save corsor and pixel positions we write a new device control block struct called dcb_oled.
Unfortunately the oled_dcb cannot be linked from the driver as there is already the terminal dcb. I guess that the chances are higher that we have multiple terminals, but not multiple OLEDs on our system. So the dcb_oled is installed in the drivers initialization.
But the terminal needs some memory too. The charcter cursor position an a buffer for a virtual screen is needed, so we use the term_dcb and link to it.

Device Descriptor

It all starts with the device descriptor of the type NUTDEVICE <source lang="c"> /*!

* \brief oled display device implementation structure.
*/

NUTDEVICE devSpiOLED = {

   NULL,                                       /*!< \brief Pointer to next device, dev_next. */
   {'S', 'S', 'D', '1', '3', '0', '3', 0, 0},  /*!< \brief Unique device name, dev_name. */
   IFTYP_STREAM,                		/*!< \brief Type of device, dev_type. */
   0,                          		/*!< \brief Base address, dev_base (not used). */
   0,                         			/*!< \brief First interrupt number, dev_irq (not used). */
   &nodeSpiOLED,             			/*!< \brief Interface control block, dev_icb. */
   &dcb_term,                                  /*!< \brief Driver control block, dev_dcb. */
   TermInit,               			/*!< \brief Driver initialization routine, dev_init. */
   TermIOCtl,                                  /*!< \brief Driver specific control function, dev_ioctl. */
   0,                                          /*!< \brief Read from device, dev_read. */
   TermWrite,                                  /*!< \brief Write to device, dev_write. */
  1. ifdef __HARVARD_ARCH__
   0,                                          /*!< \brief Write data from program space to device, dev_write_P. */
  1. endif
   TermOpen,                                   /*!< \brief Mount volume, dev_open. */
   TermClose,                                  /*!< \brief Unmount volume, dev_close. */
   0                                           /*!< \brief Request file size, dev_size. */

}; </source>

These struct has to be filled with the low level functions of your driver. Let's go into some detail: <source lang="c">

   NULL,                                       /*!< \brief Pointer to next device, dev_next. */
   {'S', 'S', 'D', '1', '3', '0', '3', 0, 0},  /*!< \brief Unique device name, dev_name. */
   IFTYP_STREAM,                		/*!< \brief Type of device, dev_type. */
   0,                          		/*!< \brief Base address, dev_base (not used). */
   0,                         			/*!< \brief First interrupt number, dev_irq (not used). */

</source> This part describes the device, in out case it is a driver for a SSD1303 display controller.
NULL is a pointer to the device handler. It is filled automatically at registration of the device by calling <source lang="c">rc = NutRegisterSpiDevice( &devSpiOLED, &DEV_OLED_BUS, DEV_OLED_CS);</source> Next is the device string that can be used for listing devices or debug purposes. The string has to be exactly 8 characters and a trailing 0.
The Interface-Type is not used today but may be in the future.
The base address and interrupt number is not used by our driver as it uses a predefined interface, the SPI bus.
Same with the interrupt number.

With the data from above, the driver knows our device and can install it and present it to some standard system functions.
If you look from the driver model view, we have the connection to the upper software layer. What is missing now, is the connection to the hardware: As the SPI bus is a known interface to Nut/OS, we look for this 'node': <source lang="c">

   &nodeSpiOLED,             			/*!< \brief Interface control block, dev_icb. */
   &dcb_term,                                  /*!< \brief Driver control block, dev_dcb. */

</source>

Interfaces are nodes as many of them can handle more than one device. SPI for instance can handle as many device as there are chip selects.
There is a struct for each type of node that takes the required parameters like addresses, interrupts and so on:

<source lang="c"> NUTSPINODE nodeSpiOLED = {

   NULL,           /*!< \brief Pointer to the bus controller driver, node_bus. */
   NULL,           /*!< \brief Pointer to device driver specific settings, node_stat. */
   OLED_SPI_RATE,  /*!< \brief Initial clock rate, node_rate. */
   OLED_SPI_MODE,  /*!< \brief Initial mode, node_mode. */
   8,              /*!< \brief Initial data bits, node_bits. */
   0               /*!< \brief Chip select, node_cs. */

}; </source>

Here we have some data that is filled at the initialization, as it is dependant on the applications hardware. Other parameters are fixed and filled in as constants.
Dependand parameters are the bus and it's settings. A general driver does no know to which bus a display will be connected, it is different on your different hardware projects and different with any architecture. So this information will be filled somewhere else. The node only takes some pointers at the initialization time to find the bus from that time onwards.
The node_bus parameter is filled by NutRegisterSpiDevice() as well as the node_cs. So using the same display driver on different hardware versions or Other than the bus, some parameters like number of bits or maximum speed is special for the device itself. These parameters are defined here as constants.
The Chipselect is again filled by calling the NutRegisterSpiDevice( &devSpiOLED, &DEV_OLED_BUS, DEV_OLED_CS); routine, it's the last parameter of that function call.

Now that the driver knows to which bus will handle the data, he needs to know what to do with the data running through it.
The next block of the NUTDEVICE struct is a pointer to another interface of the device driver, the control interface. You can omit this interface by writing NULL to that entry, but we wanted to design a terminal like screen on our OLED and Nut/OS has a terminal driver available.
Any data send to our device is passed to this control interface, can be handled in special manor or filtered. In our case term.c gets all the data and interpretes it. It additionally has a screen buffer that saves every printed character to our display in a buffer and can do things like 'clear to end of line', 'home', 'next line' and other things.

If you follow the NUTDEVICE struct of the OLED driver you'll see that it not longer refers to the OLED specific drivers we want to write, it refers to the terminal drivers. That is, cause if we print to the display, first the terminal needs to buffer the characters. If we write our own character handler, we would have put our routines here. But why writing a terminal, if one exists...

<source lang="c">

   TermInit,               			/*!< \brief Driver initialization routine, dev_init. */
   TermIOCtl,                                  /*!< \brief Driver specific control function, dev_ioctl. */
   0,                                          /*!< \brief Read from device, dev_read. */
   TermWrite,                                  /*!< \brief Write to device, dev_write. */
  1. ifdef __HARVARD_ARCH__
   0,                                          /*!< \brief Write data from program space to device, dev_write_P. */
  1. endif
   TermOpen,                                   /*!< \brief Mount volume, dev_open. */
   TermClose,                                  /*!< \brief Unmount volume, dev_close. */
   0                                           /*!< \brief Request file size, dev_size. */

</source> All the above definitions at the end of the NUTDEVICE struct are pretty clear. You find, that we do not define a function for reading from the device. This is because we cannot read from the SSD1303 controller if it is used in SPI mode. Yes, you're right, we could read from the terminal emulation if there is a support.
A better idea might be to have a stdio interface. The display is the output, the input is not part of this text. May be some buttons or an IR-receiver.

At this point of the text, printf( oled, "HELLO WORLD\n") will put the string to our device. The device will pass it to the terminal and the terminal will put it in its screen buffer. Our driver knows on which bus the display resides and which /CS line has to be used.
How do we tell the terminal driver, what to do with a character that he receives besides keeping a copy in the buffer?
We attach our low-level driver routines that make pixel-graphics from the characters to the terminal: <source lang="c"> /*!

* \brief Terminal device control block structure.
*/

TERMDCB dcb_term = {

   SpiOledInit,         /*!< \brief Initialize display subsystem, dss_init. */
   oled_char,           /*!< \brief Write display character, dss_write. */
   OledPhCmd,           /*!< \brief Write display command, dss_command. */
   VtClear,             /*!< \brief Clear display, dss_clear. */
   VtSetCursor,         /*!< \brief Set display cursor, dss_set_cursor. */
   VtCursorHome,        /*!< \brief Set display cursor home, dss_cursor_home. */
   VtCursorLeft,        /*!< \brief Move display cursor left, dss_cursor_left. */
   VtCursorRight,       /*!< \brief Move display cursor right, dss_cursor_right. */
   VtCursorMode,        /*!< \brief Switch cursor on/off, dss_cursor_mode. */
   LCD_MF_COOKEDMODE \
   | LCD_MF_AUTOLF,     /*!< \brief Mode flags. */
   0,                   /*!< \brief Status flags. */
   OLED_ROWS,           /*!< \brief Number of rows. */
   OLED_COLS,           /*!< \brief Number of columns per row. */
   OLED_COLS,           /*!< \brief Number of visible columns. */
   0,                   /*!< \brief Cursor row. */
   0,                   /*!< \brief Cursor column. */
   0                    /*!< \brief Display shadow memory. */

}; </source> As every device, if it is a physical or a virtual, the terminal has a descriptor block too.
By reading the comments it is clear what every function does and that these functions are the ones we have to write for our display.
As any other control block, the vector is important, the name of the function can be choosen by the developer. I decided to name all functions that interface between the driver and the virtual terminal with a VT in front.

Why is a simple putchar() function not enough?
Our OLED doesn't know about characters, it only transforms the bits of incoming bytes to visible dots on the screen. The first function of this device control block refers to the driver function that prints a character to the screen, as the terminal doen't know how to handle pixels.
By writing a simple function to draw a character onto the screen, you'll find out, that you loose track of the position. That is, because a terminal allowes to go back some characters, jump aline up or down or even go to beginning or end of the screen. Second reason is, that your display may not fit exactly to multiples of your charcter widht. So we need to keep track of the pixel-position we are actually writing to.

Low Level Graphics API

This layer defines and handles pixels, lines, rectangles, and their positions within "areas" on the display. Based on the connectivity methode and the functionalities of the display, the low level objects are to be written directly into graphics memory or into a shadow area in main memory. Write only or read/write display memory may be handled here.

example functions: define_area(), set_pixel(), set_line(), set_rectangle(), set_triangle(), set_arc()

In case a "shadow RAM" is used before transferring the data to the display (or if the display allows to read its own RAM) a set of get_xxxxx() functions may also be implemented.

This part needs to be discussed as a shadow memory even for small displays will result in a high usage of RAM.

High Level Graphics and Text API

On this layer, the following graphic objects could be defined and handled: Button, Slider, Check Box, Radio Button, Edit Box, List Box, Group Box, Progress Bar, Picture, Dial, Meter.

All these objects (and text) are to be positioned within an "area".