# RMCOD2026_Sentry **Repository Path**: cod_-control/rmcod2026_-sentry ## Basic Information - **Project Name**: RMCOD2026_Sentry - **Description**: COD26 赛季哨兵电控代码 - **Primary Language**: C - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 0 - **Created**: 2025-07-17 - **Last Updated**: 2026-04-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # RMCOD2026_Sentry [TOC] ## 介绍 26赛季COD舵轮哨兵代码 --- ## 环境依赖 - 开发工具:Keil V5.38a,VsCode - 软件环境:Window11 - 硬件环境:达妙H7(STM32H723VGT6) - 编译工具:Arm Compiler V6.22,C/C++编译 --- ## 使用说明 #### 底盘控制 - 底盘控制任务主要放置在 [Control_Task.c](D:\CODsoftware\Infantry_2026\Infantry_333\chassis\COD_H7_Template\Task\Src\Control_Task.c) - 底盘和云台通信主要放置在 [CAN_Task.c](D:\CODsoftware\Infantry_2026\Infantry_333\chassis\COD_H7_Template\Task\Src\CAN_Task.c) 和 [bsp_can.c](D:\CODsoftware\Infantry_2026\Infantry_333\chassis\COD_H7_Template\BSP\Src\bsp_can.c) #### 云台控制 - 云台控制任务主要放置在 [Control_Task.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\Task\Src\Control_Task.c) 和 [Shoot.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\Task\Src\Shoot.c) - 云台和底盘通信主要放置在 [CAN_Task.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\Task\Src\CAN_Task.c) 和 [bsp_can.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\BSP\Src\bsp_can.c) - 云台和上位机通信主要放置在 [MiniPC_Task.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\Task\Src\MiniPC_Task.c) 和 [MiniPC.c](D:\CODsoftware\Infantry_2026\Infantry_333\gimbal\Components\Device\Src\MiniPC.c) --- ## Chassis重要文件 #### 舵轮解算 ```c# // 舵向轮解算 Chassis_Info->Target.Chassis_Angle[0] = -(atan2f((Control_Info.Move.Vx + Control_Info.Move.Vw * sin45 * R), (Control_Info.Move.Vy + Control_Info.Move.Vw * sin45 * R)) * RadiansToDegrees); Chassis_Info->Target.Chassis_Angle[1] = -(atan2f((Control_Info.Move.Vx + Control_Info.Move.Vw * sin45 * R), (Control_Info.Move.Vy - Control_Info.Move.Vw * sin45 * R)) * RadiansToDegrees); Chassis_Info->Target.Chassis_Angle[2] = -(atan2f((Control_Info.Move.Vx - Control_Info.Move.Vw * sin45 * R), (Control_Info.Move.Vy + Control_Info.Move.Vw * sin45 * R)) * RadiansToDegrees); Chassis_Info->Target.Chassis_Angle[3] = -(atan2f((Control_Info.Move.Vx - Control_Info.Move.Vw * sin45 * R), (Control_Info.Move.Vy - Control_Info.Move.Vw * sin45 * R)) * RadiansToDegrees); Chassis_Info->Measure.Absolute_Angle[0] = (Chassis_Info->trigger.Chassis_Angle_average[2][0] - Motor_Course[0].Data.Angle + Chassis_Info->Target.Chassis_Angle[0]); Chassis_Info->Measure.Absolute_Angle[1] = (Chassis_Info->trigger.Chassis_Angle_average[2][1] - Motor_Course[1].Data.Angle + Chassis_Info->Target.Chassis_Angle[1]); Chassis_Info->Measure.Absolute_Angle[2] = (Chassis_Info->trigger.Chassis_Angle_average[2][2] - Motor_Course[2].Data.Angle + Chassis_Info->Target.Chassis_Angle[2]); Chassis_Info->Measure.Absolute_Angle[3] = (Chassis_Info->trigger.Chassis_Angle_average[2][3] - Motor_Course[3].Data.Angle + Chassis_Info->Target.Chassis_Angle[3]); for (i = 0; i < 4; i++){ // 限制舵向角度在+-180之间 if(Chassis_Info->Measure.Absolute_Angle[i]>=180) Chassis_Info->Measure.Absolute_Angle[i]-=360; else if(Chassis_Info->Measure.Absolute_Angle[i]<=-180) Chassis_Info->Measure.Absolute_Angle[i]+=360; if (fabsf(Chassis_Info->Measure.Absolute_Angle[i]) < 90.f) Chassis_Info->trigger.K[i] = 1.0f; //优劣弧判断 else if (fabsf(Chassis_Info->Measure.Absolute_Angle[i]) >= 90.f){ if (Chassis_Info->Measure.Absolute_Angle[i] >= 90){ Chassis_Info->Measure.Absolute_Angle[i] -= 180.0f; Chassis_Info->trigger.K[i] = -1.0f; } else if (Chassis_Info->Measure.Absolute_Angle[i] <= -90){ Chassis_Info->Measure.Absolute_Angle[i] += 180.0f; Chassis_Info->trigger.K[i] = -1.0f; } } } // 行进轮解算 Chassis_Info->Target.Chassis_Velocity[0] = -Chassis_Info->trigger.K[0]* (Q_sqrt(powf(Control_Info.Move.Vx + Control_Info.Move.Vw * sin45, 2.f) + powf(Control_Info.Move.Vy + Control_Info.Move.Vw * sin45, 2.f))); Chassis_Info->Target.Chassis_Velocity[1] = -Chassis_Info->trigger.K[1]* (Q_sqrt(powf(Control_Info.Move.Vx - Control_Info.Move.Vw * sin45, 2.f) + powf(Control_Info.Move.Vy - Control_Info.Move.Vw * sin45, 2.f))); Chassis_Info->Target.Chassis_Velocity[2] = Chassis_Info->trigger.K[2] * (Q_sqrt(powf(Control_Info.Move.Vx - Control_Info.Move.Vw * sin45, 2.f) + powf(Control_Info.Move.Vy + Control_Info.Move.Vw * sin45, 2.f))); Chassis_Info->Target.Chassis_Velocity[3] = Chassis_Info->trigger.K[3] * (Q_sqrt(powf(Control_Info.Move.Vx + Control_Info.Move.Vw * sin45, 2.f) + powf(Control_Info.Move.Vy - Control_Info.Move.Vw * sin45, 2.f))); } ``` --- ### 3508角度轮校准 3508舵轮使用接近开关进行校准 - 洛施达微小型方形接近开关传感器82.5NOF/82.5NOT可替代GX-F8AH8B ```c# Chassis_Cal(Chassis_Info_Typedef *Chassis_Info) PowerCtrl_Info[0].K3 = 3; PowerCtrl_Info[0].Err_Lower = 0.01; PowerCtrl_Info[0].Err_Upper = 50; PowerCtrl_Info[1].K3 = 3.5; PowerCtrl_Info[1].Err_Lower = 0.001; PowerCtrl_Info[1].Err_Upper = 500; RLS_Init(&RLS_Power_Info, 4, 1, 0.99999, 1e-5); RLS_Power_Info.Data.W[0] = 1.453e-07; RLS_Power_Info.Data.W[1] = 1.23e-07; RLS_Power_Info.Data.W[2] = 1.453e-07; RLS_Power_Info.Data.W[3] = 1.23e-07; start = 0; //接近开关高低电平获取 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { /* Prevent unused argument(s) compilation warning */ UNUSED(GPIO_Pin); if (GPIO_Pin == GPIO_PIN_13) Chassis_Info.trigger.now_state[0] = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_13); // 13 if (GPIO_Pin == GPIO_PIN_9) Chassis_Info.trigger.now_state[1] = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_9); // 9 if (GPIO_Pin == GPIO_PIN_2) Chassis_Info.trigger.now_state[2] = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2); // 0 if (GPIO_Pin == GPIO_PIN_0) Chassis_Info.trigger.now_state[3] = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); // 2 /* NOTE: This function Should not be modified, when the callback is needed, the HAL_GPIO_EXTI_Callback could be implemented in the user file */ } static void Chassis_Cal(Chassis_Info_Typedef *Chassis_Info){ if (Chassis_Info->trigger.now_state[0] == 0 && Chassis_Info->trigger.last_state[0] == 1){ Chassis_Info->trigger.Grab[0][0] += 1; Chassis_Info->trigger.Chassis_Angle_now[0] = Motor_Course[0].Data.Angle; } if (Chassis_Info->trigger.now_state[1] == 0 && Chassis_Info->trigger.last_state[1] == 1){ Chassis_Info->trigger.Grab[0][1] += 1; Chassis_Info->trigger.Chassis_Angle_now[1] = Motor_Course[1].Data.Angle; } if (Chassis_Info->trigger.now_state[2] == 0 && Chassis_Info->trigger.last_state[2] == 1){ Chassis_Info->trigger.Grab[0][2] += 1; Chassis_Info->trigger.Chassis_Angle_now[2] = Motor_Course[2].Data.Angle; } if (Chassis_Info->trigger.now_state[3] == 0 && Chassis_Info->trigger.last_state[3] == 1){ Chassis_Info->trigger.Grab[0][3] += 1; Chassis_Info->trigger.Chassis_Angle_now[3] = Motor_Course[3].Data.Angle; } //**********************************// //**********************************// if (Chassis_Info->trigger.now_state[0] == 1 && Chassis_Info->trigger.last_state[0] == 0){ Chassis_Info->trigger.Grab[1][0] += 1; Chassis_Info->trigger.Chassis_Angle_last[0] = Motor_Course[0].Data.Angle; Chassis_Info->trigger.Chassis_Angle_average[0][0] = (Chassis_Info->trigger.Chassis_Angle_now[0] + Chassis_Info->trigger.Chassis_Angle_last[0]) * 0.5; } if (Chassis_Info->trigger.now_state[1] == 1 && Chassis_Info->trigger.last_state[1] == 0){ Chassis_Info->trigger.Grab[1][1] += 1; Chassis_Info->trigger.Chassis_Angle_last[1] = Motor_Course[1].Data.Angle; Chassis_Info->trigger.Chassis_Angle_average[0][1] = (Chassis_Info->trigger.Chassis_Angle_now[1] + Chassis_Info->trigger.Chassis_Angle_last[1]) * 0.5; } if (Chassis_Info->trigger.now_state[2] == 1 && Chassis_Info->trigger.last_state[2] == 0){ Chassis_Info->trigger.Grab[1][2] += 1; Chassis_Info->trigger.Chassis_Angle_last[2] = Motor_Course[2].Data.Angle; Chassis_Info->trigger.Chassis_Angle_average[0][2] = (Chassis_Info->trigger.Chassis_Angle_now[2] + Chassis_Info->trigger.Chassis_Angle_last[2]) * 0.5; } if (Chassis_Info->trigger.now_state[3] == 1 && Chassis_Info->trigger.last_state[3] == 0){ Chassis_Info->trigger.Grab[1][3] += 1; Chassis_Info->trigger.Chassis_Angle_last[3] = Motor_Course[3].Data.Angle; Chassis_Info->trigger.Chassis_Angle_average[0][3] = (Chassis_Info->trigger.Chassis_Angle_now[3] + Chassis_Info->trigger.Chassis_Angle_last[3]) * 0.5; } for (i = 0; i < 4; i++){ Chassis_Info->trigger.Error[i] = Chassis_Info->trigger.Chassis_Angle_average[2][i] - Motor_Course[i].Data.Angle; if ((Chassis_Info->trigger.Grab[0][i] > 1) && (Chassis_Info->trigger.Grab[1][i] > 1)){ Chassis_Info->trigger.Chassis_Angle_average[1][i] = (Chassis_Info->trigger.Chassis_Angle_now[i] + Chassis_Info->trigger.Chassis_Angle_last[i]) * 0.5; if (Chassis_Info->trigger.need_trigger == 1){ Chassis_Info->trigger.Chassis_Angle_average[2][i] = ((Chassis_Info->trigger.Chassis_Angle_average[0][i] + Chassis_Info->trigger.Chassis_Angle_average[1][i]) * 0.5) - 3.0f; } PID_Calculate(&PID_Chassis_Cal[i], Chassis_Info->trigger.Chassis_Angle_average[2][i], Motor_Course[i].Data.Angle); PID_Calculate(&PID_Chassis_Cal_V[i], PID_Chassis_Cal[i].Output, Motor_Course[i].Data.Velocity); Chassis_Info->trigger.Calibrate_State[i] = 1; } else{ if (Chassis_Info->trigger.Grab[1][i] < 1){ Chassis_Info->trigger.Chassis_Angle[i] = 100; PID_Calculate(&PID_Chassis_Cal[i], Chassis_Info->trigger.Chassis_Angle[i], 0); PID_Calculate(&PID_Chassis_Cal_V[i], PID_Chassis_Cal[i].Output, Motor_Course[i].Data.Velocity); } if ((Chassis_Info->trigger.Grab[1][i] == 1)){ Chassis_Info->trigger.Chassis_Angle[i] = -90; PID_Calculate(&PID_Chassis_Cal[i], Chassis_Info->trigger.Chassis_Angle[i], 0); PID_Calculate(&PID_Chassis_Cal_V[i], PID_Chassis_Cal[i].Output, Motor_Course[i].Data.Velocity); } } } } ``` --- ### 功率控制 参考了西交的功率控制模型,具体可参考[【RM2023-电机功率模型与功率控制开源】西交利物浦大学](https://gitee.com/link?target=http%3A%2F%2Fgithub.com%2FMaxwellDemonLin%2FMotor-modeling-and-power-control) 首先我们读取电机的电流值Current与转速Velocity(rpm),将电流映射为电机的转矩,将转速转换单位为(m/s)。 通过西交的模型得出具体的预测底盘功率 部分代码如下: ```c# PowerCtrl_Info[1].Sum.Torque2_Sum = PowerCtrl_Info[1].Target.Torque_2[0] + PowerCtrl_Info[1].Target.Torque_2[1] + PowerCtrl_Info[1].Target.Torque_2[2] + PowerCtrl_Info[1].Target.Torque_2[3]; PowerCtrl_Info[1].Sum.Omiga2_Sum = PowerCtrl_Info[1].Target.Omiga_2[0] + PowerCtrl_Info[1].Target.Omiga_2[1] + PowerCtrl_Info[1].Target.Omiga_2[2] + PowerCtrl_Info[1].Target.Omiga_2[3]; PowerCtrl_Info[1].Sum.Power_Sum = PowerCtrl_Info[1].Target.Power_In[0] + PowerCtrl_Info[1].Target.Power_In[1] + PowerCtrl_Info[1].Target.Power_In[2] + PowerCtrl_Info[1].Target.Power_In[3] + PowerCtrl_Info[1].K3; PowerCtrl_Info[1].Power_Allin = PowerCtrl_Info[1].Sum.Power_Sum; PowerCtrl_Info[1].Power_Max = Chassis_Info.Power_Max - fabs(PowerCtrl_Info[0].Power_Allin); ``` ##### RLS功率模型拟合: 通过将初始化的常数Torque_2、Omiga_2、Power_In求和,得Torque2_Sum、Omiga2_Sum、Power_Allin...进行功率拟合, 通过RLS_Update对计算出的功率与实际功率进行递归最小二乘拟合,将模型中的参数K1,K2...进行更新。 舵向与行进的功率分配:通过Chassis_Info.Power_Max设定底盘最大输出功率,对舵向优先分配80%的功率,使其优先提供转向,将剩余的功率分配给行进向。 //RLS_Update(&RLS_Power_Info); 通过开启自整定确定参数 部分代码如下: ```c# VAL_LIMIT(PowerCtrl_Info[1].Power_Allin, -PowerCtrl_Info[1].Power_Max, PowerCtrl_Info[1].Power_Max); LowPassFilter1p_Update(&LPF_ChassisPower,Chassis_Info.Supercap.Chassis_Power); RLS_Power_Info.Data.U[0] = PowerCtrl_Info[0].Power_Allin + PowerCtrl_Info[1].Power_Allin; RLS_Power_Info.Data.Y[0] =LPF_ChassisPower.Output; RLS_Power_Info.Data.X[0] = PowerCtrl_Info[0].Sum.Torque2_Sum; RLS_Power_Info.Data.X[1] = PowerCtrl_Info[0].Sum.Omiga2_Sum; RLS_Power_Info.Data.X[2] = PowerCtrl_Info[1].Sum.Torque2_Sum; RLS_Power_Info.Data.X[3] = PowerCtrl_Info[1].Sum.Omiga2_Sum; //RLS_Update(&RLS_Power_Info); PowerCtrl_Info[0].K1 = RLS_Power_Info.Data.W[0]; PowerCtrl_Info[0].K2 = RLS_Power_Info.Data.W[1]; PowerCtrl_Info[1].K1 = RLS_Power_Info.Data.W[2]; PowerCtrl_Info[1].K2 = RLS_Power_Info.Data.W[3]; ``` ##### 功率分配: 读取PID误差,将误差累加,根据单电机PID误差的占比和预测功率的占总输出功率的占比运用分配因子,通过平滑函数将其化为一个隶属度。 部分代码如下: ```c# for (int i = 0; i < 4; i++) PowerCtrl_Info[0].Err[i] = PID_Chassis_angle_v[i].Err[0]; // 输入PID误差 PowerCtrl_Info[0].Sum.Err_Sum = fabsf(PowerCtrl_Info[0].Err[FL]) + fabsf(PowerCtrl_Info[0].Err[FB]) + fabsf(PowerCtrl_Info[0].Err[RB]) + fabsf(PowerCtrl_Info[0].Err[RL]); // 计算PID误差和 if (PowerCtrl_Info[0].Sum.Err_Sum > PowerCtrl_Info[0].Err_Upper) PowerCtrl_Info[0].K = 1; else if (PowerCtrl_Info[0].Sum.Err_Sum < PowerCtrl_Info[0].Err_Lower) PowerCtrl_Info[0].K = 0; else PowerCtrl_Info[0].K = (PowerCtrl_Info[0].Sum.Err_Sum - PowerCtrl_Info[0].Err_Lower) / (PowerCtrl_Info[0].Err_Upper - PowerCtrl_Info[0].Err_Lower); for (int i = 0; i < 4; i++) PowerCtrl_Info[1].Err[i] = PID_Chassis_velocity[i].Err[0]; // 输入PID误差 PowerCtrl_Info[1].Sum.Err_Sum = fabsf(PowerCtrl_Info[1].Err[FL]) + fabsf(PowerCtrl_Info[1].Err[FB]) + fabsf(PowerCtrl_Info[1].Err[RB]) + fabsf(PowerCtrl_Info[1].Err[RL]); // 计算PID误差和 if (PowerCtrl_Info[1].Sum.Err_Sum > PowerCtrl_Info[1].Err_Upper) PowerCtrl_Info[1].K = 1; else if (PowerCtrl_Info[0].Sum.Err_Sum < PowerCtrl_Info[1].Err_Lower) PowerCtrl_Info[1].K = 0; else PowerCtrl_Info[1].K = (PowerCtrl_Info[1].Sum.Err_Sum - PowerCtrl_Info[1].Err_Lower) / (PowerCtrl_Info[1].Err_Upper - PowerCtrl_Info[1].Err_Lower); // 计算分配因子K for (int i = 0; i < 4; i++){ PowerCtrl_Info[0].Menbership[i] = (PowerCtrl_Info[0].K * (fabs(PowerCtrl_Info[0].Err[i]) / PowerCtrl_Info[0].Sum.Err_Sum) + (1 - PowerCtrl_Info[0].K) * (fabs(PowerCtrl_Info[0].Target.Power_In[i]) / PowerCtrl_Info[0].Sum.Power_Sum)); VAL_LIMIT(PowerCtrl_Info[0].Menbership[i], 0, 1); PowerCtrl_Info[1].Menbership[i] = (PowerCtrl_Info[1].K * (fabs(PowerCtrl_Info[1].Err[i]) / PowerCtrl_Info[1].Sum.Err_Sum) + (1 - PowerCtrl_Info[1].K) * (fabs(PowerCtrl_Info[1].Target.Power_In[i]) / PowerCtrl_Info[1].Sum.Power_Sum)); VAL_LIMIT(PowerCtrl_Info[1].Menbership[i], 0, 1); PowerCtrl_Info[0].Power_Limit[i] = PowerCtrl_Info[0].Menbership[i] * PowerCtrl_Info[0].Power_Max; // 港科大P分配 PowerCtrl_Info[1].Power_Limit[i] = PowerCtrl_Info[1].Menbership[i] * PowerCtrl_Info[1].Power_Max; // 港科大P分配 } ``` ##### 求解受限力矩电流: 如果拟合出的功率Power_Sum大于被分配的Power_Max, 则将根据上述分配的Power_Limit进行功率限制的求解,运用求根公式,求出限制后的力矩电流值。 如若所拟合的功率Power_Sum并未超过我们所给定的Power_Max,则直接沿用PID计算所得输出即可。 部分代码如下: ```c# if (PowerCtrl_Info[0].Sum.Power_Sum >= PowerCtrl_Info[0].Power_Max){ for (int i = 0; i < 4; i++){ PowerCtrl_Info[0].A = PowerCtrl_Info[0].K1; PowerCtrl_Info[0].B = PowerCtrl_Info[0].Target.Omiga[i]; PowerCtrl_Info[0].C = PowerCtrl_Info[0].Target.Omiga_2[i] * PowerCtrl_Info[0].K2 + PowerCtrl_Info[0].K3 - PowerCtrl_Info[0].Power_Limit[i]; PowerCtrl_Info[0].Delta = powf(PowerCtrl_Info[0].B, 2.f) - 4 * PowerCtrl_Info[0].A * PowerCtrl_Info[0].C; if (isnan(PowerCtrl_Info[0].Delta) == 1 || isinf(PowerCtrl_Info[0].Delta) == 1 || (PowerCtrl_Info[0].Delta) < 0) PowerCtrl_Info[0].Delta = 0; if (PowerCtrl_Info[0].Delta >= 0){ PowerCtrl_Info[0].Sqrt = sqrt(PowerCtrl_Info[0].Delta); if (Chassis_Info.Output.Chassis_Angel[i] >= 0){ PowerCtrl_Info[0].Torque[i] = (-PowerCtrl_Info[0].B + PowerCtrl_Info[0].Sqrt) / (2 * PowerCtrl_Info[0].A); Chassis_Info.Output.Chassis_Angel[i] = (PowerCtrl_Info[0].Torque[i] / 4.577e-5); } else{ PowerCtrl_Info[0].Torque[i] = (-PowerCtrl_Info[0].B + PowerCtrl_Info[0].Sqrt) / (2 * PowerCtrl_Info[0].A); Chassis_Info.Output.Chassis_Angel[i] = (PowerCtrl_Info[0].Torque[i] / 4.577e-5); } } else{ if (Chassis_Info.Output.Chassis_Angel[i] >= 0){ PowerCtrl_Info[0].Torque[i] = (-PowerCtrl_Info[0].B) / (2 * PowerCtrl_Info[0].A); Chassis_Info.Output.Chassis_Angel[i] = (PowerCtrl_Info[0].Torque[i] / 4.577e-5); } else{ PowerCtrl_Info[0].Torque[i] = (-PowerCtrl_Info[0].B) / (2 * PowerCtrl_Info[0].A); Chassis_Info.Output.Chassis_Angel[i] =(PowerCtrl_Info[0].Torque[i] / 4.577e-5); } } } } else{ for (int i = 0; i < 4; i++) Chassis_Info.Output.Chassis_Angel[i] = PID_Chassis_angle_v[i].Output; } if (PowerCtrl_Info[1].Sum.Power_Sum >= PowerCtrl_Info[1].Power_Max){ for (i = 0; i < 4; i++){ PowerCtrl_Info[1].A = PowerCtrl_Info[1].K1; PowerCtrl_Info[1].B = PowerCtrl_Info[1].Target.Omiga[i]; PowerCtrl_Info[1].C = PowerCtrl_Info[1].Target.Omiga_2[i] * PowerCtrl_Info[1].K2 + PowerCtrl_Info[1].K3 - PowerCtrl_Info[1].Power_Limit[i]; PowerCtrl_Info[1].Delta = powf(PowerCtrl_Info[1].B, 2.f) - 4 * PowerCtrl_Info[1].A * PowerCtrl_Info[1].C; if (isnan(PowerCtrl_Info[1].Delta) == 1 || isinf(PowerCtrl_Info[1].Delta) == 1 || (PowerCtrl_Info[1].Delta) < 0) PowerCtrl_Info[1].Delta = 0; if (PowerCtrl_Info[1].Delta >= 0) { PowerCtrl_Info[1].Sqrt =Q_sqrt(PowerCtrl_Info[1].Delta); if (Chassis_Info.Output.Chassis_Velocity[i] >= 0) { PowerCtrl_Info[1].Torque[i] = (-PowerCtrl_Info[1].B + PowerCtrl_Info[1].Sqrt) / (2 * PowerCtrl_Info[1].A); Chassis_Info.Output.Chassis_Velocity[i] = (PowerCtrl_Info[1].Torque[i] / 4.577e-5);// } else { PowerCtrl_Info[1].Torque[i] = (-PowerCtrl_Info[1].B - PowerCtrl_Info[1].Sqrt) / (2 * PowerCtrl_Info[1].A); Chassis_Info.Output.Chassis_Velocity[i] =(PowerCtrl_Info[1].Torque[i] / 4.577e-5);//PowerCtrl_Info[1].Torque[i]; } } else { if (Chassis_Info.Output.Chassis_Velocity[i] >= 0) { PowerCtrl_Info[1].Torque[i] = (-PowerCtrl_Info[1].B) / (2 * PowerCtrl_Info[1].A); Chassis_Info.Output.Chassis_Velocity[i] =(PowerCtrl_Info[1].Torque[i] / 4.577e-5);// } else { PowerCtrl_Info[1].Torque[i] = (-PowerCtrl_Info[1].B) / (2 * PowerCtrl_Info[1].A); Chassis_Info.Output.Chassis_Velocity[i] =(PowerCtrl_Info[1].Torque[i] / 4.577e-5); } } } } else for (int i = 0; i < 4; i++) { Chassis_Info.Output.Chassis_Velocity[i] = PID_Chassis_velocity[i].Output; } ``` --- #### 导航模式 - Chassis_Auto 为离线导航 ```c# case Auto: case Chassis_Auto: { float _Vy; float _Vx; float Area_Status = (Referee_System_Info.rfid_status.rfid_status>>23)&0x01; // 自动模式 if ((MiniPc_Receive_Auto.Fire == 1)&&(MiniPc_Receive_Auto.Vision_Grap == 1)) Chassis_Info->Target.Shoot_Speed = Shoot_Info.Shoot.Shoot_DP * 540.0f; else Chassis_Info->Target.Shoot_Speed = 0; if((Referee_System_Info.robot_status.current_HP>280)&&(MiniPc_Receive_Auto.Vision_Grap==1)&&(Area_Status==1) &&(MiniPc_Receive_Auto.Fire_Switch_Spin==1)){ _Vy =5; _Vx =5; } else{ _Vy = -MiniPc_Receive_Auto.Vy * 2233.866; _Vx = MiniPc_Receive_Auto.Vx * 2233.866; } Control_Info.Move.Vx = (-arm_sin_f32(Control_Info.Angle_gap * DegreesToRadians) * _Vx + arm_cos_f32(Control_Info.Angle_gap * DegreesToRadians) * _Vy); Control_Info.Move.Vy = arm_cos_f32(Control_Info.Angle_gap * DegreesToRadians) * _Vx + arm_sin_f32(Control_Info.Angle_gap * DegreesToRadians) * _Vy; float Auto_Vx = fabs(_Vx); float Auto_Vy = fabs(_Vy); static unsigned int Vw_Time =0; static unsigned char is_odd_second = 0; // Vw平滑处理 static float target_vw; if ((Auto_Vx < 35) && (Auto_Vy < 35)) { if(++Vw_Time >= 1200) { Vw_Time = 0; static int cycle_count = 0; cycle_count = (cycle_count + 1) % 3; if(cycle_count == 0) target_vw = 6500.0f; else if(cycle_count == 1) target_vw = 4000.0f; else target_vw =2000.0f; } } else target_vw = 6000.0f; //3000.f; Control_Info.Move.Vw = target_vw; if ((MiniPc_Receive_Auto.Auto_to_Vision == 1) && (MiniPc_Receive_Auto.Vision_Grap == 1)) Chassis_Info->Target.Vision_Yaw_Angle = MiniPc_Receive_Auto.Vision_Yaw; else Chassis_Info->Target.Vision_Yaw_Angle = Gimbal.Yaw_Angle; break; } ``` --- #### 哨兵自动开关超电 - 因为防止在超级电容关闭不及时导致的超功率,所以把阈值给的较高 ```c# //超电 void Control_SuperCap_Mode_Update(Control_Info_Typedef *Control_Info) //当开启超电时底盘功率上限设置 { if(Chassis_Info.Supercap.Persent >= 30){ Chassis_Info.Supercap.Power_Max =Referee_System_Info.robot_status.chassis_power_limit-PID_Buffer.Output+100; Chassis_Info.Supercap.Mode_State=1; } else{ Chassis_Info.Supercap.Power_Max = Referee_System_Info.robot_status.chassis_power_limit-PID_Buffer.Output; Chassis_Info.Supercap.Mode_State=1; } Chassis_Info.Power_Max= Chassis_Info.Supercap.Power_Max; } ``` ---- #### 热量控制 - 如果使用中科视频中的虚拟枪口进行控制效果更佳(实力不足,在发射数量较多是会产生误差) ```c# //热量控制 static void Fire_Ctrl(){ //剩余热量 Shoot_Info.HeatCtrl.SYS_Qres = Referee_System_Info.robot_status.shooter_barrel_heat_limit - Referee_System_Info.power_heat_data.shooter_17mm_1_barrel_heat; // if( Shoot_Info.HeatCtrl.Last_SYS_Qrse != Shoot_Info.HeatCtrl.SYS_Qres){ //虚拟枪口 //Shoot_Info.HeatCtrl.SYS_Qres = Referee_System_Info.robot_status.shooter_barrel_heat_limit - Referee_System_Info.power_heat_data.shooter_17mm_1_barrel_heat ; // } // else{ // Shoot_Info.HeatCtrl.SYS_Qres =Shoot_Info.HeatCtrl.Qres; // } // if(Shoot_Info.HeatCtrl.SYS_Qres > 100){ Shoot_Info.Shoot.Shoot_DP = 19.0f; //正常拨弹盘速度 } else if((Shoot_Info.HeatCtrl.SYS_Qres > 20) &&((Shoot_Info.HeatCtrl.SYS_Qres <100))){ float a = (float)(Referee_System_Info.robot_status.shooter_barrel_cooling_value); float m = (float)Shoot_Info.HeatCtrl.SYS_Qres; float d = Shoot_Info.HeatCtrl.BulletHeat17; if (Shoot_Info.HeatCtrl.ShootCount == 0){ Shoot_Info.HeatCtrl.ShootTime = (m + 2 * a) * 10; //正常射击时间 VAL_LIMIT(Shoot_Info.HeatCtrl.ShootTime, 100, 5600); if (m < 50){ Shoot_Info.HeatCtrl.Shoot_Speed = (d * m - a - 3 * d) / (d * (Shoot_Info.HeatCtrl.ShootTime / 100.0f)) + a / d; } else{ Shoot_Info.HeatCtrl.Shoot_Speed = (d * m - a - 7 * d) / (d * (Shoot_Info.HeatCtrl.ShootTime / 100.0f)) + a / d; } } else if (0 < Shoot_Info.HeatCtrl.ShootCount && Shoot_Info.HeatCtrl.ShootCount < Shoot_Info.HeatCtrl.ShootTime){ Shoot_Info.Shoot.Shoot_DP = Shoot_Info.HeatCtrl.Shoot_Speed ; VAL_LIMIT(Shoot_Info.Shoot.Shoot_DP, 0.0f, 20.0f); } else{ Shoot_Info.Shoot.Shoot_DP = (a / d); if (Shoot_Info.Shoot.Shoot_DP < 1.f) Shoot_Info.Shoot.Shoot_DP = 0; VAL_LIMIT(Shoot_Info.Shoot.Shoot_DP, 0.0f, 20.0f); } if (Shoot_Info.HeatCtrl.ShootCount < Shoot_Info.HeatCtrl.ShootTime){ Shoot_Info.HeatCtrl.ShootCount++; } Shoot_Info.HeatCtrl.Last_Shoot_time = Shoot_Info.HeatCtrl.ShootTime; if (m>= 40){ if (Shoot_Info.HeatCtrl.ShootCount >= Shoot_Info.HeatCtrl.ShootTime){ Shoot_Info.HeatCtrl.ShootCount = 0; } } else if (m<= 32){ Shoot_Info.HeatCtrl.ShootCount = Shoot_Info.HeatCtrl.Last_Shoot_time; } } else if (Shoot_Info.HeatCtrl.SYS_Qres <= 30){ Shoot_Info.Shoot.Shoot_DP = 0.0f; //停止拨弹盘 } Shoot_Info.HeatCtrl.Last_SYS_Qrse = Shoot_Info.HeatCtrl.SYS_Qres; } ``` --- ## Gimbal重要文件 #### 射速控制 - VAL_LIMIT((Control_Info->Gimbal.Target.Shoot_Speed),6180,6400); 这个需要根据当地当时的环境进行略微调整 ```c# //保持弹频1 if(systick%121==0){ if (Shoot_Info.Shoot.initial_speed!= Control_Info->Gimbal.Target.Shoot_Speed_last){ //Shoot_Info.Shoot.initial_speed =21.8 Shoot_Info.Shoot.Fire_Trigger += SpeedAdapt(Shoot_Info.Shoot.initial_speed, 22.8,23.8,6.0,9.0); Control_Info->Gimbal.Target.Shoot_Speed += Shoot_Info.Shoot.Fire_Trigger; } else{ Shoot_Info.Shoot.Fire_Trigger = 0; } Control_Info->Gimbal.Target.Shoot_Speed_last= Shoot_Info.Shoot.initial_speed; } //限制摩擦轮转速 VAL_LIMIT((Control_Info->Gimbal.Target.Shoot_Speed),6180,6400); Control_Info->Gimbal.Target.Left_Shoot= -Control_Info->Gimbal.Target.Shoot_Speed; Control_Info->Gimbal.Target.Right_Shoot= Control_Info->Gimbal.Target.Shoot_Speed; } ``` --- #### 自瞄下位机火控 ```c# static void Control_Fire(Control_Info_Typedef *Control_Info){ if(MiniPc_Rx_Vision.Vision_Grap==1) MiniPc_Rx_Vision.Fire_Switch = IsOnTarget(INS_Info.Yaw_Angle*DegreesToRadians,-INS_Info.Roll_Angle*DegreesToRadians,MiniPc_Rx_Vision.Fire_Yaw*DegreesToRadians, MiniPc_Rx_Vision.Fire_Pitch*DegreesToRadians,MiniPc_Rx_Vision.Distance); else MiniPc_Rx_Vision.Fire_Switch=0; } ``` ```c# static bool IsOnTarget(const double cur_yaw,const double cur_pitch,const double target_yaw,const double target_pitch,const double distance) { // Judge whether to shoot shooting_range_yaw = fabs(atan2(0.185 / 2, distance)); //0.135 shooting_range_pitch = fabs(atan2(3.0 / 2, distance)); //3 pitch_ok = fabs(cur_pitch - target_pitch) < shooting_range_pitch; yaw_ok = fabs(cur_yaw - target_yaw) < shooting_range_yaw; if (yaw_ok ) { return true; } return false; } ``` ## 参考开源 1. [2025RoboMaster辽宁科技大学COD战队电控通用控制系统(达妙MC02 STM32H723VGT6)](https://gitee.com/wangcaofan/cod-h7-template) 2. [【RM2024-功率控制算法开源(含舵轮,轮腿,全自动调参)】香港科技大学ENTERPRIZE战队](https://gitee.com/link?target=http%3A%2F%2Fgithub.com%2Fhkustenterprize%2FRM2024-PowerModule%3Ftab%3Dreadme-ov-file) 3. [【RM2023-电机功率模型与功率控制开源】西交利物浦大学](https://gitee.com/link?target=http%3A%2F%2Fgithub.com%2FMaxwellDemonLin%2FMotor-modeling-and-power-control) 4. [【RM2025-超级电容与无线充电系统开源】香港科技大学ENTERPRIZE战队](https://bbs.robomaster.com/article/761385?source=4) 5. 在此十分感谢以上的个人、战队、院校。参考开源 ## 致谢 - 此代码模板源于王芃学长。感谢我的电控学长,付增波,陈泓翔,王芃。是他们留下来的达妙H7通用控制系统让我能在此基础上修改完善。尤其是感谢付增波学长,我许多的代码都是对他的拙劣模仿; 感谢陈泓翔项管,对我代码不断完善提供指导和建议。 - 感谢导航组的代雨欣,叶少龙同学和齐志昂学长为哨兵赋予灵魂。感谢视觉组张济仁同学和韩宇学长带来的自瞄策略。感谢硬件组尹纪兴和尹贻菲同学的超电,让哨兵体验到150w的功率。感谢王兴瑞同学的机械设计舵轮哨兵,让我能将此项目运用在实际机器人上。 - 致敬传奇操作手————朱君豪同学 - **感谢和我一路走来的COD成员!** - 感谢沈阳工业大学(辽阳分校)赫兹矩阵战队、沈阳理工大学 Ambition战队、东北大学T-DT战队、南京航空航天大学长空御风战队、大连理工大学凌Bug战队、辽宁石油化工大学TCN战队(排名不分先后)的开源或交流,使我可以更好的 完善此项目。 ## 展望 - 我们的队员王浩同学提出可以使用4310MIT模式作为角度轮 - 后续发现我们哨兵启动慢的原因可能是裁判系统接收数据较慢,可以尝试把裁判系统的数据接收单独分配一个Freertos任务 ## 联系方式 QQ:1634789078