前言

本帖主要分享的是幻尔的串行舵机,看完之后能够对幻尔的串行舵机系列有一个更深的理解

幻尔官方给的驱动代码是已经完成了位置模式的代码编写,但是没有完成电机模式的代码编写

补充:舵机的电机模式是让舵机能够跟电机一样一直转。

(所有的资料均来自幻尔官方,有需要的请购买幻尔的产品,如果有佬自己自制舵机驱动板然后用的其他品牌的舵机也可以参考我的开发例程进行自行编写驱动)

二编

https://github.com/Mingyang-Sheep/Hiwonder-Serial-Servo-STM32.git

代码已上传GitHub,可以直接clone进行二次开发,直接使用(请大家点点Star~~感谢支持!)

一、硬件介绍

幻尔的舵机都需要一个驱动板连接在主控或者上位机再连接到舵机(划重点,幻尔有两款驱动板)

一款叫做TTL调试板,另一款叫做总线舵机控制器,这两个驱动板都可以与上位机(STM32、树莓派,Jestnano等)进行串口通信完成舵机控制,为了后续介绍方便,一个我就叫做调试板,另一个我就叫控制器,需要区分的时候我也会重点标记

(1)两种驱动板接线介绍

总线舵机控制器与STC15W4K32S4 单片机接线示意图(取自幻尔官方例程,主要看接线)

TTL调试板和STM32F103C8T6最小系统板的接线示意图

这两个板子都是串口通信,和上位机写好串口通信就可以完成控制

(2)总线舵机控制器

这块板子可以连接通过usb连接电脑,通过电脑端的上位机实现舵机的调试和设置读取等

(为什么它可以完成?因为他内部封装了代码!!划重点)后面也会介绍这个部分

/******************************************************
 * FileName:      LobotServoController.cpp
 ** Company:       乐幻索尔
 * Date:           2016/07/02  16:53
 * Description:    Lobot舵机控制板二次开发库,本文件包含
                 了此库的具体实现
 *****************************************************/

#include "LobotServoController.h"

#define GET_LOW_BYTE(A) (uint8_t)((A))
//宏函数 获得A的低八位
#define GET_HIGH_BYTE(A) (uint8_t)((A) >> 8)
//宏函数 获得A的高八位
#define BYTE_TO_HW(A, B) ((((uint16_t)(A)) << 8) | (uint8_t)(B))
//宏函数 以A为高八位 B为低八位 合并为16位整形

LobotServoController::LobotServoController()
{
	//初始化运行中动作组号为0xFF,运行次数为0,运行中标识为假,电池电压为0
	numOfActinGroupRunning = 0xFF;
	actionGroupRunTimes = 0;
	isRunning = false;
	batteryVoltage = 0;
#if defined(__AVR_ATmega32U4__)  //for Arduino Leonardo,Micro....
	SerialX = &Serial1;
#else
	SerialX = &Serial;
#endif
}

LobotServoController::LobotServoController(HardwareSerial &A)
{
	LobotServoController();
	SerialX = &A;
}
LobotServoController::~LobotServoController()
{
}

