Friday, 17 May 2013

Messing about with 7 segment displays and a PIC 16F628a

I scored a load of cheap 4-way 7-segment displays off eBay the other day. They worked out at about 30p each so I got 20 and stuffed them in a drawer. Then I thought that perhaps it'd be nice to get one working, just so that we know what to do with them in future projects (one already springs to mind).


So here's a simple 4-way 7-segment LCD display hooked up to a PIC 16F628a microcontroller. We're running the PIC off it's own internal oscillator (there's nothing time critical going on here) and will use a simple direct drive method to make them light up.



Basically this means we'll light one digit it a time - lighting the appropriate segments of each character in a 4-digit number and waiting a few milliseconds before moving on to the next. Persistence of vision will give the illusion that all characters are illuminated at the same time.

These particular 7-segment displays have  a common anode - each digit shares the same "power" supply and we need to pull different pins to ground to get the different segments to light up. The display also includes decimal point characters and a double-dot (colon) character for use with digital clocks and so on.






While you can use a driver chip (like a MAX7219) or even a shift register or two to display the digits on these, it's easy enough to drive them directly from the i/o pins. Eight segments and 4 characters means 12 pins are needed to display all four characters at a time.

As ever, we're using Oshonsoft PIC Simulator to write the code in BASIC and using the keyword SYMBOL to assign a descriptive word to an i/o pin. This is really handy because when it comes to laying out your circuits on a PCB, sometimes you need to flip pins around to simplify the layout. Instead of going through all your code and changing every reference to PORTB.0, for example, you simply change which pin the symbol points to.

Here's the code. It's just a counter, but demonstrates how to display all 4 digits at the same time:

Define CONF_WORD = 0x3f18
Define CLOCK_FREQUENCY = 4

AllDigital

Config PORTA = Output
Config PORTB = Output

Dim i(4) As Byte
Dim tmp As Byte
Dim j As Byte
Dim k As Byte

Symbol pin_a = PORTB.3
Symbol pin_b = PORTB.0
Symbol pin_c = PORTB.7
Symbol pin_d = PORTB.5
Symbol pin_e = PORTB.4
Symbol pin_f = PORTB.6
Symbol pin_g = PORTB.1


init:
     'rather than a single variable counting 0-9999
     'we'll keep track of the tens, hundreds, thousands
     'and units separately to make displaying them easier
     i(0) = 0
     i(1) = 0
     i(2) = 0
     i(3) = 0


loop:

     For k = 1 To 100
     
          'activate all four characters, one after the other,
          'but really quickly in succession
          
          For j = 0 To 3

          tmp = i(j)
          'turn off all characters
          PORTA = 0x00
          PORTB = 0xff

          'pull the appropriate segments in each character
          'low in order to light them up
          
          Select Case tmp
          Case 0
          'a,b,c,d,e,f=low g=high
          Low pin_a
          Low pin_b
          Low pin_c
          Low pin_d
          Low pin_e
          Low pin_f
          High pin_g

          Case 1
          'b,c=low
          High pin_a
          Low pin_b
          Low pin_c
          High pin_d
          High pin_e
          High pin_f
          High pin_g

          Case 2
          'a,b,d,e,g=low
          Low pin_a
          Low pin_b
          High pin_c
          Low pin_d
          Low pin_e
          High pin_f
          Low pin_g

          Case 3
          'a,b,c,d,g=low
          Low pin_a
          Low pin_b
          Low pin_c
          Low pin_d
          High pin_e
          High pin_f
          Low pin_g

          Case 4
          'b,c,f,g=low
          High pin_a
          Low pin_b
          Low pin_c
          High pin_d
          High pin_e
          Low pin_f
          Low pin_g

          Case 5
          'a,c,d,f,g=low
          Low pin_a
          High pin_b
          Low pin_c
          Low pin_d
          High pin_e
          Low pin_f
          Low pin_g

          Case 6
          'a,c,d,e,f,g=low
          Low pin_a
          High pin_b
          Low pin_c
          Low pin_d
          Low pin_e
          Low pin_f
          Low pin_g

          Case 7
          'a,b,c=low
          Low pin_a
          Low pin_b
          Low pin_c
          High pin_d
          High pin_e
          High pin_f
          High pin_g

          Case 8
          'a,b,c,d,e,f,g=low
          Low pin_a
          Low pin_b
          Low pin_c
          Low pin_d
          Low pin_e
          Low pin_f
          Low pin_g

          Case 9
          'a,b,c,d,f,g=low
          Low pin_a
          Low pin_b
          Low pin_c
          Low pin_d
          High pin_e
          Low pin_f
          Low pin_g

          Case Else
          High pin_a
          High pin_b
          High pin_c
          High pin_d
          High pin_e
          High pin_f
          Low pin_g

          EndSelect

          'activate the appropriate digit
          'by giving it power
          
          Select Case j
          Case 0
          High PORTA.1

          Case 1
          High PORTA.2
          Case 2
          High PORTA.6

          Case 3
          High PORTA.3
          EndSelect


          'wait a little while for the LED to appear lit
          '(thanks to persistence of vision it will appear lit
          'all the time when actually it's really flickering)
          WaitMs 2

          Next j
     Next k

     'increase the units variable
     i(0) = i(0) + 1
     
     'if we've rolled over to 10, increase the tens
     'by one and set units back to zero
     If i(0) >= 10 Then
          i(0) = 0
          i(1) = i(1) + 1
     Endif
     
     'if we've rolled over to 100, increase the
     'hundreds by one and set tens back to zero
     If i(1) >= 10 Then
          i(1) = 0
          i(2) = i(2) + 1
     Endif
     
     'if we've rolled over to 1000, increase the
     'thousands by one and set the hundreds back to zero
     If i(2) >= 10 Then
          i(2) = 0
          i(3) = i(3) + 1
     Endif

