近年来,随着大家环保意识和生活水平的提高,不少人开始在庭院里种植了小花小草。但是由于没有定期管理,很多庭院变得杂草丛生,不仅不美观,还引来一群蚊子昆虫在这安居乐业。尤其天热的时候,室外温度三四十度,谁也不想拿着除草器在庭院除草。
因此,针对上述痛点,我们设计了一款简易低成本的割草机,有了它,我们就可以在家里吹着空调,吃着西瓜,看着庭院的杂草被处理的干干净净。
可采用 Wi-Fi 模组或者 Wi-Fi&BLE 模组
推荐使用输出电压 12V 左右的锂电池
模块上的降压芯片推荐使用 LM2596S
推荐使用 MG513P60_12V 电机
推荐使用东芝半导体公司直流电机驱动芯片 TB6612FNG
推荐使用 KV 值为 900 左右的无刷直流电机
推荐使用涂鸦出品的 GUC300 模组作为系统的芯片
割草机的硬件主要分四大部分,分别是
远程通信系统的核心就是涂鸦模组+MCU方案。
MCU 对接方案是对接涂鸦平台最常用的方案之一,主要用于带有 MCU 主控的产品实现智能化。涂鸦提供硬件模组、App 软件和云服务,开发者只需要关注产品本身功能的研发,配合使用涂鸦 MCU SDK 、公版 App 及公版面板,一站式完成产品智能化,开发高效便捷。
MCU SDK 是根据涂鸦开发平台定义的产品功能,自动生成的 MCU 代码。为了减少我们使用涂鸦 Wi-Fi 通用串口协议的对接成本,MCU SDK 已搭建通讯及协议解析架构。将 MCU SDK 添加至工程并配置相关信息后,就可以快速的完成 MCU 程序开发。
涂鸦 SDK 包对 MCU 的要求如下。资源不足的用户,可自行对接串口协议,SDK 包中的函数可以作为使用参考。
涂鸦模组的选择多种多样,比如 Wi-Fi 模组,Wi-Fi&Bluetooth 模组。
为了减少硬件开发步骤,也可以直接使用对应的 涂鸦三明治开发套件。
市面上的割草机一般分为汽油割草机和电动割草机两种形式。
其中汽油割草机功率大,马力足且持续工作时间长,但是噪声和震动特别大,而且对环境也有污染,不宜用在对环境要求较高的环境。
因此我们采用了锂电池供电的方式。锂电池供电时工作噪音小、运行平稳、环保节能、价格便宜,非常适合家庭庭园使用。
电池
割草机推荐使用输出电压 12V 左右的锂电,比如 3S 或 4S 航模电池。航模电池放电可达 15C 甚至 20C 以上,动力强劲。非航模锂电池组的价格会低一些,使用寿命长,安全性好,选型的时候,建议选择最大输出电流 5A 以上的电池。
降压模块
由于后面各个系统会用到各种不同的电压,因此,除了电池之外,我们还需要准备降压模块。
模块上的降压芯片推荐 LM2596S(查看 LM2596S 数据手册),它支持可调节输出的芯片,以便获取所需的电压值。
LM2596S 输入电压范围高达 40V,输出电压包含 1.2V 至 37V,在各种线路和负载条件下的最高电压容差为 ±4%,输出负载电流可达3A。该模块效率高,还带有热关断和电流限制保护功能。
减速电机就是在直流电机基础上,增加了一个高精密的减速器。一般直流电机的转速都是一分钟几千至万余转,增加减速器可以降低电机转速,增加电机扭矩,使得可控性更强。
选择减速电机的时候,电机的额定电压需要和我们选择的电池相匹配。常见的 12V 额定电压电机供电范围在 11~16V 之间,其中选择 12V 为最佳。
本方案推荐使用 MG513P60_12V 电机。该电机额定电压为 12V,减速比为 1:60,额定输出电流为 0.36A,减速后空载转速为 183rpm 左右。它的扭矩达 2kg*cm,最大负载能力高达 6KG,再搭配 65mm 直径轮胎,速度大约为 0.5m/s,完全满足割草机的运动需求。
此外,该电机还搭配了霍尔编码器,通过磁感应的原理,可以检测电机转速,从而实现速度检测和里程距离统计。
电机驱动采用东芝半导体公司直流电机驱动芯片 TB6612FNG(查看 TB6612FNG 数据手册)。它具有大电流 MOSFET-H 桥结构,VM 电源电压最大支持 15V,输出平均电流可达 1.2A,峰值电流允许 3.2A,导通电阻也只有 0.5 欧姆。它内置过热保护和低压检测电路,PWM 信号输入频率可允许 100KHz。
相比 L298N 的热耗性和外围二极管续流电路,TB6612FNG 无需外加散热片,外围电路简单,只需外接电源滤波电容就可以直接驱动电机,利于减小系统尺寸。
TB6612FNG 驱动芯片双通道电路输出,可同时驱动2个电机。因此,对于四个电机,我们只需要两片该芯片。
另外,当 STBY 处于高电平的时候,我们可利用芯片上的 IN1 和 IN2 这两个输入信号,选择正转、反转、短路制动和停机四种模式的其中一种模式,真值表如下:
AIN1 | AIN2 | 模式 |
---|---|---|
0 | 1 | 反转 |
1 | 0 | 正转 |
1 | 1 | 短路制动 |
0 | 0 | 停机 |
参考原理图如下
割草控制系统主要有割草高度调节和割草控制两个部分组成。
本方案推荐选用无刷直流电机。无刷电机没有电刷,因而不会产生有刷电机运转时产生的电火花,可以极大减少电火花对遥控无线电设备的干扰。同时,无刷电机转动时摩擦力大大减小,运行顺畅,噪音低,系统稳定性也很好,且寿命更长。
本方案推荐选择 KV 值 900 左右的电机。在选型的时候,需要注意下电机 KV 值和扭矩。它表示电压每升高1伏,转速增加的数值,对于无刷电机,这个值是个常量。同型号时,相同条件下,KV值越小,扭矩越大。
尺寸方面,选择接近 2212 这类小电机的尺寸即可。
电子调速器简称 ESC,它根据控制信号调节电机转速,是电机的驱动器。
本方案采用了一款航模爱好者比较常用的 20A 电调。开发者也可以选用别的型号,但是需要注意电调输出电流需要和无刷直流电机相适配。
这类电调的优点有很多,比如
舵机就是集成了直流电机,电机控制器和减速器等,并将其封装在一个便于安装的外壳里的伺服单元。它是一个能够利用简单的输入信号,比较精确地转动指定角度的电机系统。舵机安装了一个电位器(或其他角度传感器)检测输出轴转动角度,控制板根据电位器的信息能够比较准确的控制和保持输出轴的角度,形成闭环控制。
舵机有3根线,一般来说棕色线接 GND,红色线接电源正极,橙色线为信号线,或者黑色线接 GND,红色先接电源正极,白色线为信号线。
舵机电源输入电压一般 4~6V,通常推荐 5V,输入信号采用标准 PWM(脉冲宽度调制)信号,周期固定为 20ms(50Hz),脉宽可为 0.5ms 到 2.5ms 之间,对应舵机转角 0°~180°。
我们采用两个舵机,配上一些金属盘,组合成一个升降结构,通过同步控制两个舵机,实现高度调节。
定位导航系统主要由两个功能,分别是定位功能和定向功能。
定位,当然需要用 GNSS 全球导航卫星系统啦。目前为止,全球总共有四大 GNSS 系统,分别为美国GPS、俄罗斯 GLONASS、欧盟 GALILEO 和中国北斗卫星导航系统。国内外相关模组厂商也很多,模组品类也是应有尽有,单模多模,单频点多频点,十分丰富。
这里,我们采用涂鸦出品的 GUC300 模组。
GUC300 模组由一个高集成度的 GNSS 芯片 UFirebird-UC6226NIS 及其外围电路构成,内置 SAW 滤波器、LNA 以及26 MHz TCXO 等器件,保证了快速、精准的定位效果,并支持 GPS 和北斗双系统联合定位,向客户提供了高效、便捷的定位体验。
GUC300 主要特性如下
定位引擎特性:
支持 GPS 和 BDS
RF 采用宽带设计,输入信号以 1575 MHz 为中心频点
可接收和跟踪 1575.42 MHz GPS L1 信号
可接收和跟踪 1561.098 MHz BDS B1 信号
GUC300 默认配置为接收 GPS 和 BDS 信号,可通过命令配置为 GPS+BDS、GPS+GLONASS 或单系统工作,其它系统配置请联系涂鸦技术支持获取 GNSS 固件
GUC300 模组默认会每隔 1 秒输出 GNRMC GNGGA GPGSV 等语句,串口波特率为 9600,为了方便起见,我们只需要解析其中的 $GNGGA 即可,例如
$GNGGA,071520.00,3018.12971,N,12003.84423,E,1,20,1.47,60.5,M,,M,,\*61
该数据帧的结构及各字段释义如下:
$GNGGA,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,M,<10>,M,<11>,<12>*xx<CR><LF>
参考电路如下所示:
本方案推荐的型号是 QMC5883L(查看 QMC5883L 数据手册)。
电子罗盘由三维磁阻传感器、双轴倾角传感器和 MCU 构成。三维磁阻传感器用来测量地球磁场,倾角传感器是在磁力仪非水平状态时进行补偿;MCU 处理磁力仪和倾角传感器的信号以及数据输出和软铁、硬铁补偿。磁力仪是采用三个互相垂直的磁阻传感器,每个轴向上的传感器检测在该方向上的地磁场强度。向前的方向称为 X 方向的传感器检测地磁场在 X 方向的矢量值;向右或 Y 方向的传感器检测地磁场在 Y 方向的矢量值;向下或 Z 方向的传感器检测地磁场在 Z 方向的矢量值。每个方向的传感器的灵敏度都已根据在该方向上地磁场的分矢量调整到最佳点,并具有非常低的横轴灵敏度。传感器产生的模拟输出信号进行放大后送入 MCU 进行处理。
方向的计算,我们可以调用下面这个公式
direction.x=atan2((double)Mag_data.y,(double)Mag_data.x)*57.3+180;
其中 Mag_data.x 和 Mag_data.y 分别是 QMC5883 在 X 轴和 Y 轴输出的数据。atan2 为反正切计算,计算出来是弧度值,还需要乘 57.3,将弧度转换为角度。最后,再加 180 度,使角度值吻合大家的约定俗成,方便理解。
对于计算出来的结果,0 或 360 表示正南方向,90 表示正西方向,180 表示正北方向,270 表示正东方向。
以上材料准备齐全后,就可以开始我们的搭建之旅。
首先,我们先设计一个底盘,用于安装轮子,舵机等相关器件。底板结构件请单击 底板结构文件 下载。
安装完成后的效果如图。
此时需要再设计一个电路板,将硬件设计中的电机驱动,电源等设备集成进来,也需要留舵机,电机,电子罗盘等相关信号接口,减少飞线,使整机更加简洁。另外,还可以加一些功能进去,比如增加蜂鸣器用于报警,增加 LED 用于信号提示等。
参考电路如下所示:
最后,再将 GPS,主控板,电源模块等都整合到电路板上,这样整机基本完成。
登录 涂鸦 IoT 平台。
选择 创建产品。
在页面左下角,选择 找不到品类? 。
在 自定义创建 区域内,填写参数后单击 创建产品。
在 功能定义 页签中,添加自定义功能。功能点可以根据需求自行增减,功能点名称以及属性也可根据需求自行修改。
本方案中需要添加以下功能点:
在 设备面板 页签中,按照界面提示选择 App 面板。调试阶段推荐选择开发调试面板便于测试。
在 硬件开发 页签中,选择 涂鸦标准模组MCU SDK开发。
选择一款模组后,在下方 开发资料 区域单击 下载全部 下载所需资料,其中就包含了涂鸦 MCU SDK 文件。
至此,产品创建阶段已经基本完成,此时可以通过涂鸦智能 App 和虚拟设备体验 DP 数据的接收和发送。
在进行割草机应用开发之前,需要在 MCU 工程中移植涂鸦 MCU SDK。移植过程可参考文档:GD32F4系列单片机移植涂鸦MCU-SDK。
移植了MCU-SDK后,再搭配一块烧录并授权了通用对接固件的CBU模组,此时 MCU 就具备了连接涂鸦云和上报下发 DP 点的功能。
同时,本 Demo 工程代码还移植了 FreeRTOS 系统以便于开发。
完成准备工作后,正式开始割草机的应用功能开发。
本方案完整示例代码:tuya-iotos-embeded-mcu-demo-wifi-ble-samrt-lawn-mower
本方案最主要的器件就是电机舵机和电调,4个电机各控制了一个小轮,负责实现小车的运动;两个舵机组合在一起用来控制电调和电调上面的刀片的高低位置;电调则是控制刀片旋转,同时使转速可调控。
首先需为上述器件编写初始化和设置接口,方便后续调用。相关的接口都实现在 servo_motor.c 文件中。
#define MOTOR_PORT_1 GPIOA
#define MOTOR_PIN_1 GPIO_PIN_15
#define MOTOR_PORT_1_P GPIOD
#define MOTOR_PIN_1_P GPIO_PIN_0
#define MOTOR_PORT_1_N GPIOD
#define MOTOR_PIN_1_N GPIO_PIN_7
#define MOTOR_PORT_2 GPIOB
#define MOTOR_PIN_2 GPIO_PIN_9
#define MOTOR_PORT_2_P GPIOD
#define MOTOR_PIN_2_P GPIO_PIN_1
#define MOTOR_PORT_2_N GPIOD
#define MOTOR_PIN_2_N GPIO_PIN_2
#define MOTOR_PORT_3 GPIOB
#define MOTOR_PIN_3 GPIO_PIN_10
#define MOTOR_PORT_3_P GPIOD
#define MOTOR_PIN_3_P GPIO_PIN_3
#define MOTOR_PORT_3_N GPIOD
#define MOTOR_PIN_3_N GPIO_PIN_4
#define MOTOR_PORT_4 GPIOB
#define MOTOR_PIN_4 GPIO_PIN_11
#define MOTOR_PORT_4_P GPIOD
#define MOTOR_PIN_4_P GPIO_PIN_8
#define MOTOR_PORT_4_N GPIOD
#define MOTOR_PIN_4_N GPIO_PIN_9
#define MOTOR_CHANNEL_1 TIMER_CH_0
#define MOTOR_CHANNEL_2 TIMER_CH_1
#define MOTOR_CHANNEL_3 TIMER_CH_2
#define MOTOR_CHANNEL_4 TIMER_CH_3
#define SERVO_PORT_1 GPIOC
#define SERVO_PIN_1 GPIO_PIN_6
#define SERVO_PORT_2 GPIOC
#define SERVO_PIN_2 GPIO_PIN_7
#define BLADE_MOTOR_PORT GPIOC
#define BLADE_MOTOR_PIN GPIO_PIN_8
void servo_motor_init(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOD);
/*Configure PD1~8 Output motor Positive and Negative pin to drive wheels_1~4*/
gpio_mode_set(GPIOD, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, MOTOR_PIN_1_P | MOTOR_PIN_1_N | MOTOR_PIN_2_P | MOTOR_PIN_2_N | MOTOR_PIN_3_P | MOTOR_PIN_3_N | MOTOR_PIN_4_P | MOTOR_PIN_4_N);
gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,MOTOR_PIN_1_P | MOTOR_PIN_1_N | MOTOR_PIN_2_P | MOTOR_PIN_2_N | MOTOR_PIN_3_P | MOTOR_PIN_3_N | MOTOR_PIN_4_P | MOTOR_PIN_4_N);
gpio_bit_reset(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_1_N | MOTOR_PIN_2_P | MOTOR_PIN_2_N | MOTOR_PIN_3_P | MOTOR_PIN_3_N | MOTOR_PIN_4_P | MOTOR_PIN_4_N);
/*Configure PA15(TIMER1_CH0) Output PWM pulse to drive wheels_1*/
gpio_mode_set(MOTOR_PORT_1, GPIO_MODE_AF, GPIO_PUPD_NONE, MOTOR_PIN_1);
gpio_output_options_set(MOTOR_PORT_1, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,MOTOR_PIN_1);
gpio_af_set(MOTOR_PORT_1, GPIO_AF_1, MOTOR_PIN_1);
/*Configure PB9(TIMER1_CH1) Output PWM pulse to drive wheels_2*/
gpio_mode_set(MOTOR_PORT_2, GPIO_MODE_AF, GPIO_PUPD_NONE, MOTOR_PIN_2);
gpio_output_options_set(MOTOR_PORT_2, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,MOTOR_PIN_2);
gpio_af_set(MOTOR_PORT_2, GPIO_AF_1, MOTOR_PIN_2);
/*Configure PB10(TIMER1_CH2) Output PWM pulse to drive wheels_3*/
gpio_mode_set(MOTOR_PORT_3, GPIO_MODE_AF, GPIO_PUPD_NONE, MOTOR_PIN_3);
gpio_output_options_set(MOTOR_PORT_3, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,MOTOR_PIN_3);
gpio_af_set(MOTOR_PORT_3, GPIO_AF_1, MOTOR_PIN_3);
/*Configure PB11(TIMER1_CH3) Output PWM pulse to drive wheels_4*/
gpio_mode_set(MOTOR_PORT_4, GPIO_MODE_AF, GPIO_PUPD_NONE, MOTOR_PIN_4);
gpio_output_options_set(MOTOR_PORT_4, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,MOTOR_PIN_4);
gpio_af_set(MOTOR_PORT_4, GPIO_AF_1, MOTOR_PIN_4);
/*Configure PC6(TIMER2_CH0) Output PWM pulse to drive servo no.1*/
gpio_mode_set(SERVO_PORT_1, GPIO_MODE_AF, GPIO_PUPD_NONE, SERVO_PIN_1);
gpio_output_options_set(SERVO_PORT_1, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,SERVO_PIN_1);
gpio_af_set(SERVO_PORT_1, GPIO_AF_2, SERVO_PIN_1);
/*Configure PC7(TIMER2_CH1) Output PWM pulse to drive servo no.2*/
gpio_mode_set(SERVO_PORT_2, GPIO_MODE_AF, GPIO_PUPD_NONE, SERVO_PIN_2);
gpio_output_options_set(SERVO_PORT_2, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,SERVO_PIN_2);
gpio_af_set(SERVO_PORT_2, GPIO_AF_2, SERVO_PIN_2);
/*Configure PC8(TIMER2_CH2) Output PWM pulse to drive blade motor*/
gpio_mode_set(BLADE_MOTOR_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, BLADE_MOTOR_PIN);
gpio_output_options_set(BLADE_MOTOR_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,BLADE_MOTOR_PIN);
gpio_af_set(BLADE_MOTOR_PORT, GPIO_AF_2, BLADE_MOTOR_PIN);
timer_config();
}
void timer_config(void)
{
uint16_t i = 0;
/* TIMER1 configuration: generate PWM signals with different duty cycles:
TIMER1CLK = SystemCoreClock / 120 = 1MHz */
timer_oc_parameter_struct timer_ocintpara;
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER1);
rcu_periph_clock_enable(RCU_TIMER2);
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
timer_struct_para_init(&timer_initpara);
timer_deinit(TIMER1);
timer_deinit(TIMER2);
/* TIMER1 configuration */
timer_initpara.prescaler = 119;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 500; /* 500*(1/1MHZ) = 500us */
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER1,&timer_initpara);
/* TIMER2 configuration */
timer_initpara.prescaler = 119;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 20000; /* 20000*(1/1MHZ) = 20ms */
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER2,&timer_initpara);
timer_channel_output_struct_para_init(&timer_ocintpara);
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocintpara.outputnstate = TIMER_CCXN_DISABLE;
timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_LOW;
timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW;
for(i = 0; i < 4; i++) {
timer_channel_output_config(TIMER1,i,&timer_ocintpara);
timer_channel_output_pulse_value_config(TIMER1,i,0);
timer_channel_output_mode_config(TIMER1,i,TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIMER1,i,TIMER_OC_SHADOW_DISABLE);
}
for(i = 0; i < 3; i++) {
timer_channel_output_config(TIMER2,i,&timer_ocintpara);
timer_channel_output_pulse_value_config(TIMER2,i,0);
timer_channel_output_mode_config(TIMER2,i,TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIMER2,i,TIMER_OC_SHADOW_DISABLE);
}
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER1);
timer_auto_reload_shadow_enable(TIMER2);
/* TIMER enable */
timer_enable(TIMER1);
timer_enable(TIMER2);
}
void car_moving(MOVING_DIRECTION direction, uint16_t speed_value)
{
uint8_t i;
switch(direction) {
case forward:
gpio_bit_set(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_2_P | MOTOR_PIN_3_P | MOTOR_PIN_4_P);
gpio_bit_reset(GPIOD, MOTOR_PIN_1_N | MOTOR_PIN_2_N | MOTOR_PIN_3_N | MOTOR_PIN_4_N);
for(i = 0; i < 4; i++) {
timer_channel_output_pulse_value_config(TIMER1,i,speed_value);
}
break;
case right:
gpio_bit_set(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_2_P | MOTOR_PIN_3_P | MOTOR_PIN_4_P);
gpio_bit_reset(GPIOD, MOTOR_PIN_1_N | MOTOR_PIN_2_N | MOTOR_PIN_3_N | MOTOR_PIN_4_N);
//Since the two sets of wheels on the right are always faster than the left in the actual commissioning process, 60 is added to compensate
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_1,speed_value + 60);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_3,speed_value + 60);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_2,speed_value);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_4,speed_value);
break;
case left:
gpio_bit_set(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_2_P | MOTOR_PIN_3_P | MOTOR_PIN_4_P);
gpio_bit_reset(GPIOD, MOTOR_PIN_1_N | MOTOR_PIN_2_N | MOTOR_PIN_3_N | MOTOR_PIN_4_N);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_1,speed_value);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_3,speed_value);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_2,speed_value + 50);
timer_channel_output_pulse_value_config(TIMER1,MOTOR_CHANNEL_4,speed_value + 50);
break;
case backward:
gpio_bit_reset(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_2_P | MOTOR_PIN_3_P | MOTOR_PIN_4_P);
gpio_bit_set(GPIOD, MOTOR_PIN_1_N | MOTOR_PIN_2_N | MOTOR_PIN_3_N | MOTOR_PIN_4_N);
for(i = 0; i < 4; i++) {
timer_channel_output_pulse_value_config(TIMER1,i,speed_value);
}
break;
case stop:
gpio_bit_reset(GPIOD, MOTOR_PIN_1_P | MOTOR_PIN_2_P | MOTOR_PIN_3_P | MOTOR_PIN_4_P);
gpio_bit_reset(GPIOD, MOTOR_PIN_1_N | MOTOR_PIN_2_N | MOTOR_PIN_3_N | MOTOR_PIN_4_N);
for(i = 0; i < 4; i++) {
timer_channel_output_pulse_value_config(TIMER1,i,speed_value);
}
break;
default:
break;
}
}
由于本方案的四个轮子都是由独立电机控制的,在给定相同占空比的 PWM 输出时,电机与电机之间的误差以及其他因素都会导致小车并不能按照预想的那样驶出一条直线,因此我们有必要引入 PID 算法来动态调整各个轮子的实际转速。
理想中的直线行驶,即代表四个轮子转动了同样的距离,同时因为这四个轮子的直径都是相同的,也就相当于要求转速是一致的。而电机的转速又直接影响了电机的编码器输出脉冲数,因此一个简单的 PID 控制思路就产生了:实时采集单位时间下电机编码器输出的脉冲数作为PID算法的输入,占空比增量作为被控项,通过不断的反馈调整最终使四个电机的单位时间脉冲数都收敛至同一期望值。
void movement_system_init(void)
{
rcu_periph_clock_enable(RCU_SYSCFG);
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_0);
nvic_irq_enable(EXTI0_IRQn, 2U, 0U);
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN0);
exti_init(EXTI_0, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_flag_clear(EXTI_0);
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_1);
nvic_irq_enable(EXTI1_IRQn, 2U, 0U);
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN1);
exti_init(EXTI_1, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_flag_clear(EXTI_1);
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_2);
nvic_irq_enable(EXTI2_IRQn, 2U, 0U);
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN2);
exti_init(EXTI_2, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_flag_clear(EXTI_2);
gpio_mode_set(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_3);
nvic_irq_enable(EXTI3_IRQn, 2U, 0U);
syscfg_exti_line_config(EXTI_SOURCE_GPIOC, EXTI_SOURCE_PIN3);
exti_init(EXTI_3, EXTI_INTERRUPT, EXTI_TRIG_RISING);
exti_interrupt_flag_clear(EXTI_3);
}
void EXTI0_IRQHandler(void)
{
if(RESET != exti_interrupt_flag_get(EXTI_0)){
move_state.encoder_pulse[0]++;
}
exti_interrupt_flag_clear(EXTI_0);
}
void EXTI1_IRQHandler(void)
{
if(RESET != exti_interrupt_flag_get(EXTI_1)){
move_state.encoder_pulse[1]++;
}
exti_interrupt_flag_clear(EXTI_1);
}
void EXTI2_IRQHandler(void)
{
if(RESET != exti_interrupt_flag_get(EXTI_2)){
move_state.encoder_pulse[2]++;
}
exti_interrupt_flag_clear(EXTI_2);
}
void EXTI3_IRQHandler(void)
{
if(RESET != exti_interrupt_flag_get(EXTI_3)){
move_state.encoder_pulse[3]++;
}
exti_interrupt_flag_clear(EXTI_3);
}
int e[4]={0},e1[4]={0},e2[4]={0}; //pid 偏差
float uk[4]={0.0},uk1[4]={0.0},duk[4]={0.0}; //pid输出
float Kp=0.8,Ki=0.3,Kd=0.9; //pid控制系数
int out[4] = {0};
static void forward_correction(uint32_t *pulse_num, uint32_t *pulse_num_old, uint32_t standard_increment)
{
uint8_t i = 0;
for(i = 0;i < 4;i++) {
e[i] = standard_increment - (pulse_num[i] - pulse_num_old[i]);
duk[i] = (Kp*(e[i]-e1[i])+Ki*e[i])/100;
uk[i] = uk1[i] + duk[i];
out[i] = (int)uk[i];
uk1[i] = uk[i];
e2[i] = e1[i];
e1[i] = e[i];
single_motor_speed_set(i, (uint16_t)(200+(out[i]*5)));
}
return;
}
void movement_logic_handle(void)
{
MOVE_STATE_T *p = NULL;
p = &move_state;
uint8_t i = 0;
MOVING_DIRECTION turning_state;
switch(p->todo_type) {
......
case on_the_move:
if(forward_sampling_time_flag == 20) { //20*20ms = 400ms
for(i = 0;i < 4;i++) {
pulse_test[i] = p->encoder_pulse[i]-p->encoder_pulse_old[i];
}
forward_correction(p->encoder_pulse,p->encoder_pulse_old,390);
for(i = 0;i < 4;i++) {
p->encoder_pulse_old[i] = p->encoder_pulse[i];
}
forward_sampling_time_flag = 0;
}
forward_sampling_time_flag++;
todo_judge();
break;
......
default:
break;
}
}
以弓字模式为例,当小车直线行驶到规定面积长度时就需要进行直角转弯动作。在上一节的例程中出现的todo_judge函数就是用于转弯判断的,判断依据也是根据电机编码器的脉冲数来转换的,将长度换算成编码器脉冲数,当脉冲数达到规定长度转换出的脉冲数时就开始转弯动作。同时,小车也需要区分当前是在长边转弯还是在宽边转弯,是往左转还是往右转。todo_judge()函数中有一个swich判断,case分支就是代表小车当前是将要在长边左右转还是在宽边左右转。case内的代码主要是判断当前直线行驶距离是否达到转弯条件,若达到则改变小车的直行转弯状态(change_to_do(turning);),然后改变下一个行驶阶段进入该函数时的case分支(p->run_step_flag = width_right;),最后通过宽度是否达到设定值来判断整个弓字行驶是否结束:
static void todo_judge(void)
{
MOVE_STATE_T *p = NULL;
p = &move_state;
switch(p->run_step_flag) {
case length_right:
if(pulse_to_distance(p->encoder_pulse[0]) >= (p->range_length_mm - 10)) {
if((p->current_angle + 900) > 3600) {
p->target_angle = p->current_angle + 900 - 3600;
}else{
p->target_angle = p->current_angle + 900;
}
change_to_do(turning);
p->run_step_flag = width_right;
}
break;
case width_right:
if(pulse_to_distance(p->encoder_pulse[0]) >= 100) {
if((p->current_angle + 900) > 3600) {
p->target_angle = p->current_angle + 900 - 3600;
}else{
p->target_angle = p->current_angle + 900;
}
change_to_do(turning);
p->run_step_flag = length_left;
width_remain_mm -= 100;
}
break;
case length_left:
if(pulse_to_distance(p->encoder_pulse[0]) >= (p->range_length_mm - 10)) {
if(p->current_angle < 900) {
p->target_angle = 3600 - (900 - p->current_angle);
}else{
p->target_angle = p->current_angle - 900;
}
change_to_do(turning);
p->run_step_flag = width_left;
}
break;
case width_left:
if(pulse_to_distance(p->encoder_pulse[0]) >= 100) {
if(p->current_angle < 900) {
p->target_angle = 3600 - (900 - p->current_angle);
}else{
p->target_angle = p->current_angle - 900;
}
change_to_do(turning);
p->run_step_flag = length_right;
width_remain_mm -= 100;
}
break;
default:
break;
}
if(width_remain_mm <= 0) {
change_to_do(to_stop);
move_state.bow_mode_switch = pdFALSE;
}
}
以上例程中的 p->target_angle 为目标方位角,p->current_angle 为当前方位角,用于控制小车的转弯角度。目标方位角是由即将转弯时的小车当前方位角加减90度得来的,而当前方位角则是由地磁传感器数据计算得出。传感器型号为QMC5883L,是IIC驱动的,具体驱动代码都在 QMC5883L.c 文件里,此处不再赘述。主要用到的传感器接口是 QMC5883L_GetAngle(),获取当前方位角:
void move_control_task(void *pvParameters)
{
float_xyz Mag_angle;
uint8_t test_flag = 50;
vTaskDelay(200);
QMC_Init();
QMC5883L_GetAngle(&Mag_angle);
move_state.current_angle = (int16_t)Mag_angle.x;
vTaskDelay(500);
while(1) {
if(test_flag){
vTaskDelay(20);
test_flag--;
}else if(test_flag == 0) {
vTaskDelay(20);
movement_logic_handle();
}
QMC5883L_GetAngle(&Mag_angle);
move_state.current_angle = (int16_t)Mag_angle.x;
}
}
得到了方位角后,就可以用在转弯过程中的判断了。此处实现了一个angle_correction()接口,用于得出当前小车是需要继续左转或者右转还是已经转到目标角度可以直行了。和直行一样,angle_correction() 接口在运动逻辑轮询处理函数中的 case 分支 turning 中调用:
static MOVING_DIRECTION angle_correction(void)
{
int16_t target,current = 0;
target = move_state.target_angle;
current = move_state.current_angle;
if(target < current) {
if(current - target <= 20) {
return forward;
}
if(current - target <= 1800) {
return left;
}else{
return right;
}
}else if(target > current) {
if(target - current <= 20) {
return forward;
}
if(target - current <= 1800) {
return right;
}else{
return left;
}
}else if(current == target) {
return forward;
}
}
void movement_logic_handle(void)
{
MOVE_STATE_T *p = NULL;
p = &move_state;
uint8_t i = 0;
MOVING_DIRECTION turning_state;
switch(p->todo_type) {
......
case on_the_move:
if(forward_sampling_time_flag == 20) { //20*20ms = 400ms
for(i = 0;i < 4;i++) {
pulse_test[i] = p->encoder_pulse[i]-p->encoder_pulse_old[i];
}
forward_correction(p->encoder_pulse,p->encoder_pulse_old,390);
for(i = 0;i < 4;i++) {
p->encoder_pulse_old[i] = p->encoder_pulse[i];
}
forward_sampling_time_flag = 0;
}
forward_sampling_time_flag++;
todo_judge();
break;
case turning:
turning_state = angle_correction();
if(turning_state == right) {
car_full_turn(right,150);
turning_sampling_time_flag = 0;
}else if(turning_state == left) {
car_full_turn(left,150);
turning_sampling_time_flag = 0;
}else if(turning_state == forward) {
car_moving(stop,0);
turning_sampling_time_flag++;
}
if(turning_sampling_time_flag >= 25) {
p->todo_type = on_the_move;
car_moving(forward,200);
forward_sampling_time_flag = 0;
turning_sampling_time_flag = 0;
for(i = 0;i < 4;i++) {
p->encoder_pulse[i] = 0;
p->encoder_pulse_old[i] = 0;
}
}
break;
......
default:
break;
}
}
刀片位置是由两个舵机一起控制的,在位置上下变动的同时还需要保持刀片的水平姿态不变,这就需要两个舵机在相反的方向变动相同的角度。这里直接封装出一个刀片位置设置接口,入参为位置枚举值:
void blade_position_set(BLADE_POSITION value)
{
switch(value) {
case low:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0,3100);
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_1,3000);
break;
case medium:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0,2800);
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_1,2700);
break;
case high:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_0,2600);
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_1,2500);
break;
default:
break;
}
}
刀片速度则是由电调控制的,封装接口为 blade_speed_set();
void blade_speed_set(BLADE_SPEED speed)
{
switch(speed) {
case init:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,1500);
break;
case off:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,0);
break;
case low_speed:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,1800);
break;
case medium_speed:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,1900);
break;
case high_speed:
timer_channel_output_pulse_value_config(TIMER2,TIMER_CH_2,2000);
break;
default:
break;
}
}
本方案采用的涂鸦 GUC300 模组是通过串口通信的方式给 MCU 发送 GNSS 数据的,这里我们使用 MCU 的 usart1 来接收数据,usart0 已经被作为与 Wi-Fi 模组通信的串口。
void uart_init(void)
{
/* USART interrupt configuration */
nvic_irq_enable(USART0_IRQn, 0, 0);
nvic_irq_enable(USART1_IRQn, 1, 1);
/* enable USART clock */
rcu_periph_clock_enable(RCU_USART0);
/* connect port to USART0_Tx */
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_9);
/* connect port to USART0_Rx */
gpio_af_set(GPIOA, GPIO_AF_7, GPIO_PIN_10);
/* configure USART Tx as alternate function push-pull */
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_9);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_9);
/* configure USART Rx as alternate function push-pull */
gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_10);
gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_10);
/* USART configure */
usart_deinit(USART0);
usart_baudrate_set(USART0,115200U);
usart_receive_config(USART0, USART_RECEIVE_ENABLE);
usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
usart_enable(USART0);
/* enable USART clock */
rcu_periph_clock_enable(RCU_USART1);
/* connect port to USART0_Tx */
gpio_af_set(GPIOD, GPIO_AF_7, GPIO_PIN_5);
/* connect port to USART0_Rx */
gpio_af_set(GPIOD, GPIO_AF_7, GPIO_PIN_6);
/* configure USART Tx as alternate function push-pull */
gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_5);
gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_5);
/* configure USART Rx as alternate function push-pull */
gpio_mode_set(GPIOD, GPIO_MODE_AF, GPIO_PUPD_PULLUP,GPIO_PIN_6);
gpio_output_options_set(GPIOD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ,GPIO_PIN_6);
/* USART configure */
usart_deinit(USART1);
usart_baudrate_set(USART1,9600U);
usart_receive_config(USART1, USART_RECEIVE_ENABLE);
usart_transmit_config(USART1, USART_TRANSMIT_ENABLE);
usart_enable(USART1);
/* enable USART0 receive interrupt */
usart_interrupt_enable(USART0, USART_INT_RBNE);
/* enable USART1 receive interrupt */
usart_interrupt_enable(USART1, USART_INT_RBNE);
}
然后是串口接收中断服务函数。涂鸦GPS模组会每隔1秒输出 GNRMC GNGGA GPGSV GPGGA 等语句,我们只需要解析其中的 $GPGGA 即可。
$GPGGA 语句包括17个字段:语句标识头,世界时间,纬度,纬度半球,经度,经度半球,定位质量指示,使用卫星数量,HDOP-水平精度因子,椭球高,高度单位,大地水准面高度异常差值,高度单位,差分GPS数据期限,差分参考基站标号,校验和结束标记(用回车符CR和换行符LF),分别用14个逗号进行分隔。
格式示例:$GPGGA,014434.70,3817.13334637,N,12139.72994196,E,4,07,1.5,6.571,M,8.942,M,0.7,0016*79
按照上述格式对串口数据进行处理:
#define USART_FH_len 6 //帧头长度
char USART_FH[USART_FH_len]={'$','G','N','G','G','A'}; //帧头
#define USART_FT_len 2 //帧尾长度
uint8_t USART_FT[USART_FT_len]={0X0D,0X0A}; //帧尾
uint8_t data_buf[128] = {0};
uint8_t rx_buf[128] = {0};
uint8_t buff_len = 0;
void USART1_IRQHandler(void)
{
if((RESET != usart_interrupt_flag_get(USART1, USART_INT_FLAG_RBNE)) &&
(RESET != usart_flag_get(USART1, USART_FLAG_RBNE))){
rx_buf[buff_len++] = (uint8_t)usart_data_receive(USART1);
if(rx_buf[0] != USART_FH[0]) {
rx_buf[0] = 0;
buff_len = 0;
}
if(buff_len >= USART_FH_len) {
if(strncmp((char*)rx_buf,USART_FH,USART_FH_len) == 0) {
if(strncmp(&rx_buf[buff_len-2],(char*)USART_FT,USART_FT_len) == 0) {
memcpy(data_buf,rx_buf,buff_len);
memset(rx_buf,0,buff_len);
buff_len = 0;
}
}else {
memset(rx_buf,0,USART_FH_len);
buff_len = 0;
}
}
}
}
void gps_data_task(void *pvParameters)
{
MAP_POINT_t *p;
p = &map_point[point_1];
uint8_t data_len = 0;
while(1) {
vTaskDelay(100);
data_len = strlen((char*)data_buf);
if(data_len != 0){
gps_data_pick(point_1, data_buf, data_len);
memset(data_buf,0,data_len);
}
}
}
到这里,智能割草机 Demo 就完成了。在这款智能割草机的基础上还有很多功能可以深入开发,使体验更加人性化,智能化。同时您可以基于涂鸦 IoT 开发平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。