stm32h7: implement sdmmc_host-based SDMMC controller driver

Change-Id: I26c47c630ea364de043a224b549d7867fb1e5794
This commit is contained in:
Aidan MacDonald 2025-12-30 00:30:11 +00:00 committed by Solomon Peachy
parent 38db211a48
commit 141b4a223f
7 changed files with 579 additions and 85 deletions

View file

@ -2024,7 +2024,7 @@ target/arm/stm32/debug-stm32h7.c
target/arm/stm32/gpio-stm32h7.c
target/arm/stm32/i2c-stm32h7.c
target/arm/stm32/pcm-stm32h7.c
target/arm/stm32/sd-stm32h7.c
target/arm/stm32/sdmmc-stm32h7.c
target/arm/stm32/spi-stm32h7.c
target/arm/stm32/system-stm32h7.c
target/arm/stm32/timer-stm32h7.c

View file

@ -34,6 +34,8 @@ enum stm_clock
STM_CLOCK_SPI5_KER,
STM_CLOCK_SPI6_KER,
STM_CLOCK_LTDC_KER,
STM_CLOCK_SDMMC1_KER,
STM_CLOCK_SDMMC2_KER,
STM_NUM_CLOCKS,
};

View file

@ -28,7 +28,8 @@ void i2c_irq_handler(void) ATTR_IRQ_HANDLER;
void lcdc_irq_handler(void) ATTR_IRQ_HANDLER;
void otg_hs_irq_handler(void) ATTR_IRQ_HANDLER;
void sai_irq_handler(void) ATTR_IRQ_HANDLER;
void sdmmc_irq_handler(void) ATTR_IRQ_HANDLER;
void sdmmc1_irq_handler(void) ATTR_IRQ_HANDLER;
void sdmmc2_irq_handler(void) ATTR_IRQ_HANDLER;
void spi1_irq_handler(void) ATTR_IRQ_HANDLER;
void spi2_irq_handler(void) ATTR_IRQ_HANDLER;
void spi3_irq_handler(void) ATTR_IRQ_HANDLER;

View file

@ -1,81 +0,0 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 by Aidan MacDonald
*
* 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "storage.h"
#include "sdmmc.h"
#include "sd.h"
int sd_init(void)
{
return 0;
}
void sd_enable(bool on)
{
(void)on;
}
int sd_event(long id, intptr_t data)
{
return storage_event_default_handler(id, data, 0, STORAGE_SD);
}
long sd_last_disk_activity(void)
{
return 0;
}
bool sd_present(IF_MD_NONVOID(int drive))
{
IF_MD((void)drive);
return true;
}
bool sd_removable(IF_MD_NONVOID(int card_no))
{
IF_MD((void)card_no);
return true;
}
tCardInfo *card_get_info_target(int card_no)
{
(void)card_no;
static tCardInfo null_info = {0};
return &null_info;
}
int sd_read_sectors(IF_MD(int drive,) sector_t start, int count, void *buf)
{
IF_MD((void)drive);
(void)start;
(void)count;
(void)buf;
return -1;
}
int sd_write_sectors(IF_MD(int drive,) sector_t start, int count, const void *buf)
{
IF_MD((void)drive);
(void)start;
(void)count;
(void)buf;
return -1;
}

View file

