Files
agv-control-slam/.claude/skills/curtis-motor-protocol.md
2025-11-15 16:05:59 +08:00

30 KiB
Raw Permalink Blame History

柯蒂斯电机控制器 CAN 通讯协议

概述

本技能文档提供柯蒂斯Curtis1298+1220C 电机控制器 CAN 通讯协议参考信息及完整的通讯实现案例。

协议名称: Can 通讯协议1298+1220C 版本: V4.0 公司: 上海诺力智能科技有限公司

控制器组件:

  • Curtis 1298 软件: Noblift_PS20V_Agv_9820C_191125_12_0_P209_V403_1.exe
  • Curtis 1220C 参数: 1220C-2201_20191106.CPF
  • Rema 手柄通讯: 09-01-14 CAN Protocol Specification

功能说明:

  • 车辆行走、升降、转向均通过 CAN 总线指令进行控制
  • 支持手动驾驶和自动驾驶模式切换
  • 与 USB-CAN 接口配套使用(参见 can-protocol 技能)

CAN 总线设置

基本参数

波特率125 kbps
Timing0: 0x03
Timing1: 0x1C

CAN ID 分配

Curtis 控制器发送MISO

  • 0x260PDO1_MISO - 状态反馈数据
  • 0x360PDO2_MISO - 保留
  • 发送频率: 20ms ± 5ms

AGV 控制器发送MOSI

  • 0x1E5PDO1_MOSI - 控制命令(行走、提升)
  • 0x2E5PDO1_MOSI - 转向角度命令

注意: REMA CAN 手柄使用 ID 0x1E0

驾驶模式切换条件

手动驾驶模式:

  • 手柄下的接近开关 J9/SW_3 必须闭合ON才能有效

自动驾驶模式:

  • J24/SW_1 = ON 为自动驾驶模式有效

通讯超时:

  • "AGV 通讯超时"参数可在诺力设置中配置
  • Curtis 监控 AGV 的 CAN 帧是否在线
  • 重要: 必须以 ≤20ms 周期持续发送控制命令,否则控制器会进入超时保护

数据帧格式定义

PDO1_MOSIAGV → Curtis- 0x1E5

功能: AGV 上位机向 Curtis 控制器发送控制命令

Byte0 - 控制位 1

Bit 功能 0 1
0 调速开关 有效 无效
1 紧急反向开关 无效 有效
2 龟速开关 无效 有效
3 喇叭开关 无效 有效
4 左侧提升开关 无效 有效
5 左侧下降开关 无效 有效
6 右侧提升开关 无效 有效
7 右侧下降开关 无效 有效

Byte1 - 控制位 2仅 AGV CAN 发送REMA 手柄不发送)

Bit 功能 0 1
0 AGV 标识 无效 AGV 连接
1 紧急停车 无效 紧停
2 找中完成标记 - 完成
3-7 保留 - -

Byte2-3 - 行走调速信号

  • 范围: -4096 ~ +4095有符号 16 位)
  • Byte2 低 8 位
  • Byte3 高 8 位

Byte4-5 - 提升/上下倾调速信号(左侧)

  • 范围: -4096 ~ +4095有符号 16 位)
  • Byte4 低 8 位
  • Byte5 高 8 位

Byte6-7 - 提升调速信号(右侧)

  • 范围: -4096 ~ +4095有符号 16 位)
  • Byte6 低 8 位
  • Byte7 高 8 位

PDO1_MOSIAGV → Curtis- 0x2E5

功能: 转向角度控制命令

Byte0-1 - 转向角度要求

  • 范围: -900 ~ +900对应 -90° ~ +90°
  • Byte0 低 8 位
  • Byte1 高 8 位

重要配置: 1220C 参数需设置为:

command device/can/
  can steer left stop to centre = -900
  can steer right stop to centre = 900

Byte2-7 - 保留

  • 填充 0x00

PDO1_MISOCurtis → AGV- 0x260

功能: Curtis 控制器向 AGV 上位机反馈状态数据

Byte0 - 行走故障代码

  • 格式: 16 进制
  • 说明: 行走电机相关故障代码

Byte1 - 转向故障代码

  • 格式: 16 进制
  • 说明: 转向电机相关故障代码

Byte2-3 - 转向角度反馈

  • 范围: -16383 ~ +16383对应 -90° ~ +90°
  • Byte2 低位
  • Byte3 高位

Byte4-5 - 电机转速

  • 范围: -4096 ~ +4095区分正反转
  • Byte4 低 8 位
  • Byte5 高 8 位

Byte6 - 电量

  • 范围: 0 ~ 100百分比

