Patchwork [v5,2/4] exynos4210: Added SD host controller model

login
register
mail settings
Submitter Peter A. G. Crosthwaite
Date July 5, 2012, 4:04 a.m.
Message ID <0020234e7fc3001f66ef67a1d3e19a01b6e498a5.1341457220.git.peter.crosthwaite@petalogix.com>
Download mbox | patch
Permalink /patch/169066/
State New
Headers show

Comments

Peter A. G. Crosthwaite - July 5, 2012, 4:04 a.m.
From: Igor Mitsyanko <i.mitsyanko@samsung.com>

Custom Exynos4210 SD/MMC host controller, based on SD association standard host
controller ver. 2.00.

Signed-off-by: Igor Mitsyanko <i.mitsyanko@samsung.com>
---
changed from v4 (Igor):
set irq on SLOTINT status instead of interrupt registers status; instead;
conditional in exynos4210_sdhci_can_issue_command() made more readable and documented;
add a comment to exynos4210_sdhci_writefn()'s SDHC_CLKCON case statement;
do not override superclass property in exynos4210.sdhci class, set properties to required value in realize function
changed from v3:
rebased for new Makefile system
fixed commit msg typo (Andreas review)
 hw/arm/Makefile.objs  |    1 +
 hw/exynos4210.c       |   20 +++
 hw/exynos4210_sdhci.c |  443 +++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 464 insertions(+), 0 deletions(-)
 create mode 100644 hw/exynos4210_sdhci.c
Peter Maydell - July 16, 2012, 5:13 p.m.
On 5 July 2012 05:04, Peter A. G. Crosthwaite
<peter.crosthwaite@petalogix.com> wrote:
> From: Igor Mitsyanko <i.mitsyanko@samsung.com>
>
> Custom Exynos4210 SD/MMC host controller, based on SD association standard host
> controller ver. 2.00.
>
> Signed-off-by: Igor Mitsyanko <i.mitsyanko@samsung.com>
> ---
> changed from v4 (Igor):
> set irq on SLOTINT status instead of interrupt registers status; instead;

The IRQ handling code still looks really weird. I would expect
that the code would be:
  [code which updates various kinds of irq related state]
  sdhci_update_irq();

where sdhci_update_irq() calls qemu_set_irq() based on the state.

At the moment it looks as if you're using slotint as a cached value
of the expression
"((s->norintsts & s->norintsigen) || (s->errintsts & s->errintsigen) ||
 ((s->norintsts & SDHC_NIS_INSERT) && (s->wakcon & SDHC_WKUP_ON_INS)) ||
 ((s->norintsts & SDHC_NIS_REMOVE) && (s->wakcon & SDHC_WKUP_ON_RMV)))"

[can these two ever have different values?] and also attempting to
shortcut by manually updating slotint in codepaths which change only
parts of the state which this expression is testing. Why not just do
things the simple and straightforward way and get rid of slotint
completely?

-- PMM
Mitsyanko Igor - July 17, 2012, 12:55 p.m.
On 07/16/2012 09:13 PM, Peter Maydell wrote:
> On 5 July 2012 05:04, Peter A. G. Crosthwaite
> <peter.crosthwaite@petalogix.com> wrote:
>> From: Igor Mitsyanko <i.mitsyanko@samsung.com>
>>
>> Custom Exynos4210 SD/MMC host controller, based on SD association standard host
>> controller ver. 2.00.
>>
>> Signed-off-by: Igor Mitsyanko <i.mitsyanko@samsung.com>
>> ---
>> changed from v4 (Igor):
>> set irq on SLOTINT status instead of interrupt registers status; instead;
> The IRQ handling code still looks really weird. I would expect
> that the code would be:
>    [code which updates various kinds of irq related state]
>    sdhci_update_irq();
>
> where sdhci_update_irq() calls qemu_set_irq() based on the state.
>
> At the moment it looks as if you're using slotint as a cached value
> of the expression
> "((s->norintsts & s->norintsigen) || (s->errintsts & s->errintsigen) ||
>   ((s->norintsts & SDHC_NIS_INSERT) && (s->wakcon & SDHC_WKUP_ON_INS)) ||
>   ((s->norintsts & SDHC_NIS_REMOVE) && (s->wakcon & SDHC_WKUP_ON_RMV)))"
>
> [can these two ever have different values?] and also attempting to
> shortcut by manually updating slotint in codepaths which change only
> parts of the state which this expression is testing. Why not just do
> things the simple and straightforward way and get rid of slotint
> completely?
>
> -- PMM
>

