移植FreeRTOS的STM32F103双轮平衡小车(开源,代码文末)

news2024/11/18 9:36:36

耗时大概三四天吧,主要时间还是花在硬件方面上,

ps:之前因为还想再完善一点,就一直放在草稿里,并不是今天才写的。面试官看到希望理解。。。

引言

1、系统概述

1.1、设计任务

利用stm32f103作为主控,移植FreeRTOS来进行实时任务调度

1.2、设计要求

利用MPU6050来读取相应的角度,用STM32对MPU6050读取到的数据进行处理,使用pid算法控制双轮小车达到自平衡。并且拓展手机app的功能来对小车移动方向进行控制

1.3、硬件清单

这里说一下我用到的材料

  • 两个电机,型号为JGB37-520编码电机
  • 2WD平衡车亚克力底盘
  • 一块tb6612电机驱动板
  • 航模电池1500
  • 一个5V降压模块
  • 嘉立创免费打样的PCB板,跟大家提一句,最好用面包板测试连接的线路正确再打,要不改来改去麻烦
  • 排母若干
  • 排针若干
  • DHT11船型开关一个

2、方案设计与论证

2.1、芯片选择方案
芯片

使用stm32F103RCT6作为主控

stm32是一个低功耗,高性能32位单片机,片内含4k Bytes ISP(In-system programmable)的可反复擦写1000次的Flash只读程序存储器。主要性能有:与MCS-51单片机产品兼容、全静态操作:0Hz~33Hz、 三级加密程序存储器、32个可编程I/O口线、三个16位定时器/计数器、八个中断源、全双工UART串行通道、掉电后中断可唤醒、看门狗定时器、双数据指针、掉电标识符、易编程。

系列名称STM32F脉冲宽度调制2 ( 16 位)(电动机控制)
封装类型LQFP程序存储器类型闪存
安装类型表面贴装宽度10.2mm
引脚数目64高度1.45mm
装置核芯ARM Cortex M3计时器分辨率16Bit
数据总线宽度32Bit模数转换器3 ( 16 x 12 位)
程序存储器大小256 kbI2C通道数目2
最大频率72MHz长度10.2mm
内存大小48 kb最高工作温度+85°C
USB通道1个设备模数转换器单元数目3
PWM单元数目2计时器数目6
模数转换器通道16模数转换器分辨率12Bit
SPI通道数目3最低工作温度-40°C
典型工作电源电压2. →3.6 vPWM分辨率16Bit
USART 通道数量5指令集结构RISC
计时器6 x 16 位CAN通道数目1
尺寸10.2 x 10.2 x 1.45mm

加一个STM32F103RCT6各字段的含义
STM32(芯片系列):STM32代表ARM Cortex-M 内核的32位微控制器
103(芯片子系列):101基本型,102USB基本型(USB2.0),103代表增强型系列,105或107互联型
F(产品类型):F代表通用系列
R(引脚数量):T=36, C=48, R=64, V=100 ,Z =144
C(闪存容量):4=16K,6=32K,8=64K,B=128K,C=256K,D=384K,E=512K

T(表封装):
H代表BGA封装
T代表LQFP封装
U代表VFQFPN封装
Y代表WLCSP64

6(工作温度范围):6代表-40 — 85℃,7代表-40 —105℃
 

2.2 、系统概述

本设计是一个具有自动调节平衡功能的两轮小车。由MPU6050模块、1.44寸LCD显示屏、TB6612电机驱动模块、霍尔电机、航模电池供电电路等模块组成。本项目研究一种使用单片机PID算法的自平衡方案。这种方案后续可以制作成为自平衡代步工具,自平衡自行车等等。

2.3、设计要求

IIC通信
PID算法的调节,要求抖动不得超过1cm’
小车可以正常达到自平衡
lcd显示偏航/俯仰/滚动角的数据
小车可以通过控制进行简单的前进后退

2.4、系统总体设计

利用stm32和MPU6050进行通信,实时获取mpu6050发送过来的数据,并且在1.44LCD上面显示,利用mpu6050的数据,结合PID算法,控制电机驱动块去控制电机的正方转,以达到自平衡的目的。

2.5、重要功能模块程序实现原理分析

2.5.1、MPU6050模块的介绍

其实通俗一点来说,这个模块就是用来测量小车的各姿态角

在讨论姿态角之前我们可以了解一下机体坐标系

机体坐标系

小车的姿态角——欧拉角

可以想象一下飞机进行上下点头的样子,这个时候上半的机身与水平面形成的夹角就是俯仰角

小车的姿态角——航偏角

 可以想象一下飞机的机头向左向右摆头和最开始机头位置形成的夹角

小车的姿态角——翻滚角

