转载自: 如何实现智能小车的通信协议 - 古月居
通信协议设计
在Part2《如何开发智能小车的驱动器软件》中我们已经介绍了什么是通讯方式的协议层,它是指通信双方对数据传送控制的一种约定,协议中包括数据格式、同步方式、传送速度、传送步骤、检纠错方式以及控制字符定义等,通信双方必须共同遵守。更具体一些常见的协议如下图所示
接下来以OriginCar机器人为例,介绍其通信协议。
首先关于几个细节需要和大家拉平,上位机一般而言指的控制器,以OriginCar为例是RDK X3开发板;下位机一般指的是驱动板,此处指OriginCar主板。
第二点,上下位机的指令发什么由开发者决定,往往取决于希望获取什么样的数据,例如对于下位机而言需要的是一个速度指令,那么上位机只需要发布X、Y、Z轴的速度指令即可,下位机负责对数据帧进行解析。
第三点,数据帧包含了帧头、帧尾以及校验位,关于这三位方式很多,但是上下位机一定要做到同步,这样确保数据解析正确。
上位机下发指令
下位机上发指令
通信协议的下位机实现
以和上位机通信的串口3为例,关于USART3
初始化等在上一个Part已经和大家介绍.
波特率设置
// system.c
void systemInit(void)
{
...
uart3_init(115200);
...
}
//usartx.c
void uart3_init(u32 bound)
{
...
USART_InitStructure.USART_BaudRate = bound;
...
}
数据接收
关于数据的接收,其实是通过串口中断进行获取,并根据上位机下发指令进行数据解析,即可得到上位机下发的X、Y、Z速度,并将该速度给到运动控制进行实现。
int USART3_IRQHandler(void)
{
static u8 Count=0;
u8 Usart_Receive;
if(USART_GetITStatus(USART3, USART_IT_RXNE) != RESET)
{
Usart_Receive = USART_ReceiveData(USART3); //读取数据
if(Time_count<CONTROL_DELAY)
return 0;
Receive_Data.buffer[Count]=Usart_Receive;
if(Usart_Receive == FRAME_HEADER||Count>0)
Count++;
else
Count=0;
if (Count == 11) //验证数据包的长度
{
Count=0; //为串口数据重新填入数组做准备
if(Receive_Data.buffer[10] == FRAME_TAIL) //验证数据包的帧尾
{
//数据异或位校验计算,模式0是发送数据校验
if(Receive_Data.buffer[9] ==Check_Sum(9,0)) {
float Vz;
PS2_ON_Flag=0;
Remote_ON_Flag=0;
APP_ON_Flag=0;
CAN_ON_Flag=0;
Usart1_ON_Flag=0;
Usart5_ON_Flag=0;
command_lost_count=0;
Move_X=XYZ_Target_Speed_transition(Receive_Data.buffer[3],Receive_Data.buffer[4]);
Move_Y=XYZ_Target_Speed_transition(Receive_Data.buffer[5],Receive_Data.buffer[6]);
Vz =XYZ_Target_Speed_transition(Receive_Data.buffer[7],Receive_Data.buffer[8]);
Move_Z=Vz_to_Akm_Angle(Move_X, Vz);
}
}
}
}
return 0;
}
float XYZ_Target_Speed_transition(u8 High,u8 Low)
{
//数据转换的中间变量
short transition;
//将高8位和低8位整合成一个16位的short型数据
transition=((High<<8)+Low);
return transition/1000+(transition%1000)*0.001; //单位转换, mm/s->m/s
}
数据发送
发送数据的方式:将要发送的数据打包到一个数组中,数组的长度是 24 字节,使用串口逐位发送出去。因为串口一次只能发送一个 8 位(1 个字节)的数据,因此 2 个字节(short 型)的数据会拆分成高 8 位和低 8 位发送。
以下给出数据封装代码,其作用就是将下位机上发指令按照代码的形式进行封装。,
void data_transition(void)
{
Send_Data.Sensor_Str.Frame_Header = FRAME_HEADER; //帧头
Send_Data.Sensor_Str.Frame_Tail = FRAME_TAIL; //帧尾
Send_Data.Sensor_Str.X_speed = ((MOTOR_A.Encoder+MOTOR_B.Encoder)/2)*1000;
Send_Data.Sensor_Str.Y_speed = 0;
Send_Data.Sensor_Str.Z_speed = ((MOTOR_B.Encoder-MOTOR_A.Encoder)/Wheel_spacing)*1000;
//加速度计三轴加速度
Send_Data.Sensor_Str.Accelerometer.X_data= accel[1]; //加速度计Y轴转换到ROS坐标X轴
Send_Data.Sensor_Str.Accelerometer.Y_data=-accel[0]; //加速度计X轴转换到ROS坐标Y轴
Send_Data.Sensor_Str.Accelerometer.Z_data= accel[2]; //加速度计Z轴转换到ROS坐标Z轴
//角速度计三轴角速度
Send_Data.Sensor_Str.Gyroscope.X_data= gyro[1]; //角速度计Y轴转换到ROS坐标X轴
Send_Data.Sensor_Str.Gyroscope.Y_data=-gyro[0]; //角速度计X轴转换到ROS坐标Y轴
if(Flag_Stop==0)
//如果电机控制位使能状态,那么正常发送Z轴角速度
Send_Data.Sensor_Str.Gyroscope.Z_data=gyro[2];
else
//如果机器人是静止的(电机控制位失能),那么发送的Z轴角速度为0
Send_Data.Sensor_Str.Gyroscope.Z_data=0;
//电池电压(这里将浮点数放大一千倍传输,相应的在接收端在接收到数据后也会缩小一千倍)
Send_Data.Sensor_Str.Power_Voltage = Voltage*1000;
Send_Data.buffer[0]=Send_Data.Sensor_Str.Frame_Header; //帧头
Send_Data.buffer[1]=Flag_Stop; //小车软件失能标志位
//小车三轴速度,各轴都拆分为两个8位数据再发送
Send_Data.buffer[2]=Send_Data.Sensor_Str.X_speed >>8;
Send_Data.buffer[3]=Send_Data.Sensor_Str.X_speed ;
Send_Data.buffer[4]=Send_Data.Sensor_Str.Y_speed>>8;
Send_Data.buffer[5]=Send_Data.Sensor_Str.Y_speed;
Send_Data.buffer[6]=Send_Data.Sensor_Str.Z_speed >>8;
Send_Data.buffer[7]=Send_Data.Sensor_Str.Z_speed ;
//IMU加速度计三轴加速度,各轴都拆分为两个8位数据再发送
Send_Data.buffer[8]=Send_Data.Sensor_Str.Accelerometer.X_data>>8;
Send_Data.buffer[9]=Send_Data.Sensor_Str.Accelerometer.X_data;
Send_Data.buffer[10]=Send_Data.Sensor_Str.Accelerometer.Y_data>>8;
Send_Data.buffer[11]=Send_Data.Sensor_Str.Accelerometer.Y_data;
Send_Data.buffer[12]=Send_Data.Sensor_Str.Accelerometer.Z_data>>8;
Send_Data.buffer[13]=Send_Data.Sensor_Str.Accelerometer.Z_data;
//IMU角速度计三轴角速度,各轴都拆分为两个8位数据再发送
Send_Data.buffer[14]=Send_Data.Sensor_Str.Gyroscope.X_data>>8;
Send_Data.buffer[15]=Send_Data.Sensor_Str.Gyroscope.X_data;
Send_Data.buffer[16]=Send_Data.Sensor_Str.Gyroscope.Y_data>>8;
Send_Data.buffer[17]=Send_Data.Sensor_Str.Gyroscope.Y_data;
Send_Data.buffer[18]=Send_Data.Sensor_Str.Gyroscope.Z_data>>8;
Send_Data.buffer[19]=Send_Data.Sensor_Str.Gyroscope.Z_data;
//电池电压,拆分为两个8位数据发送
Send_Data.buffer[20]=Send_Data.Sensor_Str.Power_Voltage >>8;
Send_Data.buffer[21]=Send_Data.Sensor_Str.Power_Voltage;
//数据校验位计算,模式1是发送数据校验
Send_Data.buffer[22]=Check_Sum(22,1);
Send_Data.buffer[23]=Send_Data.Sensor_Str.Frame_Tail; //帧尾
}
通信协议的上位机实现
数据接收
bool origincar_base::Get_Sensor_Data()
{
short transition_16 = 0, j = 0, Header_Pos = 0, Tail_Pos = 0;
uint8_t Receive_Data_Pr[RECEIVE_DATA_SIZE] = {0};
Stm32_Serial.read(Receive_Data_Pr,sizeof (Receive_Data_Pr));
for (j = 0; j < 24; j++) {
if (Receive_Data_Pr[j] == FRAME_HEADER)
Header_Pos=j;
else if (Receive_Data_Pr[j] == FRAME_TAIL)
Tail_Pos = j;
}
if (Tail_Pos == (Header_Pos + 23)) {
memcpy(Receive_Data.rx, Receive_Data_Pr, sizeof(Receive_Data_Pr));
} else if (Header_Pos == (1 + Tail_Pos)) {
for (j = 0;j < 24; j++)
Receive_Data.rx[j] = Receive_Data_Pr[(j+Header_Pos) % 24];
} else {
return false;
}
Receive_Data.Frame_Header = Receive_Data.rx[0];
Receive_Data.Frame_Tail = Receive_Data.rx[23];
if (Receive_Data.Frame_Header == FRAME_HEADER) {
if (Receive_Data.Frame_Tail == FRAME_TAIL) {
if (Receive_Data.rx[22] == Check_Sum(22,READ_DATA_CHECK)||(Header_Pos == (1 + Tail_Pos))) {
Receive_Data.Flag_Stop=Receive_Data.rx[1];
Robot_Vel.X = Odom_Trans(Receive_Data.rx[2],Receive_Data.rx[3]);
Robot_Vel.Y = Odom_Trans(Receive_Data.rx[4],Receive_Data.rx[5]);
Robot_Vel.Z = Odom_Trans(Receive_Data.rx[6],Receive_Data.rx[7]);
Mpu6050_Data.accele_x_data = IMU_Trans(Receive_Data.rx[8],Receive_Data.rx[9]);
Mpu6050_Data.accele_y_data = IMU_Trans(Receive_Data.rx[10],Receive_Data.rx[11]);
Mpu6050_Data.accele_z_data = IMU_Trans(Receive_Data.rx[12],Receive_Data.rx[13]);
Mpu6050_Data.gyros_x_data = IMU_Trans(Receive_Data.rx[14],Receive_Data.rx[15]);
Mpu6050_Data.gyros_y_data = IMU_Trans(Receive_Data.rx[16],Receive_Data.rx[17]);
Mpu6050_Data.gyros_z_data = IMU_Trans(Receive_Data.rx[18],Receive_Data.rx[19]);
Mpu6050.linear_acceleration.x = Mpu6050_Data.accele_x_data / ACCEl_RATIO;
Mpu6050.linear_acceleration.y = Mpu6050_Data.accele_y_data / ACCEl_RATIO;
Mpu6050.linear_acceleration.z = Mpu6050_Data.accele_z_data / ACCEl_RATIO;
Mpu6050.angular_velocity.x = Mpu6050_Data.gyros_x_data * GYROSCOPE_RATIO;
Mpu6050.angular_velocity.y = Mpu6050_Data.gyros_y_data * GYROSCOPE_RATIO;
Mpu6050.angular_velocity.z = Mpu6050_Data.gyros_z_data * GYROSCOPE_RATIO;
transition_16 = 0;
transition_16 |= Receive_Data.rx[20]<<8;
transition_16 |= Receive_Data.rx[21];
Power_voltage = transition_16/1000+(transition_16 % 1000)*0.001;
return true;
}
}
}
return false;
}
数据发送
void origincar_base::Cmd_Vel_Callback(const geometry_msgs::msg::Twist::SharedPtr twist_aux)
{
short transition;
Send_Data.tx[0]=FRAME_HEADER;
Send_Data.tx[1] = 0;
Send_Data.tx[2] = 0;
transition=0;
transition = twist_aux->linear.x*1000;
Send_Data.tx[4] = transition;
Send_Data.tx[3] = transition>>8;
transition=0;
transition = twist_aux->linear.y*1000;
Send_Data.tx[6] = transition;
Send_Data.tx[5] = transition>>8;
transition=0;
transition = (twist_aux->angular.z)*1000;
Send_Data.tx[8] = transition;
Send_Data.tx[7] = transition>>8;
Send_Data.tx[9]=Check_Sum(9,SEND_DATA_CHECK);
Send_Data.tx[10]=FRAME_TAIL;
try {
if (akm_cmd_vel == "none") {
Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx));
}
} catch (serial::IOException& e) {
RCLCPP_ERROR(this->get_logger(),("Unable to send data through serial port"));
}
}
构建机器人ROS 驱动
其实完成上下位机的解析与数据封装发送后,ROS相关的驱动就自然而然出来了。大家可以看到在origincar_base
中是小车上位机ROS底层驱动实现的代码,笔者当时写的时候关注的点由两个,第一个哪些数据是之后开发需要的,第二个点是如何让代码变得更好用。
哪些数据是之后开发需要的?
其实主要是传感器数据,例如电压的电压值,对于开发者而言,电压值有可能影响后续功能能否正常使用,如何让开发者不看小车就知道小车电量如何是一件很重要的事情。
void origincar_base::Publish_Voltage()
{
std_msgs::msg::Float32 voltage_msgs;
static float Count_Voltage_Pub = 0;
if (Count_Voltage_Pub++ > 10) {
Count_Voltage_Pub = 0;
voltage_msgs.data = Power_voltage;
voltage_publisher->publish(voltage_msgs);
}
}
又好比里程计和IMU值,同样重要,甚至此处可能涉及了里程计和IMU校正的问题,所以自然也需要做一层封装.
void origincar_base::Publish_ImuSensor()
{
sensor_msgs::msg::Imu Imu_Data_Pub;
Imu_Data_Pub.header.stamp = rclcpp::Node::now();
Imu_Data_Pub.header.frame_id = gyro_frame_id;
Imu_Data_Pub.orientation.x = Mpu6050.orientation.x;
Imu_Data_Pub.orientation.y = Mpu6050.orientation.y;
Imu_Data_Pub.orientation.z = Mpu6050.orientation.z;
Imu_Data_Pub.orientation.w = Mpu6050.orientation.w;
Imu_Data_Pub.orientation_covariance[0] = 1e6;
Imu_Data_Pub.orientation_covariance[4] = 1e6;
Imu_Data_Pub.orientation_covariance[8] = 1e-6;
Imu_Data_Pub.angular_velocity.x = Mpu6050.angular_velocity.x;
Imu_Data_Pub.angular_velocity.y = Mpu6050.angular_velocity.y;
Imu_Data_Pub.angular_velocity.z = Mpu6050.angular_velocity.z;
Imu_Data_Pub.angular_velocity_covariance[0] = 1e6;
Imu_Data_Pub.angular_velocity_covariance[4] = 1e6;
Imu_Data_Pub.angular_velocity_covariance[8] = 1e-6;
Imu_Data_Pub.linear_acceleration.x = Mpu6050.linear_acceleration.x;
Imu_Data_Pub.linear_acceleration.y = Mpu6050.linear_acceleration.y;
Imu_Data_Pub.linear_acceleration.z = Mpu6050.linear_acceleration.z;
imu_publisher->publish(Imu_Data_Pub);
}
void origincar_base::Publish_Odom()
{
tf2::Quaternion q;
q.setRPY(0,0,Robot_Pos.Z);
geometry_msgs::msg::Quaternion odom_quat=tf2::toMsg(q);
origincar_msg::msg::Data robotpose;
origincar_msg::msg::Data robotvel;
nav_msgs::msg::Odometry odom;
odom.header.stamp = rclcpp::Node::now();
odom.header.frame_id = odom_frame_id;
odom.child_frame_id = robot_frame_id;
odom.pose.pose.position.x = Robot_Pos.X;
odom.pose.pose.position.y = Robot_Pos.Y;
odom.pose.pose.position.z = 0.0;
odom.pose.pose.orientation = odom_quat;
odom.twist.twist.linear.x = Robot_Vel.X;
odom.twist.twist.linear.y = Robot_Vel.Y;
odom.twist.twist.angular.z = Robot_Vel.Z;
robotpose.x = Robot_Pos.X;
robotpose.y = Robot_Pos.Y;
robotpose.z = Robot_Pos.Z;
robotvel.x = Robot_Vel.X;
robotvel.y = Robot_Vel.Y;
robotvel.z = Robot_Vel.Z;
odom_publisher->publish(odom);
robotpose_publisher->publish(robotpose);
robotvel_publisher->publish(robotvel);
}
谈到这,再来说如何对里程计和IMU进行校正
大家可以看到这一段:
void origincar_base::Control()
{
...
Robot_Pos.X+=1.03*(Robot_Vel.X * cos(Robot_Pos.Z) - Robot_Vel.Y * sin(Robot_Pos.Z)) * Sampling_Time;
Robot_Pos.Y+=1.125*(Robot_Vel.X * sin(Robot_Pos.Z) + Robot_Vel.Y * cos(Robot_Pos.Z)) * Sampling_Time;
Robot_Pos.Z+=Robot_Vel.Z * Sampling_Time;
...
}
此处对小车具体执行的X、Y、Z进行了控制,大家可以如何解决呢?让小车真实走1m距离,然后观察odom位置是多少,如果存在差异,那么说明控制需要修正。比如此时实际运行的距离是1.023m,而里程计反馈的是1.139m,那么计算线速度的校准系数 = 实际运行距离 / 里程计反馈距离 比如:1.023/1.139 = 0.898;这个值就应该填入以上1.03的位置。
如何让代码变得更好用?
这里其实解决了一个很重要的问题,例如底盘挂掉了,或者你希望一关掉底盘程序车就停下来该如何做。
此处用了一些小ticks,即引入了一个新线程去做调度。
void sigintHandler(int sig)
{
sig = sig;
printf("OriginCar shutdown...\n");
serial::Serial Stm32_Serial;
Stm32_Serial.setPort("/dev/ttyACM0");
Stm32_Serial.setBaudrate(115200);
serial::Timeout _time = serial::Timeout::simpleTimeout(2000);
Stm32_Serial.setTimeout(_time);
Stm32_Serial.open();
SEND_DATA Send_Data;
if (Stm32_Serial.isOpen()) {
Send_Data.tx[0]=FRAME_HEADER;
Send_Data.tx[1] = 0;
Send_Data.tx[2] = 0;
Send_Data.tx[4] = 0;
Send_Data.tx[3] = 0;
Send_Data.tx[6] = 0;
Send_Data.tx[5] = 0;
Send_Data.tx[7] = 0;
Send_Data.tx[8] = 0;
int check_sum = 0;
for (int k = 0; k < 9; k++) {
check_sum = check_sum^Send_Data.tx[k];
}
Send_Data.tx[9]=check_sum;
Send_Data.tx[10]=FRAME_TAIL;
try {
Stm32_Serial.write(Send_Data.tx,sizeof (Send_Data.tx));
} catch (serial::IOException& e) {
}
}
// 关闭ROS2接口,清除资源
rclcpp::shutdown();
}