Posted in

尝试做个机械臂缘起:看过稚辉君的机械臂,备受震撼。后来小米发布了私服电机,故想尝试一下。 思路:简单来想,机械臂的最本质 – 掘金_AI阅读总结 — 包阅AI

包阅导读总结

1. 机械臂、ESP32、小米电机、3D 打印、控制电机

2. 本文记录了制作机械臂的过程,包括缘起和思路,分阶段实现了简单运行电机、建模、3D 打印、前端控制、单片机开发等,列出已实现和未实现的目标,介绍了物料清单和各部分的关键操作,还展望了未来的进阶方向。

3.

– 缘起与思路

– 看过稚辉君的机械臂后想尝试,基于控制电机的思路制定渐进目标

– 已实现的阶段目标

– 简单运行电机

– solidworks 建模与 3D 打印

– 前端项目实现网页操作控制电机旋转

– 尝试运行单片机及相关通信协议

– 改写单片机项目实现蓝牙与 TWAI 集合

– 完善前端项目实现网页精准控制电机

– 未来的进阶目标

– 优化 3D 模型

– 引入动画系统

– 单片机改为 Rust 语言开发

– 尝试使用人工智能

– 物料清单

– 详细列举了制作机械臂所需的各种物料及价格

– 各部分操作

– 如电机连接与控制、单片机环境搭建与项目运行、前端项目的开发步骤等

思维导图:

文章地址:https://juejin.cn/post/7399530649999867938

文章来源:juejin.cn

作者:岁聿云暮

发布时间:2024/8/5 17:05

语言:中文

总字数:7974字

预计阅读时间:32分钟

评分:81分

标签:机械臂,小米电机,3D建模,单片机编程,前端开发


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

缘起:看过稚辉君的机械臂,备受震撼。后来小米发布了伺服电机,故想尝试一下。
思路:简单来想,机械臂的最本质就是控制电机,我们只需要让电机按照我们的预想来进行旋转就可以了。当然这只是最简单的目标。由此可以计划出以下几点渐进的目标。
已实现的阶段目标:

  1. 简单运行电机: 电机连接电脑,然后随便转转
  2. solidworks建模部分: 用3d建模软件画出3d模型
  3. 3d打印部分: 找淘宝商家打印出3d模型,验证下是否可以安装
  4. 前端项目篇部分: 使用threejs + vue3做个类似数字孪生,简单点说就是网页操作控制电机旋转
  5. 尝试运行单片机: 运行乐鑫官方的helloworld项目
  6. 蓝牙部分: 单片机运行乐鑫官方的蓝牙项目,并使用手机连接通信
  7. TWAI部分: 单片机运行乐鑫官方的TWAI项目,并与小米电机通信
  8. 改写单片机项目: 根据需求需要将蓝牙项目和TWAI项目组合一起,以实现网页控制esp32单片机向小米电机发送信息
  9. 完善前端项目: 使用网页控制电机精准旋转
  10. 组装,最终成品: 组装电路,并于上位机(网页)联调
  11. 总结
  12. 未来展望
  13. github
  14. 参考文章

未来的进阶目标:

  1. 优化3d模型
  2. 动画系统(关键帧),引入贝塞尔曲线,简化机械臂的运动控制,就像css的动画那样(待验证可行性)
  3. 单片机改为Rust语言开发(应该可行: 参考文章 )
  4. 尝试使用人工智能

备注:除了前端,其余技术未接触过,所以分的比较细。

所有物料的清单:

名称 数量 价格
小米电机 3 1497元
ESP32 N16R8 1 54.90元
TJA1050 CAN模块 1 6元
艾迈斯线XT30插头带线 3 12.6元
usb-can转换器 1 79元
24V10A电源 1 46.56元
DC5.5转XT30转接线 1 10元
黑色绝缘胶带 1 3.97元
杜邦线 2板 5.26元
螺丝螺母套装 1 17.91元
SolidWorks软件安装费 1 10元
3D打印模型 1 100元
12V电源 1 11.16元
12V转5V转换器 1 4.9元
硅胶线18AWG(红黑2米) 2 3.4元
艾迈斯线XT30连接器线公转母 2 4.2元
合计 1866.86