可以想象一下飞机在空中水平翻滚的样子

 2.5.2 ESP8266模块

大概是这个样子

主要就是通过AT指令来进行控制

esp8266.c
#include "esp8266.h"
#include "string.h"
#include "usart.h"
#include "usart2.h"
#include "delay.h"

char a[]="AT+CWMODE=1";
char b[]="AT+RST";
char c[]="AT+CWJAP=\"lfh\",\"81009738\"";                
char d[]="AT+CIPMUX=1";
char e[]="AT+CIPSTART=0,\"TCP\",\"115.29.109.104\",6552";
char f[]="AT+CWLAP";

void esp8266_start_trans(void)
{
		//重启
	esp8266_send_cmd1((u8 *)b);
    delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);


	//设置工作模式 1:station模式   2:AP模式  3:兼容 AP+station模式
	esp8266_send_cmd1((u8 *)a);
	delay_ms(1000);
	delay_ms(1000);
	
	//重启
	esp8266_send_cmd1((u8 *)b);
  delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);

	//连接WIFI
	esp8266_send_cmd1((u8 *)c);
  delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);
	delay_ms(1000);

	
	esp8266_send_cmd1((u8 *)d);
  delay_ms(1000);
	delay_ms(1000);

	esp8266_send_cmd1((u8 *)e);
  delay_ms(1000);
	delay_ms(1000);

}

void esp8266_send_cmd1(u8 *cmd)
{
  u2_printf("%s\r\n",cmd);	//发送命令,需要加换行符

}
 

esp8266.h
#ifndef __ESP8266_H
#define __ESP8266_H
#include "stdio.h"	
#include "stm32f10x.h"


//函数
void esp8266_send_cmd1(u8 *cmd);
void esp8266_start_trans(void);

#endif

3.PID算法控制小车保持平衡

原作者平衡小车PID,就该这么调!!! - 知乎 (zhihu.com)

看到这里大家应该都接触过PID算法,这里呢主要就是通过读取MPU6050数据计算出来的姿态角来控制电机的转速来保持平衡。

在平衡小车的PID中,分为三种PID,分别是直立环、速度环、转向环。任何一个PID最终计算出来的都是电机PWM,都是需要赋值给电机的。环环嵌套,得出电机的最终控制PWM。

3.1 直立环PID

在直立环中,PID的入口参数为:平衡小车的姿态角和姿态角对应的角速度。

值得说明,MPU6050得出来的姿态角有三种:PITCH(俯仰角)、ROLL(翻滚角)、YAW(航向角)

一般来说,MPU6050都是平放且平行装在平衡小车上,这样对调参也比较直观

如果平放且平行安装,那么直立环PID的入口参数为:Pitch或roll。直立环中,有一个较为重要的概念,也就是机械中值。通俗讲,小车在不接受任何外力或者电机作用,能够找到一个角度自我平衡。如何理解这句话:很简单,小车电机不转动,人的手扶着小车,小车总能找到一个角度,自我短期平衡。此时的角度就是机械中值。

代码

/*******************************************************************
函数功能:直立PD控制
入口参数:角度、机械平衡角度(机械中值)、角速度
返回  值:直立控制PWM
作    者:张巧龙
******************************************************************/
int balance_UP(float Angle,float Mechanical_balance,float Gyro)
{  
   float Bias;//角度误差
   int balance;//直立环计算出来的电机控制pwm
   Bias=Angle-Mechanical_balance;                   
   //===求出平衡的角度中值和机械相关
   balance=balance_UP_KP*Bias+balance_UP_KD*Gyro;  
   //===计算平衡控制的电机PWM  PD控制   kp是P系数 kd是D系数 
   return balance;
}

从程序上看:

balance_UP_KP 为直立环的P

balance_UP_KD 为直立环的D。

如何确定P和D的大小和极性?

3.1.1 直立环 P 范围确定:

需要先确定PWM的范围,例如,定时器最大的PWM为7200,此时占空比为100%,电机应该是全速运行。如果小车需要直立,摆幅,差不多就要≤10°。,如果超过此范围,小车抖动较为厉害根据直立环的程序:

//***********平衡车控制******************************************
//函数功能:控制小车保持直立
//Angle:采集到的实际角度值
//Gyro: 采集到的实际角速度值
int zhili(float Angle,float Gyro)
{  
   float err;
	 int pwm_zhili;
	 err=Car_zero-Angle+Car_zero_offset;    //期望值-实际值,这里期望小车平衡,因此期望值就是机械中值       
	 pwm_zhili=zhili_Kp*err+Gyro*zhili_Kd;//计算平衡控制的电机PWM
	 return pwm_zhili;
}

