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 如何映射到电机对象?
- 反馈如何更新到状态对象?
- 真机控制前必须做哪些安全保护?