Raspberry Pi console on a 128×160 TFT display

It can be really annoying if you want to connect to a headless Raspberry Pi which has been assigned a (yet) unknown DHCP address. In this posting, I will show how I added a 128×160 pixel TFT display to the Pi in order to display and even change the network configuration on the fly.

Hardware part

My primary intention was to find a display that would fit into my transparent RS Components Raspberry Pi case, such that the display could be mounted on the inside, which would give it some protection. Due to the case’s dimensions, the maximum display board height was approximately six centimeters. I did hence order a SainSmart 128×160 pixel TFT display from eBay, which turned out to use a ST7735 controller. The PCB is 5.0cm long and 3.4cm wide, and fits nicely into the case. Well, after changing the right angle pin headers for straight ones, that is.

Connecting the display was a straightforward task. I hooked the SPI pins up to MOSI and SCK (unidirectional – the MISO pin is left unconnected), and attached the Chip Select (CS) pin to CE0. The display has two further inputs that I attached to the Raspberry, namely the data/control pin (DC) and the reset (RES) signal, which I attached to GPIO pins 24 and 25. The display runs on 5V, so I used the supply voltage from pin 2 of the Raspberry’s P1 connector.

Software part

I actually needed several tries to get the display working seamlessly. If you are only interested in the outcome, click here to jump to the relevant part. Otherwise feel free to follow the full story.

Approach #1: Writing a daemon to display the IP address

My first thought was to do everything in userland and use the wiringPi library and existing Arduino code for the display’s initialization and pixel drawing from the Adafruit ST7735 library.

The following steps were required to accomplish this:

  • Installation of rpi-update by following the instructions on https://github.com/Hexxeh/rpi-update, and running it to update the Raspberry Pi’s firmware.
  • Installation of wiringPi by checking the latest version out from its git repository, and following its compilation instructions.
  • Adapting the code example that ships with the display to work on Raspbian. This basically included setting the GPIOs for Chip Select, Data/Control and Reset to outputs and wiring them accordingly.
  • I did also use the bitmap fonts provided by Benedikt K. on the mikrocontroller.net board in order to realize larger font sizes.

The result was that the display works, but updates very slowly. When writing text to the display, a complete screen takes more than three seconds to fill. This results from the fact that one pixel is painted at a time in the example application. Yet, in order to paint this pixel, the bounding box for the paining area must be set prior to transmitting the desired color information, which incurs a significant overhead.

The only solution to this issue would be to set the bounding box to the complete display size and send all pixels in a burst transfer, which motivated me to move to the kernel space and try the following approach.

Approach #2: Registering the display as a framebuffer device

When searching for support of the ST7735 in Linux, I came across a driver provided by ngreatorex, which is supposed to register the LCD display as a framebuffer device. The framebuffer offers higher flexibility with regard to the contents that shall be displayed (it can, e.g., also be used as output device for mplayer), and also uses aforementioned burst operation to draw the full screen at once. A first comparison of the source code against the previously mentioned Arduino library yielded only slight differences (apart from two errors in the control sequences for the ST7735_CASET and ST7735_RASET commands – a value of 0x00 is transmitted too often in both cases). Still, the solution appeared feasible and was worth trying.

In order to compile kernel modules like this framebuffer driver, both the kernel sources and the Module.symvers file are required. As none of them are shipped with Raspbian, I checked the kernel sources out (following these instructions), copied the kernel configuration file from the /boot/ partition, and compiled the kernel locally on the Pi. After approximately eight hours, the compilation of my brand new kernel was finished, and it was time to compile the modules as well. Once this has also completed, I backed up my old kernel and copied the new one (a file called Image) from the /arch/arm/boot subdirectory of the kernel tree to the /boot/ directory. I recommend to also copy the file System.map, which has been created in the root directory of the newly compiled kernel to /boot/. Finally (but this is my personal preference), copy .config to /boot in order to save the hassle of re-configuring the kernel should a recompilation become necessary. I did also install the kernel headers by running sudo make headers_install (although I figured later that this would not have been necessary).

When I tried to compile the ST7735 module now, an error message saying that “deferred IO for graphics” was required popped up. As I wasn’t sure if there would be further dependencies in the kernel configuration, I added the PicoLCD driver with framebuffer support as a kernel module. This module also relies on deferred graphics IO and thus inserts all required lines into the kernel configuration.

 Device Drivers --->  
    HID Devices (HID_SUPPORT [=y]) --->  
       Special HID drivers --->
          <M> PicoLCD (graphic version)
          [*]   Framebuffer support