Goto loop
End



And here's a video showing the counter in action:



Wednesday, 15 May 2013

More crazy Farnell packaging

We love Farnell here at Nerd Towers - pop onto their website click around a bit and the next morning, a parcel arrives with all your lovely goodies. Sometimes you can leave it as late at 7pm and your stuff still arrives the next day! But sometimes their choice of packaging leaves a bit to be desired.

Here's what arrived this morning, and the carton it arrived in:


The speakers came in their own dedicated and padded boxes too. Surely a padded envelope would have sufficed?

Tuesday, 14 May 2013

Artwork for screen printing PCBs

Since we got back from Berlin (after a two-day drive on each leg of the journey, and a total of 1,500 miles there and back) we've been giving lots of thought to silkscreen printing for making lots of PCBs.

We've gone back-and-forth with numerous designs, going from A4 panels (so front and back would fit on a single A3 frame) to 160x140 panels (double-sided 200x200mm copper clad board is easier to source than A4 sheets) to eurocard and back to A4.

After about six iterations of the same board design - each time reducing the number of jumpers and vias on our double-sided board, we still couldn't get a design which used as much of the copper board as possible: no matter what the layout, there was always quite a bit of waste on each panel. And then we had an epiphany moment....

We've been concentrating on making a hex-based board for the Dreadball Board Game. And because the miniatures stand on 25mm clear bases, we've been concentrating on making each "cell" on the board at least 30mm - allowing for a 2mm border around each cell, this allows a 28mm push-button surface (miniatures for other games are sometimes provided on 28mm round slotta-style bases). And it's because of these relatively large cells we can't get them to fit nicely on any "standard" sized copper clad board. But here's a thought....

The miniatures in Dreadball are much smaller than standard (read Games Workshop) miniatures. Originally they were designed to be used as-is from out of the box, but a lot of players complained that they fall over easily (because the bases are so ridiculously small)


So Mantic included some clear acrylic bases for the miniatures to stand up in. But the bases provided are pretty lame too - the recess for the rounded player base is too large and the miniature doesn't clip in nicely - it needs to be glued into place. Interestingly, all of the pre-release artwork for the game shows the miniatures painted up minus the clear acrylic bases, which suggests they were just added in as an after-thought.


So already we're thinking of cutting some fresh clear acrylic bases for the miniatures, to make the playing pieces clip in nicely instead of flopping about or needing to be held in with glue.



And then we got to thinking -  if we're going to laser-cut our own bases from 3mm sheet acrylic anyway, why not make them a bit smaller and just reduce the size of each cell in our game board?

And that's exactly what we did.
This time, we're using cell dimensions of 20mm. Allowing for a 1mm border all around (a 2mm gutter between each space on the board) means each hexagon needs to be at least 22mm high.

And by some bizarre coincidence, when we placed a pattern of 8 hexes across and 4 hexes down, reduced to 22mm instead of 30mm, the total PCB layout fits exactly into a 160mm x 100mm rectangle!


This gives us four "blocks" of 8 hexes per module. Which is a nice number for handling with software/firmware - certainly much easier to deal with than 3 columns of 8 which is what we had on our latest design. And the amount of waste around the outside of the module is kept to an absolute minimum. So all in all, we're quite happy with this latest design.

Making a 3x3 grid of these modules gives us a board of 24 hexes across and 12 hexes down - a few over from the 11x23 that make up the widest points of a Dreadball pitch, but sufficient to make a playable surface. Double-sided eurocard-sized copper board is all over the internet, so this design is perfect in terms of ease of manufacture as well as for sourcing the materials. The final board made up from these modules is a decent size too:


