The Raspberry Pi (RasPi) offers an excellent micro with a ridiculous
amount of memory for $35 or so. But even better, it comes with a
built-in GPU that can be used from bare-metal (non-Linux)
code. Add a keyboard and this becomes a great way to break the
tether to TeraTerm
(or Hyperterm
or whatever), and drive high-res video with your bare-metal
system. All you need is a bit of code.
My project reads characters from a PS/2 keyboard and displays the
characters on the RasPi's video display at 800x600 resolution.
The program uses a GPIO interrupt on the PS/2 clock line to accept the
character, then uses a bare-metal printf(), with support for
redirection, to update the display. The project contains a number
of libraries suitable for inclusion in your own projects. And the
executable image weighs in at just 12K bytes of code.
Although my project doesn't do color graphics (I am a text-based life
form), the video library in this project includes code for drawing
pixels of any color.
Note I am using a Raspberry Pi B (2 USB ports, 26-pin I/O connector at
P2). I have not yet ported this project to a RasPi-2. My
check of the web shows this probably won't be difficult, I just haven't
done it yet.
This is a display generated on a bare-metal RasPi at 800x600
resolution, using chars read from a PS/2 keyboard hooked to a couple of
GPIO lines.
There isn't much to the interface electronics. You only need a
couple of resistor dividers to drop the 5 VDC signals from the keyboard
down to the 3.3 VDC that the RasPi needs.
This is what the whole setup looks like. The grey ribbon cable
connects to the RasPi's P2 connector for GPIO and power for the
keyboard. I salvaged a short cable with a PS/2 keyboard connector
on the end for hooking to the keyboard.
Credits
The code in the following libraries is based on several
sources.
Getting started
You will need a toolchain. These libraries all use GCC for
compiling and linking. Rather than repeat all the information,
refer to Brian's pages above for downloading and setting up the
Launchpad
compiler suite; see Part 1 of the tutorials.
Note that Brian uses CMake for building his projects, while I use
make. Refer to the various makefiles in my projects for details.
I also use Visual Studio 2008 as my IDE. You may need to modify
the Launchpad setup to accomodate whatever IDE you choose.
You will need the
Broadcom
BCM2835
ARM Peripherals reference document. Be warned, this
is one of the worst MCU reference manuals I've ever had to use; poorly
written, confusing, full of errors and outright omissions. If
you're lucky, the information you want is in there somewhere.
Otherwise, it's the web or serious experimentation.
After you build your project, you will need to write the binary file to
your RasPi SD card as kernel.img. This replaces the original
kernel.img, which was a Linux image. Rebooting your RasPi will
load your new kernel.img file into memory at address 0x8000 and start
execution. Refer to Brian's tutorials for more details.
Target-agnostic libraries
I've worked to write my libraries so you compile them one time and
never edit them afterwards. I don't want to edit a PS/2 keyboard
library because project B uses a different pair of GPIO pins from
project A; that's just asking for trouble.
To support this target-independence, the libraries expect the main
program to register the GPIO pins involved. This means that when
the main program runs, it calls a specific init() function and passes
in identifiers for the GPIO pins in use. The library code uses
this info to initialize the correct pins and the interrupt
support. This lets you
define your I/O setup at run-time, not compile-time. You'll see
an example of this in the kbdtest.c code below.
PS/2
keyboard
I originally looked at connecting an USB keyboard to the
RasPi.
That's what the connectors are for, right? But I didn't feel like
digging into the complexities of USB, so I hit the web for
projects. I
did find a few sites where people had done bare-metal USB keyboards on
the RasPi,
but their comments often implied it was a work in progress. I saw
comments that the code worked for 50% of the keyboards, or certain
features were missing, or it locks up sometimes. Not very
encouraging.
Since I have a few PS/2 keyboards laying around (who doesn't, right?),
I decided it might be simpler just to hook one up to the RasPi.
Besides, this would let me try out GPIO interrupts.
First off, the PS/2 keyboard uses 5-volt logic. The RasPi inputs
cannot tolerate 5 VDC levels. Connecting a 5-volt peripheral to
the
RasPi is asking to blow up a few inputs, if not the entire chip.
Here is a schematic for a simple resistor divider that lets you safely
connect a PS/2 keyboard to the RasPi:
The code supporting the PS/2 keyboard lives in the ps2kbd library and
is based on Paul Stoffregen's code for the Teensy. I have changed
a few of the key mappings, such as the arrow keys and the backspace key.
The code uses a falling-edge on P2-16 (GPIO 23) to mark the valid edge
of the PS/2 CLK signal. This falling edge causes an
interrupt. The ISR handler records the state of P2-18 (GPIO 24)
as the DATA level, then adds this new bit of data to the accumulated
bits to build up the keycode. If all eight bits of a keyboard
byte have been accumulated, the ISR code adds the new byte to a holding
buffer and resets its counters for the next byte. Control then
leaves the ISR.
I have provided functions for checking availability of keyboard data
and for reading the next byte of data. Like Paul's original code,
my version does not output anything to the keyboard; no control of
LEDs, for example. I also do not handle the CAPS LOCK key or any
of the Window's specific keys.
Not writing data to the keyboard could cause an issue if you use an
adaptor from a USB keyboard to PS/2 connector; some adapters output a
polling sequence, waiting for the host to write back an acknowledge
command of some kind. Since this code doesn't write anything out,
this ack won't happen.
Note that unlike Paul's original code, my version does not directly use
an interrupt handler. Instead, the interrupt code is wrapped in a
callback function, which is called by other, higher-level code that
contains the true ISR. I'll provide more details on this feature
below.
GPIO interrupts
Most RasPi tutorials
start with using a GPIO to blink an LED. Brian covers this very
well in his tutorials, so I won't bother.
Eventually, though, you need to detect a change on a GPIO input and
trigger an interrupt. That is a bit trickier, since the Broadcom
BCM2835 interrupt system is not like anything I've used on other ARMs.
The BCM2835 funnels almost all interrupts (SPI, GPIO, timers, whatever)
through a single interrupt handler, the IRQ. (A second handler,
the FIQ (fast interrupt request), provides high priority for a single
interrupt source.) This means your interrupt service routine
(ISR) must wade through all the interrupt sources your code has
enabled, to find the source that caused the current interrup
t.
You enable a GPIO interrupt with these steps:
- Determine the GPIO pin to use
- Determine the edge to use; I'll be using a falling edge to detect
a change in the PS/2 keyboard CLK signal
- For a falling edge, modify the appropriate GPFENx register by
setting the bit cooresponding to the selected GPIO pin
- Clear the pending bit in the appropriate GPEDSx register by
setting the bit cooresponding to the selected GPIO pin
- Set the proper IRQ enable bit in the correct IRQ_ENABLEx register
- Enable global interrupts (usually done in main())
Here is the code for setting up the PS/2 keyboard:
void ps2kbd_Init(uint8_t
dataGPIO,
uint8_t clkGPIO,
const
PS2Keymap_t *map,
void (**Callback)(void))
{
uint32_t mask;
clkPinMask = (1
<< (clkGPIO % 32));
dataPinMask = (1
<< (dataGPIO % 32));
/*
* First, set the flags in the GPIO module so a falling edge
* on the CLK pin will set an interrupt flag. The
register
* for this is GPFEN0 for GPIO bits 0-31 or GPENF1 for GPIO
* bits 32-53.
*/
if (clkGPIO <
32)
{
EventDetStatusReg = &(BCM2835_GPIO->GPEDS0);
BCM2835_GPIO->GPFEN0 |= clkPinMask;
}
else
{
EventDetStatusReg = &(BCM2835_GPIO->GPEDS1);
BCM2835_GPIO->GPFEN1 |= clkPinMask;
}
/*
* The appropriate bit in the GPIO Event Detect Status
Register
* GPEDS0 or GPEDS1) will be set whenever the CLK input goes
low.
* To be safe, clear that bit now by writing a 1 to it.
*/
*EventDetStatusReg = clkPinMask;
/*
* Next, set the proper bit in the IRQ module so an IRQ
interrupt
* fires when the CLK pin goes low. The register for
this is
* IRQ_ENABLE2.
*
* Unfortunately, the BCM2835 ARM Peripherals manual
sucks. The
* table listing the interrupts from the ARM peripherals
includes
* the following GPIO interrupt information:
*
* IRQ 49 is gpio_int[0]
* IRQ 50 is gpio_int[1]
* IRQ 51 is gpio_int[2]
* IRQ 52 is gpio_int[3]
*
* However, this is the only place in the document where the
string
* "gpio_int" appears. No clue as to what the four
subsets of gpio_int
* mean or how to tell to which subset a GPIO pin belongs.
*
* By experimentation, I discovered that if clkGPIO is 24,
the GPIO
* interrupt flag I need is IRQ 49. No idea why that
is the case.
* The following code uses a /32 to arrive at a shift value
for
* creating an IRQ mask, but it could well be wrong.
If you use
* a clkGPIO other than 24 and this code breaks, dork around
with
* other flags until you find one that works. Sorry.
*/
mask = 1 <<
((clkGPIO / 32) + 17);
BCM2835_IRQ->IRQ_ENABLE2 = mask;
if (dataGPIO <
32)
{
InputReg = &(BCM2835_GPIO->GPLEV0);
}
else
{
InputReg = &(BCM2835_GPIO->GPLEV1);
}
keymap = map;
head = 0;
tail = 0;
*Callback =
&ps2kbd_Callback;
}
The calling routine specifies a GPIO bit number (0-52) for the CLK pin
and for the DATA pin. It also specifies a keyboard map, based on
Paul's original code; this can be a US, French, or German keymap.
Finally, the calling routine provides a location for storing the
keyboard-specific ISR handler function pointer.
This function pointer, known as a callback, lets the main program
define the ISR for all interrupts, but invoke the callback function so
the PS/2 code can deal with any incoming chars. This means the
main code is not responsible for handling chars or checking the
keymap. It also means the PS/2 library code doesn't know in
advance which GPIO pins are used.
Video and the mailboxes
The ARM part of the BCM2835 talks with the GPU part of the BCM2835
through mailboxes. These are a set of BCM2835 registers and
corresponding RAM buffers dedicated to exchanging info between the
processors. I will defer to Arjan's code and Github posts, as he
provides considerable detail on this and he has working examples of
bare-metal code. I have rolled his code into my video and console
projects here; read through the code for details.
Arjan's code includes functions for character I/O (like printing a text
string to the console). I have left his code intact but I don't
use it. I moved that functionality to a dedicated layer of
character support code; see my printf and chario files.
Note that I've modified Arjan's code slightly, to support an 800x600
display versus his original 800x480 display. Regardless of which
resuolution you use, you will likely need to modify the config.txt file
on your RasPi SD card. Open config.txt with an editor and locate
the lines that refer to the framebuffer_width and framebuffer_height
values. Edit these to match the WIDTH and HEIGHT macros you use
in console.c. In my case, I made these changes:
# uncomment to force a console
size. By default it will be display's size minus
# overscan.
framebuffer_width=800
framebuffer_height=600
When you reboot your RasPi with these settings, the video display will
default to 800x600.
If you run the console code and you get a smear of dots across the
display, it's likely because your framebuffer settings don't match the
settings in your video code.
printf and chario
printf() has always been an issue in the bare-metal world. The
newlib library was intended to provide a printf() (and other functions)
that work without an underlying OS, but I've never liked the newlib
concept. This is mostly because newlib requires some variant of
sbrk(), used by malloc() to allocate memory buffers. This is a
layer of complexity I am not comfortable with. So my solution was
to build a suite of printf() routines that do not use newlib.
Actually, I started with Arjan's printf code and modified it. It
now calls a putchar() function to write a character as part of
printf(). My putchar() uses a function pointer to route the
character to some active output device. Using a function pointer
for selecting the output device lets me redirect the output from
printf() on-the-fly. Like the traditional STDOUT stream
mechanism, I can send chars to the console, then change and send them
to a UART.
This character redirection happens in the chario library. The
function pointer to use is passed from the main program to the chario
routines through a registration function. There is one
registration function each for character output, character input, and
checking for available input chars. You can see this at work in
the following code from my kbdtest.c file:
/*
*
Hook
the character I/O system into the video console for output and the
*
PS/2
keyboard for input.
*/
chario_OutRegister((outcharFuncPtr)&console_putc);
//
aim printf at the video display
chario_InRegister((incharFuncPtr)&ps2kbd_Read);
// read
chars from PS/2 keyboard
chario_AvailRegister((availcharFuncPtr)&ps2kbd_Available); // check PS/2 kbd
for chars
printf("\n\rkbdtest\n\r");
I included getchar() in my printf library module, for reading a
character from an input device, simply because I didn't want to be
bothered with yet another library module. I also included two
routines for reading a string (with editing) from the input
device. One of these routines locks until it sees a carriage
return (CR), while the other version lets you run background tasks
until a CR is received. My test program uses this second version,
called get_line_r().
kbdtest
That's about it. All of the above code is rolled into a set of
libraries and used by a program called kbdtest. This program sets
up the video display and the PS/2 keyboard. It then displays a
title on the video screen and prompts you for input. Type some
chars (you can use backspace to erase mistakes) and hit Enter; the
program echoes your text.
Not a lot on the surface, but all of the important issues for character
I/O, GPIO interrupts, video control, and the other low-level details
are covered.
You can use this code a starting point for several RasPi
projects. Home control, hot-house monitoring, data logging; this
is a pretty solid foundation for your next RasPi project.
Thanks to the underlying libraries, the kbdtest program is pretty
small. Here's the whole kbdtest.c source file:
/*
*
kbdtest.c
basic PS/2 keyboard input test
*
*
Test
operation of a PS/2 keyboard wired to P1 on
*
the
Raspberry Pi.
*
*
PS/2
CLK line goes to P1-16 through voltage divider
*
PS/2
DATA line goes to P1-18 through voltage divider
*
PS/2
5 VDC line goes to P1-2 or P1-4
*
PS/2
GND line goes to P1-6
*
*
You
MUST use a voltage divider between the PS/2 CLK
*
or
DATA lines and the RasPi P1 connector! The RasPi
*
cannot
handle 5 VDC logic signals. If you don't
*
divide
the PS/2 signals down to 3.3 VDC, you will
*
damage
the RasPi!
*
*
Building
this code will yield a kernel.img file. Copy
*
this
file to the root directory in a RasPi boot SD card,
*
install
the card into your RasPi, power it up, and the
*
video
display should show the test results.
*
*
Note
that you will have to set the display parameters
*
in
your SD card's config.txt file to match those of
*
the
console display parameters. For example, if your
*
console
library is built to use 800 x 600 pixel display,
*
you
will need to edit your SD card's config.txt to
*
show
a framebuffer_width of 800 and a framebuffer_height
*
of
600.
*
*
Karl
Lunt 7 Aug 2015
*/
#include <stdio.h>
#include "target.h"
#include "bcm2835.h"
#include "raspi.h"
#include "chario.h"
#include "printf.h"
#include "console.h"
#include "systimer.h"
#include "ps2kbd.h"
static void
(*CallbackForPS2Kbd)();
int
c;
/** Main
function - we'll never return from here */
void kernel_main( unsigned int
r0, unsigned int r1, unsigned int atags )
{
int
index;
char buff[1024];
console_init();
/*
*
Hook
the character I/O system into the video console for output and the
*
PS/2
keyboard for input.
*/
chario_OutRegister((outcharFuncPtr)&console_putc); // aim printf at the
video display
chario_InRegister((incharFuncPtr)&ps2kbd_Read);
// read chars from PS/2 keyboard
chario_AvailRegister((availcharFuncPtr)&ps2kbd_Available); // check PS/2 kbd
for chars
printf("\n\rkbdtest\n\r");
/*
*
Define
the GPIO pins used by the PS/2 keyboard. Also select the
keyboard
*
map
and record the PS/2 function for handling keyboard interrupts.
*/
ps2kbd_Init(PS2_KBD_DATA_PIN, PS2_KBD_CLK_PIN, &PS2Keymap_US,
&CallbackForPS2Kbd);
_enable_interrupts();
/*
Never exit as there is no OS to exit to! */
while(1)
{
index = 0;
printf("\n\r> ");
while (get_line_r(buff, sizeof(buff)-1, &index)
== 0)
{
// I don't have any background
stuff, but if I did, it would go here.
}
printf("\n\rYou typed: %s", buff);
}
}
/*
*
Declare
the universal ISR handler for IRQ interrupts.
*
*
The
only interrupt we expect is from the PS/2 keyboard. Code
*
for
handling that interrupt is in the callback function recorded
*
from
our earlier call to ps2kbd_Init(). All we have to do is
*
invoke
that callback function and the keyboard interrupt is
*
handled.
*/
void
__attribute__((interrupt("IRQ"))) interrupt_vector(void)
{
CallbackForPS2Kbd();
// process any keyboard interrupt
}
Wrapping up
The source, header files, and make files are in
this ZIP archive. I have preserved
the folder structure I use in my development. The support folder
contains the project folders for all of the library code, while the
library folder contains the libxxx.a libraries. The common folder
contains the low-level source file used to configure the MCU on startup
and to provide the interrupt vector table. It also contains the
loader file (raspi.ld), which places the executable image in memory and
allocates RAM for the program.
You can read the individual make files (xxx.mak) to see how all of
these files play together. You don't have to maintain this
layout, of course. Feel free to rearrange as best suits your dev
tools.
OK, that's it. Another long post, but this was a fun
project. If you hit any snags, drop me an email.
Home