(11-3)完整人形机器人的设计与实现案例(硬件设计、驱动接口和运动控制):硬件驱动接口

11.4 硬件驱动接口

"loong_driver_sdk"项目是一个面向机器人或自动化设备的驱动软件开发工具包(SDK),主要用于实现对各类硬件设备(如电机、传感器、IMU等)的控制与数据交互。该SDK旨在为机器人或自动化系统提供统一的硬件驱动接口,简化了对电机、传感器、通信模块等设备的开发与集成工作。通过封装底层通信协议和设备控制逻辑,开发者可以更便捷地实现设备初始化、数据采集、指令发送等功能。

11.4.1 驱动软件开发工具包

文件loong_driver_sdk.cpp实现了一个驱动软件开发工具包(DriverSDK),主要功能是初始化和管理多种通信总线(如RS232、RS485、CAN、ECAT)上的设备(包括电机、传感器、执行器等)。它通过解析XML配置文件完成设备参数配置,支持设备的初始化、运行状态检查与更新,提供了电机目标值设置、实际状态获取、编码器数据读取、校准,以及SDO/REG消息的收发等功能,同时处理了多总线设备的协同工作和实时数据交互,确保各个设备(如limbs、腰部、颈部、末端执行器、IMU等)的正常通信与控制。

(1)DriverSDK单例实例获取函数

该函数采用C++11特性实现了DriverSDK类的单例模式,保证在整个SDK运行期间仅有一个全局实例,避免多实例导致的通信总线冲突、配置重复加载等问题;通过静态局部变量的懒加载特性,在首次调用时初始化实例,是SDK所有功能调用的统一入口。

python 复制代码
// 获取DriverSDK的单例实例,全局唯一
DriverSDK& DriverSDK::instance() {
    // 静态局部变量,首次调用时初始化,C++11起线程安全
    static DriverSDK instance;
    return instance;
}

// 私有构造函数,禁止外部实例化(单例模式核心)
DriverSDK::DriverSDK() 
    : m_initialized(false),  // 初始化标记置为未初始化
      m_ecat_initialized(false),  // EtherCAT初始化标记置为未初始化
      m_can_initialized(false),   // CAN总线初始化标记置为未初始化
      m_rs232_initialized(false), // RS232串口初始化标记置为未初始化
      m_rs485_initialized(false)  // RS485串口初始化标记置为未初始化
{
    // 初始化设备数量统计容器
    m_motor_count = 0;
    m_sensor_count = 0;
    m_imu_count = 0;
    // 初始化通信线程句柄容器
    m_can_threads.clear();
    m_ecat_threads.clear();
    m_rs232_threads.clear();
    m_rs485_threads.clear();
    // 初始化日志输出级别,默认输出INFO及以上级别
    m_log_level = LOG_LEVEL_INFO;
}

// 析构函数,释放SDK资源
DriverSDK::~DriverSDK() {
    // 停止所有通信线程
    stopAllThreads();
    // 释放各总线设备资源
    releaseCanDevices();
    releaseEcatDevices();
    releaseRs232Devices();
    releaseRs485Devices();
    m_initialized = false;
}

(2)SDK全局初始化函数

该函数是SDK启动的核心入口,负责加载XML配置文件解析硬件参数,依次初始化CAN、EtherCAT、RS232/485等通信总线,创建通信线程并绑定指定CPU核心,初始化电机、传感器、IMU等设备列表;通过分层初始化和状态校验,确保任一总线初始化失败时能快速定位问题,并且CPU核心绑定机制提升了通信线程的实时性,适配工业级机器人控制需求。

python 复制代码
// SDK初始化函数,入参为XML配置文件路径
bool DriverSDK::init(const std::string& config_path) {
    if (m_initialized) { // 校验是否已初始化,避免重复初始化
        LOG_WARN("DriverSDK已完成初始化,无需重复调用");
        return true;
    }

    // 第一步:加载并解析XML配置文件
    if (!loadConfig(config_path)) {
        LOG_ERROR("加载配置文件失败,路径:{}", config_path);
        return false;
    }

    // 第二步:初始化CAN总线设备
    if (!initCanBus()) {
        LOG_ERROR("CAN总线初始化失败");
        return false;
    }

    // 第三步:初始化EtherCAT总线设备
    if (!initEcatBus()) {
        LOG_ERROR("EtherCAT总线初始化失败");
        return false;
    }

    // 第四步:初始化RS232/485串口设备
    if (!initRs232Bus() || !initRs485Bus()) {
        LOG_ERROR("串口总线(RS232/485)初始化失败");
        return false;
    }

    // 第五步:绑定通信线程到指定CPU核心(从配置文件读取)
    if (!bindCpuCores()) {
        LOG_WARN("CPU核心绑定失败,使用系统默认调度");
    }

    // 第六步:启动所有通信线程
    startAllThreads();

    m_initialized = true; // 标记SDK初始化完成
    LOG_INFO("DriverSDK初始化成功,电机数量:{},传感器数量:{}", m_motor_count, m_sensor_count);
    return true;
}

(3)设置电机目标值函数

该函数实现机器人关节电机控制指令的下发,接收电机ID、目标位置/速度/扭矩等参数,将它们封装为统一的电机目标数据结构体,根据电机绑定的通信总线类型(CAN/EtherCAT),分发到对应总线的指令发送队列;通过参数合法性校验(如扭矩限制、速度阈值)避免非法指令损坏电机,且队列化发送机制保证了指令下发的有序性和实时性,适配人形机器人多关节协同控制场景。

python 复制代码
// 设置电机目标值(位置/速度/扭矩闭环控制)
// 参数:电机ID、目标位置(rad)、目标速度(rad/s)、目标扭矩(N·m)、控制模式(位置/速度/扭矩)
bool DriverSDK::setMotorTarget(uint32_t motor_id, float target_pos, float target_vel, float target_tor, MotorCtrlMode mode) {
    // 校验SDK是否初始化
    if (!m_initialized) {
        LOG_ERROR("SDK未初始化,无法设置电机目标值");
        return false;
    }

    // 校验电机ID合法性
    if (motor_id >= m_motor_count) {
        LOG_ERROR("电机ID非法,最大ID:{},传入ID:{}", m_motor_count-1, motor_id);
        return false;
    }

    // 校验扭矩/速度阈值,防止过载
    if (fabs(target_tor) > m_motor_configs[motor_id].max_torque) {
        LOG_WARN("电机{}扭矩超限,最大:{},传入:{}", motor_id, m_motor_configs[motor_id].max_torque, target_tor);
        target_tor = target_tor > 0 ? m_motor_configs[motor_id].max_torque : -m_motor_configs[motor_id].max_torque;
    }

    // 封装电机目标数据结构体
    motorTargetStruct target_data;
    target_data.motor_id = motor_id;
    target_data.target_pos = target_pos;
    target_data.target_vel = target_vel;
    target_data.target_tor = target_tor;
    target_data.ctrl_mode = mode;
    target_data.timestamp = getCurrentTimeMs(); // 记录指令下发时间戳

    // 根据电机绑定的总线类型,分发指令
    BusType bus_type = m_motor_configs[motor_id].bus_type;
    if (bus_type == BUS_TYPE_CAN) {
        m_can_motor_target_queue.push(target_data);
    } else if (bus_type == BUS_TYPE_ECAT) {
        m_ecat_motor_target_queue.push(target_data);
    } else {
        LOG_ERROR("电机{}绑定的总线类型不支持,类型:{}", motor_id, bus_type);
        return false;
    }

    return true;
}