Eighteen inches isn't a massive board, but is still a pretty decent size. And when the board is all made up with etched squares and flashing LEDs, the closer proximity of the miniature playing pieces will make for a slightly more chaotic-looking game - just like a real futuristic sports simulation!



We're planning on getting a silkscreen made up with the top design on top and a mirrored copy of the bottom design on the bottom of an A4 sized frame. This will allow us to use the same screen for both sides of the board - we just need to mask off the unused bit and make sure the board is correctly lined up before each print, but that's not a major issue... is it?

Wednesday, 8 May 2013

Screen printing PCBs

It's official - making a homebrew screen from a bit of left-over silk, stapled to a picture frame and masked with pound-shop parcel tape, not only works, but works really well! On our frame we had a stencil of the pcb tracks, and a stencil for the solder mask. We started by covering one half of the screen with parcel tape...


... then swiped some acrylic paint over the other half of the screen:


In seconds we had a dozen or more perfectly printed PCBs (on test paper admittedly, not copper board, but it proved the concept)


Knowing we could now rattle out PCBs quickly and easily, the next thing to do was make sure the second print lined up with the first. Here we used red acrylic instead of the expensive solder mask paint:


They actually lined up pretty good. Not perfect (but then the screen wasn't fully taught and the board was lined up by eye) but not bad at all. It'd make a perfectly workable PCB!


All in all, we're calling our screen-printing test a success.
We got a screen made and using acrylic paint, quickly and easily printed a number of circuit boards in seconds. When it comes to making batches of PCBs with the same design, this is definitely something we want to use, in place of sending designs off to China!

Tuesday, 7 May 2013

Nerds in Berlin (again)

We've been here in Berlin for a few days - in the salubrious district of Kreuzberg - and slowly getting to understand (some) of the appeal. It has many similarities to Brighton: only a grubby, dirtier, graffiti-strewn version - and no seaside. The last time I came, I hated it. This time, the weather is better, there are more people about, and we've met some really cool characters. Kreuzberg is still a dirty, rotten area of Berlin, but - as with so many places - it's the people that make the city, and we've met some pretty cool ones!

One of the more exciting things about our short visit was visiting the drop-in print making workshop. There are places like this all over Berlin: places that hackerspaces should be aspiring to. It's basically a shop where you turn up, explain what you need to do and they supply the parts, materials, equipment and (where it's needed) expertise to help you get the job done.


The shop is open an manned by staff five days a week; there's a massive indie/punk subculture in Berlin where people make their own t-shirts, fliers and cd/album covers, mostly using screen printing. Everyone seems to know someone who knows screen-printing! So we took ourselves along to see how it all works.

Andrea made up a simple screen by hand-stretching some medium-grade silk over an A4 sized picture frame and stapled it along each edge. Although you can get machine-stretched frames, this hand-made one seemed pretty taught, so we thought we'd just give it a go. At the print-shop they provided the photo-sensitive emulsion. It's ready mixed, so you just need to put a thin film on your screen and leave it to dry.


the photo-emulsion is a bright lime-green colour. When it's "baked" it goes a dark, leafy-green colour.

