初识单片机

基础构造

ISP

简介

ISP(In-System Programming)在系统可编程,指电路板上的空白器件可以编程写入最终用户代码, 而不需要从电路板上取下器件,已经编程的器件也可以用 ISP方式擦除或再 编程. ISP 的时候需要用到(bootloader)自举程序,自举程序存储在 STM32 器件的内部自举 ROM 存储器(系统存储器)中.其主要任务是通过一种可用的串行外设(USART, CAN, USB, I2C 等)将应用程序下载到内部 Flash 中.每种串行接口都定义了相应的通信协议, 其中包含兼容的命令集和序列.

使用

通过串口下载,最主要的优点是成本低,缺点是只能用于下载程序,不能硬件仿真.

普通ISP与一键ISP,普通ISP在下载程序时候需要手动配置BOOT的启动方式,而一键RSP则通过独特的硬件电路和 上位机的配合使用来达到一键下载的功能.

BOOT的启动方式的选择
BOOT0 BOOT1 启动方式
0 X 内部FLASH(内部仿真器支持)
1 0 系统存储器(串口支持)
1 1 内部SRAM

寄存器

带着这些问题看:

什么是存储器映射?

什么是寄存器映射?

丝印

在任何一个单片机芯片上都有丝印,这时候就要涉及怎么看正方向.一般芯片都会有一个小圆点.表示正方向的起始部分,正方向在这个小圆点的基础上逆时针旋转表示从1脚2脚到最后.但有时候还会有一个比较大的点.看两个点比较小的那个点.那还有一种方法,就是正看丝印逆时针方向左上角为第一个脚.

高低位切换

看完书后听课,这时候就立即理解代码是怎么实现的.其实就是封装.

1
2
3
4
5
6
7
8
9
10
11
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE+0x00010000)
#define GPIOB_BASE (APB2PERIPH_BASE +0X0c00)
#define GPIOB_ODR *(unsignedint*)(GPIOB_BASE+0x0C)
//简单的位运算就行了
//PB0输出高电平
GPIOB_ODR |=(1<<0);//将对应寄存器的第0位拉高成高电平
//PB0输出低电平
GPIOB_ODR &=~(1<<0);//将对应寄存器的第0位拉低成低电平


通过产家的手册,单片机的寄存器的值都是连续的所以就可以用struct的结构体变量进行封装.

GPIO

重点

GPIO跟引脚有什么区别?

GPIO属于引脚,因为引脚包括电源VCC,时钟晶振,boot引脚等.

初始化顺序

1.选具体的GPIO

2.配置工作模式(CRL和CRH寄存器)

3.控制GPIO输出高低电平(ODR,BRR和BSRR)

函数库

常见的问题

重复定义,之前在引用别人的代码时经常犯的错误.

为什么呢?

比如我在main.c中包含两个头文件

1
2
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"

在#include “stm32f10x_gpio.h”这个头文件中又包含#include “stm32f10x.h”这个头文件,相当于把后者在前者中又拷贝一份.所以在main.c中再次调用的时候就重复定义了.

所以就引入宏定义

1
2
3
4
#ifndef  __STM32F10X_GPIO_H//调用约定大写,防止重复编程
#define __STM32F10X_GPIO_H
...
#endif

小技巧

参数宏

1
2
3
4
5
6
#define LED_G(a)  if(a) \
GPIO_ResetBits(LED_G_GPIO_PORT, LED_G_GPIO_PIN); \
else GPIO_SetBits(LED_G_GPIO_PORT, LED_G_GPIO_PIN);

//其中,"\"为增行符,在这里与转义'\'不一样,进行宏定义封装可读性更好.

小总结

自己构建库函数的过程.

1.汇编编写的启动文件.

1
2
startup_stm32f10x_hd.s(hd表示falsh容量)
//设置堆栈指针,设置PC指针,初始化中断向量表,配置系统时钟,对用C库函数_main,最终去到C的世界

2.时钟配置文件

system_stm32f10x.c:把外部高速时钟HSE,经过PLL倍频为72M

3.外设相关的

stm32f10x.h:实现了寄存器之外映射

stm32f10x_xx.c:外设的驱动库文件

stm32f10x_xx.h:存放外设的初始化结构体,外设初始化结构体成员的参数列表,外设固件库函数的声明

xx:GPIO,USART,I2C,SPI,FSMC等外设.

4.内核相关的

CMSIS-Cortex 微控制器软件接口标准

core_cm3.h:实现了内核里面外设的寄存器映射

core_cm3.c:内核外设驱动固件库

NVIC(嵌套向量中断控制器),SysTick(系统滴答定时器)