Linux seems to ignore SLOTINT status register, probably because it only 
supports single slot configuration while SLOTINT really required for 
multislot controllers only, so I think we can remove it completely and 
simply return 0 on reads. Same for status of WAKCON register, nobody 
cares about controller's wakeup functionality. Then update irq function 
could be simplified to

  "qemu_set_irq(s->irq, (s->norintsts & s->norintsigen) || (s->errintsts 
& s->errintsigen))"

I think thats how it was done originally. I'll send incremental patch to 
Peter off-list, if he and everyone else agree to handle interrupts this way.
Peter Maydell - July 17, 2012, 1:37 p.m.
On 17 July 2012 13:55, Igor Mitsyanko <i.mitsyanko@samsung.com> wrote:
> On 07/16/2012 09:13 PM, Peter Maydell wrote:
>> The IRQ handling code still looks really weird. I would expect
>> that the code would be:
>>    [code which updates various kinds of irq related state]
>>    sdhci_update_irq();
>>
>> where sdhci_update_irq() calls qemu_set_irq() based on the state.
>>
>> At the moment it looks as if you're using slotint as a cached value
>> of the expression
>> "((s->norintsts & s->norintsigen) || (s->errintsts & s->errintsigen) ||
>>   ((s->norintsts & SDHC_NIS_INSERT) && (s->wakcon & SDHC_WKUP_ON_INS)) ||
>>   ((s->norintsts & SDHC_NIS_REMOVE) && (s->wakcon & SDHC_WKUP_ON_RMV)))"
>>
>> [can these two ever have different values?] and also attempting to
>> shortcut by manually updating slotint in codepaths which change only
>> parts of the state which this expression is testing. Why not just do
>> things the simple and straightforward way and get rid of slotint
>> completely?

> Linux seems to ignore SLOTINT status register, probably because it only
> supports single slot configuration while SLOTINT really required for
> multislot controllers only, so I think we can remove it completely and
> simply return 0 on reads. Same for status of WAKCON register, nobody cares
> about controller's wakeup functionality. Then update irq function could be
> simplified to
>
>  "qemu_set_irq(s->irq, (s->norintsts & s->norintsigen) || (s->errintsts &
> s->errintsigen))"

We should be modelling what the hardware does, not just what Linux happens
to use.