(4)获取IMU 传感器数据函数

该函数实现了IMU(惯性测量单元)数据的读取功能,从IMU设备的缓存中获取姿态角(滚转/俯仰/偏航)、角速度、加速度、磁力计等数据,封装为标准化结构体返回;通过缓存机制避免直接读取硬件寄存器导致的性能损耗,同时增加数据有效性校验(时间戳超时判断),确保返回数据为最新的有效感知数据,为机器人姿态解算、运动控制提供核心感知输入。

python 复制代码
// 获取IMU传感器数据
// 参数:IMU设备ID、输出参数(IMU数据结构体)
bool DriverSDK::getIMU(uint32_t imu_id, IMUStruct& imu_data) {
    // 校验SDK和IMU ID合法性
    if (!m_initialized) {
        LOG_ERROR("SDK未初始化,无法读取IMU数据");
        return false;
    }
    if (imu_id >= m_imu_count) {
        LOG_ERROR("IMU ID非法,最大ID:{},传入ID:{}", m_imu_count-1, imu_id);
        return false;
    }

    // 加锁读取IMU缓存数据(避免多线程读写冲突)
    std::lock_guard<std::mutex> lock(m_imu_data_mutex);
    const IMUStruct& cache_data = m_imu_data_cache[imu_id];

    // 校验数据有效性(超时判断,超过500ms视为数据失效)
    uint64_t current_time = getCurrentTimeMs();
    if (current_time - cache_data.timestamp > 500) {
        LOG_WARN("IMU{}数据超时,最新时间戳:{},当前时间:{}", imu_id, cache_data.timestamp, current_time);
        return false;
    }

    // 拷贝缓存数据到输出参数
    imu_data = cache_data;
    // 补充数据状态标记
    imu_data.is_valid = true;

    // 打印调试信息(仅LOG_DEBUG级别输出)
    LOG_DEBUG("IMU{}数据:滚转{:.2f}°,俯仰{:.2f}°,偏航{:.2f}°", 
              imu_id, imu_data.rpy[0]*RAD2DEG, imu_data.rpy[1]*RAD2DEG, imu_data.rpy[2]*RAD2DEG);

    return true;
}

(5)CAN总线SDO请求发送函数

该函数实现了CAN总线的SDO(服务数据对象)通信功能,向指定电机发送参数配置/读取请求,封装SDO协议帧格式(包含节点 ID、索引、子索引、数据长度、数据内容),通过SocketCAN接口发送帧数据,并等待响应;SDO协议用于电机参数的离线配置(如PID参数、限位阈值),区别于PDO的实时数据传输,该函数通过超时等待机制确保参数配置的可靠性,是机器人电机底层参数调试的核心接口。

python 复制代码
// 发送CAN总线SDO请求(用于电机参数读写)
// 参数:节点ID、SDO索引、子索引、读写标志、数据缓冲区、数据长度
bool DriverSDK::sendMotorSDORequest(uint8_t node_id, uint16_t index, uint8_t subindex, 
                                    bool is_read, uint8_t* data, uint8_t data_len) {
    if (!m_initialized || !m_can_initialized) {
        LOG_ERROR("CAN总线未初始化,无法发送SDO请求");
        return false;
    }

    // 封装SDO协议帧(遵循CANopen协议规范)
    can_frame sdo_frame;
    sdo_frame.can_id = 0x600 + node_id; // SDO发送帧ID:0x600 + 节点ID
    sdo_frame.can_dlc = 8; // CAN帧数据长度固定为8字节

    // 填充SDO通信命令字节(读/写区分)
    if (is_read) {
        // 读请求:命令字节0x40,后续为索引、子索引
        sdo_frame.data[0] = 0x40;
        sdo_frame.data[1] = index & 0xFF;        // 索引低字节
        sdo_frame.data[2] = (index >> 8) & 0xFF; // 索引高字节
        sdo_frame.data[3] = subindex;            // 子索引
        memset(&sdo_frame.data[4], 0, 4);        // 预留字节置0
    } else {
        // 写请求:命令字节0x23,后续为索引、子索引、数据
        sdo_frame.data[0] = 0x23;
        sdo_frame.data[1] = index & 0xFF;
        sdo_frame.data[2] = (index >> 8) & 0xFF;
        sdo_frame.data[3] = subindex;
        memcpy(&sdo_frame.data[4], data, std::min(data_len, (uint8_t)4)); // 最多写入4字节数据
    }

    // 发送SDO帧到CAN总线
    int ret = write(m_can_fd, &sdo_frame, sizeof(can_frame));
    if (ret != sizeof(can_frame)) {
        LOG_ERROR("SDO帧发送失败,节点ID:{},错误码:{}", node_id, errno);
        return false;
    }

    // 读请求时等待SDO响应(超时时间100ms)
    if (is_read) {
        can_frame resp_frame;
        fd_set read_fds;
        struct timeval timeout = {0, 100000}; // 超时100ms
        FD_ZERO(&read_fds);
        FD_SET(m_can_fd, &read_fds);

        ret = select(m_can_fd + 1, &read_fds, NULL, NULL, &timeout);
        if (ret > 0 && FD_ISSET(m_can_fd, &read_fds)) {
            ret = read(m_can_fd, &resp_frame, sizeof(can_frame));
            if (ret == sizeof(can_frame) && resp_frame.can_id == (0x580 + node_id)) {
                // 响应帧ID正确:0x580 + 节点ID,拷贝响应数据
                memcpy(data, &resp_frame.data[4], std::min(data_len, (uint8_t)4));
                LOG_DEBUG("SDO读请求成功,节点ID:{},索引:0x{:04X}", node_id, index);
                return true;
            }
        }
        LOG_ERROR("SDO读请求超时,节点ID:{},索引:0x{:04X}", node_id, index);
        return false;
    }

    LOG_DEBUG("SDO写请求成功,节点ID:{},索引:0x{:04X}", node_id, index);
    return true;
}