The addition of the PicoLCD module in make menuconfig leads to the line

CONFIG_FB_DEFERRED_IO=y

being added to the kernel configuration in the graphics support section. Once the kernel and all modules have been compiled (again), the ST7735 module can be compiled without errors.

The library linked required some minor adaptation to reflect the GPIO pins used for the Reset and Data/Control pins. Once everything was set up and the ST7735 framebuffer module was compiled and installed using sudo make modules_install, the time was right to check whether it could be inserted into the running kernel by running sudo modprobe -v st7735fb. Although the module (and its dependencies) were loaded successfully, nothing changed on the TFT display. Loading the second module, rpi_st7735fb, which caters for defining the GPIO connections, failed with the message “ERROR: could not insert ‘rpi_st7735fb’: Invalid argument”. A look at dmesg yields the answer: “Driver [spidev] already registered for spi0.0”. My first though was if this was some relict from my wiringPi installation. In fact, however, when unloading the spidev module, the error message disappeared upon the insertion of the newly compiled module. Still, the display just stayed blank.

Third approach: Use the code provided by Kamal Mostafa

Kamal is a Ubuntu kernel developer, so he knows what he’s doing. In this particular case, he has connected an Adafruit display with ST7735 driver to his Raspberry Pi. The most convenient way would probably have been to just download his modified kernel source. I did, however, manually incorporate the changes to the kernel, namely removing the SPI port 0 binding in the file ./arch/arm/mach-bcm2708/bcm2708.c by eliminating lines 588-593:

  .chip_select = 0,
  .mode = SPI_MODE_0,
}, {
  .modalias = "spidev",
  .max_speed_hz = 500000,
  .bus_num = 0,

Again, a recompilation of the kernel is required, but as the SPI port 0 (i.e., the one where the LCD display is attached) is now free for use by the framebuffer, everything works nicely and dmesgnow reads

[   70.931883] fb1: ST7735 frame buffer device,
[   70.931894] 	using 40960 KiB of video memory
[   70.931901] 	rst_gpio=24 dc_gpio=25

After incorporating the other changes to /etc/inittab, as proposed on Kamal’s website, the Raspberry Pi now displays a login console after bootup, which can be used to directly check the network configuration and or adapt it using a keyboard connected to the Pi’s USB port.

Keypad plus Teensy++

I re-discovered a 2×2 keypad from Sparkfun (COM-09277), which I ordered several months ago, but only managed to do some very preliminary experiments. The keypad has RGB LEDs installed underneath each button (see photo on the left), which need to be multiplexed due to the shared connections. In this post, I’ll describe how I assembled the keypad into a case and configured it as a PC keyboard. I use it to convert key combinations to pre-defined text fragments (email footers, long passwords, common terminal commands, etc).

Hardware part

I had wired up the unit already, and initially connected it to a Teensy 2.0 on a breadboard (which can actually be seen in the background of the photo), on which I tried out different timer frequencies for multiplexing the LEDs. I didn’t like the fragile character of the breadboard, though. So I set out on the quest to integrate the complete circuit into a decent case. The 2×2 keypad measures roughly 50x50mm, and its mounting holes were approximately 5mm off the sides. So I set out to find a case that was slightly larger than the keypad, and at the same time had supports that fit the mounting holes of the keypad. This wasn’t a trivial task, and I eventually settled on a grey Strapubox 2412 case (black ones were not in stock, unfortunately), which was only slightly larger than the keypad, but the mounting standoffs were not really aligned with the keypad, so some little modifications were necessary. By extending the holes of the bezel and the keypad PCB, the PCB now neatly fits into the case, into which I have also cut the four holes of 16x16mm square using a dremel to cut along the markings roughly, and a scalpel to smooth the edges later on. The little pin in the center of the case also had to be taken out to allow the bezel to fit inside. The resulting upper case now perfectly fit the keypad PCB, which occupies about half of the available vertical space in the case. The remaining 8mm of height are more than sufficient to place a microcontroller board inside. For this project, I used a Teensy++ 2.0 device, which I also found hiding in my component stash. The nice side effect is that the Teensy is exactly two inches long and hence the same length as the keypad PCB. I soldered a 90 degree pin header with 2 contacts to the reset and GND pins to give it additional support against the back panel. All remaining space was filled using pieces of (non-conductive) styrofoam which came shipped with the pin headers.

    

Software part

I didn’t feel like re-inventing the wheel, so I based my code design on the USB Keyboard code provided on the Teensy website. In addition to the existing code, I created two C files that would read the keypad status and multiplex the LEDs to have them display the desired color.

Buttons

My button code is based on a periodic timer (I used Timer 1), which I configured to fire every 32ms (more precisely, I set the prescaler to 1024, resulting in 16MHz/1024 = 15625 ticks per second. The timer is configured to fire when OCR value of 500 (i.e., 32 milliseconds) is reached and to reset OCR to zero when fired).

TCCR1B = (1<<CS12)|(1<<CS10)|(1<<WGM12);
TIMSK1 |= (1<<OCIE1A);
OCR1A = 500;

Whenever the interrupt handler is called, I store the previous button state in a variable called prevState, followed by polling the button states and storing them in the currentState variable. The debounced button state is then returned by the function getButtonStates(), which performs a logical AND operation of the current and the previous readings.

uint8_t prevState, currentState;
ISR(TIMER1_COMPA_vect) {
  prevState = currentState;
  currentState = [...read button states...]
}

uint8_t getButtonStates(void) {
  return (prevState & currentState);
}
LEDs

The LED driver also relies on a timer, but needs a somewhat higher frequency, though. I used Timer 0 and configured it to use a prescaler of 64 and an overflow value of 96 (in fact it took me quite a while to optimize between LED brightness and visible flicker). That’s one call every 384 microseconds. The interrupt handler code is based on a state machine, which traverses all colors of all LEDs and activates the corresponding pins if required. The desired primary colors are stored in the ledState[4][3] double array, the first index denoting the LED that is addressed, and the second index the three colors that can be set.

ISR(TIMER0_OVF_vect) {
  if (state >= 16) {
    state = 0;
  }

  switch (state) {
  case 0:
    TURN_BLUE_OFF; TURN_LED_4_OFF; 
    SET_LED_1_ON; 
    if (ledState[0][0]) { TURN_RED_ON; }
    break;
  case 1:
    TURN_RED_OFF;
    if (ledState[0][1]) { TURN_GREEN_ON; }
    break;
  case 2:
    [...]
  default:
    asm("nop");
  }
  state++;
}
Main program

Finally, three actions were inserted into the infinite loop of the main program. Firstly, a screensaver was activated which would indefinitely cycle each LED through the available colors if no button is pressed. Secondly, a function was written that takes any button presses and writes them to an array. Thirdly, I included code to check this array for known sequences and to emit the corresponding USB keystrokes.

#define ANIMATION_DELAY 5 /* Default animation delay in cycles */
uint8_t animation = ANIMATION_DELAY;
uint8_t color = 1;
uint8_t keyorder[8];
uint8_t repetitionLock = 0;

while (1) {
  uint8_t debouncedStates = getButtonStates();

  /* SCREENSAVER */
  if (debouncedStates == 0) {
    if (animation == 0) {
      for (uint8_t i=0;i<8;i++) keyorder[i] = 0; /* Clear key buffer */
      setLed(color%4, (color%7)+1, 0); 
      if (++color==28) color = 0;
      animation = ANIMATION_DELAY;
    } else animation--;	/* Wait for next animation step */

  /* END SCREENSAVER, BEGIN BUTTON PRESSED HANDLER */

  } else {
    if ((repetitionLock == 0) || (keyorder[0] != debouncedStates)) {
      for (uint8_t i=7;i>0;i--) keyorder[i] = keyorder[i-1];
      keyorder[0] = debouncedStates;
      repetitionLock = 10; /* 200 ms before key is accepted again */
      animation = 10 * ANIMATION_DELAY; /* wait for more input */
    }
  }
  /* END BUTTON PRESSED HANDLER */ 

  /* PATTERN DETECTION */
  [...match keyorder array against known sequences...] 
  [...emit USB keystrokes according to the detected sequence]
  /* END PATTERN DETECTION */

  if (repetitionLock > 0) repetitionLock--;
  _delay_ms(20);
}

Result

In the picture below one can see the semi-final version (it still features the connection to an external reset button for easier programming on the left) in screensaver mode, i.e., cycling the LEDs. I tend to still make minor modifications to the code, but consider the hardware part of the device pretty final.