A thin film of paint is dropped into a trough (it's expensive stuff and is difficult to re-use, so only the smallest amount needed is taken out of the pot) and the trough is swiped quickly up both sides of the silk - working vertically, from bottom to top


The aim is not to place a film on the surface of the screen (although this may happen) - it should be as thin as possible and we're just trying to get the emulsion to fill the gaps in the silk. Sometimes the paint doesn't get quite into the corners, which is why the screen needs to be oversized: our frame is for an A4 picture so is actually a little larger than A4, all the way around


While the screen was drying, in a dark, warm room, we set about preparing the artwork. Some PCB circuits were laser-printed onto 100gsm paper (not acetate as we were expecting). The paper was then coated with cooking oil (rapeseed I think) and it magically turned translucent: a handy tip to remember for future screen printing - much cheaper than expensive acetate sheets!

we weren't sure, so coated both sides of our sheet with oil

About half an hour later (or however long it takes to drink a coffee in the sunshine, at the cafe opposite) and the screen was ready for exposing. We placed the image onto the exposure bed, then the screen on top and turned on the vacuum to help remove any air bubbles trapped between the image, screen and glass bed. It took a good ten minutes - but only because the exposure box was absolutely massive!


Matt at BuildBrighton always wanted to make a large exposure bed - but this one may be outside even what he was dreaming of!

A4 on medium-guage silk takes about ten minutes to expose, apparently. So after ten minutes, we took the screen out and blasted it with a high-pressure hose. The screen wasn't as dark as we'd expected (but apparently the emulsion continues reacting in sunlight, so is yet to get a bit darker) but after a minute or so of washing, the magic started to happen. All the "unbaked" emulsion washed away and we were left with a perfect copy of our PCB design on the silk:

(our pcb design was only eurocard sized, so we put a solder mask on the bottom half of the screen)

All in all, an amazing success.
The whole thing cost less than five euros and we got help and learned a lot about the how and why of screen printing. Why the screens need to be larger than the image, why different meshes are needed for printing onto different surfaces and so on.

It was a really interesting few hours and has given us a lot of confidence with our own homebrew screen printing rig. Of course, you can use professionally manufactured screens (there were some machine-stretched screens at the workshop which were tighter than a snare drum skin!) and spend a lot of money getting everything "just so". But you can also fudge it with some cobbled-together gear and a bit of know-how. And that's what got us excited.

All we need to do now is go back and see if we can actually print an image with the screen!


Thursday, 25 April 2013

Working UV controller

Our UV controller is finally complete.
We didn't have any rotary encoders to hand so fudged it with a rotary potentiometer and some push buttons. If we were to make this again, we'd go for a slightly more sophisticated menu system and use a single rotary encoder with button built in. But this will do for now!




Here's the component layout. There are loads of extra connections for power and ground connections. There's nothing worse than soldering up a board and having to tack two or more wires together because there's nowhere on the board to add on another ground wire or power cable. This time, we made sure there are plenty!



Define CONF_WORD = 0x3f71
Define CLOCK_FREQUENCY = 4
AllDigital

Config PORTA = Input
Config PORTB = Output
Config PORTC = Output
Config PORTD = Output
Config PORTE = Output

Symbol white_led_pin = PORTD.1
Symbol uv_led_pin1 = PORTD.2
Symbol uv_led_pin2 = PORTD.3
Symbol uv_led_pin3 = PORTC.4
Symbol uv_led_pin4 = PORTC.5
Symbol uv_led_pin5 = PORTD.4
Symbol uv_led_pin6 = PORTD.5
Symbol uv_led_pin7 = PORTD.6
Symbol uv_led_pin8 = PORTB.4
Symbol speaker = PORTE.2

PORTB = 0x00
PORTD = 0x00

ADCON1 = 00001110b 'AN0 is analogue, all others digital
Define ADC_CLOCK = 3
Define ADC_SAMPLEUS = 50

Dim state As Byte
Dim lastmenu As Word
Dim menu As Word
Dim tmpw As Word
Dim tmp As Byte
Dim setting As Byte
Dim lastsetting As Byte
Dim uvled As Byte
Dim brightwhite As Byte
Dim brightuv As Byte
Dim buttonpressed As Byte
Dim timermins As Byte
Dim timersecs As Byte
Dim intrcnt As Byte
Dim seccount As Word
Dim redrawtimer As Byte

Dim led_counter As Byte

Define LCD_BITS = 4
Define LCD_DREG = PORTB
Define LCD_DBIT = 0
Define LCD_RSREG = PORTB
Define LCD_RSBIT = 5
Define LCD_RWREG = PORTB
Define LCD_RWBIT = 6
Define LCD_EREG = PORTB
Define LCD_EBIT = 7
Define LCD_READ_BUSY_FLAG = 0
Define LCD_DATAUS = 200
Define LCD_INITMS = 200

init:
     'allow everything to settle
     WaitMs 500

     Lcdinit 0 '0=no cursor
     Lcdcmdout LcdClear
     state = 0
     menu = 0
     lastmenu = 0
     setting = 0
     lastsetting = 0
     uvled = 0
     buttonpressed = 0
     intrcnt = 0
     redrawtimer = 0
     led_counter = 0
     
     Read 2, brightwhite
     Read 3, brightuv

     If brightwhite > 10 Then brightwhite = 10
     If brightuv > 10 Then brightuv = 10

     timermins = 1
     timersecs = 28

     Gosub resettimer

     'create a timer to fire every 1m/s
     '(we use this for the countdown AND to control the LEDs)
     
     T1CON = 0x00
     T1CON.T1OSCEN = 1
     T1CON.TMR1CS = 0
     T1CON.TMR1ON = 0
     Gosub preload_timer

     'enable global and peripheral interrupts
     INTCON.GIE = 1
     INTCON.PEIE = 1
     PIE1.TMR1IE = 1
     PIR1.TMR1IF = 0
     
     state = 0

loop:

     '-----------------------------------------------------------------------
     'read the value from RA0 to see if the user has changed the menu setting
     '-----------------------------------------------------------------------
     Adcin 0, menu
     If menu > lastmenu Then
          tmpw = menu - lastmenu
     Else
          tmpw = lastmenu - menu
     Endif

     If tmpw > 3 Then
          'dial has been moved more than the error in reading the A/C
          'so convert this to a setting value
          If menu >= 0 And menu < 340 Then
               'setting one:
               setting = 1
          Else
               If menu >= 340 And menu < 680 Then
                    'centre position: off
                    setting = 0
               Else
                    If menu >= 680 And menu <= 1024 Then
                         'setting two
                         setting = 2
                    Endif
               Endif
          Endif
     Endif

     '----------------------------------------------------
     'if the setting has changed, update the state machine
     '----------------------------------------------------
     If setting <> lastsetting Then

          'stop the timer
          T1CON.TMR1ON = 0
     
          'turn off any leds (they'll come back on again in a minute)
          Low white_led_pin 'turn off the white LED(s)
          Gosub turn_uv_off

          'something has changed: update the state as necessary
          Select Case setting
               Case 1 'start the uv leds again
               state = 1
               uvled = 1

               Case 0
               state = 0
               uvled = 0 'turn off the UV LEDs
               
               Case 2
               state = 2
               uvled = 0 'make sure uv LEDs are off

          EndSelect
     Endif

     lastsetting = setting
     lastmenu = menu


     '---------------------
     'decide what to do now
     '---------------------
     Select Case state
          Case 0
          'redraw the opening menu
          Gosub drawmenu1a
          state = 3 'wait for user to select a menu option

          Case 1
          'start flashing the UV LEDs, one row at a time
          Gosub resettimer
          Lcdcmdout LcdClear
          Lcdout "Time remaining:"
          Gosub drawtimer
          state = 5
          redrawtimer = 0
          'start timer1 interrupt
          Gosub preload_timer
          T1CON.TMR1ON = 1
          
          Case 2
          'show the white LEDs only
          Read 2, brightwhite
          If brightwhite > 10 Then brightwhite = 10
          Gosub drawmenu2
          Gosub turn_uv_off
          'start timer1 interrupt
          Gosub preload_timer
          T1CON.TMR1ON = 1
          state = 4 'wait for the user to select up/down

          Case 3
          'user can select between brightness and timer or hit select
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               Gosub drawmenu1b
               state = 7
          Endif
          
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               state = 8
               Gosub drawmenu4
          Endif

          Case 4
          'White LED is running
          'user can press up/down to select the white light brightness
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               If brightwhite < 10 Then
                    brightwhite = brightwhite + 1
                    Gosub drawmenu2
               Endif
          Endif

          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               Low white_led_pin
               Write 2, brightwhite
               Gosub drawsaved
               For tmp = 1 To 10
                    WaitMs 200
               Next tmp
               Gosub drawmenu2
          Endif

          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               If brightwhite > 0 Then
                    brightwhite = brightwhite - 1
                    Gosub drawmenu2
               Endif
          Endif
                    

          Case 5
          'UV lights are running
          'user can press up/down to increase/decrease the CURRENT time by one sec
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               Gosub increase_time_one_sec
               redrawtimer = 1
          Endif

          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               Gosub decrease_time_one_sec
               redrawtimer = 1
          Endif
          
          If redrawtimer = 1 Then
               redrawtimer = 0
               Gosub drawtimer
          Endif

          'check the timer hasn't hit zero
          If timermins = 0 And timersecs = 0 Then
               'stop the timer
               T1CON.TMR1ON = 0
               'draw zero zero
               Gosub drawtimer
               'time out, do nothing until menu selector moved
               Low white_led_pin
               Gosub turn_uv_off
               state = 6
          Endif
               
          Case 6
          'play a tune
          FreqOut speaker, 440, 200
          FreqOut speaker, 392, 200
          FreqOut speaker, 440, 200
          FreqOut speaker, 392, 200
          state = 13

          Case 7
          'user can select between brightness and timer or hit select
          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               Gosub drawmenu1a
               state = 3
          Endif
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               Read 3, brightuv
               If brightuv > 10 Then brightuv = 10
               Gosub drawmenu3
               state = 9
          Endif

          Case 8
          'set the timer minutes
          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               state = 10
               Gosub drawmenu4
          Endif
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               state = 11
               Gosub drawmenu4
          Endif

          Case 9
          'set the UV brightness
          'user can press up/down to select the UV light brightness
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               If brightuv < 10 Then
                    brightuv = brightuv + 1
                    Gosub drawmenu3
               Endif
          Endif

          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               Gosub turn_uv_off
               Write 3, brightuv
               Gosub drawsaved
               For tmp = 1 To 10
                    WaitMs 200
               Next tmp
               Gosub drawmenu1a
               state = 3
          Endif

          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               If brightuv > 0 Then
                    brightuv = brightuv - 1
                    Gosub drawmenu3
               Endif
          Endif


          Case 10
          'set the UV timer seconds
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               state = 8
               Gosub drawmenu4
          Endif
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               state = 12
               Gosub drawmenu4
          Endif
          
          Case 11
          'use up/down to change the timer minutes
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               Gosub increase_time_one_min
               Gosub drawmenu4
          Endif
          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               Gosub decrease_time_one_min
               Gosub drawmenu4
          Endif
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               Write 10, timermins
               Write 11, timersecs
               state = 8
               Gosub drawmenu4
          Endif

          Case 12
          'use up/down to change the timer seconds
          Gosub is_button_one_pressed
          If buttonpressed = 1 Then
               Gosub increase_time_one_sec
               Gosub drawmenu4
          Endif
          Gosub is_button_three_pressed
          If buttonpressed = 1 Then
               Gosub decrease_time_one_sec
               Gosub drawmenu4
          Endif
          Gosub is_button_two_pressed
          If buttonpressed = 1 Then
               Write 10, timermins
               Write 11, timersecs
               state = 10
               Gosub drawmenu4
          Endif

          Case 13
          'do nothing: wait for the user to move the menu selector to something else

     EndSelect
