Patchwork [v2] Add AACI audio playback support to the ARM Versatile/PB platform

login
register
mail settings
Submitter contact@elasticsheep.com
Date May 13, 2011, 10:08 p.m.
Message ID <1305324490-12113-1-git-send-email-contact@elasticsheep.com>
Download mbox | patch
Permalink /patch/95533/
State New
Headers show

Comments

contact@elasticsheep.com - May 13, 2011, 10:08 p.m.
The ACLink bus has been removed.
The PL041/AACI driver now directly control the LM4549 codec driver.

The AC97 emulation in ac97.c has not been reused. Making the current
implementation sharable is not trivial and could bring regression
to other platforms.

Versatile/PB test build:
linux-2.6.38.5
buildroot-2010.11
alsa-lib-1.0.22
alsa-utils-1.0.22
mpg123-0.66

Qemu host: Ubuntu 10.04 in Vmware/OS X

Playback tested successfully with aplay and mpg123.

Signed-off-by: Mathieu Sonet <contact@elasticsheep.com>
---
 Makefile.target  |    1 +
 hw/lm4549.c      |  307 +++++++++++++++++++++++++++++++++++++++++
 hw/lm4549.h      |   45 ++++++
 hw/pl041.c       |  406 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hw/pl041.h       |  125 +++++++++++++++++
 hw/pl041.hx      |   61 ++++++++
 hw/versatilepb.c |    3 +
 7 files changed, 948 insertions(+), 0 deletions(-)
 create mode 100644 hw/lm4549.c
 create mode 100644 hw/lm4549.h
 create mode 100644 hw/pl041.c
 create mode 100644 hw/pl041.h
 create mode 100644 hw/pl041.hx

--
1.7.0.4
malc - May 14, 2011, 12:01 a.m.
On Fri, 13 May 2011, Mathieu Sonet wrote:

> The ACLink bus has been removed.
> The PL041/AACI driver now directly control the LM4549 codec driver.
> 
> The AC97 emulation in ac97.c has not been reused. Making the current
> implementation sharable is not trivial and could bring regression
> to other platforms.
> 
> Versatile/PB test build:
> linux-2.6.38.5
> buildroot-2010.11
> alsa-lib-1.0.22
> alsa-utils-1.0.22
> mpg123-0.66
> 
> Qemu host: Ubuntu 10.04 in Vmware/OS X
> 
> Playback tested successfully with aplay and mpg123.
> 

Once again, no objections.

[..snip..]
Peter Maydell - Aug. 2, 2011, 1:40 p.m.
On 13 May 2011 23:08, Mathieu Sonet <contact@elasticsheep.com> wrote:
[PL041/AACI patches]

I apologise for the exceedingly delayed review here.

So to start with, I tested this patch with the vexpress-a9
board (by adding the one line to add a pl041 to it) and it
works, which is really cool.

A minor note on patch formatting: since the bit of the email up
to the '---' goes into the git commit history, it should be a
description of this version of the patch rather than a list
of the differences to the previous version. (I usually put the
"v1->v2" summary after the '---' before the diffstat.)

Moving on to the code review...

> +/*** Functions ***/

I'm not a fan of comments which state the obvious.

> +
> +static void lm4549_store_init(lm4549_state *s, uint32_t offset, uint32_t value,
> +                              uint32_t is_read_only)

Making value a uint16_t would be consistent with lm4549_store and _load.

