耗时大概三四天吧,主要时间还是花在硬件方面上,
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 kb | I2C通道数目 | 2 |
最大频率 | 72MHz | 长度 | 10.2mm |
内存大小 | 48 kb | 最高工作温度 | +85°C |
USB通道 | 1个设备 | 模数转换器单元数目 | 3 |
PWM单元数目 | 2 | 计时器数目 | 6 |
模数转换器通道 | 16 | 模数转换器分辨率 | 12Bit |
SPI通道数目 | 3 | 最低工作温度 | -40°C |
典型工作电源电压 | 2. →3.6 v | PWM分辨率 | 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)三部分组成。
-
比例项(Proportional):比例项根据被控对象当前的偏差(实际值与目标值之间的差异)来计算控制量。比例增益系数Kp用于调节比例作用的强弱程度。当偏差较大时,比例项的作用也较大,控制量的调整幅度较大。
-
积分项(Integral):积分项考虑了被控对象在一段时间内累积的偏差,用来消除系统的静态偏差。积分增益系数Ki用于调节积分作用的强弱程度。积分项对长时间偏差的处理比例项更为有效,可以帮助系统快速消除稳态误差。
-
微分项(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;
}
-
#include "encoder.h"
:包含头文件encoder.h,用于声明编码器相关函数和宏定义。 -
Encoder_Count_RCC()
:配置编码器使用的定时器(TIM2和TIM3)的时钟。 -
Encoder_Count_GPIO()
:配置编码器引脚的模式和速度。其中TIM2连接到PA0和PA1引脚,TIM3连接到PA6和PA7引脚。 -
Encoder_Count_Configuration()
:对编码器进行功能配置。具体步骤如下: -
a. 初始化TIM2和TIM3的输入捕获参数(TIM_ICInitStruct),设置通道为1和2。
b. 设置输入捕获滤波器和极性,均为上升沿触发。
c. 初始化TIM2和TIM3的输入捕获配置。
d. 配置编码器模式为TI12(两个信号边沿同时触发)。
e. 配置定时器时钟分频系数、周期和预分频值。
f. 使能TIM2和TIM3定时器。 -
Encoder_Count_Init()
:调用前面的函数,完成编码器的初始化。 -
Encoder_Value(TIM_TypeDef* TIMx)
:读取编码器的值。
a. 使用TIMx的CNT寄存器读取编码器计数值。
b. 判断最高位是否为1,如果是则表示发生反转。
c. 将最高位清零,保留其他位的值,并将结果减去32767(最大计数值的一半)。 -
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、遇到的问题分享一下
裸机开发硬件电路问题
- 制作pcb时记得打孔,位置要根据小车底盘上你所想要通过六角铜柱来连接的孔来确定,大小也要根据底盘上的开孔大小确定,毕竟用的是同一根六角铜柱进行连接。
- 在pcb打板前可以使用面包板进行连接
- 排针排母买多点省邮费
- 编码器接线注意不要接反了,接对了一瞬间电机上的LED灯就会亮
- 焊接时注意不要忘记焊接或者虚焊,建议用电压表测通路
- 在PCB上也是要测电压才慢慢加模块
- 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图片、原理图