misc.h

misc.c

5.头文件的配置文件

stm32f10x_config.h:头文件的头文件

1
2
3
4
5
6
7
stm32f10x_gpio.h
stm32f10x_usart.h
stm32f10x_i2c.h
stm32f10x_spi.h
stm32f10x_adc.h
stm32f10x_fsmc.h
...

6.专门存放中断服务函数的C文件

stm32f10x_it.c

stm32f10x_it.h

中断服务函数可以任意放在其他地方,并不是一定要放在stm32f10x_it.c

这样整个架构就基本清楚了.

#include “stm32f10x.h”//相当于51单片机中的 #include <reg51.h>

注意双引号” “和尖括号<>的区别

双引号先去当前自己的新建的工程目录下找,找不到才去keil这个软件的根目录下找.

1
2
3
4
int main()
{
//来到这里的时候,系统的时钟已经被配置成72M
}

位带操作

就是把寄存器里面的每一位都重新找了地址,地址在位带别名区里,地址的一个位会重新膨胀成4个字节大小.因为STM32总线都是32位的,按照这种32位的操作,效率最高.

总结来看:每个寄存器的每一个位TA都对应一个地址,如果操作这个地址的话那么就可以单独实现这个位的读和写.其他位不影响.

片上外设和SRAM均有1MB的位带区,位带区里面的每一个位都可以通过位带别名区的地址来访问.位带区的一个位,对应位带别名区的4个字节.

如果不设置位带别名区,之前操作一个位需要:读-修改-写,三个过程,比较耗时间.现在用1个32位的寄存器映射一个位,这样操作这个32位的寄存器直接就实现了对1个位的操作,效率提高了.

重点是公式:

1
2
// 把“位带地址+位序号”转换成别名地址的宏 
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr &0x00FFFFFF)<<5)+(bitnum<<2))

要操作哪一个位,就把要操作的地址转换位指针然后对指针进行操作.实现寄存器位的单独操作.

时钟树

HSE(高速的外部时钟)

来源:无源晶振(4-16M),通常使用8M
控制:RCC_CR时钟控制寄存器的位16:HSEON控制

HSI(高速的内部时钟)

来源:芯片内部,大小为8M,当HSE故障时,系统时钟会自动切换到HSI,直到HSE启动成功.
控制:RCC_CR时钟控制寄存器的位0:HSEON控制

当HSE故障时,HSI会被选为系统时钟(会降频).

PLLCLK(锁相环时钟)

来源:(HSI/2,HSE)经过倍频得到

控制:CFGR:PLLXTPRE,PLLMUL

注意:PLL时钟源头使用HIS/2的时候,PLLMUL最大只能是16,这个时段PLLCLK最大只能是64M,小于官方推荐最大时钟72M

系统时钟

SYSCLK,最高为72M(推荐)

来源:HSI,HSE,PLLCLK

控制:CFGR:SW

注意:通常的配置是SYSSCLK=PLLCLK=72M

HCLK时钟(AHB高速总线时钟)

速度最高为72M.为AHB总线的外设提供时钟,为Cortex系统定时器提供时钟(SysTick),为内核提供时钟(FCLK)

来源:系统时钟分频得到,一般设置HCLK=SYSCLK=72M

控制:CFGR:HPRE

PCLK1时钟(APB1低速总线时钟)

最高为36M.为APB1总线的外设提供时钟.2分倍频之后则为APB1总线的定时器2-7提供时钟,最大为72M

来源:HCLK分频得到,一般配置PCLK1=HCLK/2=36M

控制:RCC_CFGR时钟配置寄存器的PPRE1位

PCLK2时钟

最高为72M.为APB1总线的外设提供时钟.为APB1总线的定位器1和8提供时钟,最大为72M.

来源:HCLK分频得到,一般配置PCLK1=HCLK=72M

控制:PCC_CCFGR时钟配置寄存器的PPRE2位.

PTC时钟(实时时钟)

RTC时钟:为芯片内部的RTC外设提供时钟.

来源:HSE_RTC(HSE分频得到),LSE(外部32.768KHZ的晶体提供),LSI(32KHZ).

控制:RCC备份域控制寄存器RCC_BDCR:RTCSEL位控制.

IWDGCLK独立看门狗时钟

由LSI提供

MCO时钟输出

微控制器时钟输出引脚,由PA8复用所得.

来源:PLLCLK/2,HSE,HSI,SYSCLK

控制:CRGR:MCO

如果自己写时钟配置的函数想检测的对不对,就可以用MCO这个输出引脚,通过示波器来监控这个波形,看下频率对不对.

中断总结