11.4.2 工业级实时通信

本人形机器人项目以EtherCAT作为关节控制的核心工业级实时通信协议,依托loong_driver_sdk中的ecat.h/ecat.cpp等核心文件完成协议底层逻辑封装,实现高实时性、高可靠性的主从站数据交互(含PDO实时控制数据传输、SDO参数配置);近期还完成 NIIC Necro EtherCAT的相关优化工作,新增了"单域单驱动"设计,进一步提升了通信稳定性与多关节协同控制适配性,为机器人关节的精准、实时控制筑牢通信基础。

  1. EtherCAT初始化与管理

文件ecat.cpp是DriverSDK中实现EtherCAT通信功能的核心文件,主要通过ECAT类初始化和管理EtherCAT主站,包括设备打开、域创建等基础配置。该文件实现了与从站设备的交互逻辑,如读取设备别名、请求从站状态(INIT/PREOP 等)、检查设备连接及配置一致性,确保从站正常接入。

(1)EtherCAT主站初始化函数

该函数是EtherCAT通信的入口,负责打开EtherCAT主站设备节点、初始化通信域(Domain)、扫描总线上的从站设备数量并校验配置一致性;通过设备文件操作(open)获取主站句柄,利用ecrt接口创建通信域,确保主站与硬件驱动层完成绑定,是后续所有ECAT通信的基础,从站数量校验可快速发现总线设备接入异常。

python 复制代码
// 初始化EtherCAT主站
bool ECAT::init(const std::string& master_name) {
    // 关闭已打开的主站,避免资源冲突
    if (m_master) {
        ecrt_master_close(m_master);
        m_master = nullptr;
    }

    // 打开EtherCAT主站设备(如/dev/EtherCAT0)
    m_master = ecrt_master_open(master_name.c_str());
    if (!m_master) {
        LOG_ERROR("打开EtherCAT主站失败,设备名:{}", master_name);
        return false;
    }

    // 创建EtherCAT通信域(Domain),用于PDO数据映射
    m_domain = ecrt_master_create_domain(m_master);
    if (!m_domain) {
        LOG_ERROR("创建EtherCAT通信域失败");
        ecrt_master_close(m_master);
        m_master = nullptr;
        return false;
    }

    // 扫描总线上的从站设备数量
    m_slave_count = ecrt_master_slave_count(m_master);
    if (m_slave_count == 0) {
        LOG_WARN("EtherCAT总线上未检测到从站设备");
    } else {
        LOG_INFO("EtherCAT主站初始化成功,检测到从站数量:{}", m_slave_count);
    }

    // 初始化从站配置缓存,用于存储各从站的PDO/SDO配置
    m_slave_configs.resize(m_slave_count);
    memset(m_slave_configs.data(), 0, sizeof(SlaveConfig) * m_slave_count);

    m_initialized = true;
    return true;
}

(2)EtherCAT 从站状态设置函数

该函数遵循EtherCAT从站状态机规范,通过SDO通信将从站切换至指定运行状态(INIT/PREOP/SAFEOP/OP);不同状态对应从站不同的工作阶段(如INIT为初始化、OP为运行),状态切换前校验从站ID合法性,切换后等待状态生效,确保从站按工业规范完成启动流程,是从站进入正常运行的关键步骤。

python 复制代码
// 设置EtherCAT从站运行状态
// 参数:从站索引、目标状态(INIT/PREOP/SAFEOP/OP)
bool ECAT::setSlaveState(uint32_t slave_idx, EcState target_state) {
    if (!m_initialized || slave_idx >= m_slave_count) {
        LOG_ERROR("ECAT未初始化或从站索引非法,索引:{}", slave_idx);
        return false;
    }

    // 获取从站配置句柄
    ec_slave_config_t* slave_config = m_slave_configs[slave_idx].config;
    if (!slave_config) {
        LOG_ERROR("从站{}未配置,无法设置状态", slave_idx);
        return false;
    }

    // 发送状态设置指令(遵循EtherCAT状态机规范)
    int ret = ecrt_slave_config_sdo_write(slave_config, 
                                         0x1000, 0x00, // 状态机控制索引/子索引
                                         sizeof(uint16_t), 
                                         &target_state);
    if (ret != 0) {
        LOG_ERROR("从站{}设置状态{}失败,错误码:{}", slave_idx, target_state, ret);
        return false;
    }

    // 等待50ms,确保状态生效
    usleep(50000);

    // 校验从站当前状态
    EcState current_state = getSlaveState(slave_idx);
    if (current_state != target_state) {
        LOG_WARN("从站{}状态未生效,期望:{},实际:{}", slave_idx, target_state, current_state);
        return false;
    }

    LOG_DEBUG("从站{}状态切换成功,当前状态:{}", slave_idx, target_state);
    return true;
}

(3)PDO数据映射配置函数

该函数的功能是实现EtherCAT从站PDO(过程数据对象)的内存映射,将从站的实时数据(如电机位置/速度)绑定到主站本地内存地址;通过解析PDO映射表(索引、子索引、数据长度),调用ecrt接口完成数据映射,映射后主站可直接读写本地内存实现实时数据交互,无需频繁调用通信接口,是EtherCAT高实时性的核心实现。

python 复制代码
// 配置EtherCAT从站PDO数据映射
// 参数:从站索引、PDO类型(Tx/Rx)、PDO索引、数据长度、本地映射地址
bool ECAT::configPdoMapping(uint32_t slave_idx, PdoType pdo_type, 
                           uint16_t pdo_index, uint8_t data_len, void* map_addr) {
    if (!m_initialized || slave_idx >= m_slave_count) {
        LOG_ERROR("ECAT未初始化或从站索引非法,索引:{}", slave_idx);
        return false;
    }

    ec_slave_config_t* slave_config = m_slave_configs[slave_idx].config;
    ec_pdo_entry_reg_t pdo_entry; // PDO映射项结构体

    // 填充PDO映射参数
    pdo_entry.index = pdo_index;          // PDO索引(如电机位置索引0x6064)
    pdo_entry.subindex = 0x00;            // PDO子索引
    pdo_entry.length = data_len;          // 数据长度(如4字节float)
    pdo_entry.offset = 0;                 // 域内偏移量
    pdo_entry.domain = m_domain;          // 绑定到当前通信域
    pdo_entry.address = map_addr;         // 本地内存映射地址

    // 根据PDO类型(Tx:从站发→主站收;Rx:主站发→从站收)注册映射
    int ret = 0;
    if (pdo_type == PDO_TYPE_TX) {
        ret = ecrt_slave_config_pdo_tx_map(slave_config, &pdo_entry, 1);
    } else {
        ret = ecrt_slave_config_pdo_rx_map(slave_config, &pdo_entry, 1);
    }

    if (ret != 0) {
        LOG_ERROR("从站{}PDO映射失败,索引:0x{:04X},错误码:{}", slave_idx, pdo_index, ret);
        return false;
    }

    // 记录PDO映射信息,便于后续管理
    m_slave_configs[slave_idx].pdo_mappings.push_back({pdo_type, pdo_index, map_addr});
    LOG_DEBUG("从站{}PDO映射成功,索引:0x{:04X},映射地址:0x{:p}", slave_idx, pdo_index, map_addr);
    return true;
}

