F# meets the Raspberry Pi

by Pezi 2. March 2013 22:58

I got a Pi for my birthday! A great excuse to get back into electronics.

After unsuccessfully struggling to get the F# compiler to build under the stable version of mono for Debian Wheezy, I realised that F# programs work just fine if you build normally from a windows computer, throw in FSharp.Core.dll in the /bin/ and copy it over. So I have a setup now where I work with VS2012 / Sublime and sync the executable and libraries with WinScp (or indeed the Scp plugin for FAR Manager).

Next up is to get access to the hardware. I built this C library as a shared object, dumped it in with the other binaries of my project and it worked through P/Invoke with no hassle at all ! 

I've quite a bit of electronics experience, so after I did some basic tests that the various I/O pins could be set to output and switch from High to Low, I skipped the traditional "hello world" of hardware (blinking an LED) and figured I'd try something a little bit more ambitious. I have a little LCD screen laying around in my old gear, it's a standard LCD driven by the Hitachi HD4480 LCD Controller. You can use these in 8-bit or 4-bit mode, with the 8 bit mode needing 4 more I/O pins.  I'm using the 4 bit mode because I don't really have that many pins and it's pretty easy (although a little fiddly) to use it in 4-bit mode.  

I'm using a total of 7 I/O pins,  E (enable), RS (switch between command and character mode), RW (I'm not actually using this right now) , and then DB4 through DB7 which are the 4 input bits (they are the higher nibble of the full 8 bits). This is the circuit :

In this schematic the Pi pins relate to the actual physical pin numbers, however in the code a mapping is needed over to what the Pi internally calls its GPIO pins.  I created an enum for this purposes, only containing the pins I am using for now

type GPIOPins =
    | Pin_11 = 17u
    | Pin_12 = 18u
    | Pin_13 = 27u
    | Pin_15 = 22u
    | Pin_16 = 23u
    | Pin_18 = 24u
    | Pin_22 = 25u

For example the physical pin 11 maps to GPIO number 17.  Infact when I first hooked this circuit up and wrote all the code to perform the LCD initilization I couldn't get it to work. Thankfully I happen to have a 16 channel logic analyzer in my scope so I hooked up all the inputs, set it to a single sweep triggering on the rising edge of the Enable pin over 500ms and noticed that the RW pin was always high - strange (I neglected to take a picture of the waveforms for this post :( ).  Turns out that the Pi user manual is WRONG, I have got a slightly later revision of the board where pin 13 is mapped to 27, not 21!

The next bit of code imports a couple of the functions from the C library and creates a couple of mini functions around them

[<DllImportAttribute("libbcm2835.so", EntryPoint = "bcm2835_init")>]
extern bool bcm2835_init()

[<DllImport("libbcm2835.so", EntryPoint = "bcm2835_gpio_fsel")>]
extern void bcm2835_gpio_fsel(GPIOPins pin, bool mode_out);

[<DllImport("libbcm2835.so", EntryPoint = "bcm2835_gpio_write")>]
extern void bcm2835_gpio_write(GPIOPins pin, bool value);

let fsel pin value = bcm2835_gpio_fsel(pin,value)                        
let write pin value = bcm2835_gpio_write(pin,value)            
let wait (ms:int) = System.Threading.Thread.Sleep(ms)

To use the LCD you write some bits to the data pins, then bring the Enable pin high for a few us then pull it low again ("pulse"). The LCD then does something depending on the input bits. In order to prepare it for 4-bit use I first have to send it a few 0x03 (0011) packets as per the spec indicates. Then I can switch it into 4-bit mode (0x2). From this point on, I can use all the 8-bit commands from the spec. Because I'm running in 4 bit mode I have to send the high nibble first, pulse, then send the low nibble. I wrapped some of the LCD functionality up in a F# record type (note: all this code is just a first stab, everything with hardware is inherently to do with mutating state so I won't be using a lot of the real functional features of the language just yet, but I'll see what I can do about that later)

type LCDCommands =
    | AllLow =          0b00000000
    | Clear =           0b00000001
    | Home =            0b00000010   
    | FourBit =         0b00100000   
    | TwoLine =         0b00001100    
    | DisplayOn =       0b00001100
    | CursorOn =        0b00000001
    | AutoIncCursor =   0b00000110    
    | Line2 =           0xC0
        
type LCD = { E : GPIOPins; RW : GPIOPins; RS : GPIOPins; 
             DB4 : GPIOPins; DB5 : GPIOPins; DB6 : GPIOPins; DB7 : GPIOPins; }
    with 
    member lcd.Pulse() = // toggles enable 
        write lcd.E true; wait 1
        write lcd.E false; wait 1
    member lcd.WriteNibble(value) = // write the lower four bits to the data pins and pulses
        write lcd.DB7 (value >>> 3 &&& 0x1 = 0x1)
        write lcd.DB6 (value >>> 2 &&& 0x1 = 0x1)
        write lcd.DB5 (value >>> 1 &&& 0x1 = 0x1)
        write lcd.DB4 (value &&& 0x1 = 0x1)
        lcd.Pulse()
        wait 1
    member lcd.WriteByte(value) =
        lcd.WriteNibble(value >>> 4) // write high nibble first
        lcd.WriteNibble(value)
    member lcd.Command = int >> lcd.WriteByte