由于PWM最大是7200,角度在10°,反推可以得到:直立环的P可选范围应该在0~700。

当然这只是个大致的范围,具体多少还需要进一步调试。

3.1.2 直立环P 极性确定:

极性也就是符号,P到底是给正的,也是负的。

直接给kp正负值,然后观察现象:

正常出现的现象是负反馈,小车往那边倒,电机转动使得小车往要倒的方向去追。使得小车能够往反方向站起来!

如果出现正反馈,车往哪边倒,电机转动使得小车快速倒下。这种现象就是不对的。

3.1.3 直立环P 大小确定

慢慢试错,从小到大,响应慢慢加快也就是小车倒下后恢复直立的时间越来越短,直到小车出现大幅度的低频抖动!

此时的P可以确定。

3.1.4  直立环 D 极性确定

D的极性较为好确定,设P为0,D给正负值,分别去试,看效果。

当拿起小车进行旋转时,小车的轮子应该是小车旋转方向相同,此时说明极性是对的。

如果小车的轮子转动和小车的转动方向不相同,说明此时极性是反的!

3.1.5 直立环D 大小确定

D的大小,需要联合P去调试,在P调好的基础上,加入D,从小到大慢慢去试,从程序PD可以看到,D对应的是角速度,由于角速度都是四位数以上的数值,所以可以从0.1开始试。

一直到小车出现高频的剧烈抖动。

需要说明的是,如果小车各方面机械机构都分布较为均匀,重量分布较好,重心较低,小车靠单纯的直立环能够暂稳。

但一般来说,没有谁的小车机械结构做的很好。

所以说,单纯靠直立环是无法将小车站稳的。需要再加入速度环。

单纯的直立环能使小车站稳 5s 就说明调的很好了!

3.2 速度环

速度环中,采用PI控制,积分控制和比例控制有一定的比例关系。

这里可以确定为200,别问为什么,没有为什么。问就是200!

速度环的入口参数,为小车的2个电机编码器数值,也就是测速!

没有小车速度的实时反馈,谈何速度闭环。

代码

//*****************************************************************
//函数功能:控制小车速度
//encoder_left: 左轮编码器值
//encoder_right:右轮编码器值   因为程序周期执行,所以这里编码器的值可以理解为速度
int sudu(int encoder_left,int encoder_right)
{  
	  static int pwm_sudu,Encoder_Least,Encoder;
	  static int Encoder_Integral;	
		Encoder_Least =(encoder_left+encoder_right)-Movement;  //获取最新速度偏差==测量速度(左右编码器之和)-目标速度(此次为零) 
		Encoder *= 0.8;		                             //一阶低通滤波器       
		Encoder += Encoder_Least*0.2;	                 //一阶低通滤波器    
  	Encoder_Integral +=Encoder;                     //积分出位移 积分时间:5ms
		if(Encoder_Integral>8000)  Encoder_Integral=8000;  //积分限幅
		if(Encoder_Integral<-8000)	Encoder_Integral=-8000; //积分限幅	
		pwm_sudu=sudu_Kp*Encoder+sudu_Ki*Encoder_Integral;     //速度PI控制器	
		if((pitch>=80)||(pitch<=-80))  //小车跌倒后清零
		{
		  Encoder_Integral=0;    
		}			
	  return pwm_sudu;
}

3.2.1 速度环 P 范围确定:

同样的,和直立环P的大小范围确定一样,我们需要得到电机编码器的最大值和PWM的最大值的关系!

从程序中可以看到,我们应该比较的是,2个电机的速度偏差和pwm的关系。

比如:用STM32定时器的正交解码模式对电机进行测速,10ms一次。

小车电机满速旋转时,左右两个电机,编码器相加可达160。

假设速度偏差(实际测量值与理想值)达到50%时满转。

那么有,160/2=80,7200/80=90,也就说kp最大为90。

(注意,这里只是在假设50%的前提下).

90只是一个参考值,具体多少,还是需要根据,实际测试的效果。

3.2.2 速度环 P 极性确定:

确定P的极性,需要关闭前文的直立环,也就是说整个系统的控制参数只能有速度环的P。

单单靠直立环控制小车,小车能短暂直立,但会出现往前走或往后走,然后倒下,那么速度环就是用来抑制此现象的出现。

从上文程序中可以看到:

 Encoder_Least =(Encoder_Left+Encoder_Right)-0;      
 //===获取最新速度偏差==测量速度(左右编码器之和)-目标速度(此处为零)

这句程序的意思就是,获取最新速度偏差,控制小车目标速度为0。

直立环中控制小车不倒下是用来控制小车的角度,所以直立环的机械中值是:角度

速度环控制小车不倒下是用来控制小车的速度,所以速度环的“中值”就是:速度为0

