这里写目录标题
- 下位机与PID调试助手传输的原理
- 代码讲解(基于正点原子)
- 解析数据接受和数据发送的底层函数
- 数据接受
- 数据帧格式
- 环形数组以及怎么找到它的帧头位置
- crc校验
- 数据发送
- 数据上传函数
通过前两节文章,我已经了解了基本的pid算法,现在在完成了电机编码测速,pid控制电机转速的前提,我们来解析一下下位机是如何pid调试助手进行数据传递的.
下位机与PID调试助手传输的原理
首先用c#写一个PID调试助手,然后拟定好传递数据的通信协议,然后下位机配置好串口,下位机使用串口发送指令给上位机解析(按照通信协议),上位机发送数据,下位通过串口接受到上位机传来的指令,进行解析。
代码讲解(基于正点原子)
正点原子的代码使F4系列的,我修改成了stm32F103ZET6的板子代码,
现在我们先来讲一讲函数怎么调用.
首先看到主函数里的程序。
/**
****************************************************************************************************
* @file main.c
* @author 正点原子团队(ALIENTEK)
* @version V1.0
* @date 2021-10-16
* @brief 直流有刷电机速度环PID控制 实验
* @license Copyright (c) 2020-2032, 广州市星翼电子科技有限公司
****************************************************************************************************
* @attention
*
* 实验平台:正点原子 F407电机开发板
* 在线视频:www.yuanzige.com
* 技术论坛:www.openedv.com/forum.php
* 公司网址:www.alientek.com
* 购买地址:openedv.taobao.com
*
****************************************************************************************************
*/
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/KEY/key.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/DC_MOTOR/dc_motor.h"
#include "./BSP/ADC/adc.h"
#include "./BSP/TIMER/dcmotor_tim.h"
#include "./BSP/PID/pid.h"
#include "./DEBUG/debug.h"
extern uint8_t g_run_flag;
void lcd_dis(void);
int main(void)
{
uint8_t key;
uint16_t t;
uint8_t debug_cmd = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口1初始化,用于上位机调试 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
pid_init(); /* 初始化PID参数 */
atim_timx_cplm_pwm_init(8400 - 1 , 0); /* 168 000 000 / 1 = 168 000 000 168Mhz的计数频率,计数8400次为50us */
dcmotor_init(); /* 初始化电机 */
gtim_timx_encoder_chy_init(0XFFFF, 0); /* 编码器定时器初始化,不分频直接84M的计数频率 */
btim_timx_int_init(1000 - 1 , 84 - 1); /* 基本定时器初始化,1ms计数周期 */
#if DEBUG_ENABLE /* 开启调试 */
debug_init(); /* 初始化调试 */
debug_send_motorcode(DC_MOTOR); /* 上传电机类型(直流有刷电机) */
debug_send_motorstate(IDLE_STATE); /* 上传电机状态(空闲) */
/* 同步数据(选择第1组PID,目标速度地址,P,I,D参数)到上位机 */
debug_send_initdata(TYPE_PID1, (float *)(&g_speed_pid.SetPoint), KP, KI, KD);
#endif
g_point_color = WHITE;
g_back_color = BLACK;
lcd_show_string(10, 10, 200, 16, 16, "DcMotor Test", g_point_color);
lcd_show_string(10, 30, 200, 16, 16, "KEY0:Start forward", g_point_color);
lcd_show_string(10, 50, 200, 16, 16, "KEY1:Start backward", g_point_color);
lcd_show_string(10, 70, 200, 16, 16, "KEY2:Stop", g_point_color);
while (1)
{
key = key_scan(0); /* 按键扫描 */
if(key == KEY0_PRES) /* 当key0按下 */
{
g_run_flag = 1; /* 标记电机启动 */
g_speed_pid.SetPoint += 30;
if (g_speed_pid.SetPoint == 0)
{
dcmotor_stop(); /* 停止则立刻响应 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机方向、转速 */
}
else
{
dcmotor_start(); /* 开启电机 */
if (g_speed_pid.SetPoint >= 300) /* 限速 */
{
g_speed_pid.SetPoint = 300;
}
#if DEBUG_ENABLE
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */
#endif
}
}
else if(key == KEY1_PRES) /* 当key1按下 */
{
g_run_flag = 1; /* 标记电机启动 */
g_speed_pid.SetPoint -= 30;
if (g_speed_pid.SetPoint == 0)
{
dcmotor_stop(); /* 停止则立刻响应 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机方向、转速 */
}
else
{
dcmotor_start(); /* 开启电机 */
if (g_speed_pid.SetPoint <= -300) /* 限速 */
{
g_speed_pid.SetPoint = -300;
}
#if DEBUG_ENABLE
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */
#endif
}
}
else if(key == KEY2_PRES) /* 当key2按下 */
{
dcmotor_stop(); /* 停止电机 */
pid_init(); /* 重置pid参数 */
g_run_flag = 0; /* 标记电机停止 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机转向、速度 */
#if DEBUG_ENABLE
debug_send_motorstate(BREAKED_STATE); /* 上传电机状态(刹车) */
debug_send_initdata(TYPE_PID1, (float *)(&g_speed_pid.SetPoint), KP, KI, KD); /* 同步数据到上位机 */
#endif
}
#if DEBUG_ENABLE
/* 查询接收PID助手的PID参数 */
debug_receive_pid(TYPE_PID1, (float *)&g_speed_pid.Proportion,(float *)&g_speed_pid.Integral, (float *)&g_speed_pid.Derivative);
debug_set_point_range(300, -300, 300); /* 设置目标速度范围 */
debug_cmd = debug_receive_ctrl_code(); /* 读取上位机指令 */
if (debug_cmd == BREAKED) /* 电机刹车 */
{
dcmotor_stop(); /* 停止电机 */
pid_init(); /* 重置pid参数 */
g_run_flag = 0; /* 标记电机停止 */
g_motor_data.motor_pwm = 0;
motor_pwm_set(g_motor_data.motor_pwm); /* 设置电机转向、速度 */
debug_send_motorstate(BREAKED_STATE); /* 上传电机状态(刹车) */
debug_send_initdata(TYPE_PID1, (float *)(&g_speed_pid.SetPoint), KP, KI, KD);
}
else if (debug_cmd == RUN_CODE) /* 电机运行 */
{
dcmotor_start(); /* 开启电机 */
g_speed_pid.SetPoint = 30; /* 设置目标速度:30 RPM */
g_run_flag = 1; /* 标记电机启动 */
debug_send_motorstate(RUN_STATE); /* 上传电机状态(运行) */
}
#endif
t++;
if(t % 20 == 0)
{
lcd_dis(); /* 显示数据 */
LED0_TOGGLE(); /*LED0(红灯) 翻转*/
#if DEBUG_ENABLE
debug_send_speed(g_motor_data.speed); /* 发送速度 */
#endif
}
delay_ms(10);
}
}
/**
* @brief 数据显示函数
* @param 无
* @retval 无
*/
void lcd_dis(void)
{
char buf[32];
/* 显示占空比(正负号为电机方向) */
sprintf(buf, "PWM_Duty:%.1f%c ", (float)(g_motor_data.motor_pwm * 100 / 8400), '%');
lcd_show_string(10, 150, 200, 16, 16, buf, g_point_color);
/* 设置的目标速度 */
sprintf(buf, "Set Speed :%3d RPM ", (int16_t)g_speed_pid.SetPoint);
lcd_show_string(10, 180, 200, 16, 16, buf, g_point_color);
/* 电机实际速度 */
sprintf(buf, "Speed :%3d RPM ", (int16_t)g_motor_data.speed);
lcd_show_string(10, 210, 200, 16, 16, buf, g_point_color);
}
#if DEBUG_ENABLE /* 开启调试 */
debug_init(); /* 初始化调试 */
debug_send_motorcode(DC_MOTOR); /* 上传电机类型(直流有刷电机) */
debug_send_motorstate(IDLE_STATE); /* 上传电机状态(空闲) */
/* 同步数据(选择第1组PID,目标速度地址,P,I,D参数)到上位机 */
debug_send_initdata(TYPE_PID1, (float *)(&g_speed_pid.SetPoint), KP, KI, KD);
#endif
首先初始化调试,就是将用来存放参数的结构体变量清零。
接着上传下电机类型
上传电机状态,
把此时Kp,Ki,Kd的值发给pid调试助手。
上传数据的函数。
这里我只上传了电机的速度,差不多200ms上传一次。
发送波形函数
这里我上传实际速度,目标速度,占空比值,每50ms上传一次
这里我上传三个通道,
通道1上传实际速度,
通道2上传目标速度
通道3上传占空比
接受上位机发送数据函数
这里设定目标范围,已经设定目标的变化上限。
读取上位机指令。
判断指令是否是刹车,然后执行相关操作。
判断指令是否是运行,然后执行相关操作。
在主函数的while循环里一直调用这些函数。
解析数据接受和数据发送的底层函数
数据接受
通过串口来接收上位机发来的数据包(中断接收),只要上位机一发送数据,就会产生串口中断,然后在接受中断回调函数里族逐个字节来接收数据,并解析数据。
数据帧格式
/**
* @brief 上位机数据解析
* @param *data:接收的数据(地址)
* @note 利用环形缓冲区来接收数据,再存进相应的结构体成员中
* @retval 无
*/
void debug_handle(uint8_t *data)
{
uint8_t temp[DEBUG_REV_MAX_LEN];
uint8_t i;
if (debug_rev_p >= DEBUG_REV_MAX_LEN) /* 超过缓冲区(数组)最大长度 */
{
debug_rev_p = 0; /* 地址偏移量清零 */
}
debug_rev_data[debug_rev_p] = *(data); /* 取出数据,存进数组 */
if (*data == DEBUG_DATA_END) /* 判断是否收到帧尾 */
{
if (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 4) % DEBUG_REV_MAX_LEN] == DEBUG_DATA_HEAD) /* 数据包长度为5个字节,判断第一个字节是否为帧头 */
{
for (i = 0; i < 2; i++)
{
temp[i] = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 4 + i) % DEBUG_REV_MAX_LEN]; /* 取出帧头、数据类别,5个字节的数据包没有数据域 */
}
#if EN_CRC /* 进行CRC校验 */
if (crc16_calc(temp, 2) == ((debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 4 + 2) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 4 + 3) % DEBUG_REV_MAX_LEN]))
#endif
{
if (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 4 + 1) % DEBUG_REV_MAX_LEN] == CMD_GET_ALL_DATA) /* 判断数据类别是否为:获取全部参数 */
{
debug_upload_data(&g_debug, TYPE_STATUS); /* 发送电机状态 */
debug_upload_data(&g_debug, TYPE_SPEED); /* 发送速度值 */
debug_upload_data(&g_debug, TYPE_HAL_ENC); /* 发送霍尔、编码器位置 */
debug_upload_data(&g_debug, TYPE_VBUS); /* 发送电压 */
debug_upload_data(&g_debug, TYPE_AMP); /* 发送电流 */
debug_upload_data(&g_debug, TYPE_TEMP); /* 发送温度 */
debug_upload_data(&g_debug, TYPE_SUM_LEN); /* 发送总里程 */
debug_upload_data(&g_debug, TYPE_BEM); /* 发送反电动势 */
debug_upload_data(&g_debug, TYPE_MOTOR_CODE); /* 发送电机类型 */
for (i = TYPE_PID1; i < TYPE_PID10; i++)
{
debug_upload_data(&g_debug, i); /* 发送PID参数 */
}
}
}
}
if (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5) % DEBUG_REV_MAX_LEN] == DEBUG_DATA_HEAD) /* 数据包长度为6个字节,判断第一个字节是否为帧头 */
{
for (i = 0; i < 3; i++)
{
temp[i] = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + i) % DEBUG_REV_MAX_LEN]; /* 取出帧头、数据类别、数据域 */
}
#if EN_CRC /* 进行CRC校验 */
if (crc16_calc(temp, 3) == ((debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + 3) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + 4) % DEBUG_REV_MAX_LEN]))
#endif
{
switch (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + 1) % DEBUG_REV_MAX_LEN]) /* 判断数据类别 */
{
case CMD_SET_CTR_CODE: /* 下发控制指令 */
debug_rev.Ctrl_code = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + 2) % DEBUG_REV_MAX_LEN];
break;
case CMD_SET_CTR_MODE: /* 下发控制模式 */
debug_rev.Ctrl_mode = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 5 + 2) % DEBUG_REV_MAX_LEN];
break;
}
}
}
if (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6) % DEBUG_REV_MAX_LEN] == DEBUG_DATA_HEAD) /* 数据包长度为7个字节,判断第一个字节是否为帧头 */
{
for (i = 0; i < 4; i++)
{
temp[i] = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + i) % DEBUG_REV_MAX_LEN]; /* 取出帧头、数据类别、数据域 */
}
#if EN_CRC /* 进行CRC校验 */
if (crc16_calc(temp, 4) == ((debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 4) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 5) % DEBUG_REV_MAX_LEN]))
#endif
{
switch (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 1) % DEBUG_REV_MAX_LEN]) /* 判断数据类别 */
{
case CMD_SET_SPEED: /* 设定电机速度 */
*(debug_rev.speed) = (int16_t)((debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 2) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 3) % DEBUG_REV_MAX_LEN]);
break;
case CMD_SET_TORQUE: /* 设定转矩 */
*(debug_rev.torque) = (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 2) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 6 + 3) % DEBUG_REV_MAX_LEN];
break;
}
}
}
if (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16) % DEBUG_REV_MAX_LEN] == DEBUG_DATA_HEAD) /* 数据包长度为17个字节,判断第一个字节是否为帧头 */
{
for (i = 0; i < 14; i++)
{
temp[i] = debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + i) % DEBUG_REV_MAX_LEN]; /* 取出帧头、数据类别、数据域 */
}
#if EN_CRC /* 进行CRC校验 */
if (crc16_calc(temp, 14) == ((debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + 14) % DEBUG_REV_MAX_LEN] << 8) | \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + 15) % DEBUG_REV_MAX_LEN]))
#endif
{
switch (debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + 1) % DEBUG_REV_MAX_LEN]) /* 判断数据类别 */
{
case CMD_SET_PID1:
case CMD_SET_PID2:
case CMD_SET_PID3:
case CMD_SET_PID4:
case CMD_SET_PID5:
case CMD_SET_PID6:
case CMD_SET_PID7:
case CMD_SET_PID8:
case CMD_SET_PID9:
case CMD_SET_PID10:
for (i = 0; i < 12; i++) /* 接收设定的PID参数 */
{
g_debug.pid[debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + 1) % DEBUG_REV_MAX_LEN] - CMD_SET_PID1].pid.pidi8[i] = \
debug_rev_data[(debug_rev_p + DEBUG_REV_MAX_LEN - 16 + 2 + i) % DEBUG_REV_MAX_LEN];
}
break;
}
}
}
}
debug_rev_p ++;
}
首先先来说一下上位机的解析函数
上位机数据的解析函数是用环形数组的形式来存放,这样不会产生内存碎片,通过串口不断接受数组,然后把接受到的数组按照环形的顺序赋予给数值,并判断是否是帧尾,如果是帧尾,则接着判断是几字节的包,并且首字节是否是帧头,这里四种四节的包,分别是5字节,6字节,7字节,17字节。所以可以看到代码里进行了四个if语句判断分别是什么字节,并且首字节是否为帧头,然后分别进行crc校验,如果校验正确,则把表示数组类别和数据域的包给取出来,然后根据通信协议,将参数存放结构体变量进行对应赋值。
四个if语句
环形数组以及怎么找到它的帧头位置
例如数组大小为17的数组,当它的地址偏移量到达17的时候,则重新从0开始赋值。
我们现在已经知道帧尾的位置了,接下来如何找到帧头的位置呢?
这就和上面的四个if语句对应上了,例如我们要数据包长度为5的帧头,
我们就可以用((当前的地址偏移量+数组长度-(数据包长度-1))% 数组长度)来找到帧头的位置。
crc校验
本例程数据通过crc校验进行验证。
CRC定义:
CRC即循环冗余校验码,是数据通信领域中最常用的一种查错校验码,其特征是信息字段和校验字段的长度可以任意选定。循环冗余检查(CRC)是一种数据传输检错功能,对数据进行多项式计算,并将得到的结果附在帧的后面,接收设备也执行类似的算法,以保证数据传输的正确性和完整性。
取出帧头、数据类别、数据域进行crc校验
代码:
数据发送
数据上传函数
构建数组包:
先定义一个数组 upload_data[37],为什么是37呢,因为最长的数据包是37byte。
判断数据类别并进行指令存储:
根据upload_type的值来确定数据类别,然后进行把数据域值赋给upload_data数组。
判断是否未数据类别错误,如果不是,则进行crc校验,然后把指令发送给pid调试助手。