成品:

成品.jpg

简单运行电机

物料:
小米电机 * 1
usb-can转换器 * 1
24V10A电源 * 1
艾迈斯线 * 1
DC5.5转XT30转接线 * 1

连线部分:

电机-连线.jpg
can-usb参数设置:

can-usb At指令设置.png
打开小米电机的上位机:

小米电机上位机.png
电机成功连接,点击jog运动的加、减号,电机就可以动起来了:

电机随便转转.gif

solidworks建模部分

本想做个酷一些的模型,类似稚辉君那种。刚刚简单入门,画起来还是太麻烦了,先画个简单的,以后再优化吧。
下臂:
sw-下臂.png

中臂:
sw-中臂.png

上臂:
sw-上臂.png

组合起来如下图:sw-装配体.png
还有个底座,底板没做(暂时这么称呼),大概是用来控制重心的,使机械臂能稳定立在桌面上,以下是稚慧君的机械臂模型,参考看看:

sw-底座,底板-参考.png

3d打印部分

导出stl文件格式给淘宝的3d打印商家:

sw-stl格式.jpg

模型设计的问题很多,比如螺丝孔太细,管壁太薄变形,蛮多缺陷,先勉强用用。如下图所示:

sw-3d打印.jpg简单测试下能不能匹配:

电机-壳.jpg
还行,勉强能用,就是有点丑。这章到此结束。

前端项目部分

目标: 做个网页用来控制机械臂
技术: vue3 + threejs
步骤:
1.solidworks导出3d模型:在此使用glb格式(不使用.gltf格式的原因是:gltf格式会生成.bin文件,在本地预览失败,在项目中调用失败,可能是用的方式不对)。

glb和gltf本地预览.png

  1. 搭建前端工程并搭建threejs场景(参考文章):

three基础场景.png4. 调整模型并添加控制轴:因为机械臂的每个关节只能固定一个方向旋转(参考文章: ),所以用transformControls来控制,并将其他其他轴隐藏。如下图所示:

网页控制机械臂_失败版.gif5. 模型调整:重新使用solidworks装配并导出(因为臂需要和电机是一个整体),并且调整模型的原点,以便更容易控制。

  1. 控制优化:想要的效果是骨骼系统那样,比如: three.js示例,但solidworks中似乎没有骨骼系统,所以就先用THREE.Object3D做个简单控制(参考文章 ),transformControls好像无法控制Object3D,所以舍弃,效果如下图所示。

简单控制机械臂.gif
这章先简单到此为止,接下来做单片机部分。

尝试运行单片机

目标: 运行esp32官方的hello world项目
物料选择:

  1. esp32-s3 N8R16单片机,主要原因是支持蓝牙,wifi和can通信。
  2. 一个typeC线
    如下图:

单片机物料.png

esp-idf环境搭建
  1. 根据官方文档: esp快速入门我选择的是在线安装,按默认配置安装,

espidf安装.png

espidf安装完成.png2. 成功之后桌面会有快捷键图标:

桌面图标.png
3. 双击运行会自动初始化环境,如下图,接下来编译,烧录都会用到,记得使用管理员身份运行:

espidf安装成功prompt.png

尝试helloworld
  1. 从esp-idf项目中复制出helloworld项目,粘贴至你的项目文件夹下(猜测:以后的项目应该也是去改官方的项目示例):

helloword项目路径.png2. 接下来就是按照官方文档步骤来走:
设置编译的目标平台: 成功则会生成build文件夹:

esp32设置编译目标平台.pngesp32设置编译成功.png编译代码: 编译成功之后在build文件夹下会生成.bin文件。

