【EtherCATBasics】- KRTS C++示例精讲(2)

EtherCATBasics示例讲解


目录


项目打开请查看【BaseFunction精讲】。

结构说明

  • EtherCATBasics:应用层程序,主要用于人机交互、数据显示、内核层数据交互等;

    1. EtherCATBasics.h : 数据定义
    2. EtherCATBasics.cpp:用户应用层源码
  • EtherCATBasics_64: 内核层程序(实时层程序),主要用于实时数据处理;

    1. EtherCATBasics.h : 数据定义
    2. EtherCATBasics_dll.cpp : 内核层源码
  • 其余文件说明请查看【BaseFunction精讲】中的结构说明。
    ps : 内核层中的数据、结构体需要一字节对齐,需要以MT方式构建

代码讲解

EtherCATBasics.h :与内核层共用一个头文件
cpp 复制代码
/* Copyright (c) 2011-2024 by Kithara Software GmbH. All rights reserved. */

//##############################################################################################################
//
// 文件:         EtherCATBasics.h
//
// 使用模块: EtherCAT模块, 时钟模块, 网络模块, 实时模块 
//
// 用户应用程序和内核DLL之间的共享定义,例如:如何通过EtherCAT实现基本进程数据交换的示例
//
// 开发者:      m.gru 2011-05-11
//
//##############################################################################################################

   /*=====================================================================*\
   |                    *** 免责声明 ***                     			   |
   |                                                                       |
   |       本代码仅是示例程序,您可以随意使用,我们不承担任何法律责任!		   |
   |																	   |
   \*=====================================================================*/

//##############################################################################################################
//
// 目的:
//
// 该示例说明了如何在不考虑同步读取进程数据的情况下实现基本的进程数据交换。
// 此外,该示例还展示了如何检测从站可用的分布式时钟功能以及如何利用这些功能。
// 首先,我们打开网络作为 EtherCAT 主站,并创建一个 EtherCAT 主站。
// 然后,我们让用户从连接的 EtherCAT 从站中选择一个,并将其映射到创建的数据集。
// 如果所选的 EtherCAT 从站支持分布式时钟 (DC) 功能,用户可以选择其中一种支持的模式。然后,用户必须从从站的一个 PDO 中选择一个变量。
// 将创建一个定时器,触发拓扑(已连接的 EtherCAT从站)与本示例实例化的 EtherCAT 主站之间的数据交换。
// 所选变量每 100ms 显示一次(定时器每 1ms 接收一次)。
//
//##############################################################################################################

#ifndef __SMP_ETHERCATBASICS_H
#define __SMP_ETHERCATBASICS_H

#include "../_KitharaSmp/_KitharaSmp.h"

//--------------------------------------------------------------------------------------------------------------
// SharedData 是用户定义的参数结构,用于在内核 DLL 和用户应用程序之间使用共享内存交换信息。
//--------------------------------------------------------------------------------------------------------------

struct SharedData {
  KSHandle hKernel;                                     // 内核句柄
  KSHandle hAdapter;                                    // 网络适配器句柄
  KSHandle hMaster;                                     // EtherCAT主站句柄
  KSHandle hSlave;                                      // EtherCAT从站句柄
  KSEcatMasterState masterState;                        // 获取主站状态的结构体
  KSHandle hDataSet;                                    // 用于主站和拓扑结构之间交换的数据集的句柄
  KSHandle hDataSetCallBack;                            // 回调句柄,当数据集从拓扑返回时将被调用
  KSHandle hTimerCallBack;                              // 由定时器调用的回调的句柄
  KSHandle hTimer;                                      // 将调用 DataSet 的计时器的句柄
  int varIndex;                                         // 需要查看的变量的索引
  int varSubIndex;                                      // 需要查看的变量的子索引
  uint data;                                            // 从 DataSet 复制的数据,供应用程序访问
  KSError error;                                        // 用于从内核空间 dll 向用户空间应用程序传递错误信息
};

#endif // __SMP_ETHERCATBASICS_H
EtherCATBasics.cpp 
cpp 复制代码
/* Copyright (c) 2009-2024 by Kithara Software GmbH. All rights reserved. */

//##############################################################################################################
//
// 文件:         EtherCATBasics.cpp
//
// 使用模块: EtherCAT模块, 时钟模块, 网络模块, 实时模块 
//
// 用户应用程序和内核DLL之间的共享定义,例如:如何通过EtherCAT实现基本进程数据交换的示例
//
// 开发者:      t.pet 2009-08-26
//
//##############################################################################################################

   /*=====================================================================*\
   |                    *** 免责声明 ***                     			   |
   |                                                                       |
   |       本代码仅是示例程序,您可以随意使用,我们不承担任何法律责任!		   |
   |																	   |
   \*=====================================================================*/

//##############################################################################################################
//
// 目的:
//
// 该示例说明了如何在不考虑同步读取进程数据的情况下实现基本的进程数据交换。
// 此外,该示例还展示了如何检测从站可用的分布式时钟功能以及如何利用这些功能。
// 首先,我们打开网络作为 EtherCAT 主站,并创建一个 EtherCAT 主站。
// 然后,我们让用户从连接的 EtherCAT 从站中选择一个,并将其映射到创建的数据集。
// 如果所选的 EtherCAT 从站支持分布式时钟 (DC) 功能,用户可以选择其中一种支持的模式。然后,用户必须从从站的一个 PDO 中选择一个变量。
// 将创建一个定时器,触发拓扑(已连接的 EtherCAT从站)与本示例实例化的 EtherCAT 主站之间的数据交换。
// 所选变量每 100ms 显示一次(定时器每 1ms 接收一次)。
//
//##############################################################################################################


//--------------------------------------------------------------------------------------------------------------
// 为了在主程序(用户层)和内核 DLL 之间共享数据结构的定义,我们使用了一个通用的头文件。
//--------------------------------------------------------------------------------------------------------------

#include "EtherCATBasics.h"

