本文由 EMakeFun-军弟 编写.
队伍名
项目名Github 地址鲲鹏战队鲲鹏战车https://github.com/Eronwu/roc_robot以下是RT-Thread DIY 智能车赛1000元优胜奖得主——鲲鹏战队的作品分享!
附上完整作品视频:
记得小时候大约10岁的时候那个时候家里穷没有玩具玩,某天发现老爸以前矿山里挖煤留下的工具箱里有几个崭新的轴承,如获至宝,赶紧找来锯子和木板制作了人生第一台车,大概效果如下
当时带着自制小车,在村口玩的时候,在小伙伴们里的风靡程度,不亚于现在一台跑车的回头率
就这样这辆小车陪伴我过了美好的童年,不过我永远不会忘记当年山坡翻车那个场景,如今我手背那块伤疤就是当年的记忆。最后一次见到那台车,应该是14年回老家还看到老屋角落躺着我的那辆木制轴承车,后面就没有了。。。。。放一张我想我当时玩的时候大概是这样子做个纪念吧!!!
后来上大学选了自己最爱的电子专业,错过了飞思卡尔智能车比赛(两年一届),但是那个时候还没有Robomaster都没机会再玩到车,非常的遗憾。工作5,6年后偶然看到RT-thread举办的战车制作活动,就毫不犹豫就参加了,一路制作没有遇到太多艰辛,只完成了最基础的功能,有些遗憾,但是恰好30来临之际,记录一下做车的过程,算是给自己的生日礼物好了。
一. 器材选择篇
在做这个车之前,有参考大量资料和车模,和队友们一起商量,希望做一台类似大疆步兵战车,所以我们在器件选型的时候大量参考了RobotMaster的器件参数,但是考虑大疆配件昂贵,我们自己综合了价格和性能进行选型。
MCU:STM32L475
主频:80M
完善RT-Theard系统支持
板载AP6181
集成ST-Link非常方便调试
电机选择
市面上各种直流电机种类特别多
主要有刷电机,无刷之分,考虑价格和够用原则 选择无刷直流电机
控制方式:霍尔编码,光电编码,电调,CAN 考虑控制简单和价格。
直接选用编码电机参数如下:
电机参数类型AB双相增量式霍尔编码电机供电电机24V,编码器5V减速比1比19额定转速504rpm额定转矩14kg.cm负载电流2.6A峰值电流9.5A有了前面电机参数,我们选择驱动板就简单得多了,需要关注参数为驱动电压超过24V,驱动电流大于3A,功率要超过 3*24 = 72W.
淘宝找到如下一块满足要求
产品参数供电电压6.5V~27V 不能超过27V双路电机接口没路额定输出电流7A,峰值50A功率24V电机115W 12V电机40W控制信号电平3~6.5V电压控制方式:
IN1IN2ENA1OUT1,OUT2输出00X刹车11X悬空10PWM正转调速01PWM反转调速101全速正转011全速反转前面电机需要24V供电,所以需要串联3组2S 7.4V锂电池组(或者2组3S 11.1V电池组)我们选用3组1800mA 25C航模电池,最大放电 1.8A * 25 = 45A,大于 4个电机,峰值电流 4 * 9 =36A要求
电压降压模块 选用LM2596S 24V转5V,最大电流3A,给loT-board和编码电机供电我们购买了100mm 具体参数如下:
型号100mm直径重量1.52kg负载能力40kg厚度50mm支撑轮抽直径3mm支撑轮个数9个组合图
二. 小车组装篇
电池改造
为了获取24V高电压,我们需要将3节7.4V航模电池正负极串联起来(操作过程千万注意同一个电池正负不要碰到)并安装一个拨动开关,改造之后的电池如下:
供电连接
我们先把所需的供电需求列出来:
模块需要电压供电方式IoT-Board+5V电池经过LM2596S降压到5V电机驱动板电源接口+24V电池电压直出电机驱动板5V引脚+3.3VloT-board的GPIO电平一致所以只能给3.3V否则pwm不能调速编码电机5V+5V电池经过LM2596S降压到5V接线如下:
主控板GPIO口引脚规划如下
IoT-Board GPIO引脚名字GPIO引脚序号PWM namePWM channel电机驱动板引脚PD1259pwm4channel 1电机驱动板A 引脚EN1PD1360电机驱动板A 引脚IN1PA429电机驱动板A 引脚IN2PB895pwm4channel 3电机驱动板A 引脚EN2PB996电机驱动板A 引脚IN3PA867电机驱动板A 引脚IN4PA023pwm2channel 1电机驱动板B 引脚EN1PA124电机驱动板B 引脚IN1PC217电机驱动板B 引脚IN2PB103pwm2channel 1电机驱动板B 引脚EN2PB1148电机驱动板B 引脚IN3PC433电机驱动板B 引脚IN4PB1251左前编码电机APB1352左前编码电机BPB1453右前编码电机APB1554右前编码电机BPD1461左后编码电机APD1562左后编码电机BPC663右后编码电机APC764右后编码电机B正面:
底面:
三. 软件环境搭建篇
请将以下链接复制至外部浏览器打开这部分直接参考官方给的环境搭建,非常完整,不在重复编写STM32 运行:https://github.com/RT-Thread/rt-thread/tree/master/bsp/stm32
ENV工具可以通过以下链接获取:https://pan.baidu.com/s/1cg28rk#list/path=%2F
四. PWM 板载wifi驱动移植篇
BSP code base选择
由于购买是IoT-Board潘多拉主板有两个bsp可以使用第一个是https://github.com/RT-Thread/rt-thread整个软件比较庞大,支持非常多型号主板和芯片,架构也完善。另外一个是 https://github.com/RT-Thread/IoT_Board这个是专门针对潘多拉板子官方做得bsp,潘多拉板子上所有硬件的库都完美支持,拿来即可用,比如板载wifi AP6181完全移植好,只需要实现tcp service即可完成wifi遥控。
一开始我这边是用RT-Thread BSP移植好pwm,一切调试正常,后面移植wifi时发现,AP6181,lwip,wlan等网络组件默认并没有打开,一顿操作终于移植完成,最后移植编译正常,发现无法连接wifi, 由于对wifi驱动了解不深,解决不了最后只好放弃这个方案。直接使用IoT-Board固件的wifi_manage工程,wifi问题解决,但是这个固件也不完美,这个板子是专门物联网开发的,对于pwm配置工程里面本身不包含配置选型,这样就需要把pwm从驱动到应用到配置去打通,下面是移植简单过程。
pwm驱动移植
本次制作的小车驱动方式为单pwm驱动方式,两个IO控制电机方向,具体看之前驱动板的介绍。
早已习惯源码简单粗暴的开发方式 我并没有去使用STM32官网先进的工具stm32CubeMX首先找到pwm驱动入口文件drivers\drv_pwm.cstm32_pwm_init(void)函数开始查看代码根据之前配置好的pwm频道
1#define LEFT_FORWARD_PWM "pwm4" 2#define LEFT_FORWARD_PWM_CHANNEL 1 // GPIO PD12 3 4#define LEFT_BACKWARD_PWM "pwm4" 5#define LEFT_BACKWARD_PWM_CHANNEL 3 // GPIO PB8 6 7#define RIGHT_FORWARD_PWM "pwm2" 8#define RIGHT_FORWARD_PWM_CHANNEL 1 // GPIO PA0 9 10#define RIGHT_BACKWARD_PWM "pwm2" 11#define RIGHT_BACKWARD_PWM_CHANNEL 3 // GPIO PB10填充相关枚举和结构体
1宏定义 PWM2_CONFIG PWM4_CONFIG 2static struct stm32_pwm stm32_pwm_obj[] 3最后rtconfig.h 4#define RT_USING_PWM 5 6#define BSP_USING_PWM 7#define BSP_USING_PWM2 8#define BSP_USING_PWM2_CH1 9#define BSP_USING_PWM2_CH2 10#define BSP_USING_PWM2_CH3 11#define BSP_USING_PWM2_CH4 12#define BSP_USING_PWM4 13#define BSP_USING_PWM4_CH1 14#define BSP_USING_PWM4_CH2 15#define BSP_USING_PWM4_CH3 16#define BSP_USING_PWM4_CH4实现如下几个函数,一定要注意时钟的使能:
1void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) 2void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim) 3static void pwm_get_channel(void) 4在调试pwm的过程中 我们如果遇到电机不动,可以如下将drv_pwm的log打开,然后看log哪里出错,如果整个流程都通还不动,可以对照pwm裸机程序调试
1#define DBG_SECTION_NAME "drv.pwm" 2#define DBG_LEVEL DBG_LOG 3#include <rtdbg.h>wifi tcp service收发数据
IoT-board板载wifi实在觉得另外接wifi或者其他控制方式没有必要,所以只需要实现tcp service就可以了.
1void tcprecvserv(void *parameter) 2{ 3 unsigned char *recv_data; 4 socklen_t sin_size; 5 int sock, bytes_received; 6 struct sockaddr_in server_addr, client_addr; 7 rt_bool_t stop = RT_FALSE; 8 int ret; 9 int nNetTimeout = 20; 10 recv_data = (unsigned char *)rt_malloc(BUFFER_SIZE); 11 rt_kprintf("tcpserv start ......\n"); 12 if (recv_data == RT_NULL) 13 { 14 rt_kprintf("No memory\n"); 15 return; 16 } 17 18 if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) 19 { 20 rt_kprintf("Socket error\n"); 21 22 rt_free(recv_data); 23 return; 24 } 25 26 server_addr.sin_family = AF_INET; 27 server_addr.sin_port = htons(5000); 28 server_addr.sin_addr.s_addr = INADDR_ANY; 29 rt_memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero)); 30 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (char *)&nNetTimeout, sizeof(int)); 31 if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) 32 { 33 rt_kprintf("Unable to bind\n"); 34 rt_free(recv_data); 35 return; 36 } 37 38 if (listen(sock, 5) == -1) 39 { 40 rt_kprintf("Listen error\n"); 41 42 /* release recv buffer */ 43 rt_free(recv_data); 44 return; 45 } 46 47 rt_kprintf("\nTCPServer Waiting for client on port 5000...\n"); 48 while (stop != RT_TRUE) 49 { 50 sin_size = sizeof(struct sockaddr_in); 51 52 rt_kprintf("Listen start = %d\n", connected); 53 connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size); 54 rt_kprintf("Listen end = %d\n", connected); 55 if (connected < 0) 56 { 57 rt_kprintf("accept connection failed! errno = %d\n", errno); 58 continue; 59 } 60 61 rt_kprintf("I got a connection from (%s , %d)\n", 62 inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); 63 64 while (1) 65 { 66 bytes_received = recv(connected, recv_data, BUFFER_SIZE, MSG_WAITALL); 67 if (bytes_received < 0) 68 { 69 closesocket(connected); 70 break; 71 } else if (bytes_received == 0) { 72 rt_kprintf("\nReceived warning,recv function return 0.\r\n"); 73 closesocket(connected); 74 connected = -1; 75 break; 76 } 77 rt_ringbuffer_put_force(tcp_dat, (const rt_uint8_t *)recv_data, bytes_received); 78 } 79 } 80 81 closesocket(sock); 82 rt_ringbuffer_destroy(tcp_dat); 83 rt_free(recv_data); 84 return ; 85}默认端口号为5000 ,这里特别强调一下,wifi收数据时一开始收发不知道怎么处理,正打算实现做一个ringbuffer,结果一看RT-thread有个rt_ringbuffer非常好用,使用也非常简单,解决收数据,解析数据一大麻烦.
1rt_ringbuffer_create(2*BUFFER_SIZE); // 创建ringbuffer 2rt_ringbuffer_put_force(tcp_dat, (const rt_uint8_t *)recv_data, bytes_received); // wifi收到数据后往buffer写数据然后主循环里只需要判断是否有数据就行
1while (rt_ringbuffer_data_len(tcp_dat) != 0) { 2 ...... 3 rt_ringbuffer_getchar(tcp_dat, &dat); 4 ..... 5 }就可以了,完全不用担心丢包问题好了wifi测试来一张图
五. 软件框架,控制协议、全向控制基础原理篇
代码目录:application\roc_car\applications
代码架构:
roc_car│├─docs│ *.md // 文档介绍│├─applications│ main.c // 主入口│ main.h│ protocol.h // 协议头文件│ protocol_parser.c // 协议解析文件│ protocol_parser.h│ roc_robot.c // 小车控制函数文件│ roc_robot.h│ tcprecv.c // tcp 接受数据函数│ wifi_connect.c│ ├─ports└─SConscript // RT-Thread 默认的构建脚本
对于电机的控制直接采用了 RT-Thread官方的rt-robot框架,我使用的单路pwm驱动方式,为了方便使用这个框架我对rt-robot再做了一层封装
1typedef struct { 2 single_pwm_motor_t left_forward_motor, left_backward_motor, right_forward_motor, right_backward_motor; 3 ab_phase_encoder_t left_forward_encoder, left_backward_encoder, right_forward_encoder, right_backward_encoder; 4 inc_pid_controller_t left_forward_pid, left_backward_pid, right_forward_pid, right_backward_pid; 5 wheel_t left_forward_wheel, left_backward_wheel, right_forward_wheel, right_backward_wheel; 6 7 motor_t x_servo, y_servo; 8 kinematics_t c_kinematics; 9 E_ROC_ROBOT_STATUS status; 10 rt_int8_t speed ; 11 rt_int16_t degree; 12}ST_ROC_ROBOT;使用的时候再封装如下几个操作函数:
1roc_robot_init() 2roc_robot_go_forward() 3roc_robot_go_backward() 4roc_robot_turn_right() 5roc_robot_turn_right_rotate() 6roc_robot_turn_left() 7roc_robot_turn_left_rotate() 8roc_robot_stop()控制基础原理
为了实现上面几个函数我们需要了解一些最基本的原理,首先我们控制小车前进,后退,向左,向右,左旋,右旋这六个个基本功能。
旋转原理
前面只是几个基本动作的控制,如果我们要实现全相控制呢?那么我们需要学习一下基础原理知识通过这篇麦克纳姆轮控制原理文章的讲解我们知道,全向移动底盘是一个纯线性系统,而刚体运动又可以线性分解为三个分量。那么只需要计算出麦轮底盘在Vx「沿X轴平移」、Vy「沿Y轴平移」、w「绕几何中心自转」时,四个轮子的速度,就可以通过简单的加法,计算出这三种简单运动所合成的「平动+旋转」运动时所需要的四个轮子的转速。而这三种简单运动时,四个轮子的速度可以通过简单的测试,或是推动底盘观察现象得出。
当底盘沿着 X 轴平移时:
1V左前 = +Vx 2V右前 = -Vx 3V左后 = - Vx 4V右后 = +Vx当底盘沿着 Y 轴平移时:
1V左前 = Vy 2V右前 = Vy 3V左后 = Vy 4V右后 = Vy当底盘绕几何中心自转时:
1V左前 = W 2V右前 = -W 3V左后 = W 4V右后 = -W将以上三个方程组相加,得到的恰好是根据「传统」方法计算出的角度,综合到一起就是
1V左前 = +Vx + Vy + W 2V右前 = -Vx + Vy -W 3V左后 = - Vx + Vy + W 4V右后 = +Vx + Vy -W由于 rt-robot 的全向控制和我遥控程序的坐标系不同所以重新实现了一下这个函数:
1void roc_robot_run(rt_int16_t x, rt_int16_t y, rt_int16_t rotate) 2{ 3 rt_int16_t lf_speed = x + y + rotate; 4 rt_int16_t lb_speed = -x + y + rotate; 5 rt_int16_t rf_speed = -x + y -rotate; 6 rt_int16_t rb_speed = x + y - rotate; 7 single_pwm_motor_set_speed(roc_robot.left_forward_motor, lf_speed *10); 8 single_pwm_motor_set_speed(roc_robot.left_backward_motor, lb_speed *10); 9 single_pwm_motor_set_speed(roc_robot.right_forward_motor, rf_speed *10); 10 single_pwm_motor_set_speed(roc_robot.right_backward_motor, rb_speed *10); 11}前面已经把基础原理介绍了一遍,那我们到底怎么来实现wifi遥控呢?
协议部分
1typedef struct 2{ 3 rt_uint8_t start_code ; // 8bit 0xAA 4 rt_uint8_t len; // protocol package data length 5 E_ROBOT_TYPE type; 6 rt_uint8_t addr; 7 E_CONTOROL_FUNC function; // 8 bit 8 rt_uint8_t *data; // n bit 9 rt_uint16_t sum; // check sum 16bit 10 rt_uint8_t end_code; // 8bit 0x55 11} ST_PROTOCOL; // wifi数据字节流结构体 12 13typedef enum 14{ 15 E_BATTERY = 1, 16 E_LED, 17 E_BUZZER, 18 E_INFO, 19 E_ROBOT_CONTROL_DIRECTION, //机器人控制角度 (0~360) 20 E_ROBOT_CONTROL_SPEED, //机器人控制速度 (0~100) 21 E_TEMPERATURE, 22 E_INFRARED_TRACKING, 23 E_ULTRASONIC, 24 E_INFRARED_REMOTE, 25 E_INFRARED_AVOIDANCE, 26 E_CONTROL_MODE, //12 27 E_BUTTON, 28 E_LED_MAXTRIX, 29 E_CMD_LINE, 30 E_VERSION, 31 E_UPGRADE, 32 E_PHOTORESISTOR, 33 E_SERVER_DEGREE, 34 E_CONTOROL_CODE_MAX, 35} E_CONTOROL_FUNC; // wifi控制指令功能部分我们先来看下wifi遥控界面什么样子:
Android端APP界面示意图
“A、D”部分为加减速按钮。
“B”部分为右自旋。
“C”部分为左自旋。
“E”部分为重力遥感开关,可切换到重力遥感模式
“I” 部分为遥控手柄切换。
“J” 部分为当前速度显示。
通过上面图片我们知道我将0~360度作为全向运动的方向角,基础控制速度可调节,左右旋转独立
我们先建立如下一个xy 轴和0~360度的对应控制关系坐标系如下
假如这个时候我们从wifi获取到角度 为 degree,由于apk设计原因,没有做旋转角度指盘那么x轴和y轴的速度为如下:
1Vx = cos(degree) * speed 2Vy = sin(degree)*speed上面这个计算公式,就可以很容易实现如下代码:
1void roc_robot_drive(rt_uint16_t degree) 2{ 3 LOG_D("roc_robot_drive %d", degree); 4 rt_int16_t x, y; 5 6 if (degree == 0XFFFF) { 7 roc_robot_stop(); 8 } else { 9 x = cos(degree)*roc_robot.speed; 10 y = sin(degree)*roc_robot.speed; 11 roc_robot_run(x, y, 0); 12 } 13} 14整个程序流程如下: 15 16main.c: 17 18wifi_auto_connect(); // 自动链接wifi热点函数 19 20roc_robot_init(0); // 使用RT-Robot框架初始化RT-Robot驱动, 函数前进后退方法都在roc_robot.c里面 21 22rt_thread_create("led_flash", led_flash... // 判断wifi连接情况起的线程 23 24ret = rt_thread_create("wifi_control", wifi_control... // 将接受的wifi数据转化为指令执行 25 26tcprecvserv((void *)pareser_package.buffer); // 接受wifi数据到data下六. 应用编写,手机遥控器篇
协议部分同文档小车应用篇 - 【协议部分】说明
使用标准tcp协议进行连接通信
通过不同按键响应拼凑不同字节流进行tcp socket 数据发送
开发环境
Android studio
需要的可以通过这里下载android app
https://github.com/emakefun/hummer-bot/blob/Hummer-bot4.0/APP/Emakefun_Robot.apk
总结
一开始想做战车类型,所以电机选型太大,驱动板功率很大,但是车上是亚克力,整个车容易“暴动”,需要装减震器。
前期太注重软件框架和wifi控制协议编写,导致没有时间做闭环控制调试,但是后面加入进去。
未能把摄像头云台加进去。
后面有想法用树莓派4或者jetson nano主板来做
最开心的是RT-Thread组织这个活动认识了一大帮做车的大牛,特别是指导老师吴博的知识渊博打开我的视野,另外一个吴鹏对技术的纯粹执着的态度令我敬佩。
当然也要非常感谢阿波基友和其他队友的支持
RT-Thread线上活动
1、【RT-Thread开发者大会报名】2019年RT-Thread开发者大会将登入成都、上海、深圳与开发者们见面,还有RT-Thread在中高端智能领域的应用、一站式RTT开发工具、打造IoT极速开发模式等干货演讲,期待您的参与!本次大会也设立了codelab动手实验室活动,开发者可在现场体验RT-Thread给开发带来的便捷!
立即报名
2、RT-Thread能力认证考前线上培训,将于11月25日全线截止报名,如果您有晋升、求职、寻找更好机会的需要,有深入学习和掌握RT-Thread的需求,请尽快垂询/报考!学生优惠价:168/人
学生专属报名通道
能力认证官网链接:https://www.rt-thread.org/page/rac.html(在外部浏览器打开)
立即报名(非学生)
#题外话# 喜欢RT-Thread不要忘了在GitHub上留下你的STAR哦,你的star对我们来说非常重要!链接地址:https://github.com/RT-Thread/rt-thread
你可以添加微信17775983565为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群
RT-Thread
长按二维码,关注我们
点击“阅读原文”报名线下培训/开发者大会