esp编译代码.pngesp编译成功.png烧录: 将程序写入单片中,端口可不写,会自动寻找(注意: typeC插入esp32的正确的口,我用的是COM口):

esp烧录.png

④ 执行命令idf.py monitor可看到程序的输出:

esp烧录运行.png

3.到此算简单入门了,接下来就是探索蓝牙和TWAI协议了。

蓝牙部分

目标: 运行官网的蓝牙Master项目,并尝试用LightBlue(功能类比postman,用来调试蓝牙)连接蓝牙并传输数据
物料
esp32-s3单片机 * 1
usb-typec线 * 1
蓝牙的概念:
参考文章

运行官方项目:

  1. 从esp-idf项目中复制出gatt-server项目,粘贴至你的项目文件夹下项目所在地址.png
  2. 烧录进单片机并运行
  3. 手机下载lightblue软件

lightblue.jpg4. 打开lightblue并搜索单片机的蓝牙信号:

lightblue搜索单片机.jpg5. 连接成功之后,尝试调用蓝牙的write方法给单片机传递消息:

蓝牙消息发送和接收.png6. 单片机成功接收到来自的lightblue传输的消息,这章简单到此结束。

TWAI部分

目标: 运行官方的TWAI-network项目,并尝试使用can调试工具互通数据,然后尝试发送指令给小米点机

物料
esp32-s3单片机 * 1
usb-typec线 * 1
TJA1050 can模块 * 1
杜邦线 * 6根
usb-can转换器 * 1
小米电机 * 1

Q&A:
问 为什么要用这个TWAI?
答:因为小米电机是can协议通信,而esp32-s3支持TWAI协议,TWAI兼容can2.0协议(TWAI官方文档)。
问:需要其他什么硬件支持吗?
答:esp32-s3单片机只有TWAI模块,但没有can消息的收发器,所以单片机上要额外接入TJA1050 can收发器。

CAN协议

CAN协议的参考文章,简单看下协议结构,如下图所示(小米电机采用扩展数据帧):can协议结构.png

硬件连线

这个正确的硬件连线搞了好几天,查阅了很多资料,没人指导的情况下有点懵。正确的连线情况是:运行官方项目,电脑的can调试器能收到相关消息。
单片机与TJA1050连线如下图:

TWAI-TJA1050连线.jpg

TJA1050与usb转can转换器连线如下图,usb-can转换器需要短接内置的120欧姆电阻:

TWAI-TJA1050-usb-can连线.jpg

检查单片机与TJA1050收发器是否正常连接
  1. 运行官方示例项目: twai_self_test

TWAI-test项目地址.png

  1. 正常运行的console:

TWAI-self-test.png

测试收发消息
  1. 正确连接TJA1050与usb-can转换器的线
  2. 运行官方示例项目: twai_network_master

TWAI-master路径.png3. 修改参数: usb-can转换器的波特率要与单片机的一致(在此我设置为1Mbits), 还有就是设置为包模式。can调试工具是usb-can转换器商家开发的,不同商家软件界面可能不同,但功能参数应该是相似的。如下图:

TWAI-usb-can参数设置.png4. 修改代码: 因为twai_network_master项目需要与twai_network_slave项目配合,意味着需要两块单片机(但我只有一块,想法是esp32跟usb-can转换器通信,后来证实可行)。展示了ping信号,收发消息和命令任务控制等功能。而我只需要最简单的收发消息,故简化代码逻辑,并与usb-can转换器通信。如下图:

TWAI正常收发消息.png
到此算是一个重要标志节点:可以使用TWAI收发消息。

发送指令控制小米电机
  1. 观察下小米上位机通信时的指令,来分析其中含义:

电机初始化指令通信.png2. 好吧,根据can协议看了很久没分析出什么(好像不同的can master有不同的格式),又看了遍官方文档,有单独的文件进行说明:

