1. 项目概述与核心价值
在嵌入式系统,尤其是像TI的C6000系列DSP或Sitara系列ARM处理器这类高性能计算平台上,数据搬运的效率直接决定了整个系统的实时性和吞吐量。CPU如果深陷于在内存和外设之间搬运数据的琐碎事务中,那它的核心计算能力就无从发挥。这时,DMA(直接内存访问)控制器就成了系统的无名英雄。而EDMA3,作为TI平台上的第三代增强型DMA控制器,其强大之处在于它不仅仅是一个简单的“搬运工”,更是一个可以编程的、能理解复杂数据结构的“智能物流系统”。
这个智能物流系统的核心“调度手册”,就是PaRAM(Parameter RAM,参数RAM)。我们程序员通过编写这本手册——即配置PaRAM集中的各个参数——来告诉EDMA3:从哪里搬(SRC)、搬到哪里去(DST)、每次搬多少(ACNT)、要搬多少组(BCNT)、这样的组有多少批(CCNT),以及搬完一组或一批后,下一个起点在哪里(BIDX, CIDX)。更关键的是,我们还能通过“同步维度”(SYNCDIM)这个开关,选择两种截然不同的派单模式:A同步和AB同步。理解这两种模式的区别以及如何配置PaRAM参数来驾驭它们,是解锁EDMA3全部潜力的关键。这就像你是物流总指挥,可以选择是每来一个电话(同步事件)就派一辆车送一个包裹(A同步),还是每来一个电话就派一辆车送完一整栋楼的所有包裹(AB同步)。不同的业务场景,需要不同的派单策略。
本文将从一个资深嵌入式软件工程师的视角,深入拆解EDMA3的A同步与AB同步传输机制。我不会仅仅复述技术手册里的定义,而是结合我多年在音视频编解码、雷达信号处理等项目中实际使用EDMA3的经验,带你理解每一种参数背后的设计意图,并通过具体的配置案例和代码片段,展示如何根据你的数据结构(比如二维图像、三维矩阵、乒乓缓冲区)来精准地配置PaRAM集。你会看到,一个精心设计的PaRAM配置,如何让EDMA3在后台悄无声息地完成海量数据搬运,而让你的CPU专注于核心算法运算。
2. EDMA3与PaRAM架构核心思想解析
在深入同步机制之前,我们必须先建立起对EDMA3和PaRAM架构的全局认知。很多初学者一上来就纠结于某个寄存器的某一位,却忽略了整体设计哲学,导致配置时总是磕磕绊绊。
2.1 EDMA3控制器的工作模型
你可以把EDMA3控制器想象成一个高度专业化的快递公司。这个公司有两个核心部门:
- EDMA3通道控制器(EDMA3CC):这是“客服中心”和“调度中心”。它负责接收来自各种外设(如McASP、McBSP、ADC)或软件触发的“送货请求”(事件),然后根据预先登记好的“客户档案”(PaRAM集),生成详细的“送货单”(传输请求,TR),并派发给下面的运输队。
- EDMA3传输控制器(EDMA3TC):这就是实际的“运输车队”。它接收来自CC的TR,负责执行具体的数据搬运工作,在系统总线上完成读和写操作。一个EDMA3CC可以连接多个TC,实现并行传输。
为什么这么设计?将事件管理与数据传输解耦,使得系统非常灵活。CC可以高效地管理大量通道(通常64个甚至更多)的事件队列和参数,而TC则专注于以最高效率搬运数据。这种架构特别适合多核异构系统,不同的TC可以为不同的主设备或内存区域服务。
2.2 PaRAM集:智能化的传输蓝图
PaRAM是EDMA3CC内部的一块专用RAM,通常包含512个参数集(PaRAM Set),每个集占32字节(8个32位字)。这不是普通的配置寄存器,而是DMA传输的“上下文”或“蓝图”。每个DMA或QDMA通道都关联一个PaRAM集(默认映射到Set 0,但实际使用时必须重映射)。
一个PaRAM集完整描述了一次(或一系列)传输的所有信息。其结构如下表所示:
| 偏移地址 (字节) | 参数缩写 | 全称 | 作用描述 |
|---|---|---|---|
| 0x00 | OPT | 通道选项 | 配置传输的同步模式、地址模式、完成中断/链式触发等核心行为。 |
| 0x04 | SRC | 源地址 | 数据传输起始的源字节地址。 |
| 0x08 | ACNT | 第一维数量 | 一个“数组”中包含的连续字节数。范围1-65535。 |
| BCNT | 第二维数量 | 一个“帧”中包含的“数组”个数。范围1-65535。 | |
| 0x0C | DST | 目的地址 | 数据传输起始的目的字节地址。 |
| 0x10 | SRCBIDX | 源B索引 | 在同一帧内,从一个数组的起始地址到下一个数组的起始地址的字节偏移量(有符号)。 |
| DSTBIDX | 目的B索引 | 在同一帧内,从一个数组的起始地址到下一个数组的起始地址的字节偏移量(有符号)。 | |
| 0x14 | LINK | 链接地址 | 当前参数集用尽后,用于重载新参数集的PaRAM地址偏移。FFFFh表示空链接。 |
| BCNTRLD | BCNT重载值 | 仅在A同步传输中使用。当BCNT减到0时,用于重新加载BCNT的值。 | |
| 0x18 | SRCCIDX | 源C索引 | 从一个帧的某个参考数组起始地址到下一帧第一个数组起始地址的字节偏移量(有符号)。参考点因同步模式而异。 |
| DSTCIDX | 目的C索引 | 从一个帧的某个参考数组起始地址到下一帧第一个数组起始地址的字节偏移量(有符号)。参考点因同步模式而异。 | |
| 0x1C | CCNT | 第三维数量 | 一个“块”中包含的“帧”个数。范围1-65535。 |
| RSVD | 保留 | 必须写0。 |
关键概念解读:
- 数组(Array):由ACNT个连续字节组成的数据单元。这是EDMA3传输的最小逻辑单元(在A同步模式下,一次事件就搬一个数组)。
- 帧(Frame):由BCNT个数组组成。数组之间通过SRCBIDX/DSTBIDX隔开。
- 块(Block):由CCNT个帧组成。帧之间通过SRCCIDX/DSTCIDX隔开。
- 索引(BIDX, CIDX):这是EDMA3灵活性的灵魂。它们不是简单的步进,而是有符号的字节偏移量。正数表示地址递增,负数表示地址递减。这让你能实现复杂的数据重排,例如从交错存储(如RGBRGBRGB)中提取单一颜色分量(RRRR…)。
实操心得:在配置PaRAM时,我习惯先用纸笔画一下我的源数据和目标数据在内存中的布局。标出每个数组的起始地址,计算它们之间的差值,这个差值就是BIDX。再标出每一帧第一个数组的地址,计算帧间的差值,这个差值就是CIDX。可视化能极大避免配置错误。
3. 同步传输机制深度剖析:A同步 vs AB同步
同步维度(OPT寄存器中的SYNCDIM位)的选择,决定了EDMA3CC如何响应一个同步事件,以及如何更新PaRAM集中的参数。这是EDMA3编程中最核心也最容易混淆的概念。
3.1 A同步传输:精细化的单步控制
在A同步(SYNCDIM=0)模式下,每一个同步事件只触发传输一个数组(ACNT字节)。你可以把它理解为“单步执行”模式。
工作流程:
- 一个同步事件到达EDMA3CC。
- CC提交一个传输请求(TR)给TC,这个TR只包含传输一个数组(ACNT字节)所需的信息。
- TC执行这个数组的传输。
- CC更新PaRAM集:
BCNT = BCNT - 1, 源/目的地址分别加上SRCBIDX/DSTBIDX。 - 如果
BCNT减到0,说明当前帧的所有数组都传完了。此时CC会进行“C-更新”:CCNT = CCNT - 1, 并将BCNT重置为BCNTRLD的值,同时源/目的地址分别加上SRCCIDX/DSTCIDX(注意:此时地址的参考点是上一帧的最后一个数组的起始地址)。 - 如果
CCNT也减到0,说明整个块传��完成。此时,如果OPT.STATIC=0且LINK不是FFFFh,CC会执行“链接更新”,从LINK指向的地址加载一个新的完整PaRAM集。
关键特点与适用场景:
- 事件密集型:传输一个完整的块(
BCNT * CCNT个数组)需要BCNT * CCNT个事件。事件频率可以很高。 - 实时性强:每个事件都能立即得到响应,开始传输一小块数据。适合数据流是细粒度、持续产生的场景,例如从ADC按采样点读取数据。
- 参数更新频繁:每传输一个数组,CC都会更新地址和BCNT,PaRAM集处于动态变化中。
配置示例:搬运一个3x4的二维矩阵(假设每个元素占4字节)假设我们要把源矩阵Src[3][4]连续搬运到目的地址。我们可以把一行(4个元素)看作一帧(Frame),一个元素看作一个数组(Array)。
ACNT = 4(每个元素4字节)BCNT = 4(每行4个元素)CCNT = 3(共3行)SRCBIDX = 4,DSTBIDX = 4(数组间偏移4字节)SRCCIDX = (第二行首地址 - 第一行末地址)。假设矩阵连续存储,第一行末地址是&Src[0][3],第二行首地址是&Src[1][0],中间间隔了(4-1)*4=12字节?不对,这里是最容易出错的地方!在A同步下,CIDX的参考点是上一帧最后一个数组的起始地址(即&Src[0][3])到下一帧第一个数组的起始地址(即&Src[1][0])。因为矩阵是连续存储的,&Src[0][3]和&Src[1][0]是相邻的,所以SRCCIDX = 4。同理DSTCIDX = 4。- 需要
4 * 3 = 12个事件来完成整个传输。
3.2 AB同步传输:批量化的大块搬运
在AB同步(SYNCDIM=1)模式下,每一个同步事件触发传输完整的一帧(BCNT个数组, 每个数组ACNT字节)。你可以把它理解为“批处理”模式。
工作流程:
- 一个同步事件到达EDMA3CC。
- CC提交一个TR给TC,但这个TR包含了传输一整帧(
BCNT个数组)所需的所有信息。TC会内部循环,连续搬运BCNT个数组。 - TC执行完这一整帧的传输。
- CC更新PaRAM集:
CCNT = CCNT - 1, 源/目的地址分别加上SRCCIDX/DSTCIDX(注意:此时地址的参考点是当前帧的第一个数组的起始地址)。 - 如果
CCNT减到0,说明整个块传输完成。此时执行链接更新(如果配置了的话)。
关键特点与适用场景:
- 事件稀疏化:传输一个完整的块(
CCNT帧)只需要CCNT个事件。大大减少了事件触发开销。 - 吞吐量高:一个事件触发一大块数据的连续搬运,更利于总线效率,减少仲裁开销。适合搬运已经存在于内存中的大块数据,例如处理一帧完整的图像、一个音频片段。
- 参数更新简单:CC只在每帧传输完成后才更新地址和CCNT。BCNT的递减和数组间地址的更新(通过BIDX)是在TC内部完成的,对CC透明。
配置示例:同样搬运一个3x4的二维矩阵
ACNT = 4,BCNT = 4,CCNT = 3(定义不变)SRCBIDX = 4,DSTBIDX = 4(TC内部在搬运数组时使用)SRCCIDX = (第二行首地址 - 第一行首地址)。在AB同步下,CIDX的参考点是当前帧第一个数组的起始地址(即&Src[0][0])到下一帧第一个数组的起始地址(即&Src[1][0])。对于连续存储的矩阵,SRCCIDX = 4 * 4 = 16字节。同理DSTCIDX = 16。- 只需要
3个事件就能完成整个传输。
注意事项:这是A同步和AB同步在配置上最核心的区别!CIDX的计算基准点不同。在A同步中,CIDX是“跨帧步进”,参考点是上一帧的末尾;在AB同步中,CIDX是“帧间偏移”,参考点是每一帧的开头。配置错误会导致数据被搬运到完全错误的内存区域。
3.3 同步模式选择策略
如何选择?我的经验法则如下:
- 选择A同步当:数据产生是细粒度的、周期性的,且你需要对每一个小数据单元的传输完成做出即时响应(例如,每个ADC采样完成后立即触发DMA搬运,并可能在搬运完成后触发中断进行实时处理)。或者,你的数据结构非常不规则,需要更频繁地更新PaRAM参数。
- 选择AB同步当:数据以“块”或“帧”为单位产生和处理(例如,摄像头产生一帧图像,音频缓冲区满),你希望用最少的事件触发完成大批量数据搬运,最大化传输效率,减少CPU/事件管理器的负担。
一个常见的误解:认为AB同步一定比A同步“快”。其实对于传输相同总量的数据,硬件搬运数据的时间是差不多的。AB同步的“快”体现在系统开销上:它用更少的事件、更少的CC参与(只做C-更新),让传输过程更“安静”,从而允许CPU或其他主机有更多总线带宽和周期。
4. PaRAM参数集配置实战与避坑指南
理解了原理,我们来实战配置。我会用一个更复杂的例子:将一幅YUV420SP格式的图像(例如从摄像头传感器而来)的一个区域,搬运到另一个内存区域并进行格式转换(例如裁剪并调整 stride)。这在实际视频处理中非常常见。
场景:源图像宽度srcWidth=1280,高度srcHeight=720,Y分量和UV交错存储(NV12)。我们需要裁剪出左上角(x=100, y=50),宽cropWidth=640,高cropHeight=360的区域,搬运到目的缓冲区。目的缓冲区的行跨度(stride)dstStride=1024(可能由于内存对齐要求)。
分析:这涉及到二维窗口的搬运,并且源和目的的行跨度不同。我们需要把裁剪后的每一行(640个Y像素)看作一个数组(ACNT),整个裁剪区域的行数看作帧数(CCNT)。但这里每一行是连续的,所以BCNT=1。我们需要使用A同步还是AB同步?考虑到图像处理通常以行为单位,我们可以每准备好一行数据(或由行同步信号触发)搬运一行,这里用A同步更直观。但如果我们想用一次事件搬运整个区域,就需要用AB同步,并把一行看作一个数组,整个区域看作一帧(此时BCNT=行数, CCNT=1)。为了演示两种模式,我们都实现一下。
4.1 使用A同步模式配置(逐行搬运)
假设我们有一个“行有效”信号能触发每个同步事件。
- Y分量搬运:
ACNT = cropWidth * 1(每个像素Y占1字节) =640BCNT = 1(每个事件只搬一行)CCNT = cropHeight=360SRC= 源Y平面基地址 +y * srcWidth + x=srcYBase + 50*1280 + 100DST= 目的Y缓冲区起始地址SRCBIDX= 0?不对。因为BCNT=1,在帧内没有下一个数组,BIDX实际上用不到(在C-更新时会被跳过)。但参数仍需设置,可设为0。DSTBIDX= 0SRCCIDX= 源图像中,从一行的结尾到下一行的开头偏移。即srcWidth=1280。注意参考点是行尾,对于连续行,就是一行宽度。DSTCIDX= 目的缓冲区行跨度dstStride=1024BCNTRLD= 1 (因为BCNT每次减到0后需要重载为1)OPT:设置SYNCDIM=0 (A同步),根据需要设置TCINTEN(传输完成中断)在CCNT减到0时触发中断。
工作过程:每个“行有效”事件触发,搬运640字节的一行Y数据。搬运完后,CC更新:因为BCNT=1,直接触发C-更新,CCNT减1,SRC地址加上1280跳到下一行源起始,DST地址加上1024跳到下一行目的起���,BCNT被重载为1。如此重复360次事件,完成整个Y平面搬运。
避坑技巧:当BCNT=1时,A同步和AB同步在行为上非常相似,但参数更新逻辑仍有细微差别(主要是CIDX的参考点)。我个人的建议是,只要BCNT>1,就明确根据你的触发粒���选择模式;如果BCNT=1,两种模式都可以,但选择A同步通常更符合直觉(一个事件对应一行)。
4.2 使用AB同步模式配置(整块搬运)
假设我们用一个“帧开始”或软件触发事件来启动整个裁剪区域的搬运。
- Y分量搬运:
ACNT = 1(这次我们把一个像素看作一个数组?不,这样效率低。我们把一行连续的像素看作一个数组)BCNT = cropHeight=360(每个数组是一行,一帧包含所有行)CCNT = 1(整个裁剪区域就是一帧)ACNT需要重新考虑。在AB同步下,一个事件触发一帧(BCNT个数组)。我们需要定义数组大小。如果我们定义数组为一整行(640字节),那么BCNT=1,又回到了类似情况。为了展示AB同步处理多数组的能力,我们定义数组为一行中的一段,比如32字节(32个像素)。但这会让配置复杂化,因为我们需要在TC内部通过BIDX在行内跳段,这通常不必要。- 更合理的配置:对于这种矩形区域搬运,AB同步的优势在于用一次事件搬多行,但每行本身是连续的。因此,一个更贴近AB同步精神的配置是:
ACNT = cropWidth=640(数组:一行中的连续字节)BCNT = cropHeight=360(帧:由360个这样的行数组构成)CCNT = 1SRC= 起始地址(同上)DST= 目的起始地址(同上)SRCBIDX=srcWidth=1280(TC在搬完一行后,源地址跳到下一行)DSTBIDX=dstStride=1024(TC在搬完一行后,目的地址跳到下一行)SRCCIDX= 0 (因为只有一帧,CCNT=1,用不到CIDX更新)DSTCIDX= 0BCNTRLD= 无关(AB同步不使用)OPT:设置SYNCDIM=1 (AB同步)
工作过程:一个事件触发,TC开始工作。它先搬运第一个数组(第一行的640字节),然后源地址+1280,目的地址+1024,搬运第二个数组(第二行)…… 如此循环360次(由BCNT控制),全部在TC内部完成,无需CC干预。完成后,CC进行C-更新(CCNT从1减到0),触发传输完成。
哪种更好?对于这个例子,AB同步的配置更简洁,一次事件完成所有工作,效率最高。但前提是你能接受一次性搬运整个360行数据所需的延迟和缓冲区大小。A同步则提供了更细粒度的控制。
4.3 链接(Linking)与乒乓操作
链接是EDMA3的高级功能,用于实现自动重载参数集。最常见的应用就是乒乓缓冲(Ping-Pong Buffer)。
场景:你需要持续从外设(如ADC)采集数据到内存,处理完的数据再送出去。为了避免数据覆盖,使用两个缓冲区:当DMA往缓冲区A写数据时,CPU处理缓冲区B的数据;下一帧,DMA切换到缓冲区B,CPU处理缓冲区A。
配置方法:
- 准备两个PaRAM集,例如Set 0和Set 1。它们除了SRC/DST地址指向不同的缓冲区(A和B),其他参数(ACNT, BCNT, BIDX等)都相同。
- 在Set 0的LINK字段,填入Set 1的PaRAM地址偏移(例如0x4020)。
- 在Set 1的LINK字段,填入Set 0的PaRAM地址偏移(例如0x4000)。
- 将OPT.STATIC位设为0(非静态,允许链接更新)。
- 启动通道,关联到Set 0。
工作过程:当Set 0对应的传输完成(CCNT耗尽),EDMA3CC会检查到LINK非空且STATIC=0,于是它将Set 1的全部内容拷贝到当前通道关联的PaRAM位置(覆盖Set 0)。下一次事件到来时,就会使用Set 1的参数(指向缓冲区B)进行传输。当Set 1的传输完成,又会链接回Set 0。如此循环往复,实现了自动乒乓切换,零CPU开销。
重要警告:链接地址必须是32字节对齐的(低5位为0)。通常使用相对于PaRAM基地址的偏移量来设置。务必确保链接指向一个有效的、已配置的PaRAM集,否则会导致不可预知的行为。若要终止传输链,应将LINK设置为FFFFh(空链接)。
5. 常见问题排查与调试技巧实录
即使理解了所有参数,实际调试EDMA3时也常会遇到数据错位、传输不完整、中断不触发等问题。以下是我踩过坑后总结的排查清单。
5.1 数据错位或覆盖
这是最常见的问题,根本原因几乎都是索引(BIDX, CIDX)计算错误。
- 症状:数据被搬运到了错误的内存位置,或者后一段数据覆盖了前一段数据。
- 排查步骤:
- 复查同步模式:确认你用的是A同步还是AB同步?这直接决定了CIDX的计算基准。
- 画图计算:在纸上画出源和目的内存布局图,标出每个数组、帧的起始地址。手工计算相邻数组的地址差(BIDX)和相邻帧第一个数组的地址差(CIDX)。特别注意在A同步下,CIDX是“跨帧”偏移,参考点是上一帧的最后一个数组。
- 检查符号:BIDX和CIDX是有符号16位整数。如果你的地址是递减的(例如从高地址向低地址搬运数据),需要设置负的索引。
- 检查对齐:如果使用了常数地址模式(SAM/DAM=1),必须确保SRC/DST地址是32字节对齐,且BIDX是32字节的整数倍。不对齐会导致TC报错。
5.2 传输未启动或未完成
- 症状:事件触发了,但数据没有移动;或者数据移动了,但完成中断没产生。
- 排查步骤:
- 事件映射:首先确认DMA通道号是否正确,并且该通道的事件输入是否已正确映射到你所期望的硬件事件源(如McASP的接收事件)。检查
DMAQNUM寄存器,确保通道被分配到正确的队列。 - 事件使能与触发:检查
EER(事件使能寄存器)相应位是否置1。对于软件触发,是否正确地写入了ESR寄存器。 - PaRAM关联:确认通道的PaRAM映射寄存器(
DCHMAPn)是否已从默认的0重映射到你实际使用的PaRAM集编号。 - 参数有效性:检查ACNT, BCNT, CCNT是否都大于0?任何一个为0都会导致“空传输”或“伪传输”,行为异常。
- 完成检测:
- 中断:检查OPT中的
TCINTEN(传输完成中断使能)是否置1。检查IER(中断使能寄存器)中对应的TCC码位是否置1。传输完成后,查看IPR(中断挂起寄存器)相应位是否置1。常见坑:只使能了OPT中的中断,忘了使能IER,导致中断永远不会到达CPU。 - 链式触发:如果使用链式触发(
TCCHEN),检查CER(链式事件寄存器)相应位是否置1,并确认它是否正确地触发了下游通道。
- 中断:检查OPT中的
- 链接与静态位:如果你的传输只执行一次就停止了,检查OPT.STATIC位。如果设为1,传输完成后PaRAM不会更新,也不会链接。如果你期望循环或乒乓,需要设为0,并配置正确的LINK。
- 事件映射:首先确认DMA通道号是否正确,并且该通道的事件输入是否已正确映射到你所期望的硬件事件源(如McASP的接收事件)。检查
5.3 性能达不到预期
- 症状:总线带宽利用率低,传输耗时比理论计算长。
- 排查与优化:
- 优先使用AB同步:对于大块连续数据,AB同步能减少事件处理和CC更新的开销,通常能获得更好的总线突发传输效率。
- 优化数据对齐:确保源和目的地址与总线位宽对齐(例如64位对齐)。不对齐的访问会导致多次非对齐事务,降低性能。
- 合理使用QDMA:对于软件触发的、一次性的传输,使用QDMA比配置传统DMA通道更快捷,节省了写事件置位寄存器(ESR)的步骤。
- 关注总线竞争:如果系统中有多个主设备(多核CPU, 其他DMA等),总线竞争会导致EDMA3等待。可以考虑调整EDMA3传输队列的优先级(通过
QUEPRI寄存器),或将传输安排在系统相对空闲的时段。 - 使用常量地址模式:如果源或目标是FIFO(如某些外设数据寄存器),使用常量地址模式(SAM/DAM=1)可以避免地址自增的开销。但务必遵守对齐规则。
5.4 调试工具与技巧
- 寄存器查看:在调试器(如CCS)中实时监控关键的PaRAM集内容、
IPR/CER寄存器、ER/ERH(事件寄存器)等,可以直观看到事件是否到达、参数是否更新、中断是否产生。 - 内存查看:在传输前后,对比源和目的内存区域的数据,是验证传输正确性的最直接方法。
- 使用“伪传输”测试:在复杂配置投入实际使用前,可以先将DST地址设置到一个安全的、可读写的内存区域(而不是真实的外设),用已知模式的数据(如递增数列)填充SRC区域,运行一次传输后检查DST区域,验证索引逻辑是否正确。
- 分步测试:对于复杂的多维传输,先配置成单维(CCNT=1, BCNT=1)测试ACNT和地址自增是否正确。然后增加BCNT测试BIDX,最后增加CCNT测试CIDX。分步验证能快速定位问题维度。
最后,EDMA3是一个功能极其强大的模块,其复杂性对应着极高的灵活性。掌握它的最佳方式就是多动手实验,从简单的单次传输开始,逐步增加维度、使用索引、尝试链接和乒乓。每一次成功的配置,都会让你对“智能数据搬运”有更深的理解。当你的CPU终于从繁重的数据拷贝中解放出来,全速运行那些复杂的算法时,你就会觉得花在理解PaRAM上的每一分钟都是值得的。