基于stm32的平衡小车

本文详细介绍了基于STM32F103C8T6微控制器的平衡车项目,包括电机驱动的TB6612FNG模块接线与PWM控制,MPU6050陀螺仪的初始化与数据采集,编码器的捕获机制,以及PID控制器在直立、速度和转向环中的应用。蓝牙通信用于接收手机指令,实现前进、后退、转向等功能。整个项目强调了PID算法在系统稳定性中的关键作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

前言

一、电机驱动部分

1、TB6612FNG电机驱动模块接线方式:

2、代码使用定时器2的4路输出pwm

3、gpio引脚初始化,以及前进,后退引脚设置

二、MPU6050陀螺仪部分

三、编码器捕获部分

四、pid部分

1、直立环KD

2、速度环KI

3、转向环(PD)

五、蓝牙通信部分

总结


前言

经过几天对平衡车的理论学习和动手实践,终于完成了平衡车的基本功能,实现前进,后退,直立,转向。本项目用到了两个电机,一个两块亚力克板,一个mpu6050陀螺仪,TB6612FNG电机驱动模块,通信方式使用蓝牙模块进行简单的通信,主控芯片使用stm32f103c8t6。

一、电机驱动部分

1、TB6612FNG电机驱动模块接线方式:

VM 接12V电源,给电机供电

STBY 置1使能AIN1,AIN2,BIN1,BIN2

VCC 接5V电源

GND 接地

AO1,AO2 输出控制电机1正反转

BO1,BO2 输入控制电机2正反转

PWMA 控制AO1,AO2使能

PWMB 控制BO1,BO2使能

AIN1,AIN2 输入控制控制电机1正反转

BIN1,BIN2 输入控制控制电机2正反转

2、代码使用定时器2的4路输出pwm

具体代码如下

void time2_pwm_init(uint16_t arr,uint16_t pre)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//定时器2使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpio引脚使能
    
    GPIO_InitTypeDef gpio_init={0};
    gpio_init.GPIO_Pin=GPIO_Pin_2 | GPIO_Pin_3;
    gpio_init.GPIO_Mode=GPIO_Mode_AF_PP;
    gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&gpio_init);
    
    TIM_TimeBaseInitTypeDef tim2_base={0};
    tim2_base.TIM_ClockDivision=TIM_CKD_DIV1;
    tim2_base.TIM_CounterMode=TIM_CounterMode_Up;
    tim2_base.TIM_Period=arr;
    tim2_base.TIM_Prescaler=pre;
    tim2_base.TIM_RepetitionCounter=DISABLE;
    TIM_TimeBaseInit(TIM2,&tim2_base);
    
    TIM_OCInitTypeDef time2_oc={0};
    time2_oc.TIM_OCMode=TIM_OCMode_PWM1;
    time2_oc.TIM_OCPolarity=TIM_OCPolarity_High;
    time2_oc.TIM_Pulse=0;
    time2_oc.TIM_OutputState=TIM_OutputState_Enable;
    TIM_OC3Init(TIM2,&time2_oc);
    TIM_OC4Init(TIM2,&time2_oc);
    
    TIM_Cmd(TIM2,ENABLE);//启动定时器
}

3、gpio引脚初始化,以及前进,后退引脚设置

代码

void mator_gpio_init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    GPIO_InitTypeDef gpio_init={0};
    gpio_init.GPIO_Pin=GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    gpio_init.GPIO_Mode=GPIO_Mode_Out_PP;
    gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&gpio_init);
    
    GPIO_WriteBit(GPIOB,GPIO_Pin_12,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_13,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_14,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
}

void mator_forward(void)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_12,Bit_SET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_13,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_14,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_SET);
}

void mator_back(void)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_12,Bit_RESET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_13,Bit_SET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_14,Bit_SET);
    GPIO_WriteBit(GPIOB,GPIO_Pin_15,Bit_RESET);
}

二、MPU6050陀螺仪部分

主要使用的是mpu6050官方的dmp库

代码如下

//采集俯仰角,翻滚角,偏航角
void MPU6050_Pose(void)
{
	
	dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors,&more);	 
	
	if(sensors & INV_WXYZ_QUAT )
	{
		q0 = quat[0] / q30;	
		q1 = quat[1] / q30;
		q2 = quat[2] / q30;
		q3 = quat[3] / q30;

		Pitch = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3;	// pitch
		Roll  = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)*57.3;	// roll
		Yaw   = atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3;	//yaw
		
	}
}