一般来说,中断就是异常,异常就是中断.

类型

系统异常,体现在内核水平

外部中断,体现在外设水平

NVIC

嵌套向量中断器,属于内核外设,管理着包括内核和片上所有外设的中断相关的功能.

两个重要的库文件:

core_cm3.h和misc.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
代码 17-1 NVIC结构体定义,来自固件库头文件:core_cm3.h 
typedef struct {
__IO uint32_t ISER[8]; // 中断使能寄存器
uint32_t RESERVED0[24];
__IO uint32_t ICER[8]; // 中断清除寄存器
uint32_t RSERVED1[24];
__IO uint32_t ISPR[8]; // 中断使能悬起寄存器
uint32_t RESERVED2[24];
__IO uint32_t ICPR[8]; // 中断清除悬起寄存器
uint32_t RESERVED3[24];
__IO uint32_t IABR[8]; // 中断有效位寄存器
uint32_t RESERVED4[56];
__IO uint8_t IP[240]; // 中断优先级寄存器(8Bit wide)
uint32_t RESERVED5[644];
__O uint32_t STIR; // 软件触发中断寄存器
} NVIC_Type;
//在配置中断的时候我们一般只用ISER、ICER和IP这三个寄存器,ISER用来使能中断,ICER用来失能中断,IP用来设置中断优先级。

优先级的定义

优先级设定:NVIC~>IPRx

stm32只操作高八位用于表达优先级

优先级分组

将上面的4位分为两大组,配置主优先级和次优先级.如下表

优先级分组 主优先级 子优先级 描述

NVIC_PriorityGroup_0 0 0-15 主-0bit,子-4bit

NVIC_PriorityGroup_1 0-1 0-7 主-1bit,子-3bit

NVIC_PriorityGroup_2 0-3 0-3 主-2bit,子-2bit

NVIC_PriorityGroup_3 0-7 0-1 主-3bit,子-1bit

NVIC_PriorityGroup_4 0-15 0 主-4bit,子-0bit

编程顺序

<1>使能中断请求

<2>配置中断优先级分组

<3>配置NVIC寄存器,初始化NVIC_InitYypeDef

<4>编写中断服务函数

EXTI—外部中断事件控制器

stm32每一个GPIO都可以产生中断,怎么产生?体现在GPIO上就是电平的变化,由高电平变成低电平或者是由低电平变成高电平.电平的变化需要外设进行管理.最后传给NVIC(嵌套向量中断器)处理中断.管理外部的就到EXTI.

输入线

exti管理外部中20根线(STM32F1XX)

给个理解,上升沿触发选择触发寄存器和下降沿触发选择触发寄存器在F1中与边沿检测电路相连.上升沿是按键按下时产生脉冲,下降沿是按键弹开的脉冲.

信号分为两种,一种是到外设,还有一种是到脉冲发生器,触发ADC开始采集.还可以触发定时器开始计时.

初始化结构体

1
2
3
4
5
EXIT_InitTypeDef
<1>EXTI_Line//用于产生中断/事件线
<2>EXTI_Mode//EXIT模式(中断/事件)
<3>EXTI_Trigger//触发(上/下/上下)
<4>EXTI_LineCmd//使能或者是失能(IMR/EMR)

编程要点

<1>初始化要连接到EXTI的GPIO

<2>初始化EXTI用于产生中断/事件

<3>初始化EXTI,用于处理中断

<4>编写中断服务函数

<5>main函数

系统定时器(SysTick)

简介:24位,只能递减,存在于内核,嵌套在NVIC中

定时时间计算

1-t:一个计数循环的时间,跟reload和CLK有关

2-CLK:72M或者是9M,由CTRL寄存器配置

3-RELOAD:24位,用户自己配置

t=reload*(1/clk)

CLk=72M,t=(72)*(1/72M)=1us

CLK=72M,t=(72000)*(1/72M)=1Ms

PID

简述

误差调控,调控目标值使得实际输出为0.

举个例子:
小车左右各装一个电机,同时给相同的PWM占空比,但是小车总不是直线行使,两个电机在实际情况下不能保持一致.需要闭环控制,编码器测速反馈,控制器使用PID算法动态改变输出值使得目标速度=实际速度

所以PID

P(比例项)比例项的输出值仅取决于当前时刻的误差,与历史时刻无关.当前存在误差时,比例项输出一个与误差呈正比的值,当前不存在误差时,比例项输出0

I(积分项)这个比较难理解:意思是,每次进行PID调控时积分项都读取当前的error,然后累加到之前所有的error之和上.