Byte7 - 状态位

Bit 功能 0 1
0 手自动状态 手动 自动
1 紧急停车 无急停 急停
2-7 预留 - -

PDO2_MISOCurtis → AGV- 0x360

功能: 保留用途

数据字节定义:

  • Byte0 ~ Byte7保留填充 0x00

通讯协议摘要表

上位机向控制器发送MOSI

类型 CAN ID Data1 Data2 Data3 Data4 Data5 Data6 Data7 Data8
MOSI1 0x1E5 控制位1 控制位2 行走低位 行走高位 左提升低位 左提升高位 右提升低位 右提升高位
MOSI2 0x2E5 角度低位 角度高位 0x00 0x00 0x00 0x00 0x00 0x00

控制器回复上位机MISO

类型 CAN ID Data1 Data2 Data3 Data4 Data5 Data6 Data7 Data8
MISO1 0x260 行走故障 转向故障 角度低位 角度高位 转速低位 转速高位 电量 状态位
MISO2 0x360 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

完整通讯实现案例

头文件和数据结构定义

// curtis_motor_controller.h
#ifndef CURTIS_MOTOR_CONTROLLER_H
#define CURTIS_MOTOR_CONTROLLER_H

#include "ControlCAN.h"
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

// CAN 设备配置
#define CAN_DEVICE_TYPE     VCI_USBCAN2  // 4
#define CAN_DEVICE_INDEX    0            // 第一个设备
#define CAN_CHANNEL         0            // CAN1 通道

// Curtis CAN ID 定义
#define CURTIS_ID_DRIVE_CMD     0x1E5    // 行走/提升命令
#define CURTIS_ID_STEER_CMD     0x2E5    // 转向命令
#define CURTIS_ID_STATUS        0x260    // 状态反馈
#define CURTIS_ID_RESERVED      0x360    // 保留

// 数据范围定义
#define DRIVE_SPEED_MIN         -4096
#define DRIVE_SPEED_MAX         4095
#define LIFT_SPEED_MIN          -4096
#define LIFT_SPEED_MAX          4095
#define STEER_ANGLE_MIN         -900     // -90度
#define STEER_ANGLE_MAX         900      // +90度

// 控制位掩码定义Byte0
#define CTRL_BIT0_SPEED_SWITCH      0x01  // 调速开关0=有效)
#define CTRL_BIT1_EMERGENCY_REVERSE 0x02  // 紧急反向
#define CTRL_BIT2_TURTLE_MODE       0x04  // 龟速开关
#define CTRL_BIT3_HORN              0x08  // 喇叭
#define CTRL_BIT4_LIFT_LEFT_UP      0x10  // 左侧提升
#define CTRL_BIT5_LIFT_LEFT_DOWN    0x20  // 左侧下降
#define CTRL_BIT6_LIFT_RIGHT_UP     0x40  // 右侧提升
#define CTRL_BIT7_LIFT_RIGHT_DOWN   0x80  // 右侧下降

// 控制位掩码定义Byte1 - AGV专用
#define CTRL_AGV_CONNECTED          0x01  // AGV连接标识
#define CTRL_EMERGENCY_STOP         0x02  // 紧急停车
#define CTRL_CENTERING_DONE         0x04  // 找中完成

// 状态位掩码定义Byte7
#define STATUS_AUTO_MODE            0x01  // 0=手动, 1=自动
#define STATUS_EMERGENCY_STOP       0x02  // 0=无急停, 1=急停

/**
 * Curtis 控制器状态结构
 */
typedef struct {
    uint8_t  driveError;        // 行走故障代码
    uint8_t  steerError;        // 转向故障代码
    float    currentAngle;      // 当前转向角度(度)
    int16_t  motorSpeed;        // 电机转速
    uint8_t  batteryLevel;      // 电量百分比 0-100
    bool     isAutoMode;        // 是否自动模式
    bool     isEmergencyStop;   // 是否急停状态
    uint32_t lastUpdateTime;    // 最后更新时间戳(毫秒)
} CurtisStatus;

/**
 * Curtis 控制命令结构
 */
typedef struct {
    int16_t driveSpeed;         // 行走速度 -4096~4095
    int16_t liftSpeedLeft;      // 左侧提升速度 -4096~4095
    int16_t liftSpeedRight;     // 右侧提升速度 -4096~4095
    int16_t steerAngle;         // 转向角度 -900~900
    uint8_t controlBits0;       // 控制位0
    uint8_t controlBits1;       // 控制位1AGV专用
} CurtisCommand;

/**
 * Curtis 控制器管理器
 */
