匆匆忙忙的2020年结束了,在机器人平台开发方面算是搞清了些端倪。近来总算有时间回顾过往,稍作整理。计划写一个专题系列,内容即为“机器人控制系统设计与实现”,这将会是我的系列文章的宗旨。机器人平台从地面无人车到空中无人机到水下无人潜航器,这将是一个可上九天揽月、可下五洋捉鳖的宏大蓝图。讲述内容从控制算法到嵌入式软件开发,从计算机仿真到单片机实战,从底层驱动到上层应用。所有的代码源于项目实战,讲述方式将是雅俗共赏。记录与分享心得的过程是快乐的,既是悦己、也可及人。如果有志同道合的朋友们可以动动小爪点个关注,我们一起成长、一起进步叭!
第一篇从实现一个四轮四驱小车开始~
在机器人的大家庭里,轮式机器人独领风骚。轮式机器人常见的有两轮平衡车(二驱差速)、四轮车(四驱差速)、三轮车(二驱)、阿克曼转向小车(与汽车最为相似)、麦克纳姆轮(Mecanum wheel)小车、全向轮(omni wheel)小车等。感兴趣的小伙伴可以分别去了解运动与控制原理,市场上的绝大部分地面移动机器人大抵都是基于以上平台了。这里我们实现一个四轮小车,四轮都有电机驱动,左右电机各为一组,控制左右侧电机差速即可实现转向,很简单有木有? 但是麻雀虽小,五脏俱全,在此篇中我们基于STM32实现机器人的控制,使用UCOS-III操作系统,实现遥控器通信、传感器数据读取、航向角串级PID控制、电机PID控制全部功能。上位机地面站的开发我放在下一篇四轴飞行器的讲解中。
搭建四轮小车的机械部分就不细讲了,有条件的自己设计机架机加工或3D打印,没条件的直接买一个四轮底盘即可,电机、轮子配套,成本不高。
电机选择就很多了,小型四轮车常见的是编码器电机,如图1所示,一般是六根接线,两根编码器电源线,两根电源线,两根编码器信号线(A相B相)。只需要一路编码器就可用于测速,两路可以测方向,控制板需要解析编码器信号做速度闭环控制。
另外一种是总线电机,常见的是CAN总线电机、串口总线电机,这种电机一般会有配套的驱动器,控制板只需要给定速度、位置信号。如图所示是大疆的M3508行星减速电机与配套的C620电调(RoboMaster用的电机同一系列),可以使用CAN总线控制和PWM两种控制方式。本篇我们就用这个电机,基于CAN总线方式去做速度闭环。
驱动板是小车的大脑,由于本篇所实现的功能只是实现基本运动,就是遥控器控制小车直行和转弯,所以只需要一个底层驱动控制板,在后续增加高级功能之后会叠加上层控制器(如树莓派、英伟达)。
板子可以买,以可以画。主要是要有IIC、串口、CAN总线、编码器接口等外设接口和电源接口。前述的编码器电机的接口是通用的,淘宝上有很多板子是带了这种接口的,如果没有的话将电机的线分别接到对应的IO口即可,只是不太好看。
本文选择自主开发的使用STM32F7主控的驱动板,时钟频率216MHz,一般的智能小车应用都足够了。
传感器主要是需要姿态传感器,一般使用九轴惯性传感器,即三轴陀螺仪、三轴加速度计、三轴磁力计。最终目的就是解算三轴姿态角,这里简单粗暴,直接买了一个姿态角传感器,如图所示是维特智能的传感器,串口输出三轴姿态角,其内部还是使用姿态解算算法算出来的。如果使用MPU9250等模块就需要自己进行解算了,后面有时间专门写一篇滤波算法与姿态解算。
建议选用锂电池。
本系列文章都会使用操作系统,UCOS-III和FreeRTOS原理差不多,只不过前者商用后者免费,我使用的是UCOS-III。如果没有嵌入式操作系统的小伙伴们自行补补课叭,不会也没关系。不影响核心代码部分的阅读。
在空的操作系统的工程模板中,创建4个任务,一个CommunicateTask,SensorTask,StabilizationTask,MotorTask,分别进行遥控器通信、传感器数据读取、姿态控制、电机控制。使用操作系统的优势就是实时性,请大家牢记这个概念。 在操作系统下,每个任务都像是同时运行的,其实是分时复用,只有在多核的处理器上才能实现真正的同时。
首先在main文件的起始部分定义任务优先级、任务堆栈、堆栈大小、任务函数等。这里的startTask用于创建其他任务,创建完之后就挂起自己。
//任务优先级
#define START_TASK_PRIO 3
//任务堆栈大小
#define START_STK_SIZE 128
//任务控制块
OS_TCB StartTaskTCB;
//任务堆栈
CPU_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *p_arg);
//communicate任务
//设置任务优先级
#define COMMUNICATE_TASK_PRIO 5 // SBUS 信号的更新是在串口中断中进行的
//任务堆栈大小
#define COMMUNICATE_STK_SIZE 512
//任务控制块
OS_TCB CommunicateTaskTCB;
//任务堆栈
CPU_STK COMMUNICATE_TASK_STK[COMMUNICATE_STK_SIZE];
//led0任务
void communicate_task(void *p_arg);
//stabalizer任务
//设置任务优先级
#define STABILIZATION_TASK_PRIO 4
//任务堆栈大小
#define STABILIZATION_STK_SIZE 2048
//任务控制块
OS_TCB StabilizationTaskTCB;
//任务堆栈
CPU_STK STABILIZATION_TASK_STK[STABILIZATION_STK_SIZE];
//led0任务
void stabilization_task(void *p_arg);
//Motor电机任务
//设置任务优先级
#define MOTOR_TASK_PRIO 7
//任务堆栈大小
#define MOTOR_STK_SIZE 512
//任务控制块
OS_TCB MotorTaskTCB;
//任务堆栈
CPU_STK MOTOR_TASK_STK[MOTOR_STK_SIZE];
//motor任务
u8 motor_task(void *p_arg);
//sensorTask 参数配置任务 在线调试参数并写入flash
//设置任务优先级
#define SENSOR_TASK_PRIO 6
//任务堆栈大小
#define SENSOR_STK_SIZE 512
//任务控制块
OS_TCB SensorTaskTCB;
//任务堆栈
CPU_STK SENSOR_TASK_STK[SENSOR_STK_SIZE];
//motor任务
u8 sensor_task(void *p_arg);
在main函数中系统初始化、外设初始化、创建开始任务
int main(void)
{
OS_ERR err;
CPU_SR_ALLOC();
Write_Through(); //Cahce强制透写
MPU_Memory_Protection(); //保护相关存储区域
Cache_Enable(); //打开L1-Cache
Stm32_Clock_Init(432, 25, 2, 9); //设置时钟,216Mhz
HAL_Init(); //初始化HAL库
delay_init(216); //延时初始化
uart1_init(100000); //串口1初始化
uart2_init(115200); //串口2初始化
uart3_init(115200); //串口3初始化
IIC_Init(); //IIC通讯口初始化
//uart_imu_Init(); // 初始化串口IMU
//JY901_Init(); // 初始化IIC IMU , IMU的初始化都只需要执行一次,之后注释掉这两行代码
MY_ADC_Init();
KEY_Init(); //按键初始化
LED_Init(); //初始化LED
PWM_Init();
CAN1_Mode_Init(CAN_SJW_1TQ, CAN_BS2_6TQ, CAN_BS1_11TQ, 3, CAN_MODE_NORMAL); //CAN初始化正常模式,波特率1000Kbps
OSInit(&err); //初始化UCOSIII
OS_CRITICAL_ENTER(); //进入临界区
//创建开始任务
OSTaskCreate((OS_TCB *)&StartTaskTCB, //任务控制块
(CPU_CHAR *)"start task", //任务名字
(OS_TASK_PTR)start_task, //任务函数
(void *)0, //传递给任务函数的参数
(OS_PRIO)START_TASK_PRIO, //任务优先级
(CPU_STK *)&START_TASK_STK[0], //任务堆栈基地址
(CPU_STK_SIZE)START_STK_SIZE / 10, //任务堆栈深度限位
(CPU_STK_SIZE)START_STK_SIZE, //任务堆栈大小
(OS_MSG_QTY)0, //任务内部消息队列能够接收的最大消息数目,为0时禁止接收消息
(OS_TICK)0, //当使能时间片轮转时的时间片长度,为0时为默认长度,
(void *)0, //用户补充的存储区
(OS_OPT)OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR, //任务选项
(OS_ERR *)&err); //存放该函数错误时的返回值
OS_CRITICAL_EXIT(); //退出临界区
OSStart(&err); //开启UCOSIII
while (1)
;
}
开始任务的内容如下,内容就是创建其他四个任务,之后把自己挂起。
//开始任务函数
void start_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
p_arg = p_arg;
CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
OSStatTaskCPUUsageInit(&err); //统计任务
#endif
#ifdef CPU_CFG_INT_DIS_MEAS_EN //如果使能了测量中断关闭时间
CPU_IntDisMeasMaxCurReset();
#endif
#if OS_CFG_SCHED_ROUND_ROBIN_EN //当使用时间片轮转的时候
//使能时间片轮转调度功能,设置默认的时间片长度
OSSchedRoundRobinCfg(DEF_ENABLED, 1, &err);
#endif
__HAL_RCC_CRC_CLK_ENABLE(); //使能CRC时钟
OS_CRITICAL_ENTER(); //进入临界区
//communicate任务 通信任务
OSTaskCreate((OS_TCB *)&CommunicateTaskTCB,
(CPU_CHAR *)"Communicate task",
(OS_TASK_PTR)communicate_task,
(void *)0,
(OS_PRIO)COMMUNICATE_TASK_PRIO,
(CPU_STK *)&COMMUNICATE_TASK_STK[0],
(CPU_STK_SIZE)COMMUNICATE_STK_SIZE / 10,
(CPU_STK_SIZE)COMMUNICATE_STK_SIZE,
(OS_MSG_QTY)0,
(OS_TICK)10,
(void *)0,
(OS_OPT)OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_SAVE_FP,
(OS_ERR *)&err);
//Stabilizaiton任务 姿态控制
OSTaskCreate((OS_TCB *)&StabilizationTaskTCB,
(CPU_CHAR *)"Stabilization task",
(OS_TASK_PTR)stabilization_task,
(void *)0,
(OS_PRIO)STABILIZATION_TASK_PRIO,
(CPU_STK *)&STABILIZATION_TASK_STK[0],
(CPU_STK_SIZE)STABILIZATION_STK_SIZE / 10,
(CPU_STK_SIZE)STABILIZATION_STK_SIZE,
(OS_MSG_QTY)0,
(OS_TICK)10,
(void *)0,
(OS_OPT)OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR |OS_OPT_TASK_SAVE_FP,
(OS_ERR *)&err);
//Motor任务
OSTaskCreate((OS_TCB *)&MotorTaskTCB,
(CPU_CHAR *)"Motor task",
(OS_TASK_PTR)motor_task,
(void *)0,
(OS_PRIO)MOTOR_TASK_PRIO,
(CPU_STK *)&MOTOR_TASK_STK[0],
(CPU_STK_SIZE)MOTOR_STK_SIZE / 10,
(CPU_STK_SIZE)MOTOR_STK_SIZE,
(OS_MSG_QTY)0,
(OS_TICK)10,
(void *)0,
(OS_OPT)OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR | OS_OPT_TASK_SAVE_FP,
(OS_ERR *)&err);
//Sensor任务
OSTaskCreate((OS_TCB *)&SensorTaskTCB,
(CPU_CHAR *)"Sensor task",
(OS_TASK_PTR)sensor_task,
(void *)0,
(OS_PRIO)SENSOR_TASK_PRIO,
(CPU_STK *)&SENSOR_TASK_STK[0],
(CPU_STK_SIZE)SENSOR_STK_SIZE / 10,
(CPU_STK_SIZE)SENSOR_STK_SIZE,
(OS_MSG_QTY)0,
(OS_TICK)10,
(void *)0,
(OS_OPT)OS_OPT_TASK_STK_CHK | OS_OPT_TASK_STK_CLR |OS_OPT_TASK_SAVE_FP,
(OS_ERR *)&err);
OS_TaskSuspend((OS_TCB *)&StartTaskTCB, &err); //挂起开始任务
OS_CRITICAL_EXIT(); //退出临界区
}
紧接着写四个主要任务,这四个函数是主体部分,分别实现遥控器通信、传感器数据读取、姿态控制、电机伺服。四个函数全是空的,啥也没干,只有一个while循环中delay_ms(10),意思就是每隔10ms执行一次这个任务(是不是就是裸机里面的while(1)循环?)后续文章会依次往各个任务里面加内容。
//通信任务
void communicate_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
while (1)
{
delay_ms(10);
}
}
void stabilization_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
{
delay_ms(10);
}
}
u8 sensor_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
{
delay_ms(10);
}
}
u8 motor_task(void *p_arg)
{
OS_ERR err;
CPU_SR_ALLOC();
{
delay_ms(10);
}
}
以上关于操作系统的代码部分如果没有操作系统基础的小伙伴们可能有点云里雾里,可以不用管。只需要明白,它其实是把一个main函数变成了多个main函数,名义上的main函数只进行初始化和任务创建,每个void XXX_task()就是一个main函数,多个main函数分时复用,就像多个任务同时执行。
下一篇开始进行核心代码的实现。