diff mbox

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

Message ID 1305069201-15961-1-git-send-email-contact@elasticsheep.com
State New
Headers show

Commit Message

contact@elasticsheep.com May 10, 2011, 11:13 p.m. UTC
The PL041 driver provides an interface to an ACLink bus.
The LM4549 driver emulates a DAC connected on the ACLink bus.
Only audio playback is implemented.

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/aclink.c      |  121 +++++++++++++++
 hw/aclink.h      |   63 ++++++++
 hw/lm4549.c      |  368 +++++++++++++++++++++++++++++++++++++++++++++
 hw/pl041.c       |  436 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 hw/pl041.h       |  126 ++++++++++++++++
 hw/pl041.hx      |   62 ++++++++
 hw/versatilepb.c |    6 +
 8 files changed, 1183 insertions(+), 0 deletions(-)
 create mode 100644 hw/aclink.c
 create mode 100644 hw/aclink.h
 create mode 100644 hw/lm4549.c
 create mode 100644 hw/pl041.c
 create mode 100644 hw/pl041.h
 create mode 100644 hw/pl041.hx

--
1.7.0.4

Comments

malc May 10, 2011, 11:25 p.m. UTC | #1
On Tue, 10 May 2011, Mathieu Sonet wrote:

> The PL041 driver provides an interface to an ACLink bus.
> The LM4549 driver emulates a DAC connected on the ACLink bus.
> Only audio playback is implemented.
> 
> 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>

Looks fine to me.
Paul Brook May 11, 2011, 9:58 a.m. UTC | #2
> The PL041 driver provides an interface to an ACLink bus.
> The LM4549 driver emulates a DAC connected on the ACLink bus.
> Only audio playback is implemented.

Shouldn't this be shared with the other AC97 devices?

Paul
contact@elasticsheep.com May 11, 2011, 6:14 p.m. UTC | #3
Paul Brook wrote:
>> The PL041 driver provides an interface to an ACLink bus.
>> The LM4549 driver emulates a DAC connected on the ACLink bus.
>> Only audio playback is implemented.
> 
> Shouldn't this be shared with the other AC97 devices?
> 
> Paul

I organized the code in 3 different drivers (PL041 <=> ACLink <=> 
LM4549) to decorrelate the codec interface from its implementation. This 
could allow the use of alternative AC97 models with the same PL041 
implementation.

On the other hand the current ac97.c implementation is a closely coupled 
combination of a PCI/ACLink bridge (Intel 82801AA) with a generic AC97 
codec. This has prevent me to easily reuse this code.

The milkymist-ac97 implementation is another case. It looks like a basic 
implementation with the AC97 registers directly mapped on the system bus.

Using the ACLink bus I defined, it could be interesting to implement 
separately the PCI/ACLink bridge from ac97.c.

Is it what you mean by saying this should be shared with the other AC97 
devices ?

Mathieu
Paul Brook May 12, 2011, 5:41 p.m. UTC | #4
> On the other hand the current ac97.c implementation is a closely coupled
> combination of a PCI/ACLink bridge (Intel 82801AA) with a generic AC97
> codec. This has prevent me to easily reuse this code.
> 
> The milkymist-ac97 implementation is another case. It looks like a basic
> implementation with the AC97 registers directly mapped on the system bus.
> 
> Using the ACLink bus I defined, it could be interesting to implement
> separately the PCI/ACLink bridge from ac97.c.
> 
> Is it what you mean by saying this should be shared with the other AC97
> devices ?

Yes. The whole point of AClink is that it separates the host bridge from the 
codec. We now have at least three devices implementing this.  Your aclink 
implementation is only used by one of these, which gives me little confidence 
it actually does what it claims.

Paul
contact@elasticsheep.com May 12, 2011, 9 p.m. UTC | #5
Paul Brook wrote:
>> On the other hand the current ac97.c implementation is a closely coupled
>> combination of a PCI/ACLink bridge (Intel 82801AA) with a generic AC97
>> codec. This has prevent me to easily reuse this code.
>>
>> The milkymist-ac97 implementation is another case. It looks like a basic
>> implementation with the AC97 registers directly mapped on the system bus.
>>
>> Using the ACLink bus I defined, it could be interesting to implement
>> separately the PCI/ACLink bridge from ac97.c.
>>
>> Is it what you mean by saying this should be shared with the other AC97
>> devices ?
> 
> Yes. The whole point of AClink is that it separates the host bridge from the 
> codec. We now have at least three devices implementing this.  Your aclink 
> implementation is only used by one of these, which gives me little confidence 
> it actually does what it claims.
> 
> Paul

