前言:
关于一些前期配置,测试情况请观看下面这个博客
驱动串口总线舵机


从今天开始学习
幻尔科技总线舵机通信协议
关于这部分在SerialServoCmd文件里

一、舵机指令包格式

在这里插入图片描述

帧头: 连续收到两个 0x55 ,表示有数据包到达。
ID: 每个舵机都有一个 ID 号。ID 号范围 0~253,转换为十六进制 0x00~0xFD。广播 ID: ID 号 254(0xFE) 为广播 ID,若控制器发出的 ID 号为 254(0xFE),所有的舵机均接收指令,但都不返回应答信息,(读取舵机 ID 号除外,具体说明参见下面指令介绍)以防总线冲突。
数据长度: 等于待发送的数据(包含本身一个字节)长度,即数据长度 Length加 3 等于这一包指令的长度,从帧头到校验和。
指令: 控制舵机的各种指令,如位置、速度控制等。
参数: 除指令外需要补充的控制信息。
校验和: 校验和 Checksum,计算方法如下:
Checksum = ~ (ID + Length + Cmd+ Prm1 + … Prm N)若括号内的计算和超出 255,则取最低的一个字节,“~”表示取反。

说明:
数据长度为 数据长度+指令+ 参数+校验和 参数个数 = 数据长度-3

二、串口舵机连接

1.硬件连接

在这里插入图片描述

2.74HC126D

74HC126D介绍:
功能描述:
在这里插入图片描述
可以看出,当OE输出高电平时 输入是高电平那么输出就是高电平,输入是低电平输出就是低电平。

OE为低电平时不管输入状态是什么,输出都是高阻抗关断状态(抽象理解为悬空)

高阻输出一般是指数字电路输出时不为高电平或低电平,而是相当于断开的一种状态,输出点的电位由后面的电路决定。

这个芯片的作用就是,当需要写入的时候,拉低TX_CON,这样,串口TX发送什么,输出就是什么。拉高RX_CON,这样,串口接收RX就相当于悬空,什么也不干。接收数据也是如此.

三、关于ctypes

关于ctypes的介绍
ctypes

四、串口舵机命令代码

# 串口舵机的命令
#!/usr/bin/python3
# encoding: utf-8
import serial # 导入串口库
import pigpio # 导入pigpio库  由c语言编写的库函数 并提供python接口
import time # 导入时间库
import ctypes # ctypes是Python的一个外部库,提供和C语言兼容的数据类型,可以很方便地调用C DLL中的函数。

LOBOT_SERVO_FRAME_HEADER         = 0x55  # 机架头
LOBOT_SERVO_MOVE_TIME_WRITE      = 1     # 移动时间写入
LOBOT_SERVO_MOVE_TIME_READ       = 2     # 移动时间读
LOBOT_SERVO_MOVE_TIME_WAIT_WRITE = 7     # 移动时间等待写
LOBOT_SERVO_MOVE_TIME_WAIT_READ  = 8     # 移动时间等待读
LOBOT_SERVO_MOVE_START           = 11    # 移动开始
LOBOT_SERVO_MOVE_STOP            = 12    # 移动停止
LOBOT_SERVO_ID_WRITE             = 13    # 舵机ID写
LOBOT_SERVO_ID_READ              = 14    # 舵机ID读
LOBOT_SERVO_ANGLE_OFFSET_ADJUST  = 17    # 角度偏移调整
LOBOT_SERVO_ANGLE_OFFSET_WRITE   = 18    # 角度偏移写
LOBOT_SERVO_ANGLE_OFFSET_READ    = 19    # 角度偏移读
LOBOT_SERVO_ANGLE_LIMIT_WRITE    = 20    # 角度限制写
LOBOT_SERVO_ANGLE_LIMIT_READ     = 21    # 角度限制读
LOBOT_SERVO_VIN_LIMIT_WRITE      = 22    # VIN限制写
LOBOT_SERVO_VIN_LIMIT_READ       = 23    # VIN限制读
LOBOT_SERVO_TEMP_MAX_LIMIT_WRITE = 24    # TEMP最大限度写
LOBOT_SERVO_TEMP_MAX_LIMIT_READ  = 25    # TEMP最大限度读
LOBOT_SERVO_TEMP_READ            = 26    # TEMP读
LOBOT_SERVO_VIN_READ             = 27    # VIN读
LOBOT_SERVO_POS_READ             = 28    # POS读
LOBOT_SERVO_OR_MOTOR_MODE_WRITE  = 29    # 模式写
LOBOT_SERVO_OR_MOTOR_MODE_READ   = 30    # 模式读
LOBOT_SERVO_LOAD_OR_UNLOAD_WRITE = 31    # 加载或卸载写
LOBOT_SERVO_LOAD_OR_UNLOAD_READ  = 32    # 加载或卸载读
LOBOT_SERVO_LED_CTRL_WRITE       = 33    # LED控制写
LOBOT_SERVO_LED_CTRL_READ        = 34    # LED控制读
LOBOT_SERVO_LED_ERROR_WRITE      = 35    # LED错误写
LOBOT_SERVO_LED_ERROR_READ       = 36    # LED错误读

