diff --git a/hw/arm/Makefile.objs b/hw/arm/Makefile.objs
index 497c474..1ef36df 100644
--- a/hw/arm/Makefile.objs
+++ b/hw/arm/Makefile.objs
@@ -43,3 +43,4 @@ obj-y += ftpwmtmr010.o
 obj-y += ftwdt010.o
 obj-y += ftrtc011.o
 obj-y += ftdmac020.o
+obj-y += ftapbbrg020.o
diff --git a/hw/arm/faraday_a360.c b/hw/arm/faraday_a360.c
index 98e5cc0..dff28b1 100644
--- a/hw/arm/faraday_a360.c
+++ b/hw/arm/faraday_a360.c
@@ -77,6 +77,9 @@ a360_device_init(A360State *s)
                                        pic[40], /* TC */
                                        pic[41], /* ERR */
                                        NULL);
+
+    /* ftapbbrg020 */
+    s->pdma[0] = sysbus_create_simple("ftapbbrg020", 0x90500000, pic[24]);
 }
 
 static void
diff --git a/hw/arm/faraday_a369.c b/hw/arm/faraday_a369.c
index 406aee0..99e5f99 100644
--- a/hw/arm/faraday_a369.c
+++ b/hw/arm/faraday_a369.c
@@ -121,6 +121,9 @@ a369_device_init(A369State *s)
                                        pic[17], /* TC */
                                        pic[18], /* ERR */
                                        NULL);
+
+    /* ftapbbrg020 */
+    s->pdma[0] = sysbus_create_simple("ftapbbrg020", 0x90f00000, pic[14]);
 }
 
 static void
