LPC2148 Timers and PWM: Exact Delays, VIC Interrupts and LED Dimming

Software delay loops change whenever you change the clock or compiler settings. Hardware timers do not. The LPC2148 has two 32-bit general purpose timers, Timer0 and Timer1, plus a separate PWM unit with six outputs. In this tutorial you will build an accurate millisecond delay with Timer0, run a periodic interrupt through the Vectored Interrupt Controller (VIC), and generate a 1 kHz PWM signal to dim an LED smoothly.

How an LPC2148 Timer Counts

Each timer has a 32-bit prescale counter and a 32-bit timer counter. The prescale counter counts PCLK cycles; when it reaches the value in the prescale register (PR) it resets and the timer counter (TC) increments by one. Four match registers (MR0 to MR3) are compared continuously with TC. On a match, the match control register (MCR) can generate an interrupt, reset TC, or stop the timer.

TC tick frequency = PCLK / (PR + 1)
Match period      = (MR0 + 1) / tick frequency   (when MR0 resets TC)

PCLK = 15 MHz, want 1 kHz tick (1 ms):
PR = 15000000 / 1000 - 1 = 14999

Want 1 MHz tick (1 us):
PR = 15000000 / 1000000 - 1 = 14
RegisterPurpose
T0TCRBit 0 enables counting, bit 1 holds TC and PC in reset
T0PRPrescale value
T0TCCurrent timer count
T0MR0Match value 0
T0MCRBit 0 interrupt on MR0, bit 1 reset TC on MR0, bit 2 stop on MR0
T0IRInterrupt flags; write 1 to bit 0 to clear the MR0 flag
PCONPBit 1 powers Timer0, bit 5 powers the PWM unit

Timer0 Delay and Periodic Interrupt

The first function polls the timer for a blocking delay. The second part sets up Timer0 to interrupt every 500 ms and toggle an LED on P1.16 from the interrupt service routine. ARM7 has no NVIC, so the routine is registered in the VIC: its address goes into a vector address slot, and the matching vector control register holds the interrupt source number with the enable bit (bit 5). Timer0 is VIC channel 4.

timer.c: Timer0 blocking delay and 500 ms VIC interrupt on the LPC2148 (Keil ARM7)
#include <lpc214x.h>

#define LED1 (1u << 16)                 /* P1.16 */

/* Blocking delay using Timer1, 1 ms tick at PCLK = 15 MHz */
void delay_ms(unsigned int ms)
{
    PCONP |= (1u << 2);                 /* power Timer1 */
    T1TCR = 0x02;                       /* reset counters */
    T1PR  = 14999;                      /* 15 MHz / 15000 = 1 kHz */
    T1MR0 = ms;
    T1MCR = 0x04;                       /* stop on MR0 */
    T1TCR = 0x01;                       /* start */
    while (T1TCR & 0x01)                /* counter stops itself at MR0 */
        ;
}

/* Timer0 match 0 interrupt service routine */
void timer0_isr(void) __irq
{
    if (IO1PIN & LED1)
        IO1CLR = LED1;
    else
        IO1SET = LED1;

    T0IR = 0x01;                        /* clear MR0 interrupt flag */
    VICVectAddr = 0;                    /* tell the VIC the ISR is done */
}

static void timer0_init_500ms(void)
{
    PCONP |= (1u << 1);                 /* power Timer0 */
    T0TCR = 0x02;                       /* reset */
    T0PR  = 14999;                      /* 1 ms tick */
    T0MR0 = 500 - 1;                    /* 500 ticks */
    T0MCR = 0x03;                       /* interrupt and reset on MR0 */

    VICVectAddr0 = (unsigned long)timer0_isr;
    VICVectCntl0 = 0x20 | 4;            /* slot enabled, source 4 = Timer0 */
    VICIntEnable = (1u << 4);

    T0TCR = 0x01;                       /* start */
}

int main(void)
{
    IO1DIR |= LED1 | (1u << 17);
    timer0_init_500ms();                /* LED1 toggles in the background */

    while (1) {                         /* LED2 on P1.17 blinks from main */
        IO1SET = (1u << 17);
        delay_ms(100);
        IO1CLR = (1u << 17);
        delay_ms(900);
    }
}

Two lines in the ISR are essential. Writing 1 to T0IR bit 0 clears the match flag; without it the interrupt fires again immediately and the main loop never runs. Writing any value to VICVectAddr signals the end of the interrupt to the VIC so it can accept the next one. The __irq keyword tells the Keil ARM compiler to generate the special IRQ entry and return sequence that ARM7 requires.

