LabVIEW UDS刷写Main.vi:状态机编排与图莫斯协议栈协同设计 1. 这不是“主VI”而是UDS刷写流程的中央调度器很多人第一次看到这个标题里的“Main.vi”下意识会把它当成LabVIEW里那个带图标、能双击运行的普通顶层VI——就像Windows桌面的快捷方式一样点开就走。但在这套基于图莫斯Toumos的CAN UDS升级上位机系统里Main.vi根本不是入口而是整个刷写生命周期的编排中枢与状态仲裁器。它不直接发帧、不解析响应、不管理CAN硬件资源却决定着每一帧该不该发、什么时候发、发完之后该跳转到哪一步、出错时该回滚到哪个检查点。这种设计思路和传统LabVIEW初学者习惯的“一个VI干所有事”完全不同。我最早做第一版UDS上位机时就把所有逻辑塞进Main.vi初始化CAN卡、读ECU ID、请求下载、分段传输、校验、重编程……结果调试时只要一个环节出错整个VI就卡死在某个While循环里连错误码都抓不到。后来在某次整车厂现场支持中一位做了十年汽车电子诊断的老工程师指着我的代码说“你这不是上位机是单片机裸奔程序。”这句话让我彻底重构了架构。真正的Main.vi必须像交通指挥中心——红绿灯怎么配时、应急车道何时启用、事故路段如何绕行全由它动态决策而不是靠司机即子VI自己拍脑袋。这也解释了为什么网络热词里反复出现“uds刷写详细流程”“uds 31服务”“uds 19服务”这些关键词它们不是孤立命令而是一组有严格时序、状态依赖、容错回退的原子操作。Main.vi的核心价值就是把ISO 14229-1里那些用文字描述的“若NRC为0x78则等待P2*扩展时间后重试”“若服务$31子功能$01执行失败则需先执行$22读取当前安全等级”这类规则翻译成LabVIEW可执行、可监控、可中断、可日志追溯的状态机逻辑。它不处理CAN物理层的位填充、ACK应答、总线仲裁那是图莫斯底层驱动的事但它必须知道当CAN报文ID为0x7E0的响应帧迟迟不来时是等超时、还是主动发Tester Present、或是触发安全解锁流程这个判断就是Main.vi存在的全部意义。提示如果你的Main.vi里还充斥着大量“Call Library Function Node”调用图莫斯DLL、或者直接拖拽CAN Open VI放在前面板上说明你还没跳出“工具链使用者”的思维。真正的编排者应该只和“状态”“事件”“超时”“条件分支”打交道硬件交互全部下沉到独立的Driver Layer VI中。2. 图莫斯不是“插件”而是UDS协议栈的可信执行环境网上搜索热词里频繁出现“图莫斯删除ldf文件”“图莫斯 CAN UDS”但很少有人讲清楚图莫斯Toumos在这里的角色既不是CAN卡驱动也不是通用通信中间件而是一个经过车规级验证的UDS协议栈运行容器。它把ISO 14229-1标准里定义的26个核心服务$10-$37, $85等、NRC码体系0x11/0x12/0x33/0x78等、会话控制逻辑Default/Programming/Extended、安全访问流程Seed-Key机制、DTC读取规则全部封装成一组稳定、可复用、带完整错误注入测试能力的API。你不需要自己手写$22服务的请求帧格式0x22 2字节DID也不用纠结$31服务子功能$01的参数编码规则0x31 01 XX YY ZZ图莫斯已经把这些“协议细节”变成了LabVIEW里一个输入控件、一个输出簇、一个布尔标志位。举个实际例子UDS刷写中最容易踩坑的“$34 Request Download”服务。标准要求请求帧必须包含服务ID0x34、数据格式标识符DFI通常0x00、内存地址长度AL如0x04、内存长度长度ML如0x04、起始地址4字节、长度4字节。初学者常在这里栽跟头——比如把地址长度设成0x02对应16位地址但ECU实际要求32位或者把DFI错填成0x10带压缩而ECU只支持0x00无压缩。图莫斯的RequestDownload.vi内部已经做了强校验当你传入一个32位地址数组它自动补零并按大端序打包当你传入长度值超过ECU最大块大小从LDF文件解析得出它直接返回错误簇而非发非法帧。这背后是图莫斯加载LDFLogical Data Format文件后构建的完整ECU内存映射模型——而“图莫斯删除ldf文件”这个热搜词恰恰暴露了很多人没理解LDF才是刷写配置的唯一信源它定义了每个Flash段的起始地址、长度、擦除粒度、校验算法CRC16/CRC32、甚至安全访问所需的密钥种子偏移量。所以Main.vi和图莫斯的关系不是“调用者-被调用者”而是“策略-执行器”。Main.vi决定“现在要请求下载第3段数据”图莫斯负责“确保请求帧完全合规并在收到0x74响应后把接收到的NACK码如0x31表示拒绝下载准确反馈回来”。这种分工让Main.vi可以专注流程逻辑比如当图莫斯返回NRC 0x31时Main.vi应触发“擦除对应扇区”子流程当返回NRC 0x78requestCorrectlyReceived-ResponsePending时它必须启动P2*超时计时器通常2秒并在超时前持续发送Tester Present$3E保持会话激活。这些决策树才是Main.vi真正要写的代码。注意图莫斯的LDF文件不是可选配置。如果你跳过LDF加载步骤直接用硬编码地址刷写哪怕CAN通信物理层一切正常也会在$34服务阶段收到NRC 0x31requestOutOfRange。因为ECU的Bootloader会校验请求地址是否落在其允许刷写的Memory Range内——这个Range信息只存在于LDF的 节点里。3. Main.vi的四大核心状态机从“准备”到“收尾”的闭环控制把Main.vi看作一个静态VI是最大的误解。它本质上是一个多层级嵌套的状态机State Machine外层管理刷写宏观阶段Preparation → Erase → Transfer → Verify → PostProcessing内层处理每个阶段的原子操作如Transfer阶段包含RequestDownload → TransferData → RequestTransferExit三步。网络热词里高频出现的“uds刷写流程”“uds 31服务”正是这个状态机中关键跃迁的触发点。下面拆解Main.vi实际运行时的四个不可绕过的状态模块3.1 初始化与ECU握手状态State: InitAndHandshake这不是简单的“打开CAN端口”就能完成。Main.vi在此状态要完成三件事CAN硬件预检调用图莫斯的CAN_Open.vi但必须捕获其返回的错误码。常见错误如“can not open com port”端口被占用、“can总线未连接”物理层断开、“波特率不匹配”ECU要求500kbps而配置成250kbps。这里有个实操技巧不要依赖图莫斯的默认超时通常1秒而是在Main.vi中加一个“CAN Link Test”子VI连续发5帧0x7DFDiagnostic Request并监听0x7E8Diagnostic Response只有3帧以上收到响应才判定总线连通。会话模式切换UDS要求必须先进入Programming Session$10 02才能执行刷写相关服务。但ECU可能处于Default Session$10 01此时直接发$10 02会返回NRC 0x7FserviceNotSupported。Main.vi必须先发$10 01再发$10 02并校验响应中的Session Control Type0x02和P2*时间参数用于后续超时计算。安全等级预置很多ECU在Programming Session下仍要求Security Access$27服务。Main.vi需从LDF中读取Security Level如Level 0x01然后执行Seed-Key流程——图莫斯的GetSeed.vi返回6字节SeedMain.vi用预置Key Algorithm如XORRotate生成Key再调用SendKey.vi。若Key错误ECU返回NRC 0x33securityAccessDenied此时Main.vi应记录失败次数并触发锁止延迟Lockout Timer而非无限重试。3.2 内存擦除状态State: MemoryErase这是刷写前最耗时也最易出错的环节。网络热词“uds 31服务”指的就是$31服务RoutineControl其中子功能$01常用于擦除Flash。Main.vi在此状态的关键逻辑是擦除范围校验从LDF中提取待擦除Segment的StartAddress和Length计算出EndAddress。若ECU的擦除粒度是4KB而请求擦除3KB图莫斯会返回NRC 0x31requestOutOfRange。Main.vi必须提前做对齐计算StartAddress向下对齐到4KB边界Length向上扩展到4KB整数倍。擦除进度监控$31服务响应中ECU可能返回0x00success或0x01inProgress。后者意味着擦除尚未完成Main.vi必须启动P2*超时通常10秒并在此期间每500ms发一次$31 01 XX YYRoutineControl request with same subfunction查询状态直到收到0x00或超时。失败回退机制若擦除超时Main.vi不能直接报错退出。它必须执行“安全复位”发$11 01ECU Reset强制重启ECU再重新进入Programming Session否则ECU可能卡在异常状态无法响应后续命令。3.3 数据传输状态State: DataTransfer这是Main.vi最密集的状态包含三个子状态循环RequestDownload$34传入当前Block的地址、长度图莫斯返回最大块大小MaxNumberOfBytes和内存属性。Main.vi据此将固件Bin文件切分成多个Block如每块256字节。TransferData$36对每个BlockMain.vi需构造$36请求帧含BlockSequenceCounter并处理ECU的0x76响应。关键点在于若ECU返回NRC 0x78responsePendingMain.vi必须暂停发送下一个Block转而发Tester Present$3E 00维持会话直到收到0x76响应或P2*超时。RequestTransferExit$37所有Block发送完毕后发$37通知ECU结束传输。若ECU返回NRC 0x7EsubFunctionNotSupported说明它不支持此服务Main.vi应跳过此步直接进入Verify阶段。3.4 校验与收尾状态State: VerifyAndFinalize最后一步看似简单实则暗藏玄机Check Programming Integrity$31 03调用$31子功能03校验Flash内容。ECU返回CRC值Main.vi需用相同算法如CRC32-MPEG2计算本地Bin文件对应段的CRC并比对。若不一致Main.vi必须标记“校验失败”并触发“重刷该Block”流程而非整体失败。ECU Reset$11 01成功后发$11 01重启ECU。但注意有些ECU要求Reset后立即进入Default Session$10 01Main.vi需在Reset后延时500ms再发$10 01确认ECU已恢复正常。日志归档Main.vi在此状态生成完整刷写报告含时间戳、ECU VIN、固件版本、各阶段耗时、错误码统计并保存为CSV文件。这是售后追溯的唯一依据——当用户搜索“uds故障诊断”时这份日志比任何口头描述都更有说服力。4. 超时管理P2*、P2start、P3——UDS刷写的生命线几乎所有UDS刷写失败案例根源都在超时参数设置错误。网络热词里“access error: 404 -- not found cant locate document: /notsupported.asp”看似是网页错误实则是开发者混淆了HTTP超时和UDS超时概念的典型表现——他们把Web开发经验直接套用到车载诊断上结果在Main.vi里用固定1秒延时去等ECU响应而ECU的P2*Response Pending Time可能是2000msP3*Programming Time可能是30秒。Main.vi的超时管理不是简单的Wait函数而是一套与ECU实时协商的动态计时系统。ISO 14229-1定义了三类关键超时P2Response Pending Time*当ECU返回NRC 0x78requestCorrectlyReceived-ResponsePending时上位机必须在此时间内持续发送Tester Present$3E否则会话超时。P2值由ECU在$10服务响应中给出如0x07D0 2000ms。Main.vi必须在收到0x78后立即启动P2计时器并在剩余时间500ms时发$3E 00。P2startStart of P2ECU开始处理请求到返回第一个响应0x78或最终响应的时间上限。若ECU在P2start内未返回任何响应Main.vi应判定为“ECU无响应”触发硬件复位。P2start通常为P2*的2倍如4000ms。P3Programming Time*执行$31/$34等耗时服务的最大允许时间。例如擦除Flash可能需要15秒Main.vi必须为此服务单独设置P3计时器如0x3A98 15000ms而非复用P2。我在某次项目中遇到一个诡异问题刷写总是卡在$34服务图莫斯返回“Timeout waiting for response”但用CANoe抓包发现ECU明明在2秒后发了0x74响应。排查发现Main.vi里P2计时器被错误地设为1000ms硬编码而ECU实际P2是2000ms。修复方法很简单在$10服务响应解析后用图莫斯的GetP2Star.vi动态读取ECU返回的P2值并将其写入Main.vi的全局变量或移位寄存器中后续所有$34/$36/$37服务都以此值为基准。更稳妥的做法是在Main.vi的“InitAndHandshake”状态末尾专门加一个“Read ECU Timing Parameters”子VI一次性获取P2、P2start、P3并缓存到一个TimingConfig簇中供所有状态机调用。提示不要在Main.vi里用“Wait (ms)”函数实现超时LabVIEW的Wait函数会阻塞整个线程导致无法同时处理CAN接收、UI刷新、日志写入。正确做法是使用“Elapsed Time Express VI”配合While循环或采用“Timed Loop”结构让超时检测与其他任务并行执行。5. 错误码NRC驱动的自适应恢复从“报错退出”到“智能重试”UDS协议里最让人头疼的不是命令发不出去而是ECU返回的各种NRCNegative Response Code。网络热词里“uds nrc”“uds诊断”高频出现正说明开发者普遍缺乏对NRC的系统性处理能力。Main.vi如果只是简单弹窗显示“NRC 0x12”那它只是个报错器不是刷写控制器。真正的Main.vi必须把每个NRC翻译成具体的恢复动作并嵌入状态机流转逻辑中。以下是Main.vi必须内置的NRC响应策略表基于ISO 14229-1标准NRC码含义Main.vi应执行的动作实操要点0x11serviceNotSupported记录日志跳过该服务继续下一阶段常见于ECU Bootloader不支持$31服务需改用$2E写入Flash控制寄存器0x12subFunctionNotSupported检查子功能号是否超出ECU支持范围降级尝试其他子功能如$31 01失败可尝试$31 02擦除RAM做兼容性测试0x22incorrectMessageLengthOrInvalidFormat校验请求帧长度检查LDF中定义的DID长度是否匹配多因LDF文件版本与ECU不匹配导致需强制重新加载LDF0x31requestOutOfRange重新计算内存地址对齐检查LDF中Segment定义是否正确必须从LDF读取ActualStartAddress而非使用固件文件偏移0x33securityAccessDenied记录失败次数触发Lockout Timer如30秒避免暴力破解Key算法错误时ECU会增加锁止时间Main.vi需同步更新Timer0x78requestCorrectlyReceived-ResponsePending启动P2*计时器周期发送$3E Tester Present发送间隔必须≤P2*/2且最后一次$3E需在P2*结束前100ms发出0x7EsubFunctionNotSupportedInActiveSession检查当前会话模式必要时发$10 02切换到Programming Session常见于ECU在Default Session下拒绝$34服务0x7FserviceNotSupportedInActiveSession同上但需确认ECU是否支持该服务的当前会话模式可能需先执行$27安全访问再切换会话举个真实案例某次刷写中Main.vi在$34服务收到NRC 0x7E。按常规思路我们会认为“服务不支持”直接报错。但深入分析发现ECU的Bootloader文档明确写了$34仅在Programming Session下有效而Main.vi在进入Transfer状态前错误地停留在Default Session。修复方案是在State: DataTransfer的入口处强制插入一个“Verify Session Mode”子VI调用图莫斯的GetCurrentSession.vi若返回0x01Default则立即发$10 02切换。这个检查点后来被固化到Main.vi的每个关键服务调用前。另一个关键经验NRC处理必须带上下文记忆。比如$27服务连续3次返回0x33Main.vi不应第4次再试而应记录“Security Access Lockout”并提示用户“请等待30秒后重试”。这个“30秒”不是随意定的而是从ECU的$27响应中解析出的Lockout Time单位秒图莫斯的GetLockoutTime.vi可直接获取。Main.vi用一个Shift Register存储当前Lockout剩余时间并在UI上实时倒计时显示——这才是用户真正需要的“智能反馈”而不是冷冰冰的“access error”。6. LabVIEW工程结构实战为什么Main.vi必须“瘦”而Driver Layer必须“厚”很多初学者的LabVIEW项目Main.vi动辄上千行代码里面混着CAN初始化、UI控件绑定、日志写入、错误处理、状态机跳转……结果一升级图莫斯版本整个Main.vi就得重写。这违背了“高内聚、低耦合”的工程原则。真正的工业级设计是让Main.vi只做三件事状态流转、条件判断、数据路由所有硬件交互、协议解析、文件操作全部下沉到独立的Driver Layer。网络热词里“labview做上位机控制界面”“labview串口通信”之所以难落地就是因为没做好这层解耦。一个健壮的Main.vi工程结构应包含以下核心VI均位于独立的Library中Main.vi顶层编排纯状态机无任何硬件调用。它只接收来自UI的“Start Flashing”事件输出“Flashing Progress”和“Error Code”到UI。所有子VI调用都通过“Call By Reference”方式确保Main.vi不持有任何硬件句柄。Driver Layer.vi驱动层封装图莫斯所有API调用。例如CAN_Open.vi输入端口名、波特率输出CAN Handle、UDS_Request.vi输入Service ID、Subfunction、Data输出Response Cluster、LDF_Parse.vi输入LDF路径输出Memory Map簇。这些VI内部处理DLL调用、错误码转换、超时重试对外只暴露简洁接口。Protocol Layer.vi协议层实现UDS标准逻辑。例如CalculateCRC32.vi输入数据流输出CRC值、BuildRequestFrame.vi输入Service ID和参数输出符合ISO 15765-2的CAN帧簇、ParseResponse.vi输入CAN帧输出NRC码和Payload。这些VI与图莫斯无关可复用于其他CAN诊断项目。Utility Layer.vi工具层提供通用功能。例如LogWriter.vi输入日志级别、消息输出文件路径、INI_Read.vi读取配置文件、BinFileReader.vi解析Intel Hex或S19格式固件。这种分层带来的好处是当图莫斯升级到新版本如从v2.1到v3.0只需修改Driver Layer.vi中的DLL调用签名Main.vi和Protocol Layer.vi完全不用动。同样若客户要求支持CAN FD只需重写Driver Layer.vi中的CAN_Open.vi和UDS_Request.viMain.vi依然原封不动。我在某车企项目中曾用同一套Main.vi框架无缝切换了三家不同供应商的CAN硬件Vector VN1630、Kvaser Leaf Light、Peak PCAN-USB只因Driver Layer.vi做了标准化抽象。注意LabVIEW的“Project Explorer”里必须为每个Layer创建独立的Library.lvlib并设置“Strictly Typed VI Reference”保护接口。这样能防止其他VI绕过Driver Layer直接调用图莫斯DLL保证架构完整性。7. UI交互设计从“按钮点击”到“状态感知”的用户体验升级Main.vi的UI不是装饰品而是刷写过程的“神经末梢”。网络热词里“labview培训”“labview安装错误”暴露出大量开发者忽视UI与底层逻辑的深度绑定。一个合格的Main.vi UI必须做到用户点击“开始刷写”按钮时UI立即禁用所有控件刷写进行中进度条实时反映Block传输百分比收到NRC 0x78时状态栏显示“ECU处理中请勿断电”校验失败时高亮显示具体失败的Block编号和CRC差异值。这些细节决定了用户是信任你的工具还是骂着关掉程序。具体实现上Main.vi的UI应包含四个核心区域设备连接区显示CAN端口状态绿色/红色指示灯、当前会话模式Default/Programming/Extended、ECU VIN从$22 F190服务读取。关键点在于VIN读取必须在InitAndHandshake状态完成后自动触发而非让用户手动点击“读VIN”按钮。流程控制区主按钮“开始刷写”旁应有“暂停”“继续”“中止”三个状态按钮。Pause功能不是简单停止While循环而是将当前状态如DataTransfer-Block5保存到Shift RegisterResume时从中断点继续。Abort则触发“安全退出”流程先发$11 01复位ECU再关闭CAN端口。进度可视化区用环形进度条显示整体进度0%-100%下方用水平进度条显示当前Block传输进度0%-100%。每个Block传输完成时在列表框中追加一行“Block 5/24: OK (256 bytes, CRC match)”。若失败则显示“Block 12/24: FAIL (NRC 0x31, retrying...)”。日志与诊断区实时滚动显示底层通信日志如“[TX] 0x7E0: 34 00 00 00 00 00 00 00”、“[RX] 0x7E8: 74 00 00 00 00 00 00 00”并用颜色区分绿色成功红色NRC蓝色Tester Present。右侧提供“导出日志”按钮生成带时间戳的TXT文件。一个被低估的细节UI刷新频率必须与CAN通信速率匹配。如果Main.vi的While循环以10ms周期运行而CAN帧处理需要5ms那么UI刷新就会滞后。正确做法是在Main.vi的主循环中用“Timed Loop”结构设置循环时间为1ms高于CAN最高波特率对应的帧间隔并将UI更新如进度条赋值放在独立的“UI Update SubVI”中通过“Queue”与主逻辑解耦。这样即使CAN处理卡顿UI依然流畅。最后分享一个小技巧在UI右下角加一个“Debug Mode”开关。开启时Main.vi会在每个状态跳转前弹出对话框显示“即将进入State: MemoryEraseECU Address0x00000000Length0x00001000”方便现场调试。这个开关默认关闭但能救命——去年在某工厂产线就是靠它快速定位到LDF中地址偏移量配置错误的问题。8. 实战避坑指南那些让Main.vi崩溃的“隐形杀手”即使你完全理解了上述所有原理Main.vi在真实环境中仍可能突然崩溃。这些“隐形杀手”往往不在UDS标准文档里而是源于LabVIEW运行时、Windows系统、CAN硬件的交互细节。结合我踩过的坑列出五个最致命的实战陷阱8.1 “CAN端口被占用”背后的进程残留现象Main.vi首次运行正常重启后报错“can not open com port”。根因LabVIEW开发环境Development Environment在调试时会加载CAN驱动即使关闭VI驱动句柄有时未完全释放。Windows任务管理器里看不到相关进程但netstat -ano可查到占用端口的PID。解决方案在Main.vi的CAN_Close.vi末尾强制调用图莫斯的CAN_Reset.vi并添加100ms延时每次Open前先执行“Port Availability Check”子VI尝试用CreateFileAPI打开端口失败则提示用户重启LabVIEW。8.2 “UDS 19服务”读取DTC时的缓冲区溢出现象执行$19服务读取DTC时Main.vi崩溃或返回乱码。根因$19服务响应长度可变取决于ECU存储的DTC数量图莫斯的默认缓冲区如1024字节可能不足。当ECU返回2000字节响应时LabVIEW数组越界。解决方案在调用$19服务前先发$19 02reportNumberOfDTCByStatusMask获取DTC总数再根据公式BufferSize 2 TotalDTC * 4每个DTC占4字节动态分配缓冲区传给图莫斯的UDS_Request.vi。8.3 “LabVIEW安装错误”引发的DLL版本冲突现象Main.vi在某台电脑上运行报错“无法加载图莫斯DLL”。根因图莫斯DLL依赖特定版本的Microsoft Visual C Redistributable而LabVIEW安装包自带的VC版本与之不兼容。解决方案在Main.vi的“InitAndHandshake”状态开头加入“Dependency Checker”子VI用GetFileVersionInfoAPI读取图莫斯DLL的依赖清单并比对系统中已安装的VC版本。若不匹配弹窗提示用户安装对应版本如vc_redist.x64.exe 2015。8.4 “CAN总线仲裁”导致的帧丢失误判现象Main.vi偶尔在$34服务后收不到响应判定为超时。根因当多个节点如ECU、其他诊断仪同时向总线发帧发生仲裁失败时图莫斯底层驱动可能丢弃本应接收的响应帧。解决方案在Main.vi的CAN接收逻辑中增加“Frame Loss Detection”机制每发一帧记录其Sequence ID每收一帧检查ID是否在预期范围内。若连续3帧ID缺失触发“Bus Load Check”子VI用图莫斯的CAN_GetBusLoad.vi读取总线负载率若70%则降低发送频率或提示用户断开其他节点。8.5 “LabVIEW Web服务”与CAN通信的线程竞争现象启用LabVIEW Web服务后Main.vi刷写成功率骤降。根因Web服务默认使用LabVIEW主线程与CAN通信VI争抢CPU资源导致超时。解决方案将Main.vi的主循环放入独立的“Real-Time Priority”线程通过Set Thread PriorityAPI并禁用Web服务的“Allow Remote Front Panels”选项改用REST API提供只读状态查询。这些坑每一个都曾让我加班到凌晨三点。但正是这些血泪教训让我明白Main.vi的稳定性不取决于你写了多少行UDS代码而取决于你对LabVIEW运行时、Windows系统、CAN物理层的理解深度。当你能把“can总线仲裁”和“LabVIEW线程优先级”联系起来时你才算真正驾驭了这套系统。我在实际使用中发现最有效的预防措施是在Main.vi的“InitAndHandshake”状态末尾加一个“System Health Check”子VI它自动执行端口检测、DLL依赖验证、总线负载测试、内存占用评估并生成一份健康报告。这个报告不显示给用户但会写入日志——当问题发生时它能帮你瞬间锁定是软件问题还是硬件问题。这个小技巧让我们的现场支持响应时间缩短了70%。