Bug when using LPC11UXX PwmOut for class D audio (possibly hardware)

30 Apr 2014

Hey guys. I'm trying to play some sound effects using PWM (just like a class D amplifier), but I'm having trouble getting my code to work on the LPC11U24. When the sound effect plays, there's another tone being overlaid on top of it at the sample frequency. In the case of my attached code, there's a 2kHz square wave being played on top of the sound effect. My theory is that changing the duty cycle is introducing a glitch in the output that's manifesting as a square wave. The same code runs fine on an LPC1768. Is there any possibility this is a software problem and not a hardware problem? I should mention, I've tried using FastPWM without success.

main.cpp

#include "mbed.h"

//1KB sound effect table (8-bit signed, 2000Hz sample rate)
signed char soundEffectTable[] = {
    0x01,0x03,0x09,0x10,0x16,0x17,0x12,0x07,0xFB,0xEF,0xE6,0xE3,0xE5,0xE9,0xEF,0xF5,
    0xFB,0x01,0x09,0x10,0x15,0x18,0x16,0x0E,0x02,0xF5,0xEC,0xE8,0xEA,0xF0,0xF8,0xFE,
    0x02,0x04,0x04,0x04,0x04,0x04,0x03,0x02,0x01,0x00,0xFF,0x01,0x04,0x09,0x0E,0x10,
    0x0F,0x08,0xFD,0xEF,0xE0,0xD2,0xC7,0xC1,0xC0,0xC5,0xD0,0xE0,0xF4,0x0B,0x22,0x38,
    0x4A,0x56,0x5A,0x55,0x47,0x30,0x14,0xF7,0xDC,0xC8,0xBE,0xBE,0xC8,0xDA,0xF0,0x06,
    0x1B,0x2B,0x32,0x30,0x25,0x12,0xFC,0xE7,0xD6,0xCE,0xD0,0xDC,0xF0,0x08,0x21,0x36,
    0x45,0x4B,0x4A,0x42,0x36,0x27,0x17,0x08,0xFC,0xF5,0xF2,0xF3,0xF8,0xFC,0xFF,0xFB,
    0xF2,0xE3,0xD1,0xC0,0xB4,0xAF,0xB3,0xC0,0xD3,0xEC,0x06,0x1E,0x33,0x41,0x49,0x49,
    0x42,0x33,0x20,0x0B,0xF5,0xE3,0xD5,0xCC,0xC9,0xCB,0xD1,0xDA,0xE4,0xEF,0xF8,0x00,
    0x05,0x09,0x0A,0x0A,0x08,0x06,0x05,0x03,0x03,0x04,0x05,0x07,0x0A,0x0E,0x11,0x14,
    0x17,0x19,0x1A,0x1B,0x1C,0x1C,0x1B,0x1A,0x18,0x15,0x12,0x0F,0x0A,0x06,0x01,0xFC,
    0xF7,0xF2,0xED,0xEA,0xE6,0xE4,0xE3,0xE2,0xE2,0xE4,0xE6,0xE8,0xEC,0xEF,0xF3,0xF6,
    0xFA,0xFD,0x00,0x03,0x05,0x06,0x07,0x07,0x07,0x06,0x05,0x04,0x02,0x00,0xFF,0xFD,
    0xFB,0xF9,0xF8,0xF6,0xF5,0xF4,0xF4,0xF3,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xFA,0xFB,
    0xFC,0xFC,0xFD,0xFD,0xFE,0xFF,0x00,0x01,0x02,0x04,0x06,0x09,0x0C,0x0F,0x12,0x15,
    0x17,0x1A,0x1B,0x1C,0x1C,0x1C,0x1A,0x18,0x16,0x13,0x10,0x0D,0x0A,0x06,0x04,0x01,
    0xFF,0xFD,0xFC,0xFB,0xFA,0xF9,0xF9,0xF9,0xF9,0xF8,0xF8,0xF7,0xF6,0xF5,0xF4,0xF4,
    0xF3,0xF2,0xF1,0xF1,0xF0,0xF0,0xF0,0xF0,0xF1,0xF1,0xF1,0xF2,0xF3,0xF4,0xF4,0xF5,
    0xF6,0xF8,0xF9,0xFA,0xFB,0xFD,0xFE,0xFF,0x00,0x01,0x02,0x03,0x04,0x04,0x04,0x04,
    0x04,0x04,0x05,0x05,0x05,0x06,0x06,0x07,0x08,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,
    0x10,0x10,0x10,0x10,0x0F,0x0F,0x0E,0x0D,0x0D,0x0C,0x0B,0x09,0x08,0x06,0x05,0x03,
    0x01,0x00,0xFE,0xFD,0xFC,0xFB,0xFA,0xFA,0xF9,0xF9,0xF9,0xF9,0xF8,0xF8,0xF8,0xF8,
    0xF7,0xF7,0xF6,0xF5,0xF5,0xF4,0xF3,0xF2,0xF2,0xF1,0xF1,0xF2,0xF2,0xF3,0xF4,0xF5,
    0xF6,0xF7,0xF8,0xF9,0xFA,0xFA,0xFB,0xFC,0xFD,0xFD,0xFE,0xFF,0x01,0x02,0x04,0x05,
    0x06,0x08,0x09,0x0A,0x0B,0x0C,0x0C,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,0x0D,
    0x0C,0x0B,0x0A,0x09,0x08,0x07,0x06,0x05,0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x04,
    0x04,0x04,0x04,0x03,0x03,0x02,0x02,0x01,0x01,0x00,0xFF,0xFE,0xFC,0xFA,0xF9,0xF7,
    0xF5,0xF3,0xF2,0xF0,0xEE,0xEC,0xEB,0xEB,0xEC,0xEE,0xEF,0xEE,0xEC,0xE9,0xE9,0xED,
    0xF4,0xFD,0x07,0x11,0x16,0x14,0x0C,0xFF,0xEF,0xE1,0xDA,0xDD,0xE8,0xFA,0x0F,0x24,
    0x33,0x3B,0x39,0x2E,0x1C,0x07,0xF2,0xE2,0xD7,0xD2,0xD6,0xDF,0xEE,0xFE,0x0F,0x1F,
    0x2C,0x37,0x3E,0x42,0x41,0x3B,0x31,0x22,0x10,0xFC,0xE7,0xD3,0xC2,0xB6,0xB3,0xB8,
    0xC7,0xDE,0xFA,0x17,0x30,0x40,0x47,0x44,0x3A,0x2B,0x1C,0x0E,0x01,0xF5,0xEA,0xE0,
    0xD8,0xD3,0xD2,0xD5,0xDB,0xE6,0xF4,0x04,0x15,0x24,0x2F,0x35,0x33,0x27,0x11,0xF4,
    0xD4,0xB6,0xA0,0x94,0x94,0xA1,0xB8,0xD5,0xF4,0x11,0x29,0x38,0x3F,0x3D,0x35,0x2B,
    0x1F,0x14,0x0B,0x04,0x00,0xFF,0x00,0x04,0x0C,0x15,0x20,0x2A,0x32,0x37,0x37,0x31,
    0x26,0x17,0x05,0xF4,0xE3,0xD6,0xCB,0xC3,0xBF,0xBD,0xBD,0xC1,0xC8,0xD3,0xE1,0xF2,
    0x05,0x18,0x2A,0x38,0x43,0x4A,0x4D,0x4C,0x49,0x43,0x3A,0x30,0x23,0x14,0x05,0xF9,
    0xEF,0xEA,0xE7,0xE6,0xE5,0xE2,0xDB,0xCF,0xC1,0xB2,0xA4,0x9A,0x96,0x9B,0xA8,0xBC,
    0xD6,0xF2,0x0D,0x24,0x36,0x40,0x44,0x41,0x39,0x2F,0x23,0x18,0x0E,0x05,0xFF,0xFC,
    0xFC,0xFF,0x05,0x0D,0x15,0x1A,0x1C,0x19,0x11,0x06,0xFA,0xEF,0xE7,0xE3,0xE4,0xEA,
    0xF3,0xFD,0x06,0x0E,0x13,0x15,0x14,0x10,0x0C,0x09,0x08,0x0A,0x0F,0x16,0x1E,0x24,
    0x29,0x2B,0x29,0x25,0x1E,0x15,0x0A,0x00,0xF5,0xEB,0xE1,0xD9,0xD2,0xCD,0xC9,0xC8,
    0xC9,0xCD,0xD3,0xDA,0xE2,0xEA,0xEF,0xF1,0xF1,0xED,0xE8,0xE1,0xDC,0xD8,0xD7,0xDB,
    0xE2,0xED,0xFC,0x0C,0x1D,0x2D,0x3B,0x45,0x4B,0x4D,0x4A,0x44,0x3C,0x33,0x2A,0x22,
    0x1A,0x13,0x0C,0x05,0xFE,0xF7,0xF0,0xE9,0xE3,0xDF,0xDD,0xDE,0xE0,0xE4,0xE8,0xEB,
    0xED,0xEC,0xEA,0xE6,0xE2,0xDF,0xDD,0xDE,0xE2,0xE9,0xF2,0xFE,0x0B,0x19,0x26,0x33,
    0x3E,0x45,0x4A,0x4B,0x49,0x43,0x3C,0x32,0x28,0x1C,0x11,0x06,0xFA,0xEF,0xE4,0xD9,
    0xCF,0xC6,0xBF,0xBA,0xB7,0xB8,0xBB,0xC1,0xC8,0xD2,0xDC,0xE6,0xF0,0xF9,0x01,0x08,
    0x0D,0x11,0x13,0x16,0x18,0x1A,0x1C,0x1E,0x20,0x21,0x21,0x20,0x1D,0x19,0x13,0x0C,
    0x05,0xFF,0xFB,0xF8,0xF7,0xF9,0xFB,0xFF,0x02,0x05,0x06,0x06,0x04,0x02,0xFE,0xFB,
    0xF9,0xF7,0xF7,0xF9,0xFC,0x00,0x05,0x0A,0x0E,0x11,0x12,0x12,0x0F,0x0B,0x06,0x00,
    0xFB,0xF8,0xF6,0xF7,0xFA,0xFE,0x03,0x08,0x0C,0x0F,0x0F,0x0D,0x08,0x01,0xF7,0xEE,
    0xE5,0xDE,0xD9,0xD8,0xD9,0xDD,0xE3,0xE9,0xF0,0xF4,0xF7,0xF8,0xF7,0xF4,0xF1,0xEE,
    0xED,0xEE,0xF1,0xF7,0xFE,0x06,0x0F,0x18,0x20,0x26,0x2A,0x2C,0x2B,0x29,0x25,0x20,
    0x1B,0x15,0x10,0x0B,0x06,0x01,0xFC,0xF7,0xF2,0xED,0xE9,0xE6,0xE5,0xE7,0xEA,0xEF,
    0xF7,0xFF,0x07,0x0F,0x16,0x1A,0x1D,0x1D,0x1A,0x16,0x10,0x09,0x02,0xFC,0xF7,0xF3,
    0xF1,0xF1,0xF1,0xF3,0xF4,0xF6,0xF8,0xF9,0xFA,0xFA,0xFA,0xFA,0xF9,0xF8,0xF8,0xF7,
    0xF7,0xF7,0xF7,0xF9,0xFB,0xFE,0x02,0x05,0x09,0x0B,0x0D,0x0E,0x0D,0x0B,0x07,0x02,
    0xFD,0xF9,0xF5,0xF3,0xF2,0xF2,0xF4,0xF6,0xF9,0xFC,0xFE,0xFF,0xFF,0xFE,0xFE,0xFD,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
};