/*********************************************************************************
 * Function:  moveServo
 * Description: 控制单个舵机转动
 * Parameters:   sevoID:舵机ID,Position:目标位置,Time:转动时间
                    舵机ID取值:0<=舵机ID<=31,Time取值: Time > 0
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::moveServo(uint8_t servoID, uint16_t Position, uint16_t Time)
{
	uint8_t buf[11];
	if (servoID > 31 || !(Time > 0)) { //舵机ID不能打于31,可根据对应控制板修改
		return;
	}
	buf[0] = FRAME_HEADER;                   //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = 8;                              //数据长度=要控制舵机数*3+5,此处=1*3+5
	buf[3] = CMD_SERVO_MOVE;                 //填充舵机移动指令
	buf[4] = 1;                              //要控制的舵机个数
	buf[5] = GET_LOW_BYTE(Time);             //填充时间的低八位
	buf[6] = GET_HIGH_BYTE(Time);            //填充时间的高八位
	buf[7] = servoID;                        //舵机ID
	buf[8] = GET_LOW_BYTE(Position);         //填充目标位置的低八位
	buf[9] = GET_HIGH_BYTE(Position);        //填充目标位置的高八位

	SerialX->write(buf, 10);
}

/*********************************************************************************
 * Function:  moveServos
 * Description: 控制多个舵机转动
 * Parameters:   servos[]:舵机结体数组,Num:舵机个数,Time:转动时间
                    0 < Num <= 32,Time > 0
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::moveServos(LobotServo servos[], uint8_t Num, uint16_t Time)
{
	uint8_t buf[103];    //建立缓存
	if (Num < 1 || Num > 32 || !(Time > 0)) {
		return; //舵机数不能为零和大与32,时间不能为零
	}
	buf[0] = FRAME_HEADER;    //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = Num * 3 + 5;     //数据长度 = 要控制舵机数*3+5
	buf[3] = CMD_SERVO_MOVE;  //填充舵机移动指令
	buf[4] = Num;             //要控制的舵机个数
	buf[5] = GET_LOW_BYTE(Time); //取得时间的低八位
	buf[6] = GET_HIGH_BYTE(Time); //取得时间的高八位
	uint8_t index = 7;
	for (uint8_t i = 0; i < Num; i++) { //循环填充舵机ID和对应目标位置
		buf[index++] = servos[i].ID; //填充舵机ID
		buf[index++] = GET_LOW_BYTE(servos[i].Position); //填充目标位置低八位
		buf[index++] = GET_HIGH_BYTE(servos[i].Position);//填充目标位置高八位
	}
	SerialX->write(buf, buf[2] + 2); //发送帧,长度为数据长度+两个字节的帧头
}

/*********************************************************************************
 * Function:  moveServos
 * Description: 控制多个舵机转动
 * Parameters:   Num:舵机个数,Time:转动时间,...:舵机ID,转动角,舵机ID,转动角度 如此类推
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::moveServos(uint8_t Num, uint16_t Time, ...)
{
	uint8_t buf[128];
	va_list arg_ptr = NULL;
	va_start(arg_ptr, Time); //取得可变参数首地址
	if (Num < 1 || Num > 32 || (!(Time > 0)) || arg_ptr == NULL) {
		return; //舵机数不能为零和大与32,时间不能为零,可变参数不能为空
	}
	buf[0] = FRAME_HEADER;     //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = Num * 3 + 5;      //数据长度 = 要控制舵机数 * 3 + 5
	buf[3] = CMD_SERVO_MOVE;   //舵机移动指令
	buf[4] = Num;              //要控制舵机数
	buf[5] = GET_LOW_BYTE(Time); //取得时间的低八位
	buf[6] = GET_HIGH_BYTE(Time); //取得时间的高八位
	uint8_t index = 7;
	for (uint8_t i = 0; i < Num; i++) { //从可变参数中取得并循环填充舵机ID和对应目标位置
		uint16_t tmp = va_arg(arg_ptr, uint16_t); //可参数中取得舵机ID
		buf[index++] = GET_LOW_BYTE(tmp); //貌似avrgcc中可变参数整形都是十六位
		                                  //再取其低八位
		uint16_t pos = va_arg(arg_ptr, uint16_t); //可变参数中取得对应目标位置
		buf[index++] = GET_LOW_BYTE(pos); //填充目标位置低八位
		buf[index++] = GET_HIGH_BYTE(pos); //填充目标位置高八位
	}
	va_end(arg_ptr);     //置空arg_ptr
	SerialX->write(buf, buf[2] + 2); //发送帧
}


/*********************************************************************************
 * Function:  runActionGroup
 * Description: 运行指定动作组
 * Parameters:   NumOfAction:动作组序号, Times:执行次数
 * Return:       无返回
 * Others:       Times = 0 时无限循环
 **********************************************************************************/
void LobotServoController::runActionGroup(uint8_t numOfAction, uint16_t Times)
{
	uint8_t buf[7];
	buf[0] = FRAME_HEADER;   //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = 5;      //数据长度,数据帧除帧头部分数据字节数,此命令固定为5
	buf[3] = CMD_ACTION_GROUP_RUN; //填充运行动作组命令
	buf[4] = numOfAction;      //填充要运行的动作组号
	buf[5] = GET_LOW_BYTE(Times); //取得要运行次数的低八位
	buf[6] = GET_HIGH_BYTE(Times); //取得要运行次数的高八位
	SerialX->write(buf, 7);      //发送数据帧
}