所以在写积分项的程序时要单独定义一个变量用来存储error累加的和.积分项就是避免产生稳态误差,如果系统一直产生稳态误差则积分项会一直累积这个误差,然后积分项输出的控制力度就会不断加大,最终不断加大的驱动力肯定会减小误差直到误差为0,误差为0时,比例项直接输出0,所以此时比例没有驱动力,但是积分项误差为0,只是表示积分项的驱动力不再变化.而之前积分项累积的驱动力不会归零,所以误差为0时,积分项可以有恒定的驱动力.这个力就可以对抗系统的自发偏移(例如摩擦力)

D(微分项)

可以理解为斜率,当调控时期望达到稳态但是当前状态的趋势却是不断变大的,那么需要调控斜率来实现

微分项给系统增加阻尼,可以有效防止系统超调,尤其是惯性比较大的系统

Kd越大,微分项权重越大,系统阻尼越大,但系统卡顿现象也会随之增加

离散化实现

连续形式PID:
$$
out(t) = K_p \cdot error(t) + K_i \cdot \int_{0}^{t} error(\tau) d\tau + K_d \cdot \frac{d}{dt} error(t)
$$
离散形式PID
$$
out(k) = K_p \cdot error(k) + K_i \cdot T \sum_{j=0}^{k} error(j) + K_d \cdot \frac{error(k) - error(k-1)}{T}
$$
•若将T并入Ki 和Kd,则:
$$
out(k) = K_p \cdot error(k) + K_i’ \cdot \sum_{j=0}^{k} error(j) + K_d’ \cdot (error(k) - error(k-1))
$$

位置式和增量式

位置式PID:
$$
out(k) = K_p * error(k) + K_i * \sum_{j=0}^{k} error(j) + K_d * (error(k) - error(k-1))
$$
当k=k-1时:
$$
out(k-1) = K_p * error(k-1) + K_i * \sum_{j=0}^{k-1} error(j) + K_d * (error(k-1) - error(k-2))
$$
两式相减,得到增量式PID:
$$
∆out(k) = K_p * (error(k) - error(k-1)) + K_i * error(k) + K_d * (error(k) - 2*error(k-1) + error(k-2))
$$
区别:

比如阀门控制

位置式PID返回的是具体此次调控的值(全量),增量式PID反映的是相较于上一次增加了多少.比如2%…

程序实现框架

位置式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//pid.h
//定义变量
//目标值,实际值,输出值
//比例项,积分项,微分项的权重
//本次误差,上次误差,误差积分
//pid.c

//设定一个周期T,每隔一个T程序执行到这里一次
/*执行PID调控*/
//获取实际值--读取传感器--视觉OPENMV
//获取本次误差和上次误差
//误差积分(累加)
//PID计算
//输出限幅
//执行控制

增量式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//pid.h
//定义变量
//目标值,实际值,输出值
//比例项,积分项,微分项的权重
//本次误差,上次误差,上上次误差
//pid.c

//设定一个周期T,每隔一个T程序执行到这里一次
/*执行PID调控*/
//获取实际值--读取传感器--视觉OPENMV
//获取本次误差和上次误差,上上次误差
//PID计算
//输出限幅
//执行控制,看被控对象是否能直接接收并输出增量,如果不行就输出全量

改进措施

积分限幅

适用于位置式PID定速控制

要解决的问题:如果执行器因为卡住、断电、损坏等原因不能消除误差,则误差积分会无限制加大,进而达到深度饱和状态,此时PID控制器会持续输出最大的调控力,即使后续执行器恢复正常,PID控制器在短时间内也会维持最大的调控力,直到误差积分从深度饱和状态退出

积分限幅实现思路:对误差积分或积分项输出进行判断,如果幅值超过指定阈值,则进行限制

实现思路

1.单独对误差积分进行限幅

设置标志变量误差积分ErrorInt对这个变量施行判断误差积分的上下限的操作.超出限度分别设定对应的上下限.

2.对整个积分项输出进行限幅(IntOut替换原来的ErrorInt)

测定:看积分项输出占总输出的最大比例.(用SerialPlot)

积分分离

适用于位置式PID定位置控制

要解决的问题:积分项作用一般位于调控后期,用来消除持续的误差,调控前期一般误差较大且不需要积分项作用,如果此时仍然进行积分,则调控进行到后期时,积分项可能已经累积了过大的调控力,这会导致超调积分分离

实现思路:对误差大小进行判断,如果误差绝对值小于指定阈值,则加入积分项作用,反之,则直接将误差积分清零或不加入积分项作用

用于实际只用KD控制器的时候仍然有误差.误差比较小.

产生的原因是:

1.误差太小.误差*Kp是P项输出的力这个力太小而无法驱动电机转动.