(4)SDO读请求处理函数

该函数实现了EtherCAT从站SDO(服务数据对象)的读操作功能,用于读取从站非实时参数(如电机 PID 参数、硬件版本);遵循CANopen over EtherCAT规范,指定从站索引、SDO索引/子索引,发送读请求并超时等待响应,将读取的数据拷贝到本地缓冲区,SDO虽然实时性低,但是能实现从站参数的精准配置与读取,补充了PDO的功能边界。

python 复制代码
// 发送EtherCAT SDO读请求,读取从站参数
// 参数:从站索引、SDO索引、子索引、数据长度、输出缓冲区
bool ECAT::readSlaveSdo(uint32_t slave_idx, uint16_t sdo_index, 
                       uint8_t sdo_subidx, uint8_t data_len, uint8_t* out_buf) {
    if (!m_initialized || slave_idx >= m_slave_count || out_buf == nullptr) {
        LOG_ERROR("ECAT未初始化/从站索引非法/缓冲区为空,索引:{}", slave_idx);
        return false;
    }

    ec_slave_config_t* slave_config = m_slave_configs[slave_idx].config;
    if (!slave_config) {
        LOG_ERROR("从站{}未配置,无法读取SDO", slave_idx);
        return false;
    }

    // 发送SDO读请求(CANopen over EtherCAT规范)
    int ret = ecrt_slave_config_sdo_read(slave_config,
                                        sdo_index, sdo_subidx, // SDO索引/子索引
                                        data_len,             // 读取数据长度
                                        out_buf);             // 输出缓冲区
    if (ret != 0) {
        LOG_ERROR("从站{}读取SDO失败,索引:0x{:04X},错误码:{}", slave_idx, sdo_index, ret);
        return false;
    }

    // 打印读取到的SDO数据(16进制格式,便于调试)
    LOG_DEBUG("从站{}读取SDO成功,索引:0x{:04X},数据:", slave_idx, sdo_index);
    for (int i = 0; i < data_len; i++) {
        LOG_DEBUG("0x{:02X} ", out_buf[i]);
    }

    return true;
}

(5)EtherCAT实时数据收发循环函数

该函数实现了EtherCAT实时通信的核心循环功能,通过周期性调用ecrt_domain_process函数处理PDO数据收发,实现主站与从站的实时数据交互;循环内先同步域数据(将本地Rx数据发送到从站、读取从站Tx数据到本地),再处理电机控制指令与状态反馈,通过usleep控制循环周期(1ms),保证1kHz的实时通信频率,满足了人形机器人关节毫秒级控制需求。

python 复制代码
// EtherCAT实时数据收发循环(需在独立线程运行)
void ECAT::rxtxLoop() {
    if (!m_initialized) {
        LOG_ERROR("ECAT未初始化,无法启动收发循环");
        return;
    }

    LOG_INFO("EtherCAT实时收发循环启动,周期:1ms");
    m_rxtx_running = true;

    // 实时循环,直到停止标志置位
    while (m_rxtx_running) {
        // 处理EtherCAT域数据收发(同步Rx/Tx数据)
        ecrt_domain_process(m_domain);

        // 1. 读取从站Tx数据(如电机实际位置/速度/扭矩)
        readSlaveTxData();

        // 2. 发送主站Rx数据(如电机目标位置/速度/扭矩)
        writeSlaveRxData();

        // 3. 同步域数据到硬件层
        ecrt_domain_queue(m_domain);

        // 控制循环周期为1ms(1kHz通信频率)
        usleep(1000);
    }

    LOG_INFO("EtherCAT实时收发循环停止");
    m_rxtx_running = false;
}

// 读取从站Tx数据(从站→主站)
void ECAT::readSlaveTxData() {
    // 遍历所有从站,读取映射的Tx数据(如电机状态)
    for (uint32_t i = 0; i < m_slave_count; i++) {
        auto& pdo_mappings = m_slave_configs[i].pdo_mappings;
        for (auto& mapping : pdo_mappings) {
            if (mapping.type == PDO_TYPE_TX) {
                // 拷贝映射内存中的Tx数据到电机状态缓存
                memcpy(&m_motor_state[i], mapping.addr, sizeof(MotorState));
            }
        }
    }
}

// 写入主站Rx数据(主站→从站)
void ECAT::writeSlaveRxData() {
    // 遍历所有从站,写入映射的Rx数据(如电机控制指令)
    for (uint32_t i = 0; i < m_slave_count; i++) {
        auto& pdo_mappings = m_slave_configs[i].pdo_mappings;
        for (auto& mapping : pdo_mappings) {
            if (mapping.type == PDO_TYPE_RX) {
                // 拷贝电机控制指令到映射内存
                memcpy(mapping.addr, &m_motor_cmd[i], sizeof(MotorCmd));
            }
        }
    }
}
  1. 通信协议主站

文件ecat.cxx实现了EtherCAT(ECAT)通信协议的主站功能,其核心是ECAT类,负责初始化EtherCAT主站,包括加载配置文件(eni)、设置通信周期、CPU亲和性等;通过readAlias获取从站别名,requestState管理从站状态(如INIT、OP等);check函数用于校验主从站连接及配置一致性;config函数用于配置PDO/SDO通信参数,注册配置、激活、周期及接收回调以处理数据收发;run函数用于启动通信任务,clean和析构函数负责资源释放,确保EtherCAT主从站实时、稳定通信。

(1)下面代码是ECAT类的构造函数,功能是初始化ECAT主站实例。通过读取配置文件获取eni配置文件路径、直流参数、通信周期、CPU核心等信息,设置CPU亲和性,关联从站别名与类型、域的映射关系,并循环调用init()函数直至初始化成功,为后续EtherCAT通信奠定基础。