/*********************************************************************************
 * Function:  stopActiongGroup
 * Description: 停止动作组运行
 * Parameters:   Speed: 目标速度
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::stopActionGroup(void)
{
	uint8_t buf[4];
	buf[0] = FRAME_HEADER;     //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = 2;                //数据长度,数据帧除帧头部分数据字节数,此命令固定为2
	buf[3] = CMD_ACTION_GROUP_STOP; //填充停止运行动作组命令

	SerialX->write(buf, 4);      //发送数据帧
}

/*********************************************************************************
 * Function:  setActionGroupSpeed
 * Description: 设定指定动作组的运行速度
 * Parameters:   NumOfAction: 动作组序号 , Speed:目标速度
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::setActionGroupSpeed(uint8_t numOfAction, uint16_t Speed)
{
	uint8_t buf[7];
	buf[0] = FRAME_HEADER;     //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = 5;                //数据长度,数据帧除帧头部分数据字节数,此命令固定为5
	buf[3] = CMD_ACTION_GROUP_SPEED; //填充设置动作组速度命令
	buf[4] = numOfAction;      //填充要设置的动作组号
	buf[5] = GET_LOW_BYTE(Speed); //获得目标速度的低八位
	buf[6] = GET_HIGH_BYTE(Speed); //获得目标熟读的高八位

	SerialX->write(buf, 7);      //发送数据帧
}


/*********************************************************************************
 * Function:  setAllActionGroupSpeed
 * Description: 设置所有动作组的运行速度
 * Parameters:   Speed: 目标速度
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::setAllActionGroupSpeed(uint16_t Speed)
{
	setActionGroupSpeed(0xFF, Speed); //调用动作组速度设定,组号为0xFF时设置所有组的速度
}

/*********************************************************************************
 * Function:  getBatteryVoltage
 * Description: 发送获取电池电压命令
 * Parameters:   无输入参数
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::getBatteryVoltage()
{
	uint8_t buf[4];
 uint8_t recvBuf[32];
  uint8_t count = 10000; 
  byte i = 0;
  int ret = 1;
  
	buf[0] = FRAME_HEADER;         //填充帧头
	buf[1] = FRAME_HEADER;
	buf[2] = 2;                   //数据长度,数据帧除帧头部分数据字节数,此命令固定为2
	buf[3] = CMD_GET_BATTERY_VOLTAGE; //填充后的电池电压命令

	SerialX->write(buf, 4);        //发送数据帧
}

/*********************************************************************************
 * Function:  setServoUnload
 * Description: 设置舵机掉电
 * Parameters:   numOfServos:要控制舵机的个数,ID:舵机的ID 
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::setServoUnload(uint8_t numOfServos, ...)
{
  uint8_t buf[128];
  va_list arg_ptr = NULL;
  va_start(arg_ptr, numOfServos); //取得可变参数首地址
  if (numOfServos < 1 || numOfServos > 32  || arg_ptr == NULL) {
    return; //舵机数不能为零和大与32,时间不能为零,可变参数不能为空
  }
  buf[0] = FRAME_HEADER;     //填充帧头
  buf[1] = FRAME_HEADER;
  buf[2] = numOfServos + 3;      //数据长度 = 要控制舵机数 * 3 + 5
  buf[3] = CMD_MULT_SERVO_UNLOAD;   //舵机移动指令
  buf[4] = numOfServos;              //要控制舵机数

  uint8_t index = 5;
  for (uint8_t i = 0; i < numOfServos; i++) { //从可变参数中取得并循环填充舵机ID和对应目标位置
     uint16_t getId = va_arg(arg_ptr, uint16_t); //可参数中取得舵机
     buf[index++] = GET_LOW_BYTE(getId);
  }
  va_end(arg_ptr);     //置空arg_ptr
  SerialX->write(buf, buf[2] + 2); //发送帧
}

/*********************************************************************************
 * Function:  getServosPos
 * Description: 读取多个舵机指令
 * Parameters:   numServos:舵机的个数,id:要读取舵机的ID
 * Return:       
 * Others:
 **********************************************************************************/
uint8_t LobotServoController::getServosPos(uint8_t numOfServos, ...)
{
  int count = 10000;
  int ret;
  int i = 0;
  uint8_t buf[128];
  va_list arg_ptr = NULL;
  va_start(arg_ptr, numOfServos); //取得可变参数首地址
  if (numOfServos < 1 || numOfServos > 32  || arg_ptr == NULL) {
    return; //舵机数不能为零和大与32,时间不能为零,可变参数不能为空
  }
  buf[0] = FRAME_HEADER;     //填充帧头
  buf[1] = FRAME_HEADER;
  buf[2] = numOfServos  + 3;      //数据长度 = 要控制舵机数 * 3 + 3
  buf[3] = CMD_MULT_SERVO_POS_READ;   //舵机移动指令
  buf[4] = numOfServos;              //要控制舵机数

  uint8_t index = 5;
  for (uint8_t i = 0; i < numOfServos; i++) { //从可变参数中取得并循环填充舵机ID和对应目标位置
     uint16_t getId = va_arg(arg_ptr, uint16_t); //可参数中取得舵机
     buf[index++] = GET_LOW_BYTE(getId);
  }
  va_end(arg_ptr);     //置空arg_ptr
  SerialX->write(buf, buf[2] + 2); //发送帧
}

