本文由 EMakeFun-军弟 编写.

队伍名

项目名Github 地址
鲲鹏战队鲲鹏战车https://github.com/Eronwu/roc_robot

以下是RT-Thread DIY 智能车赛1000元优胜奖得主——鲲鹏战队的作品分享!

附上完整作品视频:

记得小时候大约10岁的时候那个时候家里穷没有玩具玩,某天发现老爸以前矿山里挖煤留下的工具箱里有几个崭新的轴承,如获至宝,赶紧找来锯子和木板制作了人生第一台车,大概效果如下

640?wx_fmt=jpeg

当时带着自制小车,在村口玩的时候,在小伙伴们里的风靡程度,不亚于现在一台跑车的回头率

640?wx_fmt=jpeg

就这样这辆小车陪伴我过了美好的童年,不过我永远不会忘记当年山坡翻车那个场景,如今我手背那块伤疤就是当年的记忆。最后一次见到那台车,应该是14年回老家还看到老屋角落躺着我的那辆木制轴承车,后面就没有了。。。。。放一张我想我当时玩的时候大概是这样子做个纪念吧!!!

640?wx_fmt=png

后来上大学选了自己最爱的电子专业,错过了飞思卡尔智能车比赛(两年一届),但是那个时候还没有Robomaster都没机会再玩到车,非常的遗憾。工作5,6年后偶然看到RT-thread举办的战车制作活动,就毫不犹豫就参加了,一路制作没有遇到太多艰辛,只完成了最基础的功能,有些遗憾,但是恰好30来临之际,记录一下做车的过程,算是给自己的生日礼物好了。



制作过程

一. 器材选择篇

在做这个车之前,有参考大量资料和车模,和队友们一起商量,希望做一台类似大疆步兵战车,所以我们在器件选型的时候大量参考了RobotMaster的器件参数,但是考虑大疆配件昂贵,我们自己综合了价格和性能进行选型。


主板

采用官方推荐潘多拉IoT Board

特点:
  • MCU:STM32L475

  • 主频:80M

  • 完善RT-Theard系统支持

  • 板载AP6181

  • 集成ST-Link非常方便调试

640?wx_fmt=jpeg

电机选择

  • 市面上各种直流电机种类特别多

  • 主要有刷电机,无刷之分,考虑价格和够用原则 选择无刷直流电机

  • 控制方式:霍尔编码,光电编码,电调,CAN 考虑控制简单和价格。

直接选用编码电机参数如下:


电机参数
类型AB双相增量式霍尔编码电机
供电电机24V,编码器5V
减速比1比19
额定转速504rpm
额定转矩14kg.cm
负载电流2.6A
峰值电流9.5A


电机驱动板选择

有了前面电机参数,我们选择驱动板就简单得多了,需要关注参数为驱动电压超过24V,驱动电流大于3A,功率要超过 3*24 = 72W.

淘宝找到如下一块满足要求

640?wx_fmt=jpeg

产品参数
供电电压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要求

640?wx_fmt=png

640?wx_fmt=jpeg

电压降压模块
选用LM2596S 24V转5V,最大电流3A,给loT-board和编码电机供电

640?wx_fmt=png

麦克纳姆轮

我们购买了100mm 具体参数如下:

型号100mm直径
重量1.52kg
负载能力40kg
厚度50mm
支撑轮抽直径3mm
支撑轮个数9个

640?wx_fmt=jpeg

底盘

由于我们器材都是自选,市面上底盘都无法安装,所以只要自己画个底盘,为了方便调试先做成亚克力,后面定型再做成铝合金

640?wx_fmt=png

组合图

640?wx_fmt=jpeg

二. 小车组装篇

电池改造

为了获取24V高电压,我们需要将3节7.4V航模电池正负极串联起来(操作过程千万注意同一个电池正负不要碰到)并安装一个拨动开关,改造之后的电池如下: 

640?wx_fmt=jpeg

供电连接

我们先把所需的供电需求列出来:

模块需要电压供电方式
IoT-Board+5V电池经过LM2596S降压到5V
电机驱动板电源接口+24V电池电压直出
电机驱动板5V引脚+3.3VloT-board的GPIO电平一致所以只能给3.3V否则pwm不能调速
编码电机5V+5V电池经过LM2596S降压到5V

接线如下:

640?wx_fmt=jpeg

主控板GPIO口引脚规划如下

IoT-Board GPIO引脚名字GPIO引脚序号PWM namePWM channel电机驱动板引脚
PD1259pwm4channel 1电机驱动板A 引脚EN1
PD1360

电机驱动板A 引脚IN1
PA429