I understand your concern.

In fact after digging the Intel PCI bridge documentation, I see that it 
  offers a mapping of the AC97 registers in the PCI I/O space.

Reusing my current ACLink bus with this bridge would mean to encode 
register accesses into ACLink frames and then to decode them again on 
the codec side. Not very simple just for the sake of device models 
correctness and no added value.

Also QEMU may not need N different re-implemention of an AC97 codec.

So I will ditch ACLink/LM4549 and will instead interface the PL041 
driver with the codec defined in ac97.c.

   PCI---AC97
PL041--/

Thanks for your input
Mathieu
diff mbox

Patch

diff --git a/Makefile.target b/Makefile.target
index 21f864a..cdd7b40 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 aclink.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/aclink.c b/hw/aclink.c
new file mode 100644
index 0000000..c335f60
--- /dev/null
+++ b/hw/aclink.c
@@ -0,0 +1,121 @@ 
+/*
+ * ACLink Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This file defines the ACLink bus interface to exchange data
+ * between an host and a codec.
+ *
+ */
+
+#include "aclink.h"
+
+/*** Types ***/
+
+struct ACLinkBus {
+    BusState qbus;
+    ACLinkControllerInfo *controller_info;
+    uint32_t bitclk;
+};
+
+struct BusInfo aclink_bus_info = {
+    .name = "aclink",
+    .size = sizeof(ACLinkBus),
+};
+
+/*** Functions ***/
+
+ACLinkBus *aclink_create_bus(DeviceState *parent, const char *name)
+{
+    BusState *bus;
+    bus = qbus_create(&aclink_bus_info, parent, name);
+    return FROM_QBUS(ACLinkBus, bus);
+}
+
+void aclink_set_controller_info(ACLinkBus *bus, ACLinkControllerInfo *info)
+{
+    bus->controller_info = info;
+}
+
+static int aclink_device_init(DeviceState *dev, DeviceInfo *base_info)
+{
+    ACLinkDeviceInfo *info = container_of(base_info, ACLinkDeviceInfo, qdev);
+    ACLinkDevice *s = ACLINK_DEVICE_FROM_QDEV(dev);
+    ACLinkBus *bus;
+
+    bus = FROM_QBUS(ACLinkBus, qdev_get_parent_bus(dev));
+    if (QLIST_FIRST(&bus->qbus.children) != dev
+        || QLIST_NEXT(dev, sibling) != NULL) {
+        hw_error("Too many devices on the ACLINK bus");
+    }
+
+    s->info = info;
+    return info->init(s);
+}
+
+void aclink_register_device(ACLinkDeviceInfo *info)
+{
+    assert(info->qdev.size >= sizeof(ACLinkDevice));
+    info->qdev.init = aclink_device_init;
+    info->qdev.bus_info = &aclink_bus_info;
+    qdev_register(&info->qdev);
+}
+
+DeviceState *aclink_create_device(ACLinkBus *bus, const char *name)
+{
+    DeviceState *dev;
+    dev = qdev_create(&bus->qbus, name);
+    qdev_init_nofail(dev);
+    return dev;
+}
+
+static ACLinkDevice *aclink_get_device(ACLinkBus *bus)
+{
+    DeviceState *dev = QLIST_FIRST(&bus->qbus.children);
+    if (!dev) {
+        return NULL;
+    }
+    return ACLINK_DEVICE_FROM_QDEV(dev);
+}
+
+void aclink_bitclk_enable(ACLinkDevice *dev, uint32_t on)
+{
+    ACLinkBus *bus = FROM_QBUS(ACLinkBus, qdev_get_parent_bus(&dev->qdev));
+    uint32_t has_changed;
+
+    on = (on > 0) ? 1 : 0;
+    has_changed = (bus->bitclk != on) ? 1 : 0;
+
+    bus->bitclk = on;
+    if (has_changed) {
+        bus->controller_info->bitclk_state_changed(bus->qbus.parent);
+    }
+}
+
+uint32_t aclink_bitclk_is_enabled(ACLinkBus *bus)
+{
+    return bus->bitclk;
+}
+
+void aclink_sdataout_slot12(ACLinkBus *bus, uint32_t slot1, uint32_t slot2)
+{
+    ACLinkDevice *device = aclink_get_device(bus);
+    device->info->sdataout_slot12(device, slot1, slot2);
+}
+
+void aclink_sdataout_slot34(ACLinkBus *bus, uint32_t slot3, uint32_t slot4)
+{
+    ACLinkDevice *device = aclink_get_device(bus);
+    device->info->sdataout_slot34(device, slot3, slot4);
+}
+
+void aclink_sdatain_slot12(ACLinkDevice *dev, uint32_t slot1, uint32_t slot2)
+{
+    ACLinkBus *bus = FROM_QBUS(ACLinkBus, qdev_get_parent_bus(&dev->qdev));
+    bus->controller_info->sdatain_slot12(bus->qbus.parent, slot1, slot2);
+}
diff --git a/hw/aclink.h b/hw/aclink.h
new file mode 100644
index 0000000..d360d4b
--- /dev/null
+++ b/hw/aclink.h
@@ -0,0 +1,63 @@ 
+/*
+ * ACLink Interface
+ *
+ * Copyright (c) 2011
+ * Written by Mathieu Sonet - www.elasticsheep.com
+ *
+ * This code is licenced under the GPL.
+ *
+ * *****************************************************************
+ *
+ * This file defines the ACLink bus interface to exchange data
+ * between an host and a codec.
+ *
+ */
+
+#ifndef ACLINK_H
+#define ACLINK_H
+
+#include "qdev.h"
+
+typedef struct ACLinkBus ACLinkBus;
+typedef struct ACLinkDevice ACLinkDevice;
+
+/* Controller */
+typedef struct {
+    void (*sdatain_slot12)(DeviceState *dev, uint32_t slot1, uint32_t slot2);
+    void (*bitclk_state_changed)(DeviceState *dev);
+} ACLinkControllerInfo;
+
+/* Device  */
+typedef struct {
+    DeviceInfo qdev;
+    int (*init)(ACLinkDevice *dev);
+    void (*sdataout_slot12)(ACLinkDevice *dev, uint32_t slot1, uint32_t slot2);
+    void (*sdataout_slot34)(ACLinkDevice *dev, uint32_t slot3, uint32_t slot4);
+} ACLinkDeviceInfo;
+
+struct ACLinkDevice {
+    DeviceState qdev;
+    ACLinkDeviceInfo *info;
+};
+
+#define ACLINK_DEVICE_FROM_QDEV(dev) DO_UPCAST(ACLinkDevice, qdev, dev)
+#define FROM_ACLINK_DEVICE(type, dev) DO_UPCAST(type, aclinkdev, dev)
+
+ACLinkBus *aclink_create_bus(DeviceState *parent, const char *name);
+void aclink_set_controller_info(ACLinkBus *bus, ACLinkControllerInfo *info);
+
+DeviceState *aclink_create_device(ACLinkBus *bus, const char *name);
+void aclink_register_device(ACLinkDeviceInfo *info);
+
+/* Common interface */
+uint32_t aclink_bitclk_is_enabled(ACLinkBus *bus);
+
+/* Controller => device interface */
+void aclink_sdataout_slot12(ACLinkBus *bus, uint32_t slot1, uint32_t slot2);
+void aclink_sdataout_slot34(ACLinkBus *bus, uint32_t slot3, uint32_t slot4);
+
+/* Device => controller interface */
+void aclink_sdatain_slot12(ACLinkDevice *bus, uint32_t slot1, uint32_t slot2);
+void aclink_bitclk_enable(ACLinkDevice *dev, uint32_t on);
+
+#endif
diff --git a/hw/lm4549.c b/hw/lm4549.c
new file mode 100644
index 0000000..050a7a0
--- /dev/null
+++ b/hw/lm4549.c
@@ -0,0 +1,368 @@ 
+/*
+ * 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 connected on an ACLINK bus.
+ *
+ */
+
+#include "sysbus.h"
+#include "aclink.h"
+
+#include "audio/audio.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
+
+/*** Local prototypes ***/
+void lm4549_sdataout_slot12(ACLinkDevice *dev, uint32_t slot1, uint32_t slot2);
+void lm4549_sdataout_slot34(ACLinkDevice *dev, uint32_t slot3, uint32_t slot4);
+
+/*** 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
+};
+
+/*** LM4549 device state ***/
+typedef struct {
+    struct {
+        uint16_t value;
+        uint16_t default_value;
+        uint16_t read_only;
+    } data[128];
+} lm4549_registers;
+
+typedef struct {
+    ACLinkDevice aclinkdev;
+    lm4549_registers codec_regs;
+    QEMUSoundCard card;
+    SWVoiceOut *voice;
+
+#define BUFFER_SIZE (512)
+    uint32_t buffer[BUFFER_SIZE];
+    uint32_t buffer_level;
+} lm4549_state;
+
+/*** 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("lm4549_store: 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("lm4549_store: Out of bound offset 0x%x\n", (int)offset);
+    }
+
+    if (r->data[offset].read_only) {
+        DPRINTF("lm4549_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("lm4549_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];
+            }
+        }
+
+        /* Regulate the data throughput by disabling further transfer
+           from the ACLink controller */
+        aclink_bitclk_enable(&s->aclinkdev, 0);
+    }
+}
+
+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("lm4549_audio_out_callback size = %i free = %i\n", size, free);
+#endif
+
+    /* Detect that no more date are coming from the ACLink
+       => 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);
+    }
+
+    /* Enable the bitclk to get data again */
+    aclink_bitclk_enable(&s->aclinkdev, 1);
+}
+
+static uint32_t lm4549_read(ACLinkDevice *dev, target_phys_addr_t offset)
+{
+    lm4549_state *s = FROM_ACLINK_DEVICE(lm4549_state, dev);
+    uint32_t value = 0;
+
+    /* Read the stored value */
+    value = lm4549_load(s, offset);
+    DPRINTF("lm4549_read [0x%02x] = 0x%04x\n", offset, value);
+
+    return value;
+}
+
+static void lm4549_write(ACLinkDevice *dev,
+                         target_phys_addr_t offset, uint32_t value)
+{
+    lm4549_state *s = FROM_ACLINK_DEVICE(lm4549_state, dev);
+
+    DPRINTF("lm4549_write [0x%02x] = 0x%04x\n", offset, value);
+
+    /* Store the new value */
+    lm4549_store(s, offset, value);
+
+    switch (offset) {
+    case LM4549_Reset:
+        lm4549_store_reset(s);
+        break;
+    case LM4549_PCM_Front_DAC_Rate:
+        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",
+            dev,
+            lm4549_audio_out_callback,
+            &as
+        );
+        break;
+    }
+}
+
+void lm4549_sdataout_slot12(ACLinkDevice *dev, uint32_t slot1, uint32_t slot2)
+{
+#define SLOT1_RW    (1 << 19)
+    uint16_t control = (slot1 >> 12) & 0x7F;
+    uint16_t data = (slot2 >> 4) & 0xFFFF;
+    uint32_t value = 0;
+
+    if ((slot1 & SLOT1_RW) == 0) {
+        /* Write operation */
+        lm4549_write(dev, control, data);
+    } else {
+        /* Read operation */
+        value = lm4549_read(dev, control);
+
+        /* Write the return value in SDATAIN */
+        aclink_sdatain_slot12(dev, slot1 & ~SLOT1_RW, value << 4);
+    }
+}
+
+void lm4549_sdataout_slot34(ACLinkDevice *dev, uint32_t slot3, uint32_t slot4)
+{
+    lm4549_state *s = FROM_ACLINK_DEVICE(lm4549_state, dev);
+    uint32_t sample = ((slot3 >> 4) & 0xFFFF) + (((slot4 >> 4) & 0xFFFF) << 16);
+
+    if (s->buffer_level >= BUFFER_SIZE) {
+        hw_error("sdataout slot34: overrun\n");
+    }
+
+    /* 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);
+    }
+}
+
+static int lm4549_init(ACLinkDevice *dev)
+{
+    lm4549_state *s = FROM_ACLINK_DEVICE(lm4549_state, dev);
+    struct audsettings as;
+
+    /* 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,       0x0000, 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);
+
+    /* Enable the ACLink clock */
+    aclink_bitclk_enable(dev, 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
+
+    return 0;
+}
+
+static ACLinkDeviceInfo lm4549_info = {
+    .qdev = {
+        .name = "lm4549",
+        .size = sizeof(lm4549_state)
+    },
+    .init = lm4549_init,
+    .sdataout_slot12 = lm4549_sdataout_slot12,
+    .sdataout_slot34 = lm4549_sdataout_slot34,
+};
+
+static void lm4549_register_device(void)
+{
+    aclink_register_device(&lm4549_info);
+}
+
+device_init(lm4549_register_device)
diff --git a/hw/pl041.c b/hw/pl041.c
new file mode 100644
index 0000000..0c84dd8
--- /dev/null
+++ b/hw/pl041.c
@@ -0,0 +1,436 @@ 
+/*
+ * 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.
+ * It connects the system bus to an ACLink bus on which an audio
+ * codec can be connected.
+ *
+ */
+
+#include "sysbus.h"
+
+#include "pl041.h"
+#include "aclink.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
+
+/*** 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;
+    ACLinkBus *aclink;
+} 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
+
+/*** Local prototypes ***/
+
+static void pl041_isr1_update(pl041_state *s);
+
+/*** Functions ***/
+
+#if defined(PL041_DEBUG_LEVEL)
+static const char *get_reg_name(target_phys_addr_t offset)
+{
+    if (offset <= PL041_dr4_3) {
+        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];
+                uint32_t slot3, slot4;
+
+                /* Check the sample width */
+                switch (s->regs.txcr1 & TSIZE_MASK) {
+                case TSIZE_16BITS:
+                    /* 20-bit left justification */
+                    slot3 = (sample & 0xFFFF) << 4;
+                    slot4 = ((sample >> 16) & 0xFFFF) << 4;
+                    break;
+                case TSIZE_18BITS:
+                case TSIZE_20BITS:
+                case TSIZE_12BITS:
+                default:
+                    hw_error("Unsupported TSize\n");
+                    break;
+                }
+
+                /* Stop sending if the clock is disabled */
+                if (aclink_bitclk_is_enabled(s->aclink) == 0) {
+                    DBG_L1("bitclk is disabled => pause the transfer\n");
+                    break;
+                }
+
+                /* Transmit a sample on the ACLINK bus */
+                aclink_sdataout_slot34(s->aclink, slot3, slot4);
+            }
+
+            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_sdatain_slot12(DeviceState *dev,
+                                 uint32_t slot1, uint32_t slot2)
+{
+    pl041_state *s = (pl041_state *)dev;
+
+    DBG_L1("pl041_sdatain_slot12 0x%08x 0x%08x\n", slot1, slot2);
+
+    s->regs.sl1rx = slot1;
+    s->regs.sl2rx = slot2;
+
+    s->regs.slfr &= ~SL1RXBUSY | ~SL2RXBUSY;
+    s->regs.slfr |= SL1RXVALID | SL2RXVALID;
+}
+
+static void pl041_bitclk_state_changed(DeviceState *dev)
+{
+    pl041_state *s = (pl041_state *)dev;
+
+    /* Check if the bitclk signal is enabled */
+    if (aclink_bitclk_is_enabled(s->aclink) == 1) {
+        DBG_L1("bitclk enabled\n");
+
+        /* Trigger pending transfers */
+        pl041_fifo1_transmit(s);
+        pl041_isr1_update(s);
+    } else {
+        DBG_L1("bitclk disabled\n");
+    }
+}
+
+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;
+
+    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;
+        aclink_sdataout_slot12(s->aclink, s->regs.sl1tx, s->regs.sl2tx);
+        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 ACLinkControllerInfo pl041_controller_info = {
+    .sdatain_slot12 = pl041_sdatain_slot12,
+    .bitclk_state_changed = pl041_bitclk_state_changed,
+};
+
+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);
+
+    /* Create the ACLink bus */
+    s->aclink = aclink_create_bus(&dev->qdev, "aclink");
+    aclink_set_controller_info(s->aclink, &pl041_controller_info);
+
+    /* Reset the device */
+    pl041_reset(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..51b66e3
--- /dev/null
+++ b/hw/pl041.h
@@ -0,0 +1,126 @@ 
+/*
+ * 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.
+ * It connects the system bus to an ACLink bus on which an audio
+ * codec can be connected.
+ *
+ */
+
+/* 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..529f892
--- /dev/null
+++ b/hw/pl041.hx
@@ -0,0 +1,62 @@ 
+/*
+ * 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.
+ * It connects the system bus to an ACLink bus on which an audio
+ * codec can be connected.
+ *
+ */
+
+/* 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..51e7d47 100644
--- a/hw/versatilepb.c
+++ b/hw/versatilepb.c
@@ -17,6 +17,7 @@ 
 #include "usb-ohci.h"
 #include "boards.h"
 #include "blockdev.h"
+#include "aclink.h"

 /* Primary interrupt controller.  */

@@ -258,6 +259,11 @@  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 and connect the LM4549 codec */
+    dev = sysbus_create_varargs("pl041", 0x10004000, sic[24]);
+    ACLinkBus* bus = (ACLinkBus *)qdev_get_child_bus(dev, "aclink");
+    aclink_create_device(bus, "lm4549");
+
     /* Memory map for Versatile/PB:  */
     /* 0x10000000 System registers.  */
     /* 0x10001000 PCI controller config registers.  */