开发一款智能割草机原型

更新时间Invalid date

概况

近年来,随着大家环保意识和生活水平的提高,不少人开始在庭院里种植了小花小草。但是由于没有定期管理,很多庭院变得杂草丛生,不仅不美观,还引来一群蚊子昆虫在这安居乐业。尤其天热的时候,室外温度三四十度,谁也不想拿着除草器在庭院除草。

因此,针对上述痛点,我们设计了一款简易低成本的割草机,有了它,我们就可以在家里吹着空调,吃着西瓜,看着庭院的杂草被处理的干干净净。

功能设计

  • 锂电池供电,噪声小,环保节能,支持低电量报警
  • 支持路程计量
  • 可根据割草需求,调节割草高度
  • 具备自主路径规划的能力,我们只需要划出割草范围,割草机就会自动完成割草工作
  • 支持手机 App 操控割草机

物料清单

硬件 (10)软件 (1)
  • 涂鸦云模组

    数量:1

    可采用 Wi-Fi 模组或者 Wi-Fi&BLE 模组

  • 电池

    数量:1

    推荐使用输出电压 12V 左右的锂电池

  • 降压模块

    数量:1

    模块上的降压芯片推荐使用 LM2596S

  • 减速电机

    数量:1

    推荐使用 MG513P60_12V 电机

  • 电机驱动芯片

    数量:1

    推荐使用东芝半导体公司直流电机驱动芯片 TB6612FNG

  • 割草控制系统电机

    数量:1

    推荐使用 KV 值为 900 左右的无刷直流电机

  • 电子调速器

    数量:1

  • 舵机

    数量:1

  • GNSS 全球导航卫星系统

    数量:1

    推荐使用涂鸦出品的 GUC300 模组作为系统的芯片

  • 电子罗盘

    数量:1