//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// 别忘了输入你的序列号(6位客户编号),这是打开驱动程序所必需的。
// 
// 如果你使用Demo版本,也可以使用"DEMO"代替。
// 如果你使用Beta版本,也可以使用"BETA"代替。
//
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

// 如上说所,定义的客户号 
const char _pCustomerNumber[] = "DEMO";

// 主程序入口
void runSample() {
  // 调用KitharaSmp.h 中的函数,输出文本
  outputTxt("***** Kithara example program 'EtherCATBasics' *****");

  // 错误码定义,KSError 是 Kithara API 所有函数的返回类型,通过 【KSError】 可以查询接口的返回错误信息。
  KSError ksError;

  //------------------------------------------------------------------------------------------------------------
  // 打开驱动程序的第一步,所有KRTS程序必须进行的操作。
  // 只要该函数调用成功后,我们可以使用其他函数。如果打开失败,则无法调用其他函数。
  // 此函数接受您的客户编号作为参数,其中包含 Kithara(如果适用可以为"DEMO"或"BETA")。
  //------------------------------------------------------------------------------------------------------------
  ksError = KS_openDriver(
              _pCustomerNumber);                        // 客户编号
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_openDriver", "Unable to open the driver!");
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // 创建共享内存
  // 为实时层中的DLL和此用户层应用程序之间的通信。
  //------------------------------------------------------------------------------------------------------------

  KSHandle hSharedMemory;
  ksError = KS_createSharedMemEx(
              &hSharedMemory,                           // 返回创建的共享内存句柄
              "",                                       // 共享内存的名称
              sizeof(SharedData),                       // 共享内存的大小
              KSF_NO_FLAGS);                            // 无标记,此选项可以进行一些特殊设定
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_createSharedMemEx", "Unable to create shared memory!");
    KS_closeDriver();
    return;
  }


  // 要访问共享内存,应用程序需要使用刚创建的共享内存的句柄来获取指向分配的共享内存的指针。
  SharedData* pApp = NULL;								// 自定义的共享内存结构体
  ksError = KS_getSharedMemEx(
              hSharedMemory,                            // 共享内存的句柄
              (void**)&pApp,                            // 指向共享内存的结构的指针
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_getSharedMemEx", "Unable to map shared memory!");
    KS_closeDriver();
    return;
  }

  // 确定操作系统的位数大小以决定是加载32位还是64位内核DLL。
  KSSystemInformation systemInfo;                       // 获取系统信息的结构体
  systemInfo.structSize = sizeof(KSSystemInformation);  // 不要忘记设备结构体大小
  ksError = KS_getSystemInformation(
              &systemInfo,                              // 结构体指针用于获取结构体数据
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_getSystemInformation", "Unable to get system information to distinguish bitsize!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // 想要在内核级别上使用DLL中的函数,必须加载dll,在调用里面的函数!
  // 注意!加载程序必须找到DLL,因此它应该放在搜索路径的目录中!
  // 因为我们想要在共享内存中传递加载的内核的句柄,所以我们不使用加载内核时的初始化函数,
  // 而是在填充内核句柄和初始化内核所需的所有信息之后,显式调用初始化函数。
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_loadKernel(
              &pApp->hKernel,                           // 返回内核操作句柄
              systemInfo.isSys64Bit ?                   // 根据系统位数加载内核Dll
                "EtherCATBasics_64.dll" :               
                "EtherCATBasics_32.dll",                
              NULL,                                     // 需要支持的函数名称(未使用)
              NULL,                                     // 函数参数 (未使用)
              KSF_KERNEL_EXEC);                         // 内核空间中加载
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_loadKernel", "Unable to load DLL! Is the DLL in the search path?");
    KS_closeDriver();
    return;
  }

  // 查询并展示所有受支持的网络适配器
  char pDeviceName[256];			// 用于保存设备名称

  outputTxt(" ");
  outputTxt("Following network adapters found:");

  for (int i = 0;; ++i) {
	// KS_enumDevices()可以用于查询分配给Kithara驱动程序的所有网络适配器的名称。
    ksError = KS_enumDevices(
                "NET",                                  // 'NET' 代表搜索网络设备
                i,                                      // 从0开始的枚举索引号
                pDeviceName,                            // 返回设备名称
                KSF_NO_FLAGS);                          // 无标记
    if (ksError != KS_OK) {
      if (KSERROR_CODE(ksError) != KSERROR_DEVICE_NOT_FOUND)
        outputErr(ksError, "KS_enumDevices", "Unable to query network device name!");
      if (KSERROR_CODE(ksError) == KSERROR_DEVICE_NOT_FOUND && !i) {
        outputTxt("No network adapters found!");
        outputTxt(" ");
        KS_closeDriver();
        return;
      }
      break;
    }
    // 输出索引号对应的设备名称
    outputDec(i, "", ": ", false);
    outputTxt(pDeviceName);
  }
  outputTxt(" ");


  // 输入想要打开的适配器索引号
  outputTxt("Attention!");
  outputTxt("By selecting a device its Windows driver gets removed and replaced by the");
  outputTxt("appropriate Kithara driver. This will render the network device for the duration");
  outputTxt("of this sample invisible to Windows.");
  outputTxt("Be sure that all other Applications using that device are closed right now!");


  // 输入并保存索引号并根据索引和再次获取设备名称
  int deviceIndex = inputDec("Device number: ", 0);
  ksError = KS_enumDevices(
              "NET",                                    // 'NET' 代表搜索网络设备
              deviceIndex,                              // 选择的设备索引号
              pDeviceName,                              // 返回设备名称
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_enumDevices", "Unable to query selected network device name!");
    KS_closeDriver();
    return;
  }

  outputTxt("Selected device: ", false);
  outputTxt(pDeviceName);
  outputTxt(" ");


  // 根据设备名称,打开以太网适配器。
  ksError = KS_openNetworkAdapter(
              &pApp->hAdapter,                          // 获取适配器句柄
              pDeviceName,                              // 输入适配器的硬件ID
              NULL,                                     // 设配器配置选项
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_openNetworkAdapter", "Failed to open network adapter!");
    KS_closeDriver();
    return;
  }

  // 创建主站
  // 输入ESI文件,文件夹路径,ESI文件可以在大多数设备官网中获取。 
  char* pXmlPath = inputTxt("Please enter config path to XML files: ", "C:\\Program Files\\Kithara\\RealTime Suite Demo\\xml");
  ksError = KS_createEcatMaster(
              &pApp->hMaster,                           // 返回主站句柄
              pApp->hAdapter,                           // 适配器句柄
              pXmlPath,                                 // ESI文件夹路径
              "",                                       // 拓扑文件
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_createEcatMaster", "Failed to create EtherCAT master!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // 显式调用初始化函数。
  // 调用内核的【_initFunction】函数,并传递共享内存的句柄,这样内核就可以从句柄中检索到共享内存的指针,并根据共享内存中存储的信息进行所有必要的资源分配。
  // 更为详细的内核初始化操作可以查看内核层【_initFunction】函数
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_execKernelFunctionEx(
              pApp->hKernel,                            // 内核句柄
              "_initFunction",                          // 函数名称
              hSharedMemory,                            // 共享内存的句柄
              KS_INVALID_HANDLE,                        // 上下文
              KSF_NO_FLAGS);                            // 未使用
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_execKernelFunctionEx", "Unable to initialize the kernel DLL!");
    KS_closeDriver();
    return;
  }

  bool dcLicensed = true;			// 是否支持DC模式
  // 输出从站在线个数
  outputDec(pApp->masterState.slavesOnline, "slavesOnline = ");	
  outputTxt("Choose a slave: ");
  KSEcatSlaveState slaveState;		// 从站状态的结构体
  slaveState.structSize = sizeof(KSEcatSlaveState);     // 初始化结构体大小

  // 遍历所有已连接的从设备,显示它们的基本信息,让用户选择其中一个。
  for (int i = 0; i < pApp->masterState.slavesOnline; ++i) {

    //----------------------------------------------------------------------------------------------------------
  // 使用KS_enumEcatSlaves()函数来迭代所有在线的从站设备。如果它的第二个参数大于在线从设备的数量,它将返回KSERROR_DEVICE_NOT_FOUND。
    //----------------------------------------------------------------------------------------------------------

    ksError = KS_enumEcatSlaves(
                pApp->hMaster,                          // EtherCAT 主站句柄
                i,                                      // 从0开始的枚举索引号
                &slaveState,                            // 从站状态结构体
                KSF_NO_FLAGS);                          // 无标记
    if (KSERROR_CODE(ksError) == KSERROR_DEVICE_NOT_FOUND)
      break;
    if (ksError != KS_OK) {
      outputErr(ksError, "KS_enumEcatSlaves", "Failed to enumerate EtherCAT slaves!");
      KS_closeDriver();
      return;
    }


    //----------------------------------------------------------------------------------------------------------
    // 创建从站
    // KS_enumEcatSlaves()函数填充了KSEcatSlaveState结构体,其中包含了从设备的基本信息。
    // 为了获取更多的详细信息,需要为该从站设备创建一个句柄。可以直接将接收到的KSEcatSlaveState结构体传递给KS_createEcatSlaveIndirect()函数。
    //----------------------------------------------------------------------------------------------------------

    KSHandle hSlave;
    ksError = KS_createEcatSlaveIndirect(
                pApp->hMaster,                          // 主站句柄
                &hSlave,                                // 返回新从站句柄
                &slaveState,                            // 返回从站信息
                KSF_FORCE_OVERRIDE);                    // 忽略缺少的XML信息
    if (ksError != KS_OK) {
      outputErr(ksError, "KS_createEcatSlaveIndirect", "Failed to create EtherCAT slave!");
      KS_closeDriver();
      return;
    }

    // 修改从站状态,只有在等于或高于的KS_ECAT_STATE_PREOP状态下,才能查询在线从机信息。
    ksError = KS_changeEcatState(
                hSlave,                                 // 从站句柄
                KS_ECAT_STATE_PREOP,                    // 需要切换的状态
                KSF_NO_FLAGS);                          // 无标志
    if (ksError != KS_OK) {
      outputErr(ksError, "KS_changeEcatState", "Failed to change EtherCAT slave state to PREOP!");
      KS_closeDriver();
      return;
    }


    //----------------------------------------------------------------------------------------------------------
    // 查询从站信息
	// KS_queryEcatSlaveInfo()函数将KSEcatSlaveInfo结构填充了所有可用的信息。
	// 通过使用KSF_PDO和KSF_SDO标志,您可以决定是否想要有关过程数据对象、服务数据对象或两者的信息。
	// 因为我们只对从设备的名称感兴趣,而不需要对象信息,所以我们使用flags == KSF_NO_FLAGS。
    //----------------------------------------------------------------------------------------------------------

    KSEcatSlaveInfo* pSlaveInfo;
    ksError = KS_queryEcatSlaveInfo(
                hSlave,                                 // 从站句柄
                &pSlaveInfo,                            // 返回从站信息
                KSF_NO_FLAGS);                          // 无标记
    if (ksError != KS_OK) {
      outputErr(ksError, "KS_queryEcatSlaveInfo", "Unable to read slave information!");
      KS_closeDriver();
      return;
    }

    //----------------------------------------------------------------------------------------------------------
	// 为了测试一个从节点是否支持分布式时钟,我们只需要枚举分布式时钟的操作模式。
	// 如果索引等于0,则该从节点支持分布式时钟。
    //----------------------------------------------------------------------------------------------------------

    char pDcOpMode[256];
    ksError = KS_enumEcatDcOpModes(
                hSlave,                                 // 从站句柄
                0,                                      // OP模式
                pDcOpMode,                              // 模式名称
                NULL,                                   // 模式描述
                KSF_NO_FLAGS);                          // 无标记
    if (KSERROR_CODE(ksError) == KSERROR_FEATURE_NOT_LICENSED) {
      ksError = KS_OK;
      dcLicensed = false;
    }

    if (ksError != KS_OK && KSERROR_CODE(ksError) != KSERROR_DEVICE_NOT_FOUND) {
      outputErr(ksError, "KS_enumEcatDcOpModes", "Failed to query EtherCAT DC op mode");
      KS_closeDriver();
      return;
    }

    // 输出从站索引以及从站是否支持分布式时钟和从站名称。
    if (KSERROR_CODE(ksError) == KSERROR_DEVICE_NOT_FOUND || !dcLicensed)
      outputDec(i, "", ": [noDC] ", false);
    else
      outputDec(i, "", ": [DC]   ", false);
    outputTxt(pSlaveInfo->name);



    // 删除用于查询从属信息以供显示从站对象。
    ksError = KS_deleteEcatSlave(
                hSlave);                                // 需要删除的从站句柄
    if (ksError != KS_OK) {
      outputErr(ksError, "KS_deleteEcatSlave", "Unable to delete slave!");
      KS_closeDriver();
      return;
    }
  }

   // 请输入选择的从站,已查询从站信息
  int slaveIndex = inputDec("Slave index: ", -1);

  if (slaveIndex < 0 || slaveIndex >= pApp->masterState.slavesOnline) {
    outputTxt("Invalid slave index!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // 在指定的索引处创建一个 ECAT 从节点设备。
  // 只用索引,不需要 ID、厂商 ID、产品 ID 或修订号。
  //------------------------------------------------------------------------------------------------------------

  // 创建从站 
  ksError = KS_createEcatSlave(
              pApp->hMaster,                            // 主站句柄
              &pApp->hSlave,                            // 返回从站句柄
              0,                                        // 该位置相对于的标识符,0表示绝对位置
              slaveIndex,                               // 从站位置
              0,                                        // 供应商 ID(0 = any)
              0,                                        // 产品ID (0 = any)
              0,                                        // 版本(0 = any)
              KSF_FORCE_OVERRIDE);                      // 忽略缺少的XML信息
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_createEcatSlave", "Failed to create EtherCAT slave!");
    KS_closeDriver();
    return;
  }


  // 只有在KS_ECAT_STATE_PREOP等于或高于的状态下,才能查询在线从机信息。
  ksError = KS_changeEcatState(
              pApp->hSlave,                             // 从站句柄
              KS_ECAT_STATE_PREOP,                      // 需要修改的从站状态
              KSF_NO_FLAGS);                            // 无状态
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_changeEcatState", "Failed to change EtherCAT slave state to PREOP!");
    KS_closeDriver();
    return;
  }

  // 如果DC功能可用,则显示所有可用的分布式时钟操作模式。
  if (dcLicensed) {
    char pDcOpMode[256];
    char pDcOpModeDescription[256];

    outputTxt(" ");

    int dcModeCount = -1;
    for (int i = 0;; ++i) {


      //--------------------------------------------------------------------------------------------------------
	  // 使用 KS_enumEcatDcOpModes() 函数,您可以枚举所有可用的分布式时钟操作模式。
	  // 我们使用此函数来显示所有可用的分布式时钟操作模式,以便让用户选择其中一个。
      //--------------------------------------------------------------------------------------------------------

      ksError = KS_enumEcatDcOpModes(
                  pApp->hSlave,                         // 从站句柄
                  i,                                    // 从零开始枚举索引
                  pDcOpMode,                            // 获取DC模式名称
                  pDcOpModeDescription,                 // 获取DC模式描述信息
                  KSF_NO_FLAGS);                        // 无标记
      if (ksError != KS_OK) {
        if (KSERROR_CODE(ksError) != KSERROR_DEVICE_NOT_FOUND) {
          outputErr(ksError, "KS_enumEcatDcOpModes", "Unable to query DC op mode!");
          KS_closeDriver();
          return;
        }

        break;
      }
      dcModeCount = i;
      if (i == 0)
          outputTxt("Following DC op modes where found:");

      outputDec(i, "", ": ", false);
      outputTxt(pDcOpMode, false);
      outputTxt(" - ", false);
      outputTxt(pDcOpModeDescription);
    }

    //----------------------------------------------------------------------------------------------------------
    // 如果有 DC 操作可用,让用户通过索引选择一个并验证选择。
    //----------------------------------------------------------------------------------------------------------

    if (dcModeCount >= 0) {
      int dcOpModeIndex = inputDec("Op mode number: ", -1);

      outputTxt(" ");

      ksError = KS_enumEcatDcOpModes(
                  pApp->hSlave,                         // 从站句柄
                  dcOpModeIndex ,                       // 索引
                  pDcOpMode,                            // 获取DC模式名称
                  pDcOpModeDescription,                 // 获取DC模式描述信息
                  KSF_NO_FLAGS);                        // 无标记
      if (ksError != KS_OK) {
        outputErr(ksError, "KS_enumEcatDcOpModes", "Unable to query DC op mode!");
        KS_closeDriver();
        return;
      }

      outputTxt("Selected op mode: ", false);
      outputTxt(pDcOpMode, false);
      outputTxt(" - ", false);
      outputTxt(pDcOpModeDescription, false);
      outputTxt(" ");


      // 获取DC操作模式的参数。
      KSEcatDcParams dcParams;
      ksError = KS_lookupEcatDcOpMode(
                  pApp->hSlave,                         // 从站句柄
                  pDcOpMode,                            // 操作模式
                  &dcParams,                            // 返回DC模式参数
                  KSF_NO_FLAGS);                        // 无标记
      if (ksError != KS_OK) {
        outputErr(ksError, "KS_lookupEcatDcOpMode", "Unable to lookup DC op mode!");
        KS_closeDriver();
        return;
      }

      // 根据需要调整参数。
	  // ...


      // 如果配置了从站节点以使用此DC操作模式(可能调整了dcParams)。
      ksError = KS_configEcatDcOpMode(
                  pApp->hSlave,                         // 从站句柄
                  pDcOpMode,                            // 模式名称
                  &dcParams,                            // DC参数设定
                  KSF_NO_FLAGS);                        // 无标记
      if (ksError != KS_OK) {
        outputErr(ksError, "KS_configEcatDcOpMode", "Unable to lookup DC op mode!");
        KS_closeDriver();
        return;
      }
    }
  }


  //------------------------------------------------------------------------------------------------------------
  // 现在将为所选的从属节点分配DataSet。
  // 只要KS_assignEcatDataSet()没有返回错误,我们就可以分配对象。
  // 简化同步对象的选择,使用提供的特殊常量:KS_ECAT_SYNC_INPUT、KS_ECAT_SYNC_OUTPUT和KS_ECAT_SYNC_ALL。
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_assignEcatDataSet(
              pApp->hDataSet,                           // DataSet 句柄
              pApp->hSlave,                             // 从站句柄
              KS_ECAT_SYNC_ALL,                         // 同步对象的类型
              0,                                        // 在DataSet中的特殊位置
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_assignEcatDataSet", "Failed to assign slave!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // KS_queryEcatSlaveInfo()用所有可用信息填充KSEcatSlaveInfo结构。
  // 使用Flags KSF_PDO和KSF_SDO可以决定是否需要有关PDO,SDO对象或两者。
  //------------------------------------------------------------------------------------------------------------

  KSEcatSlaveInfo* pSlaveInfo;
  ksError = KS_queryEcatSlaveInfo(
              pApp->hSlave,                             // 从站句柄
              &pSlaveInfo,                              // 从站信息的结构体
              KSF_PDO);                                 // 查询PDO数据
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_queryEcatSlaveInfo", "Unable to read slave information!");
    KS_closeDriver();
    return;
  }

  //------------------------------------------------------------------------------------------------------------
  // 让用户选择一个PDO对象。
  //------------------------------------------------------------------------------------------------------------

  // 遍历并输出PDO信息
  outputTxt(" ");
  outputTxt("Choose a PDO: (only active and readable PDOs are displayed)");

  for (int i = 0; i < pSlaveInfo->objCount; ++i) {
    KSEcatDataObjInfo* pObjInfo = pSlaveInfo->objs[i];
    if ((pObjInfo->objType & KS_DATAOBJ_PDO_TYPE) &&
        (pObjInfo->objType & KS_DATAOBJ_ACTIVE) &&
        (pObjInfo->objType & KS_DATAOBJ_READABLE)) {
      outputDec(i, "", ": ", false);
      outputTxt(pObjInfo->name);
    }
  }

  int pdoIndex = inputDec("PDO index: ", -1);
  if (pdoIndex < 0 || pdoIndex >= pSlaveInfo->objCount) {
    outputTxt("Invalid PDO index!");
    KS_closeDriver();
    return;
  }

  KSEcatDataObjInfo* pObjInfo = pSlaveInfo->objs[pdoIndex];
    if (!(pObjInfo->objType & KS_DATAOBJ_PDO_TYPE) ||
      !(pObjInfo->objType & KS_DATAOBJ_ACTIVE) ||
      !(pObjInfo->objType & KS_DATAOBJ_READABLE)) {
    outputTxt("Invalid PDO!");
    KS_closeDriver();
    return;
  }

  outputTxt(" ");
  outputTxt("Choose a variable: ");
  for (int i = 0; i < pObjInfo->varCount; ++i) {
    KSEcatDataVarInfo* pVarInfo = pObjInfo->vars[i];
    outputDec(i, "", ": ", false);
    outputTxt(pVarInfo->name);
  }
  int varIndex = inputDec("Variable index: ", -1);

  if (varIndex < 0 || varIndex >= pObjInfo->varCount) {
    outputTxt("Invalid variable index!");
    KS_closeDriver();
    return;
  }

  pApp->varIndex    = pObjInfo->index;
  pApp->varSubIndex = pObjInfo->vars[varIndex]->subIndex;


  //------------------------------------------------------------------------------------------------------------
  // 开始进行数据交换
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_execKernelFunctionEx(
              pApp->hKernel,                            // 内核句柄
              "_startDataExchange",                     // 调用内核层函数
              KS_INVALID_HANDLE,                        // 传参
              KS_INVALID_HANDLE,                        // 上下文
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_execKernelFunctionEx", "Error while executing kernel functions!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  //这是主循环。它将每100 ms更新所选变量的显示,直到用户按下"q"键或数据交换错误将被检测到时退出。
  //------------------------------------------------------------------------------------------------------------

  outputTxt(" ");
  outputTxt("Press [Q] to finish the process data exchange...");
  outputTxt(" ");

  for (;;) {
    waitTime(100 * ms);
    outputHex08(pApp->data, "Value: ", "\r", false);

    if (pApp->error != KS_OK)
      break;

    if (myKbhit() != 0) {
      int key = myGetch();
      if (key == 'q' || key == 'Q')
        break;
    }
  }

  if (pApp->error != KS_OK)
    outputErr(ksError, "Error while receiving data!");

  //  清理内核层资源
  ksError = KS_execKernelFunctionEx(
              pApp->hKernel,                            // 内核句柄
              "_exitFunction",                          // 内核层退出函数
              KS_INVALID_HANDLE,                        // 传参
              KS_INVALID_HANDLE,                        // 上下文
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    outputErr(ksError, "KS_execKernelFunctionEx", "Error while deallocating resources on kernel level!");
    KS_closeDriver();
    return;
  }


  //------------------------------------------------------------------------------------------------------------
  // 使用共享句柄卸载内核DLL。
  // 尽管KS_closeDriver()释放了所有分配的资源(如共享内存和加载的内核),
  // 明确释放您分配的资源是很好的风格。
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_freeKernel(
              pApp->hKernel);                           // 内核句柄
  if (ksError != KS_OK)
    outputErr(ksError, "KS_freeKernel", "Unable to unload the kernel!");


  // 清理共享内存
  ksError = KS_freeSharedMemEx(
              hSharedMemory,                            // 共享内存句柄
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK)
    outputErr(ksError, "KS_freeSharedMemEx", "Unable to remove shared memory!");

  // 关闭设备,清理所有资源
  ksError = KS_closeDriver();
  if (ksError != KS_OK)
    outputErr(ksError, "KS_closeDriver", "Unable to close the driver!");

  waitTime(500 * ms);
  outputTxt(" ");
  outputTxt("End of program 'EtherCATBasics'.");
}
EtherCATBasics_dll.cpp
cpp 复制代码
/* Copyright (c) 2011-2024 by Kithara Software GmbH. All rights reserved. */

