Sunday, June 9, 2013

IR Firmware for the PIC 18F

Almost every one of my designs involves a periodic timer interrupt. This is used for all timing. For the IR receive, the input may have to be sampled every 25 us. This is a very short interrupt period. For an IR send, the input has to be toggled every 12.5 us. This is just not practical.

It was time to explore some of the features of the PIC 18F that I had long ignored. One was the PWM feature which was used to do the IR send. The other was CCP that was used to do the IR Receive. The hardware used has been described in a previous blog.

The CCP Peripherals on a PIC 18F

The PIC18F97J60 family has 5 CCP units. Each unit contains three components - a 8 bit or 16 bit counter (TMR), a 16 bit register (CCPR) and a comparator that compares the two. The CCP unit operates in one of three modes - Capture, Compare and PWM.

In the Capture mode, the register latches the value of the counter when an event occurs on the input pin. This could be a rising edge, a falling edge, every 4th rising edge or every 16th rising edge. In the Compare mode, the counter is compared with the CCP register. When the two match, the output pin is driven. The output pin can be driven high, low, toggled or continue to show the latch output. In the PWM mode, the counter counts up to a given value. The counter is compared with the CCP register and the output is driven high at the start and low once the counter matches the register. So the counter max value determines the PWM frequency and the register controls the duty cycle.

The Capture mode uses an input and the Compare and PWM modes use an output. The pins have to be designated as inputs or outputs using the TRIS register.

Using PWM for IR Send

Sending an IR signal involves sending out a series of pulses. Each pulse is a burst of a carrier frequency - somewhere around 40 KHz. The signal is used to drive an IR LED.

The PWM output is set to a 40 KHz output with a duty cycle of 50%. The output has to be periodically turned on and off. This is easily achieved by setting the PWM duty cycle to 0% to shut it off. One timer is used by the PWM unit. A second one is used to provide the timing for switching the PWM output on and off. In this circuit, CCP4 is used along with Timer2. The output is on RD2. A second timer, Timer3 is used to provide the on/off timing.

The timing of the send signal is generated by using Timer 3 to interrupt. The time period for each on and off cycle of the IR send signal is loaded into Timer 3 and the PWM is switched accordingly on receiving the interrupt. The interrupt period depends on elements of the IR signal format and ranges around a few hundred to a few thousand microseconds. This is a great improvement on the 12.5 us interrupt required for a bit-banged IR signal.

IR Send: Initialisation

  • Set Timer2 to 25us (40 KHz)
  • Set the RD2 pin to output
  • Set CCP to PWM mode
  • Assign Timer2 to PWM
  • Set up Timer3 for interrupt
  • Initially, set duty cycle to 0%
  • Initially, disable Timer3 interrupt
void IRSendInit(void) {
    // Init for PWM
    // PWM Period = (PR2 + 1) * 4 * Tosc * Prescaler
    //            = 65 * 4 * 1/41.667 * 4
    //            = 65 * 4 * 24 * 4
    //            = 24.96us
    // Clock is 41.67MHz, prescaler set to 16, timer set to
    // Clock = 4 / 41.67MHz = 96ns
    // PWM period = 96ns * 16 * 64 = 98us
    T2CON = 0x7d;   //xxxx xx01 - Prescaler = 16
                    //xxxx x1xx - Tmr2 on
                    //x111 1xxx - Postscaler = 16 (Don't care)
    PR2 = 64;
    TRISDbits.TRISD2 = 0;  // Set RD2/CCP4 to output
    CCPR4L = 0;       // Duty cycle set to 0%
    CCP4CON = 0x0C;   // xx00 xxxx - lowest bits for duty cycle
                      // xxxx 11xx - PWM Mode

    T3CON = 0x81;     // 1xxx xxxx - 16 bit timer mode
                      // x0xx 0xxx - Timer 1 & 2 used for CCP/PWM
                      // xx00 xxxx - Prescaler = 1:1
                      // xxxx xx0x - Use internal clock
                      // xxxx xxx1 - Enable Timer3

    PIE2bits.TMR3IE = 0;  // Disable interrupt
}

IR Send: Start

  • Fill buffer with data to be transmitted
  • Enable Tiemr 3 interrupt
void IrSendStart(void) {
    // Fill up buffer
    ...
    PIE2bits.TMR3IE = 1;      // Enable interrupt
}

IR Send: ISR

  • Clear the interrupt flag
  • Set the output on or off using a duty cycle of 50% or 0%
  • Set the timer for the next edge