步骤

  • 第 1 步:硬件设计方案

    割草机的硬件主要分四大部分,分别是

    • 远程通讯系统:采用涂鸦无线联网模组,使用 App 即可远程控制割草机,时刻查看割草机动态。
    • 动力系统:采用 12V 带多重保护功能的锂电池组,配减速电机和橡胶轮胎,为小车提供动力支持。
    • 割草控制系统:使用两个舵机实现割草高度调节,用电调控制无刷直流电机进行割草。
    • 定位导航系统:采用 GNSS 模组和数字地磁传感器,实现割草机定位和导航的功能。

    远程通讯系统

    远程通信系统的核心就是涂鸦模组+MCU方案。

    MCU 对接方案是对接涂鸦平台最常用的方案之一,主要用于带有 MCU 主控的产品实现智能化。涂鸦提供硬件模组、App 软件和云服务,开发者只需要关注产品本身功能的研发,配合使用涂鸦 MCU SDK 、公版 App 及公版面板,一站式完成产品智能化,开发高效便捷。

    MCU SDK 是根据涂鸦开发平台定义的产品功能,自动生成的 MCU 代码。为了减少我们使用涂鸦 Wi-Fi 通用串口协议的对接成本,MCU SDK 已搭建通讯及协议解析架构。将 MCU SDK 添加至工程并配置相关信息后,就可以快速的完成 MCU 程序开发。

    硬件资源要求

    涂鸦 SDK 包对 MCU 的要求如下。资源不足的用户,可自行对接串口协议,SDK 包中的函数可以作为使用参考。

    • 内存:4KB
    • RAM:与 DP 点数据长度有关,约为 100B (如需添加 OTA 功能,则需大于 260B)
    • 函数嵌套级数:9 级

    涂鸦模组选型

    涂鸦模组的选择多种多样,比如 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 停机

    参考原理图如下

    drv_sch

    割草控制系统

    割草控制系统主要有割草高度调节和割草控制两个部分组成。

    无刷直流电机

    本方案推荐选用无刷直流电机。无刷电机没有电刷,因而不会产生有刷电机运转时产生的电火花,可以极大减少电火花对遥控无线电设备的干扰。同时,无刷电机转动时摩擦力大大减小,运行顺畅,噪音低,系统稳定性也很好,且寿命更长。

    motor

    本方案推荐选择 KV 值 900 左右的电机。在选型的时候,需要注意下电机 KV 值和扭矩。它表示电压每升高1伏,转速增加的数值,对于无刷电机,这个值是个常量。同型号时,相同条件下,KV值越小,扭矩越大。

    尺寸方面,选择接近 2212 这类小电机的尺寸即可。

    电子调速器

    电子调速器简称 ESC,它根据控制信号调节电机转速,是电机的驱动器。

    本方案采用了一款航模爱好者比较常用的 20A 电调。开发者也可以选用别的型号,但是需要注意电调输出电流需要和无刷直流电机相适配。

    这类电调的优点有很多,比如

    • 油门响应速度极快;
    • 具有非常出色的电机兼容性;
    • 高度智能化,自适应能力强,仅保留进角设定项 (高进角/中进角),使用极为简单;
    • 信号线为双绞线,有效降低信号在铜线内传输所产生的串扰,令工作更加稳定;
    • 使用 MOSFET 专用驱动芯片,性能和稳定性远超用分立元件搭建的驱动电路;
    • 超低内阻 MOSFET,耐流能力强大。

    舵机

    舵机就是集成了直流电机,电机控制器和减速器等,并将其封装在一个便于安装的外壳里的伺服单元。它是一个能够利用简单的输入信号,比较精确地转动指定角度的电机系统。舵机安装了一个电位器(或其他角度传感器)检测输出轴转动角度,控制板根据电位器的信息能够比较准确的控制和保持输出轴的角度,形成闭环控制。

    舵机有3根线,一般来说棕色线接 GND,红色线接电源正极,橙色线为信号线,或者黑色线接 GND,红色先接电源正极,白色线为信号线。

    舵机电源输入电压一般 4~6V,通常推荐 5V,输入信号采用标准 PWM(脉冲宽度调制)信号,周期固定为 20ms(50Hz),脉宽可为 0.5ms 到 2.5ms 之间,对应舵机转角 0°~180°。

    我们采用两个舵机,配上一些金属盘,组合成一个升降结构,通过同步控制两个舵机,实现高度调节。

    四.定位导航系统

    定位导航系统主要由两个功能,分别是定位功能和定向功能。

    GNSS

    定位,当然需要用 GNSS 全球导航卫星系统啦。目前为止,全球总共有四大 GNSS 系统,分别为美国GPS、俄罗斯 GLONASS、欧盟 GALILEO 和中国北斗卫星导航系统。国内外相关模组厂商也很多,模组品类也是应有尽有,单模多模,单频点多频点,十分丰富。

    这里,我们采用涂鸦出品的 GUC300 模组

    GUC300 模组由一个高集成度的 GNSS 芯片 UFirebird-UC6226NIS 及其外围电路构成,内置 SAW 滤波器、LNA 以及26 MHz TCXO 等器件,保证了快速、精准的定位效果,并支持 GPS 和北斗双系统联合定位,向客户提供了高效、便捷的定位体验。

    GUC300 主要特性如下

    • 定位引擎特性:

      • 64 通道同时跟踪
      • 热启动时间快于 1.2 秒
      • 冷启动灵敏度 -145 dBm,跟踪灵敏度 -158 dBm
      • 数据更新速率最高达 10 Hz
    • 支持 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>

    • $GNGGA:起始引导符及语句格式说明(本句为GPS定位数据);
    • <1>:UTC时间,格式为hhmmss.sss;
    • <2>:纬度,格式为ddmm.mmmm(第一位是零也将传送);
    • <3>:纬度半球,N或S (北纬或南纬)
    • <4>:经度,格式为dddmm.mmmm(第一位零也将传送);
    • <5>:经度半球,E或W(东经或西经)
    • <6>:GPS状态, 0 为初始化, 1 为单点定位, 2 为码差分, 3为 无效 PPS, 4 为固定解, 5 为浮点解, 6为正在估算 7 为人工输入固定值, 8 为模拟模式, 9 为 WAAS差分;
    • <7>:使用卫星数量,从00到12(第一个零也将传送)
    • <8>:HDOP-水平精度因子,0.5 到 99.9,一般认为 HDOP 越小,质量越好。
    • <9>:海拔高度,-9999.9 到 9999.9米
    • M:指单位米
    • <10>:大地水准面高度异常差值,-9999.9 到 9999.9 米
    • M:指单位米
    • <11>:差分 GPS 数据期限(RTCM SC-104),最后设立 RTCM 传送的秒数量,如不是差分定位则为空
    • <12>:差分参考基站标号,从 0000 到 1023(首位 0 也将传送)。
    • xx:从 $ 开始到 * 之间的所有 ASCII 码的异或校验
    • <CR> 回车符,结束标记
    • <LF> 换行符,结束标记

    参考电路如下所示:

    GUC300应用原理图

    GUC300应用PCB

    电子罗盘

    本方案推荐的型号是 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 用于信号提示等。

    sch

    参考电路如下所示:

    电路板原理图

    电路板PCB

    最后,再将 GPS,主控板,电源模块等都整合到电路板上,这样整机基本完成。

  • 第 2 步:创建产品

    1. 登录 涂鸦 IoT 平台

    2. 选择 创建产品

    3. 在页面左下角,选择 找不到品类?

    4. 自定义创建 区域内,填写参数后单击 创建产品

      • 产品名称、产品描述、产品型号:用户自定义填写。
      • 通讯协议:选择 Wi-Fi-蓝牙

    5. 功能定义 页签中,添加自定义功能。功能点可以根据需求自行增减,功能点名称以及属性也可根据需求自行修改。

      本方案中需要添加以下功能点:

      • 标准功能:方向控制。
      • 自定义功能:刀片位置、刀片转速、范围长度设置、范围宽度设置、弓字除草。

    6. 设备面板 页签中,按照界面提示选择 App 面板。调试阶段推荐选择开发调试面板便于测试。

    7. 硬件开发 页签中,选择 涂鸦标准模组MCU SDK开发

    8. 选择一款模组后,在下方 开发资料 区域单击 下载全部 下载所需资料,其中就包含了涂鸦 MCU SDK 文件。

      至此,产品创建阶段已经基本完成,此时可以通过涂鸦智能 App 和虚拟设备体验 DP 数据的接收和发送。

  • 第 3 步:MCU 固件开发

    在进行割草机应用开发之前,需要在 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 文件中。

    • 输出I/O口宏定义,方便后续修改:
    #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
    
    • 初始化接口,调用后即打开GPIO时钟,同时设置输出IO的普通输出模式和PWM模式,然后调用timer_config去设置PWM的具体参数:
    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);
    }
    
    • 完成初始化接口后,需要对输出特定电机或舵机的 PWM 进行占空比调节,将调节占空比这个操作封装成通用的接口。以四轮的电机为例,car_moving 这一接口实现的操作就是根据传入的方向和速度参数去控制电机的正负极电平和占空比,继而控制车轮转动的方向和转速:
    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;
    		}
    }
    

    PID 控制实现直线行驶

    由于本方案的四个轮子都是由独立电机控制的,在给定相同占空比的 PWM 输出时,电机与电机之间的误差以及其他因素都会导致小车并不能按照预想的那样驶出一条直线,因此我们有必要引入 PID 算法来动态调整各个轮子的实际转速。
    理想中的直线行驶,即代表四个轮子转动了同样的距离,同时因为这四个轮子的直径都是相同的,也就相当于要求转速是一致的。而电机的转速又直接影响了电机的编码器输出脉冲数,因此一个简单的 PID 控制思路就产生了:实时采集单位时间下电机编码器输出的脉冲数作为PID算法的输入,占空比增量作为被控项,通过不断的反馈调整最终使四个电机的单位时间脉冲数都收敛至同一期望值。

    • 首先需要采集四路电机编码器单位时间脉冲增量,这里是简单的通过外部中断计数脉冲的方式来实现(有关外部中断的配置都放在 movement.c 文件中的 movement_system_init() 函数内):
    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);
    }
    
    • 实现forward_correction()接口,该接口将采集到的脉冲数pulse_num、上一次采集的脉冲数pulse_num_old和期望的脉冲增量standard_increment作为参数传入PID控制函数,根据计算结果调用single_motor_speed_set()调整对应单个电机的PWM占空比:
    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;
    }
    
    • 在运动逻辑轮询处理函数中,当进入直线状态的switch分支时,调用上面提到的forward_correction()接口来调整电机转速,然后再把当前采集到的脉冲数encoder_pulse赋值给encoder_pulse_old(todo_judge()将进行转弯需求判断,会在后文讲到):
    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;			
    		}
    }
    

    5.GNSS数据接收

    本方案采用的涂鸦 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 开发平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。

更进一步