@ -0,0 +1,497 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 by Aidan MacDonald
*
* 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#include "sdmmc-stm32h7.h"
#include "kernel.h"
#include "panic.h"
#include "regs/stm32h743/rcc.h"
#include "regs/stm32h743/sdmmc.h"
#include <string.h>
/* cmd_wait flags */
#define WAIT_CMD 0x01
#define WAIT_DATA 0x02
/* Maximum number of bytes for 1 transfer */
#define MAX_DATA_LEN \
(BM_SDMMC_DLENR_DATALENGTH >> BP_SDMMC_DLENR_DATALENGTH)
/* IRQs for command phase */
#define CMD_SUCCESS_BITS \
__reg_orm(SDMMC_STAR, CMDSENT, CMDREND)
#define CMD_ERROR_BITS \
__reg_orm(SDMMC_STAR, CTIMEOUT, CCRCFAIL)
#define CMD_END_BITS \
(CMD_SUCCESS_BITS | CMD_ERROR_BITS)
/* IRQs for data phase */
#define DATA_SUCCESS_BITS \
__reg_orm(SDMMC_STAR, DATAEND)
#define DATA_ERROR_BITS \
__reg_orm(SDMMC_STAR, DTIMEOUT, DABORT, DCRCFAIL, TXUNDERR, RXOVERR)
#define DATA_END_BITS \
(DATA_SUCCESS_BITS | DATA_ERROR_BITS)
static size_t get_sdmmc_bus_freq(uint32_t clock)
{
switch (clock)
{
case SDMMC_BUS_CLOCK_400KHZ: return 400000;
case SDMMC_BUS_CLOCK_25MHZ: return 25000000;
case SDMMC_BUS_CLOCK_50MHZ: return 50000000;
default: return 0;
}
}
static size_t abs_diff(size_t a, size_t b)
{
return a > b ? a - b : b - a;
}
static bool stm32h7_sdmmc_is_powered_off(struct stm32h7_sdmmc_controller *ctl)
{
uint32_t pwrctrl = reg_readlf(ctl->regs, SDMMC_POWER, PWRCTRL);
return pwrctrl == BV_SDMMC_POWER_PWRCTRL_POWER_CYCLE;
}
void stm32h7_reset_sdmmc1(void)
{
reg_writef(RCC_AHB3RSTR, SDMMC1RST(1));
reg_writef(RCC_AHB3RSTR, SDMMC1RST(0));
}
void stm32h7_sdmmc_init(struct stm32h7_sdmmc_controller *ctl,
uint32_t instance,
enum stm_clock clock,
void (*reset_sdmmc)(void),
void (*vcc_enable)(bool))
{
memset(ctl, 0, sizeof(ctl));
ctl->regs = instance;
ctl->clock = clock;
ctl->reset_sdmmc = reset_sdmmc;
ctl->vcc_enable = vcc_enable;
semaphore_init(&ctl->sem, 1, 0);
}
void stm32h7_sdmmc_set_power_enabled(void *controller, bool enabled)
{
struct stm32h7_sdmmc_controller *ctl = controller;
if (enabled)
{
/* Enable VCC */
if (ctl->vcc_enable)
ctl->vcc_enable(true);
/* Stay in power-off state for >= 1ms */
reg_writelf(ctl->regs, SDMMC_POWER, PWRCTRL_V(POWER_OFF));
sleep(1);
/* Bus clock is now needed, so enable kernel clock */
stm_clock_enable(ctl->clock);
/* Configure bus parameters */
stm32h7_sdmmc_set_bus_width(ctl, SDMMC_BUS_WIDTH_1BIT);
stm32h7_sdmmc_set_bus_clock(ctl, SDMMC_BUS_CLOCK_400KHZ);
reg_writelf(ctl->regs, SDMMC_CLKCR, PWRSAV(0));
/* Power on and wait for >= 74 SDMMC clock cycles (= 185us) */
reg_writelf(ctl->regs, SDMMC_POWER, PWRCTRL_V(POWER_ON));
udelay(200);
/* Automatically stop clock when bus is not in use */
reg_writelf(ctl->regs, SDMMC_CLKCR, PWRSAV(1));
}
else
{
/* Controller reset */
ctl->reset_sdmmc();
/*
* Disable kernel clock while powered down. The SDMMC block
* diagrams from the reference manual show that sdmmc_ker_ck
* is only used for the clock management subunit. Most likely
* the kernel clock isn't needed if the clock output is static
* and the bus is powered down; some quick testing shows this
* seems to be true.
*/
stm_clock_disable(ctl->clock);
/* Disable VCC */
if (ctl->vcc_enable)
ctl->vcc_enable(false);
/* Stay in power cycle state for >= 1ms */
reg_writelf(ctl->regs, SDMMC_POWER, PWRCTRL_V(POWER_CYCLE));
sleep(1);
}
}
void stm32h7_sdmmc_set_bus_width(void *controller, uint32_t width)
{
struct stm32h7_sdmmc_controller *ctl = controller;
/* Ignore requests from sdmmc_host while powered off */
if (stm32h7_sdmmc_is_powered_off(ctl))
return;
if (width == SDMMC_BUS_WIDTH_1BIT)
reg_writelf(ctl->regs, SDMMC_CLKCR, WIDBUS_V(1BIT));
else if (width == SDMMC_BUS_WIDTH_4BIT)
reg_writelf(ctl->regs, SDMMC_CLKCR, WIDBUS_V(4BIT));
else if (width == SDMMC_BUS_WIDTH_8BIT)
reg_writelf(ctl->regs, SDMMC_CLKCR, WIDBUS_V(8BIT));
else
panicf("%s", __func__);
}
void stm32h7_sdmmc_set_bus_clock(void *controller, uint32_t clock)
{
struct stm32h7_sdmmc_controller *ctl = controller;
/* Ignore requests from sdmmc_host while powered off */
if (stm32h7_sdmmc_is_powered_off(ctl))
return;
size_t ker_freq = stm_clock_get_frequency(ctl->clock);
size_t bus_freq = get_sdmmc_bus_freq(clock);
if (!bus_freq)
panicf("%s", __func__);
size_t div[2];
size_t freq[2];
/* Divider only supports even values, or can be bypassed (div=1) */
div[0] = ker_freq / bus_freq;
if (div[0] <= 1)
{
div[0] = 1;
div[1] = 2;
}
else
{
div[0] &= ~1u;
div[1] = div[0] + 2;
}
for (int i = 0; i < 2; ++i)
freq[i] = ker_freq / div[i];
/* Pick divider with lowest error */
int idx;
if (abs_diff(freq[0], bus_freq) < abs_diff(freq[1], bus_freq))
idx = 0;
else
idx = 1;
/* Set divider */
ctl->bus_freq = freq[idx];
reg_writelf(ctl->regs, SDMMC_CLKCR,
SELCLKRX_V(SDMMC_IO_IN_CK),
BUSSPEED_V(SLOW),
DDR(0),
NEGEDGE(0),
CLKDIV(div[idx] / 2));
}
int stm32h7_sdmmc_submit_command(void *controller,
const struct sdmmc_host_command *cmd,
struct sdmmc_host_response *resp)
{
struct stm32h7_sdmmc_controller *ctl = controller;
uint32_t maskr = CMD_ERROR_BITS;
uint32_t cmdr = __reg_orf(SDMMC_CMDR, CPSMEN(1), CMDINDEX(cmd->command));
uint32_t cmd_wait = WAIT_CMD;
void *buff_addr = cmd->buffer;
size_t buff_size = cmd->nr_blocks * cmd->block_len;
/*
* Handle response length setting
*/
switch (SDMMC_RESP_LENGTH(cmd->flags))
{
case SDMMC_RESP_NONE:
reg_vwritef(cmdr, SDMMC_CMDR, WAITRESP_V(NONE));
reg_vwritef(maskr, SDMMC_MASKR, CMDSENT(1));
break;
case SDMMC_RESP_SHORT:
if (cmd->flags & SDMMC_RESP_NOCRC)
{
reg_vwritef(cmdr, SDMMC_CMDR, WAITRESP_V(SHORT_NOCRC));
reg_vwritef(maskr, SDMMC_MASKR, CCRCFAIL(0));
}
else
{
reg_vwritef(cmdr, SDMMC_CMDR, WAITRESP_V(SHORT));
}
reg_vwritef(maskr, SDMMC_MASKR, CMDREND(1));
break;
case SDMMC_RESP_LONG:
reg_vwritef(cmdr, SDMMC_CMDR, WAITRESP_V(LONG));
reg_vwritef(maskr, SDMMC_MASKR, CMDREND(1));
break;
default:
panicf("%s: bad resp mode", __func__);
break;
}
/*
* Handle commands with data transfers
*/
if (SDMMC_DATA_PRESENT(cmd->flags))
{
if ((uintptr_t)buff_addr & (CACHEALIGN_SIZE - 1))
panicf("%s: unaligned buffer", __func__);
if (buff_size > MAX_DATA_LEN)
panicf("%s: buffer too big", __func__);
/*
* IDMA on the SDMMC controller can't access the DTCM.
* This is only possible by bounce-buffering in one of
* the other memories accessible to IDMA, then using
* another DMA process to copy the resulting buffer to
* DTCM, which seems unnecessarily convoluted.
*/
if ((uintptr_t)buff_addr >= STM32_DTCM_BASE &&
(uintptr_t)buff_addr < STM32_DTCM_BASE + STM32_DTCM_SIZE)
panicf("%s: buffer in DTCM not supported", __func__);
/* Set block size */
uint32_t dctrl = 0;
uint32_t dblocksize = find_first_set_bit(cmd->block_len);
if (dblocksize > 14 || (cmd->block_len & (cmd->block_len - 1)))
panicf("%s: incorrect block size", __func__);
reg_vwritef(dctrl, SDMMC_DCTRL, DBLOCKSIZE(dblocksize));
/* Set data direction & perform needed cache operations */
if (SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_WRITE)
{
commit_dcache_range(cmd->buffer, buff_size);
reg_vwritef(dctrl, SDMMC_DCTRL, DTDIR(0));
}
else
{
discard_dcache_range(cmd->buffer, buff_size);
reg_vwritef(dctrl, SDMMC_DCTRL, DTDIR(1));
}
/* Set buffer address in IDMA */
reg_varl(ctl->regs, SDMMC_IDMABASE0R) = (uintptr_t)buff_addr;
reg_assignlf(ctl->regs, SDMMC_IDMACTRLR, IDMAEN(1));
/* Use a 10 second timeout (DTIMER is in units of bus clocks) */
reg_varl(ctl->regs, SDMMC_DTIMER) = 10 * ctl->bus_freq;
reg_varl(ctl->regs, SDMMC_DLENR) = buff_size;
/* DCTRL must be written last */
reg_varl(ctl->regs, SDMMC_DCTRL) = dctrl;
/* Enable data phase */
reg_vwritef(cmdr, SDMMC_CMDR, CMDTRANS(1));
maskr |= DATA_END_BITS;
cmd_wait |= WAIT_DATA;
}
else
{
/* Disable data transfer */
reg_assignlf(ctl->regs, SDMMC_IDMACTRLR, IDMAEN(0));
reg_varl(ctl->regs, SDMMC_DTIMER) = 0;
reg_varl(ctl->regs, SDMMC_DLENR) = 0;
reg_varl(ctl->regs, SDMMC_DCTRL) = 0;
}
/*
* Set CMDSTOP bit for CMD12 (stop transmission) command;
* this is needed to stop the DPSM in case of an error in
* the previous data transfer command.
*/
if (cmd->command == SD_STOP_TRANSMISSION)
reg_vwritef(cmdr, SDMMC_CMDR, CMDSTOP(1));
/* Update controller state */
ctl->cmd_resp = resp;
ctl->cmd_wait = cmd_wait;
ctl->cmd_error = SDMMC_STATUS_OK;
ctl->need_cmd12 = false;
/* Barrier to ensure state is written before IRQ handler can run */
membarrier();
/* Unmask IRQs and begin command execution */
reg_varl(ctl->regs, SDMMC_MASKR) = maskr;
reg_varl(ctl->regs, SDMMC_ARGR) = cmd->argument;
reg_varl(ctl->regs, SDMMC_CMDR) = cmdr;
/* Wait for command completion */
semaphore_wait(&ctl->sem, TIMEOUT_BLOCK);
/*
* Discard data from speculative reads that may have
* accessed the buffer during the DMA transfer.
*/
int cmd_error = ctl->cmd_error;
if (cmd_error == SDMMC_STATUS_OK)
{
if (SDMMC_DATA_DIR(cmd->flags) == SDMMC_DATA_READ)
discard_dcache_range(buff_addr, buff_size);
}
/*
* If a data transfer command fails we need to issue CMD12
* to stop DMA before returning. There is no other way to
* abort the DMA transfer.
*/
if (ctl->need_cmd12)
{
static const struct sdmmc_host_command cmd12 = {
.command = SD_STOP_TRANSMISSION,
.flags = SDMMC_RESP_SHORT | SDMMC_RESP_BUSY,
};
/*
* Recursion depth is limited to 1 because this can
* only happen due to a failed data transfer command,
* and CMD12 is not a data transfer command.
*/
stm32h7_sdmmc_submit_command(ctl, &cmd12, NULL);
}
/* Return error from original command */
return cmd_error;
}
void stm32h7_sdmmc_abort_command(void *controller)
{
struct stm32h7_sdmmc_controller *ctl = controller;
/* Prevent the IRQ handler from preempting us */
reg_varl(ctl->regs, SDMMC_MASKR) = 0;
arm_dsb();
/*
* Cancel any waiting command, including asking for
* CMD12 if we need to abort DMA.
*/
if (ctl->cmd_wait)
{
if (ctl->cmd_wait & WAIT_DATA)
ctl->need_cmd12 = true;
ctl->cmd_wait = 0;
semaphore_release(&ctl->sem);
}
}
void stm32h7_sdmmc_irq_handler(struct stm32h7_sdmmc_controller *ctl)
{
uint32_t star = reg_readl(ctl->regs, SDMMC_STAR);
uint32_t maskr = reg_readl(ctl->regs, SDMMC_MASKR);
uint32_t icr = 0;
if (!ctl->cmd_wait)
panicf("sdmmc_irq: not waiting: %08lx", star);
/*
* Ignore interrupts which we haven't enabled; this is needed
* to ensure the correct interrupt is used to detect command
* completion (CMDSENT or CMDREND).
*/
star &= maskr;
if (ctl->cmd_wait & WAIT_CMD)
{
if (star & CMD_END_BITS)
{
if (reg_vreadf(star, SDMMC_STAR, CTIMEOUT))
ctl->cmd_error = SDMMC_STATUS_TIMEOUT;
else if (reg_vreadf(star, SDMMC_STAR, CCRCFAIL))
ctl->cmd_error = SDMMC_STATUS_INVALID_CRC;
/* Copy the response, if present */
if (ctl->cmd_resp && reg_vreadf(star, SDMMC_STAR, CMDREND))
{
ctl->cmd_resp->data[0] = reg_readl(ctl->regs, SDMMC_RESPR(0));
ctl->cmd_resp->data[1] = reg_readl(ctl->regs, SDMMC_RESPR(1));
ctl->cmd_resp->data[2] = reg_readl(ctl->regs, SDMMC_RESPR(2));
ctl->cmd_resp->data[3] = reg_readl(ctl->regs, SDMMC_RESPR(3));
ctl->cmd_resp = NULL;
}
ctl->cmd_wait &= ~WAIT_CMD;
icr |= CMD_END_BITS;
}
}
if (ctl->cmd_wait & WAIT_DATA)
{
if ((star & DATA_END_BITS) || ctl->cmd_error)
{
if (ctl->cmd_error)
{
/*
* An error in the command phase of a data transfer
* command may leave the DPSM active & DMA ongoing.
* Set a flag so submit_command() can handle error
* recovery.
*/
ctl->need_cmd12 = true;
}
else
{
if (reg_vreadf(star, SDMMC_STAR, DTIMEOUT))
ctl->cmd_error = SDMMC_STATUS_TIMEOUT;
else if (reg_vreadf(star, SDMMC_STAR, DCRCFAIL))
ctl->cmd_error = SDMMC_STATUS_INVALID_CRC;
else if (star & DATA_ERROR_BITS)
ctl->cmd_error = SDMMC_STATUS_ERROR;
}
ctl->cmd_wait &= ~WAIT_DATA;
icr |= DATA_END_BITS;
}
}
/*
* Each interrupt should complete at least one phase
* but in error cases we can get spurious interrupts.
*/
if (icr)
{
/* Clear handled interrupts */
reg_varl(ctl->regs, SDMMC_MASKR) &= ~icr;
reg_varl(ctl->regs, SDMMC_ICR) = icr;
}
/* Signal command complete if there are no more phases to wait for */
if (ctl->cmd_wait == 0)
semaphore_release(&ctl->sem);
}