typedef struct {
    DWORD deviceType;
    DWORD deviceIndex;
    DWORD canChannel;
    bool  isConnected;
    CurtisStatus status;
    CurtisCommand lastCommand;
} CurtisController;

// 函数声明
bool Curtis_Initialize(CurtisController* ctrl);
bool Curtis_Connect(CurtisController* ctrl);
void Curtis_Disconnect(CurtisController* ctrl);
bool Curtis_SendCommand(CurtisController* ctrl, const CurtisCommand* cmd);
bool Curtis_ReceiveStatus(CurtisController* ctrl);
void Curtis_EmergencyStop(CurtisController* ctrl);
int16_t Curtis_SpeedPercentToCAN(float percent);
float Curtis_SpeedCANToPercent(int16_t canValue);
int16_t Curtis_AngleDegreeToCAN(float degrees);
float Curtis_AngleCANToDegree(int16_t canValue);

#endif // CURTIS_MOTOR_CONTROLLER_H

实现文件

// curtis_motor_controller.c
#include "curtis_motor_controller.h"
#include <windows.h>

/**
 * 初始化 Curtis 控制器结构
 */
bool Curtis_Initialize(CurtisController* ctrl) {
    if (ctrl == NULL) {
        return false;
    }

    memset(ctrl, 0, sizeof(CurtisController));
    ctrl->deviceType = CAN_DEVICE_TYPE;
    ctrl->deviceIndex = CAN_DEVICE_INDEX;
    ctrl->canChannel = CAN_CHANNEL;
    ctrl->isConnected = false;

    return true;
}

/**
 * 连接到 Curtis 控制器(通过 CAN 总线)
 */
bool Curtis_Connect(CurtisController* ctrl) {
    if (ctrl == NULL) {
        printf("[Curtis] 错误: 控制器指针为空\n");
        return false;
    }

    if (ctrl->isConnected) {
        printf("[Curtis] 警告: 已经连接\n");
        return true;
    }

    // 步骤1: 打开 CAN 设备
    printf("[Curtis] 正在打开 CAN 设备 (类型=%d, 索引=%d)...\n",
           ctrl->deviceType, ctrl->deviceIndex);

    DWORD ret = VCI_OpenDevice(ctrl->deviceType, ctrl->deviceIndex, 0);
    if (ret != 1) {
        printf("[Curtis] 错误: 打开设备失败 (返回值=%d)\n", ret);
        return false;
    }
    printf("[Curtis] 设备打开成功\n");

    // 步骤2: 配置 CAN 参数125kbps
    VCI_INIT_CONFIG config;
    config.AccCode = 0x80000008;
    config.AccMask = 0xFFFFFFFF;  // 接收所有帧
    config.Filter = 1;             // 接收标准帧和扩展帧
    config.Timing0 = 0x03;         // 125 kbps
    config.Timing1 = 0x1C;
    config.Mode = 0;               // 正常模式
    config.Reserved = 0;

    printf("[Curtis] 正在初始化 CAN (125kbps)...\n");
    ret = VCI_InitCAN(ctrl->deviceType, ctrl->deviceIndex, ctrl->canChannel, &config);
    if (ret != 1) {
        printf("[Curtis] 错误: 初始化 CAN 失败 (返回值=%d)\n", ret);
        VCI_CloseDevice(ctrl->deviceType, ctrl->deviceIndex);
        return false;
    }
    printf("[Curtis] CAN 初始化成功\n");

    // 步骤3: 启动 CAN 通道
    printf("[Curtis] 正在启动 CAN 通道...\n");
    ret = VCI_StartCAN(ctrl->deviceType, ctrl->deviceIndex, ctrl->canChannel);
    if (ret != 1) {
        printf("[Curtis] 错误: 启动 CAN 失败 (返回值=%d)\n", ret);
        VCI_CloseDevice(ctrl->deviceType, ctrl->deviceIndex);
        return false;
    }
    printf("[Curtis] CAN 通道启动成功\n");

    // 步骤4: 清空缓冲区
    VCI_ClearBuffer(ctrl->deviceType, ctrl->deviceIndex, ctrl->canChannel);

    ctrl->isConnected = true;
    printf("[Curtis] ✓ 连接成功!准备通讯\n");

    return true;
}

/**
 * 断开与 Curtis 控制器的连接
 */