I've now gone and found the SDHCI specification. (figure 1-6 and table 1-6
in the simplified spec v3.00 are relevant here.) What happens is that the
SLOTINT bit tracks the external interrupt line's state, and that interrupt
line is the logical combination of the various *sts/*sigen registers.
I would suggest two functions:

int sdhci_slotint(SDHCIState *s)
which just calculates and returns the external interrupt line state
(might be able to make this 'static'), and

void sdhci_update_irq(SDHCIState *s)
{
    qemu_set_irq(sdhci_slotint(s));
}

Then just call sdhci_update_irq() any time you update state that
can affect the interrupt line.

The 'read the SLOTINT register' implementation just calls
sdhci_slotint(), obviously.

This approach:
 * doesn't store any extra state in our state struct that the hardware
   doesn't also have as stored state
 * doesn't prematurely optimise the calculation of the interrupt line
   state, so it's nice and clear how the irq line works and that it
   follows the spec

-- PMM
Mitsyanko Igor - July 17, 2012, 2:58 p.m.
On 07/17/2012 05:37 PM, Peter Maydell wrote:
> On 17 July 2012 13:55, Igor Mitsyanko <i.mitsyanko@samsung.com> wrote:
>> On 07/16/2012 09:13 PM, Peter Maydell wrote:
>>> The IRQ handling code still looks really weird. I would expect
>>> that the code would be:
>>>     [code which updates various kinds of irq related state]
>>>     sdhci_update_irq();
>>>
>>> where sdhci_update_irq() calls qemu_set_irq() based on the state.
>>>
>>> At the moment it looks as if you're using slotint as a cached value
>>> of the expression
>>> "((s->norintsts & s->norintsigen) || (s->errintsts & s->errintsigen) ||
>>>    ((s->norintsts & SDHC_NIS_INSERT) && (s->wakcon & SDHC_WKUP_ON_INS)) ||
>>>    ((s->norintsts & SDHC_NIS_REMOVE) && (s->wakcon & SDHC_WKUP_ON_RMV)))"
>>>
>>> [can these two ever have different values?] and also attempting to
>>> shortcut by manually updating slotint in codepaths which change only
>>> parts of the state which this expression is testing. Why not just do
>>> things the simple and straightforward way and get rid of slotint
>>> completely?
>> Linux seems to ignore SLOTINT status register, probably because it only
>> supports single slot configuration while SLOTINT really required for
>> multislot controllers only, so I think we can remove it completely and
>> simply return 0 on reads. Same for status of WAKCON register, nobody cares
>> about controller's wakeup functionality. Then update irq function could be
>> simplified to
>>
>>   "qemu_set_irq(s->irq, (s->norintsts & s->norintsigen) || (s->errintsts &
>> s->errintsigen))"
> We should be modelling what the hardware does, not just what Linux happens
> to use.
>
> I've now gone and found the SDHCI specification. (figure 1-6 and table 1-6
> in the simplified spec v3.00 are relevant here.) What happens is that the
> SLOTINT bit tracks the external interrupt line's state, and that interrupt
> line is the logical combination of the various *sts/*sigen registers.
> I would suggest two functions:
>
> int sdhci_slotint(SDHCIState *s)
> which just calculates and returns the external interrupt line state
> (might be able to make this 'static'), and
>
> void sdhci_update_irq(SDHCIState *s)
> {
>      qemu_set_irq(sdhci_slotint(s));
> }
>
> Then just call sdhci_update_irq() any time you update state that
> can affect the interrupt line.
>
> The 'read the SLOTINT register' implementation just calls
> sdhci_slotint(), obviously.
>
> This approach:
>   * doesn't store any extra state in our state struct that the hardware
>     doesn't also have as stored state
>   * doesn't prematurely optimise the calculation of the interrupt line
>     state, so it's nice and clear how the irq line works and that it
>     follows the spec
>
> -- PMM
>

Ok, but I'd rather make sdhci_slotint() return uint8_t, to emphasize 
that this function returns
a value of hardware SLOTINT register.
Peter Maydell - July 17, 2012, 3:04 p.m.
On 17 July 2012 15:58, Igor Mitsyanko <i.mitsyanko@samsung.com> wrote:
> On 07/17/2012 05:37 PM, Peter Maydell wrote:
>> I would suggest two functions:
>>
>> int sdhci_slotint(SDHCIState *s)
>> which just calculates and returns the external interrupt line state
>> (might be able to make this 'static'), and

> Ok, but I'd rather make sdhci_slotint() return uint8_t, to emphasize that
> this function returns a value of hardware SLOTINT register.

That's fine; the thing I want to avoid is having by-hand updates
of slotint all over the code.

-- PMM
Peter A. G. Crosthwaite - July 18, 2012, 7:13 a.m.
Will merge Igors corrections into v6

Regards,
Peter

On Wed, Jul 18, 2012 at 1:04 AM, Peter Maydell <peter.maydell@linaro.org> wrote:
> On 17 July 2012 15:58, Igor Mitsyanko <i.mitsyanko@samsung.com> wrote:
>> On 07/17/2012 05:37 PM, Peter Maydell wrote:
>>> I would suggest two functions:
>>>
>>> int sdhci_slotint(SDHCIState *s)
>>> which just calculates and returns the external interrupt line state
>>> (might be able to make this 'static'), and
>
>> Ok, but I'd rather make sdhci_slotint() return uint8_t, to emphasize that
>> this function returns a value of hardware SLOTINT register.
>
> That's fine; the thing I want to avoid is having by-hand updates
> of slotint all over the code.
>
> -- PMM

Patch

diff --git a/hw/arm/Makefile.objs b/hw/arm/Makefile.objs
index 88ff47d..c3afb6e 100644
--- a/hw/arm/Makefile.objs
+++ b/hw/arm/Makefile.objs
@@ -11,6 +11,7 @@  obj-y += realview_gic.o realview.o arm_sysctl.o arm11mpcore.o a9mpcore.o
 obj-y += exynos4210_gic.o exynos4210_combiner.o exynos4210.o
 obj-y += exynos4_boards.o exynos4210_uart.o exynos4210_pwm.o
 obj-y += exynos4210_pmu.o exynos4210_mct.o exynos4210_fimd.o
+obj-y += exynos4210_sdhci.o
 obj-y += arm_l2x0.o
 obj-y += arm_mptimer.o a15mpcore.o
 obj-y += armv7m.o armv7m_nvic.o stellaris.o pl022.o stellaris_enet.o
diff --git a/hw/exynos4210.c b/hw/exynos4210.c
index 9c20b3f..1a2e3d9 100644
--- a/hw/exynos4210.c
+++ b/hw/exynos4210.c
@@ -56,6 +56,12 @@ 
 #define EXYNOS4210_EXT_COMBINER_BASE_ADDR   0x10440000
 #define EXYNOS4210_INT_COMBINER_BASE_ADDR   0x10448000
 
+/* SD/MMC host controllers SFR base addresses */
+#define EXYNOS4210_SDHC0_BASE_ADDR          0x12510000
+#define EXYNOS4210_SDHC1_BASE_ADDR          0x12520000
+#define EXYNOS4210_SDHC2_BASE_ADDR          0x12530000
+#define EXYNOS4210_SDHC3_BASE_ADDR          0x12540000
+
 /* PMU SFR base address */
 #define EXYNOS4210_PMU_BASE_ADDR            0x10020000
 
