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
| Register | Purpose |
|---|---|
| T0TCR | Bit 0 enables counting, bit 1 holds TC and PC in reset |
| T0PR | Prescale value |
| T0TC | Current timer count |
| T0MR0 | Match value 0 |
| T0MCR | Bit 0 interrupt on MR0, bit 1 reset TC on MR0, bit 2 stop on MR0 |
| T0IR | Interrupt flags; write 1 to bit 0 to clear the MR0 flag |
| PCONP | Bit 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.
#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.
| Output | Pin | PINSEL setting |
|---|---|---|
| PWM1 | P0.0 | PINSEL0 bits 1:0 = 10 |
| PWM2 | P0.7 | PINSEL0 bits 15:14 = 10 |
| PWM3 | P0.1 | PINSEL0 bits 3:2 = 10 |
| PWM4 | P0.8 | PINSEL0 bits 17:16 = 10 |
| PWM5 | P0.21 | PINSEL1 bits 11:10 = 01 |
| PWM6 | P0.9 | PINSEL0 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
#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.