void Curtis_Disconnect(CurtisController* ctrl) {
    if (ctrl == NULL) {
        return;
    }

    if (!ctrl->isConnected) {
        printf("[Curtis] 警告: 尚未连接\n");
        return;
    }

    // 发送停止命令
    printf("[Curtis] 正在发送停止命令...\n");
    CurtisCommand stopCmd = {0};
    stopCmd.controlBits0 = 0x00;  // 调速开关有效
    stopCmd.controlBits1 = CTRL_AGV_CONNECTED;  // 保持AGV连接标识
    Curtis_SendCommand(ctrl, &stopCmd);
    Sleep(50);  // 等待命令发送

    // 关闭设备
    printf("[Curtis] 正在关闭 CAN 设备...\n");
    VCI_CloseDevice(ctrl->deviceType, ctrl->deviceIndex);

    ctrl->isConnected = false;
    printf("[Curtis] ✓ 断开连接完成\n");
}

/**
 * 发送控制命令到 Curtis 控制器
 */
bool Curtis_SendCommand(CurtisController* ctrl, const CurtisCommand* cmd) {
    if (ctrl == NULL || cmd == NULL) {
        return false;
    }

    if (!ctrl->isConnected) {
        printf("[Curtis] 错误: 未连接到控制器\n");
        return false;
    }

    // 限制数值范围
    int16_t driveSpeed = cmd->driveSpeed;
    if (driveSpeed < DRIVE_SPEED_MIN) driveSpeed = DRIVE_SPEED_MIN;
    if (driveSpeed > DRIVE_SPEED_MAX) driveSpeed = DRIVE_SPEED_MAX;

    int16_t liftLeft = cmd->liftSpeedLeft;
    if (liftLeft < LIFT_SPEED_MIN) liftLeft = LIFT_SPEED_MIN;
    if (liftLeft > LIFT_SPEED_MAX) liftLeft = LIFT_SPEED_MAX;

    int16_t liftRight = cmd->liftSpeedRight;
    if (liftRight < LIFT_SPEED_MIN) liftRight = LIFT_SPEED_MIN;
    if (liftRight > LIFT_SPEED_MAX) liftRight = LIFT_SPEED_MAX;

    int16_t steerAngle = cmd->steerAngle;
    if (steerAngle < STEER_ANGLE_MIN) steerAngle = STEER_ANGLE_MIN;
    if (steerAngle > STEER_ANGLE_MAX) steerAngle = STEER_ANGLE_MAX;

    // 发送帧1: 0x1E5 - 行走和提升命令
    VCI_CAN_OBJ frame1;
    memset(&frame1, 0, sizeof(VCI_CAN_OBJ));
    frame1.ID = CURTIS_ID_DRIVE_CMD;
    frame1.SendType = 0;        // 正常发送(自动重试)
    frame1.RemoteFlag = 0;      // 数据帧
    frame1.ExternFlag = 0;      // 标准帧
    frame1.DataLen = 8;

    frame1.Data[0] = cmd->controlBits0;
    frame1.Data[1] = cmd->controlBits1 | CTRL_AGV_CONNECTED;  // 确保AGV标识位
    frame1.Data[2] = driveSpeed & 0xFF;
    frame1.Data[3] = (driveSpeed >> 8) & 0xFF;
    frame1.Data[4] = liftLeft & 0xFF;
    frame1.Data[5] = (liftLeft >> 8) & 0xFF;
    frame1.Data[6] = liftRight & 0xFF;
    frame1.Data[7] = (liftRight >> 8) & 0xFF;

    DWORD sent = VCI_Transmit(ctrl->deviceType, ctrl->deviceIndex,
                              ctrl->canChannel, &frame1, 1);
    if (sent != 1) {
        printf("[Curtis] 错误: 发送 0x1E5 失败\n");
        return false;
    }

    // 发送帧2: 0x2E5 - 转向命令
    VCI_CAN_OBJ frame2;
    memset(&frame2, 0, sizeof(VCI_CAN_OBJ));
    frame2.ID = CURTIS_ID_STEER_CMD;
    frame2.SendType = 0;
    frame2.RemoteFlag = 0;
    frame2.ExternFlag = 0;
    frame2.DataLen = 8;

    frame2.Data[0] = steerAngle & 0xFF;
    frame2.Data[1] = (steerAngle >> 8) & 0xFF;
    // Byte2-7 保留填充0

    sent = VCI_Transmit(ctrl->deviceType, ctrl->deviceIndex,
                        ctrl->canChannel, &frame2, 1);
    if (sent != 1) {
        printf("[Curtis] 错误: 发送 0x2E5 失败\n");
        return false;
    }

    // 保存最后发送的命令
    ctrl->lastCommand = *cmd;

    return true;
}

/**
 * 接收 Curtis 控制器状态反馈
 */