Ticker audioTicker;
PwmOut pwmOut(p26);
volatile int idx;

void onAudioTicker()
{
    //Load the next sample
    pwmOut = ((int)soundEffectTable[idx++] + 127) / 255.0;

    //Check if we're done
    if (idx >= 1024) {
        idx = 0;
        audioTicker.detach();
    }
}

int main()
{
    //Configure the PWM output for class D style audio (200kHz)
    pwmOut.period_us(5);

    //Play the sound effect every 2.0 seconds
    while (1) {
        //Start the ticker at a sample rate of 2000Hz
        audioTicker.attach_us(&onAudioTicker, 500);
        wait(2.0);
    }
}
01 May 2014

Just did some digging around over at GitHub, and the last three lines of pwmout_write() in pwmout_api.c seem suspicious:

pwmout_api.c

void pwmout_write(pwmout_t* obj, float value) {
    if (value < 0.0f) {
        value = 0.0;
    } else if (value > 1.0f) {
        value = 1.0;
    }
    
    timer_mr tid = pwm_timer_map[obj->pwm];
    LPC_CTxxBx_Type *timer = Timers[tid.timer];
    uint32_t t_off = timer->MR3 - (uint32_t)((float)(timer->MR3) * value);
    
    timer->TCR = TCR_RESET;
    timer->MR[tid.mr] = t_off;
    timer->TCR = TCR_CNT_EN;
}