pi = pigpio.pi()  # 初始化pigpio库,创建一个实例
# ser=serial.Serial("/dev/ttyUSB0",9600,timeout=0.5) #使用USB连接串行口
# ser=serial.Serial("/dev/ttyAMA0",9600,timeout=0.5) #使用树莓派的GPIO口连接串行口
serialHandle = serial.Serial("/dev/ttyAMA0", 115200)  # 初始化串口, 波特率为115200


# 使用pigpio配置驱动串口的引脚模式为输出
def portInit():  # 配置用到的IO口
 # 说明  pigpio使用的是BCM编码  
    # RX_CON和TX_CON是使能信号  来决定TX还是RX输出servo signal
    pi.set_mode(17, pigpio.OUTPUT)  # 配置RX_CON 即 GPIO17 为输出
    pi.write(17, 0)# GPIO 17 低电平
    pi.set_mode(27, pigpio.OUTPUT)  # 配置TX_CON 即 GPIO27 为输出
    pi.write(27, 1) # GPIO 27 高电平


portInit() # 执行端口初始化

# 配置为串口写模式
def portWrite():  # 配置单线串口为输出
    pi.write(27, 1)  # 拉高TX_CON 即 GPIO27  串口发送的是什么输出就是什么
    pi.write(17, 0)  # 拉低RX_CON 即 GPIO17  相当于悬空接受引脚,什么都不干

# 配置为串口读模式
def portRead():  # 配置单线串口为输入
    pi.write(17, 1)  # 拉高RX_CON 即 GPIO17
    pi.write(27, 0)  # 拉低TX_CON 即 GPIO27 悬空

# 复位,重新打开串口
def portRest(): # 端口复位
    time.sleep(0.1)   # 延迟100us
    serialHandle.close()# 关闭串口
    pi.write(17, 1)
    pi.write(27, 1)
    serialHandle.open() # 打开串口
    time.sleep(0.1)

# 校验和 = ~(ID + Length+ Cmd + pr1+ .. + prn) 若超出255,则取最低的一个字节
def checksum(buf):
    # 计算校验和
    sum = 0x00
    for b in buf:  # 求和
        sum += b # 累加
    sum = sum - 0x55 - 0x55  # 去掉命令开头的两个 0x55
    sum = ~sum  # 取反
    return sum & 0xff  # 取最低的一个字节