2.固定位置的力并不是很强.施加力误差是持续产生的光依靠P项的输出的力不足以对抗外力.

如果此时再加上I项.就会超调.为什么前面的定速控制没有产生超调?用下面两张图详细对比.

实现思路:
1.判断误差决定是否直接给积分清零

2.判断误差决定是否加入积分的作用(另外定义标志变量)

变速积分

要解决的问题:如果积分分离阈值没有设定好,被控对象正好在阈值之外停下来,则此时控制器完全没有积分作用,误差不能消除

变速积分实现思路:变速积分是积分分离的升级版,变速积分需要设计一个函数值随误差绝对值增大而减小的函数,函数值作为调整系数,用于调整误差积分的速度或积分项作用的强度.简单来说就是误差越大,积分越弱,误差越小,积分越强并且这个变化是一个渐变的过程.

实现思路:
1.变速积分调整误差积分的速度(另外定义变量C,k设置变速衰减速度)

2.变速积分调整积分项作用的强度(另外定义变量C,k设置变速衰减速度)

微分先行

PID定位置控制

要解决的问题:普通PID的微分项对误差进行微分,当目标值大幅度跳变时,误差也会瞬间大幅度跳变,这会导致微分项突然输出一个很大的调控力,如果系统的目标值频繁大幅度切换,则此时的微分项不利于系统稳定.

具体来说:在目标值大幅度切换时,D项的输出有两个问题:

1.再切换的瞬间,极短时间内,D项输出突然由正的非常大变成负的非常大.瞬间出现又消失的非常大的正向调控力显然不太合适.

2.D项的设计初衷,是给系统加阻尼(阻碍系统实际值的变化)而如果要调控的实际值是逐渐变大的话阻尼的力应该是负向的才对.这样才能阻碍实际值变大

微分先行实现思路:将对误差的微分替换为对实际值的微分

理解:误差=目标值-实际值,当目标值=0时,误差直接=-实际值,目标值非0时,误差=-实际值+固定偏移

普通PID的微分项输出:
$$
\text{dout}(k) = K_d \cdot (\text{error}(k) - \text{error}(k-1))
$$
微分先行PID的微分项输出:
$$
\text{dout}(k) = -K_d \cdot (\text{actual}(k) - \text{actual}(k-1))
$$

不完全微分

适用于位置式PID控制

要解决的问题:传感器获取的实际值经常会受到噪声干扰,而PID控制器中的微分项对噪声最为敏感,这些噪声干扰可能会导致微分项输出抖动,进而影响系统性能

不完全微分实现思路:给微分项加入一阶惯性单元(低通滤波器)

普通PID的微分项输出:
$$
dout(k) = K_d \times (error(k) - error(k-1))
$$
•不完全微分PID的微分项输出:
$$
dout(k) = (1-\alpha) \times K_d \times (error(k) - error(k-1)) + \alpha \times dout(k-1)
$$
实现思路:

另外定义变量DifOut,α设置滤波强度

输出偏移

要解决的问题:对于一些启动需要一定力度的执行器,若输出值较小,执行器可能完全无动作,这可能会引起调控误差,同时会降低系统响应速度.比如PWM驱动电机旋转,正常情况下是PWM占空比越大,电机转速越快.但是当PWM占空比比较小时比如占空比只有1%,2%等,因为电机转动有摩擦力,所以这个比较小的PWM驱动力给到电机.电机可能就不会转动,对调控就有影响.

输出偏移实现思路:若输出值为0,则正常输出0,不进行调控;若输出值非0,则给输出值加一个固定偏移,跳过执行器无动作的阶段

输出偏移的PID输出值:
$$
\text{out}(k) =
\begin{cases}
0 & \text{if } \text{out}(k) = 0 \
\text{out}(k) + \text{offset} & \text{if } \text{out}(k) > 0 \
\text{out}(k) - \text{offset} & \text{if } \text{out}(k) < 0
\end{cases}
$$

输入死区

要解决的问题:在某些系统中,输入的目标值或实际值有微小的噪声波动,或者系统有一定的滞后,这些情况可能会导致执行器在误差很小时频繁调控,不能最终稳定下来

输入死区实现思路:若误差绝对值小于一个限度,则固定输出0,不进行调控

输入死区的PID输出值:
$$
\text{out}(k) =
\begin{cases}
0 & \text{if } |error(k)| < A \
\text{out}(k) & \text{if } |error(k)| \geq A
\end{cases}
$$

常见问题

极性相反

设置一个速度后,电机直接满速旋转不受控制—极性反了

