diff options
author | Nicolas Boichat <drinkcat@chromium.org> | 2018-06-28 10:56:56 +0800 |
---|---|---|
committer | chrome-bot <chrome-bot@chromium.org> | 2018-07-11 23:06:38 -0700 |
commit | 263a7ad89c702e6b34dfbe782e347a2d35178630 (patch) | |
tree | e6da0c88b59bfb6162365e38bfb72998f13df299 /board | |
parent | 7884e76ea35140d91a787160a5b81ae371f24c22 (diff) | |
download | chrome-ec-263a7ad89c702e6b34dfbe782e347a2d35178630.tar.gz |
kukui: emulate eMMC boot mode using SPI controller.
Kukui is not able to boot from SPI-NOR, it can only boot from
eMMC or UFS. Here we emulates the eMMC boot using SPI controller.
AP tranmits on CMD line, and eMMC replies on DAT0 line.
eMMC boot operation looks a lot like SPI: CMD is unidirectional
MOSI, DAT is unidirectional MISO. CLK is driven by the master.
However, there is no chip-select, and the clock is active for a
long time before any command is sent on the CMD line. From SPI
perspective, this looks like a lot of '1' are being sent from
the master.
To catch the commands, we setup DMA to write the data into a
circular buffer (in_msg), and monitor for a falling edge on CMD
(emmc_cmd_interrupt). Once an interrupt is received, we scan the
circular buffer, in reverse, to be as fast as possible and
minimize chances of missing the command.
We then figure out the bit-wise command alignment, decode it,
and, upon receiving BOOT_INITIATION command, setup DMA to respond
with the data on the DAT line. The data in bootblock_data.h is
preprocessed to include necessary eMMC headers: acknowledge boot
mode, start of block, CRC, end of block, etc. The host can only
slow down transfer by stopping the clock, which is compatible
with SPI.
In some cases (e.g. if the BootROM expects data over 8 lanes
instead of 1), the BootROM will quickly interrupt the transfer
with an IDLE command. In this case we interrupt the transfer,
and the BootROM will try again.
BRANCH=None
BUG=b:110907438
TEST=make BOARD=kukui BOOTBLOCK=abc -j # check build pass
Change-Id: I0f1f2d35c525c6475d90fca2cd6e97f87cd747cc
Signed-off-by: Yilun Lin <yllin@google.com>
Signed-off-by: Nicolas Boichat <drinkcat@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/1126579
Reviewed-by: Randall Spangler <rspangler@chromium.org>
Diffstat (limited to 'board')
-rw-r--r-- | board/kukui/board.c | 3 | ||||
-rw-r--r-- | board/kukui/board.h | 12 | ||||
-rw-r--r-- | board/kukui/build.mk | 3 | ||||
-rw-r--r-- | board/kukui/ec.tasklist | 3 | ||||
-rw-r--r-- | board/kukui/emmc.c | 303 | ||||
-rw-r--r-- | board/kukui/gpio.inc | 12 |
6 files changed, 333 insertions, 3 deletions
diff --git a/board/kukui/board.c b/board/kukui/board.c index af0bbf0b7b..48bb590dad 100644 --- a/board/kukui/board.c +++ b/board/kukui/board.c @@ -210,6 +210,9 @@ int pd_snk_is_vbus_provided(int port) static void board_init(void) { + /* Set SPI1 PB13/14/15 pins to high speed */ + STM32_GPIO_OSPEEDR(GPIO_B) |= 0xfc000000; + /* Enable TCPC alert interrupts */ gpio_enable_interrupt(GPIO_USB_C0_PD_INT_ODL); diff --git a/board/kukui/board.h b/board/kukui/board.h index 1da4fe0236..9f2c46fbe5 100644 --- a/board/kukui/board.h +++ b/board/kukui/board.h @@ -40,6 +40,13 @@ /* Region sizes are no longer a power of 2 so we can't enable MPU */ #undef CONFIG_MPU +/* Bootblock */ +#ifdef SECTION_IS_RO +#define CONFIG_BOOTBLOCK + +#define EMMC_SPI_PORT 2 +#endif + /* Optional features */ #define CONFIG_BOARD_PRE_INIT #define CONFIG_BOARD_VERSION_CUSTOM @@ -201,6 +208,11 @@ enum sensor_id { #include "gpio_signal.h" #include "registers.h" +#ifdef SECTION_IS_RO +/* Interrupt handler for emmc task */ +void emmc_cmd_interrupt(enum gpio_signal signal); +#endif + void board_reset_pd_mcu(void); int board_get_version(void); diff --git a/board/kukui/build.mk b/board/kukui/build.mk index 7e238d3ac9..02ef585861 100644 --- a/board/kukui/build.mk +++ b/board/kukui/build.mk @@ -12,3 +12,6 @@ CHIP_FAMILY:=stm32f0 CHIP_VARIANT:=stm32f09x board-y=battery.o board.o usb_pd_policy.o led.o +board-$(CONFIG_BOOTBLOCK)+=emmc.o + +$(out)/RO/board/$(BOARD)/emmc.o: $(out)/bootblock_data.h diff --git a/board/kukui/ec.tasklist b/board/kukui/ec.tasklist index 414c235375..4219aa45c7 100644 --- a/board/kukui/ec.tasklist +++ b/board/kukui/ec.tasklist @@ -25,5 +25,6 @@ TASK_NOTEST(PDCMD, pd_command_task, NULL, LARGER_TASK_STACK_SIZE) \ TASK_ALWAYS(HOSTCMD, host_command_task, NULL, LARGER_TASK_STACK_SIZE) \ TASK_ALWAYS(CONSOLE, console_task, NULL, LARGER_TASK_STACK_SIZE) \ - TASK_ALWAYS(PD_C0, pd_task, NULL, LARGER_TASK_STACK_SIZE) + TASK_ALWAYS(PD_C0, pd_task, NULL, LARGER_TASK_STACK_SIZE) \ + TASK_ALWAYS_RO(EMMC, emmc_task, NULL, LARGER_TASK_STACK_SIZE) diff --git a/board/kukui/emmc.c b/board/kukui/emmc.c new file mode 100644 index 0000000000..b7e8265063 --- /dev/null +++ b/board/kukui/emmc.c @@ -0,0 +1,303 @@ +/* Copyright 2018 The Chromium OS Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Transfer bootblock over SPI by emulating eMMC "Alternative Boot operation" + * (section 6.3.4 of eMMC 5.0 specification, JESD84-B50). + * + * eMMC boot operation looks a lot like SPI: CMD is unidirectional MOSI, DAT is + * unidirectional MISO. CLK is driven by the master. However, there is no + * chip-select, and the clock is active for a long time before any command is + * sent on the CMD line. From SPI perspective, this looks like a lot of '1' + * are being sent from the master. + * + * To catch the commands, we setup DMA to write the data into a circular buffer + * (in_msg), and monitor for a falling edge on CMD (emmc_cmd_interrupt). Once + * an interrupt is received, we scan the circular buffer, in reverse, to + * be as fast as possible and minimize chances of missing the command. + * + * We then figure out the bit-wise command alignment, decode it, and, upon + * receiving BOOT_INITIATION command, setup DMA to respond with the data on the + * DAT line. The data in bootblock_data.h is preprocessed to include necessary + * eMMC headers: acknowledge boot mode, start of block, CRC, end of block, etc. + * The host can only slow down transfer by stopping the clock, which is + * compatible with SPI. + * + * In some cases (e.g. if the BootROM expects data over 8 lanes instead of 1), + * the BootROM will quickly interrupt the transfer with an IDLE command. In this + * case we interrupt the transfer, and the BootROM will try again. + */ + +#include "clock.h" +#include "console.h" +#include "dma.h" +#include "endian.h" +#include "gpio.h" +#include "task.h" +#include "timer.h" + +#include "bootblock_data.h" + +#define CPRINTS(format, args...) cprints(CC_SPI, format, ## args) +#define CPRINTF(format, args...) cprintf(CC_SPI, format, ## args) + +#if EMMC_SPI_PORT == 1 +#define STM32_SPI_EMMC_REGS STM32_SPI1_REGS +#define STM32_DMAC_SPI_EMMC_TX STM32_DMAC_SPI1_TX +#define STM32_DMAC_SPI_EMMC_RX STM32_DMAC_SPI1_RX +#elif EMMC_SPI_PORT == 2 +#define STM32_SPI_EMMC_REGS STM32_SPI2_REGS +#define STM32_DMAC_SPI_EMMC_TX STM32_DMAC_SPI2_TX +#define STM32_DMAC_SPI_EMMC_RX STM32_DMAC_SPI2_RX +#else +#error "Please define EMMC_SPI_PORT in board.h." +#endif + +/* 1024 bytes circular buffer is enough for ~0.6ms @ 13Mhz. */ +#define SPI_RX_BUF_BYTES 1024 +#define SPI_RX_BUF_WORDS (SPI_RX_BUF_BYTES/4) +static uint32_t in_msg[SPI_RX_BUF_WORDS]; + +/* Macros to advance in the circular buffer. */ +#define RX_BUF_NEXT_32(i) (((i) + 1) & (SPI_RX_BUF_WORDS - 1)) +#define RX_BUF_DEC_32(i, j) (((i) - (j)) & (SPI_RX_BUF_WORDS - 1)) +#define RX_BUF_PREV_32(i) RX_BUF_DEC_32((i), 1) + +enum emmc_cmd { + EMMC_ERROR = -1, + EMMC_IDLE = 0, + EMMC_PRE_IDLE, + EMMC_BOOT, +}; + +static const struct dma_option dma_tx_option = { + STM32_DMAC_SPI_EMMC_TX, (void *)&STM32_SPI_EMMC_REGS->dr, + STM32_DMA_CCR_MSIZE_8_BIT | STM32_DMA_CCR_PSIZE_8_BIT +}; + +/* Circular RX buffer */ +static const struct dma_option dma_rx_option = { + STM32_DMAC_SPI_EMMC_RX, (void *)&STM32_SPI_EMMC_REGS->dr, + STM32_DMA_CCR_MSIZE_8_BIT | STM32_DMA_CCR_PSIZE_8_BIT | + STM32_DMA_CCR_CIRC +}; + +/* Setup DMA to transfer bootblock. */ +static void bootblock_transfer(void) +{ + dma_chan_t *txdma = dma_get_channel(STM32_DMAC_SPI_EMMC_TX); + + dma_prepare_tx(&dma_tx_option, sizeof(bootblock_raw_data), + bootblock_raw_data); + dma_go(txdma); + + CPRINTS("transfer"); +} + +/* Abort an ongoing transfer. */ +static void bootblock_stop(void) +{ + dma_disable(STM32_DMAC_SPI_EMMC_TX); + + /* + * Wait a bit to for DMA to stop writing (we can't really wait for the + * buffer to get empty, as the bus may not be clocked anymore). + */ + udelay(100); + + /* Then flush SPI FIFO, and make sure DAT line stays idle (high). */ + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; +} + +static enum emmc_cmd emmc_parse_command(int index) +{ + int32_t shift0; + uint32_t data[3]; + + if (in_msg[index] == 0xffffffff) + return EMMC_ERROR; + + data[0] = htobe32(in_msg[index]); + index = RX_BUF_NEXT_32(index); + data[1] = htobe32(in_msg[index]); + index = RX_BUF_NEXT_32(index); + data[2] = htobe32(in_msg[index]); + + /* Figure out alignment (cmd starts with 01) */ + + /* Number of leading ones. */ + shift0 = __builtin_clz(~data[0]); + + data[0] = (data[0] << shift0) | (data[1] >> (32-shift0)); + data[1] = (data[1] << shift0) | (data[2] >> (32-shift0)); + + if (data[0] == 0x40000000 && data[1] == 0x0095ffff) { + /* 400000000095 GO_IDLE_STATE */ + CPRINTS("goIdle"); + return EMMC_IDLE; + } + + if (data[0] == 0x40f0f0f0 && data[1] == 0xf0fdffff) { + /* 40f0f0f0f0fd GO_PRE_IDLE_STATE */ + CPRINTS("goPreIdle"); + return EMMC_PRE_IDLE; + } + + if (data[0] == 0x40ffffff && data[1] == 0xfae5ffff) { + /* 40fffffffae5 BOOT_INITIATION */ + CPRINTS("bootInit"); + return EMMC_BOOT; + } + + CPRINTS("eMMC error"); + return EMMC_ERROR; +} + + +/* + * Wake the EMMC task when there is a falling edge on the CMD line, so that we + * can capture the command. + */ +void emmc_cmd_interrupt(enum gpio_signal signal) +{ + task_wake(TASK_ID_EMMC); + CPRINTF("i"); +} + +static void emmc_init_spi(void) +{ +#if EMMC_SPI_PORT == 1 + /* Reset SPI */ + STM32_RCC_APB2RSTR |= STM32_RCC_PB2_SPI1; + STM32_RCC_APB2RSTR &= ~STM32_RCC_PB2_SPI1; + + /* Enable clocks to SPI module */ + STM32_RCC_APB2ENR |= STM32_RCC_PB2_SPI1; +#elif EMMC_SPI_PORT == 2 + /* Reset SPI */ + STM32_RCC_APB1RSTR |= STM32_RCC_PB1_SPI2; + STM32_RCC_APB1RSTR &= ~STM32_RCC_PB1_SPI2; + + /* Enable clocks to SPI module */ + STM32_RCC_APB1ENR |= STM32_RCC_PB1_SPI2; +#else +#error "Please define EMMC_SPI_PORT in board.h." +#endif + clock_wait_bus_cycles(BUS_APB, 1); + gpio_config_module(MODULE_SPI, 1); + + STM32_SPI_EMMC_REGS->cr2 = + STM32_SPI_CR2_FRXTH | STM32_SPI_CR2_DATASIZE(8) | + STM32_SPI_CR2_RXDMAEN | STM32_SPI_CR2_TXDMAEN; + + /* Manual CS, disable. */ + STM32_SPI_EMMC_REGS->cr1 = STM32_SPI_CR1_SSM | STM32_SPI_CR1_SSI; + + /* Flush SPI FIFO, and make sure DAT line stays idle (high). */ + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; + STM32_SPI_EMMC_REGS->dr = 0xff; + + /* Enable the SPI peripheral */ + STM32_SPI_EMMC_REGS->cr1 |= STM32_SPI_CR1_SPE; +} + +void emmc_task(void *u) +{ + int dma_pos, i; + dma_chan_t *rxdma; + enum emmc_cmd cmd; + /* Are we currently transmitting data? */ + int tx = 0; + + emmc_init_spi(); + + gpio_enable_interrupt(GPIO_EMMC_CMD); + + /* Start receiving in circular buffer in_msg. */ + rxdma = dma_get_channel(STM32_DMAC_SPI_EMMC_RX); + dma_start_rx(&dma_rx_option, sizeof(in_msg), in_msg); + + /* Enable internal chip select. */ + STM32_SPI_EMMC_REGS->cr1 &= ~STM32_SPI_CR1_SSI; + + while (1) { + /* + * TODO(b:110907438): After the bootblock has been transferred + * and AP has booted, disable SPI controller and interrupt. + */ + + /* Wait for a command */ + task_wait_event(-1); + + dma_pos = dma_bytes_done(rxdma, sizeof(in_msg)) / 4; + i = RX_BUF_PREV_32(dma_pos); + + /* + * By now, bus should be idle again (it takes <10us to transmit + * a command, less than is needed to process interrupt and wake + * this task). + */ + if (in_msg[i] != 0xffffffff) { + CPRINTF("?"); + /* TODO(b:110907438): We should probably just retry. */ + continue; + } + + /* + * Find a command, looking from the end of the buffer to make + * it faster. + */ + while (i != dma_pos && in_msg[i] == 0xffffffff) + i = RX_BUF_PREV_32(i); + + /* + * We missed the command? That should not happen if we process + * the buffer quickly enough (and the interrupt was real). + */ + if (i == dma_pos) { + CPRINTF("!"); + continue; + } + + /* + * We found the end of the command, now find the beginning + * (commands are 6-byte long so the starting point is either 2 + * or 1 word before the end of the command). + */ + i = RX_BUF_DEC_32(i, 2); + if (in_msg[i] == 0xffffffff) + i = RX_BUF_NEXT_32(i); + + cmd = emmc_parse_command(i); + + if (!tx) { + /* + * When not transferring, host will send GO_IDLE_STATE, + * GO_PRE_IDLE_STATE, then BOOT_INITIATION commands. But + * all we really care about is the BOOT_INITIATION + * command: start the transfer. + */ + if (cmd == EMMC_BOOT) { + tx = 1; + bootblock_transfer(); + } + } else { + /* + * Host sends GO_IDLE_STATE to abort the transfer (e.g. + * when an incorrect number of lanes is used) and when + * the transfer is complete. + * Also react to GO_PRE_IDLE_STATE in case we missed + * GO_IDLE_STATE command. + */ + if (cmd == EMMC_IDLE || cmd == EMMC_PRE_IDLE) { + bootblock_stop(); + tx = 0; + } + } + } +} diff --git a/board/kukui/gpio.inc b/board/kukui/gpio.inc index bbd60607e5..b4cd26bdc7 100644 --- a/board/kukui/gpio.inc +++ b/board/kukui/gpio.inc @@ -37,6 +37,10 @@ GPIO_INT(SYNC_INT, PIN(A, 5), GPIO_INT_RISING | GPIO_PULL_DOWN, sync_interrupt) GPIO_INT(CHARGER_INT_ODL, PIN(C, 13), GPIO_INPUT | GPIO_PULL_UP, rt946x_interrupt) +#ifdef SECTION_IS_RO +GPIO_INT(EMMC_CMD, PIN(A, 14), GPIO_INT_FALLING, + emmc_cmd_interrupt) +#endif /* Voltage rails control pins */ GPIO(PP3300_S0_EN, PIN(B, 6), GPIO_OUT_LOW) @@ -85,7 +89,11 @@ ALTERNATE(PIN_MASK(A, 0x0600), 1, MODULE_UART, 0) ALTERNATE(PIN_MASK(B, 0x0300), 1, MODULE_I2C, GPIO_ODR_HIGH | GPIO_PULL_UP) /* I2C MASTER: PA11/12 */ ALTERNATE(PIN_MASK(B, 0x1800), 5, MODULE_I2C, GPIO_ODR_HIGH | GPIO_PULL_UP) -/* SPI SLAVE: PB3/4/5/13/14/15 */ -ALTERNATE(PIN_MASK(B, 0xE038), 0, MODULE_SPI, 0) +/* SPI SLAVE: PB3/4/5 */ +ALTERNATE(PIN_MASK(B, 0x0038), 0, MODULE_SPI, 0) +#ifdef SECTION_IS_RO +/* SPI SLAVE: PB13/14/15 */ +ALTERNATE(PIN_MASK(B, 0xE000), 0, MODULE_SPI, 0) +#endif /* SPI SLAVE CS: PA15 */ ALTERNATE(PIN_MASK(A, 0x8000), 0, MODULE_SPI, 0) |