bool Curtis_ReceiveStatus(CurtisController* ctrl) {
    if (ctrl == NULL) {
        return false;
    }

    if (!ctrl->isConnected) {
        return false;
    }

    VCI_CAN_OBJ rxFrames[2500];
    DWORD count = VCI_Receive(ctrl->deviceType, ctrl->deviceIndex,
                              ctrl->canChannel, rxFrames, 2500, 0);

    if (count == (DWORD)-1) {
        printf("[Curtis] 错误: USB 断开或设备不存在\n");
        ctrl->isConnected = false;
        return false;
    }

    bool statusUpdated = false;

    for (DWORD i = 0; i < count; i++) {
        if (rxFrames[i].ID == CURTIS_ID_STATUS) {
            // 解析 0x260 状态帧
            ctrl->status.driveError = rxFrames[i].Data[0];
            ctrl->status.steerError = rxFrames[i].Data[1];

            int16_t angleRaw = (int16_t)((rxFrames[i].Data[3] << 8) |
                                         rxFrames[i].Data[2]);
            ctrl->status.currentAngle = angleRaw / 10.0f;

            ctrl->status.motorSpeed = (int16_t)((rxFrames[i].Data[5] << 8) |
                                                rxFrames[i].Data[4]);

            ctrl->status.batteryLevel = rxFrames[i].Data[6];

            ctrl->status.isAutoMode = (rxFrames[i].Data[7] & STATUS_AUTO_MODE) != 0;
            ctrl->status.isEmergencyStop = (rxFrames[i].Data[7] & STATUS_EMERGENCY_STOP) != 0;

            ctrl->status.lastUpdateTime = GetTickCount();
            statusUpdated = true;
        }
        else if (rxFrames[i].ID == CURTIS_ID_RESERVED) {
            // 0x360 保留帧,暂不处理
        }
    }

    return statusUpdated;
}

/**
 * 紧急停车
 */
void Curtis_EmergencyStop(CurtisController* ctrl) {
    if (ctrl == NULL || !ctrl->isConnected) {
        return;
    }

    printf("[Curtis] !!! 执行紧急停车 !!!\n");

    CurtisCommand stopCmd = {0};
    stopCmd.controlBits0 = 0x00;  // 调速开关有效
    stopCmd.controlBits1 = CTRL_EMERGENCY_STOP | CTRL_AGV_CONNECTED;
    stopCmd.driveSpeed = 0;
    stopCmd.liftSpeedLeft = 0;
    stopCmd.liftSpeedRight = 0;
    stopCmd.steerAngle = 0;

    // 连续发送3次确保收到
    for (int i = 0; i < 3; i++) {
        Curtis_SendCommand(ctrl, &stopCmd);
        Sleep(10);
    }
}

/**
 * 速度百分比转 CAN 值 (-100% ~ +100% -> -4096 ~ +4095)
 */
int16_t Curtis_SpeedPercentToCAN(float percent) {
    if (percent > 100.0f) percent = 100.0f;
    if (percent < -100.0f) percent = -100.0f;
    return (int16_t)(percent * 40.95f);
}

/**
 * CAN 值转速度百分比
 */
float Curtis_SpeedCANToPercent(int16_t canValue) {
    return canValue / 40.95f;
}

/**
 * 角度转 CAN 值 (-90° ~ +90° -> -900 ~ +900)
 */
int16_t Curtis_AngleDegreeToCAN(float degrees) {
    if (degrees > 90.0f) degrees = 90.0f;
    if (degrees < -90.0f) degrees = -90.0f;
    return (int16_t)(degrees * 10.0f);
}

/**
 * CAN 值转角度
 */
float Curtis_AngleCANToDegree(int16_t canValue) {
    return canValue / 10.0f;
}

使用示例 - 主程序

// main_curtis_demo.c
#include "curtis_motor_controller.h"
#include <windows.h>
#include <conio.h>

/**
 * 打印 Curtis 状态信息
 */
void printStatus(const CurtisStatus* status) {
    printf("\n========== Curtis 状态 ==========\n");
    printf("行走故障: 0x%02X  转向故障: 0x%02X\n",
           status->driveError, status->steerError);
    printf("转向角度: %.1f°\n", status->currentAngle);
    printf("电机转速: %d\n", status->motorSpeed);
    printf("电量: %d%%\n", status->batteryLevel);
    printf("模式: %s  急停: %s\n",
           status->isAutoMode ? "自动" : "手动",
           status->isEmergencyStop ? "是" : "否");
    printf("================================\n\n");
}

/**
 * 主程序示例:演示完整的 Curtis 控制流程
 */