//##############################################################################################################
//
// 文件:         EtherCATBasics_dll.cpp
//
// 使用模块: EtherCAT模块, 时钟模块, 网络模块, 实时模块 
//
// 用户应用程序和内核DLL之间的共享定义,例如:如何通过EtherCAT实现基本进程数据交换的示例
//
// 开发者:      m.gru 2011-05-11
//
//##############################################################################################################

   /*=====================================================================*\
   |                    *** 免责声明 ***                     			   |
   |                                                                       |
   |       本代码仅是示例程序,您可以随意使用,我们不承担任何法律责任!		   |
   |																	   |
   \*=====================================================================*

//##############################################################################################################
//
// 目的:
//
// 该示例说明了如何在不考虑同步读取进程数据的情况下实现基本的进程数据交换。
// 此外,该示例还展示了如何检测从站可用的分布式时钟功能以及如何利用这些功能。
// 首先,我们打开网络作为 EtherCAT 主站,并创建一个 EtherCAT 主站。
// 然后,我们让用户从连接的 EtherCAT 从站中选择一个,并将其映射到创建的数据集。
// 如果所选的 EtherCAT 从站支持分布式时钟 (DC) 功能,用户可以选择其中一种支持的模式。然后,用户必须从从站的一个 PDO 中选择一个变量。
// 将创建一个定时器,触发拓扑(已连接的 EtherCAT从站)与本示例实例化的 EtherCAT 主站之间的数据交换。
// 所选变量每 100ms 显示一次(定时器每 1ms 接收一次)。
//
//##############################################################################################################

//--------------------------------------------------------------------------------------------------------------
// 为了在主程序和内核 DLL 之间共享数据结构定义,我们使用一个公共头文件。
//--------------------------------------------------------------------------------------------------------------

#include "EtherCATBasics.h"

// 共享内存用于在内核层 DLL 和用户层的应用程序之间共享数据。
SharedData* _pSys = NULL;


// 指向从站选定 ECAT 设备变量接收到的数据。
void* _pSlaveData;

// 在数据集中要查看的变量的位置和长度。
int _bitOffset;
int _bitLength;


// 前置声明:定时器回调、数据集回调,声明在文件末尾定义的回调函数。
KSError __stdcall timerCallBack  (void* /*pArgs*/, void* /*pContext*/);
KSError __stdcall dataSetCallBack(void* /*pArgs*/, void* /*pContext*/);

