Add callback of half complete in DMA processing, Fix DMA size bug. Signed-off-by: Gang He <ganghe@sifli.com>
493 lines
16 KiB
C
493 lines
16 KiB
C
/*
|
|
* Copyright (c) 2025 Core Devices LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define DT_DRV_COMPAT sifli_sf32lb_dmac
|
|
|
|
#include <zephyr/arch/cpu.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/devicetree.h>
|
|
#include <zephyr/drivers/clock_control/sf32lb.h>
|
|
#include <zephyr/drivers/dma.h>
|
|
#include <zephyr/logging/log.h>
|
|
#include <zephyr/irq.h>
|
|
#include <zephyr/spinlock.h>
|
|
#include <zephyr/sys/atomic.h>
|
|
#include <zephyr/toolchain.h>
|
|
|
|
#include <register.h>
|
|
|
|
LOG_MODULE_REGISTER(dma_sf32lb, CONFIG_DMA_LOG_LEVEL);
|
|
|
|
#define DMAC_MAX_LEN DMAC_CNDTR1_NDT
|
|
#define DMAC_MAX_PL 3U
|
|
|
|
#define DMAC_ISR offsetof(DMAC_TypeDef, ISR)
|
|
#define DMAC_IFCR offsetof(DMAC_TypeDef, IFCR)
|
|
#define DMAC_CCR1 offsetof(DMAC_TypeDef, CCR1)
|
|
#define DMAC_CCR2 offsetof(DMAC_TypeDef, CCR2)
|
|
#define DMAC_CCRX(n) (DMAC_CCR1 + (DMAC_CCR2 - DMAC_CCR1) * (n))
|
|
#define DMAC_CNDTR1 offsetof(DMAC_TypeDef, CNDTR1)
|
|
#define DMAC_CNDTR2 offsetof(DMAC_TypeDef, CNDTR2)
|
|
#define DMAC_CNDTRX(n) (DMAC_CNDTR1 + (DMAC_CNDTR2 - DMAC_CNDTR1) * (n))
|
|
#define DMAC_CPAR1 offsetof(DMAC_TypeDef, CPAR1)
|
|
#define DMAC_CPAR2 offsetof(DMAC_TypeDef, CPAR2)
|
|
#define DMAC_CPARX(n) (DMAC_CPAR1 + (DMAC_CPAR2 - DMAC_CPAR1) * (n))
|
|
#define DMAC_CM0AR1 offsetof(DMAC_TypeDef, CM0AR1)
|
|
#define DMAC_CM0AR2 offsetof(DMAC_TypeDef, CM0AR2)
|
|
#define DMAC_CM0ARX(n) (DMAC_CM0AR1 + (DMAC_CM0AR2 - DMAC_CM0AR1) * (n))
|
|
#define DMAC_CBSR1 offsetof(DMAC_TypeDef, CBSR1)
|
|
#define DMAC_CBSR2 offsetof(DMAC_TypeDef, CBSR2)
|
|
#define DMAC_CBSRX(n) (DMAC_CBSR1 + (DMAC_CBSR2 - DMAC_CBSR1) * (n))
|
|
#define DMAC_CSELR1 offsetof(DMAC_TypeDef, CSELR1)
|
|
#define DMAC_CSELR2 offsetof(DMAC_TypeDef, CSELR2)
|
|
|
|
#define DMAC_ISR_TCIF(n) (DMAC_ISR_TCIF1_Msk << (n * 4U))
|
|
#define DMAC_ISR_HTIF(n) (DMAC_ISR_HTIF1_Msk << (n * 4U))
|
|
|
|
#define DMAC_IFCR_ALL(n) \
|
|
((DMAC_IFCR_CGIF1_Msk | DMAC_IFCR_CTCIF1_Msk | DMAC_IFCR_CHTIF1_Msk | \
|
|
DMAC_IFCR_CTEIF1_Msk) \
|
|
<< (n * 4U))
|
|
#define DMAC_IFCR_CTCIF(n) (DMAC_IFCR_CTCIF1_Msk << (n * 4U))
|
|
#define DMAC_IFCR_CTEIF(n) (DMAC_IFCR_CTEIF1_Msk << (n * 4U))
|
|
#define DMAC_IFCR_CTHIF(n) (DMAC_IFCR_CHTIF1_Msk << (n * 4U))
|
|
|
|
#define DMAC_CCRX_PSIZE(n) FIELD_PREP(DMAC_CCR1_PSIZE_Msk, LOG2CEIL(n))
|
|
#define DMAC_CCRX_MSIZE(n) FIELD_PREP(DMAC_CCR1_MSIZE_Msk, LOG2CEIL(n))
|
|
|
|
#define DMAC_MAX_CH 8U
|
|
|
|
struct dma_sf32lb_irq_ctx {
|
|
const struct device *dev;
|
|
uint8_t channel;
|
|
};
|
|
|
|
struct dma_sf32lb_config {
|
|
uintptr_t dmac;
|
|
uint8_t n_channels;
|
|
uint8_t n_requests;
|
|
struct sf32lb_clock_dt_spec clock;
|
|
void (*irq_configure)(void);
|
|
struct dma_sf32lb_channel *channels;
|
|
};
|
|
|
|
struct dma_sf32lb_channel {
|
|
dma_callback_t callback;
|
|
void *user_data;
|
|
uint32_t size;
|
|
enum dma_channel_direction direction;
|
|
};
|
|
|
|
struct dma_sf32lb_data {
|
|
struct dma_context ctx;
|
|
struct k_spinlock lock;
|
|
ATOMIC_DEFINE(status, DMAC_MAX_CH);
|
|
};
|
|
|
|
static void dma_sf32lb_isr(const struct device *dev, uint8_t channel)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
struct dma_sf32lb_data *data = dev->data;
|
|
uint32_t isr;
|
|
int status;
|
|
|
|
isr = sys_read32(config->dmac + DMAC_ISR);
|
|
if ((isr & DMAC_ISR_TCIF(channel)) != 0U) {
|
|
status = DMA_STATUS_COMPLETE;
|
|
} else if ((isr & DMAC_ISR_HTIF(channel)) != 0U) {
|
|
status = DMA_STATUS_HALF_COMPLETE;
|
|
atomic_clear_bit(data->status, channel);
|
|
} else if (isr) {
|
|
status = -EIO;
|
|
} else {
|
|
status = -EINPROGRESS;
|
|
}
|
|
|
|
if (status != -EINPROGRESS && config->channels[channel].callback != NULL) {
|
|
config->channels[channel].callback(dev, config->channels[channel].user_data,
|
|
channel, status);
|
|
}
|
|
|
|
sys_write32(DMAC_IFCR_ALL(channel), config->dmac + DMAC_IFCR);
|
|
}
|
|
|
|
#define DMA_SF32LB_IRQ_DEFINE(n, _) \
|
|
static void dma_sf32lb_isr_ch##n(const struct device *dev) \
|
|
{ \
|
|
dma_sf32lb_isr(dev, n); \
|
|
}
|
|
|
|
LISTIFY(8, DMA_SF32LB_IRQ_DEFINE, ())
|
|
|
|
static int check_dma_config(uint32_t channel, struct dma_config *config_dma,
|
|
const struct dma_sf32lb_config *config)
|
|
{
|
|
if (channel >= config->n_channels) {
|
|
LOG_ERR("Invalid channel (%" PRIu32 ", max %" PRIu32 ")", channel,
|
|
config->n_channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config_dma->block_count != 1U) {
|
|
LOG_ERR("Chained block transfer not supported (%" PRIu32 ", max 1)",
|
|
config_dma->block_count);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if (config_dma->head_block->block_size > DMAC_MAX_LEN) {
|
|
LOG_ERR("Block size exceeds maximum (%" PRIu32 ", max %lu)",
|
|
config_dma->head_block->block_size, DMAC_MAX_LEN);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config_dma->dma_slot >= config->n_requests) {
|
|
LOG_ERR("Invalid DMA slot (%" PRIu32 ", max %" PRIu32 ")", config_dma->dma_slot,
|
|
config->n_requests);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config_dma->channel_priority > DMAC_MAX_PL) {
|
|
LOG_ERR("Invalid channel priority (%" PRIu32 ", max %" PRIu32 ")",
|
|
config_dma->channel_priority, DMAC_MAX_PL);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((config_dma->head_block->source_addr_adj == DMA_ADDR_ADJ_DECREMENT) |
|
|
(config_dma->head_block->dest_addr_adj == DMA_ADDR_ADJ_DECREMENT)) {
|
|
LOG_ERR("Address decrement not supported");
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
if ((config_dma->source_data_size != 1U) && (config_dma->source_data_size != 2U) &&
|
|
(config_dma->source_data_size != 4U)) {
|
|
LOG_ERR("Invalid source data size (%" PRIu32 ", must be 1, 2, or 4)",
|
|
config_dma->source_data_size);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((config_dma->dest_data_size != 1U) && (config_dma->dest_data_size != 2U) &&
|
|
(config_dma->dest_data_size != 4U)) {
|
|
LOG_ERR("Invalid destination data size (%" PRIu32 ", must be 1, 2, or 4)",
|
|
config_dma->dest_data_size);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (config_dma->dest_data_size != config_dma->source_data_size) {
|
|
LOG_ERR("Destination and source sizes not equal");
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dma_sf32lb_config(const struct device *dev, uint32_t channel,
|
|
struct dma_config *config_dma)
|
|
{
|
|
int ret;
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
struct dma_sf32lb_data *data = dev->data;
|
|
uint32_t ccrx;
|
|
uint32_t cselrx;
|
|
uint32_t cparx;
|
|
uint32_t cm0arx;
|
|
|
|
ret = check_dma_config(channel, config_dma, config);
|
|
if (ret < 0) {
|
|
return ret;
|
|
}
|
|
|
|
/* configure transfer parameters */
|
|
ccrx = sys_read32(config->dmac + DMAC_CCRX(channel));
|
|
if ((ccrx & DMAC_CCR1_EN) != 0U) {
|
|
LOG_ERR("Configuration not possible with DMA enabled");
|
|
return -EIO;
|
|
}
|
|
|
|
ccrx &= ~(DMAC_CCR1_TCIE | DMAC_CCR1_HTIE | DMAC_CCR1_TEIE | DMAC_CCR1_DIR_Msk |
|
|
DMAC_CCR1_CIRC_Msk | DMAC_CCR1_PINC_Msk | DMAC_CCR1_MINC_Msk |
|
|
DMAC_CCR1_PSIZE_Msk | DMAC_CCR1_MSIZE_Msk | DMAC_CCR1_PL_Msk |
|
|
DMAC_CCR1_MEM2MEM_Msk);
|
|
|
|
ccrx |= FIELD_PREP(DMAC_CCR1_PL_Msk, config_dma->channel_priority);
|
|
|
|
if (config_dma->head_block->dest_reload_en || config_dma->head_block->source_reload_en) {
|
|
ccrx |= DMAC_CCR1_CIRC;
|
|
}
|
|
|
|
if (config_dma->half_complete_callback_en) {
|
|
ccrx |= DMAC_CCR1_HTIE;
|
|
}
|
|
|
|
switch (config_dma->channel_direction) {
|
|
case MEMORY_TO_MEMORY:
|
|
ccrx |= DMAC_CCR1_MEM2MEM;
|
|
__fallthrough;
|
|
case PERIPHERAL_TO_MEMORY:
|
|
ccrx |= DMAC_CCRX_PSIZE(config_dma->source_data_size) |
|
|
DMAC_CCRX_MSIZE(config_dma->dest_data_size);
|
|
|
|
if (config_dma->head_block->source_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
|
|
ccrx |= DMAC_CCR1_PINC;
|
|
}
|
|
if (config_dma->head_block->dest_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
|
|
ccrx |= DMAC_CCR1_MINC;
|
|
}
|
|
|
|
cparx = config_dma->head_block->source_address;
|
|
cm0arx = config_dma->head_block->dest_address;
|
|
break;
|
|
case MEMORY_TO_PERIPHERAL:
|
|
ccrx |= DMAC_CCR1_DIR | DMAC_CCRX_PSIZE(config_dma->dest_data_size) |
|
|
DMAC_CCRX_MSIZE(config_dma->source_data_size);
|
|
|
|
if (config_dma->head_block->source_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
|
|
ccrx |= DMAC_CCR1_MINC;
|
|
}
|
|
if (config_dma->head_block->dest_addr_adj == DMA_ADDR_ADJ_INCREMENT) {
|
|
ccrx |= DMAC_CCR1_PINC;
|
|
}
|
|
|
|
cparx = config_dma->head_block->dest_address;
|
|
cm0arx = config_dma->head_block->source_address;
|
|
break;
|
|
default:
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
sys_write32(ccrx, config->dmac + DMAC_CCRX(channel));
|
|
|
|
/* single transfer */
|
|
sys_write32(FIELD_PREP(DMAC_CBSR1_BS_Msk, 0U), config->dmac + DMAC_CBSRX(channel));
|
|
|
|
/* configure transfer size, src/dst addresses */
|
|
sys_write32(config_dma->head_block->block_size, config->dmac + DMAC_CNDTRX(channel));
|
|
sys_write32(cparx, config->dmac + DMAC_CPARX(channel));
|
|
sys_write32(cm0arx, config->dmac + DMAC_CM0ARX(channel));
|
|
|
|
/* configure request */
|
|
K_SPINLOCK(&data->lock) {
|
|
if (channel < 4U) {
|
|
cselrx = sys_read32(config->dmac + DMAC_CSELR1);
|
|
cselrx &= ~(DMAC_CSELR1_C1S_Msk << (channel * 8U));
|
|
cselrx |= FIELD_PREP(DMAC_CSELR1_C1S_Msk << (channel * 8U),
|
|
config_dma->dma_slot);
|
|
sys_write32(cselrx, config->dmac + DMAC_CSELR1);
|
|
} else {
|
|
cselrx = sys_read32(config->dmac + DMAC_CSELR2);
|
|
cselrx &= ~(DMAC_CSELR1_C1S_Msk << ((channel - 4U) * 8U));
|
|
cselrx |= FIELD_PREP(DMAC_CSELR1_C1S_Msk << ((channel - 4U) * 8U),
|
|
config_dma->dma_slot);
|
|
sys_write32(cselrx, config->dmac + DMAC_CSELR2);
|
|
}
|
|
}
|
|
|
|
config->channels[channel].callback = config_dma->dma_callback;
|
|
config->channels[channel].user_data = config_dma->user_data;
|
|
config->channels[channel].direction = config_dma->channel_direction;
|
|
config->channels[channel].size = config_dma->source_data_size;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dma_sf32lb_reload(const struct device *dev, uint32_t channel, uint32_t src, uint32_t dst,
|
|
size_t size)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
uint32_t ccrx;
|
|
uint32_t cparx;
|
|
uint32_t cm0arx;
|
|
|
|
if (channel >= config->n_channels) {
|
|
LOG_ERR("Invalid channel (%" PRIu32 ", max %" PRIu32 ")", channel,
|
|
config->n_channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (size > DMAC_MAX_LEN) {
|
|
LOG_ERR("Block size exceeds maximum (%" PRIu32 ", max %lu)", size, DMAC_MAX_LEN);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ccrx = sys_read32(config->dmac + DMAC_CCRX(channel));
|
|
if ((ccrx & DMAC_CCR1_EN) != 0U) {
|
|
LOG_ERR("Channel %" PRIu32 " is busy", channel);
|
|
return -EBUSY;
|
|
}
|
|
|
|
/* configure size, src/dst addresses */
|
|
if (config->channels[channel].size == 4) {
|
|
size >>= 2;
|
|
} else if (config->channels[channel].size == 2) {
|
|
size >>= 1;
|
|
} else {
|
|
}
|
|
|
|
sys_write32(size, config->dmac + DMAC_CNDTRX(channel));
|
|
|
|
switch (config->channels[channel].direction) {
|
|
case MEMORY_TO_MEMORY:
|
|
case PERIPHERAL_TO_MEMORY:
|
|
cparx = src;
|
|
cm0arx = dst;
|
|
break;
|
|
case MEMORY_TO_PERIPHERAL:
|
|
cparx = dst;
|
|
cm0arx = src;
|
|
break;
|
|
default:
|
|
__ASSERT_NO_MSG(false);
|
|
return -ENOTSUP;
|
|
}
|
|
|
|
sys_write32(cparx, config->dmac + DMAC_CPARX(channel));
|
|
sys_write32(cm0arx, config->dmac + DMAC_CM0ARX(channel));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dma_sf32lb_start(const struct device *dev, uint32_t channel)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
struct dma_sf32lb_data *data = dev->data;
|
|
uint32_t ccrx;
|
|
|
|
if (channel >= config->n_channels) {
|
|
LOG_ERR("Invalid channel (%" PRIu32 ", max %" PRIu32 ")", channel,
|
|
config->n_channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
ccrx = sys_read32(config->dmac + DMAC_CCRX(channel));
|
|
if ((ccrx & DMAC_CCR1_EN) != 0U) {
|
|
LOG_ERR("start not possible with DMA enabled");
|
|
return -EIO;
|
|
}
|
|
|
|
/* clear all transfer flags */
|
|
sys_write32(DMAC_IFCR_ALL(channel), config->dmac + DMAC_IFCR);
|
|
|
|
/* enable DMA, complete/error IRQs if callback configured */
|
|
ccrx |= DMAC_CCR1_EN;
|
|
if (config->channels[channel].callback != NULL) {
|
|
ccrx |= DMAC_CCR1_TCIE | DMAC_CCR1_TEIE;
|
|
}
|
|
sys_write32(ccrx, config->dmac + DMAC_CCRX(channel));
|
|
atomic_set_bit(data->status, channel);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dma_sf32lb_stop(const struct device *dev, uint32_t channel)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
struct dma_sf32lb_data *data = dev->data;
|
|
uint32_t ccrx;
|
|
|
|
if (channel >= config->n_channels) {
|
|
LOG_ERR("Invalid channel (%" PRIu32 ", max %" PRIu32 ")", channel,
|
|
config->n_channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* disable DMA and complete/error IRQs */
|
|
ccrx = sys_read32(config->dmac + DMAC_CCRX(channel));
|
|
ccrx &= ~(DMAC_CCR1_EN | DMAC_CCR1_TCIE | DMAC_CCR1_TEIE);
|
|
sys_write32(ccrx, config->dmac + DMAC_CCRX(channel));
|
|
|
|
atomic_clear_bit(data->status, channel);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dma_sf32lb_get_status(const struct device *dev, uint32_t channel,
|
|
struct dma_status *stat)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
struct dma_sf32lb_data *data = dev->data;
|
|
|
|
if (channel >= config->n_channels) {
|
|
LOG_ERR("Invalid channel (%" PRIu32 ", max %" PRIu32 ")", channel,
|
|
config->n_channels);
|
|
return -EINVAL;
|
|
}
|
|
|
|
stat->busy = atomic_test_bit(data->status, channel);
|
|
stat->dir = config->channels[channel].direction;
|
|
stat->pending_length = sys_read32(config->dmac + DMAC_CNDTRX(channel));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static DEVICE_API(dma, dma_sf32lb_driver_api) = {
|
|
.config = dma_sf32lb_config,
|
|
.reload = dma_sf32lb_reload,
|
|
.start = dma_sf32lb_start,
|
|
.stop = dma_sf32lb_stop,
|
|
.get_status = dma_sf32lb_get_status,
|
|
};
|
|
|
|
static int dma_sf32lb_init(const struct device *dev)
|
|
{
|
|
const struct dma_sf32lb_config *config = dev->config;
|
|
|
|
if (!sf32lb_clock_is_ready_dt(&config->clock)) {
|
|
return -ENODEV;
|
|
}
|
|
|
|
(void)sf32lb_clock_control_on_dt(&config->clock);
|
|
|
|
for (uint8_t channel = 0U; channel < config->n_channels; channel++) {
|
|
uint32_t ccrx;
|
|
|
|
ccrx = sys_read32(config->dmac + DMAC_CCRX(channel));
|
|
ccrx &= ~(DMAC_CCR1_EN | DMAC_CCR1_TCIE | DMAC_CCR1_HTIE | DMAC_CCR1_TEIE);
|
|
sys_write32(ccrx, config->dmac + DMAC_CCRX(channel));
|
|
}
|
|
|
|
config->irq_configure();
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define DMA_SF32LB_IRQ_CONFIGURE(n, inst) \
|
|
IRQ_CONNECT(DT_INST_IRQ_BY_IDX(inst, n, irq), DT_INST_IRQ_BY_IDX(inst, n, priority), \
|
|
dma_sf32lb_isr_ch##n, DEVICE_DT_INST_GET(inst), 0); \
|
|
irq_enable(DT_INST_IRQ_BY_IDX(inst, n, irq));
|
|
|
|
#define DMA_SF32LB_CONFIGURE_ALL_IRQS(inst, n) LISTIFY(n, DMA_SF32LB_IRQ_CONFIGURE, (), inst)
|
|
|
|
#define DMA_SF32LB_DEFINE(inst) \
|
|
static void irq_configure##inst(void) \
|
|
{ \
|
|
DMA_SF32LB_CONFIGURE_ALL_IRQS(inst, DT_INST_NUM_IRQS(inst)); \
|
|
} \
|
|
\
|
|
static struct dma_sf32lb_channel channels##inst[DT_INST_PROP(inst, dma_channels)]; \
|
|
\
|
|
static const struct dma_sf32lb_config config##inst = { \
|
|
.dmac = DT_INST_REG_ADDR(inst), \
|
|
.n_channels = DT_INST_PROP(inst, dma_channels), \
|
|
.n_requests = DT_INST_PROP(inst, dma_requests), \
|
|
.clock = SF32LB_CLOCK_DT_INST_SPEC_GET(inst), \
|
|
.irq_configure = irq_configure##inst, \
|
|
.channels = channels##inst, \
|
|
}; \
|
|
\
|
|
ATOMIC_DEFINE(atomic##inst, DT_INST_PROP(inst, dma_channels)); \
|
|
\
|
|
static struct dma_sf32lb_data data##inst = { \
|
|
.ctx = \
|
|
{ \
|
|
.magic = DMA_MAGIC, \
|
|
.atomic = atomic##inst, \
|
|
.dma_channels = DT_INST_PROP(inst, dma_channels), \
|
|
}, \
|
|
}; \
|
|
\
|
|
DEVICE_DT_INST_DEFINE(inst, dma_sf32lb_init, NULL, &data##inst, &config##inst, \
|
|
PRE_KERNEL_1, CONFIG_DMA_INIT_PRIORITY, &dma_sf32lb_driver_api);
|
|
|
|
DT_INST_FOREACH_STATUS_OKAY(DMA_SF32LB_DEFINE)
|