int main() {
    CurtisController curtis;
    bool running = true;

    printf("===== Curtis 电机控制器 CAN 通讯演示 =====\n\n");

    // ============ 1. 初始化 ============
    printf("步骤 1: 初始化控制器...\n");
    if (!Curtis_Initialize(&curtis)) {
        printf("初始化失败!\n");
        return -1;
    }

    // ============ 2. 连接 ============
    printf("\n步骤 2: 连接到 Curtis 控制器...\n");
    if (!Curtis_Connect(&curtis)) {
        printf("连接失败!\n");
        return -1;
    }

    // 等待控制器稳定
    Sleep(100);

    // ============ 3. 主控制循环 ============
    printf("\n步骤 3: 进入主控制循环\n");
    printf("控制说明:\n");
    printf("  W/S - 前进/后退\n");
    printf("  A/D - 左转/右转\n");
    printf("  Q/E - 提升/下降\n");
    printf("  空格 - 停止\n");
    printf("  ESC - 退出程序\n");
    printf("\n开始控制...\n\n");

    CurtisCommand cmd = {0};
    cmd.controlBits0 = 0x00;  // 调速开关有效
    cmd.controlBits1 = CTRL_AGV_CONNECTED;

    DWORD lastSendTime = GetTickCount();
    DWORD lastPrintTime = GetTickCount();
    int loopCount = 0;

    while (running) {
        DWORD currentTime = GetTickCount();

        // ---- 接收状态(每次循环) ----
        Curtis_ReceiveStatus(&curtis);

        // ---- 键盘控制 ----
        if (_kbhit()) {
            char ch = _getch();

            switch (ch) {
                case 'w': case 'W':
                    cmd.driveSpeed = Curtis_SpeedPercentToCAN(30.0f);  // 前进30%
                    printf("[控制] 前进 30%%\n");
                    break;

                case 's': case 'S':
                    cmd.driveSpeed = Curtis_SpeedPercentToCAN(-30.0f);  // 后退30%
                    printf("[控制] 后退 30%%\n");
                    break;

                case 'a': case 'A':
                    cmd.steerAngle = Curtis_AngleDegreeToCAN(-30.0f);  // 左转30度
                    printf("[控制] 左转 30°\n");
                    break;

                case 'd': case 'D':
                    cmd.steerAngle = Curtis_AngleDegreeToCAN(30.0f);  // 右转30度
                    printf("[控制] 右转 30°\n");
                    break;

                case 'q': case 'Q':
                    cmd.liftSpeedLeft = Curtis_SpeedPercentToCAN(50.0f);  // 提升
                    printf("[控制] 提升\n");
                    break;

                case 'e': case 'E':
                    cmd.liftSpeedLeft = Curtis_SpeedPercentToCAN(-50.0f);  // 下降
                    printf("[控制] 下降\n");
                    break;

                case ' ':  // 空格 - 停止
                    cmd.driveSpeed = 0;
                    cmd.steerAngle = 0;
                    cmd.liftSpeedLeft = 0;
                    cmd.liftSpeedRight = 0;
                    printf("[控制] 全部停止\n");
                    break;

                case 27:  // ESC - 退出
                    printf("\n[控制] 用户请求退出\n");
                    running = false;
                    break;
            }
        }

        // ---- 检查故障和急停 ----
        if (curtis.status.driveError != 0 || curtis.status.steerError != 0) {
            printf("[警告] 检测到故障!行走=0x%02X, 转向=0x%02X\n",
                   curtis.status.driveError, curtis.status.steerError);
        }

        if (curtis.status.isEmergencyStop) {
            printf("[警告] 急停状态!停止所有运动\n");
            cmd.driveSpeed = 0;
            cmd.steerAngle = 0;
            cmd.liftSpeedLeft = 0;
            cmd.liftSpeedRight = 0;
        }

        // ---- 定时发送命令≤20ms周期 ----
        if (currentTime - lastSendTime >= 15) {  // 15ms周期
            if (!Curtis_SendCommand(&curtis, &cmd)) {
                printf("[错误] 发送命令失败!\n");
                running = false;
            }
            lastSendTime = currentTime;
        }

        // ---- 定时打印状态1秒 ----
        if (currentTime - lastPrintTime >= 1000) {
            printStatus(&curtis.status);
            lastPrintTime = currentTime;
        }

        // 避免CPU占用过高
        Sleep(5);
        loopCount++;
    }

    printf("\n总循环次数: %d\n", loopCount);

    // ============ 4. 断开连接 ============
    printf("\n步骤 4: 断开连接...\n");
    Curtis_Disconnect(&curtis);

    printf("\n===== 程序结束 =====\n");
    return 0;
}