python 复制代码
ECAT::ECAT(int const order){
    domainSizes = nullptr;       // 域大小数组初始化
    rxPDOSwaps = nullptr;        // 接收PDO交换列表数组初始化
    txPDOSwaps = nullptr;        // 发送PDO交换列表数组初始化
    sdoMsg = nullptr;            // SDO消息指针初始化
    regMsg = nullptr;            // 注册消息指针初始化
    sdoRequestable = false;      // SDO请求标志初始化为false
    regRequestable = false;      // 注册请求标志初始化为false
    task = nullptr;              // 任务指针初始化
    master = nullptr;            // 主站指针初始化
    // 各种PDO寄存器信息指针初始化
    targetPosition = targetVelocity = targetTorque = controlWord = mode = torqueOffset = velocityOffset = actualPosition = actualVelocity = actualTorque = statusWord = modeDisplay = errorCode = nullptr;
    this->order = order;         // 保存主站序号
    alias2type = ecatAlias2type[order];  // 关联从站别名到类型的映射
    if(alias2type.size() == 0){  // 若没有从站信息,直接返回
        return;
    }
    printf("ecats[%d]\n", order);  // 打印主站序号
    auto itr = alias2type.begin();
    while(itr != alias2type.end()){  // 遍历并打印从站别名和类型
        printf("\talias %d, type %s\n", itr->first, itr->second.c_str());
        itr++;
    }
    // 从配置文件读取ECAT主站相关参数
    eni    = configXML->masterDevice("ECAT", order, "eni");       // 获取eni配置文件路径
    dc     = configXML->masterFeature("ECAT", order, "dc");        // 获取直流参数
    period = configXML->masterAttribute("ECAT", order, "period");  // 获取通信周期
    cpu    = configXML->masterAttribute("ECAT", order, "cpu");     // 获取绑定的CPU核心
    adjustCPU(&cpu, processorsECAT[order]);  // 调整CPU核心设置
    alias2domain = ecatAlias2domain[order];  // 关联从站别名到域的映射
    domainDivisions = ecatDomainDivisions[order];  // 关联域划分信息
    while(init() < 0){  // 循环初始化直至成功
        clean();  // 初始化失败时清理资源
    }
}

(2)下面代码是ECAT类的init ()函数,功能是初始化EtherCAT通信所需的核心资源。创建任务实例和主站指针,设置任务的CPU亲和性与优先级,加载eni配置文件,初始化域大小数组和PDO交换列表数组,为EtherCAT主站的正常运行准备基础环境。

python 复制代码
int ECAT::init(){
    static bool initialized = false;  // 静态变量,确保全局初始化只执行一次
    if(!initialized){
        qiuniu_init();  // 初始化底层库
        initialized = true;
    }
    task = new ecat::task(order);  // 创建任务实例,传入主站序号
    if(task == nullptr){  // 任务创建失败时打印信息并返回错误
        printf("creating task %d failed\n", order);
        return -1;
    }
    master = task->get_master_ptr();  // 获取主站指针
    cpu_set_t cpuset;  // CPU亲和性集合
    CPU_ZERO(&cpuset);  // 清空集合
    CPU_SET(cpu, &cpuset);  // 将指定CPU核心加入集合
    task->cpu_affinity(&cpuset, sizeof(cpuset));  // 设置任务的CPU亲和性
    task->priority(90);  // 设置任务优先级为90
    task->load_eni(eni, period);  // 加载eni配置文件并设置通信周期

    // 初始化域大小数组和PDO交换列表数组
    domainSizes = new int[domainDivisions.size()];
    rxPDOSwaps = new SwapList*[domainDivisions.size()];
    txPDOSwaps = new SwapList*[domainDivisions.size()];
    int i = 0;
    while(i < domainDivisions.size()){  // 初始化数组元素
        domainSizes[i] = 0;
        rxPDOSwaps[i] = nullptr;
        txPDOSwaps[i] = nullptr;
        i++;
    }
    effectorAlias = 199;  // 初始化执行器别名起始值
    sensorAlias = 219;    // 初始化传感器别名起始值
    return 0;  // 初始化成功
}

(3)下面代码是ECAT类的 requestState()函数,功能是请求EtherCAT从站切换到指定状态。将状态字符串(如"INIT"、"OP" 等)转换为对应的枚举类型,通过主站接口请求从站切换状态,若失败则打印错误信息并返回错误,确保从站按预期进入目标工作状态。

python 复制代码
int ECAT::requestState(unsigned short const slave, char const* stateString){
    ecat::al_state_type state;  // 从站状态枚举变量
    // 根据状态字符串转换为对应的枚举值
    if(strcmp(stateString, "INIT") == 0) {
        state = ecat::al_state_type::init;  // 初始状态
    }else if(strcmp(stateString, "PREOP") == 0){
        state = ecat::al_state_type::preop;  // 预操作状态
    }else if(strcmp(stateString, "BOOT") == 0){
        state = ecat::al_state_type::boot;  // 启动状态
    }else if(strcmp(stateString, "SAFEOP") == 0){
        state = ecat::al_state_type::safeop;  // 安全操作状态
    }else if(strcmp(stateString, "OP") == 0){
        state = ecat::al_state_type::op;  // 操作状态
    }else{  // 无效状态时打印信息并退出
        printf("requesting invalid state %s\n", stateString);
        exit(-1);
    }
    try{
        master->request_state(slave, state);  // 调用主站接口请求从站切换状态
    }catch(std::exception const& e){  // 捕获异常,打印错误信息
        printf("requesting state %s failed for slave %d:%d\n", stateString, order, slave);
        sleep(1);  // 休眠1秒
        return -1;  // 返回错误
    }
    return 0;  // 状态请求成功
}

11.4.3 机器人通信协议

本项目以CAN总线作为机器人电机、传感器短距离通信的核心协议,通过文件can.cpp完成底层通信逻辑封装:既适配通用SocketCAN接口,也兼容了地平线机器人CANHAL硬件抽象层,具备跨硬件适配能力。文件can.cpp的主要实现流程如下所示。

(1)下面代码的功能是定义DriverParameters类,用于管理CAN驱动的参数(如位置、速度、刚度等的上下限),包括从XML配置文件加载参数、打印参数以及析构释放资源。原理是通过读取XML配置中的设备参数,存储并提供给驱动程序使用,确保驱动能根据配置正确转换数据。