//mpu6050初始化
void MPU6050_Init(void)
{
	int result=0;
	//IIC_Init();
	result=mpu_init();
	if(!result)
	{	 		 
	
		printf("mpu initialization complete......\n ");		//mpu initialization complete	 	  

		if(!mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL))		//mpu_set_sensor
			printf("mpu_set_sensor complete ......\n");
		else
			printf("mpu_set_sensor come across error ......\n");

		if(!mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL))	//mpu_configure_fifo
			printf("mpu_configure_fifo complete ......\n");
		else
			printf("mpu_configure_fifo come across error ......\n");

		if(!mpu_set_sample_rate(DEFAULT_MPU_HZ))	   	  		//mpu_set_sample_rate
		 printf("mpu_set_sample_rate complete ......\n");
		else
		 	printf("mpu_set_sample_rate error ......\n");

		if(!dmp_load_motion_driver_firmware())   	  			//dmp_load_motion_driver_firmvare
			printf("dmp_load_motion_driver_firmware complete ......\n");
		else
			printf("dmp_load_motion_driver_firmware come across error ......\n");

		if(!dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation))) 	  //dmp_set_orientation
		 	printf("dmp_set_orientation complete ......\n");
		else
		 	printf("dmp_set_orientation come across error ......\n");

		if(!dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT | DMP_FEATURE_TAP |
		    DMP_FEATURE_ANDROID_ORIENT | DMP_FEATURE_SEND_RAW_ACCEL | DMP_FEATURE_SEND_CAL_GYRO |
		    DMP_FEATURE_GYRO_CAL))		   	 					 //dmp_enable_feature
		 	printf("dmp_enable_feature complete ......\n");
		else
		 	printf("dmp_enable_feature come across error ......\n");

		if(!dmp_set_fifo_rate(DEFAULT_MPU_HZ))   	 			 //dmp_set_fifo_rate
		 	printf("dmp_set_fifo_rate complete ......\n");
		else
		 	printf("dmp_set_fifo_rate come across error ......\n");

		run_self_test();		//自检

		if(!mpu_set_dmp_state(1))
		 	printf("mpu_set_dmp_state complete ......\n");
		else
		 	printf("mpu_set_dmp_state come across error ......\n");
	}
	else												 //MPU6050状态指示灯 STM32核心板 PC13 绿色灯亮起为不正常
	 {
	 GPIO_ResetBits(GPIOC, GPIO_Pin_13);				//MPU6050状态指示灯 STM32核心板 PC13 绿色灯亮起为不正常
	 while(1);
	 }
	 
}

三、编码器捕获部分

使用定时器3,和4的通道1和通道2进行4倍频地计数,主要代码如下:

void time3_encoder_init()
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//定时器2使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//gpio引脚使能
    
    GPIO_InitTypeDef gpio_init={0};
    gpio_init.GPIO_Pin=GPIO_Pin_6 | GPIO_Pin_7;
    gpio_init.GPIO_Mode=GPIO_Mode_IN_FLOATING;
    gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&gpio_init);
    
    TIM_TimeBaseInitTypeDef time3_base={0};
    time3_base.TIM_ClockDivision=TIM_CKD_DIV1;
    time3_base.TIM_CounterMode=TIM_CounterMode_Up;
    time3_base.TIM_Period=65535;
    time3_base.TIM_Prescaler=0;
    time3_base.TIM_RepetitionCounter=DISABLE;
    TIM_TimeBaseInit(TIM3,&time3_base);
    
    TIM_ICInitTypeDef time3_ic={0};
    time3_ic.TIM_Channel=TIM_Channel_1;
    time3_ic.TIM_ICFilter=0;
    time3_ic.TIM_ICPolarity=TIM_ICPolarity_Rising;
    time3_ic.TIM_ICPrescaler=TIM_ICPSC_DIV1;
    time3_ic.TIM_ICSelection=TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM3,&time3_ic);
    time3_ic.TIM_Channel=TIM_Channel_2;
    TIM_ICInit(TIM3,&time3_ic);
    TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
    
    //初始化标志位,计数器
    TIM_ClearFlag(TIM3,TIM_FLAG_Update);//清除标志位
    TIM_SetCounter(TIM3,0);//TIM3->CNT=0;
    
    //配置中断
    NVIC_InitTypeDef nvic_init={0};
    nvic_init.NVIC_IRQChannel=TIM3_IRQn;//中断通道
    nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
    nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
    nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
    NVIC_Init(&nvic_init);
    
    TIM_ITConfig(TIM3,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
    TIM_Cmd(TIM3,ENABLE);//开启定时器
}

