1. 为什么要在Codesys里“徒手”连接MySQL如果你在工业自动化领域摸爬滚打过一阵子肯定遇到过这样的场景产线上的PLC需要把生产数据——比如设备运行状态、产量、良品率——实时地记录到远程的数据库里方便MES制造执行系统或者上层的信息平台做分析和报表。通常大家会想到几种办法用OPC UA、用专门的数据库中间件、或者购买现成的Codesys数据库功能库。这些方法当然能用但我自己踩过不少坑。OPC UA配置复杂中间件又是一层额外的成本和故障点。至于付费的MySQL库功能是封装好了但用起来总觉得“隔靴搔痒”。比如它可能对SQL语句的长度有限制或者返回的数据格式固定你想灵活地解析查询结果、处理超长文本就会束手束脚。更重要的是当你需要深度定制通信逻辑比如根据数据库的响应动态调整心跳包频率或者实现一套自己的重连机制时现成的库往往就帮不上忙了。所以我决定自己动手从最底层的网络协议开始在Codesys里用Socket直接“对话”MySQL。这听起来有点硬核但好处是巨大的你获得了完全的控制权。通信过程的每一个字节都由你掌控你可以优化数据包大小以适应窄带网络可以自定义加密方式可以精细地处理每一种异常情况。而且它不依赖任何第三方付费组件纯靠Codesys的标准库实现部署起来干干净净。这个过程本质上就是让PLC扮演一个MySQL网络协议客户端。你需要理解MySQL客户端和服务器之间“握手”、“聊天”的原始规则然后用Codesys的Socket功能按照这个规则去组装和解析数据包。接下来我就带你一步步拆解这个过程从环境准备到代码实现手把手教你构建这个轻量级却强大的数据库直连模块。2. 动手前的准备理解MySQL协议与Codesys工具在开始写代码之前我们得先搞清楚两件事MySQL协议大概是怎么一回事以及Codesys给我们提供了哪些“武器”。2.1 MySQL网络协议初探你可以把MySQL客户端和服务器的通信想象成两个人用一套固定的暗号写信。每次通信都由一个或多个“数据包”组成。每个包都有一个简单的结构包长度前3个字节表示这个包“内容”部分有多长。序列号第4个字节用来标识这是对话中的第几句话防止话序错乱。包内容从第5个字节开始才是真正的数据比如登录信息、SQL命令、查询结果。整个连接过程主要分为几个阶段握手阶段服务器先发来一个“问候包”里面包含协议版本、一个20字节的随机数用于加密挑战等信息。认证阶段客户端用这个随机数和用户密码经过SHA1加密运算生成一个“响应”连同用户名、数据库名等信息打包回给服务器。命令阶段认证通过后客户端就可以发送各种命令包了比如查询COM_QUERY。结果集阶段服务器返回查询结果这个部分稍微复杂它包含列信息、行数据以及结束标记。我们实现的核心就是按照这个规则在Codesys里构造出每一个阶段的正确数据包并正确解析服务器返回的包。2.2 Codesys的必备“武器库”Codesys本身不直接提供MySQL驱动但它提供了强大的网络和内存操作基础库这正是我们需要的。主要依赖两个公共库CAA Net Base Services这个库提供了TCP/IP Socket通信的所有基础功能比如创建套接字、连接服务器、发送和接收数据。它是我们与MySQL服务器建立物理连接的桥梁。CAA Memory这个库至关重要。因为我们要直接操作内存字节来构建和解析协议数据包。里面的MemMove函数允许我们在不同的内存区域间安全、高效地复制数据是处理二进制协议包的利器。在开始编程前请确保你的Codesys开发环境已经安装了这两个库。通常它们会在标准库列表中如果没有需要从管理器中安装。3. 核心数据结构设计定义我们的“信封”在编程中好的数据结构是成功的一半。我们需要定义几个结构体STRUCT来对应MySQL协议中的关键数据包这样在组装和解析时就会非常清晰。3.1 服务器握手包HandShakePacket当客户端连接上MySQL服务器的3306端口后服务器会主动发来第一个包即握手初始包。我们需要一个结构体来存放解析出来的信息。TYPE HandShakePacket : STRUCT FrameDataLen : UINT; // 包内容长度3字节 DataBaseMsg : STRING(50); // 服务器版本信息例如“5.7.35-log” Sqid : BYTE; // 序列号第一个包通常是0 ProtoolId : BYTE; // 协议版本号 ServerThreadId : UDINT; // 服务器为这个连接分配的线程ID AuthBuffer : ARRAY[0..19] OF BYTE; // 关键的20字节随机挑战码 ServerCap : WORD; // 服务器能力标志告诉客户端它支持哪些功能 CharSetCode : BYTE; // 服务器默认字符集编码 ServerState : WORD; // 服务器状态 END_STRUCT END_TYPE这个结构体就像一张表格当收到服务器的握手包后我们把二进制数据按字段“对号入座”填进去。其中AuthBuffer字段里的20个随机字节是后续密码加密的关键输入。3.2 客户端认证包AuthPacket收到挑战后客户端需要回应一个认证包。这个包的内容比握手包丰富它告诉服务器“我是谁我想连哪个库这是我的加密密码”。TYPE AuthPacket : STRUCT FrameDataLen : UDINT : 0; // 包内容长度先初始化为0计算后填充 Sqid : BYTE : 1; // 序列号此时应为1 ClientCap1 : WORD : 16#A68D; // 客户端能力标志低位 ClientCap2 : WORD : 16#006F; // 客户端能力标志高位扩展 MaxSize : UDINT : 16#00FFFFFF; // 客户端支持的最大包大小 CharSetCode : BYTE : 16#21; // 客户端请求的字符集如utf8mb4_general_ci UserName : STRING(50); // 登录用户名 DataBaseName : STRING(50); // 要连接的数据库名 Password : ARRAY[0..19] OF BYTE; // 加密后的密码摘要20字节 clientAuthPlugin : STRING(50) : mysql_native_password; // 认证插件名 END_STRUCT END_TYPE这里有几个默认值需要注意比如ClientCap1和ClientCap2它们定义了客户端支持的特性如是否使用SSL、是否支持长密码等。mysql_native_password是MySQL旧版默认的认证插件名。密码字段Password是一个20字节的数组它不是明文密码而是经过SHA1双重哈希计算后的结果。3.3 连接配置与结果存储我们还需要一些辅助结构来管理连接信息和查询结果。TYPE MysqlConnectInfo : STRUCT ServerIp : NBS.IP_ADDR : (sAddr:192.168.0.105); // 服务器IP Port : UINT : 3306; // 端口 UserName : STRING(50) : SpinMachine; // 用户名 Password : STRING(50) : 123456; // 明文密码仅用于计算 DataBase : STRING(50) : spinmachine3; // 目标数据库 END_STRUCT END_TYPE TYPE MysqlRowInfo : STRUCT ColumnCount : BYTE; // 查询结果有多少列 RowData : ARRAY[0..999] OF BYTE; // 存储一行的原始字节数据 IsNull : BOOL; // 标记结果是否为空没有行数据 END_STRUCT END_TYPEMysqlConnectInfo集中存放配置方便修改。MysqlRowInfo则用于临时存放我们从服务器返回的二进制结果集中解析出来的第一行数据RowData数组足够大可以容纳大多数查询结果。4. 关键算法与功能块实现有了数据结构接下来就要实现核心的逻辑功能。这里最关键的三个部分是SHA1加密、认证包组装、以及查询结果解析。4.1 SHA1加密功能块SHA1_FBMySQL的mysql_native_password认证方式要求对密码进行特定的SHA1哈希计算。公式是SHA1( password ) XOR SHA1( “挑战随机数” SHA1( SHA1( password ) ) )。这个过程需要在Codesys里实现。我们创建一个SHA1_FB功能块。它接收一个最多50字节的输入数组输出20字节的SHA1摘要。由于ST语言没有内置的SHA1函数我们需要手动实现其算法。这涉及到对输入信息进行填充、分块、以及一系列的逻辑运算和循环移位。代码看起来有些复杂但其核心是遵循标准的SHA1算法步骤初始化哈希值、处理消息块、进行80轮的压缩函数运算。你可以在网上找到标准的SHA1算法描述然后将其“翻译”成ST语言的循环和位操作。实现后我们就能用它来计算密码摘要了。具体步骤是先计算SHA1(明文密码)得到哈希值A再计算SHA1(A)得到哈希值B接着计算SHA1(挑战随机数 B)得到哈希值C最后将A与C进行按位异或(XOR)操作得到的20字节结果就是我们要填入AuthPacket.Password字段的内容。4.2 查询命令打包功能块MysqlQueryCmdPack_FB当我们想执行一条SQL语句时需要构造一个命令包。这个功能块负责将一条SQL字符串打包成MySQL协议能识别的格式。FUNCTION_BLOCK MysqlQueryCmdPack_FB VAR_INPUT Cmd : BYTE; // 命令码查询是0x03 InputBytes : ARRAY[0..998] OF BYTE; // SQL语句的字节数组 END_VAR VAR_OUTPUT Result : ARRAY[0..999] OF BYTE; // 打包好的完整数据包 ResultSize : UINT; // 包的总大小 END_VAR它的工作流程很直接计算SQL语句字符串的实际长度不包括结尾的0。整个包的内容长度 1字节命令码 SQL语句长度。将内容长度3字节、序列号1字节、命令码1字节、SQL语句字节依次填入Result数组。输出包的总长度内容长度4字节包头。这样我们就得到了一个可以直接通过Socket发送出去的二进制数据包。4.3 首行数据解析功能块MysqlFirstRowData_FB服务器返回的查询结果是一个结构化的二进制流。解析整个结果集比较繁琐但很多时候我们只关心第一行数据比如用SELECT ... LIMIT 1查询用户登录。这个功能块就专门做这件事。它的输入是接收到的原始字节数组GVL.Mysql_abyRx。它需要解析“结果集头”包获取列的数量。跳过紧接着的多个“列定义”包。每个列定义包的长度是可变的需要动态计算偏移量。跳过一个“EOF”包表示列定义结束。接下来就是行数据包了。它解析第一个行数据包将其内容复制到输出的RowData中。如果发现行数据包后面紧跟着另一个EOF包说明结果集为空则设置IsNull标志为真。这个过程需要仔细计算字节偏移是协议解析中最精细的部分。代码中通过一个循环来累加每个列定义包的长度包长1字节序列号从而准确地定位到第一行数据的起始位置。5. 主程序流程串联起整个通信链路现在我们把所有零件组装起来看看主程序MysqlClient如何协调工作。我习惯用一个状态机Mysql_Init_Step来清晰地管理整个连接和查询流程。5.1 连接与认证阶段步骤0建立TCP连接。使用CAA Net Base Services库的NBS.SocketCreate和NBS.Connect函数连接到MysqlConnectInfo中配置的IP和端口默认3306。连接成功后服务器会立刻发送握手包。步骤1解析握手包。从Socket读取数据填充到HandShakePacket结构体。重点是取出那20字节的AuthBuffer挑战随机数。步骤2准备认证包。根据握手包中的信息如字符集、能力标志和配置的用户名、数据库名填充AuthPacket结构体的大部分字段。然后调用SHA1_FB功能块结合用户密码和挑战随机数计算出20字节的加密密码摘要填入AuthPacket.Password。步骤3发送认证包。将AuthPacket结构体的各个字段按顺序转换为字节流并在最前面加上计算好的包长度和序列号通过Socket发送给服务器。步骤4验证认证结果。读取服务器的响应。如果响应包的第一个字节是0x00后面跟着一个OK包恭喜你认证成功如果第一个字节是0xFF后面跟着错误信息则说明认证失败密码错误、用户不存在等。此时需要关闭Socket并根据错误码处理。5.2 执行查询与处理结果认证成功后连接就进入了命令阶段。此时你可以循环执行以下操作构造查询命令使用MysqlQueryCmdPack_FB将你的SQL语句如SELECT * FROM production_data打包。发送命令将打包好的数据通过Socket发送。接收并解析响应这是一个可能需要循环读取的过程因为结果可能很大被分成多个网络包。你需要不断读取直到收到一个完整的“结果集”。首先解析结果集头部获取列数。然后解析每个列的定义名称、类型等这部分信息如果你不需要可以跳过但必须正确计算偏移量。最后解析行数据。你可以使用MysqlFirstRowData_FB来快速获取第一行或者自己写循环来解析所有行。每行数据都以长度编码的格式存储每个字段的值需要根据列定义中的类型来正确解读。5.3 连接管理与错误处理工业环境网络可能不稳定因此健壮的错误处理至关重要。超时机制为每个Socket操作连接、发送、接收设置合理的超时时间。如果超时则视为失败进行重连或报错。心跳与保活长时间空闲的连接可能被服务器或防火墙断开。可以定期例如每分钟执行一个简单的命令如COM_PING命令码0x0E来保持连接活跃。优雅断开查询完成后如果需要断开连接应发送COM_QUIT命令包命令码0x01然后关闭Socket。直接关闭Socket不是好习惯。重连逻辑在检测到连接断开发送/接收失败后主程序应能自动清理状态并尝试重新发起从步骤0开始的完整连接流程。6. 实战案例实现一个用户登录验证让我们用一个具体的例子把上面的理论串起来。假设我们有一个用户登录界面操作员输入工号和密码PLC需要去数据库的login表里验证。我们在另一个程序MysqlUserLogin中实现这个业务逻辑。它监听一个按钮WB_UserLogin的上升沿。触发登录当按钮按下设置一个B_Login标志为真启动登录状态机。检查连接状态机首先检查全局变量GVL.B_Mysql_Inited在主程序中设置如果数据库连接尚未建立则触发主程序的连接流程GVL.B_Mysql_Enable : TRUE并等待连接成功。构造查询SQL连接成功后使用操作员输入的账号WHMI_Account和密码WHMI_Password构造SQL查询字符串。这里有个关键点字符串中的单引号需要用$27单引号的ASCII十六进制来转义这是ST语言字符串字面量的规则。QueryStr : SELECT AUTP FROM login WHERE STATE1 AND USER$27; QueryStr : CONCAT(QueryStr, WHMI_Account); QueryStr : CONCAT(QueryStr, $27 AND PASSWORD$27); QueryStr : CONCAT(QueryStr, WHMI_Password); QueryStr : CONCAT(QueryStr, $27 LIMIT 1);这条SQL的意思是在login表中查找状态为1、用户名和密码都匹配的记录并返回其权限字段AUTP只取第一条。发送查询调用MysqlQueryCmdPack_FB打包这个SQL并将打包好的数据赋给全局发送缓冲区GVL.Mysql_abyTx同时设置发送标志GVL.B_Mysql_Send。主程序中的发送逻辑检测到这个标志就会执行发送动作。解析结果主程序接收并解析数据完成后设置结果标志GVL.B_Mysql_Result。登录状态机检测到这个标志后调用MysqlFirstRowData_FB解析返回数据。如果FirstRowInfo.IsNull为真说明没查到记录登录失败。如果为假则从RowData中提取出AUTP字段的值注意它是长度编码的第一个字节可能是长度转换为整数存入全局变量GVL.Mysql_AccountAutp中并设置登录成功标志GVL.B_Mysql_Login。界面反馈根据成功或失败标志在HMI上给操作员相应的提示。这个案例展示了如何将底层的协议通信封装成上层的业务功能。一旦打通了底层上层各种复杂的数据库操作都只是构造不同的SQL字符串而已。7. 避坑指南Codesys开发中的两个重要提醒在实现这个项目的过程中我遇到了两个非常典型且容易忽略的Codesys开发陷阱它们与数据库协议无关但直接影响程序的稳定性必须高度重视。第一个坑结构体STRUCT的内存布局问题。你可能认为在ST语言中定义的结构体其成员在内存中的排列顺序就是你写的顺序。但在Codesys中如果你反复修改一个结构体的成员比如增删、调整顺序有时编译器并不会自动更新内存布局。这会导致MemMove函数复制数据时源地址和目标地址对不上数据完全错乱。更隐蔽的是即使不用MemMove在普通程序访问结构体成员时也可能出现值不对的情况。解决办法是每次修改结构体定义后务必在工程上执行“Clean All”清除所有然后再“Generate Code”生成代码。这个操作会强制编译器重新计算所有变量的内存偏移确保一致性。第二个坑Socket连接与掉电保存的冲突。在工业PLC中我们经常使用“保持变量”来在断电后保存关键数据。但是我发现当Codesys的Socket处于连接状态时如果PLC突然断电掉电保存功能可能会失效。也就是说那些本该在断电瞬间写入非易失存储器的变量值丢失了上电后恢复了旧值。这是因为Socket操作可能阻塞或延迟了系统的关机保存流程。可靠的策略是避免保持长连接。每次需要操作数据库时建立连接、执行操作、然后立即断开连接。虽然增加了每次操作的握手开销但换来了数据安全性和程序的确定性。对于高频数据采集可以考虑在内存中缓存一批数据然后定时批量插入依然采用“短连接”模式。自己动手实现Codesys直连MySQL最大的成就感来自于那种“一切尽在掌握”的自由度。你不再受限于第三方库的黑盒可以针对特定的网络环境优化数据包大小可以自定义复杂的重试逻辑可以解析任何复杂的查询结果。它确实需要你投入更多时间去理解底层协议但这份投入带来的灵活性和控制力在应对复杂的工业现场需求时是非常值得的。当你看到PLC上的数据稳稳地落入远端的数据库表中那种感觉就像亲手搭建了一座坚固可靠的桥。