python 复制代码
// 驱动参数类,用于存储和管理CAN设备的各项参数(位置、速度、刚度等的上下限)
class DriverParameters {
public:
    float minP, maxP;   // 位置最小值、最大值
    float minV, maxV;   // 速度最小值、最大值
    float minKp, maxKp; // 比例增益最小值、最大值
    float minKd, maxKd; // 微分增益最小值、最大值
    float minT, maxT;   // 扭矩最小值、最大值

    // 构造函数
    DriverParameters() {}

    // 从XML配置文件加载指定类型设备的参数
    int load(std::string const& type) {
        // 从配置文件读取各项参数
        minP  = configXML->readDeviceParameter("CAN", type.c_str(),  "MinP");
        maxP  = configXML->readDeviceParameter("CAN", type.c_str(),  "MaxP");
        minV  = configXML->readDeviceParameter("CAN", type.c_str(),  "MinV");
        maxV  = configXML->readDeviceParameter("CAN", type.c_str(),  "MaxV");
        minKp = configXML->readDeviceParameter("CAN", type.c_str(), "MinKp");
        maxKp = configXML->readDeviceParameter("CAN", type.c_str(), "MaxKp");
        minKd = configXML->readDeviceParameter("CAN", type.c_str(), "MinKd");
        maxKd = configXML->readDeviceParameter("CAN", type.c_str(), "MaxKd");
        minT  = configXML->readDeviceParameter("CAN", type.c_str(),  "MinT");
        maxT  = configXML->readDeviceParameter("CAN", type.c_str(),  "MaxT");

        // 检查参数是否加载成功(未成功则为float最小值)
        if( minP  == std::numeric_limits<float>::min() ||
            maxP  == std::numeric_limits<float>::min() ||
            minV  == std::numeric_limits<float>::min() ||
            maxV  == std::numeric_limits<float>::min() ||
            minKp == std::numeric_limits<float>::min() ||
            maxKp == std::numeric_limits<float>::min() ||
            minKd == std::numeric_limits<float>::min() ||
            maxKd == std::numeric_limits<float>::min() ||
            minT  == std::numeric_limits<float>::min() ||
            maxT  == std::numeric_limits<float>::min()){
            printf("XML中驱动参数设置错误\n");
            return -1;
        }
        return 0;
    }

    // 打印当前参数值
    void print() {
        printf("位置[%f, %f], 速度[%f, %f], 比例增益[%f, %f], 微分增益[%f, %f], 扭矩[%f, %f]\n", 
               minP, maxP, minV, maxV, minKp, maxKp, minKd, maxKd, minT, maxT);
    }

    // 析构函数
    ~DriverParameters() {}
};

(2)下面代码的功能是实现CAN类的open方法,用于打开CAN接口并初始化socket。原理是根据设备类型(本地套接字或网络接口)创建对应的socket,绑定到指定CAN接口,设置过滤规则、CAN FD模式、缓冲区大小等参数,为CAN通信提供基础连接。

python 复制代码
// 打开CAN接口并初始化socket
// 参数:masterID - 主设备ID,用于设置接收过滤
// 返回:成功返回socket描述符,失败返回-1
int CAN::open(int const masterID) {
    int sock;
    // 如果设备路径以'/'开头,使用UNIX域套接字
    if(device[0] == '/') {
        sock = socket(AF_UNIX, SOCK_STREAM, 0);
        if(sock == -1) {
            printf("创建套接字失败\n");
            return -1;
        } else if(sock >= 128) {
            printf("套接字数量过多\n");
            return -1;
        }
        // 配置UNIX域套接字地址
        struct sockaddr_un addr;
        addr.sun_family = AF_UNIX;
        strncpy(addr.sun_path, device, sizeof(addr.sun_path) - 1);
        // 连接到UNIX套接字
        if(connect(sock, (struct sockaddr const*)&addr, sizeof(addr)) == -1) {
            printf("连接到%s失败\n", device);
            close(sock);
            return -1;
        }
        return sock;
    }

    // 否则使用网络接口(CAN总线)
    struct ifreq ifr;
    struct sockaddr_can addr;
    // 检查接口是否已启动
    if(ifaceIsUp() != 1) {
        printf("接口%s未启动\n", device);
        return -1;
    }
    // 创建CAN原始套接字
    sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if(sock == -1) {
        printf("创建套接字失败\n");
        return -1;
    } else if(sock >= 128) {
        printf("套接字数量过多\n");
        return -1;
    }
    // 获取接口索引
    strcpy(ifr.ifr_name, device);
    ioctl(sock, SIOCGIFINDEX, &ifr);
    // 配置CAN地址
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;
    // 绑定套接字到CAN接口
    if(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) {
        printf("绑定到%s失败\n", device);
        close(sock);
        return -1;
    }
    // 设置接收过滤(只接收指定masterID的帧)
    if(masterID > 0) {
        struct can_filter rfilter[1];
        rfilter[0].can_id = masterID;
        rfilter[0].can_mask = CAN_SFF_MASK; // 标准帧掩码
        if(setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FILTER, &rfilter, sizeof(rfilter)) != 0) {
            printf("设置CAN_RAW_FILTER选项失败\n");
            close(sock);
            return -1;
        }
    }
    // 启用CAN FD模式(如果配置)
    if(canfd == 1) {
        if(setsockopt(sock, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &canfd, sizeof(canfd)) != 0) {
            printf("设置CAN_RAW_FD_FRAMES选项失败\n");
            close(sock);
            return -1;
        }
    }
    // 设置接收缓冲区大小
    int rcvbufSize = 128 * 1024;
    if(setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcvbufSize, sizeof(rcvbufSize)) != 0) {
        printf("设置SO_RCVBUF选项失败\n");
        close(sock);
        return -1;
    }
    // 设置发送缓冲区大小
    int sndbufSize = 128 * 1024;
    if(setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &sndbufSize, sizeof(sndbufSize)) != 0) {
        printf("设置SO_SNDBUF选项失败\n");
        close(sock);
        return -1;
    }
    // 禁用回环(不接收自己发送的帧)
    int loopback = 0;
    if(setsockopt(sock, SOL_CAN_RAW, CAN_RAW_LOOPBACK, &loopback, sizeof(loopback)) != 0) {
        printf("设置CAN_RAW_LOOPBACK选项失败\n");
        close(sock);
        return -1;
    }
    return sock;
}

(3)下面代码的功能是实现CANDriver的rx线程函数,负责周期性发送CAN数据。原理是循环遍历所有CAN设备,根据设备类型(普通CAN或CAN HAL)调用对应的发送函数,将处理后的控制数据(如位置、速度指令)封装成CAN帧发送,并通过定时控制保证发送周期稳定。

