Friday, 14 November 2014

Motor controller with PIC using SLEEP and interrupt-on-change

With Real Life And Work (tm) being pretty traumatic over the last few weeks, it was nice to get away from it all and spend a few hours with not much planned, at BuildBrighton last night. As often happens, that's when things start to actually get done!

Steve's been working on an rather exciting firmware-update-over-audio idea: uploading Arduino "sketches" using squeaks and farts, rather like a ZX Spectrum used to do, back in the early 80s. It doesn't sound like much when written down, but the possibilities of such an approach are really quite exciting - maybe he'll expand more on it in the future.

In the meantime, Iain had asked for a simple project - a magnet controlled motor controller. We'd already got the PCB made up and the circuit working on a breadboard, so it was time to code up the firmware.


(note the use of a larger 640FET for the motor controller rather than a simple NPN transistor; the reason was two-fold - firstly, more current is available to the motor than if a wimpy 200mA 2N2222A were used and also, this particular FET also includes an internal fly-back diode which we need because we're switching an inductive load)


The actual functionality of the controller is pretty simple - present a magnet over a hall effect sensor, and start the motor running. Present the magnet a second time, and turn the motor off.  A two-stage state machine would easily take care of this. But there were a couple of other considerations that we had to take into account:

Firstly, he'd mentioned running a motor of a PP3 battery.
So we needed to make sure that our PIC microcontroller wasn't slurping all the power out of the battery, constantly polling the hall sensor on the input pin. This involves putting the PIC to sleep when not needed. Which means, of course, that we need to use some kind of wake-up interrupt to take it out of sleep.

Here's what we came up with:



Define CONFIG1 = 0x0984
Define CONFIG2 = 0x1cff

Define CLOCK_FREQUENCY = 1
AllDigital

symbols:

declarations:
     Dim state As Byte
     Dim timer_count As Word
     Dim i As Byte
   
     Symbol motor_output = PORTC.3
     Symbol magnet_in = PORTA.5
     Symbol led = PORTC.2
   
configpins:
     ConfigPin PORTA = Input
     ConfigPin PORTC = Output
     ConfigPin motor_output = Output
     ConfigPin magnet_in = Input
   
initialise:
     'internal oscillator defaults to 500khz internal
     WaitMs 100 'wait for internal voltages to settle
   
     'enable pull-up resistors on encoder pins, and pushbuttons
     OPTION.7 = 0 'wpuen=7
     WPUA = 0xff 'enable pull-ups on all portA
     state = 0
   
     timer_count = 0
     Low motor_output
     Low led
   
     'enable the interrupt on change register
     INTCON.IOCIE = 1
     IOCAN.5 = 1 'portA raised an interrupt on change
   
     'enable interrupts
     INTCON.GIE = 1
         
loop:
     Select Case state
                     
           Case 0
           'motor is not running
           If magnet_in = 0 Then
                 'debounce the input
                 WaitMs 10
                 If magnet_in = 0 Then
                       High motor_output
                       High led
                       WaitMs 200
                     
                       'wait for the magnet to be removed
                       While magnet_in = 0
                             'do nothing
                       Wend
                       state = 1
                 Endif
           Endif
         
           Case 1
           'motor is already running
           If magnet_in = 0 Then
                 'debounce the input
                 WaitMs 10
                 If magnet_in = 0 Then
                       Low motor_output
                       Low led
                       WaitMs 200
                     
                       'wait for the magnet to be removed
                       While magnet_in = 0
                             'do nothing
                       Wend
                       state = 0
                 Endif
           Endif
     EndSelect
   
     timer_count = timer_count + 1
     If timer_count > 5000 Then
           'testing: show the going to sleep sequence
           'For i = 1 To 3
           'High led
           'WaitMs 80
           'Low led
           'WaitMs 80
           'Next i
           timer_count = 0
           ASM: sleep
     Endif

Goto loop
End
                     
On Interrupt
     If IOCAF.5 = 1 Then
           'clear the interrupt
           IOCAF.5 = 0
           timer_count = 0
     Endif
Resume


The interesting things to note here are
a) the use of inline ASM. Oshonsoft as a compiler is great for just adding in some assembly language right in the middle of your basic code. Just use the tag ASM: and then your assembly instruction. One little gotcha to look out for is there must be a space or a tab between the ASM: statement and the command (otherwise the compiler treats the next word as a label, not an instruction)

b) the use of INTCON.IOCIE = 1 to enable interrupt-on-change. Combined with IOCAN.5 = 1 this means that when pin A5 goes low (the register to monitor rising edges on PORTA is called IOCAP) the interrupt-on-change interrupt is fired. Luckily for us, this interrupt can also bring the device out of a sleep state

c) although commented out, during testing we put a "shutdown" routine (flashed an LED a couple of times) to indicate when the device was going to go to sleep. This proved useful to demonstrate that the device was actually asleep (and that no activity was actually taking place). To see this in action, uncomment the shutdown flashing LED and comment out the ASM: SLEEP statement. You should see that the LED flashes then after a second or two delay, flashes again - this is the behaviour of the firmware. If you now re-enable the SLEEP statement, the LED only flashes once: the sleep statement has suspended the program execution (just as we hoped it would).

Of course, when using interrupt-on-change, it's important that other (unused) input pins are not accidentally firing the change-interrupt because they've been left floating. Although the IOCAN register masks out only the pins that are to be monitored, it doesn't hurt to ensure that the pull-up resistors are enabled on any input pins, just in case.

So there we have it, a quick and simple project made up over a few cups of teas and a few enjoyable hours talking rubbish with the other nerds. With the firmware written and tested, we'd better get this thing in the post so Iain can actually put it to use!