Goto loop
End

drawmenu1a:
     Lcdcmdout LcdClear
     Lcdout " UV brightness "
     Lcdcmdout LcdLine2Home
     Lcdout " UV timer <"
Return

drawmenu1b:
     Lcdcmdout LcdClear
     Lcdout " UV brightness <"
     Lcdcmdout LcdLine2Home
     Lcdout " UV timer "
Return

drawmenu2:
     Lcdcmdout LcdClear
     Lcdout "Brightness: "
     If brightwhite < 100 Then Lcdout " "
     If brightwhite < 10 Then Lcdout " "
     Lcdout #brightwhite
Return

drawmenu3:
     Lcdcmdout LcdClear
     Lcdout "Brightness: "
     If brightuv < 100 Then Lcdout " "
     If brightuv < 10 Then Lcdout " "
     Lcdout #brightuv
Return

drawmenu4:
     Lcdcmdout LcdClear
     Lcdout " Minutes: "
     If timermins < 10 Then Lcdout " "
     Lcdout #timermins
     If state = 8 Then Lcdout " <"
     Lcdcmdout LcdLine2Home
     Lcdout " Seconds: "
     If timersecs < 10 Then Lcdout " "
     Lcdout #timersecs
     If state = 10 Then Lcdout " <"
Return

drawsaved:
     Lcdcmdout LcdClear
     Lcdout " Settings saved"