电机驱动板A 引脚IN2
PB895pwm4channel 3电机驱动板A 引脚EN2
PB996

电机驱动板A 引脚IN3
PA867

电机驱动板A 引脚IN4
PA023pwm2channel 1电机驱动板B 引脚EN1
PA124

电机驱动板B 引脚IN1
PC217

电机驱动板B 引脚IN2
PB103pwm2channel 1电机驱动板B 引脚EN2
PB1148

电机驱动板B 引脚IN3
PC433

电机驱动板B 引脚IN4
PB1251

左前编码电机A
PB1352

左前编码电机B
PB1453

右前编码电机A
PB1554

右前编码电机B
PD1461

左后编码电机A
PD1562

左后编码电机B
PC663

右后编码电机A
PC764

右后编码电机B

整机安装效果图

正面:

640?wx_fmt=jpeg

底面:

640?wx_fmt=jpeg

三. 软件环境搭建篇

请将以下链接复制至外部浏览器打开

这部分直接参考官方给的环境搭建,非常完整,不在重复编写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测试来一张图

640?wx_fmt=png

五. 软件框架,控制协议、全向控制基础原理篇

代码目录: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()

控制基础原理

为了实现上面几个函数我们需要了解一些最基本的原理,首先我们控制小车前进,后退,向左,向右,左旋,右旋这六个个基本功能。

640?wx_fmt=jpeg

旋转原理

640?wx_fmt=png

前面只是几个基本动作的控制,如果我们要实现全相控制呢?那么我们需要学习一下基础原理知识通过这篇麦克纳姆轮控制原理文章的讲解我们知道,全向移动底盘是一个纯线性系统,而刚体运动又可以线性分解为三个分量。那么只需要计算出麦轮底盘在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遥控界面什么样子:

640?wx_fmt=png

Android端APP界面示意图

  • “A、D”部分为加减速按钮。

  • “B”部分为右自旋。

  • “C”部分为左自旋。

  • “E”部分为重力遥感开关,可切换到重力遥感模式

  • “I” 部分为遥控手柄切换。

  • “J” 部分为当前速度显示。

  • 通过上面图片我们知道我将0~360度作为全向运动的方向角,基础控制速度可调节,左右旋转独立

我们先建立如下一个xy 轴和0~360度的对应控制关系坐标系如下

640?wx_fmt=png

假如这个时候我们从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

总结

  1. 一开始想做战车类型,所以电机选型太大,驱动板功率很大,但是车上是亚克力,整个车容易“暴动”,需要装减震器。

  2. 前期太注重软件框架和wifi控制协议编写,导致没有时间做闭环控制调试,但是后面加入进去。

  3. 未能把摄像头云台加进去。

  4. 后面有想法用树莓派4或者jetson nano主板来做

  5. 最开心的是RT-Thread组织这个活动认识了一大帮做车的大牛,特别是指导老师吴博的知识渊博打开我的视野,另外一个吴鹏对技术的纯粹执着的态度令我敬佩。

  6. 当然也要非常感谢阿波基友和其他队友的支持


最后,恭喜鲲鹏战队全体成员,也感谢各位在文档整理上的付出!

640?wx_fmt=png

RT-Thread线上活动

1、RT-Thread开发者大会报名】2019年RT-Thread开发者大会将登入成都、上海、深圳与开发者们见面,还有RT-Thread在中高端智能领域的应用、一站式RTT开发工具、打造IoT极速开发模式等干货演讲,期待您的参与!本次大会也设立了codelab动手实验室活动,开发者可在现场体验RT-Thread给开发带来的便捷!

640?wx_fmt=png

立即报名

2、RT-Thread能力认证考前线上培训,将于11月25日全线截止报名,如果您有晋升、求职、寻找更好机会的需要,有深入学习和掌握RT-Thread的需求,请尽快垂询/报考!学生优惠价:168/人 

640?wx_fmt=png

学生专属报名通道

能力认证官网链接:https://www.rt-thread.org/page/rac.html(在外部浏览器打开)

640?wx_fmt=jpeg

立即报名(非学生)

#题外话# 喜欢RT-Thread不要忘了在GitHub上留下你的640?wx_fmt=pngSTAR640?wx_fmt=png哦,你的star对我们来说非常重要!链接地址:https://github.com/RT-Thread/rt-thread

你可以添加微信17775983565为好友,注明:公司+姓名,拉进 RT-Thread 官方微信交流群

640?wx_fmt=jpeg

RT-Thread

长按二维码,关注我们


640?wx_fmt=gif

点击“阅读原文”报名线下培训/开发者大会

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