PIC IR Software

This chapter describes how the software of my PIC IR Decoders works. You can safely skip this chapter if you're not interested in the software's internals, though I do think reading this chapter might be educational for the assembly language newbies.
Here I will describe the part of the software which is common to all my PIC IR Decoders. The actual decoding routines are described in the chapters which are dedicated to the particular protocols.
I will only explain the most important parts of the program. The rest can be read from the source files, which are included in the download package.

It goes without saying that the software is written in my own SB-Assembler, which can be freely downloaded from this same site if you want to make alterations to the software. There is no need for the assembler if you only want to use the programs as they are, because all HEX files are included in the download package.

PIC IR Software

First of all let's make a list of all the tasks the software has to perform:

  • Receive and decode IR messages, duh.
  • Scan the display.
  • Flash the central decimal point whenever a valid IR message is received.
  • Clear the display 2 seconds after receiving the last IR message.

All these tasks are to be performed simultaneously, at least they should appear to be. The first task is obvious, we need to receive and decode the IR messages because that's what we're here for. Receiving IR signals is a time critical process and may not be disturbed by other tasks.
The second task is also rather important. All digits must be shown in turn in a rapid and steady pace, otherwise we can see the display flicker. This task is also rather time critical.
The last two tasks are not very time critical, but have to be performed in a way which does not influence the first two tasks.

Main Program Loop

There is only one main program loop, which is a good start for most of our micro processor adventures. We enter this main program loop right after initializing the machine and we'll stay there for ever, and ever, and ever, and ever..... Until the power is removed from the system.

; Main program loop


;-----------------------------------------CALL THE IR RECEIVER STATE MACHINE--

                CALL    IR_MACHINE      Call the IR receiver state machine

;-----------------------------------------------------SCAN DISPLAY EVERY 5MS--

                DECF    SCAN_DELAY,F    Decrement scan delay counter
                BTFSC   STATUS,ZERO     Only scan display if counter = 0
                CALL    SCAN_DISP

;-------------------------------------------------------SYNC MAIN WITH TIMER--

.SYNC           BTFSC   TMR0,7          Wait until bit 7 of TMR = 0
                GOTO    .SYNC           Not 0 yet!
                MOVLW   TIMER_UPDATE    Reload timer again
                ADDWF   TMR0,F

                GOTO    MAIN

Most of this snippet are comment lines. If you delete the comments, there is hardly any code left. That's good, because we have to be fast to be able to decode all supported protocols.
How fast? After some careful considerations I've decided to let the main loop execute in exactly 50 micro seconds. This means that each task is called every 50µs, over and over again.

The first task is to call the IR Decoding routine IR_MACHINE. Every protocol will get its own IR Decoding routine and will be described later.
The second task is scanning (multiplexing) the displays. That doesn't have to be done every 50µs though. Therefore I used a delay counter which is decremented every iteration of the main loop. The SCAN_DISP routine is only called when this counter reaches zero.
The final piece of code in the main loop is a synchronization loop. This loop will ensure that every iteration of the main program loop will last 50µs. For this purpose I use the PIC's hardware timer. That timer will increment every instruction cycle. At 4MHz that will be every 1µs. We will simply wait until the timer overflows from $FF to $00, which can be detected when bit 7 of the timer changes from 1 to 0. As soon as the timer overflows it is updated with a value that will ensure a 50µs interval. The timer is not simply reloaded, but is updated. Imagine that we just had a very busy main loop which lasted 70µs instead of 50. The next main loop will then last only 30µs, gaining up for lost time.

Why didn't I use an interrupt driven timer? Simple, because we're in a hurry! An interrupt routine will take about 10µs extra because of the call, the return and the necessary context switching. This time can be put to better use in this time critical application.

The shortest execution time of the total main loop, excluding the time spent in the subroutines, is 11 micro seconds. The shortest execution time of the IR state machine is 6µs, which makes a total of 17µs for the entire loop. The .SYNC routine will eat away the rest of the time to complete the 50µs. This ensures ample time for the other tasks, which rarely all occur at the same time.

Display Scan Routine

The display is multiplexed, which means that there's only one of the four digits switched on at a given moment. Each digit is switched on for 5ms, after which it is switched off to allow the other digits some time of their own. Thus the total scan cycle will take 4 x 5 ms = 20 ms, which makes the scan frequency 50Hz.

The display scan routine is built around a state machine. A what? A state machine. Every time the routine is called it updates a pointer to point to a routine that has to be executed the next time. That way we execute a total of 4 different routines, one for each of the digits.
Surely there must be a reason why I used a state machine. And the reason is: time! Normally one would update a pointer, which points to the segment pattern of the next digit. Updating such a pointer involves incrementing the pointer, limiting the range of the pointer and adding an offset to the location of the segment pattern in RAM. Furthermore a shift register must be updated, which will drive the next display's anode. Updating this shift register involves shifting the previous value and limiting its range. Clearly this all takes quite some time. We will soon see that the state machine can nibble away some instruction cycles at the expence of a slight increase in the number of program steps.

; Scan display routine