void time4_encoder_init()
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//定时器2使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//gpio引脚使能
    
    GPIO_InitTypeDef gpio_init={0};
    gpio_init.GPIO_Pin=GPIO_Pin_6 | GPIO_Pin_7;
    gpio_init.GPIO_Mode=GPIO_Mode_IN_FLOATING;
    gpio_init.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&gpio_init);
    
    TIM_TimeBaseInitTypeDef time4_base={0};
    time4_base.TIM_ClockDivision=TIM_CKD_DIV1;
    time4_base.TIM_CounterMode=TIM_CounterMode_Up;
    time4_base.TIM_Period=65535;
    time4_base.TIM_Prescaler=0;
    time4_base.TIM_RepetitionCounter=DISABLE;
    TIM_TimeBaseInit(TIM4,&time4_base);
    
    TIM_ICInitTypeDef time4_ic={0};
    time4_ic.TIM_Channel=TIM_Channel_1;
    time4_ic.TIM_ICFilter=0;
    time4_ic.TIM_ICPolarity=TIM_ICPolarity_Rising;
    time4_ic.TIM_ICPrescaler=TIM_ICPSC_DIV1;
    time4_ic.TIM_ICSelection=TIM_ICSelection_DirectTI;
    TIM_ICInit(TIM4,&time4_ic);
    time4_ic.TIM_Channel=TIM_Channel_2;
    TIM_ICInit(TIM4,&time4_ic);
    TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//配置编码器计数。BothEdge(底部边缘)
    
    //初始化标志位,计数器
    TIM_ClearFlag(TIM4,TIM_FLAG_Update);//清除标志位
    TIM_SetCounter(TIM4,0);//TIM3->CNT=0;
    
    //配置中断
    NVIC_InitTypeDef nvic_init={0};
    nvic_init.NVIC_IRQChannel=TIM4_IRQn;//中断通道
    nvic_init.NVIC_IRQChannelCmd=ENABLE;//中断使能
    nvic_init.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级;
    nvic_init.NVIC_IRQChannelSubPriority=1;//响应优先级
    NVIC_Init(&nvic_init);
    
    TIM_ITConfig(TIM4,TIM_IT_Update | TIM_IT_CC1 |TIM_IT_CC2,ENABLE);//配置定时器,允许更新中断,CC1,CC2捕获中断
    TIM_Cmd(TIM4,ENABLE);//开启定时器
}

四、pid部分

使用定时器1每隔5ms进行一次mpu6050的数据采集,并进行直立环控制,每40ms,进行一次速度环控制,防止影响直立控制。

1、直立环KD

小车的偏转角度作为Kp的系数,角加速度作为Kd的系数

代码如下:

float angle_pid_realize(struct _pid *pid,float angle)
{
    if(Pitch==0||Pitch<-20||Pitch>20)              //MPU6050状态指示灯 STM32核心板 PC13 绿色灯亮起为不正常
    {
          //Pitch=1;
      GPIO_ResetBits(GPIOC, GPIO_Pin_13);          //MPU6050状态指示灯 STM32核心板 PC13 绿色灯亮起为不正常
    }
    else 
    {GPIO_SetBits(GPIOC, GPIO_Pin_13);}             //MPU6050状态正常时LED灯熄灭
    
    pid->err=angle-pid->target_val;//计算目标值与实际值的误差
    pid->actual_val=pid->Kp*pid->err+pid->Kd*gyro[0];//角度PD控制,gyro[0]x轴偏转角速度
    return pid->actual_val;
}

2、速度环KI

速度环控制小车的位移,实现定点停下的功能,代码如下

void speed_control(void)
{
    mator.car_speed = (mator.left_tal_count  + mator.right_tal_count );// * 0.5 ;          //左右电机脉冲数平均值作为小车当前车速
    mator.left_tal_count =mator.right_tal_count = 0;      //全局变量 注意及时清零        
    BST_fCarSpeedOld *= 0.7;//一阶滤波,占上次的70%
    BST_fCarSpeedOld +=mator.car_speed*0.3;//一阶滤波,占本次的30%
    
    BST_fCarPosition += BST_fCarSpeedOld;          //路程  即速度积分       1/11 3:03
    BST_fCarPosition +=BST_fBluetoothSpeed;
    
    //积分上限设限//
    if((s32)BST_fCarPosition > SPEED_INTEGRAL_MAX)    BST_fCarPosition = SPEED_INTEGRAL_MAX;
    if((s32)BST_fCarPosition < SPEED_INTEGRAL_MIN)    BST_fCarPosition = SPEED_INTEGRAL_MIN;
                                                                                                  
    mator.speed_pwm = (BST_fCarSpeedOld -CAR_SPEED_SET) * BST_fCarSpeed_P + (BST_fCarPosition - CAR_POSITION_SET) * BST_fCarSpeed_I; //速度PI算法 速度*P +位移*I=速度PWM输出
}

3、转向环(PD)

主要是偏航角作为Kd的系数

代码如下

float turn_pid_realize(uint8_t ccd,short yaw)
{
    float turn=0;
    turn=(Turn_val-yaw)*Turn_Kd;
    //printf("turn=%d\r\n",yaw);
    return turn;
}

五、蓝牙通信部分

使用串口3与蓝牙进行通信,手机通过蓝牙助手给蓝牙发送消息,蓝牙模块通过串口发送消息到小车。