Any particular reason we need to reset the timer and re-enable it every time?

01 May 2014
01 May 2014

Just tried removing the timer->TCR = TCR_RESET; and timer->TCR = TCR_CNT_EN; lines in pwmout_write(), and the problem seems to have disappeared! I've submit a pull request with the patch. It looks like pwmout_pulsewidth_us() might suffer from the same issue, but I have no way of testing it to make sure...

01 May 2014

Thanks for finding this, fixed it also in FastPWM (I guess i just copied that from mbed code).

One reason this is done is that the LPC11u24 lacks PWM synchronization logic which most others do have. This generally makes sure the new match value is only loaded on the next period. However the original 'solution' only increases the number of glitches you get by making 100% sure you get a glitch. So you are correct it is better with it gone. In the period code it is required however. Both because it makes sense (otherwise you can get an overflow), and because otherwise it doesn't seem to be doing anything whatsoever, which I don't get yet. Probably doing something wrong, but because of the overflow issue it is required anyway.

One thing to note, I don't know how high your requirements are, but if reasonably high, consider another microcontroller. Lets say the match register has as value 100, the counter is at 80, and you put in a new duty cycle which corresponds with a match value of 70. Most other devices would keep the 80, and once it resets do the next cycle with 70. The LPC11u24 now will immediatly put in the 70, causing no match to happen during the entire period: a glitch.