How the PWM Unit Works

The PWM unit is a timer with seven match registers. PWMMR0 sets the period by resetting the counter, and PWMMR1 to PWMMR6 set the point in each period where outputs PWM1 to PWM6 go low in single-edge mode. Each output goes high at the start of the period and low at its match value, so the duty cycle is PWMMRn divided by PWMMR0.

A new match value does not take effect when you write it. You must also set the matching bit in the latch enable register PWMLER; the new value is then loaded at the start of the next period. This prevents glitches when the duty cycle changes in the middle of a cycle.

OutputPinPINSEL setting
PWM1P0.0PINSEL0 bits 1:0 = 10
PWM2P0.7PINSEL0 bits 15:14 = 10
PWM3P0.1PINSEL0 bits 3:2 = 10
PWM4P0.8PINSEL0 bits 17:16 = 10
PWM5P0.21PINSEL1 bits 11:10 = 01
PWM6P0.9PINSEL0 bits 19:18 = 10

PWM1 and PWM3 share pins with UART0, so this example uses PWM2 on P0.7 and keeps the serial port free. Connect P0.7 to an LED through a 330 ohm resistor to ground.

PWM LED Dimming Code

pwm.c: 1 kHz PWM on PWM2 (P0.7) with a breathing LED effect
#include <lpc214x.h>

#define PWM_PERIOD 1000u                 /* 1000 us = 1 kHz */

static void pwm2_init(void)
{
    PCONP  |= (1u << 5);                 /* power PWM unit */
    PINSEL0 = (PINSEL0 & ~(3u << 14)) | (2u << 14);  /* P0.7 = PWM2 */

    PWMTCR = 0x02;                       /* reset counter */
    PWMPR  = 14;                         /* 15 MHz / 15 = 1 MHz tick */
    PWMMR0 = PWM_PERIOD;                 /* period */
    PWMMR2 = 0;                          /* start at 0 % duty */
    PWMMCR = (1u << 1);                  /* reset counter on MR0 */
    PWMPCR = (1u << 10);                 /* enable PWM2 output, single edge */
    PWMLER = (1u << 0) | (1u << 2);      /* latch MR0 and MR2 */
    PWMTCR = (1u << 0) | (1u << 3);      /* counter enable + PWM mode */
}

static void pwm2_set_duty(unsigned int permille)  /* 0 to 1000 */
{
    if (permille > PWM_PERIOD) permille = PWM_PERIOD;
    PWMMR2 = permille;
    PWMLER = (1u << 2);                  /* apply at next period */
}

static void delay_ms(unsigned int ms)
{
    volatile unsigned int i, j;
    for (i = 0; i < ms; i++)
        for (j = 0; j < 5000; j++);
}

int main(void)
{
    unsigned int d;

    pwm2_init();
    while (1) {
        for (d = 0; d <= 1000; d += 10) { pwm2_set_duty(d); delay_ms(10); }
        for (d = 1000; d > 0; d -= 10)  { pwm2_set_duty(d); delay_ms(10); }
    }
}

Setting bit 3 of PWMTCR switches the block into PWM mode, which is what makes the shadow latch work; without it the unit behaves like a plain timer and the pin does not toggle. The prescaler of 14 gives a 1 microsecond tick, so the duty value maps directly to tenths of a percent.

Common Mistakes

  • Timings are four times too fast or slow: PCLK is not 15 MHz. Check VPBDIV and recalculate PR.
  • Interrupt fires once and then the program freezes: VICVectAddr is not written at the end of the ISR.
  • Main loop never runs: the T0IR flag is not cleared, so the interrupt keeps re-entering.
  • PWM duty does not change: PWMLER was not written after updating the match register.
  • PWM pin stays low: PWMPCR enable bit or PINSEL setting is missing, or PWM mode bit 3 in PWMTCR is not set.

Summary

LPC2148 timers count PCLK divided by PR plus one, and match registers trigger interrupts, resets or stops. Register an ISR in the VIC with a vector address, a control slot containing the source number and enable bit, and an enable bit in VICIntEnable, and always clear T0IR and write VICVectAddr before returning. The PWM unit uses PWMMR0 for the period, PWMMR1 to PWMMR6 for duty, and PWMLER to apply changes cleanly.