View file

@ -0,0 +1,75 @@
/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2025 by Aidan MacDonald
*
* 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
#ifndef __SDMMC_STM32H7_H__
#define __SDMMC_STM32H7_H__
#include "sdmmc_host.h"
#include "semaphore.h"
#include "clock-stm32h7.h"
struct stm32h7_sdmmc_controller
{
/* SDMMC instance address */
uint32_t regs;
/* SDMMC kernel clock */
enum stm_clock clock;
/* Callback to reset SDMMC instance in RCC */
void (*reset_sdmmc)(void);
/* Callback to enable/disable VCC supply to card */
void (*vcc_enable)(bool);
/* Semaphore to wait for command completion */
struct semaphore sem;
/* Current SDMMC_CK frequency in Hz (used for timeout calculation) */
uint32_t bus_freq;
/*
* Command handling state, may be read & written
* by both thread & IRQ handler.
*/
struct sdmmc_host_response *cmd_resp;
uint32_t cmd_wait;
int cmd_error;
int need_cmd12;
};
void stm32h7_reset_sdmmc1(void);
void stm32h7_sdmmc_init(struct stm32h7_sdmmc_controller *controller,
uint32_t instance,
enum stm_clock clock,
void (*reset_sdmmc)(void),
void (*vcc_enable)(bool));
void stm32h7_sdmmc_set_power_enabled(void *controller, bool enabled);
void stm32h7_sdmmc_set_bus_width(void *controller, uint32_t width);
void stm32h7_sdmmc_set_bus_clock(void *controller, uint32_t clock);
int stm32h7_sdmmc_submit_command(void *controller,
const struct sdmmc_host_command *cmd,
struct sdmmc_host_response *resp);
void stm32h7_sdmmc_abort_command(void *controller);
void stm32h7_sdmmc_irq_handler(struct stm32h7_sdmmc_controller *controller);
#endif /* __SDMMC_STM32H7_H__ */

View file

@ -76,7 +76,7 @@ __vectors_platform:
.word UIE /* [ 46] */
.word dma_irq_handler /* [ 47] DMA1 Stream7 */
.word UIE /* [ 48] */
.word sdmmc_irq_handler /* [ 49] SDMMC1 */
.word sdmmc1_irq_handler /* [ 49] SDMMC1 */
.word UIE /* [ 50] */
.word spi3_irq_handler /* [ 51] SPI3 */
.word UIE /* [ 52] */
@ -151,7 +151,7 @@ __vectors_platform:
.word UIE /* [121] */
.word dma_irq_handler /* [122] MDMA */
.word UIE /* [123] */
.word sdmmc_irq_handler /* [124] SDMMC2 */
.word sdmmc2_irq_handler /* [124] SDMMC2 */
.word UIE /* [125] */
.word UIE /* [126] */
.word UIE /* [127] */