Qt C++打造包装打码机控制系统:架构、关键技术与避坑指南 在包装设备这一行待久了你会发现打码机控制系统是个很值得琢磨的交叉领域。它既要接PLC、编码器、光电传感器、伺服变频器这些现场信号又要跟喷码机、激光机通讯还要把生产日期、批号、二维码、防窜货码实时准确地打到包装上。硬件和控制对象五花八门但上位机和HMI这块用Qt C来做确实是一个用常见技术解决复杂工业问题的典型例子。这篇文章就围绕一个基于Qt C的包装打码机控制系统聊聊架构、关键技术、实现细节以及我踩过的坑。如果你正在做类似的设备软件、上位机开发或者准备从零开始搭一套打码机控制程序这篇内容适合你参考。1. 系统整体设计与思路拆解1.1 打码机的工艺逻辑与控制需求先搞清楚打码机在产线上到底做什么。包装盒经过输送线光电传感器检测到产品到位编码器实时反馈输送带速度打码机在包装表面喷印生产日期、批号、二维码、logo等可变信息。听起来像打印机往移动的纸上打印但难点在于产品不是静止的输送带速度会波动包装位置可能有误差打码内容还经常需要切换。整个控制系统要完成的活包括几块接收上位机或产线PLC的信号把打码内容下发到喷头控制器根据编码器脉冲计算打码触发位置在正确的时机发出喷印指令还要检测打码结果统计产量记录报警。现场操作员需要的是一个简单直观的界面能快速切换产品规格、调整打码位置、查看报警不用像调试验证时那样翻一堆参数。所以软件不只是“界面 一个串口发送函数”那么简单而是一个带状态机、任务队列、参数管理、数据库记录、异常处理的小型实时控制软件。时间精度需求不极端但要稳定可靠不能卡不能丢任务更不能在连续生产时把打码内容搞错。1.2 为什么选择Qt C而不是其它方案我见过不少设备厂用组态软件做HMI比如传统的WinCC、组态王这一类的。优点是上手快、画面控件现成但遇到复杂的逻辑和联动控制时就很别扭脚本能力弱不好维护授权费用不低而且跨平台很麻烦。后面很多厂家转Visual C#做上位机界面开发确实方便但要在工业现场长期稳定跑垃圾回收和实时性总让人心里没底。再说如果硬件平台不是Windows比如不带屏幕的嵌入式Linux板子C#的方案就得重写。Qt C的优势主要是这么几点跨平台同一套界面代码可以跑Windows、Linux、国产化系统甚至嵌入式环境。C性能和资源可控适合做串口、网络、图像采集这类偏底层的逻辑。Qt的signal/slot机制非常适合界面不卡、后台干活这种场景界面线程和设备线程天然能解耦。自带的串口、网络、SQL、JSON、XML等模块覆盖了打码机控制系统的大多数需求不用引入太多第三方依赖。当然用Qt C的代价是开发门槛比脚本语言高编译慢有些坑只能自己踩。但做过一出稳定交付的设备软件之后你会发现这个成本完全可以接受。1.3 系统整体分层架构我在这个系统里把程序按四层拆从上到下分别是界面层、业务逻辑层、设备驱动层、硬件抽象层。这样做最大的好处是现场换了一款打码机或者把PLC从国产换成另一个品牌只需要替换驱动层界面和业务逻辑基本不用动。层次核心职责关键Qt技术界面层操作员交互、状态展示、参数配置QWidget / QML、QSS样式、状态指示业务逻辑层任务队列、状态机、配方管理、数据解析QStateMachine或手工状态机、信号槽设备驱动层串口/网口收发、Modbus报文、协议解析QSerialPort、QTcpSocket、QModbusClient硬件抽象层统一设备接口、屏蔽设备差异C抽象类、插件式驱动加载Qt的signal/slot可以把层与层之间的调用变成“事件通知”。比如驱动层收到一个完整的打码完成帧发出printFinished信号业务层收到后更新任务队列界面层通过另一个信号刷新产量计数。整个过程不需要业务层直接操作控件界面自然就不容易卡。这样的分层模式也方便做单元测试。设备没到现场时我会写一个虚拟打码机驱动随机返回成功或失败先把业务逻辑的边界条件测一遍。真正到了现场把虚拟驱动换成真实驱动接口不变省掉很多联调的痛苦。2. 核心模块解析与关键技术选型2.1 人机交互界面从需求到控件布局操作员实际在车间里戴着纱手套点屏幕环境可能还有粉尘、震动所以界面设计不能照搬办公软件的审美。大按钮、大字体、少层级、状态一眼可见这几个原则比好看重要得多。主界面我通常分成三块顶部是当前产品信息、运行状态、日期时间左侧是功能菜单包括生产监控、参数设置、配方管理、报警记录中间是核心操作区显示产品型号、批次号、打码内容预览和启停按钮。启停按钮做“确认二次弹窗”是必要的否则操作员在忙碌时很容易误触。Qt中QWidget和QML都可以做这类界面。我目前更偏向QWidget QSS原因是工业屏的显卡和内存不强QWidget相对轻量控件种类够用团队里如果需要调试逻辑C代码和UI可以放在同一个工程里定位问题更直接。QML做动画和Flex布局更漂亮但需要额外维护JS状态对产线控制这种强交互场景没有明显优势。界面里的数字输入建议用QSpinBox配套单位提示不要用QLineEdit让操作员随手输入。密码权限、配方选择、日期格式等都要做成控件而不是自由文本这样可以减少大量人为输入错误。字体建议中等字号常见的是15到18px太小了在触摸屏上点不准。2.2 实时控制链路通讯选型与多线程模型打码机控制系统的通讯链路一般有两根一根连接PLC用来获取产线运行状态、启停信号和生产模式另一根连接打码机用来下发打码内容和触发指令。PLC通讯常用Modbus RTU走RS485或者Modbus TCP走网口。打码机则比较杂有些是用串口私有协议有些是网口TCP协议甚至有些直接提供SDK。串口通讯里RS232多用于近距离RS485用于远距离多机。波特率从9600到115200都有打码机一般不需要太高速率9600或19200很常见。重点是帧格式、超时和校验。Modbus RTU有CRC16校验私有协议里也有自己的校验不能偷懒。我见过很多现场问题就是校验没做好偶发乱码当成正常数据结果打码内容里多了几个看不见的字符。多线程模型上我没有一上来就开一堆线程。Qt的QSerialPort和QTcpSocket都是异步的靠readyRead信号触发读取本身不阻塞UI。所以简单场景下串口读写在主线程的事件循环里就够。真正需要独立线程的是重型数据处理比如图像检测、数据库批量查询、复杂的文件导入导出。用QThread还是QtConcurrent取决于任务是常驻的还是单次执行。常驻的采集循环用继承QThread或moveToThread都行我习惯用moveToThread配合worker对象因为可以把信号槽全挂在worker上线程退出也更好控制。有一点必须强调信号槽跨线程连接时连接方式默认是队列连接槽函数在接收线程的事件循环里执行不会阻塞发送线程。但要注意别在槽函数里做耗时操作否则接收线程自己的事件循环会被卡住后续信号处理就会延迟。2.3 参数管理与配方体系打码机控制系统里的配方概念很简单每种包装规格对应一套打码设置包括产品名称、批号规则、日期格式、打码位置偏移、喷印速度、数据库字段映射、打码模式等等。换产品时操作员不需要逐项改参数选一个配方就全部切换了。配方存储我建议用两步基础设备参数放配置文件例如使用QSettings或INI文件业务配方放SQLite数据库。为什么不全放SQLite因为设备参数在程序启动时就要读放配置文件更直观坏了也容易手动改。业务配方数量多而且需要增删改查放数据库更合适比如需要按客户名筛选配方时SQL一条语句就搞定。配方数据大概是这样的JSON结构{ name: 250mL纯奶箱, productCode: MILK-250, batchRule: YYYYMMDD, dateFormat: yyyy-MM-dd, offsetXmm: 12.5, offsetYmm: 8.0, contentTemplate: {date} {batch} {code}, triggerMode: encoder, preRollPulses: 128 }保存参数时最容易踩的坑是直接往原文件写写到一半断电文件就坏了。正确做法是“原子写”先写临时文件比如settings.tmp成功后用QFile::remove加QFile::rename替换正式文件。QSettings也支持同步但频繁同步会伤闪存寿命不如自己控制写盘时机。生产记录和报警记录另说数据库写入要批量提交。参数从界面改到实际生效要区分“即时生效”和“下次启动生效”。打码偏移量这类参数改了应该立即生效否则操作员要重启设备才能看到效果体验很糟。而串口号、波特率这类通讯参数改的时候就要弹窗提示修改后需要重启系统。2.4 日志、报警与数据追溯产线设备最怕事后查不清问题。这个系统除了打码任务还要把运行日志、报警事件、生产报表都存下来。日志我建议分文件系统运行日志记录启动、停止、参数变更和普通调试信息报警日志单独建表生产明细单独存。用qInstallMessageHandler可以统一管理Qt的日志输出把级别、时间、文件行号写进文件。文件按天分片只保留最近30天避免磁盘被撑爆。报警分级要设计好。有的报警只需要提醒比如料位低、打码内容即将过期有的报警需要停机并等待人工处理比如打码头通讯断开、编码器无反馈。不能把一切异常都当成停机故障否则一个误报就停整条产线谁用谁难受。报警产生之后还要做“报警恢复”记录。比如通讯断开了五秒后又自动恢复如果只有报警记录没有恢复记录查询的时候会以为设备一直处于故障状态。我的做法是报警表里加一个clear_time字段恢复时更新这条记录而不是另写一条。数据追溯这一块每一条打码记录至少包含时间、产品批号、目标内容、操作员、班次、设备状态。这样的话如果市场端发现某个批次二维码扫不出来能快速回溯到是哪个时间段、哪台设备、用哪个参数打的对质量问题处理非常有帮助。之后对接MES系统时这批数据也能上游直接读取。3. 主要功能实现与核心代码思路3.1 设备通讯层用抽象接口隔离具体硬件驱动层我是从抽象类开始的。所有设备驱动都实现同一个接口上层只关心open()、close()、sendCommand()、readStatus()这些方法以及dataReady、errorOccurred这些信号。具体到某个打码机型号再在子类里实现私有协议。class IDeviceDriver : public QObject { Q_OBJECT public: virtual bool open(const QVariantMap params) 0; virtual void close() 0; virtual bool sendCommand(const QByteArray data) 0; virtual QVariantMap readStatus() 0; signals: void dataReady(const QVariantMap data); void errorOccurred(const QString error); };比如Modbus RTU驱动在底层用QModbusClient异步读写。QModbus的异步接口很典型sendReadRequest后等readFinished信号不能像同步阻塞库那样“发一个请求然后读返回”。所以Modbus栈里的请求超时和重试逻辑都要在信号槽里管理。一个寄存器读取周期大概是50到200毫秒如果你连续读十个寄存器列表可以把多个地址合并成一条读请求效率会高很多。串口打码机驱动则要自己处理残帧和粘包。很多打码机协议是“帧头命令长度数据校验”的结构接收时不能一收到数据就认为是一整帧应该把数据追加到缓冲区然后循环解析。这里给一个解帧思路void PrinterDriver::onReadyRead() { m_buffer.append(m_port-readAll()); while (true) { int frameLen tryParseFrame(m_buffer); if (frameLen 0) break; QByteArray frame m_buffer.left(frameLen); m_buffer.remove(0, frameLen); handleFrame(frame); } }tryParseFrame只负责找出当前缓冲区能否构成一个合法完整帧缺数据就返回-1长度不足就继续等。这里最容易犯的错是假设“每次收到一包就是一帧”。真实串口环境里一次收到半个帧或一口气收到三四个帧都很常见不做缓冲解析迟早要出问题。3.2 喷印任务队列与状态机设计打码机不像普通打印机那样点了打印就不知道后续。它要响应产线上的触发信号还要按顺序完成多个任务。比如同一个批次里产品A打日期产品B打失效期和二维码可能交替出现。这时任务不能只存一条“当前内容”而是要有一个待执行队列。队列数据结构我简化一下struct PrintTask { QString productCode; QByteArray content; int preRollPulses; int offsetAdjust; int priority; };任务入队后状态机按顺序出队。状态至少要有这几个空闲、等待触发、准备打码、打码执行、完成反馈、故障暂停。不能只用布尔变量“工作在跑/不在跑”因为打码过程有时间窗触发信号来了但队列为空这种情况下可能不需要打码队列有任务但触发信号一直不来就要超时报警说明传感器可能坏了或者产品没到位。一个简化的状态迁移原则是空闲状态下收到“设备已准备好”信号且队列非空进入等待触发。等待触发状态下收到编码器位置到达窗口进入打码执行把内容发送给打码头。打码执行状态下收到“打码完成”反馈进入完成反馈同时弹出下一个任务。任何状态下通讯异常或急停信号都进入故障暂停人工复位后回到空闲。状态机用Qt的原生QStateMachine也能做但很多嵌入式编译环境不想引入额外依赖我通常用enum State switch把每个状态的进入动作、条件判断和事件响应写成单独的处理函数。重点不是谁更高级而是状态转移的每个分支都要明确不然两个if套在一起状态就乱了。还有一点触发信号高电平持续时间和打码时间窗口可能非常短比如输送线速度1.5米/秒偏差点只有几毫米对应的脉冲窗口可能就是几十个脉冲。这时候软件如果先去刷新界面再处理触发绝对来不及。正确做法是让驱动层的脉冲计数、位置比较放在实时线程里处理只有到了窗口才通过信号通知业务层下发内容。UI刷新和数据库写入都不能卡在这一环。3.3 界面刷新与设备指令的线程同步界面卡顿是这类系统升级过程中最常见的投诉。“点一个按钮要等两秒才有反应”十有八九是对应槽函数里做了耗时操作。比如一位新人会在按钮点击里直接调用串口同步读取// 反例 void MainWindow::onBtnQueryStatus() { m_serial-write(queryCmd); m_serial-waitForReadyRead(2000); // 阻塞两秒界面就卡两秒 QByteArray resp m_serial-readAll(); ... }waitForReadyRead在事件循环里会阻塞所有消息处理界面自然卡死。不是不能同步读而是不能放在UI线程。能用异步API就用异步API比如QModbusClient本身异步如果设备库只能同步那需要把同步调用封装到独立线程里。数据共享方面最简单是只通过信号槽传值不直接跨线程操作对象。例如设备线程拿到状态结构体后emit stateChanged(state)UI线程在槽函数里得到一份拷贝只更新控件。如果要共享缓冲区比如编码器累计值那就要用QMutex或QReadWriteLock保护。我更喜欢尽量减少共享谁的数据谁线程处理跨线程只发不可变对象或值类型。界面刷新频率也要控制。我常看到有人用10毫秒的QTimer去刷状态灯结果CPU占用跑满一个核。其实工业状态灯、产量数字的变化并没这么快100毫秒刷新完全够用。高速脉冲计数和位置计算不要走UI定时器应该在底层用计数器中断或硬件模块完成界面只是周期性显示。3.4 数据库记录与查询优化QSqlDatabase配合QSQLITE是Qt项目里最常用的本地数据库。但要注意SQLite对并发写的限制比较严格。设备线程每分钟甚至每秒都要写打码记录UI线程在同时查报警记录容易导致“database is locked”。我的处理方式是所有数据库写入都进同一线程或同一连接写入采用事务批量提交。比如每收集到50条打码记录才提交一次事务既减少IO次数也降低死锁概率。查询单独使用一个只读连接避免和写连接抢锁。插入示例QSqlDatabase db QSqlDatabase::database(produceDb); db.transaction(); QSqlQuery query(db); query.prepare(INSERT INTO print_log(...) VALUES (?, ?, ?)); for (const auto item : batch) { query.addBindValue(item.time); query.addBindValue(item.productCode); query.addBindValue(item.content); query.exec(); } db.commit();查询大数据量时不要一次性加载到界面。用QTableView加QSqlQueryModel或自定义Model按需获取数据会更好。但QSqlQueryModel直接在UI线程里执行复杂查询也会卡可以把查询放到QtConcurrent::run拿到结果后通过信号槽回填Model。记录表要定期清理比如保留最近3个月清理后执行VACUUM回收文件大小。4. 现场调试与常见问题排查4.1 电磁干扰导致通讯失败打码机控制系统的现场往往不仅有打码机还有变频器、伺服驱动器、大功率电机。这些设备会通过辐射和地线干扰通讯尤其是RS485总线。现象是设备运行一段时间后串口通讯突然丢包、CRC校验错误严重的时候上位机和PLC彻底连不上。排查思路不要一上来就换代码。先用串口工具抓数据看是不是收到了大量乱码。如果是十有八九是物理层问题。处理办法通讯线用屏蔽双绞线屏蔽层单端接地不要两端都接避免地环流。RS485总线两端加终端电阻阻值按照标准选120Ω。走线尽量远离动力线不要跟变频器输出线捆在同一个线槽里。如果距离远采用光电隔离的RS485转换器让设备之间没有电气直接连接。波特率适当降低比如9600和19200比115200抗干扰能力强不少。软件上也要有重试机制。比如Modbus请求超时后重发三次连续三次失败才报通讯故障。自动重连也很重要通讯短暂中断后驱动层要能自动恢复不能一断就让整条产线停机等人点按钮。重连要有退避策略最开始的间隔短一些比如1秒、2秒、5秒不要每100毫秒死循环去握手。4.2 打码位置偏移与编码器脉冲处理产品在输送线上跑打码位置偏了这是最影响交付质量的问题之一。位置偏移的原因主要有几个编码器脉冲数标定不准、光电传感器触发位置有延迟、输送带存在打滑、以及上位机取编码器值方法不对。我做过一次现场排查产品打码位置总是越跑越偏。一开始怀疑是打码机本身精度问题后来发现是编码器脉冲计数在软件里没有清零旧产品的累计脉冲把新产品的触发位置冲掉了。这个问题的教训是每次产品经过光电传感器后脉冲计数器必须清零或重新装载预置值。脉冲数和毫米的换算关系要先标定。方法很简单让输送带走一段已知距离采集编码器脉冲数算出每毫米脉冲数。比如走500毫米编码器反馈12000个脉冲那每毫米就是24个脉冲。有经验的厂会在设备调试时把这个参数写到系统配置里但很多非标设备根本没标定软件默认给了个经验值位置自然偏。触发提前量可以用下面这个公式近似int preTriggerPulses qRound(distanceOffsetMm * pulsePerMm) qRound(encoderPulsePerSecond * triggerDelaySec);distanceOffsetMm是传感器到打码头安装位置的距离triggerDelaySec是打码机从收到指令到真正喷印的固有延迟。这个延迟值不同品牌差别很大需要从打码机文档里拿或者现场实测。最好在画面上提供一个“偏移微调”参数让机械调试人员在实际运行时校准而不是重新改代码编译。4.3 界面卡顿与实时性矛盾前面说了一部分界面卡顿的原因这里再补充几个实际排查点动态创建控件导致的卡顿。比如操作员切换产品时如果程序根据当前产品动态创建一排标签和输入框控件数量多创建销毁都会慢。解决办法是提前用固定数量的控件或Model/View动态更新文本。日志表格无限增长。QTableWidget每增加一行行数越来越多后整体刷新越来越慢。解决办法是限制最大行数比如5000行超出后删除最前面的数据。实时曲线刷新方式不当。如果直接把每个采样点都添加到QCustomPlot等绘图控件数据量大了肯定卡。正确做法是维护一个环形缓冲区曲线控件只负责绘制缓冲区中的最新区间或者降低采样显示频率到100到200毫秒一个点。还有一个容易忽略的问题定时器里做了网络扫描或DNS解析。Qt的QTcpSocket连接一个不存在的IP如果没有设置超时可能阻塞几十秒。所有网络连接都要设setSocketOption(KeepAliveOption)并在请求前用state()判断连接状态失败后异步重连。4.4 参数保存丢失与配置损坏这套系统在现场跑了一段时间后可能出现“参数明明改了重启后变回原来的值”“界面一直报打不开配置文件”。大多数情况是写盘方式不严谨导致配置文件损坏或者写入时使用了不正确的路径。QSettings需要注意路径和格式。如果配置文件的目录存在权限问题写不进去QSettings不会报错但数据没落盘。启动时要检查配置文件是否可写不行就要提示操作员。另外QSettings建议不要频繁sync在一批参数修改完的时候统一写。配置损坏后的恢复策略也很重要。我会在每次成功写入后生成一个备份文件比如config.ini.bak。启动时如果主配置文件解析失败自动加载备份文件同时在状态栏提示“配置恢复自备份”。这个功能看似不起眼但在车间断电频繁的环境里能省不少麻烦。提到断电数据记录也一样。SQLite事务如果没有提交电源突然断开可能会导致数据库文件损坏。应对办法包括定期备份数据库文件、配置WAL模式、尽量用事务批量提交。如果发现损坏Sqlite提供了PRAGMA integrity_check能帮忙判断然后从备份恢复。工业现场不能指望每台机器都有人会修数据库所以软件层面要尽量减少单个文件长期反复写入。5. 工程化落地与后续扩展5.1 版本管理、编译部署与安装包设备软件和纯办公软件不同部署到现场之后很难随时远程调试所以在版本管理上要格外谨慎。我习惯用Git管理代码分支按“开发版、测试版、正式版”分开每次重大改动打tag。版本号建议采用主版本.次版本.修订号比如2.3.1同时把编译时间和Git短哈希写进“关于”页面里这样现场反馈问题时能准确知道对方跑的是哪个版本。Qt程序的发布Windows上要带上Qt动态库、插件目录platforms、sqldrivers等和必要资源文件。用自带的windeployqt可以自动拷贝大部分依赖但一些自定义动态库和第三方库还需要手动处理。打包安装包时不光要装程序文件还要在安装过程中把运行目录、数据库目录、日志目录建好并且设置合适的权限。不要默认所有人都用管理员运行软件尽量让程序在当前用户目录下保存配置和日志避免UAC的干扰和文件夹权限问题。这里有一个经验程序启动时显示当前版本号和设备编号设备编号由安装人员手动输入存到配置里。这个编号后面做数据追溯对应产线位置和PLC站号非常重要。很多设备厂忽略了这个字段结果出问题时只能翻照片确认是哪台设备效率很低。5.2 对接ERP/MES系统的接口设计包装打码控制系统的价值到了一定阶段就不只是“把码打出来”而是跟工厂的ERP/MES系统联动。订单下来后打码内容不应该由操作员手工录入而是从系统自动下发。举个例子一个产品批次号对应的二维码内容、有效期、生产指令通过接口传到打码机控制系统系统自动生成配方和喷印任务完成后把产量记录回传给MES。接口方案上工业环境里HTTP REST JSON是最通用的MQTT也有不少厂在用。推荐用Qt的QNetworkAccessManager做HTTP客户端使用异步方式不能在UI线程里同步等待响应。服务端地址、账号令牌、证书这些配置都放在单独配置文件中并要考虑离线缓冲网络断开时本地下发的任务先缓存到本地队列网络恢复后再补传。协议设计要简单清晰。比如请求批次任务的消息{ type: new_print_job, jobId: JOB20250113001, productCode: MILK-250, batchNo: 20250113A1, template: {date}|{batch}|{qrcode}, expiryDate: 20260112, quantity: 1200 }打码内容模板不要写死在程序里尽量做成可配置表达式。因为今天可能是“日期批号”明天可能就要求“批号二维码防伪码”如果模板是代码写死的每改一次都得发新版本。模板表达式放在配方里由操作员或管理员维护系统启动时解析一次运行时按表达式拼内容。二维码和防伪码的生成算法尽量抽象成插件或动态库不跟主程序耦合这样不同客户可以用不同的加密规则。5.3 我的实操心得与建议这套系统做下来我最深的体会是打码机控制系统80%的问题不在代码而在通讯协议、时序和现场干扰。代码写得好不好往往要到设备联调和连续试生产阶段才能看出来。所以开发过程中一定要给自己留出充足的自测时间尽量在实验室里搭建模拟环境用虚拟PLC和虚拟打码机把流程跑通再进场调试。另一个建议是状态机和任务队列一定要在一开始就定义清楚不要写到哪算哪。我在早期一个项目里图省事用一堆if (step 1)、if (step 2)去控制流程后来加了暂停、复位、换料、异常中断几个状态之后代码直接变成一团乱麻。重构的时候改成了结构化的状态机所有状态和事件都列成表格进展一下就顺了很多。还有一个小技巧现场调试打码位置偏移时尽量在系统里留一个“测试打码”按钮操作员或机械调试人员不用进参数页面就能手动触发一次打码配合白纸或包装盒做试喷。这个功能看起来简单但比让操作员去记“按F几”要可靠得多。最后界面上的报警信息不要只写代码比如“ERR-102”要多写一句话“通讯超时请检查打码机电源和通讯线”。很多操作员不懂代码含义报警信息不直观就会被当成系统有问题其实是设备没开机。好的报警提示才是整个系统稳定运行的开始。