Ian McCrum School of Engineering Embedded Systems Introduction to Interrupts, Timers and the Seven Segment Display ulster.ac.uk Executing code Some simple concepts A program is just a list of machine instructions burnt into memory. A program has a starting address, the compiler/linker decides where to place code. Each CPU executes lines of code sequentially, it uses an internal register called the Program Counter to keep track of the address in memory of what to execute. On Power up or reset the Program Counter is initialised to zero (in the PIC range of microprocessors). There is a “JUMP” instruction there that jumps to an actual program. Interrupts Doing two things at once (apparently!) If an “event” can force a new value into the Program Counter this will force a “jump” to the new value. We describe this as “interrupting the main code” Most microprocessors allow this – and have mechanisms to ensure a return can be made, back to wherever the code was before it was interrupted. Any internal registers used in the CPU must also be saved before and restored after the execution of the interrupt code. This is done for you in XC8. Most microprocessors have instructions to enable and disable the interrupt mechanism, perhaps main code has to do something which should not be interrupted. Events – hardware interrupts Typical sources of interrupt It is useful to setup a timer so that when it reaches a trigger value it generates an interrupt, this allows main code to be doing something useful whilst waiting for a temporal event If a external activity will take a long time, or an unknown time then it is useful to have that activity interrupt the CPU when it occurs. The external event can be a change of voltage on an input pin, you can choose rising or falling edge events. Or it can be any change on a group of digital inputs. (see RBO/INT0 and the Interrupt on change feature on RB4,5,6, and 7 – section 4.2 in the databook) Events – hardware interrupts Typical sources of interrupt If one of the built in peripherals such as the ADC, UART, SPI or IIC might take a bit of time to complete an data transfer then it can be useful to interrupt the CPU when that transfer is complete and needs CPU interaction. Some peripherals have several possible sources of interrupt. The UART transmit hardware can interrupt the CPU when it has just finished transmitting, the UART receive hardware can interrupt the CPU when an incoming character has arrived. (it can take thousands of microseconds to send or receive a serial character at 1200 baud for example – 10 bits at 1/1200 second for each bit) Interrupts in the PIC16F877 Refer to the databook; chapter 14, section 11 There are 15 sources of interrupt; three tiers deep in the hardware. To use interrupts you must; • Set the specific peripheral interrupt enable and also • Set the generic interrupt enable bit. There may be other enables needed( see PEIE) and other PICs may allow two or more interrupts, high priority and low priority, the CPU can almost do three things at once! Interrupt Logic – PIC16F877 From: figure 14-10 of Microchip PIC16F87XA databook To use Timer 1 or Timer 2 you must also remember to set PEIE Thus a ‘1’ on TMR0IE will enable a Timer 0 interrupt, but you also need a ‘1’ on the Global Interrupt Enable (GIE) to fully arm the hardware. The Interrupt will only happen when the Timer 0 Interrupt Flag (TMR0IF) goes to a ‘1’ Your software must (manually) clear TMR0IF inside the interrupt code, to avoid an immediate re-interrupt on leaving the Interrupt code. Timer 0 • TMR0 is an 8-bit timer/counter Readable & writable • 8-bit programmable prescaler (2:1 …256:1) • Internal or external clock select • T0IF Interrupt on overflow of FF to 00 • Edge select for external clock We will use an Internal clock with a prescale; hence T0CS=0, PSA=0 Timer 0 (note ZERO not “Oh”) Details In general timer 0 can either count input changes on RA4 or time things, the system clock is FOSC/4 which for a 4MHz crystal gives one microsecond resolution. The prescaler divides by 2, 4, 8,16, 32, 64,128 or 256 depending on the 3 bit value stored in PS2, PS1 and PS0 in the OPTION_REG. The TMR0 interrupt is generated when the TMR0 register overflows from 0xFF to 0x00. This overflow sets bit T0IF (INTCON<2>). The interrupt can be masked by clearing bit T0IE (INTCON<5>). Bit T0IF must be cleared in software by the Timer0 module Interrupt Service Routine (ISR) Using Timer 0 Three ways of timing things After initialising the OPTION_REG, If you store 00 in the TMR register it will start incrementing at the chosen period. You can either (a) Keep checking TMR0 until it reaches a target value (b) Store (0xff-your target), start the timer then keep checking TMR0IF – it gets set on a ff->00 rollover (c) Set up as for (b) above but allow an interrupt to occur Timer 0 Special Function Registers SFRs which can be accessed from software Timer 0 – setting it up Refer to the block diagram of the hardware – then it is obvious we want T0CS = 0 and PSA=0 and a value in PS2:0 The XC8 Compiler Using it with the PIC16F By including <xc.h> the compiler can find a number of declarations - each hardware register is known as a SFR and each bit or group of bits are accessible by using the structure below; these are often the same names as the databook (XC32 is better than the XC8…) TMR0=0x00; // an 8 bit register, clear to zero or a start value OPTION_REGbits.T0CS=0; // use internal clock source OPTION_REGbits.PSA=0; // use prescale unless divide by 1 OPTION_REGbits.PS=0b101; // this is the pattern for 1:64 INT_CONbits.T0IE=1; // Enable interrupts for timer zero We will also need to do a GIE=1 to fully allow interrupts to happen. Note that XC8 also allows other GIE and T0IE to be referred to in code without the SFRbits. prefix This may change in future versions of XC8 Interrupt Service Routines & vectors You have seen how the PIC16F877 has 15 sources of interrupt. These can cause a jump to interrupt code (if enabled correctly) • The code is described as an Interrupt Service Routine (ISR) • The jump is described as using an Interrupt Vector • The vector must be setup to point to the address of the ISR A vector is simply an address stored in memory where the CPU can find it – the reset vector is at 0b000 and the interrupt vector is at 0b004. The compiler places a “JUMP” instruction at both of these, and the hardware inside the CPU jumps to the appropriate address when reset or interrupted. Some CPUs just read the vector table and take action themselves. PIC18’s have two interrupt vectors, PIC32’s have hundreds! The XC8 compiler and interrupts There are several ways of doing this The function qualifier interrupt (or __interrupt) can be applied to a C function definition so that it will be executed once the interrupt occurs. It must have void return and parameter lists – obviously since no code will ever “call it” The compiler will process the interrupt function differently to any other functions, generating code to save and restore any registers used and return using a special instruction. Other Microchip compilers used other keywords; __attribute__((interrupt,auto_psv,(irq(52)))) or __ISR(_TIMER_0_VECTOR, ipl2auto) – nearly every event in the PIC32 architecture has its own vector So be sure to check in the compiler manual as this is non standard C From section 5.9 of Microchips XC8 Compiler manual Example code – 7 segment displays See the code section of the website If 4 separate seven segment displays are wired to parallel output ports so that one digit at a time can be driven then a workable 4 digit display can be created. It is important to let each display show its digit for 5 msecs out of every 20 – the human eye’s persistence of vision will appear to see all 4 digits lit. This is known as a multiplexed display it suits a simple ISR. We drove the segments with PORTD and the 4 digit select lines (DIGITS_CTRL) with PORTB<3:0> Example code – 7 segment displays The ISR and main code must communicate. 4 memory locations are set aside to hold what is to be sent to each of 4 displays. (we use 5 locations – see later) The 4 locations are written to by main code and read by the ISR The write to the 4 locations must not be interrupted (literally), for example if writing ‘1’ ‘9’ ‘4’ ‘2’ over the top of ‘2’, ‘0’, ‘1’ ,’4’ and an interrupt occurred half way through this write sequence, it is likely the display would read 1914 or 2042 depending on the direction of main’s writing sequence Thus we disable interrupts whilst altering the digit array in main. Global Variables For main to communicate to the ISR Variables declared outside a function are considered to be global and readable by any function in the declaring file. As a matter of style I declare then as extern in a header file and then define them (initialise them) in a file called setup.c. Any program wanting to access them need only include “setup.h” and ensure setup.c is compiled and included in the list of project files Timer functions: Initialisation This should make sense if you read 5.0 of the databook. I would prefer the modern XC8 convention of accessing each bit on a line by itself – the code is more self documenting and easier to read, though more typing is involved. TMR0=156; // an 8 bit register, 256 to 156 is 100 in decimal i.e every 100 clocks OPTION_REGbits.T0CS=0; // use internal clock source OPTION_REGbits.PSA=0; // use prescale unless divide by 1 OPTION_REGbits.PS=0b100; // this is the pattern for 1:32 (i.e 3200 clocks) INT_CONbits.T0IE=1; // Enable interrupts for timer zero - 3.2 milliseconds Timer functions: ISR If timer zero has its interrupts enabled and the interrupt has actually happened the code inside the if statement gets executed. In practice there is only one source of interrupt so the checks are not actually needed – but a future coder could alter the code so it is better to be sure It is vital to reload the TMR register and reset the T0IF flag, then we don’t get interrupted for another 3.2 milliseconds Seven Segment display: main code Inside main you must setup (initialise) timer 0 so that interrupts occur And simply drop a digit into each of the first 4 elements in the digits array The code here drops in the ASCII code for each digit, the ISR extracts an actual number. Note how interrupts are disabled whilst updating the array and then re-enabled after the update is complete. Note: sprintf acts just like printf except that the destination for the formatted characters is a string instead of the screen. Thus sprintf(astringname,”%04d”,1942); Plants ‘1’ , ‘9’, ‘4’ and ‘2’ inside the string, it must already have a ‘\0’ Seven Segment display: ISR code Working backwards, if the array digit contains “1942”, then digit[0] = ‘1’, digit[1]=‘9’ and so on. The ascii character ‘1’ has an actual value of 0x30 and the ASCII character ‘9’ has the actual value 0x39. Since the ASCII character ‘0’ has a value 0x30 the 2nd line [digits[current_digit]-’0’] will have a value of the number 1,9,4 or 2 depending on whether the variable current_digit has a value of 0,1,2 or 3. Assuming 1, then sevseg[digits[current_digit]-’0’] will have a value of 0b1101111, the code for sevseg[9]. Hence a 9 is displayed on the appropriate 7-seg display, all segments apart from f are lit on the second display in from the left… for 3.2 milliseconds. The 1st line above outputs the correct pattern 0b0100 if curreent_digit is a 1 and the 3rd and 4th lines adjust current_digit for the next interrupt (the ‘4’ has to be dispalyred on the 3rd digit in) Complete Code to drive 7-segments This also shows how multiple file compilation works (there are 9 files!) • You must add files to the project navigator in MPLAB X (the window in the top left of the IDE) • Normal convention is that every .c file has a .h apart from main • .h files should not use memory, you should never #include .c files, only .h files • Every .h file should be guarded so its contents are only read once, see the top 2 and last lines of my .h files • Every .c file should compile on its own (right click in the navigator and select compile file) • These rules (recommendations) ease porting drivers to new projects. I have used the software “Programmers Notepad” to display the code in the overhead slides that follow, note the tab at the top that is highlit. Other Interrupts and Timers Be careful if you use 2 or more interrupts Multiple interrupts are very tricky to code, very, very tricky to debug and analyse in a deterministic way. On the PIC16F there is only one interrupt vector so the one ISR must check all xxIF flags to see who interrupted. Such code must consider what to do if an interrupt occurs whilst in an ISR and must consider if an interrupt might get missed (if 2 occur from the one source before the first one is serviced.) PICs with multiple vectors are slightly easier to code – but a good embedded systems engineer must still consider if an interrupt might get missed, and if the priority is correct, some interrupts are more important than others. Other Interrupts and Timers Different PIC chips vary The PIC16F877 has 3 timers, timer 1 and timer 2 are both 16 bits and have extra registers that trigger wraparound – when TMR1 reaches the value stored in PR2 it resets to zero, it need not go up to 0xFFFF. Each timer has special functionality, timer 2 is useful for generating PWM signals PIC32 chips and some PIC18F have 5 timers, on the PIC32m a pair of timers can be used to provide a 32 bit peripheral.