一款简易低成本智能割草机制作——嵌入式功能实现篇

智能割草机软件方案介绍:

功能需求如下:
一款简易低成本智能割草机制作——嵌入式功能实现篇_第1张图片
接下来逐步和大家介绍以上功能的实现:

一、产品创建

  • 首先登录涂鸦智能IoT平台创建产品,点击创建产品,在标准类目栏的最下方找到“找不到品类”,点击进入自定义产品创建页面。

一款简易低成本智能割草机制作——嵌入式功能实现篇_第2张图片

  • 输入产品名称和描述,通讯协议选择WIFI-蓝牙,点击创建产品。

一款简易低成本智能割草机制作——嵌入式功能实现篇_第3张图片

  • 在功能定义一栏添加DP点,如下图所示,本demo添加了标准功能:“方向控制”、以及自定义功能“刀片位置”、“刀片转速”、“范围长度设置”、“范围宽度设置”、“弓字除草”等;功能点可以根据需求自行增减,功能点名称以及属性也可根据需求自行修改。

  • 进入设备面板,可以选择自己喜欢的模板或者自己自定义面板,调试阶段推荐选择开发调试面板,便于测试。

  • 点开硬件开发一栏,对接方式选择“涂鸦标准模组MCU SDK开发”,模组选择CBU Wi-Fi&Bluetooth模组(在实际开发过程中可选择手上已有的涂鸦模组即可)

一款简易低成本智能割草机制作——嵌入式功能实现篇_第4张图片

  • 在下方开发资料处点击下载全部,其中就包含了涂鸦 MCU-SDK 文件。
    一款简易低成本智能割草机制作——嵌入式功能实现篇_第5张图片

  • 至此,产品创建阶段已经基本完成,此时可以通过“涂鸦智能”app和虚拟设备体验dp数据的接收和发送。
    一款简易低成本智能割草机制作——嵌入式功能实现篇_第6张图片

二、MCU固件开发

在进行我们的割草机应用开发之前,我们需要在我们的MCU工程中移植涂鸦 MCU-SDK。移植过程可参考文档:GD32F4系列单片机移植涂鸦MCU-SDK

移植了MCU-SDK后,再搭配一块烧录并授权了通用对接固件的CBU模组,我们的MCU就具备了连接涂鸦云和上报下发dp点的功能。

同时,本demo工程代码还移植了FreeRTOS系统以便于开发。

完成准备工作后,正式开始割草机的应用功能开发。

本demo例程地址:github

1.电机、舵机、电调驱动

本demo最主要的器件就是电机舵机和电调了,4个电机各控制了一个小轮,负责实现小车的运动;两个舵机组合在一起用来控制电调和电调上面的刀片的高低位置;电调则是控制刀片旋转,同时使转速可调控。

首先为上述器件编写初始化和设置接口,方便后续调用。相关的接口都实现在 servo_motor.c文件中。

  • 输出IO口宏定义,方便后续修改:
#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;
		}
}

2.PID控制实现直线行驶

由于本demo方案的四个轮子都是由独立电机控制的,在给定相同占空比的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;		
		}
	
}

3.实现直角转弯

以弓字模式为例,当小车直线行驶到规定面积长度时就需要进行直角转弯动作。在上一节的例程中出现的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;		
		}
	
}

4.刀片位置与刀片速度设置

刀片位置是由两个舵机一起控制的,在位置上下变动的同时还需要保持刀片的水平姿态不变,这就需要两个舵机在相反的方向变动相同的角度。这里直接封装出一个刀片位置设置接口,入参为位置枚举值:

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数据接收

本demo采用的涂鸦GUC300模组是通过串口通信的方式给MCU发送GNSS数据的,这里我们使用MCU的usart1来接收数据,usart0已经被作为与wifi模组通信的串口。

  • 首先是串口初始化:
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 等语句,我们只需要解析其中的 G P G G A 即 可 。 GPGGA即可。 GPGGAGPGGA 语句包括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 平台丰富它的功能,也可以更加方便的搭建更多智能产品原型,加速智能产品的开发流程。

你可能感兴趣的:(#,割草机,物联网,嵌入式,割草机,定位)