diff mbox

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

Message ID 4E84AB63.3090709@elasticsheep.com
State New
Headers show

Commit Message

contact@elasticsheep.com Sept. 29, 2011, 5:31 p.m. UTC
This driver emulates the ARM AACI interface (PL041) connected to a 
LM4549 codec.
It enables audio playback for the Versatile/PB platform.

Limitations:
- Supports only a playback on one channel (Versatile/Vexpress)
- Supports only a TX FIFO in compact-mode.
- Record is not supported.
- The PL041 is hardwired to a LM4549 codec.

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 speaker-test/aplay/mpg123.

Signed-off-by: Mathieu Sonet <contact@elasticsheep.com>
---
v2->v3

This version takes into account the remarks from Peter Maydell's review:
* Both pl041 and lm4549 source code has been simplified by removing 
extraneous abstractions.
* hw_error occurences has been replaced by warnings.
* The FIFO depth is now a qdev property.
* The model now has load/save support.

  Makefile.target  |    1 +
  hw/lm4549.c      |  326 +++++++++++++++++++++++++++++++++++
  hw/lm4549.h      |   44 +++++
  hw/pl041.c       |  501 
++++++++++++++++++++++++++++++++++++++++++++++++++++++
  hw/pl041.h       |  135 +++++++++++++++
  hw/pl041.hx      |   81 +++++++++
  hw/versatilepb.c |    8 +
  7 files changed, 1096 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

      /* 0x10001000 PCI controller config registers.  */
--
1.7.0.4

Comments

malc Sept. 29, 2011, 6:13 p.m. UTC | #1
On Thu, 29 Sep 2011, Mathieu Sonet wrote:

> This driver emulates the ARM AACI interface (PL041) connected to a LM4549
> codec.
> It enables audio playback for the Versatile/PB platform.
> 
> Limitations:
> - Supports only a playback on one channel (Versatile/Vexpress)
> - Supports only a TX FIFO in compact-mode.
> - Record is not supported.
> - The PL041 is hardwired to a LM4549 codec.
> 
> 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 speaker-test/aplay/mpg123.

No objections from me.

[..snip..]
Peter Maydell Oct. 7, 2011, 4:31 p.m. UTC | #2
On 29 September 2011 18:31, Mathieu Sonet <contact@elasticsheep.com> wrote:
> This driver emulates the ARM AACI interface (PL041) connected to a LM4549
> codec.
> It enables audio playback for the Versatile/PB platform.
>
> Limitations:
> - Supports only a playback on one channel (Versatile/Vexpress)
> - Supports only a TX FIFO in compact-mode.

Actually you seem to have implemented a weird hybrid of compact
and non-compact modes.