SCAN_DISP       MOVLW   MS5_COUNT       Reload scan delay
                MOVWF   SCAN_DELAY
                MOVLW   %1111.1111      Switch off all Anode drivers
                MOVWF   PORTA            during the switch
                MOVF    SCAN_STATE,W    Jump to next display's routine
                MOVWF   PCL

The first part of the scan routine is common to all digits. First of all the scan delay counter is reloaded. This counter will ensure that the SCAN_DISP routine is called every 5ms.
Then all anode drivers are switched off. Switching off all digits at this stage ensures that we won't see ghost segments which slightly light up when we load a new pattern while the previous anode is still driven.
Then the state machine kicks into action. A pointer to the routine for the next state is loaded into the program counter PCL. I took care that all state machine routines start at program page $00, which makes indirect jumping a little easier because PCLATCH can remain $00. Loading PCL with a state pointer effectively means that we're jumping to one of the four routines SCAN_DISP1, SCAN_DISP2, SCAN_DISP3 and SCAN_DISP4.

;-------------------------------------------------------------SCAN DISPLAY 1--

SCAN_DISP1      MOVF    DIGIT1,W        Get pattern of left most digit
                MOVWF   PORTB            and send it out
                MOVLW   %1111.1110      Switch left most digit on
                MOVWF   PORTA
                MOVLW   #SCAN_DISP2     Next time do SCAN_DISP2
                MOVWF   SCAN_STATE

I will use SCAN_DISP1 as an example here. The other 3 routines are basically the same. So the state machine directed us here, which means that digit 1 has to be switched on now. We simply get the pattern for DIGIT1 and copy it to PORTB, the segment outputs. Then we drive the anode for digit 1 by loading PORTA with the proper bit pattern. Finally we set the state machine pointer to the SCAN_DISP2 routine, which will be called the next time we have to scan the display.
Do you recognize the power of the state machine now? We can simply use direct addressing to get the pattern for digit 1, no pointer needs to be incremented, limited and offset to get the pattern. And the proper anode is directly addressed, without having to shift and limit a shift register.
The total execution time of the display scan routine is 15µs.

I said that the other 3 state routines are basically the same. That is not entirely true because I abused two of them to do some extra tasks.

;-------------------------------------------------------------SCAN DISPLAY 2--

SCAN_DISP2      MOVF    DIGIT2,W        Get pattern of 2nd left digit
                MOVWF   PORTB            and send it out
                MOVLW   %1111.1101      Switch 2nd left digit on
                MOVWF   PORTA
                MOVLW   #SCAN_DISP3     Next time do SCAN_DISP3
                MOVWF   SCAN_STATE

                DECF    DP_DELAY,F      Decrement DP_DELAY every 20ms
                BTFSS   STATUS,ZERO
                RETURN                  We're done if counter <> 0 !

                BSF     DIGIT2,7        Clear Digit 2's decimal point now

SCAN_DISP2 is one of the states which differs slightly. The first 6 instructions should look very familiar to you. Yes they are very similar to the ones found in SCAN_DISP1. It's up to you to point out the 3 differences.

But the fun is at the end of this routine. There a counter called DP_DELAY is decremented, and every time it reaches $00 bit 7 of DIGIT2 is set. This piece of code's purpose is to switch off the decimal point that is used to indicate that a good IR message was received. Remember that was one of the 4 original tasks of the software!
Every time a good IR message is received DP_DELAY is loaded with a small value and bit 7 of DIGIT2 is made 0, which effectively lights the center dot. This small value of DP_DELAY is decremented each time SCAN_DISP2 is called, which is every 20ms. As soon as the counter reaches 0 we set bit 7 of DIGIT2, effectively turning the dot off again.
The keen observer will notice that the counter continues to count down, even if the dot is off. That is no problem, because after 256 iterations the dot, which is already off, is turned off yet again. Not stopping the counter at 0 again saves us some valuable micro seconds.

;-------------------------------------------------------------SCAN DISPLAY 4--

SCAN_DISP4      MOVF    DIGIT4,W        Get pattern of right most digit
                MOVWF   PORTB            and send it out
                MOVLW   %1111.0111      Switch right most digit on
                MOVWF   PORTA
                MOVLW   #SCAN_DISP1     Next time do SCAN_DISP1 again
                MOVWF   SCAN_STATE

                DECF    CLR_DELAY,F     Decrement CLR_DELAY every 20ms
                BTFSS   STATUS,ZERO
                RETURN                  We're done if counter <> 0 !

                MOVLW   %1011.1111      Only display 4 dashes
                MOVWF   DIGIT1
                MOVWF   DIGIT2
                MOVWF   DIGIT3
                MOVWF   DIGIT4

SCAN_DISP3 has no extra's and is identical to the SCAN_DISP1. But SCAN_DISP4 does have some extra's too. Again the first 6 instructions are similar to the ones found in SCAN_DISP1.
After showing digit 4 to the user the counter CLR_DELAY is decremented, which is done every 20ms. If that counter reaches zero all four digits are cleared to a dash in the center. That way we can make the displayed code disappear after a few seconds when no valid IR messages are received any more.
Each time a valid IR message is received a large value is loaded into CLR_DELAY and the received message is displayed. CLR_DELAY will be reloaded as long as valid IR messages are received. Once the counter is not updated in time it will eventually reach zero, which causes the display to be cleared.