Return

resettimer:
     Read 10, timermins
     Read 11, timersecs

     If timermins = 255 Then timermins = 1
     If timersecs = 255 Then timersecs = 30

     If timermins = 0 And timersecs = 0 Then
          timermins = 1
          timersecs = 30
     Endif
Return

increase_time_one_sec:
     timersecs = timersecs + 1
     If timersecs > 59 Then
          timersecs = 0
          timermins = timermins + 1
     Endif
Return

decrease_time_one_sec:
     If timersecs > 0 Then
          timersecs = timersecs - 1
     Else
          If timermins > 0 Then
               timersecs = 59
               timermins = timermins - 1
          Else
               'timer is already at zero
               timersecs = 0
               timermins = 0
          Endif
     Endif
Return

increase_time_one_min:
     If timermins < 99 Then
          timermins = timermins + 1
     Endif
Return

decrease_time_one_min:
     If timermins > 0 Then
          timermins = timermins - 1
     Endif
Return

drawtimer:
     Lcdcmdout LcdLine2Home
     Lcdout " "
     If timermins < 10 Then Lcdout " "
     Lcdout #timermins
     Lcdout "m "
     If timersecs < 10 Then Lcdout " "
     Lcdout #timersecs
     Lcdout "sec"
Return

is_button_one_pressed:
     buttonpressed = 0
     If PORTA.1 = 1 Then
          'debounce the button press
          WaitMs 1
          If PORTA.1 = 1 Then
               While PORTA.1 = 1
                    'do nothing
               Wend
               buttonpressed = 1
          Endif
     Endif
Return

is_button_two_pressed:
     buttonpressed = 0
     If PORTA.2 = 1 Then
          'debounce the button press
          WaitMs 1
          If PORTA.2 = 1 Then
               While PORTA.2 = 1
                    'do nothing
               Wend
               buttonpressed = 1
          Endif
     Endif
Return

is_button_three_pressed:
     buttonpressed = 0
     If PORTA.3 = 1 Then
          'debounce the button press
          WaitMs 1
          If PORTA.3 = 1 Then
               While PORTA.3 = 1
                    'do nothing
               Wend
               buttonpressed = 1
          Endif
     Endif
Return