//--------------------------------------------------------------------------------------------------------------
// 这是初始化函数。
// 它在加载内核后被调用,并将共享内存的句柄作为参数传递。
//
// 注意!请记住,所有函数都应声明为 'extern "C"'!
// 否则它们的名字可能无法被加载器找到。
// 必须通过 '__declspec(dllexport)' 导出它们。
//--------------------------------------------------------------------------------------------------------------

extern "C" KSError __declspec(dllexport) __stdcall _initFunction(void* pArgs, void* /*pContext*/) {
  KSError ksError;


  // 共享内存的指针通过 KS_execKernelFunctionEx() 作为 'pArgs' 参数传递。
  _pSys = (SharedData*)pArgs;


  //------------------------------------------------------------------------------------------------------------
  // 在这里我们等待主站连接到拓扑。
  // 代替轮询主站状态,您也可以注册一个回调来处理此事件。
  // 详情请参阅手册中的 KS_installEcatHandler()。
  //------------------------------------------------------------------------------------------------------------

  _pSys->masterState.structSize = sizeof(KSEcatMasterState); // 不要忘记初始化 structSize!

  for (int i = 0; i < 50; ++i) {
    ksError = KS_queryEcatMasterState(
                _pSys->hMaster,                         // 主站句柄
                &_pSys->masterState,                    // 返回KSEcatMasterState 结构体
                KSF_NO_FLAGS);                          // 无标志
    if (ksError != KS_OK)
      return ksError;

    if (_pSys->masterState.connected)
      break;

    KS_microDelay(100 * ms);
  }

  if (_pSys->masterState.connected == 0)
    return KSERROR_CATEGORY_ETHERCAT;

  //------------------------------------------------------------------------------------------------------------
  // 为了准备过程数据交换,我们需要创建一个数据集并分配一个同步对象给它。
  // 因为我们稍后会使用 KS_getEcatDataObjAddress() 来获取内存位置。
  //------------------------------------------------------------------------------------------------------------


  ksError = KS_createEcatDataSet(
              _pSys->hMaster,                           // 主站句柄
              &_pSys->hDataSet,                         // 写入新数据集句柄的地址
              NULL,                                     // 数据集数据的应用程序空间指针(未使用)
              NULL,                                     // 数据集数据的内核空间指针(未使用)
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK)
    return ksError;


  //------------------------------------------------------------------------------------------------------------
  // 创建一个回调,当数据集从从站返回数据时调用。
  // 安装数据集处理器允许我们在从从站接收到数据时作出反应。
  //------------------------------------------------------------------------------------------------------------
  ksError = KS_createCallBack(
              &_pSys->hDataSetCallBack,                 // 写入新回调句柄的地址
              dataSetCallBack,                          // 回调函数
              NULL,                                     // 回调参数(未使用)
              KSF_DIRECT_EXEC,                          // 标志,这里内核级别
              0);                                       // 优先级(内核级别未使用)
  if (ksError != KS_OK)
    return ksError;

  // 安装创建的回调作为数据集处理器。
  ksError = KS_installEcatHandler(
              _pSys->hDataSet,                          // 数据集句柄
              KS_DATASET_SIGNAL,                        // 事件代码
              _pSys->hDataSetCallBack,                  // 回调句柄
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


 // 创建一个定时器回调,该回调将定期执行并将数据集发送到拓扑。
  ksError = KS_createCallBack(
              &_pSys->hTimerCallBack,                   // 写入新回调句柄的地址
              timerCallBack,                            // 回调函数
              NULL,                                     // 回调参数(未使用)
              KSF_DIRECT_EXEC,                          // 标志,这里内核级别
              0);                                       // 优先级(内核级别未使用)
  if (ksError != KS_OK)
    return ksError;


  // 创建一个周期为 1 毫秒的定时器,并分配回调给它。
  ksError = KS_createTimer(
              &_pSys->hTimer,                           // 写入新定时器句柄的地址
              1 * ms,                                   // 定时器周期(100纳秒单位)
              _pSys->hTimerCallBack,                    // 回调句柄
              KSF_REALTIME_EXEC |                       // 精确的高分辨率实时定时器
                KSF_DONT_START);                        // 不立即启动
  if (ksError != KS_OK)
    return ksError;
    
  return KS_OK;
}


//--------------------------------------------------------------------------------------------------------------
// 这是清理函数,关闭 EtherCAT 主站和网络设备,并移除定时器、其回调、数据集、数据集处理器、数据集回调和 EtherCAT 从站。
//--------------------------------------------------------------------------------------------------------------

extern "C" KSError __declspec(dllexport) __stdcall _exitFunction(void* /*pArgs*/, void* /*pContext*/) {
  if (_pSys == NULL)                                    // 共享内存未映射!
    return KSERROR_FUNCTION_NOT_AVAILABLE;              // _initFunction 未调用?

  KSError ksError;


  // 关闭
  ksError = KS_changeEcatState(
              _pSys->hDataSet,                          // 数据集句柄
              KS_ECAT_STATE_SAFEOP,                     // 状态
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


  // 停止定时器。
  ksError = KS_stopTimer(
              _pSys->hTimer);                           // 定时器句柄
  if (ksError != KS_OK)
    return ksError;

  // 切换 SAFEOP
  ksError = KS_changeEcatState(
              _pSys->hDataSet,                          // DataSet 句柄
              KS_ECAT_STATE_INIT,                       // 状态
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


  // 移除定时器
  ksError = KS_removeTimer(
              _pSys->hTimer);                           // 定时器句柄
  if (ksError != KS_OK)
    return ksError;


  // 移除定时器回调。
  ksError = KS_removeCallBack(
              _pSys->hTimerCallBack);                   // 定时器回调句柄
  if (ksError != KS_OK)
    return ksError;


  // 卸载数据集处理器
  ksError = KS_installEcatHandler(
              _pSys->hDataSet,                          // DataSet 句柄
              KS_DATASET_SIGNAL,                        // 回调事件类型
              KS_INVALID_HANDLE,                        // 使事件无效
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


  // 移除数据集回调
  ksError = KS_removeCallBack(
              _pSys->hDataSetCallBack);                 // DataSet 回调句柄
  if (ksError != KS_OK)
    return ksError;


  // 删除数据集
  ksError = KS_deleteEcatDataSet(
              _pSys->hDataSet);                         // DataSet 句柄
  if (ksError != KS_OK)
    return ksError;


  // 切换 OP 状态与 EtherCAT 从站
  ksError = KS_changeEcatState(
              _pSys->hSlave,                            // 从站句柄
              KS_ECAT_STATE_INIT,                       // 状态类型
              KSF_NO_FLAGS);                            // DataSet 
  if (ksError != KS_OK)
    return ksError;


  // EtherCAT 主站的状态应在结束时更改为 'init'
  ksError = KS_changeEcatState(
              _pSys->hMaster,                           // 主站句柄
              KS_ECAT_STATE_INIT,                       // 状态类型
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


  //------------------------------------------------------------------------------------------------------------
  // 删除EtherCAT从站
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_deleteEcatSlave(
              _pSys->hSlave);                           // 从站句柄
  if (ksError != KS_OK)
    return ksError;


  //------------------------------------------------------------------------------------------------------------
  // 关闭EtherCAT主站
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_closeEcatMaster(
              _pSys->hMaster);                          // 主站句柄
  if (ksError != KS_OK)
    return ksError;


  //------------------------------------------------------------------------------------------------------------
  // 关闭网络适配器
  //------------------------------------------------------------------------------------------------------------

  ksError = KS_closeNetwork(
              _pSys->hAdapter,                          // 网络适配器句柄
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;

  return KS_OK;
}


//--------------------------------------------------------------------------------------------------------------
// _startDataExchange()函数将被用户调用,以开始数据交换。
//--------------------------------------------------------------------------------------------------------------

extern "C" __declspec(dllexport) KSError __stdcall _startDataExchange(void* /*pArgs*/, void* /*pContext*/) {
  KSError ksError;

  _pSys->error = KS_OK;


  // 查询所选变量的地址、位偏移量和位长度。
  ksError = KS_getEcatDataObjAddress(
              _pSys->hDataSet,                          // DataSet句柄
              _pSys->hSlave,                            // 从站句柄
              _pSys->varIndex,                          // 索引号
              _pSys->varSubIndex,                       // 子索引号
              NULL,                                     // 应用程序空间数据指针(未使用)
              &_pSlaveData,                             // 返回从站数据
              &_bitOffset,                              // 返回位偏移量
              &_bitLength,                              // 返回位长度
              KSF_NO_FLAGS);                            // 无标志
  if (ksError != KS_OK)
    return ksError;


  //------------------------------------------------------------------------------------------------------------
  // 如果 EtherCAT 从站具有分布式时钟(Distributed Clocks)功能,我们就需要启用该功能,以确保该示例能按预期运行。
  // 在进入 SAFEOP 之前必须调用 KS_activateEcatDcMode(),它将启动相关的定时器。
  //------------------------------------------------------------------------------------------------------------

  int64ref dcStartTime = 0;
  ksError = KS_activateEcatDcMode(
              _pSys->hDataSet,                          // 主站, 从站或DataSet 句柄
              dcStartTime,                              // startTime,在不久的将来的一段时间为0
              1000000,                                  // 循环时间(ns)->1 ms
              0,                                        // 偏移时间(ns)
              _pSys->hTimer,                            // 定时器句柄
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK && KSERROR_CODE(ksError) != KSERROR_FEATURE_NOT_LICENSED)
    return ksError;


  // 为了读取过程数据,必须将EtherCAT从站的状态更改为"KS_ECAT_STATE_SAFEOP"。
  ksError = KS_changeEcatState(
              _pSys->hDataSet,                          // DataSet 句柄
              KS_ECAT_STATE_SAFEOP,                     // 切换状态
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK)
    return ksError;



  // 现在将Slave切换到OP。
  ksError = KS_changeEcatState(
              _pSys->hDataSet,                          // DataSet 句柄
              KS_ECAT_STATE_OP,                         // 切换状态
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK)
    return ksError;

  return KS_OK;
}


// 该回调由定时器定期调用,并通过 KS_postEcatDataSet()启动进程数据交换。
KSError __stdcall timerCallBack(void* /*pArgs*/, void* /*pContext*/) {
  KSError ksError;

  if (_pSys->error != KS_OK)
    return _pSys->error;

  // 下发数据
  ksError = KS_postEcatDataSet(
              _pSys->hDataSet,                          // DataSet 句柄
              KSF_NO_FLAGS);                            // 无标记

  return _pSys->error = ksError;
}


//--------------------------------------------------------------------------------------------------------------
// 当 DataSet 从从站拓扑返回主站时,将调用此回调。
// 从站将向其中写入进程数据。
// KS_readEcatDataSet() 用于检索 DataSet 以访问数据。
//--------------------------------------------------------------------------------------------------------------

KSError __stdcall dataSetCallBack(void* /*pArgs*/, void* /*pContext*/) {
  KSError ksError;
  static bool running = false;

  ksError = KS_readEcatDataSet(
              _pSys->hDataSet,                          // DataSet 句柄
              KSF_NO_FLAGS);                            // 无标记
  if (ksError != KS_OK) {
    if ((running == 0) && (KSERROR_CODE(ksError) == KSERROR_NO_RESPONSE))

      // 该错误表示从站设备没有回答 KS_postEcatDataSet()。
      // 有些从站设备需要更多时间才能完全进入 SAFEOP 并应答 DataSet。
      // 因此,我们在此忽略这个错误。
      // 实际应用中的应用程序应该在启动和运行阶段以不同方式处理这个错误。
      return KS_OK;
    _pSys->error = ksError;
    return ksError;
  }


  //------------------------------------------------------------------------------------------------------------
  // 一旦 KS_readEcatDataSet() 成功返回数据,就说明从站程序已进入 SAFEOP 或更高版本。
  // 从现在起,我们将认真处理每一个错误。
  //------------------------------------------------------------------------------------------------------------

  running = true;


  //------------------------------------------------------------------------------------------------------------
  // 根据bitLength获取数据,然后通过bitOffset向右移位。
  //------------------------------------------------------------------------------------------------------------

  if (_bitLength > 0 && _bitLength <= 8)
    _pSys->data = *reinterpret_cast<byte*>(_pSlaveData) >> _bitOffset;

  if (_bitLength > 8 && _bitLength <= 16)
    _pSys->data = *reinterpret_cast<ushort*>(_pSlaveData) >> _bitOffset;

  if (_bitLength > 24 && _bitLength <= 32)
    _pSys->data = *reinterpret_cast<uint*>(_pSlaveData) >> _bitOffset;


  //------------------------------------------------------------------------------------------------------------
  // 获取想要的数据,并将结果存储在共享内存中,供应用程序访问。
  //------------------------------------------------------------------------------------------------------------

  if (_bitLength < 32)
    _pSys->data &= (1 << _bitLength) - 1;

    _pSys->error = ksError;
    return ksError;
}


//--------------------------------------------------------------------------------------------------------------
// 需要实现 DllMain 函数,该函数在 DLL 加载时不会被执行。
//
// 对于初始化,请定义一个特殊的 init 函数,并在调用 KS_loadKernel()时将其名称作为参数传递给它,或者在加载内核的句柄以后在加载的 DLL 调用函数(如本例所示)时使用,请不要在加载内核时执行的 init 函数,而是在加载内核后自己明确地调用它,并根据需要传递参数,如本例所示。
//--------------------------------------------------------------------------------------------------------------

#define WIN32_LEAN_AND_MEAN
#pragma pack(push, 8)
#include <windows.h>
#pragma pack(pop)

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD reason, LPVOID pReserved) {
  return TRUE;
}
相关推荐
java1234_小锋20 分钟前
MyBatis如何处理延迟加载?
java·开发语言
FeboReigns37 分钟前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns38 分钟前
C++简明教程(10)(初识类)
c语言·开发语言·c++
学前端的小朱39 分钟前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
zh路西法1 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt1 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小1 小时前
C++面试八股文:指针与引用的区别
c++·面试
摇光931 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
沐泽Mu1 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式
沐泽Mu2 小时前
嵌入式学习-QT-Day09
开发语言·qt·学习