高级示例 - 路径跟踪控制

// path_tracking_demo.c
#include "curtis_motor_controller.h"
#include <windows.h>
#include <math.h>

/**
 * 路径点结构
 */
typedef struct {
    float x;
    float y;
    float targetSpeed;  // 目标速度百分比
} PathPoint;

/**
 * 执行路径跟踪控制
 */
bool executePathTracking(CurtisController* ctrl, const PathPoint* path, int pathLength) {
    if (ctrl == NULL || path == NULL || pathLength <= 0) {
        return false;
    }

    printf("[路径跟踪] 开始执行,共 %d 个路径点\n", pathLength);

    CurtisCommand cmd = {0};
    cmd.controlBits0 = 0x00;
    cmd.controlBits1 = CTRL_AGV_CONNECTED;

    for (int i = 0; i < pathLength; i++) {
        printf("\n[路径跟踪] 执行路径点 %d/%d: (%.2f, %.2f), 速度=%.1f%%\n",
               i + 1, pathLength, path[i].x, path[i].y, path[i].targetSpeed);

        // 设置速度
        cmd.driveSpeed = Curtis_SpeedPercentToCAN(path[i].targetSpeed);

        // 模拟转向角度计算(实际应用中根据路径规划算法计算)
        float steerAngleDeg = 0.0f;
        if (i + 1 < pathLength) {
            float dx = path[i + 1].x - path[i].x;
            float dy = path[i + 1].y - path[i].y;
            steerAngleDeg = atan2f(dy, dx) * 180.0f / 3.14159f;
            // 限制在 ±45 度
            if (steerAngleDeg > 45.0f) steerAngleDeg = 45.0f;
            if (steerAngleDeg < -45.0f) steerAngleDeg = -45.0f;
        }
        cmd.steerAngle = Curtis_AngleDegreeToCAN(steerAngleDeg);

        // 执行该路径点持续1秒
        DWORD startTime = GetTickCount();
        while (GetTickCount() - startTime < 1000) {
            // 接收状态
            Curtis_ReceiveStatus(ctrl);

            // 检查故障
            if (ctrl->status.driveError || ctrl->status.steerError) {
                printf("[路径跟踪] 检测到故障,中止路径跟踪\n");
                Curtis_EmergencyStop(ctrl);
                return false;
            }

            // 检查急停
            if (ctrl->status.isEmergencyStop) {
                printf("[路径跟踪] 检测到急停,中止路径跟踪\n");
                return false;
            }

            // 发送命令15ms周期
            Curtis_SendCommand(ctrl, &cmd);
            Sleep(15);
        }
    }

    // 路径结束,停止
    printf("\n[路径跟踪] 路径执行完成,停止车辆\n");
    cmd.driveSpeed = 0;
    cmd.steerAngle = 0;
    for (int i = 0; i < 5; i++) {
        Curtis_SendCommand(ctrl, &cmd);
        Sleep(15);
    }

    return true;
}

/**
 * 路径跟踪示例主程序
 */
int main() {
    CurtisController curtis;

    printf("===== Curtis 路径跟踪演示 =====\n\n");

    // 初始化并连接
    if (!Curtis_Initialize(&curtis) || !Curtis_Connect(&curtis)) {
        printf("初始化失败!\n");
        return -1;
    }

    Sleep(100);

    // 定义测试路径(矩形路径)
    PathPoint path[] = {
        {0.0f, 0.0f, 30.0f},    // 起点
        {5.0f, 0.0f, 30.0f},    // 直行5米
        {5.0f, 3.0f, 25.0f},    // 右转前进3米
        {0.0f, 3.0f, 25.0f},    // 左转前进5米
        {0.0f, 0.0f, 20.0f}     // 返回起点
    };
    int pathLength = sizeof(path) / sizeof(PathPoint);

    // 执行路径跟踪
    bool success = executePathTracking(&curtis, path, pathLength);

    if (success) {
        printf("\n✓ 路径跟踪成功完成\n");
    } else {
        printf("\n✗ 路径跟踪失败\n");
    }

    // 断开连接
    Curtis_Disconnect(&curtis);

    printf("\n===== 程序结束 =====\n");
    return 0;
}

错误处理和最佳实践

错误处理

/**
 * 检查通讯超时
 */
bool Curtis_CheckTimeout(CurtisController* ctrl, DWORD timeoutMs) {
    if (ctrl == NULL) {
        return true;  // 视为超时
    }

    DWORD currentTime = GetTickCount();
    DWORD elapsed = currentTime - ctrl->status.lastUpdateTime;

    if (elapsed > timeoutMs) {
        printf("[Curtis] 警告: 通讯超时 (%d ms)\n", elapsed);
        return true;
    }

    return false;
}