通信协议代码如下

void USART3_IRQHandler(void)                	//串口x中断服务程序
{
    uint8_t Res;
//    uint8_t i;
    if(USART_GetITStatus(USART3,USART_IT_RXNE) !=  RESET)//判断中断位
    {
        USART_ClearITPendingBit(USART3, USART_IT_RXNE);
        Res = USART_ReceiveData(USART3); //接收数据
        if(Res!='\n')
        {
            rx_buf[rx_size++]=Res;
        }
        else
        {
//            for(i=0;i<rx_size;i++)
//                printf("%c",rx_buf[i]);
//            printf("\r\n");
            if(memcmp("a1",rx_buf,2)==0)//前进
            {
                BST_fBluetoothSpeed=100;
                //printf("前进\r\n");
            }
            else if(memcmp("a2",rx_buf,2)==0)//后退
            {
                BST_fBluetoothSpeed=-100;
                //printf("后退\r\n");
            }
            else if(memcmp("a3",rx_buf,2)==0)//左转
            {
                BST_fBluetoothSpeed=0;
                Turn_val=90;
                Yaw_old=Yaw;
                //printf("左转\r\n");
            }
            else if(memcmp("a4",rx_buf,2)==0)//右转
            {
                BST_fBluetoothSpeed=0;
                Turn_val=-90;
                 Yaw_old=Yaw;
                //printf("右转\r\n");
            }
            else if(memcmp("a0",rx_buf,2)==0)//停下
            {
                BST_fBluetoothSpeed=0;
                //printf("停下\r\n");
            }
            rx_size=0;
        }
    }
} 

总结

        经过对平衡车的学习,我对pid算法有了更加深刻的理解,在mpu6050陀螺仪的使用也越来越熟练,平衡小车的核心就是pid算法,所以pid算法对小车和控制类的学习是十分重要的。

### STM32 平衡小车 编码器 测速 实现方法 在STM32单片机上实现平衡小车的编码器测速主要依赖于定时器的功能来捕捉编码器脉冲信号并计算速度。对于平衡小车而言,通常会安装两个轮子上的增量式旋转编码器,以便分别监测左右电机的速度。 #### 定时器配置 为了获取编码器反馈的信息,在硬件连接完成后需初始化外设资源——即设置特定的通用输入/输出端口(GPIO),以及使能相应通道的外部中断功能;更重要的是要正确配置定时器工作于编码器接口模式下[^1]: ```c // 配置 TIM2 作为编码器计数器 void Encoder_Init(void){ __HAL_RCC_TIM2_CLK_ENABLE(); // 开启TIM2时钟 TIM_Encoder_InitTypeDef sConfig; sConfig.EncoderMode = TIM_ENCODERMODE_TI1; // 使用 TI1 进行四倍频 sConfig.IC1Polarity = TIM_ICPOLARITY_RISING; // IC1 上升沿触发 sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; // IC1 映射到 TI1 sConfig.IC1Prescaler = TIM_ICPSC_DIV1; // 不分频 sConfig.IC1Filter = 0x0A; // 滤波系数为10 htim2.Instance = TIM2; HAL_TIM_Encoder_DeInit(&htim2); if (HAL_TIM_Encoder_Init(&htim2, &sConfig) != HAL_OK){ Error_Handler(); } } ``` 此部分代码展示了如何利用`TIM2`定时器配合编码器完成位置检测的任务。这里选择了`TIM_ENCODERMODE_TI1`表示仅使用第一个通道(通常是A相),而实际应用中可能还需要考虑B相信号以获得更精确的方向判断[^2]。 #### 中断服务程序设计 当设定好上述参数之后,则可以在另一个独立运行的定时器产生的周期性事件里处理数据采集逻辑,比如每隔一定毫秒读一次当前累计脉冲数量,并据此推算瞬时角位移变化率从而得出线速度值: ```c uint32_t last_count = 0; void Speed_Calculation(void){ uint32_t current_count = __HAL_TIM_GET_COUNTER(&htim2); // 获取最新计数值 int32_t delta_count = (int32_t)(current_count - last_count); float angular_velocity = ((float)delta_count / ENCODER_PPR) * TWO_PI; // 转换成弧度制下的角速度 float linear_speed = WHEEL_RADIUS * angular_velocity; // 计算线速度 last_count = current_count; printf("Current speed: %.2f m/s\n",linear_speed); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if(htim->Instance==TIM3){ // 假定由TIM3负责触发采样动作 Speed_Calculation(); } } ``` 以上片段说明了怎样基于前一时刻与本时刻之间累积步进量之差求得单位时间内发生的转动角度进而转换成直线运动速率的方法。值得注意的是这里的`TWO_PI`,`ENCODER_PPR`(每转脉冲数), 和 `WHEEL_RADIUS`均应依据具体设备规格予以适当调整。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值