驱动力给正,测得的速度是负的,PID极性反了之后,PID会认为,现在实际速度是负,目标速度是正,所以得加大输出使得实际速度回到正,但是极性反了继续加大输出只会让实际速度负的更厉害.速度负的更大后,PID就会更加加大正向力度的调控,更大的正向调控力又导致更大的负向速度.形成正反馈

解决方法:确保调控输出的函数和传感器输入的函数极性一致

KPID取值

Kp=目标值/实际输出的最大值

阈值处理HSV

双环PID

单环PID只能对被控对象的一个物理量进行闭环控制,而当用户需要对被控对象的多个维度物理量(例如:速度、位置、角度等)进行控制时,则需要多个PID控制环路,即多环PID,多个PID串级连接,因此也称作串级PID

多环PID相较于单环PID,功能上,可以实现对更多物理量的控制,性能上,可以使系统拥有更高的准确性、稳定性和响应速度

注意内环调控周期和外环调控周期的关系,一般情况是外环调控周期要大于等于内环调控周期.为什么?

1.如果外环周期小于内环周期,外环输出值刷新很快,而内环读取这个值很慢,那么这个外环输出值刷新这么快根本没有意义.

2.一般来说内环要控制反应更快的物理量,需要有更高的调控频率也就是更短的调控周期.

核心:外环PID的输出值作用于内环PID的目标值

模糊控制PID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//pid.c
#include "fuzzy_pid.h"

// 模糊PID控制器初始化
void fuzzy_pid_init(fuzzy_pid_type_def* pid, uint8_t mode,
const float base_PID[3], float max_out, float max_iout,
float e_range, float ec_range) {
// 参数校验
if (pid == NULL || base_PID == NULL || e_range <= 0 || ec_range <= 0)
return;

// 设置基础PID参数
pid->base_kp = base_PID[0];
pid->base_ki = base_PID[1];
pid->base_kd = base_PID[2];
pid->kp = base_PID[0];
pid->ki = base_PID[1];
pid->kd = base_PID[2];

pid->mode = mode;
pid->max_out = max_out;
pid->max_iout = max_iout;
pid->e_range = e_range;
pid->ec_range = ec_range;

// 初始化状态变量
fuzzy_pid_reset(pid);

/*************************************************
* 模糊规则表初始化(可根据实际系统调整)
* 规则原理:
* 1. 当误差较大时,增大Kp、减小Kd,避免超调
* 2. 当误差较小时,增大Kd、减小Kp,避免振荡
* 3. Ki随误差变化适当调整
*************************************************/

// Kp规则表 (用于调整比例增益)
// 行: 误差变化率ec(列) | 列: 误差e(行)
float kp_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE] = {
/* e\ec NB NM NS ZO PS PM PB */
/* NB */{0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9},
/* NM */{0.5, 0.55, 0.65, 0.7, 0.75, 0.8, 0.85},
/* NS */{0.4, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95},
/* ZO */{0.3, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85},
/* PS */{0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.95},
/* PM */{0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75},
/* PB */{0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4}
};

// Ki规则表 (用于调整积分增益)
float ki_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE] = {
/* e\ec NB NM NS ZO PS PM PB */
/* NB */{0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11},
/* NM */{0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10},
/* NS */{0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09},
/* ZO */{0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08},
/* PS */{0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09},
/* PM */{0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.10},
/* PB */{0.05, 0.06, 0.07, 0.08, 0.09, 0.10, 0.11}
};

// Kd规则表 (用于调整微分增益)
float kd_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE] = {
/* e\ec NB NM NS ZO PS PM PB */
/* NB */{1.4, 1.3, 1.2, 1.1, 1.0, 0.9, 0.8},
/* NM */{1.3, 1.2, 1.1, 1.0, 0.9, 0.8, 0.7},
/* NS */{1.2, 1.1, 1.0, 0.9, 0.8, 0.7, 0.6},
/* ZO */{1.1, 1.0, 0.9, 0.8, 0.7, 0.6, 0.5},
/* PS */{1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4},
/* PM */{0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3},
/* PB */{0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2}
};

// 复制规则表
memcpy(pid->kp_rules, kp_rules, sizeof(kp_rules));
memcpy(pid->ki_rules, ki_rules, sizeof(ki_rules));
memcpy(pid->kd_rules, kd_rules, sizeof(kd_rules));
}

// 重置PID内部状态
void fuzzy_pid_reset(fuzzy_pid_type_def* pid) {
pid->error[0] = pid->error[1] = pid->error[2] = 0.0f;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->out = pid->Pout = pid->Iout = pid->Dout = 0.0f;
pid->measure = pid->set = 0.0f;

// 重置PID参数为初始值
pid->kp = pid->base_kp;
pid->ki = pid->base_ki;
pid->kd = pid->base_kd;
}