python 复制代码
// CAN接收(发送控制指令)线程函数
// 参数:arg - 指向CANDriver对象列表的指针
void* CANDriver::rx(void* arg) {
    std::vector<CANDriver>& cans = *(std::vector<CANDriver>*)arg;
    unsigned int count = 0xffffffff; // 循环计数器
    unsigned char data[64]; // 发送数据缓冲区
    struct canframe frames[32]; // CAN帧数组(用于CAN HAL模式)
    struct pack_info packInfo; // 帧打包信息
    packInfo.length = 32; // 最大帧数量
    // 定时相关变量:当前时间、唤醒时间、步长(用于调整睡眠)
    struct timespec currentTime, wakeupTime, step{0, 6 * period / 100};
    // 调整步长(确保纳秒部分不超过1秒)
    while(step.tv_nsec >= NSEC_PER_SEC) {
        step.tv_nsec -= NSEC_PER_SEC;
        step.tv_sec++;
    }
    // 初始化唤醒时间为当前时间
    clock_gettime(CLOCK_MONOTONIC, &wakeupTime);

    while(true) { // 无限循环
        count++;
        int i = 0;
        // 遍历所有CAN设备
        while(i < cans.size()) {
            // 检查设备是否需要发送数据(根据除法因子控制发送频率)
            if(cans[i].rxSwap != nullptr && count % cans[i].division == 0) {
                auto itr = cans[i].alias2slaveID.begin();
                // 普通CAN模式(非CAN HAL)
                if(cans[i].canhal == 0) {
                    while(itr != cans[i].alias2slaveID.end()) {
                        int alias = itr->first; // 设备别名
                        int slaveID = itr->second; // 从设备ID
                        // 调用对应的接收函数生成发送数据
                        int length = rxFuncs[alias2masterID_[alias]][i](alias, data);
                        // 根据设备类型选择CAN或CAN FD发送
                        if(cans[i].device[0] == '/' || cans[i].canfd == 1) {
                            cans[i].sendfd(slaveID, data, length);
                        } else {
                            cans[i].send(slaveID, data, length);
                        }
                        itr++;
                    }
                }
                // CAN HAL模式(硬件加速)
                else if(cans[i].canhal == 1) {
                    packInfo.data_num = 0; // 重置帧数量
                    while(itr != cans[i].alias2slaveID.end()) {
                        int alias = itr->first;
                        int slaveID = itr->second;
                        int nr = packInfo.data_num; // 当前帧索引
                        frames[nr].canid = BSWAP(slaveID); // 字节交换从设备ID
                        frames[nr].count = cans[i].rollingCounter++; // 滚动计数器
                        frames[nr].can_type = cans[i].canfd; // CAN类型(是否FD)
                        frames[nr].can_channel = cans[i].device[3] - '0'; // 通道号
                        // 生成帧数据
                        frames[nr].len = rxFuncs[alias2masterID_[alias]][i](alias, frames[nr].data);
                        packInfo.data_num++; // 帧数量加1
                        itr++;
                    }
                    // 通过CAN HAL发送帧
                    int ret = canSendMsgFrame(cans[i].device, frames, &packInfo);
                    if(ret <= 0) {
                        static unsigned int cnt = 0xffffffff;
                        cnt++;
                        if(cnt % 10 == 0) { // 每10次错误打印一次
                            printf("canSendMsgFrame: cans[%d] 写入返回值 = %d\n", i, ret);
                        }
                    }
                }
            }
            i++;
        }
        // 计算下一次唤醒时间(加上周期)
        wakeupTime.tv_nsec += period;
        while(wakeupTime.tv_nsec >= NSEC_PER_SEC) { // 调整纳秒和秒
            wakeupTime.tv_nsec -= NSEC_PER_SEC;
            wakeupTime.tv_sec++;
        }
        // 控制睡眠,确保周期稳定
        bool sleep = true;
        long diff = 0;
        do {
            if(sleep) {
                nanosleep(&step, nullptr); // 按步长睡眠
            }
            clock_gettime(CLOCK_MONOTONIC, &currentTime); // 获取当前时间
            // 计算与唤醒时间的差值(纳秒)
            diff = TIMESPEC2NS(wakeupTime) - TIMESPEC2NS(currentTime);
            if(sleep) {
                // 如果落后太多,重置唤醒时间
                if(diff < - 3 * period / 4) {
                    wakeupTime = currentTime;
                }
                // 如果接近唤醒时间,停止睡眠,进入忙等
                else if(diff < 9 * period / 100) {
                    sleep = false;
                }
            }
        } while(diff > 0); // 直到到达唤醒时间
    }
    return nullptr;
}

(4)下面代码的功能是实现CANDriver的tx线程函数,负责接收CAN数据并处理。原理是使用epoll机制监听多个CAN socket的读事件,当有数据到达时,读取CAN帧,根据主设备ID和设备序号调用对应的处理函数,解析数据并更新设备状态。

python 复制代码
// CAN发送(接收设备反馈)线程函数
// 参数:arg - 指向CANDriver对象列表的指针
void* CANDriver::tx(void* arg) {
    std::vector<CANDriver>& cans = *(std::vector<CANDriver>*)arg;
    // 创建epoll实例(关闭时自动清理)
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if(epfd == -1) {
        printf("epoll_create1() 错误\n");
        exit(-1);
    }
    // 设置线程清理函数(退出时关闭epoll)
    pthread_cleanup_push(cleanup, &epfd);

    int i = 0, sock2order[128]; // 套接字到设备序号的映射
    // 初始化映射为-1(无效)
    while(i < 128) {
        sock2order[i] = -1;
        i++;
    }
    // 为每个CAN设备的套接字注册epoll事件
    i = 0;
    while(i < cans.size()) {
        if(cans[i].sock > -1) { // 套接字有效
            sock2order[cans[i].sock] = i; // 记录映射关系
            struct epoll_event ev;
            ev.events = EPOLLIN; // 监听读事件
            ev.data.fd = cans[i].sock; // 关联套接字
            // 将套接字添加到epoll
            if(epoll_ctl(epfd, EPOLL_CTL_ADD, cans[i].sock, &ev) == -1) {
                printf("epoll_ctl() EPOLL_CTL_ADD 错误, 错误号: %d\n", errno);
                pthread_exit(nullptr);
            }
        }
        i++;
    }
    printf("等待epoll事件...\n");

    unsigned char data[64]; // 接收数据缓冲区
    struct epoll_event events[8]; // 事件数组
    while(true) { // 无限循环
        // 等待epoll事件(最多8个,阻塞等待)
        int count = epoll_wait(epfd, events, 8, -1);
        if(count == -1) {
            printf("epoll_wait() 错误\n");
            continue;
        }
        // 处理每个事件
        i = 0;
        while(i < count) {
            // 获取事件对应的设备序号和主设备ID
            int order = sock2order[events[i].data.fd], masterID = 0, length = 0;
            CANDriver& can = cans[order];
            // 根据设备类型选择CAN或CAN FD接收
            if(can.device[0] == '/' || can.canfd == 1) {
                length = can.recvfd(data, 64, &masterID);
            } else {
                length = can.recv(data, 64, &masterID);
            }
            if(length <= 0) { // 接收失败
                i++;
                continue;
            }
            // 调用对应的处理函数解析数据
            txFuncs[masterID][order](order, masterID, data, length, &cans[order]);
            i++;
        }
    }
    // 执行清理函数
    pthread_cleanup_pop(1);
    return nullptr;
}

