No.1
前言
最近有幸参加了一期RT-Thread官方发起的RT-Robot Car DIY活动,跟着大神们的步伐我也成功的做出了一辆麦克纳姆轮PS2遥控车,心里非常的Happy,特意记录了这个制作过程用作给小白们借鉴。不多逼逼了,来开始我们造车之旅。
关于选材
本文由RT-Robot Car活动参与者SnowStorm战队队长吴鹏编写,共8698字,感谢他!
初次探索智能车本着节约成本和最低风险的原则,我们尽量选用现成的硬件材料。在探索成功后,学会了理解了其中的原理,再根据自己的需求完全设计自己的小车。(以下所有图片,点击即可查看大图)
小车制作仓:https://github.com/bluesky-ryan/snowstorm_car
官方Robot-car连接:https://github.com/RT-Thread-packages/rt-robot
(以上链接请复制至外部浏览器打开)
1、主控芯片:我们选用淘宝成品主控板(主控芯片STM32F103RCT6)(如下方左图所示)
2、底盘:麦克纳姆轮底座,某宝多的是自行选购 (如下方右图所示)
3、电机:买底座基本都带电机,我们选用带AB编码器的1:30减速电机 (如下方左图所示)
4、遥控:普通SONY PS2遥控,30-40块钱 (如下方右图所示)
5、电池:选用3S 11V航模电池(如下方左图所示)
6、线:USB转串口线一根 (如下方右图所示)
No.2
核心知识点
1.RT-Thread bsp移植。
2.STM32-CubeMXs使用。
3.RTOS使用。
4.PID控制理论。
5.麦克拉姆拉控制理论。
6.简单运动模型
开发环境
No.3
详细步骤
第一步:CubMX配置定时器1为PWM对偶模式
第二步:封装初始化、通道控制等电机控制接口(具体封装参照源码motor.c文件),最后给上层提供一个初始化接口,一个通道速度控制接口。(向?滑动查看全部)
/**
*@ingroup motor
*
*初始化定时器
*@param none
*@retrun none
*/
static void moto_pwm_init(void)
{
/* USER CODE BEGIN TIM1_Init 0 */
/* USER CODE END TIM1_Init 0 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
/* USER CODE BEGIN TIM1_Init 1 */
/* USER CODE END TIM1_Init 1 */
htim1.Instance = TIM1;
htim1.Init.Prescaler = 71;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = MOTOR_PWM_MAX - 1;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 0;
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_2) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_3) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4) != HAL_OK)
{
Error_Handler();
}
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 0;
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_LOW;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN TIM1_Init 2 */
/* USER CODE END TIM1_Init 2 */
HAL_TIM_MspPostInit(&htim1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
LOG_I("motor pwm initialization ok.\r\n");
}
/**
*@ingroup motor
*
*控制电动机标量控制,正直表示正转,负值表示反转
*
*@param ch 控制通道MOTOR_CH1/TMOTOR_CH2/TMOTOR_CH3/MOTOR_CH4,可组合使用MOTOR_CH_1|MOTOR_CH_2
*@param speed pwm控制量[-1000, 1000]
*@retrun none
*/
void motor_pwm_set(motor_chx ch, int16_t speed)
{
/* 反转 */
if (0 > speed)
{
if (-MOTOR_PWM_MAX > speed)
speed = -MOTOR_PWM_MAX;
motor_pwm_control(ch, MOTOR_DIR_REVERSE, -speed);
}
/* 正转 */
else if (0 < speed)
{
if (MOTOR_PWM_MAX < speed)
speed = MOTOR_PWM_MAX;
motor_pwm_control(ch, MOTOR_DIR_FORWARD, speed);
}
/* 停止 */
else
{
motor_pwm_control(ch, MOTOR_DIR_STOP, speed);
}
}
第三步:把主要函数加入Finsh控制台命令中,通过命令调试控制效果
/* FINSH 调试函数 */
#ifdef RT_USING_FINSH
#include <finsh.h>
FINSH_FUNCTION_EXPORT_ALIAS(motor_pwm_control, motor_control, channel direction speed);
FINSH_FUNCTION_EXPORT_ALIAS(motor_pwm_set, motor_set, channel speed);
/* FINSH 调试命令 */
#ifdef FINSH_USING_MSH
#endif /* FINSH_USING_MSH */
#endif /* RT_USING_FINSH */
第四步:通过Finsh控制台调试命令测试电机通道和PWM控制量
资料有了思路也清晰了,接下来我们要做的只是初始化一下解码器,把实时编码数读出即可
第一步:CubeMx配置解码定时器和中断
第二步:编写初始化函数和编码器数据获取函数,电机1使用中断解码,电机2、3、4使用定时器解码。(具体代码参照github上面源码,这里不再累述)
/* TIM init */
moto_pwm_init();
motor_encode2_init();
motor_encode3_init();
motor_encode4_init();
motor_encode_enable();
LOG_I("motor initialization completed.\r\n");
第三步:加入Finsh调试函数,旋转轮子查看编码值是否准确。(输入 motor_test -ge实时查看编码器值)
MSH_CMD_EXPORT(motor_test, motor_test -ge/-q);
![1565968589135](https://github.com/bluesky-ryan/snowstorm_car/blob/master/Image/motor_test)
理论知识有了,按照公式做个具体实现就好了!
第一步:实现增量PID刷新公式,具体查看源码(pid.c)
float pid_update(pid_control_t* pid, float measure_value)
第二步:将测量值输出到虚拟波形器软件上面便于观测各个值的当前情况,作者使用的是《山外多功能调试助手》,直接将数据输出到控制台串口。数据发送接口实现如下:
/* 输出数据到虚拟波形软件 */
rt_err_t send_waveform_fomate(void *buf, uint32_t size)
{
const char start[2] = {0x03, 0xfc};
const char end[2] = {0xfc, 0x03};
rt_device_t console = rt_console_get_device();
rt_device_write(console, -1, start, 2); //发送起始字符
rt_device_write(console, -1, buf, size);//发送通道数据
rt_device_write(console, -1, end, 2); //发送结束字符
return RT_EOK;
}
第三步:调节合适的速度刷新周期和PID刷新周期,周期不合适电机会剧烈抖动。作者设置周期为:
p_car->pid_sample_time = 20; /* PID刷新间隔ms */
p_car->vct_sample_time = 10; /* 速度刷新间隔ms */
第四步:调试合适的PID参数,由于作者选用电机一致性不好,所以设置参数时每个轮子正传和反转的PID参数都是独立的。具体实现查看代码:
wheel_select_pid_kx(&p_car->m_wheel[i]); /* 根据速度设置PID参数 */
作者样车PID参数:
#define CHX_PID_KX_TABLE \
{ \
{MOTOR_CH1, {1.100, 0.400, 0.500}, {1.100, 0.400, 0.500}},\
{MOTOR_CH2, {1.100, 0.400, 0.500}, {1.100, 0.400, 0.500}},\
{MOTOR_CH3, {1.100, 0.400, 0.500}, {1.100, 0.400, 0.500}},\
{MOTOR_CH4, {0.765, 0.330, 0.100}, {0.260, 0.200, 0.010}},\
}
PS2我们只需要读取遥控数据,一个扫描函数搞定,定期刷新一个按键值即可,具体代码参照(ps2.c):
int ps2_scan(ps2_ctrl_data_t *pt)
遥控映射到控制值:
/* PS映射car cmd表格,组合键命令在头添加,单键命令放后面 */
car_ps2_cmd_t ps2_to_cmd_table[] = {
{PS2_BTN_RIGHT| PS2_BTN_UP, CAR_CMD_FORWARD_RIGHT},
{PS2_BTN_LEFT | PS2_BTN_UP, CAR_CMD_FORWARD_LEFT},
{PS2_BTN_RIGHT| PS2_BTN_DOWN, CAR_CMD_BACK_RIGHT},
{PS2_BTN_LEFT | PS2_BTN_DOWN, CAR_CMD_BACK_LEFT},
{PS2_BTN_UP, CAR_CMD_FORWARD},
{PS2_BTN_DOWN, CAR_CMD_BACK},
{PS2_BTN_RIGHT, CAR_CMD_RIGHT},
{PS2_BTN_LEFT, CAR_CMD_LEFT},
{PS2_BTN_CICLE, CAR_CMD_TURN_RIGHT},
{PS2_BTN_SQUARE, CAR_CMD_TURN_LEFT},
};
控制映射到4个轮子的具体速度值:
/* 命令映射到几何控制参数 */
car_cmd_math_t cmd_to_math_table[] = {
{CAR_CMD_INVALID, { 0, 0, 0, 0}},
{CAR_CMD_STOP, { 0, 0, 0, 0}},
{CAR_CMD_FORWARD_LEFT, { 0, 120, 120, 0}},
{CAR_CMD_FORWARD_RIGHT, { 120, 0, 0, 120}},
{CAR_CMD_BACK_LEFT, {-120, 0, 0, -120}},
{CAR_CMD_BACK_RIGHT, { 0, -120, -120, 0}},
{CAR_CMD_FORWARD, { 120, 120, 120, 120}},
{CAR_CMD_BACK, {-120, -120, -120, -120}},
{CAR_CMD_RIGHT, { 120, -120, -120, 120}},
{CAR_CMD_LEFT, {-120, 120, 120, -120}},
{CAR_CMD_TURN_RIGHT, { 120, -120, 120, -120}},
{CAR_CMD_TURN_LEFT, {-120, 120, -120, 120}},
};
再开一个线程定期刷新各个轮子的控制即可。
No.4
视频预览
No.5
经验总结
彩蛋:之后还会有智能战车相关高级功能实现的教程连载,敬请期待!
END