嵌软 C 到机器人 C++ 快速学习路线

面向 MCU / CAN / 电机控制背景:用最短路径建立 C++ class、继承、组合、CMake 与三电机工程架构。

1. 学习路线:不要从语法书开始

第 1 步

先掌握 C++ 最小语法

变量、函数、引用、const、enum class、struct/class、构造函数、析构函数。

第 2 步

建立 class 分层思维

把 C 里的 xxx.h + xxx.c + struct 迁移到 C++ 的 class 和对象生命周期。

第 3 步

理解组合与继承

has-a 用组合,is-a 才用继承。机器人硬件代码里组合比继承更常用。

第 4 步

用三电机工程落地

CanBus、DamiaoMotor、MitPacket、MotorGroup、main 控制流程。

最适合你的路线:不是先学 STL 全家桶,而是先学“如何把 CAN、电机状态、协议打包、批量控制封装成 C++ 工程”。

2. C++ 基本语法:从 C 能力迁移

2.1 引用 reference

引用是 C++ 里非常重要的语法,可以理解为“安全一点的指针别名”。

void update(double& q) {
    q = 1.0;
}

double pos = 0.0;
update(pos);  // pos 被修改为 1.0

2.2 const

const 是接口稳定性的核心。能不改的参数,就写成 const。

double getPosition() const {
    return q_;
}

void sendCommand(const MitCommand& cmd) {
    // 只读取 cmd,不修改
}

2.3 enum class

比 C 的 enum 更安全,不容易污染命名空间。

enum class ControlMode {
    MIT,
    PositionVelocity,
    Disabled
};

ControlMode mode = ControlMode::MIT;

2.4 namespace

大型工程里避免名字冲突。

namespace robot {
namespace motor {

class DamiaoMotor {
    // ...
};

} // namespace motor
} // namespace robot

3. 类与对象:C 的 struct + 函数升级版

C++ 的 class 不是玄学,本质是把“数据”和“操作数据的函数”放在一起。

C 写法 C++ 写法 工程含义
motor_state_t class DamiaoMotor 保存单电机状态
can_send() CanBus::sendFrame() CAN 发送接口
pack_mit_frame() MitPacketEncoder::encode() 协议打包
motor_group_enable_all() DamiaoMotorGroup::enableAll() 多电机批量控制

3.1 单电机状态类

class DamiaoMotor {
public:
    explicit DamiaoMotor(int id)
        : id_(id) {}

    int id() const { return id_; }
    double position() const { return q_; }
    double velocity() const { return dq_; }
    double torque() const { return tau_; }
    bool online() const { return online_; }

    void updateState(double q, double dq, double tau) {
        q_ = q;
        dq_ = dq;
        tau_ = tau;
        online_ = true;
    }

private:
    int id_;
    double q_{0.0};
    double dq_{0.0};
    double tau_{0.0};
    bool online_{false};
};
关键习惯:不要让外部随便改 q_ / dq_ / tau_。状态应该通过明确的接口更新。

4. 继承、组合、多态

4.1 组合:has-a

组合表示“一个对象里面拥有另一个对象”。机器人代码里最常见。

class DamiaoMotorGroup {
public:
    DamiaoMotorGroup(CanBus& can) : can_(can) {}

private:
    CanBus& can_;
    std::array<DamiaoMotor, 3> motors_;
};

含义:

DamiaoMotorGroup has a CanBus reference
DamiaoMotorGroup has 3 DamiaoMotor

4.2 继承:is-a

继承表示“子类是一种父类”。例如 CAN 设备抽象:

class CanDevice {
public:
    virtual ~CanDevice() = default;
    virtual void onFrame(const CanFrame& frame) = 0;
};

class DamiaoCanDevice : public CanDevice {
public:
    void onFrame(const CanFrame& frame) override {
        // 解码达妙反馈
    }
};

含义:

DamiaoCanDevice is a CanDevice

4.3 多态

多态适合“多个设备有统一接口,但具体行为不同”的场景。

std::vector<std::shared_ptr<CanDevice>> devices;

for (auto& dev : devices) {
    dev->onFrame(frame);
}
机器人硬件代码不要滥用继承。优先组合,只有明确存在“is-a 关系”时再继承。

5. 工程配置:CMake 最小结构

5.1 推荐目录

damiao_3motor_demo/
├── CMakeLists.txt
├── include/
│   ├── can_bus.hpp
│   ├── damiao_motor.hpp
│   ├── damiao_motor_group.hpp
│   └── mit_packet.hpp
├── src/
│   ├── can_bus.cpp
│   ├── damiao_motor.cpp
│   ├── damiao_motor_group.cpp
│   └── mit_packet.cpp
└── app/
    └── main_3motor_position.cpp

5.2 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)