Non-compact mode: FIFOs are 20 bits wide; a word write from the
CPU writes to bits [19:0] of the FIFO slot; each 'slot' of data
to the LM4549 reads 20 bits from the FIFO.
Compact mode: FIFOs are 40 bits wide (and half as deep); a word
write from the CPU writes to bits [31:0] of the FIFO slot; each
'slot' of data to the LM4549 reads 16 bits from the FIFO (bits
[15:0] then [31:16], and you have to read two slots (ie the
2 channels of stereo).

You've implemented a single 32 bit depth FIFO which one CPU
write writes to and which always passes one 32 bit word to the
LM4549. There are two issues here:
 (1) the LM4549 can take up to 18 bits of data per slot,
which your current lm4549_write_sample() API doesn't allow.
Passing two 32 bit words here would fix that. [My point here
is that we shouldn't hardwire the missing non-compact mode
support into the API between the two components.]
 (2) when the Linux driver dynamically identifies the size
of the FIFO it does it in non-compact mode, so it will return
a value half as big as is actually right for the compact-mode
behaviour you've implemented.

(Also your FIFO is twice as deep as it should be if we're
only implementing compact mode.)

> Playback tested successfully with speaker-test/aplay/mpg123.

I find that on vexpress mpg123 playback works but can be quite stuttery.
madplay (integer only) is somewhat better, so I suspect that qemu is
spending ages emulating Neon/VFP in mpg123...

Further (minor) comments below.

> +    regfile[LM4549_PCM_Front_DAC_Rate]  = 0xBB80;
> +    regfile[LM4549_PCM_ADC_Rate]        = 0xBB80;
> +    regfile[LM4549_Vendor_ID1]          = 0x4e53;
> +    regfile[LM4549_Vendor_ID2]          = 0x4331;

Can we be consistent about upper or lower case for the hex?

> +#define MAX_FIFO_DEPTH                 (1024)
> +#define VERSATILEPB_DEFAULT_FIFO_DEPTH (256)  /* AN115B - Table 1.1 */

pl041.c shouldn't know anything about VersatilePB. The default
fifo depth should be 8 (same as the hardware PL041).

> +static uint8_t pl041_compute_periphid3(pl041_state *s)
> +{
> +    uint8_t id3 = (1 | ((s->fifo_depth >> 4) << 3));
> +    return id3;
> +}

This isn't right.

[5:3] FIFO depth (non-compact mode)
b000  8
b001  16
b010  32
b011  64
b100  128
b101  256
b110  512
b111  1024

...which isn't what your function calculates.
(Linux determines FIFO depth programmatically by stuffing words
into the FIFO until the status register says it's full, which is
why it doesn't complain.)

NB that some of the board TRMs have what seem to be incorrect
labelling on the tables of depth vs ID register bits.

> +    /* Update the irq state */
> +    qemu_set_irq(s->irq, ((s->regs.isr1 & s->regs.ie1) > 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);
> +}

This debug printf won't compile if enabled -- you forgot
to update it when you changed the main code to remove the
'mask' variable.

> +static int pl041_post_load(void *opaque, int version_id)
> +{
> +    pl041_state *s = opaque;
> +    lm4549_post_load(&s->codec);
> +    return 0;
> +}

Is it not possible to just register lm4549_post_load()
as the post_load function for lm4549_state, rather than
having a pl041 post_load hook which only passes it through?

(Something in your mailsending path is wrapping long lines, by the way.)

-- PMM
contact@elasticsheep.com Oct. 9, 2011, 7:25 p.m. UTC | #3
Peter Maydell wrote:
> On 29 September 2011 18:31, Mathieu Sonet <contact@elasticsheep.com> wrote:
>> This driver emulates the ARM AACI interface (PL041) connected to a LM4549
>> codec.
>> It enables audio playback for the Versatile/PB platform.
>>
>> Limitations:
>> - Supports only a playback on one channel (Versatile/Vexpress)
>> - Supports only a TX FIFO in compact-mode.
> 
> Actually you seem to have implemented a weird hybrid of compact
> and non-compact modes.
> 
> Non-compact mode: FIFOs are 20 bits wide; a word write from the
> CPU writes to bits [19:0] of the FIFO slot; each 'slot' of data
> to the LM4549 reads 20 bits from the FIFO.
> Compact mode: FIFOs are 40 bits wide (and half as deep); a word
> write from the CPU writes to bits [31:0] of the FIFO slot; each
> 'slot' of data to the LM4549 reads 16 bits from the FIFO (bits
> [15:0] then [31:16], and you have to read two slots (ie the
> 2 channels of stereo).
> 
> You've implemented a single 32 bit depth FIFO which one CPU
> write writes to and which always passes one 32 bit word to the
> LM4549. There are two issues here:
>  (1) the LM4549 can take up to 18 bits of data per slot,
> which your current lm4549_write_sample() API doesn't allow.
> Passing two 32 bit words here would fix that. [My point here
> is that we shouldn't hardwire the missing non-compact mode
> support into the API between the two components.]
>  (2) when the Linux driver dynamically identifies the size
> of the FIFO it does it in non-compact mode, so it will return
> a value half as big as is actually right for the compact-mode
> behaviour you've implemented.
> 
> (Also your FIFO is twice as deep as it should be if we're
> only implementing compact mode.)
> 
>> Playback tested successfully with speaker-test/aplay/mpg123.
> 
> I find that on vexpress mpg123 playback works but can be quite stuttery.
> madplay (integer only) is somewhat better, so I suspect that qemu is
> spending ages emulating Neon/VFP in mpg123...
> 
> Further (minor) comments below.
> 
>> +    regfile[LM4549_PCM_Front_DAC_Rate]  = 0xBB80;
>> +    regfile[LM4549_PCM_ADC_Rate]        = 0xBB80;
>> +    regfile[LM4549_Vendor_ID1]          = 0x4e53;
>> +    regfile[LM4549_Vendor_ID2]          = 0x4331;
> 
> Can we be consistent about upper or lower case for the hex?
> 
>> +#define MAX_FIFO_DEPTH                 (1024)
>> +#define VERSATILEPB_DEFAULT_FIFO_DEPTH (256)  /* AN115B - Table 1.1 */
> 
> pl041.c shouldn't know anything about VersatilePB. The default
> fifo depth should be 8 (same as the hardware PL041).
> 
>> +static uint8_t pl041_compute_periphid3(pl041_state *s)
>> +{
>> +    uint8_t id3 = (1 | ((s->fifo_depth >> 4) << 3));
>> +    return id3;
>> +}
> 
> This isn't right.
> 
> [5:3] FIFO depth (non-compact mode)
> b000  8
> b001  16
> b010  32
> b011  64
> b100  128
> b101  256
> b110  512
> b111  1024
> 
> ...which isn't what your function calculates.
> (Linux determines FIFO depth programmatically by stuffing words
> into the FIFO until the status register says it's full, which is
> why it doesn't complain.)
> 
> NB that some of the board TRMs have what seem to be incorrect
> labelling on the tables of depth vs ID register bits.
> 
>> +    /* Update the irq state */
>> +    qemu_set_irq(s->irq, ((s->regs.isr1 & s->regs.ie1) > 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);
>> +}
> 
> This debug printf won't compile if enabled -- you forgot
> to update it when you changed the main code to remove the
> 'mask' variable.
> 
>> +static int pl041_post_load(void *opaque, int version_id)
>> +{
>> +    pl041_state *s = opaque;
>> +    lm4549_post_load(&s->codec);
>> +    return 0;
>> +}
> 
> Is it not possible to just register lm4549_post_load()
> as the post_load function for lm4549_state, rather than
> having a pl041 post_load hook which only passes it through?

The lm4549 model has no knowledge of its pl041 client and uses a separate context structure.
The opaque pointer passed to the post_load hook can not be used directly to setup the lm4549.

I would prefer to keep the two models separated.

> 
> (Something in your mailsending path is wrapping long lines, by the way.)
> 
> -- PMM

  Mathieu
diff mbox

Patch

diff --git a/Makefile.target b/Makefile.target
index 88d2f1f..cb1e86a 100644
--- a/Makefile.target
+++ b/Makefile.target
@@ -355,6 +355,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..55b7105
--- /dev/null
+++ b/hw/lm4549.c
@@ -0,0 +1,326 @@ 
+/*
+ * 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.
+ *
+ * It supports only one playback voice and no record voice.
+ */
+
+#include "hw.h"
+#include "audio/audio.h"
+#include "lm4549.h"
+
+#if 0
+#define LM4549_DEBUG  1
+#endif
+
+#if 0
+#define LM4549_DUMP_DAC_INPUT 1
+#endif
+
+#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_Ext_Audio_ID             = 0x28,
+    LM4549_Ext_Audio_Stat_Ctrl      = 0x2A,
+    LM4549_PCM_Front_DAC_Rate       = 0x2C,
+    LM4549_PCM_ADC_Rate             = 0x32,
+    LM4549_Vendor_ID1               = 0x7C,
+    LM4549_Vendor_ID2               = 0x7E
+};
+
+static void lm4549_reset(lm4549_state *s)
+{
+    uint16_t *regfile = s->regfile;
+
+    regfile[LM4549_Reset]               = 0x0d50;
+    regfile[LM4549_Master_Volume]       = 0x8008;
+    regfile[LM4549_Line_Out_Volume]     = 0x8000;
+    regfile[LM4549_Master_Volume_Mono]  = 0x8000;
+    regfile[LM4549_PC_Beep_Volume]      = 0x0000;
+    regfile[LM4549_Phone_Volume]        = 0x8008;
+    regfile[LM4549_Mic_Volume]          = 0x8008;
+    regfile[LM4549_Line_In_Volume]      = 0x8808;
+    regfile[LM4549_CD_Volume]           = 0x8808;
+    regfile[LM4549_Video_Volume]        = 0x8808;
+    regfile[LM4549_Aux_Volume]          = 0x8808;
+    regfile[LM4549_PCM_Out_Volume]      = 0x8808;
+    regfile[LM4549_Record_Select]       = 0x0000;
+    regfile[LM4549_Record_Gain]         = 0x8000;
+    regfile[LM4549_General_Purpose]     = 0x0000;
+    regfile[LM4549_3D_Control]          = 0x0101;
+    regfile[LM4549_Powerdown_Ctrl_Stat] = 0x000f;
+    regfile[LM4549_Ext_Audio_ID]        = 0x0001;
+    regfile[LM4549_Ext_Audio_Stat_Ctrl] = 0x0000;
+    regfile[LM4549_PCM_Front_DAC_Rate]  = 0xBB80;
+    regfile[LM4549_PCM_ADC_Rate]        = 0xBB80;
+    regfile[LM4549_Vendor_ID1]          = 0x4e53;
+    regfile[LM4549_Vendor_ID2]          = 0x4331;
+}
+
+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);
+    s->voice_is_active = 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
+
+    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);
+        s->voice_is_active = 0;
+    }
+    prev_buffer_level = s->buffer_level;
+
+    /* Check if a buffer transfer is pending */
+    if (s->buffer_level == LM4549_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)
+{
+    uint16_t *regfile = s->regfile;
+    uint32_t value = 0;
+
+    /* Read the stored value */
+    assert(offset < 128);
+    value = regfile[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)
+{
+    uint16_t *regfile = s->regfile;
+
+    assert(offset < 128);
+    DPRINTF("write [0x%02x] = 0x%04x\n", offset, value);
+
+    switch (offset) {
+    case LM4549_Reset:
+        lm4549_reset(s);
+        break;
+
+    case LM4549_PCM_Front_DAC_Rate:
+        regfile[LM4549_PCM_Front_DAC_Rate] = value;
+        DPRINTF("DAC rate change = %i\n", value);
+
+        /* Re-open a voice with the new sample rate */
+        struct audsettings as;
+        as.freq = value;
+        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 |= regfile[LM4549_Powerdown_Ctrl_Stat] & 0xf;
+        regfile[LM4549_Powerdown_Ctrl_Stat] = value;
+        break;
+
+    case LM4549_Ext_Audio_ID:
+    case LM4549_Vendor_ID1:
+    case LM4549_Vendor_ID2:
+        DPRINTF("Write to read-only register 0x%x\n", (int)offset);
+        break;
+
+    default:
+        /* Store the new value */
+        regfile[offset] = value;
+        break;
+    }
+}
+
+uint32_t lm4549_write_sample(lm4549_state *s, uint32_t sample)
+{
+    if (s->buffer_level >= LM4549_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 == LM4549_BUFFER_SIZE) {
+        /* Trigger the transfer of the buffer to the audio host */
+        lm4549_audio_transfer(s);
+    }
+
+    return 1;
+}
+
+void lm4549_post_load(lm4549_state *s)
+{
+    uint16_t *regfile = s->regfile;
+
+    /* Re-open a voice with the current sample rate */
+    uint32_t freq = regfile[LM4549_PCM_Front_DAC_Rate];
+
+    printf("lm4549_post_load freq = %i\n", freq);
+    printf("lm4549_post_load voice_is_active = %i\n", s->voice_is_active);
+
+    struct audsettings as;
+    as.freq = freq;
+    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
+    );
+
+    /* Request data */
+    if (s->voice_is_active == 1) {
+        lm4549_audio_out_callback(s, AUD_get_buffer_size_out(s->voice));
+    }
+}
+
+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 registers */
+    lm4549_reset(s);
+
+    /* 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);
+
+    s->voice_is_active = 0;
+
+    /* 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
+}
+
+const VMStateDescription vmstate_lm4549_state = {
+    .name = "lm4549_state",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .minimum_version_id_old = 1,
+    .fields      = (VMStateField[]) {
+        VMSTATE_UINT32(voice_is_active, lm4549_state),
+        VMSTATE_UINT16_ARRAY(regfile, lm4549_state, 128),
+        VMSTATE_UINT32_ARRAY(buffer, lm4549_state, LM4549_BUFFER_SIZE),
+        VMSTATE_UINT32(buffer_level, lm4549_state),
+        VMSTATE_END_OF_LIST()
+    }
+};
diff --git a/hw/lm4549.h b/hw/lm4549.h
new file mode 100644
index 0000000..c8caa41
--- /dev/null
+++ b/hw/lm4549.h
@@ -0,0 +1,44 @@ 
+/*
+ * LM4549 Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ */
+
+#ifndef HW_LM4549_H
+#define HW_LM4549_H
+
+#include "audio/audio.h"
+
+typedef void (*lm4549_callback)(void *opaque);
+
+#define LM4549_BUFFER_SIZE (512)
+
+
+typedef struct {
+    QEMUSoundCard card;
+    SWVoiceOut *voice;
+    uint32_t voice_is_active;
+
+    uint16_t regfile[128];
+    lm4549_callback data_req_cb;
+    void *opaque;
+
+    uint32_t buffer[LM4549_BUFFER_SIZE];
+    uint32_t buffer_level;
+} lm4549_state;
+
+extern const VMStateDescription vmstate_lm4549_state;
+
+
+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);
+void lm4549_post_load(lm4549_state *s);
+
+#endif /* #ifndef HW_LM4549_H */
diff --git a/hw/pl041.c b/hw/pl041.c
new file mode 100644
index 0000000..bf3a28d
--- /dev/null
+++ b/hw/pl041.c
@@ -0,0 +1,501 @@ 
+/*
+ * 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.
+ *
+ * Limitations:
+ * - Supports only a playback on one channel (Versatile/Vexpress)
+ * - Supports only a TX FIFO in compact-mode.
+ * - Record is not supported.
+ * - The PL041 is hardwired to a LM4549 codec.
+ *
+ */
+
+#include "sysbus.h"
+
+#include "pl041.h"
+#include "lm4549.h"
+
+#if 0
+#define PL041_DEBUG_LEVEL 1
+#endif
+
+#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
+
+
+#define MAX_FIFO_DEPTH                 (1024)
+#define VERSATILEPB_DEFAULT_FIFO_DEPTH (256)  /* AN115B - Table 1.1 */
+
+#define SLOT1_RW    (1 << 19)
+
+
+typedef struct {
+    uint32_t level;
+    uint32_t data[MAX_FIFO_DEPTH];
+} pl041_fifo;
+
+typedef struct {
+    pl041_fifo tx_fifo;
+    pl041_fifo rx_fifo;
+} pl041_channel;
+
+typedef struct {
+    SysBusDevice busdev;
+    MemoryRegion iomem;
+    qemu_irq irq;
+
+    uint32_t fifo_depth;
+
+    pl041_regfile regs;
+    pl041_channel fifo1;
+    lm4549_state codec;
+} pl041_state;
+
+
+static const unsigned char pl041_default_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
+
+
+#if defined(PL041_DEBUG_LEVEL)
+static const char *get_reg_name(target_phys_addr_t offset)
+{
+    if (offset <= PL041_dr1_7) {
+        return pl041_regs_name[offset >> 2];
+    }
+
+    return "unknown";
+}
+#endif
+
+static uint8_t pl041_compute_periphid3(pl041_state *s)
+{
+    uint8_t id3 = (1 | ((s->fifo_depth >> 4) << 3));
+    return id3;
+}
+
+static void pl041_fifo1_write(pl041_state *s, uint32_t value)
+{
+    pl041_fifo *f = &s->fifo1.tx_fifo;
+
+#if defined(PL041_DEBUG_LEVEL)
+    /* Check the FIFO level */
+    if (f->level >= s->fifo_depth) {
+        DBG_L1("fifo1 push: overrun\n");
+    }
+#endif
+
+    /* Push the value in the FIFO */
+    if (f->level < s->fifo_depth) {
+        f->data[f->level++] = value;
+    }
+
+    /* Update the status register */
+    if (f->level > 0) {
+        s->regs.sr1 &= ~(TXUNDERRUN | TXFE);
+    }
+
+    if (f->level >= (s->fifo_depth >> 1)) {
+        s->regs.sr1 &= ~TXHE;
+    }
+
+    if (f->level >= s->fifo_depth) {
+        s->regs.sr1 |= TXFF;
+    }
+
+    DBG_L2("fifo1_push sr1 = 0x%08x\n", s->regs.sr1);
+}
+
+static void pl041_fifo1_transmit(pl041_state *s)
+{
+    pl041_fifo *f = &s->fifo1.tx_fifo;
+    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 >= (s->fifo_depth >> 1)) {
+            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 <= (s->fifo_depth >> 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)
+{
+    /* 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;
+    }
+
+    /* Update the irq state */
+    qemu_set_irq(s->irq, ((s->regs.isr1 & s->regs.ie1) > 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 uint64_t pl041_read(void *opaque, target_phys_addr_t offset,
+                                unsigned size)
+{
+    pl041_state *s = (pl041_state *)opaque;
+    int value;
+
+    if ((offset >= PL041_periphid0) && (offset <= PL041_pcellid3)) {
+        if (offset == PL041_periphid3) {
+            value = pl041_compute_periphid3(s);
+        } else {
+            value = pl041_default_id[(offset - PL041_periphid0) >> 2];
+        }
+
+        DBG_L1("pl041_read [0x%08x] => 0x%08x\n", offset, value);
+        return value;
+    } else if (offset <= PL041_dr4_7) {
+        value = *((uint32_t *)&s->regs + (offset >> 2));
+    } else {
+        DBG_L1("pl041_read: Reserved offset %x\n", (int)offset);
+        return 0;
+    }
+
+    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,
+                             uint64_t value, unsigned size)
+{
+    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), (unsigned int)value);
+
+    /* Write the register */
+    if (offset <= PL041_dr4_7) {
+        *((uint32_t *)&s->regs + (offset >> 2)) = value;
+    } else {
+        DBG_L1("pl041_write: Reserved offset %x\n", (int)offset);
+        return;
+    }
+
+    /* 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);
+
+        if (compact_mode == 0) {
+            DBG_L1("Non compact mode is not implemented");
+        }
+        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_write(s, value);
+        break;
+    }
+
+    /* Transmit the FIFO content */
+    pl041_fifo1_transmit(s);
+
+    /* Update the ISR1 register */
+    pl041_isr1_update(s);
+}
+
+static void pl041_reset(DeviceState *d)
+{
+    pl041_state *s = DO_UPCAST(pl041_state, busdev.qdev, d);
+
+    DBG_L1("pl041_reset\n");
+
+    memset(&s->regs, 0x00, sizeof(pl041_regfile));
+
+    s->regs.slfr = SL1TXEMPTY | SL2TXEMPTY | SL12TXEMPTY;
+    s->regs.sr1 = TXFE | RXFE | TXHE;
+    s->regs.isr1 = 0;
+
+    memset(&s->fifo1, 0x00, sizeof(s->fifo1));
+}
+
+static const MemoryRegionOps pl041_ops = {
+    .read = pl041_read,
+    .write = pl041_write,
+    .endianness = DEVICE_NATIVE_ENDIAN,
+};
+
+static int pl041_init(SysBusDevice *dev)
+{
+    pl041_state *s = FROM_SYSBUS(pl041_state, dev);
+
+    DBG_L1("pl041_init 0x%08x\n", (uint32_t)s);
+
+    /* Check the device properties */
+    switch (s->fifo_depth) {
+    case 8:
+    case 16:
+    case 32:
+    case 64:
+    case 128:
+    case 256:
+    case 512:
+    case 1024:
+        break;
+    default:
+        fprintf(stderr, "pl041: invalid fifo depth\n");
+        return -1;
+    }
+
+    /* Connect the device to the sysbus */
+    memory_region_init_io(&s->iomem, &pl041_ops, s, "pl041", 0x1000);
+    sysbus_init_mmio_region(dev, &s->iomem);
+    sysbus_init_irq(dev, &s->irq);
+
+    /* Init the codec */
+    lm4549_init(&s->codec, &pl041_request_data, (void *)s);
+
+    return 0;
+}
+
+static int pl041_post_load(void *opaque, int version_id)
+{
+    pl041_state *s = opaque;
+    lm4549_post_load(&s->codec);
+    return 0;
+}
+
+static const VMStateDescription vmstate_pl041_regfile = {
+    .name = "pl041_regfile",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .minimum_version_id_old = 1,
+    .fields      = (VMStateField[]) {
+#define REGISTER(name, offset) VMSTATE_UINT32(name, pl041_regfile),
+        #include "pl041.hx"
+#undef REGISTER
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static const VMStateDescription vmstate_pl041_fifo = {
+    .name = "pl041_fifo",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .minimum_version_id_old = 1,
+    .fields      = (VMStateField[]) {
+        VMSTATE_UINT32(level, pl041_fifo),
+        VMSTATE_UINT32_ARRAY(data, pl041_fifo, MAX_FIFO_DEPTH),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static const VMStateDescription vmstate_pl041_channel = {
+    .name = "pl041_fifo",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .minimum_version_id_old = 1,
+    .fields      = (VMStateField[]) {
+        VMSTATE_STRUCT(tx_fifo, pl041_channel, 0,
+                       vmstate_pl041_fifo, pl041_fifo),
+        VMSTATE_STRUCT(rx_fifo, pl041_channel, 0,
+                       vmstate_pl041_fifo, pl041_fifo),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static const VMStateDescription vmstate_pl041 = {
+    .name = "pl041",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .post_load = pl041_post_load,
+    .fields = (VMStateField[]) {
+        VMSTATE_UINT32(fifo_depth, pl041_state),
+        VMSTATE_STRUCT(regs, pl041_state, 0,
+                       vmstate_pl041_regfile, pl041_regfile),
+        VMSTATE_STRUCT(fifo1, pl041_state, 0,
+                       vmstate_pl041_channel, pl041_channel),
+        VMSTATE_STRUCT(codec, pl041_state, 0,
+                       vmstate_lm4549_state, lm4549_state),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static SysBusDeviceInfo pl041_device_info = {
+    .init = pl041_init,
+    .qdev.name = "pl041",
+    .qdev.size = sizeof(pl041_state),
+    .qdev.vmsd = &vmstate_pl041,
+    .qdev.reset = pl041_reset,
+    .qdev.no_user = 1,
+    .qdev.props = (Property[]) {
+        DEFINE_PROP_UINT32("fifo_depth", pl041_state,
+                           fifo_depth, VERSATILEPB_DEFAULT_FIFO_DEPTH),
+        DEFINE_PROP_END_OF_LIST(),
+    },
+};
+
+static void pl041_register_device(void)
+{
+    sysbus_register_withprop(&pl041_device_info);
+}
+
+device_init(pl041_register_device)
diff --git a/hw/pl041.h b/hw/pl041.h
new file mode 100644
index 0000000..1f22432
--- /dev/null
+++ b/hw/pl041.h
@@ -0,0 +1,135 @@ 
+/*
+ * Arm PrimeCell PL041 Advanced Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ */
+
+#ifndef HW_PL041_H
+#define HW_PL041_H
+
+/* 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"
+
+    PL041_periphid0 = 0xFE0,
+    PL041_periphid1 = 0xFE4,
+    PL041_periphid2 = 0xFE8,
+    PL041_periphid3 = 0xFEC,
+    PL041_pcellid0  = 0xFF0,
+    PL041_pcellid1  = 0xFF4,
+    PL041_pcellid2  = 0xFF8,
+    PL041_pcellid3  = 0xFFC,
+};
+#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)
+
+#endif /* #ifndef HW_PL041_H */
diff --git a/hw/pl041.hx b/hw/pl041.hx
new file mode 100644
index 0000000..e972996
--- /dev/null
+++ b/hw/pl041.hx
@@ -0,0 +1,81 @@ 
+/*
+ * Arm PrimeCell PL041 Advanced Audio Codec Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ */
+
+/* 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 )
+REGISTER( dr2_0,   0xB0 )
+REGISTER( dr2_1,   0xB4 )
+REGISTER( dr2_2,   0xB8 )
+REGISTER( dr2_3,   0xBC )
+REGISTER( dr2_4,   0xC0 )
+REGISTER( dr2_5,   0xC4 )
+REGISTER( dr2_6,   0xC8 )
+REGISTER( dr2_7,   0xCC )
+REGISTER( dr3_0,   0xD0 )
+REGISTER( dr3_1,   0xD4 )
+REGISTER( dr3_2,   0xD8 )
+REGISTER( dr3_3,   0xDC )
+REGISTER( dr3_4,   0xE0 )
+REGISTER( dr3_5,   0xE4 )
+REGISTER( dr3_6,   0xE8 )
+REGISTER( dr3_7,   0xEC )
+REGISTER( dr4_0,   0xF0 )
+REGISTER( dr4_1,   0xF4 )
+REGISTER( dr4_2,   0xF8 )
+REGISTER( dr4_3,   0xFC )
+REGISTER( dr4_4,   0x100 )
+REGISTER( dr4_5,   0x104 )
+REGISTER( dr4_6,   0x108 )
+REGISTER( dr4_7,   0x10C )
diff --git a/hw/versatilepb.c b/hw/versatilepb.c
index 49f8f5f..c1ca13c 100644
--- a/hw/versatilepb.c
+++ b/hw/versatilepb.c
@@ -181,6 +181,7 @@  static void versatile_init(ram_addr_t ram_size,
      qemu_irq pic[32];
      qemu_irq sic[32];
      DeviceState *dev, *sysctl;
+    DeviceState *pl041;
      PCIBus *pci_bus;
      NICInfo *nd;
      int n;
@@ -265,6 +266,13 @@  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 */
+    pl041 = qdev_create(NULL, "pl041");
+    qdev_prop_set_uint32(pl041, "fifo_depth", 256); /* AN115B - Table 
1.1 */
+    qdev_init_nofail(pl041);
+    sysbus_mmio_map(sysbus_from_qdev(pl041), 0, 0x10004000);
+    sysbus_connect_irq(sysbus_from_qdev(pl041), 0, sic[24]);
+
      /* Memory map for Versatile/PB:  */
      /* 0x10000000 System registers.  */