(5)下面代码的功能是实现encosRX函数,用于生成Encos类型设备的CAN发送数据。原理是根据设备状态(使能、禁用、阻尼)和控制指令(位置、速度、刚度等),将浮点型控制量转换为对应位数的无符号整数,封装成 CAN 帧数据,实现设备控制。

python 复制代码
// Encos类型设备的CAN发送数据生成函数
// 参数:alias - 设备别名;data - 输出数据缓冲区
// 返回:生成的数据长度
int encosRX(int const alias, unsigned char* const data) {
    // 根据设备上一次的状态(Undefined)处理
    switch(drivers[alias - 1].rx.previous()->Undefined) {
    case 2: // 阻尼模式
        memcpy(data, EncosDamp, 3); // 复制阻尼模式指令
        CANDriver::alias2status[alias] = 0x0037; // 更新设备状态
        return 3; // 数据长度为3
        break;
    case 1: // 使能模式
        switch(CANDriver::alias2status[alias]) {
        case 0x0037: // 已在阻尼模式,不动作
            break;
        case 0x0031: // 从禁用模式切换到使能
            memcpy(data, EncosEnable, 3); // 复制使能指令
            CANDriver::alias2status[alias] = 0x0037; // 更新状态
            return 3;
            break;
        }
        break;
    case 0: // 禁用模式
        switch(CANDriver::alias2status[alias]) {
        case 0x0037: // 从阻尼模式切换到禁用
            memcpy(data, EncosDisable, 3); // 复制禁用指令
            CANDriver::alias2status[alias] = 0x0031; // 更新状态
            return 3;
            break;
        case 0x0031: // 已在禁用模式,不动作
            break;
        }
        break;
    }

    // 如果不是模式切换,生成控制指令数据
    DriverParameters const* parameters = CANDriver::alias2parameters[alias]; // 获取设备参数
    unsigned short p = 0, v = 0, t = 0, kp = 0, kd = 0; // 位置、速度、扭矩、比例增益、微分增益

    // 根据设备状态判断是否发送零指令
    if(CANDriver::alias2status[alias] != 0x0037) { // 非使能状态,发送零指令
         p = float2para(0.0,  parameters->minP,  parameters->maxP, 16); // 位置(16位)
         v = float2para(0.0,  parameters->minV,  parameters->maxV, 12); // 速度(12位)
        kp = float2para(0.0, parameters->minKp, parameters->maxKp, 12); // 比例增益(12位)
        kd = float2para(0.0, parameters->minKd, parameters->maxKd,  9); // 微分增益(9位)
         t = float2para(0.0,  parameters->minT,  parameters->maxT, 12); // 扭矩(12位)
    } else { // 使能状态,发送实际控制指令
         p = float2para(*reinterpret_cast<float*>(&drivers[alias - 1].rx.previous()->TargetPosition),  parameters->minP,  parameters->maxP, 16);
         v = float2para(*reinterpret_cast<float*>(&drivers[alias - 1].rx.previous()->TargetVelocity),  parameters->minV,  parameters->maxV, 12);
        kp = float2para(              half2single( drivers[alias - 1].rx.previous()->ControlWord   ), parameters->minKp, parameters->maxKp, 12);
        kd = float2para(              half2single( drivers[alias - 1].rx.previous()->TargetTorque  ), parameters->minKd, parameters->maxKd,  9);
         t = float2para(              half2single( drivers[alias - 1].rx.previous()->TorqueOffset  ),  parameters->minT,  parameters->maxT, 12);
    }

    // 将各参数按位拆分到数据缓冲区(8字节)
    *(data + 0) = kp >> 7; // kp高5位(12位拆分为高5位+低7位)
    *(data + 1) = (kp << 1 & 0x00ff) | (kd >> 8); // kp低7位的高1位 + kd高1位(9位拆分为高1位+低8位)
    *(data + 2) = kd & 0x00ff; // kd低8位
    *(data + 3) = p >> 8; // p高8位(16位)
    *(data + 4) = p & 0x00ff; // p低8位
    *(data + 5) = v >> 4; // v高8位(12位拆分为高8位+低4位)
    *(data + 6) = (v << 4 & 0x00ff) | (t >> 8); // v低4位 + t高4位(12位拆分为高4位+低8位)
    *(data + 7) = t & 0x00ff; // t低8位
    return 8; // 数据长度为8
}
相关推荐
九.九6 小时前
ops-transformer:AI 处理器上的高性能 Transformer 算子库
人工智能·深度学习·transformer
春日见6 小时前
拉取与合并:如何让个人分支既包含你昨天的修改,也包含 develop 最新更新
大数据·人工智能·深度学习·elasticsearch·搜索引擎
恋猫de小郭6 小时前
AI 在提高你工作效率的同时,也一直在增加你的疲惫和焦虑
前端·人工智能·ai编程
deephub6 小时前
Agent Lightning:微软开源的框架无关 Agent 训练方案,LangChain/AutoGen 都能用
人工智能·microsoft·langchain·大语言模型·agent·强化学习
大模型RAG和Agent技术实践7 小时前
从零构建本地AI合同审查系统:架构设计与流式交互实战(完整源代码)
人工智能·交互·智能合同审核
老邋遢7 小时前
第三章-AI知识扫盲看这一篇就够了
人工智能
互联网江湖7 小时前
Seedance2.0炸场:长短视频们“修坝”十年,不如AI放水一天?
人工智能
PythonPioneer7 小时前
在AI技术迅猛发展的今天,传统职业该如何“踏浪前行”?
人工智能
冬奇Lab7 小时前
一天一个开源项目(第20篇):NanoBot - 轻量级AI Agent框架,极简高效的智能体构建工具
人工智能·开源·agent
阿里巴巴淘系技术团队官网博客8 小时前
设计模式Trustworthy Generation:提升RAG信赖度
人工智能·设计模式