应该不难理解!

那么如何抑制小车速度为0呢?

既然我们可以知道小车的当前速度,只要速度环的P为正反馈即可,意思就是假如向前倒,那么小车就要以更快的速度向前冲,保持直立。

同样的,屏蔽前文的直立环,分别给速度环P正负值,看现象。

正反馈的现象为:

当旋转其中一个轮子,两个轮子往相同方向旋转,到速度最大值。此时应该为正反馈。此时的现象说明,速度环的P极性是对的!

如果出现旋转其中一个轮子,另外一个轮子往反方向转动,让偏差趋向于零。这就是负反馈,此时说明P极性错误!

3.2.3 速度环 P 大小确定:

确定P极性和大小之后,由于P和I有比例关系且P为I的200倍!P和I的大小可以一同调试,可以将P和I慢慢从小到大的参数去试,观看小车效果。

如果出现以下效果:

1、小车放在地上,慢慢的,随着时间越来越长,小车会来回晃荡,此时可以认为P和I的参数过小。

2、小车放在地上,用手去推,如果小车无法回到初始位置,一直来回晃荡,来回晃荡的时候,车身出现较为大的倾斜,此时可以认为P和I的参数过大。如果车身没有出现较大的倾斜,只是小车来回晃荡,此时可以认为P和I的参数过小。

3.3 转向环

3.3.1 转向环P范围确定

我们得到的 MPU6050 输出的陀螺仪的原始数据,通过观察数据,我们发现

最大值不会超过 4 位数(正常应用在平衡小车上的时候),再根据 7200 代表占空

比 100%,所以我们估算 kp 值应该在 0~2 之间。

3.3.2 转向环P极性的确定:

先设定 kp=-0.6,我们可以看到,当我们把小车摁在地上旋转的时候,我们可

以很轻易的转动小车,说明目前小车没有通过负反馈把目标角速度控制在零附

近,而是通过正反馈帮助我们旋转小车,说明了这个时候小车的转向系统是正反

馈的。然后我们设定 kp=0.6,这个时候我们把小车摁在地上旋转会发现使用很大

的力也难以转动小车,小车会反抗我们,并通过电机保持角速度为零,这就是典

型的角速度负反馈效果,也是我们需要看到的效果。

3.3.3 转向环P大小确定

调试参数:p从小到大,然后看小车走直线情况即可确定。

如何让小车转弯:转向环很好理解,参数就是mpu6050的航向角的角速度gyro,如果要转向就给最后的输出转向的pwm一个偏差就可以了

//*************************************************************
//函数功能:控制小车转向
//Set_turn:目标旋转角速度
//Gyro_Z:陀螺仪Z轴的角速度
//不是一个严格的PD控制器,为小车的叠加控制
int zhuan(float Set_turn,float Gyro_Z)
{
  int PWM_Out=0; 
	if(Set_turn==0)
	{
	 PWM_Out=zhuan_Kd*Gyro_Z; //没有转向需求,Kd约束小车转向
	}
	if(Set_turn!=0)
	{
	 PWM_Out=zhuan_Kp*Set_turn; //有转向需求,Kp为期望小车转向 
	}
	return PWM_Out;
}

3.4 PID算法详解

其实做了什么不重要,学到什么才是最重要的,我现在就总结一下主要的概念

PID算法(Proportional-Integral-Derivative algorithm)是一种常用的控制算法,广泛应用于工业控制系统中。它通过测量被控对象的状态,计算出一个控制量来调整被控对象的输出,以使其达到预期的目标。

PID算法由比例项(Proportional)、积分项(Integral)和微分项(Derivative)三部分组成。

  1. 比例项(Proportional):比例项根据被控对象当前的偏差(实际值与目标值之间的差异)来计算控制量。比例增益系数Kp用于调节比例作用的强弱程度。当偏差较大时,比例项的作用也较大,控制量的调整幅度较大。

  2. 积分项(Integral):积分项考虑了被控对象在一段时间内累积的偏差,用来消除系统的静态偏差。积分增益系数Ki用于调节积分作用的强弱程度。积分项对长时间偏差的处理比例项更为有效,可以帮助系统快速消除稳态误差。

  3. 微分项(Derivative):微分项根据被控对象当前的偏差变化率来计算控制量。微分增益系数Kd用于调节微分作用的强弱程度。微分项可以预测系统未来的变化趋势,通过减小偏差的变化率来提高系统的稳定性。

PID算法的计算公式为: 控制量 = Kp * 偏差 + Ki * 积分偏差 + Kd * 偏差变化率

其中,偏差是实际值与目标值之间的差异,积分偏差是偏差的累积,偏差变化率是偏差的导数。