01 May 2014

Erik - wrote:

Thanks for finding this, fixed it also in FastPWM (I guess i just copied that from mbed code).

One reason this is done is that the LPC11u24 lacks PWM synchronization logic which most others do have. This generally makes sure the new match value is only loaded on the next period. However the original 'solution' only increases the number of glitches you get by making 100% sure you get a glitch. So you are correct it is better with it gone. In the period code it is required however. Both because it makes sense (otherwise you can get an overflow), and because otherwise it doesn't seem to be doing anything whatsoever, which I don't get yet. Probably doing something wrong, but because of the overflow issue it is required anyway.

One thing to note, I don't know how high your requirements are, but if reasonably high, consider another microcontroller. Lets say the match register has as value 100, the counter is at 80, and you put in a new duty cycle which corresponds with a match value of 70. Most other devices would keep the 80, and once it resets do the next cycle with 70. The LPC11u24 now will immediatly put in the 70, causing no match to happen during the entire period: a glitch.

Well, that explains my latest issue: the 2kHz tone is gone but now there's crackles. Sadly, I don't think this approach is going to work, I'm going to try using an I2C DAC instead...

P.S. I wasn't referring to the period function needing to be changed, I was referring to the pulsewidth function. Changing the period definitely requires a reset, but the pulsewidth function just ends up calling the period function if it needs to change it. Therefore, I don't think it needs to do it's own reset for what seems to be just another duty cycle adjustment.