project(damiao_3motor_demo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(damiao_core
    src/can_bus.cpp
    src/damiao_motor.cpp
    src/damiao_motor_group.cpp
    src/mit_packet.cpp
)

target_include_directories(damiao_core PUBLIC include)

add_executable(main_3motor_position
    app/main_3motor_position.cpp
)

target_link_libraries(main_3motor_position PRIVATE damiao_core)

5.3 构建

cmake -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j
./build/main_3motor_position

6. 从嵌软 C 转 C++ 的注意点

问题 C 里常见写法 C++ 推荐做法
资源初始化 init() / deinit() 构造函数 / 析构函数,RAII
状态管理 全局变量 对象私有成员 + getter
错误处理 返回 int 错误码 实时控制中仍建议返回 bool / error code,少用异常
内存 malloc/free 优先栈对象、array、vector、unique_ptr
多态 函数指针表 虚函数接口,但不要滥用
实时性 手动控制所有开销 控制循环内避免动态分配、避免异常、避免复杂日志

6.1 控制循环里的禁忌

在高频电机控制循环里,不建议做这些事:
  • 循环内频繁 new/delete
  • 循环内大量 std::cout
  • 循环内抛异常
  • 循环内创建复杂临时容器
  • 没有限位、没有超时保护就直接下发力矩/位置

7. 三电机 MIT 控制例子

这个例子参考 OpenArm 的分层思想:底层 CAN、单电机状态、协议打包/解包、电机组批量控制、main 控制流程。 你上传的笔记中也把 OpenArm 的核心抽象压缩为: CANSocket → Motor + DMCANDevice → DMDeviceCollection → OpenArm, 对应三电机最小工程就是: CanBus → DamiaoMotor → MitPacket → DamiaoMotorGroup → main

7.1 最小类关系

CanBus
  ↓
DamiaoMotorGroup
  ├── DamiaoMotor 1
  ├── DamiaoMotor 2
  └── DamiaoMotor 3

MitPacketEncoder   负责 MIT 命令打包
MitPacketDecoder   负责反馈解析
main               只负责业务流程

7.2 MIT 命令结构

struct MitCommand {
    double q{0.0};
    double dq{0.0};
    double kp{0.0};
    double kd{0.0};
    double tau{0.0};
};

7.3 MotorGroup 接口

class DamiaoMotorGroup {
public:
    DamiaoMotorGroup(CanBus& can, const std::array<int, 3>& ids);

    bool enableAll();
    bool disableAll();

    bool refreshAll();
    bool mitControlAll(const std::array<MitCommand, 3>& cmds);

    std::array<MotorState, 3> getStates() const;

private:
    CanBus& can_;
    std::array<DamiaoMotor, 3> motors_;
};

7.4 main 控制流程

int main() {
    CanBus can("can0");
    can.open();

    DamiaoMotorGroup motors(can, {1, 2, 3});

    motors.enableAll();
    motors.refreshAll();

    auto start_states = motors.getStates();

    std::array<double, 3> q_start = {
        start_states[0].q,
        start_states[1].q,
        start_states[2].q
    };

    std::array<double, 3> q_target = {
        1.0,
        0.5,
        -0.8
    };

    const double duration = 3.0;
    const double dt = 0.005;

    for (double t = 0.0; t < duration; t += dt) {
        double s = t / duration;

        std::array<MitCommand, 3> cmds;

        for (int i = 0; i < 3; ++i) {
            double q_cmd = q_start[i] + s * (q_target[i] - q_start[i]);

            cmds[i].q = q_cmd;
            cmds[i].dq = 0.0;
            cmds[i].kp = 10.0;
            cmds[i].kd = 0.5;
            cmds[i].tau = 0.0;
        }

        motors.mitControlAll(cmds);
        motors.refreshAll();

        auto states = motors.getStates();

        // 必须做安全检查:
        // 1. 位置是否超限
        // 2. 速度是否超限
        // 3. 力矩是否超限
        // 4. 通信是否超时
        // 5. 急停是否触发

        sleepFor(dt);
    }

    motors.disableAll();
    can.close();

    return 0;
}
这个例子是工程结构示例,不是可直接上真机的最终控制程序。真机前必须补齐限位、速度限制、力矩限制、通信超时、急停、逐步插值和日志记录。

8. 学完后你要能回答的问题

语法层

  • 引用和指针有什么区别?
  • const 成员函数是什么意思?
  • 构造函数和析构函数什么时候调用?
  • public / private / protected 有什么区别?

架构层

  • 什么时候用组合?什么时候用继承?
  • Motor 和 MotorDevice 为什么要分开?
  • 协议打包为什么不应该写在 main 里?
  • MotorGroup 为什么是关键抽象?

机器人工程层

  • MIT 控制参数 q/dq/kp/kd/tau 分别是什么?
  • CAN ID 如何映射到电机对象?
  • 反馈如何更新到状态对象?
  • 真机控制前必须做哪些安全保护?