PID算法的参数调节是一个重要的问题,不同的应用需要根据实际情况进行调试和优化。常见的调参方法包括经验调参、Ziegler-Nichols方法、模糊控制等。

总之,PID算法通过比例、积分和微分三个方面的控制策略来实现对被控对象的精确控制,具有简单、可靠、易于实现的特点,在工业自动化领域得到广泛应用。

4、定时器捕获模式读取脉冲

在编码器的工作原理中,编码器是通过两个信号通道(通常称为A相和B相)的脉冲信号来实时反映转子的位置和方向的。这些脉冲信号的变化会导致编码器内部的计数器(CNT)值发生相应的增减。

TIM2和TIM3是STM32的定时器模块,它们具有输入捕获(Input Capture)功能,可以用来测量外部信号的高或低电平持续时间,并将结果存储在相应的寄存器中。

在这段代码中,通过配置TIM2和TIM3的CH1和CH2通道为输入捕获模式,当接收到编码器脉冲信号时,TIM2和TIM3会自动记录捕获到的脉冲数量,并将其存储在CNT寄存器中。因此,通过读取TIM2和TIM3的CNT寄存器的值,我们就可以获取到编码器的计数值,从而知道转子的位置和方向。

具体的操作流程如下

#include "encoder.h"


//这里采用TIM2和TIM3的CH1和CH2通道进行编码器的接口输入
//定时器				CH1					CH2
//TIM2				PA0					PA1
//TIM3				PA6					PA7

