LVGL移植实战:从硬件驱动到性能优化的嵌入式GUI开发指南
1. 项目概述为什么LVGL移植是嵌入式GUI开发的关键一步如果你正在开发一个带屏幕的嵌入式设备无论是智能手表、工业HMI面板还是家用电器的小显示屏最终都绕不开一个问题如何让界面变得好看又好用自己从零开始写绘图、事件处理、动画那工程量堪比造轮子。这时候一个成熟、开源、免费的图形库就成了刚需而LVGLLight and Versatile Graphics Library无疑是当前最热门的选择之一。我最早接触LVGL是在一个STM32F4的项目上需要为一个便携式检测仪开发交互界面。当时面临的选择有emWin、Qt for MCU等但要么授权费用高昂要么对硬件资源要求苛刻。LVGL以其极致的轻量、纯C编写、丰富的控件和活跃的社区脱颖而出。但拿到LVGL源码包你会发现它并不能直接在你的板子上跑起来这中间缺失的桥梁就是“移植”。所谓“移植”核心工作就是让LVGL这个“大脑”认识并驱动你的硬件“四肢”。它需要知道如何在你特定的屏幕上画一个点显示驱动如何知道你按下了哪个按键或触摸了哪里输入驱动以及如何获取系统时间来处理动画系统心跳。这个过程是任何LVGL项目开发的起点也是决定项目后期开发效率和运行稳定性的基石。网上教程很多但往往只给步骤不讲原理一旦换块板子或者遇到异常就容易卡壳。今天我就结合多次移植的经验从底层逻辑到实操细节带你彻底吃透LVGL移植。2. 移植前的核心准备与方案选型在动手写代码之前充分的准备和正确的方案选择能避免后期大量的返工。很多人一上来就照着教程复制粘贴lv_conf.h和lv_port_...文件结果编译一堆错误运行起来不是花屏就是没反应。2.1 硬件与软件环境评估首先你得对你的硬件平台有个清晰的认识这决定了移植的复杂度和可用的优化手段。MCU/MPU核心这是计算能力的来源。是Cortex-M0/M3/M4/M7这类微控制器MCU还是Cortex-A系列的应用处理器MPU这直接影响你能否使用操作系统、能否启用硬件加速。MCU如STM32 ESP32通常资源有限RAM和Flash可能只有几十到几百KB。移植重点在于极致优化启用LVGL的裁剪功能谨慎使用特效。可能没有MMU显示驱动多采用帧缓存Frame Buffer或直接写屏模式。MPU如RK3568 i.MX6ULL通常运行Linux等操作系统资源相对充裕。移植工作更侧重于在Linux Framebuffer或Wayland等显示服务上对接LVGL并可能利用GPU进行2D加速如通过ARM2D、NXP的PXP引擎等。显示接口与屏幕这是LVGL输出的最终目的地。接口类型决定了驱动程序的编写方式。并行接口如FSMC/FMC驱动8080/6800屏常见于STM32连接RGB屏或MCU屏。优点是速度快可直接操作总线模拟时序。移植时需要配置好FSMC的时序参数实现一个向指定内存地址写数据的函数。SPI接口多见于小尺寸屏1.3寸 1.54寸。速度较慢通常需要配合DMA来提升刷新效率避免CPU被长时间占用。移植时需要实现SPI的发送函数。RGB/MIPI DSI接口多见于MPU或高性能MCU连接大尺寸液晶屏。通常由LCD控制器LTDC硬件完成时序生成驱动重点是为LVGL提供一块帧缓存Frame Buffer的内存区域。输入设备这是用户与LVGL交互的通道。触摸屏电阻/电容最常见。通常通过I2C或SPI接口读取坐标。移植需要实现一个读取触摸状态和坐标的函数。物理按键/编码器用于无触摸屏的设备。需要将按键事件如按下、释放映射为LVGL的LV_KEY_...事件。鼠标或键盘在模拟器或一些特殊设备上使用。操作系统与任务调度LVGL不是RTOS但它需要一种机制来感知时间的流逝处理动画和执行周期性的任务如刷新屏幕。裸机无OS你需要配置一个定时器如SysTick在其中断服务程序中定期调用lv_tick_inc(x)来增加心跳并在主循环中不断调用lv_timer_handler()。RTOS如FreeRTOS RT-Thread你可以创建一个专有的LVGL任务线程。在该任务中调用lv_tick_inc(x)和lv_timer_handler()。通常还会创建一个信号量或消息队列在触摸或按键中断中发送事件通知LVGL任务去读取输入设备实现异步响应避免在中断中执行耗时操作。注意评估阶段务必查阅你所用MCU/MPU的数据手册、参考手册以及屏幕的数据手册Datasheet。搞清楚屏幕的驱动IC型号如ST7789 ILI9341、分辨率、色彩格式RGB565 RGB888以及初始化序列Init Code。2.2 LVGL版本选择V8.3 vs V9.xLVGL社区非常活跃版本迭代很快。目前主要分两大分支长期稳定且生态成熟的V8.3.x和全新架构、面向未来的V9.x如9.4。选择哪个版本是移植前必须做的关键决策。LVGL 8.3.x推荐给大多数新项目优点极度稳定社区资源极其丰富。你遇到的99%的问题在GitHub、论坛都能找到答案。教程、组件、主题、工具链如SquareLine Studio的支持最为完善。对于商业项目选择V8.3风险最低。缺点代码架构相对老旧一些新特性如新的Flex/Grid布局没有。结论如果你是第一次移植或者项目周期紧、要求稳强烈建议从V8.3.11这个LTS版本开始。它经过了无数项目的验证。LVGL 9.x如9.4 适合探索和未来项目优点全新的架构渲染管线更现代为未来的高级特性如3D变换、更复杂的混合效果打下了基础。官方未来的开发重心在此。缺点仍处于快速发展期API可能变动第三方组件和工具链支持相对滞后。移植教程和问题解决方案较少需要更多自己摸索。结论如果你是一个技术爱好者想提前体验最新架构或者项目周期长可以接受前期的一些不稳定性那么可以选择V9.x。注意V9的移植接口和驱动模型与V8有较大差异。我个人在大多数生产项目中仍使用LVGL 8.3。除非客户明确要求或项目有特殊技术前瞻性需求否则V8.3的稳定性和丰富的生态是无可替代的优势。下文将以LVGL 8.3.11为例进行讲解其原理对V9同样有参考价值。2.3 工程结构规划清晰的工程结构能让后续的开发和维护事半功率。不建议把LVGL的所有源码都胡乱堆在项目里。你的项目目录/ ├── Drivers/ # 你的板级驱动BSP ├── Middlewares/ │ └── LVGL/ # LVGL中间件 │ ├── lvgl/ # 从官网下载的LVGL核心库源码src, examples等 │ ├── lvgl_port/ # 移植层文件我们主要编写的部分 │ │ ├── lv_port_disp.c/.h # 显示驱动 │ │ ├── lv_port_indev.c/.h # 输入设备驱动 │ │ └── lv_port_fs.c/.h # 文件系统驱动可选用于加载图片字体 │ └── lv_conf.h # LVGL配置文件核心 ├── Application/ # 你的应用业务逻辑 └── ...这种结构将LVGL作为中间件隔离lvgl_port目录下的文件就是我们需要针对硬件编写的“胶水代码”。lv_conf.h则是控制LVGL功能裁剪和性能调优的总开关。3. 核心移植步骤详解与驱动实现准备工作做完我们进入核心的移植编码环节。这个过程可以概括为一配、二显、三输入、四心跳。3.1 第一步配置文件 lv_conf.h 的深度定制lv_conf.h是LVGL的“大脑配置文件”直接决定了库的功能、性能和内存占用。直接从官方模板lv_conf_template.h复制并重命名为lv_conf.h然后开始修改。以下是一些关键配置项基础使能#define LV_COLOR_DEPTH 16 // 颜色深度。16位RGB565最常用兼顾性能和色彩。 #define LV_MEM_SIZE (48U * 1024U) // LVGL动态内存池大小。根据你的RAM余量设置建议至少32KB。裁剪与优化针对资源紧张的平台// 禁用不需要的模块节省Flash和RAM #define LV_USE_LOG 0 // 除非调试否则关闭日志输出 #define LV_USE_ASSERT 0 // 关闭断言发布版本用 #define LV_USE_THEME_DEFAULT 0 // 精简主题 #define LV_USE_ANIMATION 1 // 动画如果界面简单可关闭 #define LV_USE_SHADOW 0 // 阴影效果比较耗资源 #define LV_USE_BLEND_MODES 0 // 混合模式非必需可关闭 // 限制控件类型 #define LV_USE_CHART 1 // 如果你需要图表如示波器UI #define LV_USE_TABLE 0 // 如果不用表格就关闭性能相关#define LV_DISP_DEF_REFR_PERIOD 30 // 默认刷新周期(ms)。影响lv_timer_handler调用频率。 #define LV_INDEV_DEF_READ_PERIOD 30 // 输入设备默认读取周期(ms)。 #define LV_DPI_DEF 130 // 每英寸像素数影响物理尺寸换算根据屏幕尺寸调整。实操心得不要一开始就把所有功能都打开。先根据产品UI原型确定必须用到的控件和效果只启用这些。项目后期如果内存有富余再逐步打开一些美化效果。LV_MEM_SIZE设置过小会导致lv_mem_alloc失败表现为创建对象失败或随机崩溃设置过大则浪费宝贵RAM。一个简单的估算方法是创建一个最复杂的页面打印lv_mem_get_free_size()看看剩余多少然后适当调整总大小。3.2 第二步显示驱动 (lv_port_disp) 的实现显示驱动是移植中最关键、也最容易出问题的一环。它的核心任务是实现一个回调函数在这个函数里把LVGL绘制好的图像数据一个矩形区域搬运到你的屏幕上。驱动模型选择 LVGL V8.3主要支持三种显示驱动模型你需要根据硬件能力选择其一帧缓存Frame Buffer模式推荐如果RAM足够原理在RAM中开辟一块和屏幕分辨率一样大的缓冲区Frame Buffer。LVGL所有的绘制操作都在这块内存中进行。驱动回调函数只需要将这块内存的整个或部分内容一次性复制到显示设备的显存中。优点绘制和刷新解耦避免撕裂Tearing。可以利用DMA在后台搬运数据极大解放CPU。缺点消耗大量RAM例如320x240 RGB565屏需要3202402 ≈ 150KB。适用场景RAM充足的MCU如STM32F429/F7/H7带外部SDRAM或所有运行Linux/RT-Thread等系统的MPU。直接模式Direct Mode或单缓存One Buffer原理LVGL绘制完一帧图像后立即通过CPU或DMA将数据发送到屏幕。通常需要等待上一帧发送完成。优点节省RAM只需要一小块行缓存或甚至不需要额外缓存。缺点容易因绘制和发送速度不匹配导致屏幕撕裂。CPU占用率高。适用场景小分辨率屏幕、SPI接口屏或RAM极其紧缺的场合。双缓存Double Buffering原理有两块帧缓存。LVGL绘制到“后台缓存”时显示器从“前台缓存”读取数据。绘制完成后交换两个缓存。这是帧缓存的增强版完全杜绝撕裂。优点无撕裂流畅。缺点消耗双倍RAM。适用场景对流畅度要求极高且RAM充足的应用。以最常见的“帧缓存DMA”模式STM32 LTDC/RGB屏为例实现步骤初始化底层显示硬件 在lv_port_disp_init函数中首先调用你自己的LCD_Init()函数。这个函数需要完成初始化MCU的LCD控制器如STM32的LTDC。配置时序参数像素时钟、同步信号宽度等。分配帧缓存内存通常放在外部SDRAM或内部DMA可访问的RAM。将帧缓存地址告知LTDC。点亮背光。实现刷新回调函数 这是驱动核心。LVGL在需要刷新屏幕某块区域时会调用这个函数。static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { // area: 需要更新的矩形区域 (x1, y1, x2, y2) // color_p: 该区域新的图像数据数组RGB565格式 // 1. 将color_p中的数据拷贝到帧缓存的对应位置。 // 这是一个简单的内存拷贝但要注意目标地址的计算。 uint32_t offset (area-y1 * screen_width area-x1) * sizeof(lv_color_t); uint32_t copy_width lv_area_get_width(area) * sizeof(lv_color_t); for(int y area-y1; y area-y2; y) { memcpy((uint8_t*)frame_buffer offset, (uint8_t*)color_p, copy_width); offset screen_width * sizeof(lv_color_t); color_p lv_area_get_width(area); } // 2. 启动DMA将帧缓存中的这块区域数据搬运到LCD显存如果硬件需要。 // 对于STM32 LTDC帧缓存就是显存所以上一步的memcpy已经完成了更新。 // 对于SPI屏这里需要启动SPI DMA传输。 // Example: SPI_DMA_Send(area-x1, area-y1, area-x2, area-y2, (uint8_t*)color_p_start); // 3. 重要告诉LVGL刷新完成。 lv_disp_flush_ready(disp_drv); }关键细节lv_disp_flush_ready(disp_drv)必须在数据真正被发送到屏幕后调用。如果是DMA传输应该在DMA传输完成中断中调用这个函数而不是在启动DMA后立即调用。否则LVGL会认为绘制已完成开始下一帧导致数据被覆盖屏幕出现乱码。配置并注册显示驱动void lv_port_disp_init(void) { // ... 初始化硬件 ... static lv_disp_draw_buf_t draw_buf; // 绘制缓冲区描述符 static lv_color_t buf_1[SCREEN_WIDTH * 10]; // 定义一块绘制缓存行缓存 // 如果使用全屏帧缓存这里可以指向你的帧缓存地址或者设为NULL具体看模式。 lv_disp_draw_buf_init(draw_buf, buf_1, NULL, SCREEN_WIDTH * 10); static lv_disp_drv_t disp_drv; lv_disp_drv_init(disp_drv); disp_drv.hor_res SCREEN_WIDTH; disp_drv.ver_res SCREEN_HEIGHT; disp_drv.flush_cb disp_flush; // 设置刷新回调 disp_drv.draw_buf draw_buf; disp_drv.full_refresh 0; // 除非用单缓存模式且每次全刷否则设为0 // 如果屏幕旋转了90/180/270度在这里设置旋转 // disp_drv.sw_rotate 1; // disp_drv.rotated LV_DISP_ROT_90; lv_disp_t * disp lv_disp_drv_register(disp_drv); // 注册驱动 }关于“滑动撕裂”问题 这是直接模式或DMA使用不当的典型表现。当LVGL绘制下一帧的速度快于屏幕刷新速度时屏幕上半部分还是上一帧图像下半部分已经变成了新的一帧中间出现一条明显的“撕裂线”。解决方案1最佳使用帧缓存垂直同步VSync。在disp_flush中等待上一次DMA传输完成或LCD的VSync信号再开始新的数据传输和lv_disp_flush_ready。解决方案2使用双缓存。LVGL V8.3的draw_buf可以配置两个缓存实现乒乓操作。解决方案3妥协在disp_flush中不使用DMA而是用CPU阻塞拷贝并在完成后调用lv_disp_flush_ready。这会导致刷新率下降CPU占用高但简单。3.3 第三步输入设备驱动 (lv_port_indev) 的实现输入驱动负责将物理输入触摸、按键转换为LVGL能理解的事件。触摸屏是最常见的。触摸屏驱动实现初始化底层触摸IC 在lv_port_indev_init中调用你的Touch_Init()初始化I2C/SPI复位触摸芯片等。实现读取回调函数static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data) { static lv_coord_t last_x 0; static lv_coord_t last_y 0; uint8_t touched 0; uint16_t x 0, y 0; // 1. 读取触摸芯片状态和坐标 // 这是你的硬件相关代码 touched TP_Read_Point(x, y); // 假设这个函数返回是否触摸并填充坐标 // 2. 填充 data 结构体 >void lv_port_indev_init(void) { // ... 初始化触摸硬件 ... static lv_indev_drv_t indev_drv; lv_indev_drv_init(indev_drv); indev_drv.type LV_INDEV_TYPE_POINTER; // 触摸屏是指点设备 indev_drv.read_cb touchpad_read; lv_indev_t * touch_indev lv_indev_drv_register(indev_drv); // 你可以注册多个输入设备比如再注册一个按键设备 // indev_drv.type LV_INDEV_TYPE_KEYPAD; // indev_drv.read_cb keyboard_read; // lv_indev_t * kb_indev lv_indev_drv_register(indev_drv); }按键驱动实现要点 如果你用按键控制比如五向按键type设为LV_INDEV_TYPE_KEYPAD。在read_cb里你需要将按键值映射为LVGL定义的键值如LV_KEY_UP,LV_KEY_ENTER并通过>// 在SysTick中断1ms一次中 void SysTick_Handler(void) { lv_tick_inc(1); // 告诉LVGL时间过去了1ms } // 在主循环中 while(1) { lv_timer_handler(); // 处理LVGL任务包括刷新显示 // ... 你的其他任务 ... // 注意lv_timer_handler()不能放在中断中调用 }RTOS环境如FreeRTOS任务// 创建一个专有的LVGL任务 void lvgl_task(void *pvParameters) { TickType_t last_wake_time xTaskGetTickCount(); const TickType_t period pdMS_TO_TICKS(5); // 5ms周期即200Hz for(;;) { lv_tick_inc(5); // 增加5ms心跳 lv_timer_handler(); // 处理LVGL任务 vTaskDelayUntil(last_wake_time, period); // 精确延时 } } // 在触摸中断中通过队列/信号量通知lvgl_task去读取触摸数据 void Touch_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; xSemaphoreGiveFromISR(touch_semaphore, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }注意事项lv_timer_handler()的执行时间不固定取决于当前界面复杂度和是否有动画。在RTOS中务必给LVGL任务分配足够的堆栈空间建议至少4KB并设置合适的优先级通常比IDLE任务高但低于关键实时任务。4. 高级优化与调试技巧当基础移植完成界面能跑起来后接下来就是优化性能和解决那些棘手的Bug。4.1 性能优化策略启用编译器优化在MDK/IAR/GCC中将优化等级提高到-O2或-Os优化尺寸能显著提升LVGL的渲染速度。使用DMA2D/Chrom-ART如果MCU支持 STM32F4/F7/H7系列包含一个2D图形加速器DMA2D。它可以硬件加速颜色格式转换、填充、图像混合Alpha Blending和图像拷贝。LVGL官方提供了lv_gpu_stm32_dma2d.c的移植文件。启用方法在lv_conf.h中定义LV_USE_GPU_STM32_DMA2D 1并在lv_port_disp.c的disp_flush函数中将内存拷贝操作替换为DMA2D传输。对于矩形填充和图像绘制性能提升可达数倍。使用ARM2DCortex-M通用2D库 ARM2D是一个软件库但针对Cortex-M内核的SIMD指令进行了高度优化能在不支持硬件加速的M0/M3/M4内核上显著提升像素操作速度。LVGL V8.3和V9都支持ARM2D后端。启用方法从ARM官网获取ARM2D库在lv_conf.h中定义LV_USE_GPU_ARM2D 1并正确包含路径。它相当于一个高效的“软加速”。优化绘制区域Partial Update 确保你的disp_flush函数正确实现了局部刷新。LVGL只会传递发生变化的区域area参数给驱动。在驱动中你应该只更新屏幕的这一块区域而不是全屏刷新这对于SPI等慢速接口屏尤为重要。降低刷新率 如果界面相对静态可以在lv_conf.h中增大LV_DISP_DEF_REFR_PERIOD例如从30ms改为50ms减少lv_timer_handler的调用负担。4.2 显示与输入问题深度排查即使按照步骤做了第一次上电很可能还是黑屏或触摸失灵。别慌按以下步骤排查黑屏/花屏问题排查清单现象可能原因排查方法完全黑屏背光亮1. 显示硬件初始化失败。2. 帧缓存地址未正确设置到LCD控制器。3. LVGL未成功创建任何显示对象。1. 用逻辑分析仪或示波器检查LCD接口时序如LTDC的HSYNC, VSYNC, PCLK。2. 在初始化后手动向帧缓存写入一个固定颜色如全红看屏幕是否有变化绕过LVGL。3. 在lv_port_disp_init后立即创建一个简单的标签lv_label_create(lv_scr_act())看LVGL是否运行。屏幕有显示但乱码、条纹1. 颜色格式不匹配如LVGL配置RGB565但屏幕期望RGB888。2. 帧缓存内存对齐或大小问题。3. DMA传输数据宽度或地址错误。4.disp_flush中lv_disp_flush_ready调用时机错误。1. 检查LV_COLOR_DEPTH和屏幕驱动IC要求的格式。2. 确保帧缓存地址是内存对齐的通常是4字节或8字节。3. 检查DMA配置的源/目标地址、数据宽度半字字。4.重点确保lv_disp_flush_ready在数据真正传输完毕后才调用。在DMA完成中断中调用它。屏幕部分区域刷新异常1.disp_flush中的区域拷贝计算错误。2. 帧缓存的行宽stride设置错误。1. 仔细检查area-x1/y1/x2/y2到线性地址的转换计算。2. 确保拷贝时目标地址的步进是屏幕的物理宽度screen_width而不是区域的宽度。触摸失灵/不准问题排查清单现象可能原因排查方法完全无反应1. 触摸IC初始化失败I2C/SPI通信异常。2. 中断配置错误如果是中断模式。3. LVGL输入设备未注册或read_cb未正确赋值。1. 用调试器或打印检查I2C/SPI是否能正确读写触摸IC的寄存器如芯片ID。2. 在touchpad_read函数开头加调试打印看LVGL是否在周期调用它。3. 检查lv_indev_drv_register返回值是否非NULL。坐标反向或偏移1. 坐标轴方向未转换。2. 未进行触摸校准。1. 在touchpad_read中对读取的x, y值进行交换或screen_width - x等操作。2. 实现一个触摸校准界面。LVGL本身不提供需要自己写。通常采集屏幕四个角的触摸值计算出一个转换矩阵。触摸抖动、跳点1. 触摸屏本身噪声大。2. 软件无滤波。1. 检查触摸屏的供电是否稳定地线是否良好。2. 在touchpad_read中加入软件滤波例如连续采样3次取中值或做一个简单的滑动平均滤波。4.3 字体与中文显示LVGL支持内置字体和外部字体。显示中文必须使用外部字体。生成字库使用LVGL官方在线工具LVGL Font Converter。选择中文字符集如GB2312选择需要的字体和大小导出为.c文件。注意字体会占用大量Flash务必精简字符集只包含项目用到的汉字。集成字库将生成的.c文件如my_font_20.c加入工程。在需要使用该字体的C文件中声明LV_FONT_DECLARE(my_font_20)。在样式或标签中引用lv_style_set_text_font(style, my_font_20)或lv_label_set_text_font(label, my_font_20)。文件系统支持加载SD卡中的字体/图片 如果字体太大可以放在外部Flash或SD卡运行时加载。这需要实现lv_port_fs.c对接你的文件系统FatFS, LittleFS等并在lv_conf.h中启用LV_USE_FS_...相关宏。4.4 使用模拟器加速开发在硬件板子出来之前或者调试UI逻辑时在PC上使用模拟器是最高效的方式。Visual Studio Code配合LVGL Simulator扩展是当前最流行的选择。在VSCode中安装“LVGL Simulator”扩展。创建一个新的工程或打开已有的LVGL工程需要标准的lv_conf.h和lv_drv_conf.h。扩展会自动配置SDL2作为显示和输入后端。你写的LVGL应用代码几乎可以无缝在模拟器上运行、调试。你可以实时修改界面代码模拟器会热重载极大提升UI设计效率。踩过的坑模拟器环境下的lv_conf.h可能和硬件环境不同比如颜色深度、内存大小。建议通过宏定义区分例如#ifdef __WIN32__ // 或者 SIMULATOR #define LV_MEM_SIZE (1024U * 1024U) // 模拟器给1MB #else #define LV_MEM_SIZE (48U * 1024U) // 硬件环境48KB #endif移植LVGL就像为这个强大的图形引擎装上适合你战车的轮子和方向盘。整个过程充满了对硬件底层和图形系统原理的深入理解。从最开始的屏幕点亮到触摸流畅响应再到利用DMA2D实现丝滑动画每一步问题的解决都会带来巨大的成就感。我个人的体会是不要惧怕阅读LVGL的源码和社区讨论很多问题的答案就藏在驱动函数的注释和社区的Issue里。当你成功移植后你会发现构建一个美观、流畅的嵌入式GUI界面剩下的就是发挥你的创意和设计能力了。最后一个小技巧务必做好版本管理为每个稳定的驱动状态打上标签这样当LVGL库升级或者硬件变更时你能有一个清晰的回退和对比基准。