diff --git a/hw/arm/ftapbbrg020.c b/hw/arm/ftapbbrg020.c
new file mode 100644
index 0000000..1a25b86
--- /dev/null
+++ b/hw/arm/ftapbbrg020.c
@@ -0,0 +1,437 @@
+/*
+ * QEMU model of the FTAPBBRG020 DMA Controller
+ *
+ * Copyright (C) 2012 Faraday Technology
+ * Written by Dante Su <dantesu@faraday-tech.com>
+ *
+ * This file is licensed under GNU GPL v2+.
+ *
+ * Note: The FTAPBBRG020 DMA decreasing address mode is not implemented.
+ */
+
+#include <hw/sysbus.h>
+#include <sysemu/sysemu.h>
+#include <sysemu/blockdev.h>
+
+#include "ftapbbrg020.h"
+
+#define TYPE_FTAPBBRG020    "ftapbbrg020"
+
+typedef struct Ftapbbrg020State Ftapbbrg020State;
+
+typedef struct Ftapbbrg020Chan {
+    Ftapbbrg020State *chip;
+
+    int id;
+    int burst;
+    int src_bw;
+    int src_stride;
+    int dst_bw;
+    int dst_stride;
+
+    /* HW register caches */
+    uint32_t src;
+    uint32_t dst;
+    uint32_t len;
+    uint32_t cmd;
+} Ftapbbrg020Chan;
+
+typedef struct Ftapbbrg020State {
+    SysBusDevice busdev;
+    MemoryRegion iomem;
+    qemu_irq irq;
+
+    Ftapbbrg020Chan chan[4];
+    qemu_irq ack[16];
+    uint32_t req;
+
+    int busy;    /* Busy Channel ID */
+    int bh_owner;
+    QEMUBH *bh;
+} Ftapbbrg020State;
+
+#define FTAPBBRG020(obj) \
+    OBJECT_CHECK(Ftapbbrg020State, obj, TYPE_FTAPBBRG020)
+
+static uint32_t ftapbbrg020_get_isr(Ftapbbrg020State *s)
+{
+    int i;
+    uint32_t isr = 0;
+    Ftapbbrg020Chan *chan;
+
+    for (i = 0; i < 4; ++i) {
+        chan = s->chan + i;
+        isr |= (chan->cmd & CMD_INTR_STATUS);
+    }
+
+    return isr;
+}
+
+static void ftapbbrg020_update_irq(Ftapbbrg020State *s)
+{
+    uint32_t isr = ftapbbrg020_get_isr(s);
+
+    qemu_set_irq(s->irq, isr ? 1 : 0);
+}
+
+static void ftapbbrg020_chan_cmd_decode(Ftapbbrg020Chan *c)
+{
+    uint32_t tmp;
+
+    /* 1. decode burst size */
+    c->burst = (c->cmd & CMD_BURST4) ? 4 : 1;
+
+    /* 2. decode source/destination width */
+    tmp = (c->cmd >> 20) & 0x03;
+    if (tmp > 2) {
+        tmp = 2;
+    }
+    c->src_bw = c->dst_bw = 8 << (2 - tmp);
+
+    /* 3. decode source address stride */
+    switch ((c->cmd >> 8) & 0x03) {
+    case 0:
+        c->src_stride = 0;
+        break;
+    case 1:
+        c->src_stride = c->src_bw >> 3;
+        break;
+    case 2:
+        c->src_stride = 2 * (c->src_bw >> 3);
+        break;
+    case 3:
+        c->src_stride = 4 * (c->src_bw >> 3);
+        break;
+    }
+
+    /* 4. decode destination address stride */
+    switch ((c->cmd >> 12) & 0x03) {
+    case 0:
+        c->dst_stride = 0;
+        break;
+    case 1:
+        c->dst_stride = c->dst_bw >> 3;
+        break;
+    case 2:
+        c->dst_stride = 2 * (c->dst_bw >> 3);
+        break;
+    case 3:
+        c->dst_stride = 4 * (c->dst_bw >> 3);
+        break;
+    }
+}
+
+static void ftapbbrg020_chan_start(Ftapbbrg020Chan *c)
+{
+    Ftapbbrg020State *s = c->chip;
+    hwaddr src, dst;
+    uint8_t buf[4096] __attribute__ ((aligned (8)));
+    int i, len, stride, src_hs, dst_hs;
+
+    if (!(c->cmd & CMD_START)) {
+        return;
+    }
+
+    s->busy = c->id;
+
+    /* DMA src/dst address */
+    src = c->src;
+    dst = c->dst;
+    /* DMA hardware handshake id */
+    src_hs = (c->cmd >> 24) & 0xf;
+    dst_hs = (c->cmd >> 16) & 0xf;
+    /* DMA src/dst sanity check */
+    if (cpu_physical_memory_is_io(src) && c->src_stride) {
+        hw_error("ftapbbrg020: src is an I/O device with non-fixed address?\n");
+        exit(1);
+    }
+    if (cpu_physical_memory_is_io(dst) && c->dst_stride) {
+        hw_error("ftapbbrg020: dst is an I/O device with non-fixed address?\n");
+        exit(1);
+    }
+
+    while (c->len > 0) {
+        /*
+         * Postpone this DMA action
+         * if the corresponding dma request is not asserted
+         */
+        if (src_hs && !(s->req & (1 << src_hs))) {
+            break;
+        }
+        if (dst_hs && !(s->req & (1 << dst_hs))) {
+            break;
+        }
+
+        len = MIN(sizeof(buf), c->burst * (c->src_bw >> 3));
+
+        /* load data from source into local buffer */
+        if (c->src_stride) {
+            cpu_physical_memory_read(src, buf, len);
+            src += len;
+        } else {
+            stride = c->src_bw >> 3;
+            for (i = 0; i < len; i += stride) {
+                cpu_physical_memory_read(src, buf + i, stride);
+            }
+        }
+
+        /* DMA Hardware Handshake */
+        if (src_hs) {
+            qemu_set_irq(s->ack[src_hs], 1);
+        }
+
+        /* store data into destination from local buffer */
+        if (c->dst_stride) {
+            cpu_physical_memory_write(dst, buf, len);
+            dst += len;
+        } else {
+            stride = c->dst_bw >> 3;
+            for (i = 0; i < len; i += stride) {
+                cpu_physical_memory_write(dst, buf + i, stride);
+            }
+        }
+
+        /* DMA Hardware Handshake */
+        if (dst_hs) {
+            qemu_set_irq(s->ack[dst_hs], 1);
+        }
+
+        /* update the channel transfer size */
+        c->len -= len / (c->src_bw >> 3);
+
+        if (c->len == 0) {
+            /* update the channel transfer status */
+            if (c->cmd & CMD_INTR_FIN_EN) {
+                c->cmd |= CMD_INTR_FIN;
+                ftapbbrg020_update_irq(s);
+            }
+            /* clear start bit */
+            c->cmd &= 0xfffffffe;
+        }
+    }
+
+    /* update src/dst address */
+    c->src = src;
+    c->dst = dst;
+
+    s->busy = -1;
+}
+
+static void ftapbbrg020_chan_reset(Ftapbbrg020Chan *c)
+{
+    c->cmd = 0;
+    c->src = 0;
+    c->dst = 0;
+    c->len = 0;
+}
+
+static void ftapbbrg020_bh(void *opaque)
+{
+    Ftapbbrg020State *s = FTAPBBRG020(opaque);
+    Ftapbbrg020Chan  *c = NULL;
+    int i, jobs, done;
+
+    ++s->bh_owner;
+    jobs = 0;
+    done = 0;
+    for (i = 0; i < 4; ++i) {
+        c = s->chan + i;
+        if (c->cmd & CMD_START) {
+            ++jobs;
+            ftapbbrg020_chan_start(c);
+            if (!(c->cmd & CMD_START)) {
+                ++done;
+            }
+        }
+    }
+    --s->bh_owner;
+
+    /*
+     * Devices those with an infinite FIFO (always ready for R/W)
+     * would trigger a new DMA handshake transaction here.
+     * (i.e. ftnandc021, ftsdc010)
+     */
+    if ((jobs - done) && s->req) {
+        qemu_bh_schedule(s->bh);
+    }
+}
+
+static void ftapbbrg020_handle_req(void *opaque, int line, int level)
+{
+    Ftapbbrg020State *s = FTAPBBRG020(opaque);
+
+    if (level) {
+        /*
+         * Devices those wait for data from external I/O
+         * would trigger a new DMA handshake transaction here.
+         * (i.e. ftssp010)
+         */
+        if (!(s->req & (1 << line))) {
+            /* a simple workaround for BH reentry issue */
+            if (!s->bh_owner) {
+                qemu_bh_schedule(s->bh);
+            }
+        }
+        s->req |= (1 << line);
+    } else {
+        s->req &= ~(1 << line);
+        qemu_set_irq(s->ack[line], 0);
+    }
+}
+
+static void ftapbbrg020_chip_reset(Ftapbbrg020State *s)
+{
+    int i;
+
+    for (i = 0; i < 4; ++i) {
+        ftapbbrg020_chan_reset(s->chan + i);
+    }
+
+    /* We can assume our GPIO have been wired up now */
+    for (i = 0; i < 16; ++i) {
+        qemu_set_irq(s->ack[i], 0);
+    }
+    s->req = 0;
+}
+
+static uint64_t ftapbbrg020_mem_read(void *opaque, hwaddr addr, unsigned size)
+{
+    Ftapbbrg020State *s = FTAPBBRG020(opaque);
+    Ftapbbrg020Chan  *c = NULL;
+    uint32_t ret = 0;
+
+    if (addr >= REG_CHAN_BASE(0) && addr < REG_CHAN_BASE(4)) {
+        c = s->chan + REG_CHAN_ID(addr);
+        switch (addr & 0x0f) {
+        case REG_CHAN_CMD:
+            return c->cmd;
+        case REG_CHAN_SRC:
+            return c->src;
+        case REG_CHAN_DST:
+            return c->dst;
+        case REG_CHAN_CYC:
+            return c->len;
+        }
+    } else {
+        switch (addr) {
+        case REG_REVISION:
+            return 0x00010800;
+        default:
+            break;
+        }
+    }
+
+    return ret;
+}
+
+static void ftapbbrg020_mem_write(void    *opaque,
+                                  hwaddr   addr,
+                                  uint64_t val,
+                                  unsigned size)
+{
+    Ftapbbrg020State *s = FTAPBBRG020(opaque);
+    Ftapbbrg020Chan  *c = NULL;
+
+    if (addr >= REG_CHAN_BASE(0) && addr < REG_CHAN_BASE(4)) {
+        c = s->chan + REG_CHAN_ID(addr);
+        switch (addr & 0x0f) {
+        case REG_CHAN_CMD:
+            c->cmd = (uint32_t)val;
+            ftapbbrg020_update_irq(s);
+            if (c->cmd & 0x01) {
+                ftapbbrg020_chan_cmd_decode(c);
+                /* kick-off DMA engine */
+                qemu_bh_schedule(s->bh);
+            }
+            break;
+        case REG_CHAN_SRC:
+            c->src = (uint32_t)val;
+            break;
+        case REG_CHAN_DST:
+            c->dst = (uint32_t)val;
+            break;
+        case REG_CHAN_CYC:
+            c->len = (uint32_t)val & 0x00ffffff;
+            break;
+        }
+    }
+}
+
+static const MemoryRegionOps ftapbbrg020_ops = {
+    .read  = ftapbbrg020_mem_read,
+    .write = ftapbbrg020_mem_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 4,
+        .max_access_size = 4
+    }
+};
+
+static void ftapbbrg020_reset(DeviceState *ds)
+{
+    SysBusDevice *busdev = SYS_BUS_DEVICE(ds);
+    Ftapbbrg020State *s = FTAPBBRG020(FROM_SYSBUS(Ftapbbrg020State, busdev));
+
+    ftapbbrg020_chip_reset(s);
+}
+
+static int ftapbbrg020_init(SysBusDevice *dev)
+{
+    Ftapbbrg020State *s = FTAPBBRG020(FROM_SYSBUS(Ftapbbrg020State, dev));
+    int i;
+
+    memory_region_init_io(&s->iomem,
+                          &ftapbbrg020_ops,
+                          s,
+                          TYPE_FTAPBBRG020,
+                          0x1000);
+    sysbus_init_mmio(dev, &s->iomem);
+    sysbus_init_irq(dev, &s->irq);
+    qdev_init_gpio_in(&s->busdev.qdev, ftapbbrg020_handle_req, 16);
+    qdev_init_gpio_out(&s->busdev.qdev, s->ack, 16);
+
+    s->busy = -1;
+    s->bh = qemu_bh_new(ftapbbrg020_bh, s);
+    for (i = 0; i < 4; ++i) {
+        Ftapbbrg020Chan *c = s->chan + i;
+        c->id   = i;
+        c->chip = s;
+    }
+
+    return 0;
+}
+
+static const VMStateDescription vmstate_ftapbbrg020 = {
+    .name = TYPE_FTAPBBRG020,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .minimum_version_id_old = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void ftapbbrg020_class_init(ObjectClass *klass, void *data)
+{
+    SysBusDeviceClass *k = SYS_BUS_DEVICE_CLASS(klass);
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    k->init     = ftapbbrg020_init;
+    dc->vmsd    = &vmstate_ftapbbrg020;
+    dc->reset   = ftapbbrg020_reset;
+    dc->no_user = 1;
+}
+
+static const TypeInfo ftapbbrg020_info = {
+    .name           = TYPE_FTAPBBRG020,
+    .parent         = TYPE_SYS_BUS_DEVICE,
+    .instance_size  = sizeof(Ftapbbrg020State),
+    .class_init     = ftapbbrg020_class_init,
+};
+
+static void ftapbbrg020_register_types(void)
+{
+    type_register_static(&ftapbbrg020_info);
+}
+
+type_init(ftapbbrg020_register_types)
diff --git a/hw/arm/ftapbbrg020.h b/hw/arm/ftapbbrg020.h
new file mode 100644
index 0000000..9a1b5cf
--- /dev/null
+++ b/hw/arm/ftapbbrg020.h
@@ -0,0 +1,41 @@
+/*
+ * QEMU model of the FTAPBBRG020 DMA Controller
+ *
+ * Copyright (C) 2012 Faraday Technology
+ * Written by Dante Su <dantesu@faraday-tech.com>
+ *
+ * This file is licensed under GNU GPL v2+.
+ *
+ * Note: The FTAPBBRG020 DMA decreasing address mode is not implemented.
+ */
+
+#ifndef HW_ARM_FTAPBBRG020_H
+#define HW_ARM_FTAPBBRG020_H
+
+#define REG_REVISION        0xC8
+
+/*
+ * Channel base address
+ * @ch: channle id (0 <= id <= 3)
+ *      i.e. 0: Channel A
+ *           1: Channel B
+ *           2: Channel C
+ *           3: Channel D
+ */
+#define REG_CHAN_ID(off)    (((off) - 0x80) >> 4)
+#define REG_CHAN_BASE(ch)   (0x80 + ((ch) << 4))
+
+#define REG_CHAN_SRC        0x00
+#define REG_CHAN_DST        0x04
+#define REG_CHAN_CYC        0x08
+#define REG_CHAN_CMD        0x0C
+
+#define CMD_START           (1 << 0)
+#define CMD_INTR_FIN        (1 << 1)
+#define CMD_INTR_FIN_EN     (1 << 2)
+#define CMD_BURST4          (1 << 3)
+#define CMD_INTR_ERR        (1 << 4)
+#define CMD_INTR_ERR_EN     (1 << 5)
+#define CMD_INTR_STATUS     (CMD_INTR_FIN | CMD_INTR_ERR)
+
+#endif    /* __FTAPBB020_H */