//这里的定时器2、3挂载在APB1上
//**********************编码器时钟初始化*********************
void Encoder_Count_RCC(void)
{
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
}
//**********************编码器引脚初始化*********************
void Encoder_Count_GPIO(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	//**********TIM2,PA0,PA1****************
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	//**********TIM3,PA6,PA7****************
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);

}
//**********************编码器功能初始化*********************
void Encoder_Count_Configuration(void)
{
	TIM_ICInitTypeDef TIM_ICInitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	//**********TIM2,PA0,PA1***********************************
	TIM_ICStructInit(&TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_1;
	TIM_ICInitStruct.TIM_ICFilter=0xF;  //滤波
	TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInit(TIM2, &TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_2;
	TIM_ICInitStruct.TIM_ICFilter=0xF;
	TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInit(TIM2, &TIM_ICInitStruct);	
	TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_Period=65535;    //65536-1
	TIM_TimeBaseInitStruct.TIM_Prescaler=0;     //1-1
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
	TIM_Cmd(TIM2,ENABLE);  
	//**********TIM3,PA6,PA7***********************************
	TIM_ICStructInit(&TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_1;
	TIM_ICInitStruct.TIM_ICFilter=0xF;
	TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_2;
	TIM_ICInitStruct.TIM_ICFilter=0xF;
	TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;
	TIM_ICInit(TIM3, &TIM_ICInitStruct);	
	
	TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_Period=65535;    //65536-1
	TIM_TimeBaseInitStruct.TIM_Prescaler=0;     //1-1
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
	
	TIM_Cmd(TIM3,ENABLE);  
}
//**********************编码器初始化*********************
void Encoder_Count_Init(void)
{
  Encoder_Count_RCC();
	Encoder_Count_GPIO();
	Encoder_Count_Configuration();
}
//******************编码器数据读取********************************
int Encoder_Value(TIM_TypeDef* TIMx)
{ 
	int channal_val=0;
	
	channal_val = TIMx ->CNT;
	if(channal_val>>15)//  channal_val 的最高位(即符号位)移到最低位,然后判断该最低位是否为 1。
										//  如果是1那么证明从0开始反转,65535开始减,那么第十六这时候就是1
	
	{			
		channal_val =  (channal_val&0x7FFF)-32767;//通过 channal_val & 0x7FFF 将 channal_val 的最高位清零,保留其他位的值
	}	
  return channal_val;
}
//****************编码器清零*************************************
void Encoder_Count_Clear(TIM_TypeDef* TIMx)
{
  TIMx ->CNT = 0;
}


  1. #include "encoder.h":包含头文件encoder.h,用于声明编码器相关函数和宏定义。

  2. Encoder_Count_RCC():配置编码器使用的定时器(TIM2和TIM3)的时钟。

  3. Encoder_Count_GPIO():配置编码器引脚的模式和速度。其中TIM2连接到PA0和PA1引脚,TIM3连接到PA6和PA7引脚。

  4. Encoder_Count_Configuration():对编码器进行功能配置。具体步骤如下:

  5. a. 初始化TIM2和TIM3的输入捕获参数(TIM_ICInitStruct),设置通道为1和2。
    b. 设置输入捕获滤波器和极性,均为上升沿触发
    c. 初始化TIM2和TIM3的输入捕获配置。
    d. 配置编码器模式为TI12(两个信号边沿同时触发)。
    e. 配置定时器时钟分频系数、周期和预分频值。
    f. 使能TIM2和TIM3定时器。

  6. Encoder_Count_Init():调用前面的函数,完成编码器的初始化。

  7. Encoder_Value(TIM_TypeDef* TIMx):读取编码器的值。
    a. 使用TIMx的CNT寄存器读取编码器计数值。
    b. 判断最高位是否为1,如果是则表示发生反转。
    c. 将最高位清零,保留其他位的值,并将结果减去32767(最大计数值的一半)。

  8. Encoder_Count_Clear(TIM_TypeDef* TIMx):清零编码器的计数值。

然后在motor.c中定义函数MOTO_Speed_Read()来提取寄存器内的值,也就是脉冲数

void Moto_Speed_Read(u8 n)
{ 
	//1号电机测速
	if(n==1)
	{
	  speed_num1=Encoder_Value(TIM2); //读取XXms以后的编码器数值
		speed_num1=speed_num1*10;
		Encoder_Count_Clear(TIM2);       //将编码器清零,用于下次计数		
	}
	//2号电机测速
	if(n==2)
	{
	  speed_num2=-(Encoder_Value(TIM3));//读取XXms以后的编码器数值
		speed_num2=speed_num2*10;
		Encoder_Count_Clear(TIM3);      //将编码器清零,用于下次计数
	}	 
}

5、遇到的问题分享一下

裸机开发硬件电路问题

  1. 制作pcb时记得打孔,位置要根据小车底盘上你所想要通过六角铜柱来连接的孔来确定,大小也要根据底盘上的开孔大小确定,毕竟用的是同一根六角铜柱进行连接。
  2. 在pcb打板前可以使用面包板进行连接
  3. 排针排母买多点省邮费
  4. 编码器接线注意不要接反了,接对了一瞬间电机上的LED灯就会亮
  5. 焊接时注意不要忘记焊接或者虚焊,建议用电压表测通路
  6. 在PCB上也是要测电压才慢慢加模块
  7. MPU6050模块的放置,我是水平防止,有芯片的朝上。长边朝北方,这里可以自己根据俯仰角确定。注意,如果不一样代码也要修改,估计不好改

裸机开发软件方面问题

问题:在不进行定时器初始化时,就单纯烧录程序会出现小车不转的情景

知识点,对同一个定时器进行两次初始化,后面一次定时器初始化的值会覆盖前一次的。因为先对pwm进行初始化,再对编码器函数进行初始化。这里设置的定时器3的PWM计数器重装载值为7200就被编码器初始化的重装载值65535覆盖掉了。由于设置的TIM的比较寄存器的值为720,此时小车的占空比并没有预想的那样是10%,而是很小,克服不了阻力,所以没有产生转动。

一个定时器用来读取编码器的脉冲只有CH1、CH2但是通道三四不能再用来输出PWM了(这里我还不太理解,应该可以,不过用不同的最好了)

刚开始时我使用的是定时器3的CH1、CH2来进行编码器脉冲的捕获,然后使用CH3、CH4来输出两路PWM对电机进行控制。

但是在我用来输入输出pwm的定时器1中,设置的计数值为7200,而在定时器2的编码器计数模式方面,,通过设置TIMx的计数器的值为65535(即TIM_TimeBaseInitStruct.TIM_Period = 65535),表示计数器的最大值为65535。这是因为编码器一般使用两个信号相位差为90度的方波进行计数,每个方波周期内产生一个脉冲,所以计数器需要能够容纳一个完整的方波周期,即需要有足够大的计数范围来记录方波的个数。所用直流减速电机的减速比为21.3,物理脉冲为11,所以装一圈产生的脉冲为234.3。所以如果是7200的话小车只能在后续30圈到40圈时就会溢出了。不像65535那样可以存储的久,在对小车使用位置PID时也有更大的操作空间。

1)“发现个现象,PWM输入的频率最小值不能小于PWM输出的值”,这是肯定的!作为PWM输出时,计数器从0累计到你设定的temp_fre,所以作为PWM输入时,只能捕获到temp_fre以下的数值。

2)“用示波器观察,TIM2的PWM输出频率不能小于50HZ,程序里设置的30HZ,按照理论计算72分频后,1000000/65536 < 20HZ的。可是没办法输出低于50HZ的PWM”,不明白你看到了什么?

3)“我要用STM32F103C8T6来输出4路频率可调,脉宽可调的PWM,还要输入一路PWM”,请按照1),找一个最小频率的定时器做为PWM输入,如果输入的频率比较低,可以利用PWM输出时的溢出中断,在中断处理中计数,然后再补偿到输入捕获的数值中。

  • 问题:在把小车放在地上是小车当倾斜角度较小时,突然地往左边倾倒又突然地右倾倒回去
    ,越倾倒角度越大,根本无法保持基本的平衡。然后我把小车提起来,微微倾斜小车,发现小车在小角度时轮子不会往对应倾斜的角度来进行滚动来使小车保持平衡。这个时候我觉得可能是KP调节太小了,但是无论调节太大的话依旧会侧翻过头。这时候我注意到当小车角度发生变化是。轮子没有立马做出响应,可能被中断服务函数执行地太慢了。我就把里面在屏幕上显示俯仰角航片角之类的代码使用了另外一个定时器进行显示,然后发现问题解决。

  • 不要再对小车平衡的定时器中断内执行太多的程序,比如在屏幕上显示什么,就很占CPU时间。可以再开一个定时器