// 限幅函数(双向对称限幅)
void limit_max(float* num, float max) {
if (max <= 0) return;

if (*num > max) *num = max;
else if (*num < -max) *num = -max;
}

// 模糊PID计算函数
float fuzzy_pid_calc(fuzzy_pid_type_def* pid, float measure, float set) {
if (pid == NULL) return 0.0f;

// 更新历史误差
pid->error[2] = pid->error[1]; // 上上次误差
pid->error[1] = pid->error[0]; // 上一次误差

// 设置当前值和目标值
pid->measure = measure;
pid->set = set;
pid->error[0] = set - measure; // 当前误差

// 计算误差变化率 (ec = de/dt ≈ Δe/Δt)
// 假设采样时间固定(Δt=1),则ec = e(k) - e(k-1)
float ec = pid->error[0] - pid->error[1];

// 模糊更新PID参数
fuzzy_pid_update_params(pid, pid->error[0], ec);

// 进行PID计算
switch (pid->mode) {
case PID_POSITION: // 位置式PID
pid->Pout = pid->kp * pid->error[0]; // 比例项
pid->Iout += pid->ki * pid->error[0]; // 积分项(累加)

// 更新微分历史(前移)
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
pid->Dbuf[0] = pid->error[0] - pid->error[1]; // 一阶差分
pid->Dout = pid->kd * pid->Dbuf[0]; // 微分项

// 积分限幅
limit_max(&pid->Iout, pid->max_iout);

// 总输出
pid->out = pid->Pout + pid->Iout + pid->Dout;
limit_max(&pid->out, pid->max_out);
break;

case PID_DELTA: // 增量式PID
pid->Pout = pid->kp * (pid->error[0] - pid->error[1]);
pid->Iout = pid->ki * pid->error[0];

// 更新微分历史(前移)
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
// 二阶差分 = e(k) - 2e(k-1) + e(k-2)
pid->Dbuf[0] = pid->error[0] - 2.0f * pid->error[1] + pid->error[2];
pid->Dout = pid->kd * pid->Dbuf[0]; // 微分项

// 输出增量
pid->out += pid->Pout + pid->Iout + pid->Dout;
limit_max(&pid->out, pid->max_out);
break;
}
return pid->out;
}

// 模糊推理更新PID参数
void fuzzy_pid_update_params(fuzzy_pid_type_def* pid, float e, float ec) {
// 参数校验
if (pid == NULL) return;

// 使用模糊推理计算参数调整系数
float kp_factor = fuzzy_inference(e, ec, pid->kp_rules, pid->e_range, pid->ec_range);
float ki_factor = fuzzy_inference(e, ec, pid->ki_rules, pid->e_range, pid->ec_range);
float kd_factor = fuzzy_inference(e, ec, pid->kd_rules, pid->e_range, pid->ec_range);

// 应用调整系数更新PID参数
pid->kp = pid->base_kp * kp_factor;
pid->ki = pid->base_ki * ki_factor;
pid->kd = pid->base_kd * kd_factor;

// 确保参数合理性
if (pid->kp < 0.1f) pid->kp = 0.1f;
if (pid->ki < 0.01f) pid->ki = 0.01f;
if (pid->kd < 0.01f) pid->kd = 0.01f;
}

// 模糊推理函数(使用加权平均法解模糊)
float fuzzy_inference(float e, float ec, float rule[][FUZZY_SET_SIZE],
float e_range, float ec_range) {
// 归一化误差和误差变化率到[-3,3]区间
float norm_e = 3.0f * e / e_range;
float norm_ec = 3.0f * ec / ec_range;

// 边界检查
if (norm_e > 3.0f) norm_e = 3.0f;
else if (norm_e < -3.0f) norm_e = -3.0f;
if (norm_ec > 3.0f) norm_ec = 3.0f;
else if (norm_ec < -3.0f) norm_ec = -3.0f;

// 计算隶属度(使用三角形隶属函数)
float e_degree[FUZZY_SET_SIZE] = {0};
float ec_degree[FUZZY_SET_SIZE] = {0};

// 计算e的隶属度
for (int i = 0; i < FUZZY_SET_SIZE; i++) {
float center = i - 3; // [-3, -2, -1, 0, 1, 2, 3]

if (norm_e <= center - 1 || norm_e >= center + 1) {
e_degree[i] = 0.0f;
} else if (norm_e > center - 1 && norm_e <= center) {
e_degree[i] = norm_e - (center - 1);
} else {
e_degree[i] = (center + 1) - norm_e;
}
}

// 计算ec的隶属度
for (int i = 0; i < FUZZY_SET_SIZE; i++) {
float center = i - 3;

if (norm_ec <= center - 1 || norm_ec >= center + 1) {
ec_degree[i] = 0.0f;
} else if (norm_ec > center - 1 && norm_ec <= center) {
ec_degree[i] = norm_ec - (center - 1);
} else {
ec_degree[i] = (center + 1) - norm_ec;
}
}

// 模糊推理 - 使用Mamdani极大极小法
float output = 0.0f;
float weight_sum = 0.0f;

for (int i = 0; i < FUZZY_SET_SIZE; i++) {
for (int j = 0; j < FUZZY_SET_SIZE; j++) {
// 规则激活强度(取最小值)
float activation = fmin(e_degree[i], ec_degree[j]);

if (activation > 0) {
// 规则对应的输出值
float rule_output = rule[i][j];
// 加权求和
output += activation * rule_output;
weight_sum += activation;
}
}
}

// 解模糊(加权平均)
if (weight_sum > 0.001f) {
return output / weight_sum;
}
return 1.0f; // 默认不调整
}