preload_timer:
     'at 4mhz (crystal) we have 1m clock cycles per second
     'if we count up to 1,000 that's 1 millisecond
     'so let's preload timer1 with 65,535 - 1,000 = 64,535
     '(which in hex is 0xFC17)
     TMR1H = 0xfc
     TMR1L = 0x17
Return

turn_uv_off:
     Low uv_led_pin1
     Low uv_led_pin2
     Low uv_led_pin3
     Low uv_led_pin4
     Low uv_led_pin5
     Low uv_led_pin6
     Low uv_led_pin7
     Low uv_led_pin8
Return

turn_uv_on:
     High uv_led_pin1
     High uv_led_pin2
     High uv_led_pin3
     High uv_led_pin4
     High uv_led_pin5
     High uv_led_pin6
     High uv_led_pin7
     High uv_led_pin8
Return

On Interrupt
     Save System
     If PIR1.TMR1IF = 1 Then
          PIR1.TMR1IF = 0
          Gosub preload_timer
          seccount = seccount + 1
          If seccount >= 1000 Then
               seccount = 0
               Gosub decrease_time_one_sec
               redrawtimer = 1
          Endif

          led_counter = led_counter + 1
          If led_counter > 10 Then led_counter = 0

          Select Case state
               Case 4
               If led_counter >= brightwhite And brightwhite < 10 Then
                    Low white_led_pin
               Else
                    High white_led_pin
               Endif

               Case 5
               If led_counter >= brightuv And brightuv < 10 Then
                    Gosub turn_uv_off
               Else
                    Gosub turn_uv_on
               Endif
          EndSelect

     Endif
Resume


Here's a video showing the final UV controller:



In the end we went for a 10ms PWM on the LEDs. Originally we thought 30 would be fine, but it introduced noticeable flicker on the LEDs when running normally. Although the camera shows flicker on the white LEDs, in reality they appear as uniformly lit.

The video demonstrates:

  • variable UV brightness
  • changing the countdown time-out value
  • an amazingly melodic tune when the countdown has elapsed 
  • using variable brightness white LEDs for aligning silkscreen masks before exposure


That's all the electronics done - now we need to find a suitable box to enclose it all in and actually start making some silkscreens!

Using PIC Timer1 for PWM

At the heart of our UV controller is the Timer1 module and some clever use of PWM.
Let's look at PWM first of all - this is how we keep the lights on. The basic idea is this - persistence of vision gives us the illusion that an LED is light either dimly or brightly, when in fact it's flickering on and off so quickly that we can't see it flashing. The more time it spends on, the brighter it appears:




This is how most people imagine PWM working - in a "serial" manner (one instruction after another, with a suitable delay built in). For an 80% duty cycle, for example, the code might go something like this:

LED_on
Wait 1ms

LED_on
Wait 1ms
LED_on
Wait 1ms
LED_on
Wait 1ms
LED_off
Wait 1ms

Or even, more simply
LED_on
Wait 4ms
LED_off
Wait 1ms

and just repeat/loop.
In fact, a lot of servo controllers work this way - which is where the theoretical limit of 8 serially controlled servos per board comes from. A lot of times you see this for pwm - the code exactly matches the timing graph: send a pin high, wait the appropriate of time, send it low, wait out the remainder of the total cycle time.

And it works. But it's not the best way to control stuff via PWM. Let's say we've got some other stuff going on (like reading an analogue input, writing to a character display and so on). What happens now?

LED_on
Wait 4ms
LED_off
Wait 1ms
Check the analogue input
Write to the display
Loop

The problem is that doing all the other stuff, not just flickering the LEDS, takes up a lot of time. So our LEDs are on for 4ms and off for 1ms, and on for 4 out of 5 milliseconds gives us a duty cycle of 80% over 5ms. But if all this other stuff takes a few milliseconds to complete too:

On for 4ms
Off for 1ms
Stays off for 20ms while we check the inputs, update the LCD display and so on
Our 80% duty cycle is now actually on for 4ms, off for 1ms, stays off for 20ms, so on for 4 out of 25ms gives us a duty cycle of just 16%. And, because we've increased the total amount of time between each cycle, the flickering of the LEDs, on and off, becomes noticeable to the eye.

What we need is a way of getting the LEDs to update themselves, while we dedicate as much mcu power and time to the other tasks as necessary. What we need, in short, is a timer interrupt!