@@ -292,6 +298,20 @@  Exynos4210State *exynos4210_init(MemoryRegion *system_mem,
                            EXYNOS4210_UART3_FIFO_SIZE, 3, NULL,
                   s->irq_table[exynos4210_get_irq(EXYNOS4210_UART_INT_GRP, 3)]);
 
+    /*** SD/MMC host controllers ***/
+
+    sysbus_create_simple("exynos4210.sdhci", EXYNOS4210_SDHC0_BASE_ADDR,
+            s->irq_table[exynos4210_get_irq(29, 0)]);
+
+    sysbus_create_simple("exynos4210.sdhci", EXYNOS4210_SDHC1_BASE_ADDR,
+            s->irq_table[exynos4210_get_irq(29, 1)]);
+
+    sysbus_create_simple("exynos4210.sdhci", EXYNOS4210_SDHC2_BASE_ADDR,
+            s->irq_table[exynos4210_get_irq(29, 2)]);
+
+    sysbus_create_simple("exynos4210.sdhci", EXYNOS4210_SDHC3_BASE_ADDR,
+            s->irq_table[exynos4210_get_irq(29, 3)]);
+
     /*** Display controller (FIMD) ***/
     sysbus_create_varargs("exynos4210.fimd", EXYNOS4210_FIMD0_BASE_ADDR,
             s->irq_table[exynos4210_get_irq(11, 0)],
diff --git a/hw/exynos4210_sdhci.c b/hw/exynos4210_sdhci.c
new file mode 100644
index 0000000..4e87c0c
--- /dev/null
+++ b/hw/exynos4210_sdhci.c
@@ -0,0 +1,443 @@ 
+/*
+ * Samsung Exynos4210 SD/MMC host controller model
+ *
+ * Copyright (c) 2012 Samsung Electronics Co., Ltd.
+ * Mitsyanko Igor <i.mitsyanko@samsung.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "sdhci.h"
+
+#define EXYNOS4_SDHC_CAPABILITIES    0x05E80080
+#define EXYNOS4_SDHC_MAX_BUFSZ       512
+
+#define EXYNOS4_SDHC_DEBUG           0
+
+#if EXYNOS4_SDHC_DEBUG == 0
+    #define DPRINT_L1(fmt, args...)       do { } while (0)
+    #define DPRINT_L2(fmt, args...)       do { } while (0)
+    #define ERRPRINT(fmt, args...)        do { } while (0)
+#elif EXYNOS4_SDHC_DEBUG == 1
+    #define DPRINT_L1(fmt, args...)       \
+        do {fprintf(stderr, "QEMU SDHC: "fmt, ## args); } while (0)
+    #define DPRINT_L2(fmt, args...)       do { } while (0)
+    #define ERRPRINT(fmt, args...)        \
+        do {fprintf(stderr, "QEMU SDHC ERROR: "fmt, ## args); } while (0)
+#else
+    #define DPRINT_L1(fmt, args...)       \
+        do {fprintf(stderr, "QEMU SDHC: "fmt, ## args); } while (0)
+    #define DPRINT_L2(fmt, args...)       \
+        do {fprintf(stderr, "QEMU SDHC: "fmt, ## args); } while (0)
+    #define ERRPRINT(fmt, args...)        \
+        do {fprintf(stderr, "QEMU SDHC ERROR: "fmt, ## args); } while (0)
+#endif
+
+
+#define TYPE_EXYNOS4_SDHC            "exynos4210.sdhci"
+#define EXYNOS4_SDHCI(obj)           \
+     OBJECT_CHECK(Exynos4SDHCIState, (obj), TYPE_EXYNOS4_SDHC)
+
+/* ADMA Error Status Register */
+#define EXYNOS4_SDHC_FINAL_BLOCK     (1 << 10)
+#define EXYNOS4_SDHC_CONTINUE_REQ    (1 << 9)
+#define EXYNOS4_SDHC_IRQ_STAT        (1 << 8)
+/* Control register 2 */
+#define EXYNOS4_SDHC_CONTROL2        0x80
+#define EXYNOS4_SDHC_HWINITFIN       (1 << 0)
+#define EXYNOS4_SDHC_DISBUFRD        (1 << 6)
+#define EXYNOS4_SDHC_SDOPSIGPC       (1 << 12)
+#define EXYNOS4_SDHC_SDINPSIGPC      (1 << 3)
+/* Control register 3 */
+#define EXYNOS4_SDHC_CONTROL3        0x84
+/* Control register 4 */
+#define EXYNOS4_SDHC_CONTROL4        0x8C
+/* Clock control register */
+#define EXYNOS4_SDHC_SDCLK_STBL      (1 << 3)
+
+#define EXYNOS4_SDHC_CMD_USES_DAT(cmd)  \
+    (((cmd) & SDHC_CMD_DATA_PRESENT) || \
+    ((cmd) & SDHC_CMD_RESPONSE) == SDHC_CMD_RSP_WITH_BUSY)
+
+typedef struct Exynos4SDHCIState {
+    SDHCIState sdhci;
+
+    uint32_t admaerr;
+    uint32_t control2;
+    uint32_t control3;
+    bool stopped_adma;
+} Exynos4SDHCIState;
+
+static void exynos4210_sdhci_reset(DeviceState *d)
+{
+    Exynos4SDHCIState *s = EXYNOS4_SDHCI(d);
+
+    SDHCI_GET_CLASS(d)->reset(SDHCI(d));
+    s->stopped_adma = false;
+    s->admaerr = 0;
+    s->control2 = 0;
+    s->control3 = 0x7F5F3F1F;
+}
+
+static void exynos4210_sdhci_start_adma(SDHCIState *sdhci)
+{
+    Exynos4SDHCIState *s = EXYNOS4_SDHCI(sdhci);
+    unsigned int length, n, begin;
+    target_phys_addr_t entry_addr;
+    uint32_t addr;
+    uint8_t attributes;
+    const uint16_t block_size = sdhci->blksize & 0x0fff;
+    s->admaerr &=
+            ~(EXYNOS4_SDHC_FINAL_BLOCK | SDHC_ADMAERR_LENGTH_MISMATCH);
+
+    while (1) {
+        addr = length = attributes = 0;
+        entry_addr = (target_phys_addr_t)(sdhci->admasysaddr & 0xFFFFFFFFull);
+
+        /* fetch next entry from descriptor table */
+        cpu_physical_memory_read(entry_addr + 4, (uint8_t *)(&addr), 4);
+        cpu_physical_memory_read(entry_addr + 2, (uint8_t *)(&length), 2);
+        cpu_physical_memory_read(entry_addr, (uint8_t *)(&attributes), 1);
+        DPRINT_L1("ADMA loop: addr=0x%08x, len=%d, attr=%x\n",
+                addr, length, attributes);
+
+        if ((attributes & SDHC_ADMA_ATTR_VALID) == 0) {
+            /* Indicate that error occurred in ST_FDS state */
+            s->admaerr &= ~SDHC_ADMAERR_STATE_MASK;
+            s->admaerr |= SDHC_ADMAERR_STATE_ST_FDS;
+            DPRINT_L1("ADMA not valid at addr=0x%lx\n", sdhci->admasysaddr);
+
+            if (sdhci->errintstsen & SDHC_EISEN_ADMAERR) {
+                sdhci->errintsts |= SDHC_EIS_ADMAERR;
+                sdhci->norintsts |= SDHC_NIS_ERR;
+            }
+
+            if (sdhci->errintsigen & sdhci->errintsts) {
+                sdhci->slotint = 1;
+            }
+
+            qemu_set_irq(sdhci->irq, sdhci->slotint);
+            break;
+        }
+
+        if (length == 0) {
+            length = 65536;
+        }
+
+        addr &= 0xfffffffc;  /* minimum unit of addr is 4 byte */
+
+        switch (attributes & SDHC_ADMA_ATTR_ACT_MASK) {
+        case SDHC_ADMA_ATTR_ACT_TRAN:  /* data transfer */
+            if (sdhci->trnmod & SDHC_TRNS_READ) {
+                while (length) {
+                    if (sdhci->data_count == 0) {
+                        for (n = 0; n < block_size; n++) {
+                            sdhci->fifo_buffer[n] = sd_read_data(sdhci->card);
+                        }
+                    }
+                    begin = sdhci->data_count;
+                    if ((length + begin) < block_size) {
+                        sdhci->data_count = length + begin;
+                        length = 0;
+                     } else {
+                        sdhci->data_count = block_size;
+                        length -= block_size - begin;
+                    }
+                    cpu_physical_memory_write(addr, &sdhci->fifo_buffer[begin],
+                            sdhci->data_count - begin);
+                    addr += sdhci->data_count - begin;
+                    if (sdhci->data_count == block_size) {
+                        sdhci->data_count = 0;
+                        if (sdhci->trnmod & SDHC_TRNS_BLK_CNT_EN) {
+                            sdhci->blkcnt--;
+                            if (sdhci->blkcnt == 0) {
+                                break;
+                            }
+                        }
+                    }
+                }
+            } else {
+                while (length) {
+                    begin = sdhci->data_count;
+                    if ((length + begin) < block_size) {
+                        sdhci->data_count = length + begin;
+                        length = 0;
+                     } else {
+                        sdhci->data_count = block_size;
+                        length -= block_size - begin;
+                    }
+                    cpu_physical_memory_read(addr,
+                            &sdhci->fifo_buffer[begin], sdhci->data_count);
+                    addr += sdhci->data_count - begin;
+                    if (sdhci->data_count == block_size) {
+                        for (n = 0; n < block_size; n++) {
+                            sd_write_data(sdhci->card, sdhci->fifo_buffer[n]);
+                        }
+                        sdhci->data_count = 0;
+                        if (sdhci->trnmod & SDHC_TRNS_BLK_CNT_EN) {
+                            sdhci->blkcnt--;
+                            if (sdhci->blkcnt == 0) {
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+            sdhci->admasysaddr += 8;
+            break;
+        case SDHC_ADMA_ATTR_ACT_LINK:   /* link to next descriptor table */
+            sdhci->admasysaddr = addr;
+            DPRINT_L1("ADMA link: admasysaddr=0x%lx\n", sdhci->admasysaddr);
+            break;
+        default:
+            sdhci->admasysaddr += 8;
+            break;
+        }
+
+        /* ADMA transfer terminates if blkcnt == 0 or by END attribute */
+        if (((sdhci->trnmod & SDHC_TRNS_BLK_CNT_EN) && (sdhci->blkcnt == 0)) ||
+                (attributes & SDHC_ADMA_ATTR_END)) {
+            DPRINT_L2("ADMA transfer completed\n");
+            if (length || ((attributes & SDHC_ADMA_ATTR_END) &&
+               (sdhci->trnmod & SDHC_TRNS_BLK_CNT_EN) && sdhci->blkcnt != 0) ||
+               ((sdhci->trnmod & SDHC_TRNS_BLK_CNT_EN) && sdhci->blkcnt == 0 &&
+               (attributes & SDHC_ADMA_ATTR_END) == 0)) {
+                ERRPRINT("ADMA length mismatch\n");
+                s->admaerr |= SDHC_ADMAERR_LENGTH_MISMATCH |
+                        SDHC_ADMAERR_STATE_ST_TFR;
+                if (sdhci->errintstsen & SDHC_EISEN_ADMAERR) {
+                    sdhci->errintsts |= SDHC_EIS_ADMAERR;
+                    sdhci->norintsts |= SDHC_NIS_ERR;
+                }
+
+                if (sdhci->errintsigen & sdhci->errintsts) {
+                    sdhci->slotint = 1;
+                }
+
+                qemu_set_irq(sdhci->irq, sdhci->slotint);
+            }
+
+            s->admaerr |= EXYNOS4_SDHC_FINAL_BLOCK;
+            SDHCI_GET_CLASS(sdhci)->end_data_transfer(sdhci);
+            break;
+        }
+
+        if (attributes & SDHC_ADMA_ATTR_INT) {
+            DPRINT_L1("ADMA interrupt: addr=0x%lx\n", sdhci->admasysaddr);
+            s->admaerr |= EXYNOS4_SDHC_IRQ_STAT;
+            s->stopped_adma = true;
+            if (sdhci->norintstsen & SDHC_NISEN_DMA) {
+                sdhci->norintsts |= SDHC_NIS_DMA;
+            }
+            if (sdhci->norintsts & sdhci->norintsigen) {
+                sdhci->slotint = 1;
+            }
+            qemu_set_irq(sdhci->irq, sdhci->slotint);
+            break;
+        }
+    }
+}
+
+static bool exynos4210_sdhci_can_issue_command(SDHCIState *sdhci)
+{
+    Exynos4SDHCIState *s = EXYNOS4_SDHCI(sdhci);
+
+    /* Check that power is supplied and clock is enabled.
+     * If SDOPSIGPC and SDINPSIGPC bits in CONTROL2 register are not set, power
+     * is supplied regardless of the PWRCON register state */
+    if (!SDHC_CLOCK_IS_ON(sdhci->clkcon) || (!(sdhci->pwrcon & SDHC_POWER_ON) &&
+        (s->control2 & (EXYNOS4_SDHC_SDOPSIGPC | EXYNOS4_SDHC_SDINPSIGPC)))) {
+        return false;
+    }
+
+    /* Controller cannot issue a command which uses data line (unless its an
+     * ABORT command) if data line is currently busy */
+    if (((sdhci->prnsts & SDHC_DATA_INHIBIT) || sdhci->stopped_state) &&
+        (EXYNOS4_SDHC_CMD_USES_DAT(sdhci->cmdreg) &&
+        SDHC_COMMAND_TYPE(sdhci->cmdreg) != SDHC_CMD_ABORT)) {
+        return false;
+    }
+
+    return true;
+}
+
+static uint64_t
+exynos4210_sdhci_readfn(void *opaque, target_phys_addr_t offset, unsigned size)
+{
+    Exynos4SDHCIState *s = (Exynos4SDHCIState *)opaque;
+    uint32_t ret;
+
+    switch (offset & ~0x3) {
+    case SDHC_BDATA:
+        /* Buffer data port read can be disabled by CONTROL2 register */
+        if (s->control2 & EXYNOS4_SDHC_DISBUFRD) {
+            ret = 0;
+        } else {
+            ret = SDHCI_GET_CLASS(s)->mem_read(SDHCI(s), offset, size);
+        }
+        break;
+    case SDHC_ADMAERR:
+        ret = (s->admaerr >> 8 * (offset - SDHC_ADMAERR)) &
+                ((1 << 8 * size) - 1);
+        break;
+    case EXYNOS4_SDHC_CONTROL2:
+        ret = (s->control2 >> 8 * (offset - EXYNOS4_SDHC_CONTROL2)) &
+                ((1 << 8 * size) - 1);
+        break;
+    case EXYNOS4_SDHC_CONTROL3:
+        ret = (s->control3 >> 8 * (offset - EXYNOS4_SDHC_CONTROL3)) &
+                ((1 << 8 * size) - 1);
+        break;
+    case EXYNOS4_SDHC_CONTROL4:
+        ret = 0;
+        break;
+    default:
+        ret = SDHCI_GET_CLASS(s)->mem_read(SDHCI(s), offset, size);
+        break;
+    }
+
+    DPRINT_L2("read %ub: addr[0x%04x] -> %u(0x%x)\n", size, offset, ret, ret);
+    return ret;
+}
+
+static void exynos4210_sdhci_writefn(void *opaque, target_phys_addr_t offset,
+        uint64_t val, unsigned size)
+{
+    Exynos4SDHCIState *s = (Exynos4SDHCIState *)opaque;
+    SDHCIState *sdhci = SDHCI(s);
+    unsigned shift;
+
+    DPRINT_L2("write %ub: addr[0x%04x] <- %u(0x%x)\n", size, (uint32_t)offset,
+            (uint32_t)val, (uint32_t)val);
+
+    switch (offset) {
+    case SDHC_CLKCON:
+        if ((val & SDHC_CLOCK_SDCLK_EN) &&
+                (sdhci->prnsts & SDHC_CARD_PRESENT)) {
+            val |= EXYNOS4_SDHC_SDCLK_STBL;
+        } else {
+            val &= ~EXYNOS4_SDHC_SDCLK_STBL;
+        }
+        /* Break out to superclass write to handle the rest of this register */
+        break;
+    case EXYNOS4_SDHC_CONTROL2 ... EXYNOS4_SDHC_CONTROL2 + 3:
+        shift = (offset - EXYNOS4_SDHC_CONTROL2) * 8;
+        s->control2 = (s->control2 & ~(((1 << 8 * size) - 1) << shift)) |
+                (val << shift);
+        return;
+    case EXYNOS4_SDHC_CONTROL3 ... EXYNOS4_SDHC_CONTROL3 + 3:
+        shift = (offset - EXYNOS4_SDHC_CONTROL2) * 8;
+        s->control3 = (s->control3 & ~(((1 << 8 * size) - 1) << shift)) |
+                (val << shift);
+        return;
+    case SDHC_ADMAERR ... SDHC_ADMAERR + 3:
+        if (size == 4 || (size == 2 && offset == SDHC_ADMAERR) ||
+                (size == 1 && offset == (SDHC_ADMAERR + 1))) {
+            uint32_t mask = 0;
+
+            if (size == 2) {
+                mask = 0xFFFF0000;
+            } else if (size == 1) {
+                mask = 0xFFFF00FF;
+                val <<= 8;
+            }
+
+            s->admaerr = (s->admaerr & (mask | EXYNOS4_SDHC_FINAL_BLOCK |
+               EXYNOS4_SDHC_IRQ_STAT)) | (val & ~(EXYNOS4_SDHC_FINAL_BLOCK |
+               EXYNOS4_SDHC_IRQ_STAT | EXYNOS4_SDHC_CONTINUE_REQ));
+            s->admaerr &= ~(val & EXYNOS4_SDHC_IRQ_STAT);
+            if ((s->stopped_adma) && (val & EXYNOS4_SDHC_CONTINUE_REQ) &&
+                (SDHC_DMA_TYPE(sdhci->hostctl) == SDHC_CTRL_ADMA2_32)) {
+                s->stopped_adma = false;
+                SDHCI_GET_CLASS(sdhci)->do_adma(sdhci);
+            }
+        } else {
+            uint32_t mask = (1 << (size * 8)) - 1;
+            shift = 8 * (offset & 0x3);
+            val <<= shift;
+            mask = ~(mask << shift);
+            s->admaerr = (s->admaerr & mask) | val;
+        }
+        return;
+    }
+
+    SDHCI_GET_CLASS(s)->mem_write(sdhci, offset, val, size);
+}
+
+static const MemoryRegionOps exynos4210_sdhci_mmio_ops = {
+    .read = exynos4210_sdhci_readfn,
+    .write = exynos4210_sdhci_writefn,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = false
+    },
+    .endianness = DEVICE_LITTLE_ENDIAN,
+};
+
+static const VMStateDescription exynos4210_sdhci_vmstate = {
+    .name = "exynos4210.sdhci",
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (VMStateField[]) {
+        VMSTATE_STRUCT(sdhci, Exynos4SDHCIState, 1, sdhci_vmstate, SDHCIState),
+        VMSTATE_UINT32(admaerr, Exynos4SDHCIState),
+        VMSTATE_UINT32(control2, Exynos4SDHCIState),
+        VMSTATE_UINT32(control3, Exynos4SDHCIState),
+        VMSTATE_BOOL(stopped_adma, Exynos4SDHCIState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static int exynos4210_sdhci_realize(SysBusDevice *busdev)
+{
+    SDHCIState *sdhci = SDHCI(busdev);
+
+    qdev_prop_set_uint32(DEVICE(busdev), "capareg", EXYNOS4_SDHC_CAPABILITIES);
+    sdhci->buf_maxsz = EXYNOS4_SDHC_MAX_BUFSZ;
+    sdhci->fifo_buffer = g_malloc0(sdhci->buf_maxsz);
+    sysbus_init_irq(busdev, &sdhci->irq);
+    memory_region_init_io(&sdhci->iomem, &exynos4210_sdhci_mmio_ops,
+            EXYNOS4_SDHCI(sdhci), "exynos4210.sdhci", SDHC_REGISTERS_MAP_SIZE);
+    sysbus_init_mmio(busdev, &sdhci->iomem);
+    return 0;
+}
+
+static void exynos4210_sdhci_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    SysBusDeviceClass *sbdc = SYS_BUS_DEVICE_CLASS(klass);
+    SDHCIClass *k = SDHCI_CLASS(klass);
+
+    dc->vmsd = &exynos4210_sdhci_vmstate;
+    dc->reset = exynos4210_sdhci_reset;
+    sbdc->init = exynos4210_sdhci_realize;
+
+    k->can_issue_command = exynos4210_sdhci_can_issue_command;
+    k->do_adma = exynos4210_sdhci_start_adma;
+}
+
+static const TypeInfo exynos4210_sdhci_type_info = {
+    .name = TYPE_EXYNOS4_SDHC,
+    .parent = TYPE_SDHCI,
+    .instance_size = sizeof(Exynos4SDHCIState),
+    .class_init = exynos4210_sdhci_class_init,
+};
+
+static void exynos4210_sdhci_register_types(void)
+{
+    type_register_static(&exynos4210_sdhci_type_info);
+}
+
+type_init(exynos4210_sdhci_register_types)