> +{
> +    lm4549_registers *r = &s->codec_regs;
> +
> +    if (offset > 128) {
> +        DPRINTF("store_init: Out of bound offset 0x%x\n", (int)offset);
> +    }

You need a return here, otherwise we blunder on and access the
array out of bounds. (Or you could just assert(), since we only
use this function internally.)

> +    r->data[offset].value = value & 0xFFFF;
> +    r->data[offset].default_value = value & 0xFFFF;

These "& 0xFFFF" aren't necessary.

> +static void lm4549_store(lm4549_state *s, uint32_t offset, uint16_t value)

To be honest I'm not entirely convinced of the necessity of
abstracting out the register file into these _load/_store/_reset
functions rather than just having lm4549_read and _write access
the array directly. (eg since there are only 3 read-only registers
it would be simpler just to have lm4549_write's switch statement have:
 case LM4549_Extended_Audio_ID:
 case LM4549_Vendor_ID1:
 case LM4549_Vendor_ID2:
     /* Read-only registers */
     break;

rather than having all the machinery for the read_only field in
the lm4549_registers struct. Similarly if you just handle the
reset values in a reset function then your register array is
just a simple uint16_t registers[128].)

> +{
> +    lm4549_registers *r = &s->codec_regs;
> +
> +    if (offset > 128) {
> +        DPRINTF("store: Out of bound offset 0x%x\n", (int)offset);
> +    }

Needs a return again. (Also the case in lm4549_load.)

> +    r->data[offset].value = value & 0xFFFF;

Unnecessary mask.

> +    if (written_samples == s->buffer_level) {
> +        s->buffer_level = 0;
> +    } else {
> +        s->buffer_level -= written_samples;
> +
> +        if (s->buffer_level > 0) {
> +            /* Move the data back to the start of the buffer */
> +            for (i = 0; i < s->buffer_level; i++) {
> +                s->buffer[i] = s->buffer[i + written_samples];
> +            }
> +        }
> +    }

You can just delete the if () { ... } part here, because
if written_samples == s->buffer_level then the else {}
clause is equivalent in effect to s->buffer_level = 0.
[In fact "(s->buffer_level > 0)" can only be true if
written_samples != s->buffer_level, because this is all
unsigned arithmetic.]

> diff --git a/hw/pl041.c b/hw/pl041.c
> new file mode 100644
> index 0000000..123612c
> --- /dev/null
> +++ b/hw/pl041.c
> @@ -0,0 +1,406 @@
> +/*
> + * Arm PrimeCell PL041 Advanced Audio Codec Interface
> + *
> + * Copyright (c) 2011
> + * Written by Mathieu Sonet - www.elasticsheep.com
> + *
> + * This code is licenced under the GPL.
> + *
> + * *****************************************************************
> + *
> + * This driver emulates the ARM AACI interface
> + * connected to a LM4549 codec.
> + *
> + */
> +
> +#include "sysbus.h"
> +
> +#include "pl041.h"
> +#include "lm4549.h"
> +
> +/*** Debug macros ***/
> +
> +/* #define PL041_DEBUG_LEVEL 1 */
> +
> +#if defined(PL041_DEBUG_LEVEL) && (PL041_DEBUG_LEVEL >= 1)
> +#define DBG_L1(fmt, ...) \
> +do { printf("pl041: " fmt , ## __VA_ARGS__); } while (0)
> +#else
> +#define DBG_L1(fmt, ...) \
> +do { } while (0)
> +#endif
> +
> +#if defined(PL041_DEBUG_LEVEL) && (PL041_DEBUG_LEVEL >= 2)
> +#define DBG_L2(fmt, ...) \
> +do { printf("pl041: " fmt , ## __VA_ARGS__); } while (0)
> +#else
> +#define DBG_L2(fmt, ...) \
> +do { } while (0)
> +#endif
> +
> +/*** Constants ***/
> +
> +#define FIFO_DEPTH  16

The FIFOs in the pl041 in the various ARM dev boards
are actually larger than the stock pl041 FIFO. Possibly
FIFO size should be a qdev property (does changing it
have a (good or bad) effect on the model pl041's latency
or tendency to dropouts?).

> +#define SLOT1_RW    (1 << 19)
> +
> +/*** Types ***/
> +
> +typedef struct {
> +    uint32_t size;
> +    uint32_t half;
> +    uint32_t level;
> +    uint32_t data[FIFO_DEPTH];
> +} pl041_fifo;

FIFO size is a property of the pl041, not per-FIFO, so
it doesn't belong in this struct. Also, there's not much
point in just caching size / 2 as 'half'.

> +
> +typedef struct {
> +    SysBusDevice busdev;
> +    qemu_irq irq;
> +    pl041_regfile regs;
> +    pl041_fifo fifo1;
> +    lm4549_state codec;
> +} pl041_state;

...this is implicitly modelling the versatile/vexpress
modified PL041 which only has one channel -- stock PL041
has four channels with a fifo each.

> +/*** Globals ***/
> +
> +static const unsigned char pl041_id[8] = {
> +    0x41, 0x10, 0x04, 0x00, 0x0d, 0xf0, 0x05, 0xb1
> +};

The modified PL041s in the versatilepb, vexpress etc
have slightly different ID values (the FIFO depth and
number of channels are encoded in one of the bytes).

> +#if defined(PL041_DEBUG_LEVEL)
> +#define REGISTER(name, offset) #name,
> +static const char *pl041_regs_name[] = {
> +    #include "pl041.hx"
> +};
> +#undef REGISTER
> +#endif
> +
> +/*** Functions ***/
> +
> +#if defined(PL041_DEBUG_LEVEL)
> +static const char *get_reg_name(target_phys_addr_t offset)

Minor point but "pl041_reg_name()" would be a better name I think.

> +{
> +    if (offset <= 0xAC) {

Use sizeof() rather than a magic constant. Returning
"unknown" if the pl041_regs_name[] array entry is NULL
would also be more robust since it means we don't rely
on having a REGISTER() entry for each offset.

> +        return pl041_regs_name[offset >> 2];
> +    }
> +
> +    return "unknown";
> +}
> +#endif
> +
> +static void pl041_fifo1_push(pl041_state *s, uint32_t value)
> +{
> +    pl041_fifo *f = &s->fifo1;
> +
> +    /* Check the FIFO level */
> +    if (f->level >= f->size) {
> +        hw_error("fifo1 push: overrun\n");
> +    }

Calling hw_error() on a FIFO overrun looks wrong. hw_error() will
make qemu call abort() so as a general rule a device model should
not use it for any condition the guest can provoke.

> +static void pl041_isr1_update(pl041_state *s)
> +{
> +    uint32_t mask = 0;
> +
> +    /* Update ISR1 */
> +    if (s->regs.sr1 & TXUNDERRUN) {
> +        s->regs.isr1 |= URINTR;
> +    } else {
> +        s->regs.isr1 &= ~URINTR;
> +    }
> +
> +    if (s->regs.sr1 & TXHE) {
> +        s->regs.isr1 |= TXINTR;
> +    } else {
> +        s->regs.isr1 &= ~TXINTR;
> +    }
> +
> +    if (!(s->regs.sr1 & TXBUSY) && (s->regs.sr1 & TXFE)) {
> +        s->regs.isr1 |= TXCINTR;
> +    } else {
> +        s->regs.isr1 &= ~TXCINTR;
> +    }
> +
> +    /* Set the irq mask */
> +    if (s->regs.ie1 & TXUIE) {
> +        mask |= URINTR;
> +    }
> +
> +    if (s->regs.ie1 & TXIE) {
> +        mask |= TXINTR;
> +    }
> +
> +    if (s->regs.ie1 & TXCIE) {
> +        mask |= TXCINTR;
> +    }

The IEx mask bits are deliberately arranged to match the
ISRx bits, so you don't need to construct the mask like
this, you can just directly use s->regs.ie1 as your mask.

> +    /* Update the irq state */
> +    qemu_set_irq(s->irq, ((s->regs.isr1 & mask) > 0) ? 1 : 0);
> +    DBG_L2("Set interrupt sr1 = 0x%08x isr1 = 0x%08x masked = 0x%08x\n",
> +           s->regs.sr1, s->regs.isr1, s->regs.isr1 & mask);
> +}

> +static uint32_t pl041_read(void *opaque, target_phys_addr_t offset)
> +{
> +    pl041_state *s = (pl041_state *)opaque;
> +    int value;
> +
> +    if (offset >= 0xfe0 && offset < 0x1000) {
> +        DBG_L1("pl041_read [0x%08x]\n", offset);
> +        return pl041_id[(offset - 0xfe0) >> 2];
> +    }
> +
> +    if (offset < 0x110) {

Again, use sizeof().

> +        value = *((uint32_t *)&s->regs + (offset >> 2));
> +    } else {
> +        hw_error("pl041_read: Bad offset %x\n", (int)offset);

hw_error() here is definitely wrong. I'd suggest printing a
message if debugging is enabled and otherwise just read as
zero/writes ignored.

> +    }
> +
> +    switch (offset) {
> +    case PL041_allints:
> +        value = s->regs.isr1 & 0x7F;
> +        break;
> +    }

There's quite a lot of PL041 functionality unimplemented here;
I'm not going to ask you to implement it all, but a comment
at the top of the file somewhere giving a brief summary of
what isn't implemented would be good.

> +static int pl041_init(SysBusDevice *dev)
> +{
> +    pl041_state *s = FROM_SYSBUS(pl041_state, dev);
> +    int iomemtype;
> +
> +    DBG_L1("pl041_init\n");
> +
> +    /* Connect the device to the sysbus */
> +    iomemtype = cpu_register_io_memory(pl041_readfn,
> +                                       pl041_writefn,
> +                                       s,
> +                                       DEVICE_NATIVE_ENDIAN);
> +    sysbus_init_mmio(dev, 0x1000, iomemtype);
> +    sysbus_init_irq(dev, &s->irq);
> +
> +    /* Reset the device */
> +    pl041_reset(s);
> +
> +    /* Init the codec */
> +    lm4549_init(&s->codec, &pl041_request_data, (void *)s);
> +
> +    return 0;
> +}
> +
> +static void pl041_register_devices(void)
> +{
> +    sysbus_register_dev("pl041", sizeof(pl041_state), pl041_init);
> +}

This is missing save/load support, for which you need:
 (1) use sysbus_register_withprop() and pass it a SysBusDeviceInfo
 (2) write a VMStateDescription and pass it as the .qdev.vmsd
 field of your SysBusDeviceInfo

(When you do this you can then pass your reset routine as .qdev.reset
and you don't need to call it in your pl041_init() function. Watch
out that the .reset routine takes a DeviceState*.)

You can see an example of this in hw/pl190.c.

> +
> +device_init(pl041_register_devices)
> diff --git a/hw/pl041.h b/hw/pl041.h
> new file mode 100644
> index 0000000..ddf452d
> --- /dev/null
> +++ b/hw/pl041.h
> @@ -0,0 +1,125 @@
> +/*
> + * Arm PrimeCell PL041 Advanced Audio Codec Interface

This header is only used by hw/pl041.c so you might as well
just put all its contents in the .c file. (If you do leave
it as a separate header it needs #ifndef HW_PL041_H guards.)

-- PMM

Patch

diff --git a/Makefile.target b/Makefile.target
index 21f864a..1a6565f 100644
--- a/Makefile.target
+++ b/Makefile.target
@@ -354,6 +354,7 @@  obj-arm-y += syborg_virtio.o
 obj-arm-y += vexpress.o
 obj-arm-y += strongarm.o
 obj-arm-y += collie.o
+obj-arm-y += pl041.o lm4549.o

 obj-sh4-y = shix.o r2d.o sh7750.o sh7750_regnames.o tc58128.o
 obj-sh4-y += sh_timer.o sh_serial.o sh_intc.o sh_pci.o sm501.o
diff --git a/hw/lm4549.c b/hw/lm4549.c
new file mode 100644
index 0000000..e58d4d4
--- /dev/null
+++ b/hw/lm4549.c
@@ -0,0 +1,307 @@ 
+/*
+ * LM4549 Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This driver emulates the LM4549 codec.
+ *
+ */
+
+#include "hw.h"
+#include "audio/audio.h"
+#include "lm4549.h"
+
+/* #define LM4549_DEBUG  1 */
+/* #define LM4549_DUMP_DAC_INPUT 1 */
+
+#ifdef LM4549_DEBUG
+#define DPRINTF(fmt, ...) \
+do { printf("lm4549: " fmt , ## __VA_ARGS__); } while (0)
+#else
+#define DPRINTF(fmt, ...) do {} while (0)
+#endif
+
+#if defined(LM4549_DUMP_DAC_INPUT)
+#include <stdio.h>
+static FILE *fp_dac_input;
+#endif
+
+/*** LM4549 register list ***/
+
+enum {
+    LM4549_Reset                    = 0x00,
+    LM4549_Master_Volume            = 0x02,
+    LM4549_Line_Out_Volume          = 0x04,
+    LM4549_Master_Volume_Mono       = 0x06,
+    LM4549_PC_Beep_Volume           = 0x0A,
+    LM4549_Phone_Volume             = 0x0C,
+    LM4549_Mic_Volume               = 0x0E,
+    LM4549_Line_In_Volume           = 0x10,
+    LM4549_CD_Volume                = 0x12,
+    LM4549_Video_Volume             = 0x14,
+    LM4549_Aux_Volume               = 0x16,
+    LM4549_PCM_Out_Volume           = 0x18,
+    LM4549_Record_Select            = 0x1A,
+    LM4549_Record_Gain              = 0x1C,
+    LM4549_General_Purpose          = 0x20,
+    LM4549_3D_Control               = 0x22,
+    LM4549_Powerdown_Ctrl_Stat      = 0x26,
+    LM4549_Extended_Audio_ID        = 0x28,
+    LM4549_Extended_Audio_Stat_Ctrl = 0x2A,
+    LM4549_PCM_Front_DAC_Rate       = 0x2C,
+    LM4549_PCM_ADC_Rate             = 0x32,
+    LM4549_Vendor_ID1               = 0x7C,
+    LM4549_Vendor_ID2               = 0x7E
+};
+
+/*** Functions ***/
+
+static void lm4549_store_init(lm4549_state *s, uint32_t offset, uint32_t value,
+                              uint32_t is_read_only)
+{
+    lm4549_registers *r = &s->codec_regs;
+
+    if (offset > 128) {
+        DPRINTF("store_init: Out of bound offset 0x%x\n", (int)offset);
+    }
+
+    r->data[offset].value = value & 0xFFFF;
+    r->data[offset].default_value = value & 0xFFFF;
+    r->data[offset].read_only = (is_read_only > 0) ? 1 : 0;
+}
+
+static void lm4549_store_reset(lm4549_state *s)
+{
+    lm4549_registers *r = &s->codec_regs;
+    int i;
+
+    for (i = 0; i < 128; i++) {
+        r->data[i].value = r->data[i].default_value;
+    }
+}
+
+static void lm4549_store(lm4549_state *s, uint32_t offset, uint16_t value)
+{
+    lm4549_registers *r = &s->codec_regs;
+
+    if (offset > 128) {
+        DPRINTF("store: Out of bound offset 0x%x\n", (int)offset);
+    }
+
+    if (r->data[offset].read_only) {
+        DPRINTF("store: Read-only offset 0x%x\n", (int)offset);
+        return;
+    }
+
+    r->data[offset].value = value & 0xFFFF;
+}
+
+static uint16_t lm4549_load(lm4549_state *s, uint32_t offset)
+{
+    lm4549_registers *r = &s->codec_regs;
+
+    if (offset > 128) {
+        hw_error("load: Out of bound offset 0x%x\n", (int)offset);
+    }
+
+    return r->data[offset].value;
+}
+
+static void lm4549_audio_transfer(lm4549_state *s)
+{
+    uint32_t written_bytes, written_samples;
+    uint32_t i;
+
+    /* Activate the voice */
+    AUD_set_active_out(s->voice, 1);
+
+    /* Try to write the buffer content */
+    written_bytes = AUD_write(s->voice, s->buffer,
+                              s->buffer_level * sizeof(uint32_t));
+    written_samples = written_bytes >> 2;
+
+#if defined(LM4549_DUMP_DAC_INPUT)
+    fwrite(s->buffer, sizeof(uint8_t), written_bytes, fp_dac_input);
+#endif
+
+    if (written_samples == s->buffer_level) {
+        s->buffer_level = 0;
+    } else {
+        s->buffer_level -= written_samples;
+
+        if (s->buffer_level > 0) {
+            /* Move the data back to the start of the buffer */
+            for (i = 0; i < s->buffer_level; i++) {
+                s->buffer[i] = s->buffer[i + written_samples];
+            }
+        }
+    }
+}
+
+static void lm4549_audio_out_callback(void *opaque, int free)
+{
+    lm4549_state *s = (lm4549_state *)opaque;
+    static uint32_t prev_buffer_level;
+
+#ifdef LM4549_DEBUG
+    int size = AUD_get_buffer_size_out(s->voice);
+    DPRINTF("audio_out_callback size = %i free = %i\n", size, free);
+#endif
+
+    /* Detect that no data are consumed
+       => disable the voice */
+    if (s->buffer_level == prev_buffer_level) {
+        AUD_set_active_out(s->voice, 0);
+    }
+    prev_buffer_level = s->buffer_level;
+
+    /* Check if a buffer transfer is pending */
+    if (s->buffer_level == BUFFER_SIZE) {
+        lm4549_audio_transfer(s);
+
+        /* Request more data */
+        if (s->data_req_cb != NULL) {
+            (s->data_req_cb)(s->opaque);
+        }
+    }
+}
+
+uint32_t lm4549_read(lm4549_state *s, target_phys_addr_t offset)
+{
+    uint32_t value = 0;
+
+    /* Read the stored value */
+    value = lm4549_load(s, offset);
+    DPRINTF("read [0x%02x] = 0x%04x\n", offset, value);
+
+    return value;
+}
+
+void lm4549_write(lm4549_state *s,
+                  target_phys_addr_t offset, uint32_t value)
+{
+    DPRINTF("write [0x%02x] = 0x%04x\n", offset, value);
+
+    switch (offset) {
+    case LM4549_Reset:
+        lm4549_store_reset(s);
+        break;
+    case LM4549_PCM_Front_DAC_Rate:
+        lm4549_store(s, offset, value);
+        DPRINTF("DAC rate change = %i\n", lm4549_load(s, offset));
+
+        /* Re-open a voice with the new sample rate */
+        struct audsettings as;
+        as.freq = lm4549_load(s, offset);
+        as.nchannels = 2;
+        as.fmt = AUD_FMT_S16;
+        as.endianness = 0;
+
+        s->voice = AUD_open_out(
+            &s->card,
+            s->voice,
+            "lm4549.out",
+            s,
+            lm4549_audio_out_callback,
+            &as
+        );
+        break;
+    case LM4549_Powerdown_Ctrl_Stat:
+        value &= ~0xf;
+        value |= lm4549_load(s, offset) & 0xf;
+        lm4549_store(s, offset, value);
+        break;
+    default:
+        /* Store the new value */
+        lm4549_store(s, offset, value);
+        break;
+    }
+}
+
+uint32_t lm4549_write_sample(lm4549_state *s, uint32_t sample)
+{
+    if (s->buffer_level >= BUFFER_SIZE) {
+        DPRINTF("write_sample Buffer full\n");
+        return 0;
+    }
+
+    /* Store the sample in the buffer */
+    s->buffer[s->buffer_level++] = sample;
+
+    if (s->buffer_level == BUFFER_SIZE) {
+        /* Trigger the transfer of the buffer to the audio host */
+        lm4549_audio_transfer(s);
+    }
+
+    return 1;
+}
+
+void lm4549_init(lm4549_state *s, lm4549_callback data_req_cb, void* opaque)
+{
+    struct audsettings as;
+
+    /* Store the callback and opaque pointer */
+    s->data_req_cb = data_req_cb;
+    s->opaque = opaque;
+
+    /* Init the register store */
+    lm4549_store_init(s, LM4549_Reset,                     0x0d50, 0);
+    lm4549_store_init(s, LM4549_Master_Volume,             0x8008, 0);
+    lm4549_store_init(s, LM4549_Line_Out_Volume,           0x8000, 0);
+    lm4549_store_init(s, LM4549_Master_Volume_Mono,        0x8000, 0);
+    lm4549_store_init(s, LM4549_PC_Beep_Volume,            0x0000, 0);
+    lm4549_store_init(s, LM4549_Phone_Volume,              0x8008, 0);
+    lm4549_store_init(s, LM4549_Mic_Volume,                0x8008, 0);
+    lm4549_store_init(s, LM4549_Line_In_Volume,            0x8808, 0);
+    lm4549_store_init(s, LM4549_CD_Volume,                 0x8808, 0);
+    lm4549_store_init(s, LM4549_Video_Volume,              0x8808, 0);
+    lm4549_store_init(s, LM4549_Aux_Volume,                0x8808, 0);
+    lm4549_store_init(s, LM4549_PCM_Out_Volume,            0x8808, 0);
+    lm4549_store_init(s, LM4549_Record_Select,             0x0000, 0);
+    lm4549_store_init(s, LM4549_Record_Gain,               0x8000, 0);
+    lm4549_store_init(s, LM4549_General_Purpose,           0x0000, 0);
+    lm4549_store_init(s, LM4549_3D_Control,                0x0101, 0);
+    lm4549_store_init(s, LM4549_Powerdown_Ctrl_Stat,       0x000f, 0);
+    lm4549_store_init(s, LM4549_Extended_Audio_ID,         0x0001, 1);
+    lm4549_store_init(s, LM4549_Extended_Audio_Stat_Ctrl,  0x0000, 0);
+    lm4549_store_init(s, LM4549_PCM_Front_DAC_Rate,        0xBB80, 0);
+    lm4549_store_init(s, LM4549_PCM_ADC_Rate,              0xBB80, 0);
+    lm4549_store_init(s, LM4549_Vendor_ID1,                0x4e53, 1);
+    lm4549_store_init(s, LM4549_Vendor_ID2,                0x4331, 1);
+
+    /* Register an audio card */
+    AUD_register_card("lm4549", &s->card);
+
+    /* Open a default voice */
+    as.freq = 48000;
+    as.nchannels = 2;
+    as.fmt = AUD_FMT_S16;
+    as.endianness = 0;
+
+    s->voice = AUD_open_out(
+        &s->card,
+        s->voice,
+        "lm4549.out",
+        s,
+        lm4549_audio_out_callback,
+        &as
+    );
+
+    AUD_set_volume_out(s->voice, 0, 255, 255);
+
+    /* Reset the input buffer */
+    memset(s->buffer, 0x00, sizeof(s->buffer));
+    s->buffer_level = 0;
+
+#if defined(LM4549_DUMP_DAC_INPUT)
+    fp_dac_input = fopen("lm4549_dac_input.pcm", "wb");
+    if (!fp_dac_input) {
+        hw_error("Unable to open lm4549_dac_input.pcm for writing\n");
+    }
+#endif
+}
diff --git a/hw/lm4549.h b/hw/lm4549.h
new file mode 100644
index 0000000..bc61d02
--- /dev/null
+++ b/hw/lm4549.h
@@ -0,0 +1,45 @@ 
+/*
+ * LM4549 Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This driver emulates the LM4549 codec.
+ *
+ */
+
+#include "audio/audio.h"
+
+typedef void (*lm4549_callback)(void *opaque);
+
+/*** LM4549 device state ***/
+typedef struct {
+    struct {
+        uint16_t value;
+        uint16_t default_value;
+        uint16_t read_only;
+    } data[128];
+} lm4549_registers;
+
+typedef struct {
+    lm4549_registers codec_regs;
+    QEMUSoundCard card;
+    SWVoiceOut *voice;
+
+    lm4549_callback data_req_cb;
+    void *opaque;
+
+#define BUFFER_SIZE (512)
+    uint32_t buffer[BUFFER_SIZE];
+    uint32_t buffer_level;
+} lm4549_state;
+
+/*** Public interface ***/
+void lm4549_init(lm4549_state *s, lm4549_callback data_req, void *opaque);
+uint32_t lm4549_read(lm4549_state *s, target_phys_addr_t offset);
+void lm4549_write(lm4549_state *s, target_phys_addr_t offset, uint32_t value);
+uint32_t lm4549_write_sample(lm4549_state *s, uint32_t sample);
diff --git a/hw/pl041.c b/hw/pl041.c
new file mode 100644
index 0000000..123612c
--- /dev/null
+++ b/hw/pl041.c
@@ -0,0 +1,406 @@ 
+/*
+ * Arm PrimeCell PL041 Advanced Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This driver emulates the ARM AACI interface
+ * connected to a LM4549 codec.
+ *
+ */
+
+#include "sysbus.h"
+
+#include "pl041.h"
+#include "lm4549.h"
+
+/*** Debug macros ***/
+
+/* #define PL041_DEBUG_LEVEL 1 */
+
+#if defined(PL041_DEBUG_LEVEL) && (PL041_DEBUG_LEVEL >= 1)
+#define DBG_L1(fmt, ...) \
+do { printf("pl041: " fmt , ## __VA_ARGS__); } while (0)
+#else
+#define DBG_L1(fmt, ...) \
+do { } while (0)
+#endif
+
+#if defined(PL041_DEBUG_LEVEL) && (PL041_DEBUG_LEVEL >= 2)
+#define DBG_L2(fmt, ...) \
+do { printf("pl041: " fmt , ## __VA_ARGS__); } while (0)
+#else
+#define DBG_L2(fmt, ...) \
+do { } while (0)
+#endif
+
+/*** Constants ***/
+
+#define FIFO_DEPTH  16
+
+#define SLOT1_RW    (1 << 19)
+
+/*** Types ***/
+
+typedef struct {
+    uint32_t size;
+    uint32_t half;
+    uint32_t level;
+    uint32_t data[FIFO_DEPTH];
+} pl041_fifo;
+
+typedef struct {
+    SysBusDevice busdev;
+    qemu_irq irq;
+    pl041_regfile regs;
+    pl041_fifo fifo1;
+    lm4549_state codec;
+} pl041_state;
+
+/*** Globals ***/
+
+static const unsigned char pl041_id[8] = {
+    0x41, 0x10, 0x04, 0x00, 0x0d, 0xf0, 0x05, 0xb1
+};
+
+#if defined(PL041_DEBUG_LEVEL)
+#define REGISTER(name, offset) #name,
+static const char *pl041_regs_name[] = {
+    #include "pl041.hx"
+};
+#undef REGISTER
+#endif
+
+/*** Functions ***/
+
+#if defined(PL041_DEBUG_LEVEL)
+static const char *get_reg_name(target_phys_addr_t offset)
+{
+    if (offset <= 0xAC) {
+        return pl041_regs_name[offset >> 2];
+    }
+
+    return "unknown";
+}
+#endif
+
+static void pl041_fifo1_push(pl041_state *s, uint32_t value)
+{
+    pl041_fifo *f = &s->fifo1;
+
+    /* Check the FIFO level */
+    if (f->level >= f->size) {
+        hw_error("fifo1 push: overrun\n");
+    }
+
+    /* Push the value in the FIFO */
+    if (f->level < f->size) {
+        s->fifo1.data[f->level++] = value;
+    }
+
+    /* Update the status register */
+    if (f->level > 0) {
+        s->regs.sr1 &= ~(TXUNDERRUN | TXFE);
+    }
+
+    if (f->level >= (f->size >> 1)) {
+        s->regs.sr1 &= ~TXHE;
+    }
+
+    if (f->level >= f->size) {
+        s->regs.sr1 |= TXFF;
+    }
+
+    DBG_L1("fifo1_push sr1 = 0x%08x\n", s->regs.sr1);
+}
+
+static void pl041_fifo1_transmit(pl041_state *s)
+{
+    pl041_fifo *f = &s->fifo1;
+    uint32_t slots = s->regs.txcr1 & TXSLOT_MASK;
+    uint32_t written_samples;
+
+    /* Check if FIFO1 transmit is enabled */
+    if ((s->regs.txcr1 & TXEN) && (slots & (TXSLOT3 | TXSLOT4))) {
+        if (f->level >= f->half) {
+            int i;
+
+            DBG_L1("Transfer FIFO level = %i\n", f->level);
+
+            /* Try to transfer the whole FIFO */
+            for (i = 0; i < f->level; i++) {
+                uint32_t sample = f->data[i];
+
+                /* Transmit one sample to the codec */
+                if (lm4549_write_sample(&s->codec, sample) == 0) {
+                    DBG_L1("Codec buffer full\n");
+                    break;
+                }
+            }
+
+            written_samples = i;
+            if (written_samples > 0) {
+                /* Update the FIFO level */
+                f->level -= written_samples;
+
+                /* Move back the pending samples to the start of the FIFO */
+                for (i = 0; i < f->level; i++) {
+                    f->data[i] = f->data[written_samples + i];
+                }
+
+                /* Update the status register */
+                s->regs.sr1 &= ~TXFF;
+
+                if (f->level <= (f->size >> 1)) {
+                    s->regs.sr1 |= TXHE;
+                }
+
+                if (f->level == 0) {
+                    s->regs.sr1 |= TXFE | TXUNDERRUN;
+                    DBG_L1("Empty FIFO\n");
+                }
+            }
+        }
+    }
+}
+
+static void pl041_isr1_update(pl041_state *s)
+{
+    uint32_t mask = 0;
+
+    /* Update ISR1 */
+    if (s->regs.sr1 & TXUNDERRUN) {
+        s->regs.isr1 |= URINTR;
+    } else {
+        s->regs.isr1 &= ~URINTR;
+    }
+
+    if (s->regs.sr1 & TXHE) {
+        s->regs.isr1 |= TXINTR;
+    } else {
+        s->regs.isr1 &= ~TXINTR;
+    }
+
+    if (!(s->regs.sr1 & TXBUSY) && (s->regs.sr1 & TXFE)) {
+        s->regs.isr1 |= TXCINTR;
+    } else {
+        s->regs.isr1 &= ~TXCINTR;
+    }
+
+    /* Set the irq mask */
+    if (s->regs.ie1 & TXUIE) {
+        mask |= URINTR;
+    }
+
+    if (s->regs.ie1 & TXIE) {
+        mask |= TXINTR;
+    }
+
+    if (s->regs.ie1 & TXCIE) {
+        mask |= TXCINTR;
+    }
+
+    /* Update the irq state */
+    qemu_set_irq(s->irq, ((s->regs.isr1 & mask) > 0) ? 1 : 0);
+    DBG_L2("Set interrupt sr1 = 0x%08x isr1 = 0x%08x masked = 0x%08x\n",
+           s->regs.sr1, s->regs.isr1, s->regs.isr1 & mask);
+}
+
+static void pl041_request_data(void *opaque)
+{
+    pl041_state *s = (pl041_state *)opaque;
+
+    /* Trigger pending transfers */
+    pl041_fifo1_transmit(s);
+    pl041_isr1_update(s);
+}
+
+static uint32_t pl041_read(void *opaque, target_phys_addr_t offset)
+{
+    pl041_state *s = (pl041_state *)opaque;
+    int value;
+
+    if (offset >= 0xfe0 && offset < 0x1000) {
+        DBG_L1("pl041_read [0x%08x]\n", offset);
+        return pl041_id[(offset - 0xfe0) >> 2];
+    }
+
+    if (offset < 0x110) {
+        value = *((uint32_t *)&s->regs + (offset >> 2));
+    } else {
+        hw_error("pl041_read: Bad offset %x\n", (int)offset);
+    }
+
+    switch (offset) {
+    case PL041_allints:
+        value = s->regs.isr1 & 0x7F;
+        break;
+    }
+
+    DBG_L1("pl041_read [0x%08x] %s => 0x%08x\n", offset,
+           get_reg_name(offset), value);
+
+    return value;
+}
+
+static void pl041_write(void *opaque, target_phys_addr_t offset,
+                        uint32_t value)
+{
+    pl041_state *s = (pl041_state *)opaque;
+    uint16_t control, data;
+    uint32_t result;
+
+    DBG_L1("pl041_write [0x%08x] %s <= 0x%08x\n", offset,
+           get_reg_name(offset), value);
+
+    /* Write the register */
+    if (offset < 0x110) {
+        *((uint32_t *)&s->regs + (offset >> 2)) = value;
+    } else {
+        hw_error("pl041_write: Bad offset %x\n", (int)offset);
+    }
+
+    /* Execute the actions */
+    switch (offset) {
+#if defined(PL041_DEBUG_LEVEL) && (PL041_DEBUG_LEVEL >= 1)
+    case PL041_txcr1:
+    {
+        uint32_t txen = s->regs.txcr1 & TXEN;
+        uint32_t slots = (s->regs.txcr1 & TXSLOT_MASK) >> TXSLOT_MASK_BIT;
+        uint32_t tsize = (s->regs.txcr1 & TSIZE_MASK) >> TSIZE_MASK_BIT;
+        uint32_t compact_mode = (s->regs.txcr1 & TXCOMPACT) ? 1 : 0;
+        uint32_t txfen = (s->regs.txcr1 & TXFEN) > 0 ? 1 : 0;
+        DBG_L1("=> txen = %i slots = 0x%01x tsize = %i compact = %i"
+               "txfen = %i\n", txen, slots,  tsize, compact_mode, txfen);
+        break;
+    }
+#endif
+    case PL041_sl1tx:
+        s->regs.slfr &= ~SL1TXEMPTY;
+
+        control = (s->regs.sl1tx >> 12) & 0x7F;
+        data = (s->regs.sl2tx >> 4) & 0xFFFF;
+
+        if ((s->regs.sl1tx & SLOT1_RW) == 0) {
+            /* Write operation */
+            lm4549_write(&s->codec, control, data);
+        } else {
+            /* Read operation */
+            result = lm4549_read(&s->codec, control);
+
+            /* Store the returned value */
+            s->regs.sl1rx = s->regs.sl1tx & ~SLOT1_RW;
+            s->regs.sl2rx = result << 4;
+
+            s->regs.slfr &= ~SL1RXBUSY | ~SL2RXBUSY;
+            s->regs.slfr |= SL1RXVALID | SL2RXVALID;
+        }
+        break;
+
+    case PL041_sl2tx:
+        s->regs.sl2tx = value;
+        s->regs.slfr &= ~SL2TXEMPTY;
+        break;
+
+    case PL041_intclr:
+        DBG_L1("=> Clear interrupt intclr = 0x%08x isr1 = 0x%08x\n",
+               s->regs.intclr, s->regs.isr1);
+
+        if (s->regs.intclr & TXUEC1) {
+            s->regs.sr1 &= ~TXUNDERRUN;
+        }
+        break;
+
+#if defined(PL041_DEBUG_LEVEL)
+    case PL041_maincr:
+    {
+        char debug[] = " AACIFE  SL1RXEN  SL1TXEN";
+        if (!(value & AACIFE)) {
+            debug[0] = '!';
+        }
+        if (!(value & SL1RXEN)) {
+            debug[8] = '!';
+        }
+        if (!(value & SL1TXEN)) {
+            debug[17] = '!';
+        }
+        DBG_L1("%s\n", debug);
+        break;
+    }
+#endif
+
+    case PL041_dr1_0:
+    case PL041_dr1_1:
+    case PL041_dr1_2:
+    case PL041_dr1_3:
+        pl041_fifo1_push(s, value);
+        break;
+    }
+
+    /* Transmit the FIFO content */
+    pl041_fifo1_transmit(s);
+
+    /* Update the ISR1 register */
+    pl041_isr1_update(s);
+}
+
+static void pl041_reset(pl041_state *s)
+{
+    memset(&s->regs, 0x00, sizeof(pl041_regfile));
+
+    s->regs.slfr = SL1TXEMPTY | SL2TXEMPTY | SL12TXEMPTY;
+    s->regs.sr1 = TXFE | RXFE | TXHE;
+    s->regs.isr1 = 0;
+
+    s->fifo1.size = FIFO_DEPTH;
+    s->fifo1.half = FIFO_DEPTH >> 1;
+    s->fifo1.level = 0;
+    memset(s->fifo1.data, 0x00, sizeof(s->fifo1.data));
+}
+
+static CPUReadMemoryFunc * const pl041_readfn[] = {
+   pl041_read,
+   pl041_read,
+   pl041_read
+};
+
+static CPUWriteMemoryFunc * const pl041_writefn[] = {
+   pl041_write,
+   pl041_write,
+   pl041_write
+};
+
+static int pl041_init(SysBusDevice *dev)
+{
+    pl041_state *s = FROM_SYSBUS(pl041_state, dev);
+    int iomemtype;
+
+    DBG_L1("pl041_init\n");
+
+    /* Connect the device to the sysbus */
+    iomemtype = cpu_register_io_memory(pl041_readfn,
+                                       pl041_writefn,
+                                       s,
+                                       DEVICE_NATIVE_ENDIAN);
+    sysbus_init_mmio(dev, 0x1000, iomemtype);
+    sysbus_init_irq(dev, &s->irq);
+
+    /* Reset the device */
+    pl041_reset(s);
+
+    /* Init the codec */
+    lm4549_init(&s->codec, &pl041_request_data, (void *)s);
+
+    return 0;
+}
+
+static void pl041_register_devices(void)
+{
+    sysbus_register_dev("pl041", sizeof(pl041_state), pl041_init);
+}
+
+device_init(pl041_register_devices)
diff --git a/hw/pl041.h b/hw/pl041.h
new file mode 100644
index 0000000..ddf452d
--- /dev/null
+++ b/hw/pl041.h
@@ -0,0 +1,125 @@ 
+/*
+ * Arm PrimeCell PL041 Advanced Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This driver emulates the ARM AACI interface
+ * connected to a LM4549 codec.
+ *
+ */
+
+/* Register file */
+#define REGISTER(name, offset) uint32_t name;
+typedef struct {
+    #include "pl041.hx"
+} pl041_regfile;
+#undef REGISTER
+
+/* Register addresses */
+#define REGISTER(name, offset) PL041_##name = offset,
+enum {
+    #include "pl041.hx"
+};
+#undef REGISTER
+
+/* Register bits */
+
+/* IEx */
+#define TXCIE           (1 << 0)
+#define RXTIE           (1 << 1)
+#define TXIE            (1 << 2)
+#define RXIE            (1 << 3)
+#define RXOIE           (1 << 4)
+#define TXUIE           (1 << 5)
+#define RXTOIE          (1 << 6)
+
+/* TXCRx */
+#define TXEN            (1 << 0)
+#define TXSLOT1         (1 << 1)
+#define TXSLOT2         (1 << 2)
+#define TXSLOT3         (1 << 3)
+#define TXSLOT4         (1 << 4)
+#define TXCOMPACT       (1 << 15)
+#define TXFEN           (1 << 16)
+
+#define TXSLOT_MASK_BIT (1)
+#define TXSLOT_MASK     (0xFFF << TXSLOT_MASK_BIT)
+
+#define TSIZE_MASK_BIT  (13)
+#define TSIZE_MASK      (0x3 << TSIZE_MASK_BIT)
+
+#define TSIZE_16BITS    (0x0 << TSIZE_MASK_BIT)
+#define TSIZE_18BITS    (0x1 << TSIZE_MASK_BIT)
+#define TSIZE_20BITS    (0x2 << TSIZE_MASK_BIT)
+#define TSIZE_12BITS    (0x3 << TSIZE_MASK_BIT)
+
+/* SRx */
+#define RXFE         (1 << 0)
+#define TXFE         (1 << 1)
+#define RXHF         (1 << 2)
+#define TXHE         (1 << 3)
+#define RXFF         (1 << 4)
+#define TXFF         (1 << 5)
+#define RXBUSY       (1 << 6)
+#define TXBUSY       (1 << 7)
+#define RXOVERRUN    (1 << 8)
+#define TXUNDERRUN   (1 << 9)
+#define RXTIMEOUT    (1 << 10)
+#define RXTOFE       (1 << 11)
+
+/* ISRx */
+#define TXCINTR      (1 << 0)
+#define RXTOINTR     (1 << 1)
+#define TXINTR       (1 << 2)
+#define RXINTR       (1 << 3)
+#define ORINTR       (1 << 4)
+#define URINTR       (1 << 5)
+#define RXTOFEINTR   (1 << 6)
+
+/* SLFR */
+#define SL1RXBUSY    (1 << 0)
+#define SL1TXBUSY    (1 << 1)
+#define SL2RXBUSY    (1 << 2)
+#define SL2TXBUSY    (1 << 3)
+#define SL12RXBUSY   (1 << 4)
+#define SL12TXBUSY   (1 << 5)
+#define SL1RXVALID   (1 << 6)
+#define SL1TXEMPTY   (1 << 7)
+#define SL2RXVALID   (1 << 8)
+#define SL2TXEMPTY   (1 << 9)
+#define SL12RXVALID  (1 << 10)
+#define SL12TXEMPTY  (1 << 11)
+#define RAWGPIOINT   (1 << 12)
+#define RWIS         (1 << 13)
+
+/* MAINCR */
+#define AACIFE       (1 << 0)
+#define LOOPBACK     (1 << 1)
+#define LOWPOWER     (1 << 2)
+#define SL1RXEN      (1 << 3)
+#define SL1TXEN      (1 << 4)
+#define SL2RXEN      (1 << 5)
+#define SL2TXEN      (1 << 6)
+#define SL12RXEN     (1 << 7)
+#define SL12TXEN     (1 << 8)
+#define DMAENABLE    (1 << 9)
+
+/* INTCLR */
+#define WISC         (1 << 0)
+#define RXOEC1       (1 << 1)
+#define RXOEC2       (1 << 2)
+#define RXOEC3       (1 << 3)
+#define RXOEC4       (1 << 4)
+#define TXUEC1       (1 << 5)
+#define TXUEC2       (1 << 6)
+#define TXUEC3       (1 << 7)
+#define TXUEC4       (1 << 8)
+#define RXTOFEC1     (1 << 9)
+#define RXTOFEC2     (1 << 10)
+#define RXTOFEC3     (1 << 11)
+#define RXTOFEC4     (1 << 12)
diff --git a/hw/pl041.hx b/hw/pl041.hx
new file mode 100644
index 0000000..95e7c4e
--- /dev/null
+++ b/hw/pl041.hx
@@ -0,0 +1,61 @@ 
+/*
+ * Arm PrimeCell PL041 Advanced Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This driver emulates the ARM AACI interface
+ * connected to a LM4549 codec.
+ *
+ */
+
+/* PL041 register file description */
+
+REGISTER( rxcr1,   0x00 )
+REGISTER( txcr1,   0x04 )
+REGISTER( sr1,     0x08 )
+REGISTER( isr1,    0x0C )
+REGISTER( ie1,     0x10 )
+REGISTER( rxcr2,   0x14 )
+REGISTER( txcr2,   0x18 )
+REGISTER( sr2,     0x1C )
+REGISTER( isr2,    0x20 )
+REGISTER( ie2,     0x24 )
+REGISTER( rxcr3,   0x28 )
+REGISTER( txcr3,   0x2C )
+REGISTER( sr3,     0x30 )
+REGISTER( isr3,    0x34 )
+REGISTER( ie3,     0x38 )
+REGISTER( rxcr4,   0x3C )
+REGISTER( txcr4,   0x40 )
+REGISTER( sr4,     0x44 )
+REGISTER( isr4,    0x48 )
+REGISTER( ie4,     0x4C )
+REGISTER( sl1rx,   0x50 )
+REGISTER( sl1tx,   0x54 )
+REGISTER( sl2rx,   0x58 )
+REGISTER( sl2tx,   0x5C )
+REGISTER( sl12rx,  0x60 )
+REGISTER( sl12tx,  0x64 )
+REGISTER( slfr,    0x68 )
+REGISTER( slistat, 0x6C )
+REGISTER( slien,   0x70 )
+REGISTER( intclr,  0x74 )
+REGISTER( maincr,  0x78 )
+REGISTER( reset,   0x7C )
+REGISTER( sync,    0x80 )
+REGISTER( allints, 0x84 )
+REGISTER( mainfr,  0x88 )
+REGISTER( unused,  0x8C )
+REGISTER( dr1_0,   0x90 )
+REGISTER( dr1_1,   0x94 )
+REGISTER( dr1_2,   0x98 )
+REGISTER( dr1_3,   0x9C )
+REGISTER( dr1_4,   0xA0 )
+REGISTER( dr1_5,   0xA4 )
+REGISTER( dr1_6,   0xA8 )
+REGISTER( dr1_7,   0xAC )
diff --git a/hw/versatilepb.c b/hw/versatilepb.c
index 46b6a3f..f3e2222 100644
--- a/hw/versatilepb.c
+++ b/hw/versatilepb.c
@@ -258,6 +258,9 @@  static void versatile_init(ram_addr_t ram_size,
     /* Add PL031 Real Time Clock. */
     sysbus_create_simple("pl031", 0x101e8000, pic[10]);

+    /* Add PL041 AACI Interface to the LM4549 codec */
+    dev = sysbus_create_varargs("pl041", 0x10004000, sic[24]);
+
     /* Memory map for Versatile/PB:  */
     /* 0x10000000 System registers.  */
     /* 0x10001000 PCI controller config registers.  */