void IrSendISR(void) {
    PIR2bits.TMR3IF = 0;         // Clear interrupt flag
    if (output is high)
        CCPR4L = 32;             // 50% duty cycle
    else
        CCPR4L = 0;              // 0% duty cycle
    // Set timer for next pulse/space
    TMR3H = xx;
    TMR3L = xx;
}

Using CCP for IR Receive

Receiving an IR signal involves measuring the width of pulses received by the IR receiver.

The ECCP1 is used as the CCP peripheral for receiving IR signals. It is used in the Capture mode. This uses Timer1 for the counter and the RC2 input. Depending on the configuration, either the rising edge or the falling edge triggers the capture function and holds the count on Timer1 when the edge occurred. As both edges need to be measured, the rising/falling edge is reconfigured with each interrupt received. On detecting an edge, an interrupt is triggered and the count is recorded. The difference between the previous and current count is the period of the pulse. The timer is set to a period of 768 ns. With the count divided by 32, the resolution is approximately 25 us.

With each interrupt for an edge, along with the count being recorded, the edge to be detected is reversed. A rollover of Timer1 also triggers an interrupt. It can be used correct for the rollover and also to detect a lack of a signal for a long period. The Timer1 rollover period is 50ms. This is quite long by IR signal standards and usually indicates the gap between successive signal streams. For normal transmission, two rollovers within a signal are not possible and hence, the rollover interrupt is only used to detect quiet periods and not for correcting the period. A single rollover can happen and this is handled by inverting the sign of the difference. Once again, compared to the old design, an bit-banged interrupt of 25 us is replaced by an interrupt of a few hundred microseconds, reducing the load on the CPU.

IR Send: Initialisation

  • Set the RC2 pin to input
  • Set up Timer1
  • Setting ECCP1 to Capture mode, ECCP1 and Timer1 interrupts are done on Receive Start
void IRRecvInit(void) {
    TRISCbits.TRISC2 = 1;   // Set RC2 to input

    // Tosc = 1 / 41.667 = 24.000ns
    // Each count = Tosc * 4 * Prescaler
    //            = 24ns * 4 * 8
    //            = 768.0ns
    // To obtain 25us counts, multiply by 32 (gives 24.6us)
    // Rollover after 768ns * 65536 = 50.33ms
    T1CON = 0xb1;           // 1xxx xxxx - 16 bit read mode
                            // x0xx xxxx - don't use separate oscillator
                            // xx11 xxxx - Prescaler = 1:8
                            // xxxx 0xxx - Disable Timer1 oscillator
                            // xxxx xx0x - Use internal clock
                            // xxxx xxx1 - Enable Timer 1
}

IR Receive: Start

  • Set ECCP1 to capture mode, falling edge
  • Enable Tiemr1 interrupt
#define CCP1_RISE    0x05    // CCP1 control to get rising edge
#define CCP1_FALL    0x04    // CCP1 control to get falling edge
                             // 0000 xxxx - Not used (used only in PWM mode)
                             // xxxx 0101 - Capture mode (every rising edge)
                             // xxxx 0100 - Capture mode (every falling edge)

// Note: Input of IR receiver is active low.
void IrRecvStart(void) {
    // Set ECCP1 for capture mode - falling edge
    CCP1CON = CCP1_FALL;

    PIE1bits.CCP1IE = 1;     // Enable CCP1 interrupt
    PIE1bits.TMR1IE = 1;     // Enable Timer1 interrupt
}

IR Receive: ISR

  • Clear the interrupt flag
  • Get Timer1 value
  • Find difference between current and previous value
  • Correct for rollover, if any
  • Round off and divide by 32 to get count in steps of 25us
void IrRecvISR(void) {
    PIR1bits.CCP1IF = 0;        // clear interrupt

    irPeriod = TMR1L;
    irPeriod += TMR1H * 0x100;

    if (irOldPeriod > irPeriod) {
        irPeriodDiff = irPeriod + ~irOldPeriod;
    }
    else {
        irPeriodDiff = irPeriod - irOldPeriod;
    }
    irOldPeriod = irPeriod;
    // Round off diff in timer value and divide by 32
    irPeriodDiff = (irPeriodDiff + 15) >> 5;
}

Summary of Hadware Resources Used

Mode Hardware Resource Purpose
IR Send RD2 Output pin for IR Send
IR Send CCP4 PWM for IR Send
IR Send Timer2 Timebase for CCP4
IR Send Timer3 Timer Interrupt for each mark or space in IR Signal
IR Receive RC2 Input pin for IR Receive
IR Receive ECCP1 Capture for IR Receive
IR Receive Timer1 Timebase for ECCP1


No comments:

Post a Comment