/*********************************************************************************
 * Function:  receiveHandle
 * Description: 处理串口接收数据
 * Parameters:   无输入参数
 * Return:       无返回
 * Others:
 **********************************************************************************/
void LobotServoController::receiveHandle()
{
	uint8_t buf[16];
	static uint8_t len = 0;
	static uint8_t getHeader = 0;
	if (!SerialX->available())
		return;
	//如果没有数据则返回
	do {
		switch (getHeader) {
		case 0:
			if (SerialX->read() == FRAME_HEADER)
				getHeader = 1;
			break;
		case 1:
			if (SerialX->read() == FRAME_HEADER)
				getHeader = 2;
			else
				getHeader = 0;
			break;
		case 2:
			len = SerialX->read();
			getHeader = 3;
			break;
		case 3:
			if (SerialX->readBytes(buf, len - 1) > 0)
				getHeader = 4;
			else{
				len = 0;
				getHeader = 0;
				break;
			}
		case 4:
			switch (buf[0]) {
			case BATTERY_VOLTAGE: //电池电压指令
				batteryVoltage = BYTE_TO_HW(buf[2], buf[1]); //高低八位组合成电池电压
				break;
			case ACTION_GROUP_RUNNING:         //有动作组被运行
				numOfActinGroupRunning = buf[1]; //获得运行中动作组号
				actionGroupRunTimes = BYTE_TO_HW(buf[3], buf[2]); //高低八位组合成运行次数
				isRunning = true; //设置运行中标识为真
				break;
			case ACTION_GROUP_STOPPED: //动作组停止
			case  ACTION_GROUP_COMPLETE://动作组运行完成
				isRunning = false; //设置运行中标识为假
				numOfActinGroupRunning = 0xFF; //设置运行中动作组号为0xFF
				actionGroupRunTimes = 0; //设置运行次数为0
				break;
      case CMD_MULT_SERVO_POS_READ:
        for(int i=2,j=0;i<buf[1]*3+2;i+=3)
        {
            servosPos[j++] = buf[i];
            servosPos[j++] = BYTE_TO_HW(buf[i+2], buf[i+1]);
        }
			default:
				break;
			}
		default:
			len = 0;
			getHeader = 0;
			break;
		}
	} while (SerialX->available());
}

这个是在幻尔官方给的资料里我找到的,是一个处理串口通信接收数据的c++代码,通过这个代码就可以处理从上位机发过来的数据从而完成舵机的控制,这个代码是封装进了这个控制器里面的。里面操控舵机的函数看不懂没关系,后面会在协议介绍里讲解

(3)TTL调试板

这块板子就比较朴实无华,直接就是什么数据进去,什么数据出来,起到的是一个桥梁的作用

可以通过usb接电脑使用串口助手进行调试(直接按协议给他发数组)

但是没有总线舵机控制器的和电脑端上位机通信的功能

二、协议介绍(以位置模式、电机模式为例)

幻尔官方是有一个舵机通信协议手册的,能够控制舵机的各个指标都在里面,写的非常详细

这张图是最为核心的一张图,意思是只要舵机收到了类似上图格式的数据,就能完成舵机的控制

(再强调一遍!第二部分的协议讲解是舵机直接收到的数据,不然后面自己二次开发会踩坑!)

然后再整个手册的后续就会详细讲解各个指令Cmd的数值是多少,数据长度Length是多少,以及参数个数和参数所代表的含义,后续我会附上位置模式和电机模式的进行讲解。

上图就是各个Cmd的指令值以及数据Length参数

(1)位置模式

位置模式是大家常用的转角,让舵机旋转固定的角度。

如果需要直接发送数据给舵机,需要参考这个官方给的介绍,上图是位置模式的指令参数介绍

使总线舵机ID1舵机角度转到500,时间为1000ms,命令如下

55 55 01 07 01 F4 01 E8 03 crc(crc得参考协议介绍的指令包格式自己算)