I have captured some of the LCD commands in another enum - some of these have to be OR'd together as per the spec. I've just encoded the ones I'm going to use. The there's the pulse which toggles enable with a tiny delay. Because I'm in 4 bit mode I'll always be writing nibbles with a pulse at the end - the WriteByte function simply writes the high nibble first then the low nibble as the spec indicates. The last function is just a wrapper so I can directly use the LCDCommand enum.

member lcd.Initialize() = // I am only using the (annoyingly fiddly) 4 bit mode
        // assume 1000ms or so has passed since program start up
        // make sure pins are set to output
        fsel lcd.E   true; fsel lcd.RW  true
        fsel lcd.RS  true; fsel lcd.DB4 true
        fsel lcd.DB5 true; fsel lcd.DB6 true
        fsel lcd.DB7 true
        // zero them all out
        lcd.Command LCDCommands.AllLow
        // to start with we are only writing special wakeup nibbles
        lcd.WriteNibble(0x3); wait 5 // as per spec, first call has a 5ms wait
        lcd.WriteNibble(0x3); wait 1
        lcd.WriteNibble(0x3); wait 1
        // now set into 4 bit mode and send 8 bits in 2 nibbles from now on
        lcd.WriteNibble(0x2)
        lcd.Command(LCDCommands.FourBit ||| LCDCommands.TwoLine)     // set 5x8 mode 2 lines
        lcd.Command(LCDCommands.DisplayOn ||| LCDCommands.CursorOn)  // switch it on
        lcd.Command(LCDCommands.AutoIncCursor)

This is the startup sequence - set all the pins to Output, zero them all out, and then follow the startup sequence as per the spec. initially I have to just use nibbles, until the wake-up sequence is complete, then I can set it to 4-bit mode and use full byte commands. Once the display is in 4-bit mode I switch it to 5x8 mode with 2 lines and switch the screen on with a flashing cursor and so on.

member lcd.WriteText(text:string,clear) = 
        if clear then lcd.Command(LCDCommands.Clear)
        write lcd.RS true; wait 1
        Encoding.ASCII.GetBytes(text) |> Seq.iter(int >> lcd.WriteByte)
        write lcd.RS false; wait 1

Lastly a function to output some text. To do this you have to set the LCD into character output mode by pulling RS high; then you can send ASCII codes and the LCD will print them.

Pulling this all together I wrote that classic silly number-guessing game you write when learning to program, with the output on the LCD:

[<EntryPoint>]
let main argv = 
    try
        match bcm2835_init() with
        | true ->
            let lcd = { E = GPIOPins.Pin_11; RW = GPIOPins.Pin_12; RS = GPIOPins.Pin_13; 
                        DB4 = GPIOPins.Pin_15; DB5 = GPIOPins.Pin_16; DB6 = GPIOPins.Pin_18; DB7 = GPIOPins.Pin_22 }
            
            wait 1000
            lcd.Initialize()
            
            let rec loop number attempts =                
                 try
                    let guess = Console.ReadLine() |> Int32.Parse
                    if guess = number then                     
                        lcd.WriteText("CORRECT!!",true)
                        lcd.WriteByte(0xC0); 
                        lcd.WriteText("YOU WIN!",false)
                    elif attempts + 1 > 5 then
                        lcd.WriteText("WRONG!!",true)
                        lcd.WriteByte(0xC0);
                        lcd.WriteText("YOU LOSE!!",false)
                    else
                        lcd.WriteText("WRONG!! ",true)
                        lcd.WriteText((if number < guess then "< " else "> ") + guess.ToString(),false)
                        lcd.WriteByte(0xC0); wait 2
                        lcd.WriteText("GUESS AGAIN!",false)
                        loop number (attempts + 1 )
                 with
                 | _ -> printfn "Number not reconigsed. Try again"
                        loop number attempts
                        
            lcd.WriteText("Guess a number",true)
            lcd.WriteByte(0xC0); wait 2
            lcd.WriteText("0 <---> 50",false)
            loop (System.Random(DateTime.Now.Millisecond).Next(51)) 0

            
        | false -> printfn "failed to init"
    with
    | ex -> printfn "exception thrown : %s" <| ex.ToString()
    
    Console.Read()

Here's a pic of it working ..

Cool! This was just a silly project to test everything is working properly - I can take over the world now. 

Comments (2) -

Art Scott
Art Scott
11/03/2013 06:05:52 #

Hi. Thanks. GR8 blog, just what I've been looking for.
What is your setup? A or B? SD? What is that plugboard? Favorite vendors?
I'd appreciate more info on your configuration.
I'm about to purchase RPi for my nephew to get up on the learning curve.

pezi
pezi
11/03/2013 13:27:01 #

Hey! Thanks for your comment. I'm using a B Pi.  I have a bunch of old electronics / robotics gear lying around primarily from http://www.parallax.com/, who I hugely recommend both in terms of education and sensors etc (they have little to do with the Pi though). The protoboard is a rather expensive one aimed more at BS2 & SX circuit development, but it has a great deal of awesome common things on it to save a lot of messing about. I've had it a long long time, this is it http://bit.ly/VZhe0u  Cheers!

Add comment

biuquote
  • Comment
  • Preview
Loading