# 串口舵机写命令
# 指令包格式:0x55,0x55 ID号 数据长度,指令,参数1...参数n,校验和
# 数据长度等于待发送的数据(包含本身1个字节)
def serial_serro_wirte_cmd(id=None, w_cmd=None, dat1=None, dat2=None):
    portWrite()  # 端口写
        # bytearray() 方法返回一个新字节数组。这个数组里的元素是可变的,并且每个元素的值范围: 0 <= x < 256
    '''
    如果 source 为整数,则返回一个长度为 source 的初始化数组;
    如果 source 为字符串,则按照指定的 encoding 将字符串转换为字节序列;
    如果 source 为可迭代类型,则元素必须为[0 ,255] 中的整数;
    如果 source 为与 buffer 接口一致的对象,则此对象也可以被用于初始化 bytearray。
    如果没有输入任何参数,默认就是初始化数组为0个元素。
    '''
    # b'\x55\x55')    [0x55,0x55]
    buf = bytearray(b'\x55\x55')  # 帧头   buf = [0x55,0x55]
    buf.append(id) # buf = [0x55,0x55,id]
    # 指令长度
    if dat1 is None and dat2 is None:# dat1和dat2都为空
        buf.append(3) # buf = [0x55,0x55,id,3]  3个表示指令长度 指令 检验和
    elif dat1 is not None and dat2 is None: # dat1不为空dat2为空
        buf.append(4) # buf = [0x55,0x55,id,4] 4个表示指令长度,指令 dat1(低8位),校验和
    elif dat1 is not None and dat2 is not None: #dat1 和dat2都不为空
        buf.append(7)  # buf = [0x55,0x55,id,7] 7个表示指令长度,指令,dat1(高8位,低8位) dat2(高8位,低8位),校验和

    buf.append(w_cmd) # 把指令也添加到列表  # buf = [0x55,0x55,id,x,w_cmd]
    # 写数据
    if dat1 is None and dat2 is None: # dat1和dat2都为空
        pass # buf = [0x55,0x55,id,x,w_cmd]
    elif dat1 is not None and dat2 is None: # dat1不为空dat2为空
     # (dat1 & 0xff) 取最低位
        buf.append(dat1 & 0xff)  # buf = [0x55,0x55,id,x,w_cmd,dat1]
    elif dat1 is not None and dat2 is not None: # dat1 和 dat2 都不为空
        buf.extend([(0xff & dat1), (0xff & (dat1 >> 8))])  # 分低8位 高8位 放入缓存
        buf.extend([(0xff & dat2), (0xff & (dat2 >> 8))])  # 分低8位 高8位 放入缓存
    # 可能是buf = [0x55,0x55,id,x,w_cmd,dat1,dat2]
          
    # 校验和
    buf.append(checksum(buf)) # 到这数据有三种情况
    serialHandle.write(buf)  # 发送给串口
    
# 串口舵机读命令 先发送读命令 再接收,不单独使用
def serial_servo_read_cmd(id=None, r_cmd=None):
    portWrite() # 端口写
    buf = bytearray(b'\x55\x55')  # 帧头
    buf.append(id)# 添加舵机ID到列表
    buf.append(3)  # 指令长度
    buf.append(r_cmd)  # 指令
    buf.append(checksum(buf))  # 校验和
    serialHandle.write(buf)  # 发送
    time.sleep(0.00034) # 延迟 3.4us

# 获取指定读取命令的数据
def serial_servo_get_rmsg(cmd):
    serialHandle.flushInput()  # 清空接收缓存
    portRead()  # 将单线串口配置为输入
    time.sleep(0.005)  # 稍作延时,等待接收完毕
    count = serialHandle.inWaiting()    # 获取接收缓存中的字节数
    if count != 0:  # 如果接收到的数据不空
        recv_data = serialHandle.read(count) # 读取count字节个数据
        try:
        # recv_data[2] : id   recv_data[3]: 数据长度 
            if recv_data[0] == 0x55 and recv_data[1] == 0x55 and recv_data[4] == cmd: # 帧头正确  命令符合 匹配
                dat_len = recv_data[3]  # 数据长度
                serialHandle.flushInput()  # 清空接收缓存
                # 数据长度为 数据长度+指令+ 参数+校验和  参数个数 = 数据长度-3
                if dat_len == 4: # dat1为一个8位的参数dat2为空
                    # print ctypes.c_int8(ord(recv_data[5])).value    # 转换成有符号整型
                    return recv_data[5] # 返回这个8位的参数
                elif dat_len == 5:  # dat1为一个16位的参数dat2为空
                    pos = 0xffff & (recv_data[5] | (0xff00 & (recv_data[6] << 8))) # 一个16位的数据
                    return ctypes.c_int16(pos).value
                elif dat_len == 7: # dat1位16位的参数,dat2为16位的参数
                    pos1 = 0xffff & (recv_data[5] | (0xff00 & (recv_data[6] << 8)))
                    pos2 = 0xffff & (recv_data[7] | (0xff00 & (recv_data[8] << 8)))
                    return ctypes.c_int16(pos1).value, ctypes.c_int16(pos2).value
            else:  # 数据不正确,不符合接收数据的格式
                return None
        except BaseException as e:
            print(e) # 打印异常
    else:  # 接收数据为空
        serialHandle.flushInput()  # 清空接收缓存
        return None

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