In our UV controller we're going to use our timer to do two things. Firstly it'll keep track of time (since we have a countdown timer from switching on the UV LEDs so that we don't overexpose any screens/boards). But secondly, we'll use it to as a "clock" for our PWM. 

Instead of saying "turn on, do nothing while a set time elapses, turn off, do nothing while more time passes" we're going to use an interrupt, which is more akin to: "turn on the LEDs, then I'm off over here to do something else - give me a shout when that time is up". After the "on" part of the PWM time has elapsed "turn off the LEDs, then give me a shout when it's time to turn them on again".

In fact, we're going to use a slightly hybrid version of the two approaches. We're using an interrupt to run some code every 1 millisecond. At this point we'll ask "should the LEDs be on of off?" and act accordingly. We'll also keep a counter running and increase it by one; when the counter hits 1000, we know that exactly one second has passed, and adjust our countdown time by one second.  Here's how we set up Timer1 in Oshonsoft:


'create a timer to fire every 1m/s
'(we use this for the countdown AND to control the LEDs)

T1CON = 0x00
T1CON.T1OSCEN = 1
T1CON.TMR1CS = 0
T1CON.TMR1ON = 0
Gosub preload_timer

'enable global and peripheral interrupts
INTCON.GIE = 1
INTCON.PEIE = 1
PIE1.TMR1IE = 1
'clear the timer-has-tripped interrupt flag
PIR1.TMR1IF = 0


Our sub-routine preload_timer simply sets a value so that we get the timer interrupt to fire every millisecond. This can take a bit of thinking about, and there are "make-it-easy" calculators all over the 'net but they can sometimes just confuse the matter. Simply put, Timer1 is actually a 16-bit counter. Every clock instruction it increases by one. When it reaches 65,535 and rolls over to zero, the Timer1 interrupt is called. That's basically it.

What we need to do is work out what number to start counting from, so that when the mcu hits 65535 limit and rolls over to zero, exactly the right number of clock instructions have been carried out to indicate one second has elapsed. The number we need to count up to is critically dependent on the speed we're running the mcu at (the crystal value we're running it from). In our case, we're using a 4Mhz crystal.

A 4Mhz crystal means 1 million instructions are carried out every second. So 1000 instruction cycles (one instruction takes 4 clock cycles remember!) occur every millisecond. If we set our Timer1 value so that it only counts up to 1,000 instead of 65,535 before firing the interrupt, we should get a 1ms interrupt. Instead of starting at zero, we need to preload our Timer1 value with 65535-1000 = 64535. In hex, 64535 is 0xFC17

preload_timer:
'at 4mhz (crystal) we have 1m clock cycles per second
'if we count up to 1,000 that's 1 millisecond
'so let's preload timer1 with 65,535 - 1,000 = 64,535
'(which in hex is 0xFC17)
TMR1H = 0xfc
TMR1L = 0x17
Return

Now, every millisecond, wherever we are in our program, and whatever we're doing, everything will get parked and code execution jumps to the interrupt routine. When we're done here, we'll jump back to exactly where we were in the code, before we were so rudely interrupted. The interrupt routine needs to keep track of the number of milliseconds passed, and update the LEDs as necessary:


On Interrupt
     Save System
     
     'If it's Timer1 that caused the interrupt:
     If PIR1.TMR1IF = 1 Then
          'set the new timer value
          Gosub preload_timer
         
          'clear the interrupt flag
          PIR1.TMR1IF = 0
         
          'increase our (milli)second timer
          seccount = seccount + 1
          'if one whole second has passed, update the clock
          If seccount >= 1000 Then
               seccount = 0
               Gosub decrease_time_one_sec
               redrawtimer = 1
          Endif
          
          'increase our LED PWM wave
          led_counter = led_counter + 1
          If led_counter > 30 Then led_counter = 0

          'decide whether the LED(s) should be on or off
          Select Case state
               Case 4
               If led_counter >= brightwhite And brightwhite < 30 Then
                    Low white_led_pin
               Else
                    High white_led_pin
               Endif

               Case 5
               If led_counter >= brightuv And brightuv < 30 Then
                    Gosub turn_uv_off
               Else
                    Gosub turn_uv_on
               Endif
          EndSelect

     Endif
Resume

In this way, we still have PWM controlled LEDs - they are on for part of a time and off for a specific set time - but instead of just hanging around doing nothing between the on and off phases, we're freed up to run other code and the LEDs will just "look after themselves".

As well as a millisecond counter, we'll just keep an "LED brightness counter" which also increases every millisecond - but only counts up to 30 then resets to zero instead of 1,000 (as our clock counter does).  In our code, we're using a brightness level from zero to 30. All we do is see if the current LED brightness counter is less than the required LED brightness and if it is, turn the LEDs on, otherwise turn them off.

This means that one PWM cycle lasts 30 milliseconds (we don't want to make this too large else visible flickering will appear on the LEDs). We don't have a fine degree of control over the "duty cycle". We can't, for example, set it to 1%, but we have up to 30 different values we can use which equate to
1/30 = about 3%
2/30 = about 7%
3/30 = about 10%
4/30 = about 13%
and so on.

Enough with the theory - let's get this coded up and some new firmware burned onto our almost-working hardware!