// 清理PID控制器状态
void fuzzy_pid_clean(fuzzy_pid_type_def* pid) {
if (pid == NULL) return;
pid->error[0] = pid->error[1] = pid->error[2] = 0.0f;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->out = pid->Pout = pid->Iout = pid->Dout = 0.0f;
pid->measure = pid->set = 0.0f;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//pid.h
#ifndef _FUZZY_PID_H
#define _FUZZY_PID_H

#include "main.h"

// PID控制模式枚举
enum PID_MODE {
PID_POSITION = 0, // 位置式PID
PID_DELTA = 1 // 增量式PID
};

// 模糊集枚举
enum FUZZY_SETS {
NB = 0, // 负大(Negative Big)
NM, // 负中(Negative Medium)
NS, // 负小(Negative Small)
ZO, // 零(Zero)
PS, // 正小(Positive Small)
PM, // 正中(Positive Medium)
PB, // 正大(Positive Big)
FUZZY_SET_SIZE
};

// 模糊PID控制器结构体
typedef struct {
// 基础PID参数
uint8_t mode; // PID控制模式
float base_kp, base_ki, base_kd; // 基础PID参数

// 模糊控制参数
float e_range; // 误差的论域范围 [-e_range, e_range]
float ec_range; // 误差变化率的论域范围 [-ec_range, ec_range]

// 当前PID参数(模糊调整后)
float kp, ki, kd;

// 限幅参数
float max_out;
float max_iout;

// 状态变量
float measure;
float set;
float out, Pout, Iout, Dout;
float Dbuf[3]; // 微分项历史
float error[3]; // 误差历史 [0:当前, 1:上一次, 2:上上次]

// 模糊规则表
float kp_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE];
float ki_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE];
float kd_rules[FUZZY_SET_SIZE][FUZZY_SET_SIZE];
} fuzzy_pid_type_def;

// 函数声明
void fuzzy_pid_init(fuzzy_pid_type_def* pid, uint8_t mode,
const float base_PID[3], float max_out, float max_iout,
float e_range, float ec_range);
void limit_max(float* num, float max);
float fuzzy_pid_calc(fuzzy_pid_type_def* pid, float measure, float set);
void fuzzy_pid_clean(fuzzy_pid_type_def* pid);
void fuzzy_pid_reset(fuzzy_pid_type_def* pid);
void fuzzy_pid_update_params(fuzzy_pid_type_def* pid, float e, float ec);
float fuzzy_inference(float e, float ec, float rule[][FUZZY_SET_SIZE],
float e_range, float ec_range);
#endif

MPU-6050

原理

以陀螺仪的正面建立空间直角坐标系,以正面丝印右手定则确定x,y,z轴.

在MPU-6050中又分为两大传感器:加速度计和陀螺仪

加速度计是测量x,y,z上的加速度.(但是光凭借这个只能测静止的时候)这时候就要引入陀螺仪

陀螺仪的三个量:
gx–绕x轴旋转的角速度

gy–绕y轴旋转的角速度

gz–绕z轴旋转的角速度

为什么要要引入?在物体运动的过程中.由于克里奥利力会有一个角速度这时候原来要测的位置的就会发生偏转.这时候我们就需要陀螺仪去测角速度.

寄存器读写

最基础:M0速成课,控制电机,陀螺仪读取(pwn).

I2C读取,TOF传感器.

步进(无刷)电机控制

蓝牙模块

编码器读取

OELD显示,蜂鸣器.

板子.