PL330 TRM DMAC深度解析与应用实战
PL330 DMAC不仅仅是一个IP核,它体现了一种“智能卸载”的系统设计理念。通过微码编程、多通道并发、优先级调度、事件同步等机制,它让我们能把CPU从繁琐的数据搬运中解放出来,专注更高层次的逻辑处理。掌握它的精髓,不仅是为了写出高效的驱动,更是为了理解如何构建一个真正高性能、低延迟、高可靠的嵌入式系统。“最好的代码,是让硬件替你思考。” —— 致每一位深耕底层的工程师 🙇♂️✨。
简介:PL330是ARM推出的高性能多通道DMA控制器,广泛应用于嵌入式与移动系统中,通过在无需CPU干预的情况下实现内存与外设间高效数据传输,显著提升系统效率。本文深入讲解PL330的架构设计、多通道配置、优先级与中断管理、错误处理机制及地址数据宽度支持等核心特性,并结合DDI0424A技术参考手册,系统梳理其编程模型与软件接口,帮助开发者掌握PL330在复杂数据传输场景中的实际应用与优化方法。
PL330 DMA控制器架构与高级应用深度解析
在现代嵌入式系统中,CPU的算力早已不再是瓶颈,真正的挑战往往藏在“数据搬运”这件看似简单的事上。想象一下:一个4K视频流以每秒60帧的速度涌入处理器,每帧超过800万像素点,如果全靠CPU逐字节拷贝——那画面还没渲染出来,系统早就卡死了 😅。
这正是DMA(Direct Memory Access)技术存在的意义——让数据自己“走路”,而CPU则专注于真正需要它智慧的任务。而在众多DMA控制器中,ARM推出的 PL330 DMAC 堪称经典之作。它不是简单的“搬运工”,而是一位懂得编排复杂动作的“微码舞者”。今天,我们就来揭开它的神秘面纱,从底层架构到实战调优,一步步走进这个高效数据通路背后的设计哲学。
多通道独立配置与并发传输实现
说到PL330最让人眼前一亮的地方,莫过于它的 多通道并行能力 。你可能会问:“不就是多个DMA同时干活吗?有啥特别?”
别急,咱们先看个场景👇
假设你的设备正在做三件事:
- 实时采集麦克风音频(每10ms一包)
- 从摄像头读取图像帧(每33ms一帧)
- 后台偷偷把日志写进eMMC
这三个任务对延迟的要求完全不同:音频慢了会爆音,视频卡顿还能忍,日志更是无所谓。要是用单通道DMA轮着来?完蛋,音频肯定被拖垮 🫠。
但PL330最多支持8个独立DMA通道(具体数量取决于SoC厂商实现),每个通道都像一辆专车,有自己的司机、路线和油箱。它们可以同时发车,互不干扰,这才叫真正的并行!
2.1 多通道架构的设计原理
2.1.1 通道资源的物理隔离与共享机制
PL330的设计非常聪明,采用了“ 共享控制 + 独立上下文 ”的混合架构,既节省硬件成本,又保证性能。
来看一张核心拓扑图:
graph TD
A[PL330 DMA Core] --> B(共享资源)
A --> C(通道0 - 私有资源)
A --> D(通道1 - 私有资源)
A --> E(通道N - 私有资源)
B --> F[AHB Master Interface]
B --> G[Interrupt Controller]
B --> H[Event Flag Register]
B --> I[Microcode Engine]
C --> J[SAR0, DAR0, CH0_CTRL]
C --> K[Channel FSM0]
D --> L[SAR1, DAR1, CH1_CTRL]
D --> M[Channel FSM1]
E --> N[SARN, DARN, CHN_CTRL]
E --> O[Channel FSMN]
style B fill:#f9f,stroke:#333
style C fill:#bbf,stroke:#333
style D fill:#bbf,stroke:#333
style E fill:#bbf,stroke:#333
这张图清晰地告诉我们:所有通道共用总线接口、中断逻辑和微码引擎,但每个通道都有自己专属的源/目标地址寄存器(SAR/DAR)、控制寄存器和状态机。
这就意味着:
- 通道0可以在等UART FIFO腾空的时候“发呆”
- 而通道1仍然能高速地往SPI里灌数据
- 它们之间不会互相拖后腿 ✅
下表列出了典型资源的归属情况:
| 资源名称 | 是否共享 | 说明 |
|---|---|---|
| AHB主接口 | 是 | 所有通道通过同一主端口访问系统总线 |
| 中断事件标志位 | 是 | 共享但每位对应不同通道或事件类型 |
| 每通道SAR/DAR | 否 | 每个通道拥有独立的源/目的地址寄存器 |
| 微码指令存储 | 是 | 固定ROM中的微码由所有通道共享 |
| 通道状态机 | 否 | 每个通道有独立FSM控制执行流程 |
| 总线请求仲裁器 | 是 | 协调多个通道对总线的访问请求 |
这种设计就像一栋写字楼:大家共用电梯(总线)、门禁系统(中断),但每个人都有自己的办公室(私有寄存器)和工作节奏,效率自然就上去了 💼。
2.1.2 每通道独立状态机运行模型
每个DMA通道内部其实藏着一个专用的有限状态机(Finite State Machine, FSM),它是整个传输过程的大脑🧠。
它的生命周期大概是这样的:
typedef enum {
STATE_IDLE,
STATE_PREPARE,
STATE_RUNNING,
STATE_WAITING,
STATE_PAUSED,
STATE_COMPLETED
} dmach_state_t;
这些状态可不是随便设的,每一个都代表着实际的行为逻辑:
- IDLE :刚启动,还没接到活。
- PREPARE :老板给了地址和搬多少东西,准备出发。
- RUNNING :正在跑总线,一趟趟搬数据。
- WAITING :外设说“我满了,等等再写”,于是停下来喝口水。
- PAUSED :收到暂停命令,保留现场,随时可复工。
- COMPLETED :干完了!打卡下班,顺便打个报告。
关键在于: 每个通道的状态切换是完全自治的 。比如通道0在 WAITING 等I2S发送完成时,通道1完全可以继续向LCD控制器刷帧,两者井水不犯河水。
下面是状态机推进的核心逻辑(伪代码):
void pl330_channel_fsm_step(int ch_id) {
dmach_state_t *state = &channel[ch_id].state;
uint32_t events = read_event_flags(ch_id);
switch (*state) {
case STATE_IDLE:
if (is_start_requested(ch_id)) {
load_sar_dar(ch_id);
*state = STATE_PREPARE;
}
break;
case STATE_PREPARE:
issue_bus_request(ch_id);
*state = STATE_RUNNING;
break;
case STATE_RUNNING:
if (need_wait_periph(ch_id)) {
set_wait_condition(events);
*state = STATE_WAITING;
} else if (transfer_done(ch_id)) {
set_event_flag(ch_id, EV_COMPLETE);
*state = STATE_COMPLETED;
}
break;
case STATE_WAITING:
if (periph_ready(events)) {
resume_transfer(ch_id);
*state = STATE_RUNNING;
}
break;
case STATE_PAUSED:
if (resume_cmd_received()) {
*state = STATE_RUNNING;
}
break;
default:
break;
}
}
🧩 小贴士:你会发现这里没有使用全局锁!因为每个通道的操作都是独立的,避免了传统多线程编程中的竞争问题,简直是硬件级的“无锁并发”。
2.1.3 通道间冲突避免与仲裁策略
虽然通道各自为政,但它们都要走同一条AHB总线,这就像是八辆车都想上高速,谁先谁后?
PL330内置了一个基于优先级的仲裁器(Arbiter),负责协调这场“交通指挥”。
当两个通道同时请求总线时,仲裁器会根据以下规则裁决:
- 静态优先级字段 :每个通道可通过
CHx_CONTROL寄存器设置 PRIOR[7:5],值越大越优先; - 改进型轮询算法 :默认采用 Round-Robin,但在高优先级事件到来时可临时插队;
- 突发长度感知调度 :大块传输会被拆成小段,防止低带宽通道饿死。
举个例子🌰:
假设通道0正在执行内存复制(INCR16),而通道1要给I2C写控制字节(SINGLE)。如果没有公平机制,通道1可能等很久都没机会出手。但PL330的仲裁器知道轻重缓急,会在适当时候插入一次短传输,确保关键控制操作及时完成 ⚖️。
更妙的是,PL330还支持“门控启动”机制——只有当外设发出 READY 信号(如FIFO非满),才会提交总线请求。从根本上杜绝了无效争抢,真正做到“按需分配”。
总结一句话: 物理隔离上下文 + 独立状态机 + 智能仲裁 = 真正的并发执行能力 。
2.2 并发传输的软件配置方法
光有硬件还不够,得会“开车”才行。下面我们来看看如何用软件让多个通道真正跑起来。
2.2.1 通道初始化流程与寄存器映射关系
PL330的所有寄存器都在内存映射空间里,通常挂载在APB或AHB从设备区域。标准偏移遵循 ARM DDI0424A 手册定义。
以下是关键寄存器布局摘要:
| 偏移地址(hex) | 寄存器名 | 功能描述 |
|---|---|---|
| 0x00 | DS | DMA Status Register |
| 0x04 | DPC | DMA Program Counter |
| 0x08 | INTEN | Interrupt Enable |
| 0x10 + ch×0x14 | SARx | Source Address Register for channel ch |
| 0x14 + ch×0x14 | DARx | Destination Address Register |
| 0x18 + ch×0x14 | CHx_CONTROL | Channel Control Register |
| 0x1C + ch×0x14 | CHx_FC | Channel FIFO Control |
| 0x20 + ch×0x14 | CHx_SGCMD | Scatter-Gather Command Pointer |
初始化一个通道的基本步骤如下:
- 检查DMA是否空闲(DS[IDLE] == 1)
- 写入SARx和DARx设置地址
- 配置CHx_CONTROL:
- 数据宽度(DSIZE)
- 地址增量模式(INC/WRAP)
- 使能中断(IE) - 加载微码程序头(LD/ST等)
- 触发启动(写CMD_START)
示例代码(C语言风格):
#define PL330_BASE 0x12340000
#define CH_OFFSET(ch) (0x10 + (ch)*0x14)
void pl330_init_channel(int ch, uint32_t src, uint32_t dst, int size) {
uint32_t *base = (uint32_t *)PL330_BASE;
while ((readl(base + 0x00) & (1 << 0)) == 0); // 等待空闲
writel(src, base + CH_OFFSET(ch) + 0x00); // SARx
writel(dst, base + CH_OFFSET(ch) + 0x04); // DARx
uint32_t ctrl = 0;
ctrl |= (0x2 << 20); // DSIZE = 32-bit
ctrl |= (0x1 << 16); // SRC_INC
ctrl |= (0x1 << 12); // DST_INC
ctrl |= (size >> 2) & 0xFF; // Transfer Count
ctrl |= (1 << 31); // IE: Interrupt Enable
writel(ctrl, base + CH_OFFSET(ch) + 0x08); // CHx_CONTROL
uint32_t *mc = base + CH_OFFSET(ch) + 0x0C;
mc[0] = 0x00000004; // LD
mc[1] = 0x00000008; // ST
mc[2] = 0x0000000C; // FLUSH
mc[3] = 0x00000000; // DONE
writel(0x01 | (ch << 8), base + 0x28); // CMD_START + channel ID
}
💡 提示:这个函数可以安全地并发调用多个通道,只要总线允许,就能实现真正意义上的并行启动。
2.2.2 源地址与目标地址的独立设置
为了实现并发,必须确保每个通道的SAR和DAR互不交叉。
错误示例 ❌:
writel(shared_buffer, base + CH_OFFSET(0) + 0x04);
writel(shared_buffer, base + CH_OFFSET(1) + 0x04); // 冲突!
正确做法 ✅:
writel(buf_ch0, base + CH_OFFSET(0) + 0x04);
writel(buf_ch1, base + CH_OFFSET(1) + 0x04);
推荐使用静态分配或内存池管理器为每个通道预留专属缓冲区,避免意外覆盖。
2.2.3 传输参数的动态编程技术
在真实世界中,传输需求是变化的。比如视频流分帧传输、自适应采样率调整等场景,都需要运行时修改参数。
PL330支持动态重配置,前提是在通道处于IDLE状态时进行。
示例函数:
int pl330_update_transfer(int ch, uint32_t new_src, uint32_t new_dst, int len) {
uint32_t *base = (uint32_t *)PL330_BASE;
int timeout = 1000;
while (!(readl(base + 0x08) & (1 << ch)) && --timeout) {
udelay(1);
}
if (timeout == 0) return -ETIMEDOUT;
writel(1 << ch, base + 0x0C); // Clear event flag
writel(new_src, base + CH_OFFSET(ch));
writel(new_dst, base + CH_OFFSET(ch) + 0x04);
uint32_t ctrl = readl(base + CH_OFFSET(ch) + 0x08);
ctrl &= ~0xFF;
ctrl |= (len >> 2) & 0xFF;
writel(ctrl, base + CH_OFFSET(ch) + 0x08);
writel(0x01 | (ch << 8), base + 0x28); // Restart
return 0;
}
🔄 这种机制广泛用于双缓冲切换、链式传输无缝衔接等高级应用场景。
通道优先级可编程管理机制
前面说了并发,但并发不等于平等。有些任务天生更重要,比如音频不能断,网络包不能丢。怎么办?答案是: 优先级调度 。
PL330允许你为每个通道设置优先级等级,从而在资源紧张时保障关键任务。
3.1 优先级调度的理论基础
3.1.1 固定优先级与动态优先级对比分析
| 特性 | 固定优先级 | 动态优先级 |
|---|---|---|
| 调度依据 | 预设常量等级 | 运行时计算值 |
| 实现复杂度 | 简单 | 复杂 |
| 响应确定性 | 极高 | 相对较低 |
| 公平性 | 较差 | 较好 |
| PL330 支持情况 | ✅ | ❌(需软件模拟) |
PL330采用的是 固定优先级模型 ,即通过 CHx_CONTROL[PRIOR] 设置3位优先级(0~7),数值越大越优先。
flowchart TD
A[Channel Request Active?] --> B{Multiple Requests?}
B -- No --> C[Grant Bus to Single Channel]
B -- Yes --> D[Compare PRIOR Field Values]
D --> E[Highest Value Wins]
E --> F[Assert Bus Grant Signal]
不过要注意⚠️:优先级抢占只发生在 传输间隙 或 指令边界 处。例如,一个INCR8突发正在进行中,是不会被打断的。这样既能响应紧急请求,又能维持总线效率。
3.1.2 PL330中优先级字段的寄存器布局
CHx_CONTROL 寄存器结构如下:
| 位域 | 名称 | 宽度 | 描述 |
|---|---|---|---|
| [31:24] | NS | 8 | 非安全状态标志 |
| [23:20] | PC | 4 | 保护类 |
| [19:16] | TT | 4 | 传输类型编码 |
| [15:8] | CB | 8 | 循环缓冲区长度 |
| [7:5] | PRIOR | 3 | 通道优先级(0=最低,7=最高) |
| [4] | DS | 1 | 数据流控使能 |
| [3] | AS | 1 | 地址流控使能 |
| [2] | IE | 1 | 中断使能 |
| [1] | LT | 1 | 最后传输标志 |
| [0] | CB | 1 | 清除总线标志 |
设置优先级的代码很简单:
#define PL330_BASE 0x12340000
#define CH_CONTROL(ch) (*(volatile uint32_t*)(PL330_BASE + 0x04 + (ch)*0x58))
void pl330_set_priority(int channel, int priority) {
uint32_t reg = CH_CONTROL(channel);
reg &= ~(0x7 << 5);
reg |= ((priority & 0x7) << 5);
CH_CONTROL(channel) = reg;
}
🛠 示例:实时音频通道设为 PRIOR=7,日志写入设为 PRIOR=2。
3.1.3 仲裁器对高优先级通道的响应机制
实测数据显示,在典型 Cortex-A9 平台上,高优先级请求的平均响应延迟约为 100~250ns ,具备良好的确定性。
部分厂商还在PL330基础上添加了 子通道切片机制 ,将大块传输拆分为多个小段,并插入轮询检查点,进一步提升调度精度。
3.2 软件层面的优先级配置实践
3.2.1 通过PERIPH_IDx与CHx_CONTROL设置优先级等级
先确认设备ID是否正常:
#define PERIPH_ID0 (*(volatile uint8_t*)(PL330_BASE + 0xFE0))
// ... 其他ID寄存器
uint32_t read_periph_id(void) {
return (PERIPH_ID3 << 24) | (PERIPH_ID2 << 16) |
(PERIPH_ID1 << 8) | PERIPH_ID0;
}
标准值应为 0x301BB330 ,否则可能是MMIO映射错误。
然后就可以安心配置了:
pl330_set_priority(0, 7); // 视频采集:最高优先级
pl330_set_priority(1, 1); // 日志写入:低优先级
⚠️ 注意:优先级修改在传输过程中不会立即生效,必须等到下次请求发起才被仲裁器采纳。
3.2.2 不同应用场景下的优先级策略设计
| 通道 | 功能 | 数据速率 | 建议优先级 |
|---|---|---|---|
| CH0 | I2S音频输入 | 1.4 Mbps | 7 |
| CH1 | CSI-2摄像头输出 | 100 Mbps | 6 |
| CH2 | SD卡日志写入 | 10 Mbps | 2 |
| CH3 | UART调试输出 | 115200 bps | 1 |
记住一句话: 功能优先于吞吐 。哪怕摄像头数据量更大,音频也要优先保命!
3.2.3 动态调整优先级应对突发数据需求
有时候优先级不该是死的。比如网络紧急报文来了,就得临时提权。
volatile uint8_t net_priority_boost = 0;
void handle_network_interrupt(void) {
net_priority_boost = 1;
pl330_set_priority(NET_DMA_CH, 7);
}
void background_task(void) {
static int counter = 0;
if (net_priority_boost && ++counter >= 1000) {
pl330_set_priority(NET_DMA_CH, 3);
net_priority_boost = 0;
counter = 0;
}
}
🕒 利用后台任务定时恢复,防止永久性资源垄断。
传输完成通知机制:中断 vs 轮询
怎么知道DMA干完活了?两种方式:中断和轮询。各有千秋,选哪个得看场合。
4.1 中断机制的底层实现原理
4.1.1 事件标志位与中断使能的关系
PL330有最多32个事件标志位(Event Flag 0~31),分别代表各种完成或错误事件。但它们并不会自动触发中断,除非你在 INTEN 寄存器里显式开启。
write_reg(DMAC_INTEN, read_reg(DMAC_INTEN) | (1 << 0)); // 启用CH0中断
事件传播流程如下:
flowchart TD
A[DMA Channel 完成传输] --> B{是否设置了 EVT Flag?}
B -->|是| C[硬件置位 Event Flag x]
C --> D{INTEN[x] 是否使能?}
D -->|否| E[不产生中断]
D -->|是| F[触发 IRQ 输出]
F --> G[CPU 接收到中断向量]
G --> H[执行 ISR 处理函数]
🔔 重要提示:事件标志位一旦置位,在软件清除前将持续有效(电平保持特性),不会丢失。
4.1.2 中断向量表注册与CPU响应流程
在GIC系统中,ISR大概长这样:
void dma_isr_handler(void *data) {
uint32_t status = read_reg(DMAC_IRQSTATUS);
if (status & (1 << 0)) {
clear_event_flag(0);
handle_channel0_completion();
}
write_reg(DMAC_IRQCLR, status);
}
顺序很重要:先清事件标志,再清中断输出,否则可能引发“中断风暴”⚡。
4.1.3 单次传输与链式传输的中断触发差异
| 类型 | 中断频率 | 适用场景 |
|---|---|---|
| 单次传输 | 每次都触发 | 小批量随机传输 |
| 链式传输 | 整条链完成后触发一次 | 大块连续搬运 |
链式传输极大减少了中断次数,适合音频刷新、图像帧传输等周期性强的任务。
4.2 无中断模式的应用价值
4.2.1 轮询方式在确定性系统中的优势
在硬实时系统中(如电机控制、航空电子),中断延迟不可接受。此时轮询反而是更好的选择:
int poll_dma_completion(unsigned int channel_id, uint32_t timeout_us) {
uint32_t start_tick = get_microsecond_counter();
while (!(read_reg(DMAC_STATUS) & (1 << channel_id))) {
if ((get_microsecond_counter() - start_tick) > timeout_us)
return -ETIMEDOUT;
__asm__("nop");
}
return 0;
}
优点:
- 延迟完全可控(最大 = timeout_us)
- 无栈切换,无抢占风险
- 可集成进时间敏感线程
4.2.2 减少上下文切换开销提升效率
| 模式 | 单次开销(cycles) | 每秒百万次额外负载 |
|---|---|---|
| 中断模式 | ~100 | ≈ 10% CPU @1GHz |
| 轮询模式 | ~4 | ≈ 0.4% CPU |
差距高达25倍!对于高频小包传输来说,轮询简直是性能救星 🚀。
4.2.3 适用于硬实时系统的场景分析
典型场景包括:
- 实时闭环控制(机器人、无人机)
- 高速数据采集前端
- 时间触发架构(TTA)
- 功能安全等级 ASIL-D / SIL-4 系统
这类系统往往禁用动态中断,采用静态调度+轮询组合,确保最坏情况执行时间(WCET)可预测。
高级特性综合应用与错误处理
5.1 地址对齐与传输长度错误检测
PL330会在指令预取阶段自动校验地址对齐。若违反规则(如32位传输未4字节对齐),将触发ABORT异常。
软件需读取 DMAINTSTATUS 和 CHx_CSR 寄存器定位故障通道:
for (int i = 0; i < PL330_MAX_CHAN; i++) {
uint32_t csr = readl(CHAN_BASE(i) + CH_CSR);
if (csr & CH_CSR_ABORT) {
printk("Channel %d aborted. Cause: 0x%x", i, csr);
handle_channel_abort(i);
}
}
解决方案:
- 使用 __attribute__((aligned(4)))
- 添加 _Static_assert 编译期检查
static uint8_t __attribute__((aligned(4))) dma_buffer[512];
_Static_assert(((uintptr_t)dma_buffer % 4) == 0, "Buffer must be 4-byte aligned");
5.2 数据与地址宽度灵活配置
PL330支持8/16/32/64位传输,但必须匹配外设能力。
常见配置:
- I2S音频:INCR4(32位突发)
- UART:INCR1(8位单次)
设备树示例:
i2s@12340000 {
dmas = <&pdma0 5>;
dma-names = "tx";
pl330-burst-size = <4>; /* 表示32位突发 */
};
避免宽度不匹配导致的性能下降或协议错误。
5.3 外部触发源与同步传输设计
PL330支持外设事件触发DMA,比如ADC转换完成脉冲直接驱动DMA搬运。
连接方式:
graph LR
A[ADC模块] -->|EOC脉冲| B(DREQ[2])
B --> C[PL330 DMAC]
C -->|ACK| D[ADC数据寄存器]
D -->|Read| E[内存缓冲区]
通过 CHx_SCR.SYNC 位选择同步模式:
- 0:总线同步(内存复制)
- 1:事件同步(外设采集)
5.4 基于MMIO的完整控制接口实现
提供标准化API封装:
struct pl330_chan {
void __iomem *base;
int id;
};
int pl330_init(struct pl330_chan *chan, void __iomem *addr_base, int ch_id);
void pl330_start(struct pl330_chan *chan);
void pl330_pause(struct pl330_chan *chan);
void pl330_stop(struct pl330_chan *chan);
最终可接入Linux DMA Engine子系统,支持 dmatest 工具验证,形成工业级驱动框架 🏗️。
💡 结语 :
PL330 DMAC不仅仅是一个IP核,它体现了一种“智能卸载”的系统设计理念。通过微码编程、多通道并发、优先级调度、事件同步等机制,它让我们能把CPU从繁琐的数据搬运中解放出来,专注更高层次的逻辑处理。掌握它的精髓,不仅是为了写出高效的驱动,更是为了理解如何构建一个真正高性能、低延迟、高可靠的嵌入式系统。
“最好的代码,是让硬件替你思考。” —— 致每一位深耕底层的工程师 🙇♂️✨
简介:PL330是ARM推出的高性能多通道DMA控制器,广泛应用于嵌入式与移动系统中,通过在无需CPU干预的情况下实现内存与外设间高效数据传输,显著提升系统效率。本文深入讲解PL330的架构设计、多通道配置、优先级与中断管理、错误处理机制及地址数据宽度支持等核心特性,并结合DDI0424A技术参考手册,系统梳理其编程模型与软件接口,帮助开发者掌握PL330在复杂数据传输场景中的实际应用与优化方法。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)