嵌入式GUI硬件加速实战:emWin接口详解与STM32 DMA2D优化
1. 项目概述:为什么嵌入式GUI需要硬件加速?
在嵌入式系统里做图形界面开发,一个绕不开的痛点就是性能。你精心设计的UI,在开发板上跑起来却卡顿、拖影,动画一多就掉帧,这体验实在说不上好。问题的根源,往往在于CPU要同时处理业务逻辑和繁重的图形渲染任务,分身乏术。这时候,“硬件加速”就不再是一个锦上添花的功能,而是决定产品体验成败的关键技术。
硬件加速的本质,是把那些计算密集型的图形操作,比如填充一大片颜色、拷贝图像数据、进行Alpha混合(透明效果)、绘制抗锯齿图形等,从通用CPU手中“抢”过来,交给MCU内部专用的图形处理单元(GPU)或显示控制器(LCD Controller)里的特定硬件模块去执行。这些硬件模块是为图形计算量身定制的,执行效率远高于软件模拟。以填充一个矩形为例,软件可能需要循环计算每个像素,而硬件可能只需配置好起始地址、颜色和尺寸,一个DMA(直接内存访问)操作就能完成,CPU在此期间可以去处理其他任务。
emWin作为一款成熟的嵌入式图形库,其强大之处不仅在于提供了丰富的软件图形API,更在于它设计了一套灵活且底层的硬件加速接口。这套接口不是简单地提供一个“加速开关”,而是允许开发者深入到颜色转换、像素操作、混合计算等最核心的环节,用自定义的硬件驱动函数去替换库的默认软件实现。这意味着,你可以针对自己项目中使用的特定芯片(比如ST的Chrom-ART、NXP的PXP、或者瑞萨的Dave2D等),编写高度优化的驱动,将emWin的图形指令流无缝对接到硬件引擎上。
本次要深入解析的,正是emWin中这套硬件加速与自定义函数接口。我们将超越手册的简单罗列,从“为什么需要这么做”出发,拆解颜色转换、Alpha混合、抗锯齿绘制等关键环节的硬件加速原理,并给出具体的、可落地的函数对接方案和避坑指南。无论你是在STM32上优化仪表盘刷新率,还是在i.MX RT上实现流畅的滑动菜单,这些内容都将是你打通性能瓶颈的利器。
2. 核心加速接口详解与设计思路
emWin的硬件加速接口设计得非常模块化,它允许你针对不同的图形操作,分别挂接自定义的硬件函数。理解这个设计思路,比死记硬背API更重要。其核心机制是通过一系列SetFunc或SetCust开头的函数,向emWin注册你的硬件驱动函数指针。当emWin内部需要执行某项图形操作时,会首先检查是否设置了对应的自定义函数。如果设置了,就调用你的硬件函数;否则,回退到其内置的、纯软件的默认实现。
这种“回调函数(Callback)”的设计模式,在嵌入式中间件中非常常见。它保证了库的核心逻辑(如窗口管理、绘图命令解析)与底层的、硬件相关的具体实现解耦。你的任务,就是为这些回调函数提供具体实现。
2.1 颜色转换(Color Conversion)的硬件卸载
颜色转换是图形渲染中最基础也是最频繁的操作之一。它主要发生在两个场景:
- 颜色值转索引值(Color2Index):当使用调色板(Palettized)显示模式(如8位色)时,一个RGB颜色需要转换为调色板中的索引号。
- 索引值转颜色值(Index2Color):与上述相反,根据索引号取出实际的RGB颜色。
在软件中,这通常通过查表(LUT)完成。但如果你的显示控制器硬件支持颜色空间转换,或者有专用的查找表硬件,就可以通过GUICC_XXX_SetCustColorConv()系列函数将这个任务卸载。
关键函数解析:
GUICC_M888_SetCustColorConv(pfColor2IndexBulk, pfIndex2ColorBulk)这是针对24位真彩色(RGB888)模式设置批量颜色转换函数。M888表示内存中的格式是8-8-8。GUICC_M565_SetCustColorConv(...)针对16位高彩色(RGB565)模式。
参数解读:
pfColor2IndexBulk: 指向一个批量颜色转索引的函数。其参数pColor是输入颜色数组,pIndex是输出索引数组,NumItems是数量,SizeOfIndex是每个索引的字节大小(通常是1字节)。pfIndex2ColorBulk: 指向一个批量索引转颜色的函数。
硬件对接思路:如果你的硬件有颜色转换单元,可以在此函数中:
- 检查数据对齐和长度,可能需要对非对齐的数据首尾用软件处理。
- 配置硬件转换器的源/目标地址、格式和数量。
- 启动硬件DMA传输,并等待完成(或配置中断)。
- 如果没有硬件支持,你甚至可以在这里实现一个更优化的软件查表算法(例如使用SIMD指令),同样能获得比emWin通用实现更好的性能。
注意:批量处理(Bulk)是性能关键。硬件加速的优势在于处理大批量数据时开销分摊后极低。务必确保你的自定义函数是针对连续内存块进行高效操作,避免在函数内部写循环调用单次转换硬件指令,那可能比软件还慢。
2.2 填充、拷贝与位图绘制
这是最直观的硬件加速场景,对应LCD_SetDevFunc()函数。你可以为不同的操作索引(Index)设置自定义函数,例如:
LCD_DEVFUNC_FILLRECT: 填充矩形LCD_DEVFUNC_COPYRECT: 拷贝矩形区域(用于窗口移动、图像滚动)LCD_DEVFUNC_DRAWBMP_XXX: 绘制指定格式的位图
实操要点:当你通过LCD_SetDevFunc(LayerIndex, Index, *pFunc)设置一个填充矩形的硬件函数后,emWin在调用GUI_FillRect()时,如果矩形区域符合条件(比如完全在可视区域内),就会直接调用你的硬件函数。
硬件函数实现示例(伪代码):
void MyHw_FillRect(int x0, int y0, int x1, int y1, LCD_COLOR color) { // 1. 将LCD_COLOR转换为硬件接受的格式(如RGB565) uint32_t hw_color = ConvertColorToHW(color); // 2. 计算显存(FrameBuffer)中的起始地址 uint8_t* fb_addr = GetFrameBufferAddr() + (y0 * GetPitch()) + (x0 * GetBytesPerPixel()); // 3. 配置2D加速引擎:目标地址、颜色、矩形宽高 HW_2D_ENGINE->DST_ADDR = (uint32_t)fb_addr; HW_2D_ENGINE->FILL_COLOR = hw_color; HW_2D_ENGINE->RECT_WIDTH = (x1 - x0 + 1); HW_2D_ENGINE->RECT_HEIGHT = (y1 - y0 + 1); HW_2D_ENGINE->PITCH = GetPitch(); // 4. 启动填充操作,并等待完成(或使用中断/DMA回调通知emWin) HW_2D_ENGINE->CTRL |= START_FILL_BIT; while (!(HW_2D_ENGINE->STATUS & OPERATION_DONE_BIT)); }为什么需要等待或回调?因为emWin的绘图API通常是同步的,函数返回意味着绘图完成。所以你的硬件函数必须阻塞直到操作完成,或者通过更复杂的机制(如配合RTOS信号量)在回调中通知,但后者需要更深入的集成。
2.3 Alpha混合与透明效果
Alpha混合是实现半透明、渐变、阴影等高级UI效果的基础。其计算量很大,公式为:结果颜色 = 前景色 * Alpha / 255 + 背景色 * (255 - Alpha) / 255。软件实现需要对每个像素进行多次乘法和除法。
emWin提供了两个层次的Alpha混合硬件加速接口:
GUI_SetFuncAlphaBlending(): 设置批量颜色混合函数。它接收前景色数组、背景色数组、目标数组和项目数量。这非常适合混合两个整块的图像或颜色数组。GUI_SetFuncDrawAlpha(): 设置绘制带Alpha通道的内存设备或位图的函数。这更高级,直接处理包含Alpha信息的源数据块与目标块的混合。
硬件对接策略:
- 如果你的硬件有完整的2D混合单元(Blender),可以直接在
GUI_SetFuncDrawAlpha设置的回调函数中,配置硬件混合器的两个图层(前景和背景)的数据地址、像素格式和全局Alpha值,然后启动混合DMA。 - 如果硬件支持有限,比如只支持固定的几种Alpha混合模式,你可能需要将
GUI_SetFuncAlphaBlending指向一个利用硬件固定功能混合器的函数,而对于更复杂的逐像素Alpha,则可能仍需部分软件处理。
一个常见的坑:硬件混合器往往对数据对齐、步长(stride)有严格要求。在自定义函数中,必须仔细处理BytesPerLine参数。它可能不等于xSize * bytesPerPixel,因为内存中可能存在填充(padding)以满足对齐要求。直接使用错误的步长会导致图像错乱。
2.4 抗锯齿(AA)图形绘制
绘制平滑的圆、圆弧、多边形轮廓和直线,需要进行抗锯齿处理,这涉及到复杂的边缘像素灰度计算。GUI_AA_SetFuncDrawCircle(),GUI_AA_SetFuncDrawLine()等函数,允许你将特定抗锯齿图元的绘制命令直接转发给硬件。
重要前提:你的MCU必须拥有能直接绘制抗锯齿图元的硬件模块,例如某些高端MCU内置的矢量图形引擎。对于大多数只有基本2D填充/拷贝功能的硬件,此接口可能无法直接使用。此时,你仍然可以使用LCD_SetDevFunc来加速填充抗锯齿形状的内部(如果硬件支持任意形状填充),但边缘混合可能仍需软件处理。
接口使用场景:假设你的硬件引擎HW_AA_Engine有一个命令是绘制抗锯齿直线。你可以这样对接:
int MyHw_AADrawLine(int x0, int y0, int x1, int y1) { // 设置颜色、线宽等属性(这些可能通过emWin的其他状态机获取,这里简化) HW_AA_ENGINE->COLOR = GetCurrentColor(); HW_AA_ENGINE->LINE_WIDTH = 1; // 抗锯齿通常为1像素宽 // 设置起点终点 HW_AA_ENGINE->X0 = x0; HW_AA_ENGINE->Y0 = y0; HW_AA_ENGINE->X1 = x1; HW_AA_ENGINE->Y1 = y1; // 执行绘制 HW_AA_ENGINE->CMD = DRAW_AA_LINE; return 0; // 返回0表示成功 } // 在初始化时注册 GUI_AA_SetFuncDrawLine(MyHw_AADrawLine);2.5 硬件JPEG解码
显示JPEG图片是很多嵌入式UI的需求。软件解码JPEG耗时很长,尤其对于大图。如果MCU集成硬件JPEG解码器(如STM32F7/ H7系列),通过GUI_JPEG_SetpfDrawEx()接口将其利用起来,性能提升是数量级的。
工作流程:
- 你实现一个
pfDrawEx函数,并将其设置给emWin。 - 当应用调用
GUI_JPEG_Draw()时,emWin会调用你的pfDrawEx函数,并传入一个pfGetData回调函数和数据指针p。 - 在你的
pfDrawEx函数中: a. 循环调用pfGetData获取JPEG文件流数据,喂给硬件JPEG解码器。 b. 启动硬件解码。 c. 解码完成后,硬件通常输出YUV数据,你需要将其转换为RGB(如果硬件没有集成色彩空间转换单元,这一步可能需要软件或另一个硬件模块完成)。 d. 将最终的RGB数据写入显示缓冲区(可能是通过另一个2D拷贝硬件加速)。
关键挑战:
- 流式处理:JPEG文件可能很大,需要分段获取数据并喂给解码器。你的
pfGetData调用逻辑需要与解码器的输入缓冲区管理配合好。 - 色彩空间转换:这是最容易忽略的性能瓶颈。如果硬件没有YUV2RGB转换器,软件转换会抵消部分解码带来的性能收益。此时需要评估是否值得启用硬件解码。
- 内存带宽:解码后的RGB数据写入帧缓冲,可能成为新的瓶颈。如果可能,应使用DMA将解码输出直接传输到帧缓冲的指定位置。
3. 实战:为STM32的Chrom-ART加速器实现填充矩形函数
让我们以一个具体的例子,将理论转化为代码。假设我们使用STM32F429,它拥有Chrom-ART(DMA2D)加速器。我们将实现一个加速填充矩形的自定义函数,并通过LCD_SetDevFunc注册。
3.1 环境准备与硬件了解
首先,确保你的工程中已经正确初始化了DMA2D外设,并开启了相关时钟(AHB1总线)。STM32Cube HAL库或标准外设库提供了DMA2D的驱动函数,但为了极致性能,我们可能会直接操作寄存器。
DMA2D的主要工作模式之一就是“寄存器到存储器”(R2M),即用一个固定的颜色填充一个目标区域,这正是填充矩形所需。
3.2 实现自定义填充函数
#include "stm32f4xx_hal.h" // 或对应的硬件头文件 #include "GUI.h" /** * @brief 使用DMA2D硬件加速填充矩形 * @param LayerIndex: 图层索引,用于多图层系统 * @param x0, y0, x1, y1: 矩形对角坐标 * @param color: emWin格式的颜色值(LCD_COLOR) * @retval 无 */ static void _DMA2D_FillRect(int LayerIndex, int x0, int y0, int x1, int y1, LCD_COLOR color) { // 1. 获取当前激活图层的帧缓冲信息 GUI_DEVICE* pDevice = GUI_DEVICE_GetDevice(LayerIndex); void* pVRAM = pDevice->pVRAM; // 显存基地址 int BytesPerPixel = LCD_GetBitsPerPixelEx(LayerIndex) / 8; int Pitch = pDevice->vxSize * BytesPerPixel; // 一行字节数(步长) // 2. 计算目标矩形在显存中的起始地址 uint32_t dstAddress = (uint32_t)pVRAM + (y0 * Pitch) + (x0 * BytesPerPixel); uint32_t width = x1 - x0 + 1; uint32_t height = y1 - y0 + 1; // 3. 将emWin颜色转换为硬件格式 (例如RGB565或ARGB8888) uint32_t hwColor; if (BytesPerPixel == 2) { // RGB565格式 hwColor = ((color & 0xF80000) >> 8) | ((color & 0xFC00) >> 5) | ((color & 0xF8) >> 3); } else if (BytesPerPixel == 4) { // ARGB8888格式,假设emWin颜色是0xAARRGGBB hwColor = color; // 可能需要调整字节序 hwColor = __REV(hwColor); // 如果硬件要求字节序不同 } else { // 不支持的格式,回退到软件填充(或直接返回) GUI_FillRect(x0, y0, x1, y1); return; } // 4. 配置DMA2D寄存器 // 停止当前可能进行的任何传输 DMA2D->CR = 0x0; // 配置为寄存器到存储器模式,填充颜色 DMA2D->CR = DMA2D_R2M; // R2M模式 DMA2D->OPFCCR = (BytesPerPixel == 2) ? DMA2D_RGB565 : DMA2D_ARGB8888; // 输出颜色格式 DMA2D->OOR = (Pitch / BytesPerPixel) - width; // 行偏移(像素数) DMA2D->OMAR = dstAddress; // 输出存储器地址 // 配置要填充的颜色 DMA2D->OCOLR = hwColor; // 配置要传输的尺寸 DMA2D->NLR = (width << 16) | (height); // NLR[15:0]是行数,NLR[31:16]是每行像素数 // 5. 启动传输 DMA2D->CR |= DMA2D_CR_START; // 6. 等待传输完成(这里使用轮询,实际项目建议用中断+信号量以提高系统响应性) while ((DMA2D->ISR & DMA2D_FLAG_TC) == 0) {} DMA2D->IFCR |= DMA2D_FLAG_TC; // 清除传输完成标志 } /** * @brief 初始化并注册硬件加速函数 */ void BSP_LCD_HardwareAccel_Init(void) { // 初始化DMA2D硬件时钟等(通常在系统初始化时完成) // ... // 获取默认显示驱动设备(假设是第0层) GUI_DEVICE* pDevice = GUI_DEVICE_GetDevice(0); // 创建函数指针结构体并赋值 LCD_API_FUNC_LIST FuncList; GUI_MEMSET(&FuncList, 0, sizeof(FuncList)); // 清空结构体 FuncList.pfFillRect = _DMA2D_FillRect; // 将填充矩形函数指向我们的硬件实现 // 将函数列表设置到显示驱动中 LCD_SetDevFunc(pDevice, LCD_DEVFUNC_FILLRECT, (void(*)(void))&FuncList); // 可以继续设置其他加速函数,如拷贝矩形等 // FuncList.pfCopyRect = _DMA2D_CopyRect; // LCD_SetDevFunc(pDevice, LCD_DEVFUNC_COPYRECT, (void(*)(void))&FuncList); }3.3 关键步骤与避坑指南
- 获取正确的帧缓冲信息:
GUI_DEVICE_GetDevice和pVRAM是关键。必须确保你操作的是正确的图层和正确的内存地址。在多缓冲模式下,需要小心处理当前前后台缓冲区的切换。 - 颜色格式转换:这是最容易出错的地方。必须精确知道emWin内部当前使用的颜色格式(通过
LCD_GetBitsPerPixelEx获取)和你的硬件加速器要求的格式。RGB565和ARGB8888的位域排列必须完全匹配。建议编写一个独立的颜色转换测试函数进行验证。 - 步长(Pitch)计算:
Pitch是两行之间起始地址的字节偏移,它可能等于xSize * bpp,也可能因为内存对齐要求而更大。使用pDevice->vxSize(虚拟X大小)计算是最保险的。OOR(Output Offset Register)寄存器需要填入的是像素偏移,而不是字节偏移,所以是(Pitch / BytesPerPixel) - width。 - 同步与等待:示例中使用轮询等待DMA2D完成(
while循环)。这在简单应用中可行,但会阻塞CPU。更优的做法是启用DMA2D传输完成中断,并在中断服务例程中释放一个信号量。在_DMA2D_FillRect函数中启动传输后立即返回,由emWin的上层机制(或你的应用)在需要保证绘图完成时等待该信号量。这需要更复杂的集成,但能极大提高系统整体响应能力。 - 错误处理:示例中对于不支持的像素格式,回退到
GUI_FillRect。这是一个很好的降级策略,确保功能可用性。在实际产品中,你可能需要根据配置静态编译不同的函数,避免运行时判断。
4. 高级主题:缓存一致性与多缓冲
当你使用硬件加速器(如DMA2D)直接写入帧缓冲区,而该帧缓冲区所在的内存区域被CPU的数据缓存(Data Cache)覆盖时,就会遇到经典的缓存一致性问题。现象是:硬件已经写入了新数据到内存,但CPU缓存里的还是旧数据,或者反过来。这会导致屏幕上出现撕裂、残影或显示错误的数据。
4.1 问题根源与解决方案
emWin手册中“Framebuffer located in data cache area of CPU”章节专门讨论了此问题。核心解决方案有两个:
- 写通缓存(Write-Through):将帧缓冲区所在的内存区域配置为“写通”模式。这样,CPU写入缓存的数据会同时写入主存。硬件加速器总能读到最新的数据。这是最简单有效的办法,但会损失一部分写性能。
- 多缓冲配合缓存维护:如果缓存不能配置为写通,就必须使用多缓冲(Double/Triple Buffering),并配合缓存清理(Cache Clean)或无效化(Cache Invalidate)操作。
4.2 使用多缓冲与缓存清理钩子
emWin提供了GUI_DCACHE_SetClearCacheHook()函数来设置一个缓存清理钩子。其工作原理是:
- 你启用多缓冲(例如通过
WM_MULTIBUF_Enable(1))。 - emWin在后台缓冲区完成所有绘制。
- 在即将切换前后台缓冲区(执行真正的“翻页”)之前,emWin会调用你设置的钩子函数。
- 你在钩子函数中,清理包含即将变为前台的那个缓冲区的CPU数据缓存。
- 缓存清理完成后,emWin执行缓冲区切换,确保LCD控制器读取到的是刚从后台缓冲区写入内存的最新、最完整的数据。
示例钩子函数(基于CMSIS,假设帧缓冲是32位对齐的):
static void _ClearCacheHook(U32 LayerMask) { for (int i = 0; i < GUI_NUM_LAYERS; i++) { if (LayerMask & (1 << i)) { // 获取第i层当前**后台**缓冲区的地址和大小 // 这需要你根据自己多缓冲的实现方式来获取,这里是一个示例 void* pBuffer = _GetCurrentBackBuffer(i); U32 BufferSize = LCD_GET_XSIZE() * LCD_GET_YSIZE() * BYTES_PER_PIXEL; // 清理数据缓存(Clean by address) // 确保硬件加速器写入的数据对CPU缓存是可见的,并且清理后缓存中的数据与内存一致 SCB_CleanDCache_by_Addr((uint32_t*)pBuffer, BufferSize); // 注意:在某些架构和场景下,可能还需要无效化(Invalidate)缓存, // 如果CPU之后会读取这块被硬件修改过的内存。但对于纯硬件写入、CPU只读的帧缓冲, // 通常只需要清理(Clean)。 } } } // 在系统初始化时设置钩子 GUI_DCACHE_SetClearCacheHook(_ClearCacheHook); WM_MULTIBUF_Enable(1); // 启用多缓冲关键点:
- 时机至关重要:缓存清理必须在所有硬件加速绘图操作完成之后,且在缓冲区切换之前进行。
- 范围要精确:只清理需要切换的缓冲区,而不是整个缓存,以减少性能开销。
- 配合多缓冲:单缓冲模式下,由于前台缓冲区一直在被显示,你无法在显示过程中清理缓存而不造成闪烁。多缓冲将绘制和显示分离,才使得缓存维护可行。
- 动画与内存设备:对于使用
GUI_MEMDEV_系列函数实现的动画,需要额外调用GUI_MEMDEV_MULTIBUF_Enable()来确保它们也遵循多缓冲和缓存清理机制。
4.3 内存管理考量
硬件加速往往涉及DMA,而DMA要求源和目标地址在物理内存中是连续的,并且通常有对齐要求(如4字节、8字节对齐)。在分配帧缓冲区或用于加速操作的临时缓冲区时,必须使用支持缓存对齐和物理连续性的内存分配函数(例如malloc可能不满足,需要使用特定的驱动接口或链接脚本指定区域)。
emWin自己的内存池(通过GUI_ALLOC_AssignMemory()分配)主要用于窗口对象、内存设备等管理,不用于帧缓冲。帧缓冲需要你手动分配在合适的内存区域(如SDRAM、SRAM),并确保其地址和大小符合硬件加速器的要求。
通过GUI_ALLOC_GetMemInfo()等函数,你可以在开发阶段监控emWin内部内存的使用情况,确保没有因为硬件加速的引入(例如创建了更多离屏表面用于混合)而导致内存不足。
5. 调试技巧与性能评估
实现硬件加速后,如何验证它确实生效了,并且评估其性能提升?
功能验证:
- 最直接的方法:在自定义函数入口处设置一个GPIO引脚拉高,在函数退出时拉低。用示波器或逻辑分析仪观察波形。当执行相关图形操作时,如果看到该引脚产生脉冲,说明你的自定义函数被成功调用。
- 软件回退法:在自定义函数中,先调用原始的软件函数(如
GUI_FillRect),然后再执行硬件操作。观察屏幕效果是否正确。逐步注释掉软件调用,切换到纯硬件。
性能评估:
- CPU占用率:使用RTOS的任务运行时间统计功能,或者简单的系统滴答计数器,测量执行特定图形操作(如全屏填充、复杂位图绘制)前后CPU的繁忙程度。硬件加速后,CPU占用率应有显著下降。
- 帧率(FPS):实现一个简单的帧率计数器。在动画或连续刷新场景下,比较启用硬件加速前后的最大稳定帧率。
- 时间测量:直接使用CPU的周期计数器(如ARM的DWT->CYCCNT),在自定义函数内部测量硬件操作的实际执行时间。与软件实现的时间进行对比。
常见问题排查:
- 屏幕无变化或花屏:首先检查颜色格式转换是否正确。其次,检查写入的帧缓冲地址是否正确,特别是多图层、多缓冲情况下的地址计算。使用调试器查看目标内存区域的数据是否被正确写入。
- 性能提升不明显:检查硬件加速函数是否真的被频繁调用。可能该图形操作本身不是性能瓶颈,或者emWin由于某些条件(如裁剪区域太小、操作不支持)没有调用你的硬件函数。确保
LCD_SetDevFunc在显示驱动初始化之后、任何绘图操作之前被调用。 - 随机显示错误:高度怀疑是缓存一致性问题。尝试暂时关闭CPU的数据缓存,如果问题消失,则证实了这一点。然后严格按照多缓冲+缓存清理钩子的方案解决。
硬件加速的集成是一个从底层硬件到上层应用都需要仔细协调的过程。它带来的性能收益是巨大的,但调试过程也可能充满挑战。从最简单的填充矩形开始,逐步扩展到更复杂的操作,并辅以严谨的测试和性能分析,是确保项目成功的关键。当你看到原本卡顿的界面变得丝般顺滑时,这一切的努力都是值得的。