串口协议说明.png3. 数据帧蛮复杂,看的很迷糊,参考解析的文章,由此可以得到简单的四条测试指令:
① 使能指令:AA 01 00 08 03 00 FD 15 00 00 00 00 00 00 00 00 7A
简单解析下:

帧头: AA   是否扩展帧: 01中的1   data_length_code: 08中的8   帧id: 03 00 FD 15 即通信类型3 发送者的的canid为0xFD 接收者的canid为0x15   数据: 00 00 00 00 00 00 00 00   帧尾: 7A  

② jog +5rad/s指令:AA 01 00 08 12 00 FD 15 05 70 00 00 07 01 6A AA 7A
③ jog停止指令: AA 01 00 08 12 00 FD 15 05 70 00 00 07 00 7F FF 7A
④ 停止指令: AA 01 00 08 04 00 FD 15 00 00 00 00 00 00 00 00 7A

  1. 连线小米电机(电机的波特率也是1Mbits),运行项目,效果如下图:

esp32控制电机运行.gif
这章简单到此结束。

改写单片机项目

目标: 将蓝牙和TWAI项目集合在一起,单片机的最终功能是:① 接收蓝牙信息,将其转成can消息帧,传给小米电机 ② 将接收到的can消息帧通过蓝牙进行回传。如下图:

序列图.png

关键代码
  1. 将接收到蓝牙消息用TWAI发送:
        unsigned int frameId = 0;    int frameData[8];    for(int i = 0; i < param->write.len; i++) {      printf("遍历蓝牙%d值: %d", i, param->write.value[i]);      if(i < 4) {         frameId = frameId * 16 * 16 + param->write.value[i];      }else {         frameData[i - 4] = param->write.value[i];      }    }    printf("frameId%u值: %d, --- %d", frameId, frameData[0], frameData[7]);        ESP_LOGI(EXAMPLE_TAG, "-----发送can数据指令------");            emitMsg(frameId, frameData);    ESP_LOGI(EXAMPLE_TAG, "-----发送can数据指令结束-----");
  1. 将收到的can信号用蓝牙转发给网页:
static void twai_receive_task(void *arg){    while (1) {      rx_task_action_t action;      xQueueReceive(rx_task_queue, &action, portMAX_DELAY);      while(1) {          twai_message_t rx_msg;          twai_receive(&rx_msg, portMAX_DELAY);          ESP_LOGI(EXAMPLE_TAG, "接收到can信号: %lu", rx_msg.identifier);                    uint8_t notify_data[8];          for (int i = 0; i < sizeof(rx_msg.data); ++i) {              notify_data[i] = rx_msg.data[i];          }                              esp_ble_gatts_send_indicate(current_gatts_if, current_param->write.conn_id, gl_profile_tab[PROFILE_A_APP_ID].char_handle,                                  sizeof(notify_data), notify_data, false);        }    }    vTaskDelete(NULL);}

完善前端项目

目标: 与单片机建立蓝牙通信,新增控制面板模块,用网页控制小米电机的运行。

界面如下:

界面__黑暗模式.png

关键代码:

蓝牙部分:

  let available = await navigator.bluetooth.getAvailability(); ...  let device = await navigator.bluetooth.requestDevice({    filters: [{ namePrefix: "ESP" }],    optionalServices: [0x00ff],  }); let server = await device.gatt.connect(); let service = await server.getPrimaryService(0x00ff); let characteristic = await service.getCharacteristic(0xff01); ...  characteristic.writeValue(cmdFrame); ... characteristic.addEventListener("characteristicvaluechanged", (e) => {    console.log("蓝牙Notification通知: ", e, "值:");    let CAN_frame_data = Array.from(new Uint8Array(e.target.value.buffer)).map(      (num) => {        return num.toString(16);      }    );  
位置模式的指令生成

看文档之后,发现位置模式是最符合网页中控制面板的操作。

  1. 首先看看文档解释:

位置模式简介.png

  1. 其次看看如何运行这个位置模式:

位置模式指令运行顺序.png可以看到,设置位置模式主要是需要分发两条指令:①. 发送设置速度指令,即设置limit_spd②. 发送设置具体位置,即loc_ref

  1. 最后来仔细看看指令的内容:

指令表格表头.png指令0717.png指令0716.png

速度指令的速度(0-30rad)需要4个字节来表示
角度指令的弧度(rad)需要4个字节来表示

  1. 看了这么久rad,解析下它是什么(参考文章):

弧度图例.png

  1. 但是我们的控制面板用的是角度,看下互换比例(参考文章):

角度弧度互换.png
C = 2Πr, 弧度rad = 角度 * Π / 180 就行了

  1. 到此可以计算出角度对应弧度的值,看个指令的例子(参考文章):

CODE3:设置参数limit_spd速度5rad/s
AA 01 00 08 12 00 01 05 17 70 00 00 00 00 A0 40 7A

CODE4:设置参数loc_ref 位置12.5rad
AA 01 00 08 12 00 01 05 16 70 00 00 00 00 48 41 7A

  1. 那如何转换成can数据帧需要的格式,比如上边例子: 5 -> 00 00 A0 40 和 12.5 -> 00 00 48 41
    ① 去查查这个浮点数的原理简析
    ②看到参考文章里的IEEE754是不是很熟悉,js的Number是遵循这个标准,那可以直接用吗?
    ③查了MDN:number属于64位,意味着是8个字节,与我们想要的有些不同,没发现可以如何调用的方法,遂放弃

JavaScript 的Number类型是一个双精度 64 位二进制格式 IEEE 754值,类似于 Java 或者 C# 中的double。这意味着它可以表示小数值,但是存储的数字的大小和精度有一些限制。简而言之,IEEE 754 双精度浮点数使用 64 位来表示 3 个部分:

④ 造轮子:

export function double2floatCode(num) {  if(num == 0) {    return fillZero([], 32);  }  let num0b = (num).toString(2).replace('-', '');  console.log('num: ', num, '二进制: ', num0b);  let integer0b = num0b.split('.')[0];  let fraction = num0b.split('.').length === 2 ? num0b.split('.')[1] : [];      let signal = num >= 0 ? '0' : '1';    let e = 127;    let first1Index = num0b.indexOf('1');    let pointIndex = num0b.indexOf('.') || 0;      if(first1Index < pointIndex) {        e += pointIndex - 1;  }else {    e += pointIndex - first1Index;  }  let e0bArr = e.toString(2).split('');    if(e < 128) {    e0bArr = fillZero(e0bArr, 8, 'head');  }            let mantissaBasis = first1Index + (first1Index < pointIndex ? 1: 0)    let mantissa = [...integer0b, ...fraction].slice(mantissaBasis, mantissaBasis + 23);      console.log('single', signal, 'e', e, 'mantissa: ', mantissa.join(''));    const floatCodeArr = [signal, ...e0bArr, ...fillZero(mantissa, 23)];    console.log('floatCodeArr: ', floatCodeArr.join(''));  return floatCodeArr;      function fillZero(arr, len, type = 'tail') {    if(type === 'head') {      return [...new Array(len - arr.length).fill('0'), ...arr];    }else {            return [...arr , ...new Array(len - arr.length).fill('0')]    }      }}export function numToUnit8Array(num) {  let floatCodeArr = double2floatCode(num);    let binaryArr = [];  for(let i = 0; i < floatCodeArr.length; i = i + 8) {        let binaryStr = '0b' + floatCodeArr.slice(i, i + 8).join('');    binaryArr.push(binaryStr);  }    return new Uint8Array(binaryArr.reverse());}

⑤使用豆包ai辅助验证下函数的输出:

豆包.jpg

豆包2.jpg

  1. 到此可以生成位置模式指令需要的数据,所以可以进行下一步:生成指令,用策略模式来优化下:
export function generateCMD(type, params = {}) {                                        let TWAI_id = new Array(4).fill(0);  let TWAI_data = new Array(8).fill(0);  var Strategies =  {    enable: () => {      TWAI_id = [0x03, 0x00, 0xfd, 0x15];    },    disable: () => {      TWAI_id = [0x04, 0x00, 0xfd, 0x15];    },    jog5: () => {      TWAI_id = [0x12, 0x00, 0xfd, 0x15];      TWAI_data = [0x05, 0x70, 0x00, 0x00, 0x07, 0x01, 0x95, 0x54];    },    jog0: () => {      TWAI_id = [0x12, 0x00, 0xfd, 0x15];      TWAI_data = [0x05, 0x70, 0x00, 0x00, 0x07, 0x00, 0x7f, 0xff];    },        limit_spd: ({motorId, limit_spd}) => {      TWAI_id = [0x12, 0x00, 0xfd, motorId];      TWAI_data = [0x17, 0x70, 0x00, 0x00, ...numToUnit8Array(limit_spd)];    },        loc_ref: ({motorId, loc_ref}) => {      TWAI_id = [0x12, 0x00, 0xfd, motorId];      TWAI_data = [0x16, 0x70, 0x00, 0x00, ...numToUnit8Array(loc_ref)];    },        run_mode: ({motorId, run_mode}) => {      TWAI_id = [0x12, 0x00, 0xfd, motorId];      TWAI_data = [0x05, 0x70, 0x00, 0x00, run_mode, 0x00, 0x00, 0x00];    }  }  Strategies[type](params);  console.log('策略模式: ', type , [...TWAI_id, ...TWAI_data]);  return Uint8Array.from([...TWAI_id, ...TWAI_data]);}
  1. 运行位置模式需要四条指令: ①设置运行模式 ②使能 ③设置速度 ④设置位置 。为了方便使用,也为了未来的扩展,因为还有其他三种模式:运控模式,电流模式,速度模式。所以在此使用建造者模式来优化:
class MotorRotateBuilder {  motorId  cmdArr = []  setMotorId(motorId){    this.motorId = motorId;    return this;  }  enableCmd() {    this.cmdArr.push(generateCMD('enable'))    return this;  }    disableCmd() {    this.cmdArr.push(generateCMD('disable'))    return this;  }  runModeCmd() { }    coreCmd() { }  getCmdArr() {    return this.cmdArr;  }}export class LocBuilder extends MotorRotateBuilder {  constructor({ motorId }) {    super();    this.setMotorId(motorId)  }  runModeCmd() {    let cmd = generateCMD('run_mode', {motorId: this.motorId, run_mode: 1})    this.cmdArr.push(cmd);    return this;  }  coreCmd({limit_spd, loc_ref}) {    let limit_spd_cmd = generateCMD('limit_spd', {motorId: this.motorId, limit_spd});    let loc_ref_cmd = generateCMD('loc_ref', {motorId: this.motorId, loc_ref});    this.cmdArr.push(limit_spd_cmd, loc_ref_cmd);    return this;  }}export function LocDirector({motorId, limit_spd, loc_ref}) {    if(limit_spd > 30) {    limit_spd = 30;  }else if(limit_spd < 0) {    limit_spd = 0;  }  let loc_instance = LocBuilder({motorId}).runModeCmd().enableCmd().coreCmd({ limit_spd, loc_ref });  return loc_instance.getCmdArr();}
  1. 查看生成的数据:

建造者生成的指令.png11. 发现已经生成了预期想要的数据了,接下来循环发送(暂时每条指令间隔300ms,实测不能发太快)给小米电机

  1. 将控制面板绑定相关函数和参数,实现拨动滑块控制电机,如下图:

网页控制电机.gif

  1. 这章到此结束

组装,最终成品

目标: 将三个电机的电源用并联方式连接起来,连接TWAI通信网络,组装上3d打印的壳子,完成最终成品。

线路连接: 如下图:

线路.jpg

遇到的问题:

  1. 期间twai网络消息,只有当连接一个电机时候才可通信(查阅资料can通信在1Mbps下的通信距离能达到10公里,而我这一米不到就不行了),尝试设想很多原因,比如:
    ① 连线的问题,查阅资源发现要两侧添加电阻,故又去买了120欧姆的电阻。
    ② can通信线的问题,图中用的电源线和can线用的一个类型的线。
    ③ 是否是线路的电阻太高,导致线路失败。

    最终发现是:tja1050收发器需要5v的电压供电,而esp32只能提供3.3v的电压,导致电压与电阻不匹配。那为何在之前3.3v时候也能通信工作? 查看商品详情页:

tja1050商品详情.jpg所以,现在需要一个5v的电源(12v电源转5v)如下:

12v电源+电源转换器.jpg

  1. twai单片机项目问题,由于是之前拷贝官方项目改的,达到了能简单运行的目标。但现在经常发送can报文时候报错,但官方的代码太过复杂,无法正确有效修改,到了不得不解决的时候了。于是重写该部分项目代码:待写文章。
  2. 蓝牙经常会爆内存,导致单片机直接重启。解决方法是 ① menuconfig中调整蓝牙的BTC_TASK的栈容量 ② 官方项目会把蓝牙接收到write消息写入单片机的持久化磁盘中,在此优化部分逻辑并删掉相关代码。

组装后的成品:

最终成品.gif

存在的明显问题:

  1. 设计的3d模型存在很多问题,比如重心偏移站不住,电线无法走线,螺丝孔不好安装等,基本一整个都有问题,需要重新设计和做仿真测试。
  2. 蓝牙的交互还存在问题,消息发快点就不行了,需要优化。
  3. can信号的发送丢失严重,不知道是什么原因。
  4. 操作体验优化,目前由于蓝牙和can网络,导致数据交互有点慢。
  5. 网页的接收数据模块(左下角那块)需要优化,目前有点鸡肋。

总结

从5.16做到8.06,接近三个月,做的东西太粗糙了,没想象中的好,但主要逻辑是通的,勉强算完成吧。

未来展望

如果都做的顺利的话,下一个可能会做灵巧手,如下图:

灵巧手.jpg

github

地址: github.com/5sj-666/Rob…

参考文章

SolidWorks 打开stp/step不显示解决办法: zhuanlan.zhihu.com/p/661600802
机械臂cad参考: github.com/Tony607/Cyb…
esp 指南: docs.espressif.com/projects/es…
awg解释: baike.baidu.com/item/AWG/36…
螺纹孔深度标注: zhidao.baidu.com/question/16…
esp32 can总线: lastminuteengineers.com/esp32-can-b…
idf指令: docs.espressif.com/projects/es…
threejs在vue中用法: cloud.tencent.com/developer/a…
机械臂控制: www.cnblogs.com/zhnblog/p/6…
esp-idf readme: github.com/espressif/e…
esp32-tja1050: lingshunlab.com/book/esp32/…
蓝牙角色区分: blog.csdn.net/weixin_4335…
蓝牙术语解读: www.nordicsemi.cn/news/blueto…
蓝牙ATT介绍(英文版本): lpccs-docs.renesas.com/tutorial-cu…
PC蓝牙调试器: blog.csdn.net/Ikaros_521/…
STM32F103用CAN通讯驱动小米电机讲解: www.bilibili.com/video/BV1Z1…
小米电机指令详解: blog.csdn.net/qq_35003234…
详解蓝牙传输: blog.csdn.net/chengbaojin…
通过web控制蓝牙设备(入门): segmentfault.com/a/119000001…
float浮点数原理: blog.csdn.net/whyel/artic…
rad详解: blog.csdn.net/janechel/ar…
设置BTC_TASK大小: docs.espressif.com/projects/es…
获取twai状态:blog.csdn.net/HeroGuo_JP/…