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

Posted 三明治开发社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一款简易低成本智能割草机制作——嵌入式功能实现篇相关的知识,希望对你有一定的参考价值。

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

功能需求如下:

接下来逐步和大家介绍以上功能的实现:

一、产品创建

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

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

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

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

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

  • 在下方开发资料处点击下载全部,其中就包含了涂鸦 MCU-SDK 文件。

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

二、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] 制作一款可以记录运动历史数据的智能呼啦圈——嵌入式功能实现

LabVIEW做一款科学计算器

TaskBuilder如何实现低代码开发?

mind+自定义arduino库,实现简易RFID智能家居系统

mind+自定义arduino库,实现简易RFID智能家居系统

mind+自定义arduino库,实现简易RFID智能家居系统