/**
 * 通讯恢复处理
 */
bool Curtis_Reconnect(CurtisController* ctrl) {
    printf("[Curtis] 尝试重新连接...\n");

    Curtis_Disconnect(ctrl);
    Sleep(500);

    if (Curtis_Connect(ctrl)) {
        printf("[Curtis] 重新连接成功\n");
        return true;
    } else {
        printf("[Curtis] 重新连接失败\n");
        return false;
    }
}

最佳实践清单

  1. 命令发送频率

    • 必须以 ≤20ms 周期持续发送命令
    • 推荐 10-15ms 周期
    • 超时会导致控制器进入保护模式
  2. 初始化顺序

    Curtis_Initialize() → Curtis_Connect() → 主循环 → Curtis_Disconnect()
    
  3. 故障处理

    • 实时监控 driveError 和 steerError
    • 检测到故障立即停止运动
    • 记录故障代码用于诊断
  4. 急停处理

    • 检测到急停状态立即停止所有命令
    • 发送停止命令(速度=0
    • 等待急停解除后才能恢复
  5. 数值范围检查

    • 所有命令值都应进行范围限制
    • 使用提供的转换函数确保数据正确
  6. AGV标识位

    • 所有命令必须设置 AGV 连接标识Byte1 Bit0
    • 否则控制器可能不响应
  7. 断开连接

    • 程序退出前必须调用 Curtis_Disconnect()
    • 发送停止命令后再关闭设备

常见问题

问:无法控制车辆?

  • 检查 CAN 波特率是否为 125kbpsTiming0=0x03, Timing1=0x1C
  • 确认 AGV 标识位已设置Byte1 Bit0 = 1
  • 手动模式下检查 J9/SW_3 是否闭合
  • 自动模式下检查 J24/SW_1 是否为 ON
  • 确认命令发送频率 ≤20ms

问:控制器频繁超时?

  • 检查命令发送周期,必须 ≤20ms
  • 检查"AGV 通讯超时"参数设置
  • 验证 CAN 总线连接是否稳定
  • 检查是否有其他设备干扰总线

问:转向角度不准确?

  • 确认 1220C 参数配置:
    • can steer left stop to centre = -900
    • can steer right stop to centre = 900
  • 验证角度转换:-900~+900 对应 -90°~+90°
  • 检查转向电机是否正常工作

问:收到故障代码?

  • 查看 Byte0行走故障和 Byte1转向故障
  • 参考 Curtis 故障代码手册诊断
  • 检查电机连接和电源
  • 检查控制器参数配置

问:急停无法解除?

  • 检查硬件急停开关是否复位
  • 确认 Byte7 Bit1 状态
  • 发送命令清除软件急停Byte1 Bit1 = 0
  • 检查安全回路

协议版本历史

V4.0 (2021-06-05)

  • 当前版本
  • 拟制zuofuxing

相关文档

  • can-protocol.md - USB-CAN 接口函数库参考ControlCAN.dll
  • CAN_Protocol.pdf - CAN 通讯协议基础
  • 09-01-14 CAN Protocol Specification - REMA 手柄通讯协议

技术支持: 上海诺力智能科技有限公司


快速参考

CAN 配置

波特率: 125 kbps
Timing0: 0x03
Timing1: 0x1C

CAN ID 映射

AGV → Curtis:
  0x1E5 - 控制命令(行走、提升)
  0x2E5 - 转向角度

Curtis → AGV:
  0x260 - 状态反馈
  0x360 - 保留

数据范围

行走速度: -4096 ~ +4095
提升速度: -4096 ~ +4095
转向角度命令: -900 ~ +900 (-90° ~ +90°)
转向角度反馈: -16383 ~ +16383 (-90° ~ +90°)
电量: 0 ~ 100 (%)

控制位快速索引

Byte0 Bit0: 调速开关 (0=有效)
Byte1 Bit0: AGV标识 (1=连接) ← 必须设置
Byte1 Bit1: 紧急停车 (1=急停)
Byte7 Bit0: 手自动 (1=自动)
Byte7 Bit1: 急停状态 (1=急停)

关键API

Curtis_Initialize()       // 初始化控制器
Curtis_Connect()          // 连接打开设备、初始化CAN、启动
Curtis_SendCommand()      // 发送控制命令≤20ms周期
Curtis_ReceiveStatus()    // 接收状态反馈
Curtis_EmergencyStop()    // 紧急停车
Curtis_Disconnect()       // 断开连接(发送停止、关闭设备)