55 帧头
55 帧头
01 ID号
07 数据长度
01 指令Cmd (参考上图的表格
F4 参数1
01 参数2
E8 参数3
03 参数4
crc 校验和(自己算)

(2)电机模式

电机模式是让舵机跟电机一样一直旋转

设置总线舵机1为电机控制模式,转动速度为100

55 55 01 07 1D 01 00 64 00 crc(crc得参考协议介绍的指令包格式自己算)

55 帧头
55 帧头
01 ID号
07 数据长度
1D 指令Cmd (参考上图的表格
01 参数1
00 参数2
64 参数3
00 参数4
crc 校验和(自己算)

看到这里应该对舵机收到的数据有比较清晰的认知了,如果还不是很了解,请多看几遍第二部分

三、电机模式代码详解(即二次开发介绍)

(1)二次开发硬件介绍

二次开发可以选择第一部分说明的两块驱动板,两块是都可以进行二次开发的

但是两种板子开发的代码不是完全一样的!

(a)总线舵机控制器

意思就是这个控制器当中还有一份处理数据的协议,接收和发送都有他的协议,这个幻尔官方也写好了

拿位置模式的驱动指令格式对比舵机的驱动协议指令格式

这个协议跟刚才舵机的协议是有出入的,详细的可以倒回去对比看舵机的,这里不过多赘述

如果需要用这块板子进行二次开发,参考控制器的通信协议发送数据包的协议格式。

但是这块板子当中封装的函数并没有涉及到处理Cmd为电机模式的函数(参考第一部分的(2))

所以不能只在STM32端进行开发,如果需要用这块板子开发,同时需要修改板子当中封装的函数,增加一个发送电机模式的函数,如果不要求使用电机模式,参照协议手册开发即可。

(b)TTL调试板

我选择的也是这块TTL调试板,这样我就可以自己在STM32端解决发送数据的格式,直接发送按照舵机的驱动协议进行发送数据即可

(2)代码展示

void setServoMotorMode(uint8_t servoID, uint8_t rotationMode, int16_t speed)
{
// 所有变量声明移到函数开头(C89标准要求)
    uint16_t speedComplement;
    uint8_t speedLow;
    uint8_t speedHigh;
    uint8_t length;
    uint16_t sum;
    uint8_t checksum;
    // 参数合法性校验
    if (servoID > 31) return;
    if (rotationMode > 1) return;
    
    // 速度范围校验
    if ((rotationMode == 0 && (speed < -1000 || speed > 1000)) ||
        (rotationMode == 1 && (speed < -50 || speed > 50))) {
        return;
    }
    
    // 处理速度值(转换为补码并拆分字节)
    speedComplement = (uint16_t)speed;
    speedLow = speedComplement & 0xFF;
    speedHigh = (speedComplement >> 8) & 0xFF;
    
    // 计算校验和
    length = 7;  // 数据长度=参数个数+1
    sum = servoID + length + 29 + 1 + rotationMode + speedLow + speedHigh;
    checksum = ~(sum & 0xFF);
    
    // 按协议顺序填充指令数组
    LobotTxBuf[0] = 0x55;               // 帧头1
    LobotTxBuf[1] = 0x55;               // 帧头2
    LobotTxBuf[2] = servoID;            // ID号
    LobotTxBuf[3] = length;             // 数据长度
    LobotTxBuf[4] = 0x1D;               // 指令值(29的十六进制)
    LobotTxBuf[5] = 0x01;               // 参数1:舵机模式(1=电机控制模式)
    LobotTxBuf[6] = rotationMode;       // 参数2:转动模式 0表示固定占空比模式,1表示固定转速模式
    LobotTxBuf[7] = speedLow;           // 参数3:速度低8位
    LobotTxBuf[8] = speedHigh;          // 参数4:速度高8位
    LobotTxBuf[9] = checksum;           // 校验和

    // 发送指令(共10字节)
    uartWriteBuf(LobotTxBuf, 10);
}

使用幻尔官方的模板进行修改,找到LobotServoController.c文件,在最下方增加我写的这个函数,有一些模板可能在最后这个串口发送指令这里采用的函数不一样,进行修改即可。

幻尔官方的舵机还有很多功能,但是他给出来的模板代码当只完成了主要的一些舵机功能,剩下的有需要可以自己根据第二部分的协议介绍进行开发

参考资料

1.幻尔总线舵机通信协议幻尔科技总线舵机通信协议.pdf

2.幻尔STM32单片机结合TTL调试版控制总线舵机转动的开发流程

3.总线舵机控制板通信协议

后面两个资料购买了幻尔的产品之后找官方要一份就行,我没找到网上的链接。

 

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