From patchwork Fri Dec 9 13:34:41 2011 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Evgeny Voevodin X-Patchwork-Id: 130390 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [140.186.70.17]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (Client did not present a certificate) by ozlabs.org (Postfix) with ESMTPS id BA8A11007DC for ; Sat, 10 Dec 2011 01:23:20 +1100 (EST) Received: from localhost ([::1]:40250 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RZ0cT-00068E-8h for incoming@patchwork.ozlabs.org; Fri, 09 Dec 2011 08:36:09 -0500 Received: from eggs.gnu.org ([140.186.70.92]:34623) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RZ0bz-0005G3-GE for qemu-devel@nongnu.org; Fri, 09 Dec 2011 08:35:44 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1RZ0bo-0000b8-MH for qemu-devel@nongnu.org; Fri, 09 Dec 2011 08:35:39 -0500 Received: from mailout4.w1.samsung.com ([210.118.77.14]:31170) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1RZ0bn-0000as-Ru for qemu-devel@nongnu.org; Fri, 09 Dec 2011 08:35:28 -0500 MIME-version: 1.0 Content-transfer-encoding: 7BIT Content-type: TEXT/PLAIN Received: from euspt2 ([210.118.77.14]) by mailout4.w1.samsung.com (Sun Java(tm) System Messaging Server 6.3-8.04 (built Jul 29 2009; 32bit)) with ESMTP id <0LVX004XLV32GD50@mailout4.w1.samsung.com> for qemu-devel@nongnu.org; Fri, 09 Dec 2011 13:35:26 +0000 (GMT) Received: from evvoevodinPC.rnd.samsung.ru ([106.109.8.48]) by spt2.w1.samsung.com (iPlanet Messaging Server 5.2 Patch 2 (built Jul 14 2004)) with ESMTPA id <0LVX00MANV28B5@spt2.w1.samsung.com> for qemu-devel@nongnu.org; Fri, 09 Dec 2011 13:35:26 +0000 (GMT) Date: Fri, 09 Dec 2011 17:34:41 +0400 From: Evgeny Voevodin In-reply-to: <1323437682-28792-1-git-send-email-e.voevodin@samsung.com> To: qemu-devel@nongnu.org Message-id: <1323437682-28792-15-git-send-email-e.voevodin@samsung.com> X-Mailer: git-send-email 1.7.4.1 References: <1323437682-28792-1-git-send-email-e.voevodin@samsung.com> X-detected-operating-system: by eggs.gnu.org: Solaris 9.1 X-Received-From: 210.118.77.14 Cc: m.kozlov@samsung.com, Mitsyanko Igor , d.solodkiy@samsung.com, Evgeny Voevodin Subject: [Qemu-devel] [PATCH 14/15] ARM: exynos4210: added SD/MMC host controller X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org From: Mitsyanko Igor Signed-off-by: Evgeny Voevodin --- Makefile.target | 3 +- hw/exynos4210.c | 20 + hw/exynos4210_sdhc.c | 1666 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1688 insertions(+), 1 deletions(-) create mode 100644 hw/exynos4210_sdhc.c diff --git a/Makefile.target b/Makefile.target index 691582b..a5d67c9 100644 --- a/Makefile.target +++ b/Makefile.target @@ -345,7 +345,8 @@ obj-arm-y += arm_boot.o pl011.o pl031.o pl050.o pl080.o pl110.o pl181.o pl190.o obj-arm-y += versatile_pci.o obj-arm-y += realview_gic.o realview.o arm_sysctl.o arm11mpcore.o a9mpcore.o obj-arm-y += exynos4210.o exynos4210_cmu.o exynos4210_uart.o exynos4210_gic.o \ - exynos4210_combiner.o exynos4210_pwm.o exynos4210_mct.o + exynos4210_combiner.o exynos4210_pwm.o exynos4210_mct.o \ + exynos4210_sdhc.o obj-arm-y += armv7m.o armv7m_nvic.o stellaris.o pl022.o stellaris_enet.o obj-arm-y += pl061.o obj-arm-y += arm-semi.o diff --git a/hw/exynos4210.c b/hw/exynos4210.c index 5b18b68..62c90e9 100644 --- a/hw/exynos4210.c +++ b/hw/exynos4210.c @@ -95,6 +95,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 + /* Secondary CPU polling address to get loader start from */ #define EXYNOS4210_SECOND_CPU_BOOTREG 0x10020814 @@ -448,6 +454,20 @@ static void exynos4210_init(ram_addr_t ram_size, exynos4210_uart_create(addr, fifo_size, channel, NULL, uart_irq); } + /*** SD/MMC host controllers ***/ + + sysbus_create_simple("exynos4210.sdhc", EXYNOS4210_SDHC0_BASE_ADDR, + irq_table[exynos4210_get_irq(29, 0)]); + + sysbus_create_simple("exynos4210.sdhc", EXYNOS4210_SDHC1_BASE_ADDR, + irq_table[exynos4210_get_irq(29, 1)]); + + sysbus_create_simple("exynos4210.sdhc", EXYNOS4210_SDHC2_BASE_ADDR, + irq_table[exynos4210_get_irq(29, 2)]); + + sysbus_create_simple("exynos4210.sdhc", EXYNOS4210_SDHC3_BASE_ADDR, + irq_table[exynos4210_get_irq(29, 3)]); + /*** LAN adapter: this should be a 9215 but the 9118 is close enough ***/ if (board_type == BOARD_EXYNOS4210_SMDKC210) { for (n = 0; n < nb_nics; n++) { diff --git a/hw/exynos4210_sdhc.c b/hw/exynos4210_sdhc.c new file mode 100644 index 0000000..05dd05c --- /dev/null +++ b/hw/exynos4210_sdhc.c @@ -0,0 +1,1666 @@ +/* + * Samsung exynos4210 SD/MMC host controller + * (SD host controller specification ver. 2.0 compliant) + * + * Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. + * All rights reserved. + * Contributed by Mitsyanko Igor + * + * Based on MMC controller for Samsung S5PC1xx-based board emulation + * by Alexey Merkulov and Vladimir Monakhov. + * + * 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, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "hw.h" +#include "sd.h" +#include "qemu-common.h" +#include "block_int.h" +#include "blockdev.h" +#include "sysbus.h" +#include "qemu-timer.h" + +/* host controller debug messages */ +#define DEBUG_SDHC 0 + +#if DEBUG_SDHC +#define DPRINTF(fmt, args...) \ +do { fprintf(stderr, "QEMU SDHC: " fmt , ##args); } while (0) +#else +#define DPRINTF(fmt, args...) do {} while (0) +#endif + +#define TARGET_WORD_SIZE (TARGET_PHYS_ADDR_SPACE_BITS / 8) +#define READ_BUFFER_DELAY 5 +#define WRITE_BUFFER_DELAY 6 +#define INSERTION_DELAY (get_ticks_per_sec()) +#define CMD_RESPONSE (3 << 0) + +/* Default SD/MMC host controller features information, which will be + * presented in host's CAPABILITIES REGISTER at reset. + * If not stated otherwise: + * 0 - not supported, 1 - supported, other - prohibited. + */ +#define SDHC_CAPAB_64BITBUS 0ul /* 64-bit System Bus Support */ +#define SDHC_CAPAB_18V 1ul /* Voltage support 1.8v */ +#define SDHC_CAPAB_30V 0ul /* Voltage support 3.0v */ +#define SDHC_CAPAB_33V 1ul /* Voltage support 3.3v */ +#define SDHC_CAPAB_SUSPRESUME 0ul /* Suspend/resume support */ +#define SDHC_CAPAB_SDMA 1ul /* SDMA support */ +#define SDHC_CAPAB_HIGHSPEED 1ul /* High speed support */ +#define SDHC_CAPAB_ADMA 1ul /* ADMA2 support */ +/* Maximum host controller R/W buffers size + * Possible values: 512, 1024, 2048 bytes */ +#define SDHC_CAPAB_MAXBLOCKLENGTH 512ul +/* Maximum clock frequency for SDclock in MHz + * value in range 10-63 MHz, 0 - not defined */ +#define SDHC_CAPAB_BASECLKFREQ 0ul +#define SDHC_CAPAB_TOUNIT 1ul /* Timeout clock unit 0 - kHz, 1 - MHz */ +/* Timeout clock frequency 1-63, 0 - not defined */ +#define SDHC_CAPAB_TOCLKFREQ 0ul + +/* Now check all parameters and calculate CAPABILITIES REGISTER value */ +#if SDHC_CAPAB_64BITBUS > 1 || SDHC_CAPAB_18V > 1 || SDHC_CAPAB_30V > 1 ||\ + SDHC_CAPAB_33V > 1 || SDHC_CAPAB_SUSPRESUME > 1 || SDHC_CAPAB_SDMA > 1 ||\ + SDHC_CAPAB_HIGHSPEED > 1 || SDHC_CAPAB_ADMA > 1 || SDHC_CAPAB_TOUNIT > 1 +#error Capabilities features SDHC_CAPAB_x must have value 0 or 1! +#endif + +#if SDHC_CAPAB_MAXBLOCKLENGTH == 512 +#define MAX_BLOCK_LENGTH 0ul +#elif SDHC_CAPAB_MAXBLOCKLENGTH == 1024 +#define MAX_BLOCK_LENGTH 1ul +#elif SDHC_CAPAB_MAXBLOCKLENGTH == 2048 +#define MAX_BLOCK_LENGTH 2ul +#else +#error Max host controller block size can have value 512, 1024 or 2048 only! +#endif + +#if (SDHC_CAPAB_BASECLKFREQ > 0 && SDHC_CAPAB_BASECLKFREQ < 10) || \ + SDHC_CAPAB_BASECLKFREQ > 63 +#error SDclock frequency can have value in range 0, 10-63 only! +#endif + +#if SDHC_CAPAB_TOCLKFREQ > 63 +#error Timeout clock frequency can have value in range 0-63 only! +#endif + +#define SDHC_CAPAB_REG_DEFAULT ((SDHC_CAPAB_64BITBUS<<28)|(SDHC_CAPAB_18V<<26)|\ + (SDHC_CAPAB_30V<<25)|(SDHC_CAPAB_33V<<24)|(SDHC_CAPAB_SUSPRESUME<<23)|\ + (SDHC_CAPAB_SDMA<<22)|(SDHC_CAPAB_HIGHSPEED<<21)|(SDHC_CAPAB_ADMA<<19)|\ + (MAX_BLOCK_LENGTH<<16)|(SDHC_CAPAB_BASECLKFREQ<<8)|(SDHC_CAPAB_TOUNIT<<7)|\ + (SDHC_CAPAB_TOCLKFREQ)) + +/**************************************************** + * SD host controller registers offsets + ***************************************************/ + +/* R/W SDMA System Address register 0x0 */ +#define SDHC_SYSAD 0x00 + +/* R/W Host DMA Buffer Boundary and Transfer Block Size Register 0x0 */ +#define SDHC_BLKSIZE 0x04 + +/* R/W Blocks count for current transfer 0x0 */ +#define SDHC_BLKCNT 0x06 + +/* R/W Command Argument Register 0x0 */ +#define SDHC_ARGUMENT 0x08 + +/* R/W Transfer Mode Setting Register 0x0 */ +#define SDHC_TRNMOD 0x0C +#define SDHC_TRNS_DMA 0x0001 +#define SDHC_TRNS_BLK_CNT_EN 0x0002 +#define SDHC_TRNS_ACMD12 0x0004 +#define SDHC_TRNS_READ 0x0010 +#define SDHC_TRNS_MULTI 0x0020 +#define SDHC_TRNS_CEATA 0x0300 +#define SDHC_TRNS_BOOTCMD 0x1000 +#define SDHC_TRNS_BOOTACK 0x2000 + +/* R/W Command Register 0x0 */ +#define SDHC_CMDREG 0x0E +#define SDHC_CMD_RSP_WITH_BUSY (3 << 0) +#define SDHC_CMD_DATA_PRESENT (1 << 5) +#define SDHC_CMD_SUSPEND_MASK (1 << 6) +#define SDHC_CMD_RESUME_MASK (1 << 7) +#define SDHC_CMD_ABORT_MASK ((1 << 6)|(1 << 7)) +#define SDHC_CMD_TYPE_MASK ((1 << 6)|(1 << 7)) + +/* ROC Response Register 0 0x0 */ +#define SDHC_RSPREG0 0x10 +/* ROC Response Register 1 0x0 */ +#define SDHC_RSPREG1 0x14 +/* ROC Response Register 2 0x0 */ +#define SDHC_RSPREG2 0x18 +/* ROC Response Register 3 0x0 */ +#define SDHC_RSPREG3 0x1C +#define SDHC_CMD_RESP_MASK 0x00000003 +#define SDHC_CMD_CRC 0x00000008 +#define SDHC_CMD_INDEX 0x00000010 +#define SDHC_CMD_DATA 0x00000020 +#define SDHC_CMD_RESP_NONE 0x00000000 +#define SDHC_CMD_RESP_LONG 0x00000001 +#define SDHC_CMD_RESP_SHORT 0x00000002 +#define SDHC_CMD_RESP_SHORT_BUSY 0x00000003 +#define SDHC_MAKE_CMD(c, f) (((c & 0xFF) << 8) | (f & 0xFF)) + +/* R/W Buffer Data Register 0x0 */ +#define SDHC_BDATA 0x20 + +/* R/ROC Present State Register 0x000A0000 */ +#define SDHC_PRNSTS 0x24 +#define SDHC_CMD_INHIBIT 0x00000001 +#define SDHC_DATA_INHIBIT 0x00000002 +#define SDHC_DAT_LINE_ACTIVE 0x00000004 +#define SDHC_DOING_WRITE 0x00000100 +#define SDHC_DOING_READ 0x00000200 +#define SDHC_SPACE_AVAILABLE 0x00000400 +#define SDHC_DATA_AVAILABLE 0x00000800 +#define SDHC_CARD_PRESENT 0x00010000 +#define SDHC_WRITE_PROTECT 0x00080000 + +/* R/W Host control Register 0x0 */ +#define SDHC_HOSTCTL 0x28 +#define SDHC_CTRL_LED 0x01 +#define SDHC_CTRL_4BITBUS 0x02 +#define SDHC_CTRL_HIGHSPEED 0x04 +#define SDHC_CTRL_1BIT 0x00 +#define SDHC_CTRL_4BIT 0x02 +#define SDHC_CTRL_8BIT 0x20 +#define SDHC_CTRL_DMA_CHECK_MASK 0x18 +#define SDHC_CTRL_SDMA 0x00 +#define SDHC_CTRL_ADMA2_32 0x10 + +/* R/W Power Control Register 0x0 */ +#define SDHC_PWRCON 0x29 +#define SDHC_POWER_OFF 0x00 +#define SDHC_POWER_ON 0x01 +#define SDHC_POWER_180 0x0A +#define SDHC_POWER_300 0x0C +#define SDHC_POWER_330 0x0E +#define SDHC_POWER_ON_ALL 0xFF + +/* R/W Block Gap Control Register 0x0 */ +#define SDHC_BLKGAP 0x2A +#define SDHC_STOP_AT_GAP_REQ 0x01 +#define SDHC_CONTINUE_REQ 0x02 + +/* R/W WakeUp Control Register 0x0 */ +#define SDHC_WAKCON 0x2B +#define SDHC_STAWAKEUP 0x08 + +/* + * CLKCON + * SELFREQ[15:8] : base clock divided by value + * ENSDCLK[2] : SD Clock Enable + * STBLINTCLK[1] : Internal Clock Stable + * ENINTCLK[0] : Internal Clock Enable + */ +#define SDHC_CLKCON 0x2C +#define SDHC_DIVIDER_SHIFT 0x0008 +#define SDHC_CLOCK_EXT_STABLE 0x0008 +#define SDHC_CLOCK_CARD_EN 0x0004 +#define SDHC_CLOCK_INT_STABLE 0x0002 +#define SDHC_CLOCK_INT_EN 0x0001 +#define SDHC_CLOCK_CHK_MASK 0x000F + +/* R/W Timeout Control Register 0x0 */ +#define SDHC_TIMEOUTCON 0x2E +#define SDHC_TIMEOUT_MAX 0x0E + +/* R/W Software Reset Register 0x0 */ +#define SDHC_SWRST 0x2F +#define SDHC_RESET_ALL 0x01 +#define SDHC_RESET_CMD 0x02 +#define SDHC_RESET_DATA 0x04 + +/* ROC/RW1C Normal Interrupt Status Register 0x0 */ +#define SDHC_NORINTSTS 0x30 +#define SDHC_NIS_ERR 0x8000 +#define SDHC_NIS_CMDCMP 0x0001 +#define SDHC_NIS_TRSCMP 0x0002 +#define SDHC_NIS_BLKGAP 0x0004 +#define SDHC_NIS_DMA 0x0008 +#define SDHC_NIS_WBUFRDY 0x0010 +#define SDHC_NIS_RBUFRDY 0x0020 +#define SDHC_NIS_INSERT 0x0040 +#define SDHC_NIS_REMOVE 0x0080 +#define SDHC_NIS_CARDINT 0x0100 + +/* ROC/RW1C Error Interrupt Status Register 0x0 */ +#define SDHC_ERRINTSTS 0x32 +#define SDHC_EIS_CMDTIMEOUT 0x0001 +#define SDHC_EIS_BLKGAP 0x0004 +#define SDHC_EIS_CMDERR 0x000E +#define SDHC_EIS_DATATIMEOUT 0x0010 +#define SDHC_EIS_DATAERR 0x0060 +#define SDHC_EIS_CMD12ERR 0x0100 +#define SDHC_EIS_ADMAERR 0x0200 +#define SDHC_EIS_STABOOTACKERR 0x0400 + +/* R/W Normal Interrupt Status Enable Register 0x0 */ +#define SDHC_NORINTSTSEN 0x34 +#define SDHC_NISEN_CMDCMP 0x0001 +#define SDHC_NISEN_TRSCMP 0x0002 +#define SDHC_NISEN_DMA 0x0008 +#define SDHC_NISEN_WBUFRDY 0x0010 +#define SDHC_NISEN_RBUFRDY 0x0020 +#define SDHC_NISEN_INSERT 0x0040 +#define SDHC_NISEN_REMOVE 0x0080 +#define SDHC_NISEN_CARDINT 0x0100 + +/* R/W Error Interrupt Status Enable Register 0x0 */ +#define SDHC_ERRINTSTSEN 0x36 +#define SDHC_EISEN_CMDTIMEOUT 0x0001 +#define SDHC_EISEN_BLKGAP 0x0004 +#define SDHC_EISEN_ADMAERR 0x0200 +#define SDHC_EISEN_STABOOTACKERR 0x0400 + +/* R/W Normal Interrupt Signal Enable Register 0x0 */ +#define SDHC_NORINTSIGEN 0x38 +#define SDHC_NISSIG_MASK_ALL 0x00 +#define SDHC_NISSIG_RESPONSE 0x0001 +#define SDHC_NISSIG_DATA_END 0x0002 +#define SDHC_NISSIG_DMA_END 0x0008 +#define SDHC_NISSIG_SPACE_AVAIL 0x0010 +#define SDHC_NISSIG_DATA_AVAIL 0x0020 +#define SDHC_NISSIG_CARD_INSERT 0x0040 +#define SDHC_NISSIG_CARD_REMOVE 0x0080 +#define SDHC_NISSIG_CARD_CHANGE 0x00C0 +#define SDHC_NISSIG_CARD_INT 0x0100 + +/* R/W Error Interrupt Signal Enable Register 0x0 */ +#define SDHC_ERRINTSIGEN 0x3A +#define SDHC_EISSIG_STABOOTACKERR 0x0400 +#define SDHC_EISSIG_ADMAERR 0x0200 + +/* ROC Auto CMD12 error status register 0x0 */ +#define SDHC_ACMD12ERRSTS 0x3C + +/* HWInit Capabilities Register 0x05E80080 */ +#define SDHC_CAPAREG 0x40 +#define SDHC_TIMEOUT_CLK_MASK 0x0000003F +#define SDHC_TIMEOUT_CLK_SHIFT 0x0 +#define SDHC_TIMEOUT_CLK_UNIT 0x00000080 +#define SDHC_CLOCK_BASE_MASK 0x00003F00 +#define SDHC_CLOCK_BASE_SHIFT 0x8 +#define SDHC_MAX_BLOCK_MASK 0x00030000 +#define SDHC_MAX_BLOCK_SHIFT 0x10 +#define SDHC_CAN_DO_DMA 0x00400000 +#define SDHC_CAN_DO_ADMA2 0x00080000 +#define SDHC_CAN_VDD_330 0x01000000 +#define SDHC_CAN_VDD_300 0x02000000 +#define SDHC_CAN_VDD_180 0x04000000 + +/* HWInit Maximum Current Capabilities Register 0x0 */ +#define SDHC_MAXCURR 0x48 + +/* For ADMA2 */ + +/* W Force Event Auto CMD12 Error Interrupt Register 0x0000 */ +#define SDHC_FEAER 0x50 +/* W Force Event Error Interrupt Register Error Interrupt 0x0000 */ +#define SDHC_FEERR 0x52 + +/* R/W ADMA Error Status Register 0x00 */ +#define SDHC_ADMAERR 0x54 +#define SDHC_ADMAERR_FINAL_BLOCK (1 << 10) +#define SDHC_ADMAERR_CONTINUE_REQUEST (1 << 9) +#define SDHC_ADMAERR_INTRRUPT_STATUS (1 << 8) +#define SDHC_ADMAERR_LENGTH_MISMATCH (1 << 2) +#define SDHC_ADMAERR_STATE_ST_STOP (0 << 0) +#define SDHC_ADMAERR_STATE_ST_FDS (1 << 0) +#define SDHC_ADMAERR_STATE_ST_TFR (3 << 0) +#define SDHC_ADMAERR_STATE_MASK (3 << 0) + +/* R/W ADMA System Address Register 0x00 */ +#define SDHC_ADMASYSADDR 0x58 +#define SDHC_ADMA_ATTR_MSK 0x3F +#define SDHC_ADMA_ATTR_ACT_NOP (0 << 4) +#define SDHC_ADMA_ATTR_ACT_RSV (1 << 4) +#define SDHC_ADMA_ATTR_ACT_TRAN (1 << 5) +#define SDHC_ADMA_ATTR_ACT_LINK (3 << 4) +#define SDHC_ADMA_ATTR_INT (1 << 2) +#define SDHC_ADMA_ATTR_END (1 << 1) +#define SDHC_ADMA_ATTR_VALID (1 << 0) +#define SDHC_ADMA_ATTR_ACT_MASK ((1 << 4)|(1 << 5)) + +/* R/W Control register 2 0x0 */ +#define SDHC_CONTROL2 0x80 +#define SDHC_HWINITFIN 0x00000001 +#define SDHC_SDOPSIGPC 0x00001000 +#define SDHC_ENCLKOUTHOLD 0x00000100 +#define SDHC_SDINPSIGPC 0x00000008 +#define SDHC_DISBUFRD 0x00000040 +#define SDHC_ENCMDCNFMSK 0x40000000 + +/* R/W FIFO Interrupt Control (Control Register 3) 0x7F5F3F1F */ +#define SDHC_CONTROL3 0x84 + +/* R/W Control register 4 0x0 */ +#define SDHC_CONTROL4 0x8C + +/* Magic register which is used from kernel! */ +#define SDHC_SLOT_INT_STATUS 0xFC + +/* HWInit Host Controller Version Register 0x0401 */ +#define SDHC_HCVER 0xFE +#define SDHC_VENDOR_VER_MASK 0xFF00 +#define SDHC_VENDOR_VER_SHIFT 0x8 +#define SDHC_SPEC_VER_MASK 0x00FF +#define SDHC_SPEC_VER_SHIFT 0x0 + +#define SDHC_REG_SIZE 0x100 + +enum { + not_stoped = 0, /* normal SDHC state */ + gap_read = 1, /* SDHC stopped at block gap during read operation */ + gap_write = 2, /* SDHC stopped at block gap during write operation */ + adma_intr = 3 /* SDHC stopped after ADMA interrupt occurred */ +}; + +/* SD/MMC host controller state */ +typedef struct Exynos4210SDHCState { + SysBusDevice busdev; + MemoryRegion iomem; + SDState *card; + + QEMUTimer *insert_timer; /* timer for 'changing' sd card. */ + QEMUTimer *read_buffer_timer; /* read block of data to controller FIFO */ + QEMUTimer *write_buffer_timer; /* write block of data to card from FIFO */ + QEMUTimer *transfer_complete_timer; /* raise transfer complete irq */ + qemu_irq eject; + qemu_irq irq; + + uint32_t sdmasysad; /* SDMA System Address register */ + uint16_t blksize; /* Host DMA Buff Boundary and Transfer BlkSize Reg */ + uint16_t blkcnt; /* Blocks count for current transfer */ + uint32_t argument; /* Command Argument Register */ + uint16_t trnmod; /* Transfer Mode Setting Register */ + uint16_t cmdreg; /* Command Register */ + uint32_t rspreg[4]; /* Response Registers 0-3 */ +/* 32 bit bdata Buffer Data Register - access point to R and W buffers */ + uint32_t prnsts; /* Present State Register */ + uint8_t hostctl; /* Present State Register */ + uint8_t pwrcon; /* Present State Register */ + uint8_t blkgap; /* Block Gap Control Register */ + uint8_t wakcon; /* WakeUp Control Register */ + uint16_t clkcon; /* Command Register */ + uint8_t timeoutcon; /* Timeout Control Register */ + uint8_t stoped_state; /* Current SDHC state */ + /* SD host i/o FIFO buffer */ + uint32_t fifo_buffer[SDHC_CAPAB_MAXBLOCKLENGTH / TARGET_WORD_SIZE]; + uint16_t data_count; /* current element in FIFO buffer */ +/* 8 bit swrst Software Reset Register - read as 0 */ + uint16_t norintsts; /* Normal Interrupt Status Register */ + uint16_t errintsts; /* Error Interrupt Status Register */ + uint16_t norintstsen; /* Normal Interrupt Status Enable Register */ + uint16_t errintstsen; /* Error Interrupt Status Enable Register */ + uint16_t norintsigen; /* Normal Interrupt Signal Enable Register */ + uint16_t errintsigen; /* Error Interrupt Signal Enable Register */ + uint16_t acmd12errsts; /* Auto CMD12 error status register */ + uint32_t capareg; /* Capabilities Register */ + uint32_t maxcurr; /* Maximum Current Capabilities Register */ +/* 16 bit feaer Force Event Auto CMD12 Error Interrupt Reg - write only */ +/* 16 bit feerr Force Event Error Interrupt Register- write only */ + uint32_t admaerr; /* ADMA Error Status Register */ + uint32_t admasysaddr; /* ADMA System Address Register */ + uint32_t control2; /* Control Register 2 */ + uint32_t control3; /* FIFO Interrupt Control (Control Register 3) */ +/* 32 bit control4 makes no sense in emulation so always return 0 */ +/* 32 bit hcver Host Controller Ver Reg 0x2401 -SD specification v2 */ +} Exynos4210SDHCState; + +static void sdhc_raise_transfer_complete_irq(void *opaque) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + + /* free data transfer line */ + s->prnsts &= ~(SDHC_DOING_READ | SDHC_DOING_WRITE | + SDHC_DAT_LINE_ACTIVE | SDHC_DATA_INHIBIT | + SDHC_SPACE_AVAILABLE | SDHC_DATA_AVAILABLE); + + if (s->norintstsen & SDHC_NISEN_TRSCMP) { + s->norintsts |= SDHC_NIS_TRSCMP; + } + + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); +} + +/* raise command response received interrupt */ +static inline void sdhc_raise_response_recieved_irq(Exynos4210SDHCState *s) +{ + DPRINTF("raise IRQ response\n"); + + if (s->norintstsen & SDHC_NISEN_CMDCMP) { + s->norintsts |= SDHC_NIS_CMDCMP; + } + + qemu_set_irq(s->irq, ((s->norintsts & s->norintsigen) || + (s->errintsts & s->errintsigen))); +} + +static void sdhc_raise_insertion_irq(void *opaque) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + DPRINTF("raise IRQ response\n"); + + if (s->norintsts & SDHC_NIS_REMOVE) { + DPRINTF("sdhc_raise_insertion_irq: set timer!\n"); + qemu_mod_timer(s->insert_timer, + qemu_get_clock_ns(vm_clock) + INSERTION_DELAY); + } else { + DPRINTF("sdhc_raise_insertion_irq: raise irq!\n"); + if (s->norintstsen & SDHC_NIS_INSERT) { + s->norintsts |= SDHC_NIS_INSERT; + } + qemu_set_irq(s->irq, (s->norintsts & s->norintsigen)); + } +} + +static void sdhc_insert_eject(void *opaque, int irq, int level) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + DPRINTF("change card state: %s!\n", level ? "insert" : "eject"); + + if (s->norintsts & SDHC_NIS_REMOVE) { + if (level) { + DPRINTF("change card state: timer set!\n"); + qemu_mod_timer(s->insert_timer, + qemu_get_clock_ns(vm_clock) + INSERTION_DELAY); + } + } else { + if (level) { + s->prnsts = 0x1ff0000; + if (s->norintstsen & SDHC_NIS_INSERT) { + s->norintsts |= SDHC_NIS_INSERT; + } + } else { + s->prnsts = 0x1fa0000; + if (s->norintstsen & SDHC_NIS_REMOVE) { + s->norintsts |= SDHC_NIS_REMOVE; + } + } + qemu_set_irq(s->irq, (s->norintsts & s->norintsigen)); + } +} + +static void exynos4210_sdhc_reset(DeviceState *d) +{ + Exynos4210SDHCState *s = container_of(d, Exynos4210SDHCState, busdev.qdev); + unsigned long begin = (unsigned long)s + offsetof(Exynos4210SDHCState, + sdmasysad); + unsigned long len = ((unsigned long)s + offsetof(Exynos4210SDHCState, + control3)) - begin; + + /* Set all registers to 0 and then set appropriate values + * for hardware-initialize registers */ + memset((void *)begin, 0, len); + (s->card) ? (s->prnsts = 0x1ff0000) : (s->prnsts = 0x1fa0000); + s->stoped_state = not_stoped; + s->data_count = 0; + s->capareg = SDHC_CAPAB_REG_DEFAULT; + s->control3 = 0x7F5F3F1F; +} + +static void sdhc_send_command(Exynos4210SDHCState *s) +{ + SDRequest request; + uint8_t response[16]; + int rlen; + s->errintsts = 0; + if (!s->card) { + goto error; + } + + request.cmd = s->cmdreg >> 8; + request.arg = s->argument; + DPRINTF("Command %d %08x\n", request.cmd, request.arg); + rlen = sd_do_command(s->card, &request, response); + if (rlen < 0) { + goto error; + } + if ((s->cmdreg & CMD_RESPONSE) != 0) { +#define RWORD(n) ((n >= 0 ? (response[n] << 24) : 0) \ + | (response[n + 1] << 16) \ + | (response[n + 2] << 8) \ + | response[n + 3]) + + if ((rlen == 0) || (rlen != 4 && rlen != 16)) { + goto error; + } + + s->rspreg[0] = RWORD(0); + if (rlen == 4) { + s->rspreg[1] = s->rspreg[2] = s->rspreg[3] = 0; + } else { + s->rspreg[0] = RWORD(11); + s->rspreg[1] = RWORD(7); + s->rspreg[2] = RWORD(3); + s->rspreg[3] = RWORD(-1); + } + DPRINTF("Response received\n"); +#undef RWORD + } else { + DPRINTF("Command sent\n"); + } + return; + +error: + DPRINTF("Timeout\n"); + if (s->errintstsen & SDHC_EISEN_CMDTIMEOUT) { + s->errintsts |= SDHC_EIS_CMDTIMEOUT; + s->norintsts |= SDHC_NIS_ERR; + } +} + +static void sdhc_do_transfer_complete(Exynos4210SDHCState *s) +{ + /* Automatically send CMD12 to stop transfer if AutoCMD12 enabled */ + if ((s->trnmod & SDHC_TRNS_ACMD12) != 0) { + SDRequest request; + uint8_t response[16]; + + request.cmd = 0x0C; + request.arg = 0; + DPRINTF("Command Auto%d %08x\n", request.cmd, request.arg); + sd_do_command(s->card, &request, response); + } + /* pend a timer which will raise a transfer complete irq */ + qemu_mod_timer(s->transfer_complete_timer, + qemu_get_clock_ns(vm_clock) + 1); +} + +/* + * Programmed i/o data transfer + */ + +/* Fill host controlerr's read buffer with BLKSIZE bytes of data from card */ +static void sdhc_read_block_from_card(void *opaque) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + uint32_t value = 0; + int n = 0, index = 0; + uint16_t datacount = s->blksize & 0x0fff; + + if ((s->trnmod & SDHC_TRNS_MULTI) && + (s->trnmod & SDHC_TRNS_BLK_CNT_EN) && (s->blkcnt == 0)) { + return; + } + + while (datacount) { + value |= (uint32_t)sd_read_data(s->card) << (n * 8); + n++; + if (n == 4) { + s->fifo_buffer[index] = value; + value = 0; + n = 0; + index++; + } + datacount--; + } + + /* New data now available for READ through Buffer Port Register */ + s->prnsts |= SDHC_DATA_AVAILABLE; + if (s->norintstsen & SDHC_NISEN_RBUFRDY) { + s->norintsts |= SDHC_NIS_RBUFRDY; + } + + /* Clear DAT line active status if that was the last block */ + if ((s->trnmod & SDHC_TRNS_MULTI) == 0 || + ((s->trnmod & SDHC_TRNS_MULTI) && s->blkcnt == 1)) { + s->prnsts &= ~SDHC_DAT_LINE_ACTIVE; + } + + /* If stop at block gap request was set and it's not the last block of + * data - generate Block Event interrupt */ + if (s->stoped_state == gap_read && (s->trnmod & SDHC_TRNS_MULTI) && + s->blkcnt != 1) { + s->prnsts &= ~SDHC_DAT_LINE_ACTIVE; + if (s->norintstsen & SDHC_EISEN_BLKGAP) { + s->norintsts |= SDHC_EIS_BLKGAP; + } + } + + qemu_set_irq(s->irq, (s->norintsts & s->norintsigen)); +} + +/* Read data from host controller BUFFER DATA PORT register */ +static inline uint32_t sdhc_read_dataport(Exynos4210SDHCState *s) +{ + /* first check if a valid data exists in host controller input buffer + * and that buffer read is not disabled */ + if ((s->prnsts & SDHC_DATA_AVAILABLE) == 0 || + (s->control2 & SDHC_DISBUFRD)) { + DPRINTF("Trying to read from empty buffer or read disabled\n"); + return 0; + } + + uint32_t value = s->fifo_buffer[s->data_count]; + s->data_count++; + /* check if we've read all valid data (blksize bytes) from buffer */ + if (((s->data_count) * 4) >= (s->blksize & 0x0fff)) { + s->prnsts &= ~SDHC_DATA_AVAILABLE; /* no more data in a buffer */ + s->data_count = 0; /* next buff read must start at position [0] */ + + if (s->trnmod & SDHC_TRNS_BLK_CNT_EN) { + s->blkcnt--; + } + + /* if that was the last block of data */ + if ((s->trnmod & SDHC_TRNS_MULTI) == 0 || + ((s->trnmod & SDHC_TRNS_MULTI) && + (s->trnmod & SDHC_TRNS_BLK_CNT_EN) && (s->blkcnt == 0))) { + sdhc_do_transfer_complete(s); + } else if (s->stoped_state == gap_read && /* stop at gap request */ + !(s->prnsts & SDHC_DAT_LINE_ACTIVE)) { + sdhc_raise_transfer_complete_irq(s); + } else { /* if there are more data, read next block from card */ + qemu_mod_timer(s->read_buffer_timer, + qemu_get_clock_ns(vm_clock) + READ_BUFFER_DELAY); + } + } + return value; +} + +/* Write data from host controller FIFO to card */ +static void sdhc_write_block_to_card(void *opaque) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + uint32_t value = 0; + int n = 0, index = 0; + uint16_t datacount = s->blksize & 0x0fff; + + if (s->prnsts & SDHC_SPACE_AVAILABLE) { + if (s->norintstsen & SDHC_NISEN_WBUFRDY) { + s->norintsts |= SDHC_NIS_WBUFRDY; + } + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); + return; + } + + if (s->trnmod & SDHC_TRNS_BLK_CNT_EN) { + if (s->blkcnt == 0) { + return; + } else { + s->blkcnt--; + } + } + + while (datacount) { + if (n == 0) { + value = s->fifo_buffer[index]; + n = 4; + index++; + } + sd_write_data(s->card, (uint8_t)(value & 0xff)); + value >>= 8; + n--; + datacount--; + } + + /* Next data can be written through BUFFER DATORT register */ + s->prnsts |= SDHC_SPACE_AVAILABLE; + if (s->norintstsen & SDHC_NISEN_WBUFRDY) { + s->norintsts |= SDHC_NIS_WBUFRDY; + } + + /* Finish transfer if that was the last block of data */ + if ((s->trnmod & SDHC_TRNS_MULTI) == 0 || + ((s->trnmod & SDHC_TRNS_MULTI) && + (s->trnmod & SDHC_TRNS_BLK_CNT_EN) && (s->blkcnt == 0))) { + sdhc_do_transfer_complete(s); + } + + /* Generate Block Gap Event if requested and if not the last block */ + if (s->stoped_state == gap_write && (s->trnmod & SDHC_TRNS_MULTI) && + s->blkcnt > 0) { + s->prnsts &= ~SDHC_DOING_WRITE; + if (s->norintstsen & SDHC_EISEN_BLKGAP) { + s->norintsts |= SDHC_EIS_BLKGAP; + } + qemu_mod_timer(s->transfer_complete_timer, + qemu_get_clock_ns(vm_clock)+1); + } + + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); +} + +/* Write data to host controller BUFFER DATA PORT register */ +static inline void sdhc_write_dataport(Exynos4210SDHCState *s, uint32_t value) +{ + if ((s->prnsts & SDHC_SPACE_AVAILABLE) == 0) { + DPRINTF("exynos4210.sdhc: Trying to write to full buffer\n"); + return; + } + + s->fifo_buffer[s->data_count] = value; + s->data_count++; + + if (((s->data_count) * 4) >= (s->blksize & 0x0fff)) { + s->data_count = 0; + s->prnsts &= ~SDHC_SPACE_AVAILABLE; + if (s->prnsts & SDHC_DOING_WRITE) { + qemu_mod_timer(s->write_buffer_timer, + qemu_get_clock_ns(vm_clock) + WRITE_BUFFER_DELAY); + } + } +} + +/* + * Single DMA data transfer + */ + +/* Multi block SDMA transfer */ +static void sdhc_sdma_transfer_multi_blocks(Exynos4210SDHCState *s) +{ + uint32_t value = 0, datacnt, saved_datacnt, pos = 0; + int n = 0, page_aligned = 0; + int is_read = (s->trnmod & SDHC_TRNS_READ) != 0; + uint32_t dma_buf_boundary = (s->blksize & 0xf000) >> 12; + uint32_t boundary_chk = 1 << (dma_buf_boundary+12); + uint32_t boundary_count = boundary_chk - (s->sdmasysad % boundary_chk); + + /* XXX: Some sd/mmc drivers (for example, u-boot-slp) do not account for + * possible stop at page boundary if initial address is not page aligned. + * Documentation is vague on how this really behaves in hardware, so allow + * them to work properly */ + if ((s->sdmasysad % boundary_chk) == 0) { + page_aligned = 1; + } + + if (is_read) { + s->prnsts |= SDHC_DOING_READ | SDHC_DATA_INHIBIT | + SDHC_DAT_LINE_ACTIVE; + } else { + s->prnsts |= SDHC_DOING_WRITE | SDHC_DATA_INHIBIT | + SDHC_DAT_LINE_ACTIVE; + } + + /* Transfer data left from last SDMA transaction loop, after controller + * stopped at DMA buffer boundary. This is required only if transfers + * are not aligned to DMA buffer boundary */ + saved_datacnt = s->data_count; + while (saved_datacnt) { + if (is_read) { + value |= (uint32_t)sd_read_data(s->card) << (n * 8); + n++; + if (n == 4) { + cpu_physical_memory_write(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + value = 0; + n = 0; + pos += 4; + } + } else { + if (n == 0) { + cpu_physical_memory_read(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + n = 4; + pos += 4; + } + sd_write_data(s->card, value & 0xff); + value >>= 8; + n--; + } + saved_datacnt--; + } + if (s->data_count) { + s->blkcnt--; + boundary_count -= s->data_count--; + s->sdmasysad += s->data_count--; + s->data_count = 0; + } + + n = value = 0; + while (s->blkcnt) { + if ((boundary_count < (s->blksize & 0x0fff)) && page_aligned) { + datacnt = boundary_count; + s->data_count = (s->blksize & 0x0fff) - boundary_count; + } else { + datacnt = s->blksize & 0x0fff; + } + pos = 0; + saved_datacnt = datacnt; + + if (is_read) { + while (datacnt) { + value |= (uint32_t)sd_read_data(s->card) << (n * 8); + n++; + if (n == 4) { + cpu_physical_memory_write(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + value = 0; + n = 0; + pos += 4; + } + datacnt--; + } + } else { + while (datacnt) { + if (n == 0) { + cpu_physical_memory_read(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + n = 4; + pos += 4; + } + sd_write_data(s->card, value & 0xff); + value >>= 8; + n--; + datacnt--; + } + } + + s->sdmasysad += saved_datacnt; + boundary_count -= saved_datacnt; + if (s->data_count == 0) { + s->blkcnt--; + } + + if (boundary_count == 0) { + break; + } + } + + if (s->blkcnt == 0) { + sdhc_do_transfer_complete(s); + } else { + if (s->norintstsen & SDHC_NISEN_DMA) { + s->norintsts |= SDHC_NIS_DMA; + } + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); + } +} + +/* single block SDMA transfer */ +static void sdhc_sdma_transfer_single_block(Exynos4210SDHCState *s) +{ + int is_read = (s->trnmod & SDHC_TRNS_READ) != 0; + int n = 0; + uint32_t value = 0, pos = 0; + uint32_t datacnt = s->blksize & 0x0fff; + + while (datacnt) { + if (is_read) { + value |= (uint32_t)sd_read_data(s->card) << (n * 8); + n++; + if (n == 4) { + cpu_physical_memory_write(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + value = 0; + n = 0; + pos += 4; + } + } else { + if (n == 0) { + cpu_physical_memory_read(s->sdmasysad + pos, + (uint8_t *)(&value), 4); + n = 4; + pos += 4; + } + sd_write_data(s->card, value & 0xff); + value >>= 8; + n--; + } + datacnt--; + } + + if ((s->trnmod & SDHC_TRNS_BLK_CNT_EN) != 0) { + s->blkcnt--; + } + + sdhc_do_transfer_complete(s); +} + +/* Advanced DMA data transfer */ +static void sdhc_run_adma(Exynos4210SDHCState *s) +{ + uint32_t value; + int n, pos, length; + uint32_t address, datacnt; + uint16_t length_table; + uint8_t attributes; + + int is_read = (s->trnmod & SDHC_TRNS_READ) != 0; + s->admaerr &= ~(SDHC_ADMAERR_FINAL_BLOCK | SDHC_ADMAERR_LENGTH_MISMATCH); + + while (1) { + address = length_table = length = attributes = 0; + value = n = pos = 0; + + /* fetch next entry from descriptor table */ + cpu_physical_memory_read(s->admasysaddr+4, (uint8_t *)(&address), 4); + cpu_physical_memory_read(s->admasysaddr+2, + (uint8_t *)(&length_table), 2); + cpu_physical_memory_read(s->admasysaddr, (uint8_t *)(&attributes), 1); + DPRINTF("ADMA loop: addr=0x%x, len=%d, attr=%x\n", address, + length_table, 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; + + /* Generate ADMA error interrupt */ + if (s->errintstsen & SDHC_EISEN_ADMAERR) { + s->errintsts |= SDHC_EIS_ADMAERR; + s->norintsts |= SDHC_NIS_ERR; + } + qemu_set_irq(s->irq, (s->errintsigen & s->errintsts)); + break; + } + + if (length_table == 0) { + length = 65536; + } else { + length = length_table; + } + + address &= 0xfffffffc; /* minimum unit of address is 4 byte */ + + switch (attributes & SDHC_ADMA_ATTR_ACT_MASK) { + case (SDHC_ADMA_ATTR_ACT_TRAN): /* data transfer */ + for (;;) { + if (s->data_count) { + datacnt = s->data_count; + s->data_count = 0; + } else { + datacnt = s->blksize & 0x0fff; + } + + length -= datacnt; + + if (length < 0) { + s->data_count = (uint16_t)(-length); + datacnt += length; + } + + while (datacnt) { + if (is_read) { + value |= (uint32_t)sd_read_data(s->card) << (n * 8); + n++; + if (n == 4) { + cpu_physical_memory_write(address + pos, + (uint8_t *)(&value), 4); + value = 0; + n = 0; + pos += 4; + } + } else { + if (n == 0) { + cpu_physical_memory_read(address + pos, + (uint8_t *)(&value), 4); + n = 4; + pos += 4; + } + sd_write_data(s->card, value & 0xff); + value >>= 8; + n--; + } + datacnt--; + } + + if ((s->trnmod & SDHC_TRNS_BLK_CNT_EN) && + (!s->data_count)) { + s->blkcnt--; + if (s->blkcnt == 0) { + break; + } + } + + if (length == 0 || s->data_count) { + break; + } + } + s->admasysaddr += 8; + break; + case (SDHC_ADMA_ATTR_ACT_LINK): /* link to next descriptor table */ + s->admasysaddr = address; + DPRINTF("ADMA link: admasysaddr=0x%x\n", s->admasysaddr); + break; + default: + s->admasysaddr += 8; + break; + } + + /* ADMA transfer terminates if blkcnt == 0 or by END attribute */ + if (((s->trnmod & SDHC_TRNS_BLK_CNT_EN) && + (s->blkcnt == 0)) || (attributes & SDHC_ADMA_ATTR_END)) { + DPRINTF("ADMA transfer completed\n"); + if (length || ((attributes & SDHC_ADMA_ATTR_END) && + (s->trnmod & SDHC_TRNS_BLK_CNT_EN) && + s->blkcnt != 0) || ((s->trnmod & SDHC_TRNS_BLK_CNT_EN) && + s->blkcnt == 0 && (attributes & SDHC_ADMA_ATTR_END) == 0)) { + DPRINTF("SD/MMC host ADMA length mismatch\n"); + s->admaerr |= SDHC_ADMAERR_LENGTH_MISMATCH | + SDHC_ADMAERR_STATE_ST_TFR; + if (s->errintstsen & SDHC_EISEN_ADMAERR) { + DPRINTF("Set ADMA error flag\n"); + s->errintsts |= SDHC_EIS_ADMAERR; + s->norintsts |= SDHC_NIS_ERR; + } + qemu_set_irq(s->irq, (s->errintsigen & s->errintsts)); + } + + s->admaerr |= SDHC_ADMAERR_FINAL_BLOCK; + sdhc_do_transfer_complete(s); + break; + } + + if (attributes & SDHC_ADMA_ATTR_INT) { + DPRINTF("ADMA interrupt: admasysaddr=0x%x\n", s->admasysaddr); + s->admaerr |= SDHC_ADMAERR_INTRRUPT_STATUS; + s->stoped_state = adma_intr; + if (s->norintstsen & SDHC_NISEN_DMA) { + s->norintsts |= SDHC_NIS_DMA; + } + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); + break; + } + } +} + +/* Perform data transfer according to controller configuration */ +static void sdhc_process_command(Exynos4210SDHCState *s) +{ + int is_read = (s->trnmod & SDHC_TRNS_READ) != 0; + if (s->trnmod & SDHC_TRNS_DMA) { + if (!((is_read && sd_data_ready(s->card)) || + (!is_read && sd_receive_ready(s->card)))) { + return; + } + + switch (s->hostctl & SDHC_CTRL_DMA_CHECK_MASK) { + case SDHC_CTRL_SDMA: + if ((s->trnmod & SDHC_TRNS_MULTI) && + ((s->trnmod & SDHC_TRNS_BLK_CNT_EN) == 0 + || s->blkcnt == 0)) { + break; + } + + if ((s->blkcnt == 1) || !(s->trnmod & SDHC_TRNS_MULTI)) { + sdhc_sdma_transfer_single_block(s); + } else { + s->data_count = 0; /* # of bytes from last transfer */ + sdhc_sdma_transfer_multi_blocks(s); + } + break; + case SDHC_CTRL_ADMA2_32: + s->data_count = 0; /* # of bytes from last transfer */ + sdhc_run_adma(s); + break; + default: + DPRINTF("Unsupported DMA type\n"); + break; + } + } else { + if (is_read && sd_data_ready(s->card)) { + s->prnsts |= SDHC_DOING_READ | SDHC_DATA_INHIBIT | + SDHC_DAT_LINE_ACTIVE; + s->data_count = 0; + qemu_mod_timer(s->read_buffer_timer, + qemu_get_clock_ns(vm_clock) + READ_BUFFER_DELAY); + } else if (!is_read && sd_receive_ready(s->card)) { + s->prnsts |= SDHC_DOING_WRITE | SDHC_DAT_LINE_ACTIVE | + SDHC_SPACE_AVAILABLE | SDHC_DATA_INHIBIT; + s->data_count = 0; + qemu_mod_timer(s->write_buffer_timer, + qemu_get_clock_ns(vm_clock) + WRITE_BUFFER_DELAY); + } + } +} + +/* Read byte from SD/MMC host controller registers */ +static uint32_t exynos4210_sdhc_read_1(Exynos4210SDHCState *s, + target_phys_addr_t offset) +{ + switch (offset) { + case SDHC_HOSTCTL: + return s->hostctl; + case SDHC_PWRCON: + return s->pwrcon; + case SDHC_BLKGAP: + return s->blkgap; + case SDHC_WAKCON: + return s->wakcon; + case SDHC_TIMEOUTCON: + return s->timeoutcon; + case SDHC_SWRST: + return 0; + case SDHC_RSPREG0 + 0x3: + return (uint8_t)(s->rspreg[0] >> 24); + case SDHC_RSPREG1 + 0x3: + return (uint8_t)(s->rspreg[1] >> 24); + case SDHC_RSPREG2 + 0x3: + return (uint8_t)(s->rspreg[2] >> 24); + default: + DPRINTF("bad 1 byte read offset " TARGET_FMT_plx "\n", offset); + return 0xBAADBAAD; + } +} + +/* Read two bytes from SD/MMC host controller registers */ +static uint32_t exynos4210_sdhc_read_2(Exynos4210SDHCState *s, + target_phys_addr_t offset) +{ + switch (offset) { + case SDHC_BLKSIZE: + return s->blksize; + case SDHC_BLKCNT: + return s->blkcnt; + case SDHC_TRNMOD: + return s->trnmod; + case SDHC_CMDREG: + return s->cmdreg; + case SDHC_CLKCON: + return s->clkcon; + case SDHC_NORINTSTS: + qemu_set_irq(s->irq, 0); + return s->norintsts; + case SDHC_ERRINTSTS: + qemu_set_irq(s->irq, 0); + return s->errintsts; + case SDHC_NORINTSTSEN: + return s->norintstsen; + case SDHC_ERRINTSTSEN: + return s->errintstsen; + case SDHC_NORINTSIGEN: + return s->norintsigen; + case SDHC_ERRINTSIGEN: + return s->errintsigen; + case SDHC_ACMD12ERRSTS: + return s->acmd12errsts; + case SDHC_SLOT_INT_STATUS: + return 0; + case SDHC_FEAER: case SDHC_FEERR: + DPRINTF("Reading from WO register\n"); + return 0; + case SDHC_HCVER: + return 0x2401; /* SD Host Specification Version 2.0 */ + default: + DPRINTF("bad 2 byte read offset " TARGET_FMT_plx "\n", offset); + return 0xBAADBAAD; + } +} + +/* MMC read 4 bytes function */ +static uint32_t exynos4210_sdhc_read_4(Exynos4210SDHCState *s, + target_phys_addr_t offset) +{ + switch (offset) { + case SDHC_SYSAD: + return s->sdmasysad; + case SDHC_ARGUMENT: + return s->argument; + case SDHC_RSPREG0: + return s->rspreg[0]; + case SDHC_RSPREG1: + return s->rspreg[1]; + case SDHC_RSPREG2: + return s->rspreg[2]; + case SDHC_RSPREG3: + return s->rspreg[3]; + case SDHC_BDATA: + return sdhc_read_dataport(s); + case SDHC_PRNSTS: + return s->prnsts; + case SDHC_NORINTSTS: + qemu_set_irq(s->irq, 0); + return (s->errintsts << 16) | (s->norintsts); + case SDHC_NORINTSTSEN: + return (s->errintstsen << 16) | s->norintstsen; + case SDHC_NORINTSIGEN: + return (s->errintsigen << 16) | s->norintsigen; + case SDHC_CAPAREG: + return s->capareg; + case SDHC_MAXCURR: + return s->maxcurr; + case SDHC_ADMAERR: + return s->admaerr; + case SDHC_ADMASYSADDR: + return s->admasysaddr; + case SDHC_CONTROL2: + return s->control2; + case SDHC_CONTROL3: + return s->control3; + case SDHC_CONTROL4: + return 0; /* makes no sense in emulation so return 0 */ + default: + DPRINTF("bad 4 byte read offset " TARGET_FMT_plx "\n", offset); + return 0xBAADBAAD; + } +} + +/* MMC write (byte) function */ +static void +exynos4210_sdhc_write_1(Exynos4210SDHCState *s, target_phys_addr_t offset, + uint32_t value) +{ + switch (offset) { + case SDHC_HOSTCTL: + s->hostctl = value & 0x3F; + break; + case SDHC_PWRCON: + s->pwrcon = value & 0x0F; + break; + case SDHC_BLKGAP: + if (value & 0x0C) { + error_report("SDHC: ReadWait & IntAtBlockGap not implemented\n"); + } + + if ((value & SDHC_STOP_AT_GAP_REQ) && + (s->blkgap & SDHC_STOP_AT_GAP_REQ)) { + break; + } + s->blkgap = value & (SDHC_STOP_AT_GAP_REQ); + + if ((value & SDHC_CONTINUE_REQ) && s->stoped_state && + (s->blkgap & SDHC_STOP_AT_GAP_REQ) == 0) { + if (s->stoped_state == gap_read) { + s->prnsts |= SDHC_DAT_LINE_ACTIVE | SDHC_DOING_READ; + qemu_mod_timer(s->read_buffer_timer, + qemu_get_clock_ns(vm_clock) + READ_BUFFER_DELAY); + } else { + s->prnsts |= SDHC_DAT_LINE_ACTIVE | SDHC_DOING_WRITE; + qemu_mod_timer(s->write_buffer_timer, + qemu_get_clock_ns(vm_clock) + READ_BUFFER_DELAY); + } + s->stoped_state = not_stoped; + } else if (!s->stoped_state && (value & SDHC_STOP_AT_GAP_REQ)) { + if (s->prnsts & SDHC_DOING_READ) { + s->stoped_state = gap_read; + } else if (s->prnsts & SDHC_DOING_WRITE) { + s->stoped_state = gap_write; + } + } + break; + case SDHC_WAKCON: + s->wakcon = value & 0x07; + s->wakcon &= ~(value & SDHC_STAWAKEUP); + break; + case SDHC_TIMEOUTCON: + s->timeoutcon = value & 0x0F; + break; + case SDHC_SWRST: + switch (value) { + case SDHC_RESET_ALL: + exynos4210_sdhc_reset(&s->busdev.qdev); + break; + case SDHC_RESET_CMD: + s->prnsts &= ~SDHC_CMD_INHIBIT; + s->norintsts &= ~SDHC_NIS_CMDCMP; + break; + case SDHC_RESET_DATA: + s->data_count = 0; + qemu_del_timer(s->read_buffer_timer); + qemu_del_timer(s->write_buffer_timer); + s->prnsts &= ~(SDHC_SPACE_AVAILABLE | SDHC_DATA_AVAILABLE | + SDHC_DOING_READ | SDHC_DOING_WRITE | + SDHC_DATA_INHIBIT | SDHC_DAT_LINE_ACTIVE); + s->blkgap &= ~(SDHC_STOP_AT_GAP_REQ | SDHC_CONTINUE_REQ); + s->stoped_state = not_stoped; + s->norintsts &= ~(SDHC_NIS_WBUFRDY | SDHC_NIS_RBUFRDY | + SDHC_NIS_DMA | SDHC_NIS_TRSCMP | SDHC_NIS_BLKGAP); + break; + } + break; + case (SDHC_NORINTSTS+1): + if (s->norintstsen & SDHC_NISEN_CARDINT) { + value &= ~1; + } + s->norintsts &= (s->norintsts & 0x8000) | (~((value << 8) & 0xFF00)); + break; + case (SDHC_NORINTSTS): + s->norintsts &= ~(((uint16_t)value) & 0x00FF); + break; + default: + DPRINTF("bad 1 byte write offset " TARGET_FMT_plx "\n", offset); + break; + } +} + +/* MMC write 2 bytes function */ +static void +exynos4210_sdhc_write_2(Exynos4210SDHCState *s, target_phys_addr_t offset, + uint32_t value) +{ + switch (offset) { + case SDHC_BLKSIZE: + if (!(s->prnsts & (SDHC_DOING_READ | SDHC_DOING_WRITE))) { + s->blksize = value & 0x7FFF; + } + break; + case SDHC_BLKCNT: + if (!(s->prnsts & (SDHC_DOING_READ | SDHC_DOING_WRITE))) { + s->blkcnt = value; + } + break; + case SDHC_TRNMOD: + if (value & (SDHC_TRNS_BOOTCMD | SDHC_TRNS_BOOTACK | + SDHC_TRNS_CEATA)) { + error_report("QEMU SDHC: CEATA mode not implemented\n"); + } + if ((s->capareg & SDHC_CAN_DO_DMA) == 0) { + value &= ~SDHC_TRNS_DMA; + } + s->trnmod = value & 0x0037; + break; + case SDHC_CMDREG: /* Command */ + s->cmdreg = value & 0x3FFB; + + if (((s->control2 & SDHC_SDOPSIGPC) || (s->control2 & SDHC_SDINPSIGPC)) + && !(s->pwrcon & SDHC_POWER_ON)) { + DPRINTF("Can't issue command with power off\n"); + break; + } + + if ((s->clkcon & SDHC_CLOCK_CHK_MASK) != SDHC_CLOCK_CHK_MASK) { + DPRINTF("Can't issue command with clock off\n"); + break; + } + + if ((value & SDHC_CMD_TYPE_MASK) == SDHC_CMD_RESUME_MASK || + (value & SDHC_CMD_TYPE_MASK) == SDHC_CMD_SUSPEND_MASK) { + DPRINTF("sdhc: suspend/resume commands not implemented\n"); + break; + } + + if ((s->stoped_state || (s->prnsts & SDHC_DATA_INHIBIT)) && + ((value & SDHC_CMD_DATA_PRESENT) || ((value & SDHC_CMD_RSP_WITH_BUSY) && + !(value & SDHC_CMD_ABORT_MASK)))) { + DPRINTF("Can't issue command which uses DAT line\n"); + break; + } + + if ((value & SDHC_CMD_TYPE_MASK) == SDHC_CMD_ABORT_MASK && + ((s->prnsts & SDHC_DOING_READ) || (s->prnsts & SDHC_DOING_WRITE))) { + DPRINTF("ABORT command\n"); + qemu_del_timer(s->read_buffer_timer); /* stop reading data */ + qemu_del_timer(s->write_buffer_timer); /* stop writing data */ + qemu_mod_timer(s->transfer_complete_timer, + qemu_get_clock_ns(vm_clock) + 1); + } + + sdhc_send_command(s); + sdhc_raise_response_recieved_irq(s); + + if (s->blksize == 0) { + break; + } + sdhc_process_command(s); + break; + case SDHC_CLKCON: + s->clkcon = value; + if (SDHC_CLOCK_INT_EN & s->clkcon) { + s->clkcon |= SDHC_CLOCK_INT_STABLE; + } else { + s->clkcon &= ~SDHC_CLOCK_INT_STABLE; + } + if (SDHC_CLOCK_CARD_EN & s->clkcon) { + s->clkcon |= SDHC_CLOCK_EXT_STABLE; + } else { + s->clkcon &= ~SDHC_CLOCK_EXT_STABLE; + } + break; + case SDHC_NORINTSTS: + if (s->norintstsen & SDHC_NISEN_CARDINT) { + value &= ~SDHC_NIS_CARDINT; + } + s->norintsts &= (s->norintsts & 0x8000) | (uint16_t)(~(value & 0xFFFF)); + break; + case SDHC_ERRINTSTS: + s->errintsts &= ~((value & 0x077F) | 0xF880); + s->errintsts ? (s->norintsts |= SDHC_NIS_ERR) : + (s->norintsts &= ~SDHC_NIS_ERR); + break; + case SDHC_NORINTSTSEN: + s->norintstsen = value & 0x7FFF; + break; + case SDHC_ERRINTSTSEN: + s->errintstsen = value & 0x07FF; + break; + case SDHC_NORINTSIGEN: + s->norintsigen = value & 0x7FFF; + break; + case SDHC_ERRINTSIGEN: + s->errintsigen = value & 0x07FF; + break; + case SDHC_ACMD12ERRSTS: case SDHC_FEAER: case SDHC_HCVER: + break; + case SDHC_FEERR: + s->norintsts |= (value & 0x073F) & s->norintstsen; + qemu_set_irq(s->irq, (s->norintsigen & s->norintsts)); + break; + default: + DPRINTF("bad 2 byte write offset " TARGET_FMT_plx "\n", offset); + break; + } +} + +/* MMC write 4 bytes function */ +static void +exynos4210_sdhc_write_4(Exynos4210SDHCState *s, target_phys_addr_t offset, + uint32_t value) +{ + switch (offset) { + case SDHC_SYSAD: + s->sdmasysad = value; + if ((s->prnsts & (SDHC_DOING_READ | SDHC_DOING_WRITE)) + && (s->blkcnt != 0) && (s->blksize != 0) && + (s->hostctl & SDHC_CTRL_DMA_CHECK_MASK) == SDHC_CTRL_SDMA) { + sdhc_sdma_transfer_multi_blocks(s); + } + break; + case SDHC_ARGUMENT: + s->argument = value; + break; + case SDHC_BDATA: + sdhc_write_dataport(s, value); + break; + case SDHC_NORINTSTS: + if (s->norintstsen & SDHC_NISEN_CARDINT) { + value &= ~SDHC_NIS_CARDINT; + } + s->norintsts &= (s->norintsts & 0x8000) | (uint16_t)(~(value & 0xFFFF)); + + s->errintsts &= ~(((value >> 16) & 0x077F) | 0xF880); + s->errintsts ? (s->norintsts |= SDHC_NIS_ERR) : + (s->norintsts &= ~SDHC_NIS_ERR); + break; + case SDHC_NORINTSTSEN: + s->norintstsen = (uint16_t)(value & 0x7FFF); + s->errintstsen = (uint16_t)((value >> 16) & 0x07FF); + break; + case SDHC_NORINTSIGEN: + s->norintsigen = (uint16_t)(value & 0x7FFF); + s->errintsigen = (uint16_t)((value >> 16) & 0x07FF); + break; + case SDHC_CAPAREG: + if (!(s->control2 & SDHC_HWINITFIN)) { + s->capareg = value & 0x07EB3FBF; + } + break; + case SDHC_MAXCURR: + if (!(s->control2 & SDHC_HWINITFIN)) { + s->maxcurr = value & 0x00FFFFFF; + } + break; + case SDHC_ADMAERR: + s->admaerr = ((s->admaerr & SDHC_ADMAERR_FINAL_BLOCK) | + (value & 0x00000007)) & 0x00000507; + s->admaerr &= ~(value & SDHC_ADMAERR_INTRRUPT_STATUS); + if (value & SDHC_ADMAERR_CONTINUE_REQUEST) { + s->stoped_state = not_stoped; + sdhc_run_adma(s); + } + break; + case SDHC_ADMASYSADDR: + s->admasysaddr = value; + break; + case SDHC_CONTROL2: + s->control2 = value & 0xDF00DFFB; + break; + case SDHC_CONTROL3: + s->control3 = value; + break; + case SDHC_RSPREG0: case SDHC_RSPREG1: case SDHC_RSPREG2: + case SDHC_RSPREG3: case SDHC_PRNSTS: case SDHC_CONTROL4: + /* Nothing for emulation */ + break; + default: + DPRINTF("bad 4 byte write offset " TARGET_FMT_plx "\n", offset); + break; + } +} + +static uint64_t +exynos4210_sdhc_read(void *opaque, target_phys_addr_t offset, unsigned size) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + + switch (size) { + case (1): + return exynos4210_sdhc_read_1(s, offset); + case (2): + return exynos4210_sdhc_read_2(s, offset); + case (4): + return exynos4210_sdhc_read_4(s, offset); + } + return 0xBAADBAADull; +} + +static void +exynos4210_sdhc_write(void *opaque, target_phys_addr_t offset, uint64_t val, + unsigned size) +{ + Exynos4210SDHCState *s = (Exynos4210SDHCState *)opaque; + + switch (size) { + case (1): + exynos4210_sdhc_write_1(s, offset, (uint32_t)val); + break; + case (2): + exynos4210_sdhc_write_2(s, offset, (uint32_t)val); + break; + case (4): + exynos4210_sdhc_write_4(s, offset, (uint32_t)val); + break; + } +} + +static const MemoryRegionOps exynos4210_sdhc_mmio_ops = { + .read = exynos4210_sdhc_read, + .write = exynos4210_sdhc_write, + .valid = { + .min_access_size = 1, + .max_access_size = 4, + .unaligned = false + }, + .endianness = DEVICE_NATIVE_ENDIAN, +}; + +static const VMStateDescription exynos4210_sdhc_vmstate = { + .name = "exynos4210.sdhc", + .version_id = 1, + .minimum_version_id = 1, + .minimum_version_id_old = 1, + .fields = (VMStateField[]) { + VMSTATE_UINT8(stoped_state, Exynos4210SDHCState), + VMSTATE_UINT32_ARRAY(fifo_buffer, Exynos4210SDHCState, + (SDHC_CAPAB_MAXBLOCKLENGTH / TARGET_WORD_SIZE)), + VMSTATE_UINT16(data_count, Exynos4210SDHCState), + VMSTATE_UINT32(sdmasysad, Exynos4210SDHCState), + VMSTATE_UINT16(blksize, Exynos4210SDHCState), + VMSTATE_UINT16(blkcnt, Exynos4210SDHCState), + VMSTATE_UINT32(argument, Exynos4210SDHCState), + VMSTATE_UINT16(trnmod, Exynos4210SDHCState), + VMSTATE_UINT16(cmdreg, Exynos4210SDHCState), + VMSTATE_UINT32_ARRAY(rspreg, Exynos4210SDHCState, 4), + VMSTATE_UINT32(prnsts, Exynos4210SDHCState), + VMSTATE_UINT8(hostctl, Exynos4210SDHCState), + VMSTATE_UINT8(pwrcon, Exynos4210SDHCState), + VMSTATE_UINT8(blkgap, Exynos4210SDHCState), + VMSTATE_UINT8(wakcon, Exynos4210SDHCState), + VMSTATE_UINT16(clkcon, Exynos4210SDHCState), + VMSTATE_UINT8(timeoutcon, Exynos4210SDHCState), + VMSTATE_UINT16(norintsts, Exynos4210SDHCState), + VMSTATE_UINT16(errintsts, Exynos4210SDHCState), + VMSTATE_UINT16(norintstsen, Exynos4210SDHCState), + VMSTATE_UINT16(errintstsen, Exynos4210SDHCState), + VMSTATE_UINT16(norintsigen, Exynos4210SDHCState), + VMSTATE_UINT16(errintsigen, Exynos4210SDHCState), + VMSTATE_UINT16(acmd12errsts, Exynos4210SDHCState), + VMSTATE_UINT32(capareg, Exynos4210SDHCState), + VMSTATE_UINT32(maxcurr, Exynos4210SDHCState), + VMSTATE_UINT32(admaerr, Exynos4210SDHCState), + VMSTATE_UINT32(admasysaddr, Exynos4210SDHCState), + VMSTATE_UINT32(control2, Exynos4210SDHCState), + VMSTATE_UINT32(control3, Exynos4210SDHCState), + VMSTATE_END_OF_LIST() + } +}; + +static int exynos4210_sdhc_init(SysBusDevice *dev) +{ + Exynos4210SDHCState *s = FROM_SYSBUS(Exynos4210SDHCState, dev); + DriveInfo *bd; + + sysbus_init_irq(dev, &s->irq); + memory_region_init_io(&s->iomem, &exynos4210_sdhc_mmio_ops, s, + "exynos4210.sdhc", SDHC_REG_SIZE); + sysbus_init_mmio(dev, &s->iomem); + bd = drive_get_next(IF_SD); + + if ((bd == NULL)) { + s->card = NULL; + DPRINTF("s->card = NULL\n"); + } else { + s->eject = qemu_allocate_irqs(sdhc_insert_eject, s, 1)[0]; + DPRINTF("name = %s, sectors = %ld\n", + bd->bdrv->device_name, bd->bdrv->total_sectors); + s->card = sd_init(bd->bdrv, 0); + sd_set_cb(s->card, NULL, s->eject); + } + + s->insert_timer = + qemu_new_timer(vm_clock, SCALE_NS, sdhc_raise_insertion_irq, s); + + s->read_buffer_timer = + qemu_new_timer(vm_clock, SCALE_NS, sdhc_read_block_from_card, s); + + s->write_buffer_timer = + qemu_new_timer(vm_clock, SCALE_NS, sdhc_write_block_to_card, s); + + s->transfer_complete_timer = + qemu_new_timer(vm_clock, SCALE_NS, sdhc_raise_transfer_complete_irq, s); + + return 0; +} + +static SysBusDeviceInfo exynos4210_sdhc_info = { + .init = exynos4210_sdhc_init, + .qdev.name = "exynos4210.sdhc", + .qdev.size = sizeof(Exynos4210SDHCState), + .qdev.vmsd = &exynos4210_sdhc_vmstate, + .qdev.reset = exynos4210_sdhc_reset, +}; + +static void sdhc_register_devices(void) +{ + sysbus_register_withprop(&exynos4210_sdhc_info); +} + +device_init(sdhc_register_devices)