中断不能被低于当前优先级的中断抢占,也不能抢占自己-------CM3权威指南

在小车平衡的定时器中断内执行过多的程序会占用CPU时间,可能导致定时器中断的响应延迟,进而影响小车的平衡控制性能。为了避免这种情况,可以考虑开启一个额外的定时器来处理其他任务。开启一个额外的定时器可以将一些耗时较长的任务或周期性的任务从主定时器中断中剥离出来,以提高系统的响应速度和稳定性。

        问题:在使用手机app通过esp8266向串口2发送数据时反应不灵敏

知识点:注意串口、定时器优先级的配置,优先级高的中断会打断优先级低的中断

我当时使用的esp8266是通过串口2的PA2和PA3引脚进行连接,配置的中断优先级的话设置比小车平衡的time7优先级低,在执行串口2的中断服务函数时会被1ms一次的定时器中断打断,导致控制语令要不就没接收完全要不就是接收完全了却没有时间来的及处理。解决方法就是把串口2的优先级提到最高,不过这样子在频繁发送也就是不停发送指令时会阻塞平衡任务。所以也就萌生了使用RTOS的想法

6、实物图片、PCB图片、原理图

    

7、代码链接:https://pan.baidu.com/s/1ezhjzNXToPOS33J8Adapwg?pwd=rtos 
提取码:rtos

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1032561.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE List

目录 1 预备知识-泛型(Generic)1.1 泛型的引入1.2 泛型类的定义的简单演示 1.3 泛型背后作用时期和背后的简单原理1.4 泛型类的使用1.5 泛型总结 2 预备知识-包装类&#xff08;Wrapper Class&#xff09;2.1 基本数据类型和包装类直接的对应关系2.2 包装类的使用&#xff0c;装…

【C++面向对象侯捷下】2.转换函数 | 3.non-explicit-one-argument ctor

文章目录 operator double() const {} 歧义了 标准库的转换函数

美创科技参编《数字政府建设与发展研究报告(2023)》 正式发布

9月14日&#xff0c;中国信息通信研究院云计算与大数据研究所牵头编制的《数字政府建设与发展研究报告&#xff08;2023&#xff09;》正式发布。 美创科技结合在政务数据安全领域的丰富实践经验&#xff0c;参与报告编写。 《数字政府建设与发展研究报告》 以“技术、业务、数…

2023工博会强势回归!智微工业携八大系列重磅亮相

中国国际工业博览会&#xff08;简称"中国工博会"&#xff09;自1999年创办以来&#xff0c;历经二十余年发展创新&#xff0c;通过专业化、市场化、国际化、品牌化运作&#xff0c;已发展成为通过国际展览业协会&#xff08;UFI&#xff09;认证、中国工业领域规模最…

flex布局与float布局

float布局 俩栏 三栏 flex布局

Leetcode199. 二叉树的右视图

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 题解&#xff1a;力扣&#xff08;LeetCode&#xff09…

大型集团借力泛微搭建语言汇率时区统一、业务协同的国际化OA系统

国际化、全球化集团&#xff0c;业务遍布全世界&#xff0c;下属公司众多&#xff0c;集团对管理方式和企业文化塑造有着很高的要求。不少大型集团以数字化方式助力全球统一办公&#xff0c;深化企业统一管理。 面对大型集团全球化的管理诉求&#xff0c;数字化办公系统作为集…

虚拟机部署linux网络连接配置

1、虚拟机安装linux后&#xff0c;配置网络访问 虚拟机网络设置为NAT模式 linux网络配置好IP&#xff0c;主要是以下网络配置 2、linux没有ifconfig命令&#xff0c;ifconfig命令是在net-tools.x86_64包里 yum install net-tools.x86_64安装

207.Flink(二):架构及核心概念,flink从各种数据源读取数据,各种算子转化数据,将数据推送到各数据源

一、Flink架构及核心概念 1.系统架构 JobMaster是JobManager中最核心的组件,负责处理单独的作业(Job)。一个job对应一个jobManager 2.并行度 (1)并行度(Parallelism)概念 一个特定算子的子任务(subtask)的个数被称之为其并行度(parallelism)。这样,包含并行子任…

八大排序(四)--------直接插入排序

本专栏内容为&#xff1a;八大排序汇总 通过本专栏的深入学习&#xff0c;你可以了解并掌握八大排序以及相关的排序算法。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;八大排序汇总 &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库…

蓝桥杯每日一题2023.9.22

4960. 子串简写 - AcWing题库 题目描述 题目分析 原本为纯暴力但是发现会超时&#xff0c;可以加入前缀和&#xff0c;从前往后先记录一下每个位置c1出现的次数 再从前往后扫一遍&#xff0c;如果遇到c2就将答案加上此位置前的所有c1的个数&#xff08;直接加上此位置的前缀…

Mybatis学习笔记4 用javassist动态实现DAO接口基于接口的CRUD

Mybatis学习笔记3 在Web中应用Mybatis_biubiubiu0706的博客-CSDN博客 上篇最后在DAO实现类中,代码固定,没有业务逻辑,这篇笔记中对该实现类进行封装,就是说,以后不用写DAO实现类了 我们不难发现&#xff0c;这个dao实现类中的⽅法代码很固定&#xff0c;基本上就是⼀⾏代码&am…

【面试经典150 | 双指针】三数之和

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;暴力枚举方法二&#xff1a;双指针 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并附带一些对…

AIGC|从革新内容创作到社会共识建立,迎接全新技术维度

在人工智能的巨浪之下&#xff0c;我们身临一场前所未有的文化演变&#xff0c;一股革命性的力量正在重新定义我们的创造性边界。这股力量不是人类的智慧&#xff0c;而是人工智能生成内容&#xff08;AIGC&#xff09;技术&#xff0c;它正以前所未有的速度和广度改变着我们的…

上PICO,沉浸式观看亚运直播,参与跨国界游戏竞技

备受瞩目的杭州第19届亚运会&#xff0c;将于9月23日正式开幕。据悉&#xff0c;这也是有史以来项目最多的一届亚运会&#xff0c;除部分传统奥运项目外&#xff0c;还包含武术、藤球、板球、克柔术、柔术等亚洲特色项目&#xff0c;以及霹雳舞、电子竞技等深受年轻人喜爱的新兴…

数字赋能 融链发展 ——2023工博会数字化赋能专精特新“小巨人”企业高质量发展论坛顺利举行

编者按&#xff1a;2023年政府工作报告提出“加快传统产业和中小企业数字化转型”要求&#xff0c;按照《“十四五”促进中小企业发展规划》《关于开展中小企业数字化转型城市试点工作的通知》等文件的部署&#xff0c;通过开展城市试点&#xff0c;支持地方政府综合施策&#…

AI视频剪辑:批量智剪技巧大揭秘

对于许多内容创作者来说&#xff0c;视频剪辑是一项必不可少的技能。然而&#xff0c;传统的视频剪辑方法需要耗费大量的时间和精力。如今&#xff0c;有一种全新的剪辑方式正在改变这一现状&#xff0c;那就是批量AI智剪。这种智能化的剪辑方式能够让你在短时间内轻松剪辑大量…

pg数据表同步到hive表数据压缩总结

1、背景 pg库存放了大量的历史数据&#xff0c;pg的存储方式比较耗磁盘空间&#xff0c;pg的备份方式&#xff0c;通过pgdump导出后&#xff0c;进行gzip压缩&#xff0c;压缩比大概1/10&#xff0c;随着数据的积累磁盘空间告警。为了解决pg的压力&#xff0c;尝试采用hive数据…

如何在没有第三方.NET库源码的情况,调试第三库代码?

大家好&#xff0c;我是沙漠尽头的狼。 本方首发于Dotnet9&#xff0c;介绍使用dnSpy调试第三方.NET库源码&#xff0c;行文目录&#xff1a; 安装dnSpy编写示例程序调试示例程序调试.NET库原生方法总结 1. 安装dnSpy dnSpy是一款功能强大的.NET程序反编译工具&#xff0c;…

Qt创建线程(使用moveToThread方法创建子线程)

1.moveTothread方法: &#xff08;1&#xff09;要使用moveToThread方法必须继承与QObject类 &#xff08;2&#xff09;创建任务对象时不能指定父对象 例子&#xff1a; MyWork* work new MyWork(this); // error MyWork* work new MyWork; // ok &#xff08;3&#…