快速上手STM32外设
一、跑马灯
int main(void)
{
delay_init();
LED_Init(); // 需要初始化用到的模块(delay,LED)
while(1) {
GPIO_ResetBits(GPIOC,GPIO_Pin_13);
delay_ms(100);
GPIO_SetBits(GPIOC,GPIO_Pin_13);
delay_ms(100);
}
}
GPIO_ResetBits置零(低电平),GPIO_SetBits置一(高电平) 控制LED亮灭
go to definition到LED_Init内部一下
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能总线时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; // 初始化PC13引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 设置推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
// 设置IO口速度GPIO_Speed_50MHz(一般默认50MHz)
GPIO_Init(GPIOC, &GPIO_InitStructure); // 把配置好的变量输入进去
以上是对GPIO_InitTypeDef结构体的初始化设置
在led.c页面,在页面空白处右键,选择“toggle header”,进入led.h
可写一个宏定义:#define LED PCout(13) PCout(13)=1; 这句就完全等于GPIO_SetBits(GPIOC,GPIO_Pin_13); PCout(13)=0; 这句就完全等于GPIO_ResetBits(GPIOC,GPIO_Pin_13);
这样宏定义之后,控制PC13的电平就可以写得更简便
头文件中
#ifndef __LED_H
#define __LED_H
… …
#endif
这一段是固定的,照着写即可。
二、按键
int main(void)
{
u8 a;
delay_init();
LED_Init();
KEY_Init(); //按键初始化
while(1)
{
a=KEY_Scan(0);
if(a==1)
{
LED=0;
}
else if(a==2)
{
LED=1;
}
}
}
变量a被KEY_Scan函数的返回值赋了个值,如果是a=1就让灯亮,a=2灯灭,逻辑分析结束。
- key.c: ![[屏幕截图 2025-03-30 170039.png|500]] 使能了PB4和PB5两个GPIO口,但GPIO模式配置成了GPIO_Mode_IPU上拉输入模式
上拉输入的IO口在外部接这个IO口的电路(外部输入电路)为低电平时,IO口可以测到明确的低电平
上拉输入的IO口在外部接这个IO口的电路(外部输入电路)为高电平时,IO口可以测到明确的高电平
上拉输入的IO口在外部接这个IO口的电路(外部输入电路)为高阻态时,IO口可以测到明确的高电平
- key.h: ![[屏幕截图 2025-03-30 170719.png|500]]
GPIO_ReadInputDataBit( ) 这个函数就是检测IO口电平的核心函数,比如检测B4口的电平是高还是低:
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_4) == 1)就可以简化为: if(KEY1 == 1)
并且GPIO_ReadInputDataBit函数还能进一步简化: 就像第一节的PCout(13)一样: int a; a=PBin(4); 这个a如果为1那PB4就是高电平,0就是低电平。
KEY_Scan 函数
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;
if(mode) key_up=1; // mode=0,不支持长按
if(key_up && (KEY1==0 || KEY2==0)) //这句就在判断是否有任何按键被按下:没 有按键被按下前,变量key_up一直都是1,所以符合条件
{
delay_ms(10); //消抖延时
if(KEY1==0)
{
key_up = 0;
return KEY1_PRESS;
}
else if(KEY2==0)
{
key_up = 0;
return KEY2_PRESS;
}
}
if(KEY1==1 && KEY2==1)
{
key_up = 1;
}
return 0;
}
三、如何自己添加代码文件
- 创建相应模块文件夹,编写.h文件和.c文件代码(可复制粘贴)
- 配置编译器:点界面上方三色块,添加.c源文件; 点魔术棒 - C/C++ - Include Paths,添加.h头文件
- 在main函数添加新的头文件(#include "key.h")
四、定时器中断
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init();
LED_Init();
TIM3_Init(20000-1,720-1);
while(1)
{
delay_ms(100);
}
}
STM32的每个中断都有自己的抢占优先级和子优先级 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 设置中断优先级分组为分组2
定时器: TIM2,TIM3,TIM4这三个是通用定时器,TIM1是高级定时器。 可以认为定时器内部有个计数器,初始值是0,每过1ms,计数器+1,一直过了99ms,这时,定时器的计数器的值是99;紧接着到第100ms,计数器数到了100,但是计数器的值不是100,而是清零,然后继续从0开始数,同时触发定时器中断。
tim.c: ![[屏幕截图 2025-03-30 225527.png|575]] TIM3_IRQHandler函数 —— 中断服务函数(函数名是不能改的,可以在里面运行各种代码) TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; 上面这两句分别是定时器配置结构体和中断配置结构体
TIM_GetITStatus函数,获取定时器相关中断的状态。 当一个中断状态是RESET时证明这个中断还没准备好触发; 当一个中断状态是SET时证明这个中断已经准备好触发;
自动重装载值arr & 预分频系数psc
- psc 作用:对总线时钟的频率进行分频,分频之后的频率就是定时器计数频率
STM32F103C8T6最小系统板的STM32的系统总线时钟的频率是72Mhz,也就是72,000,000 Hz,这个数值要记住!
定时器的psc参数对系统总线时钟频率分频,psc=720 – 1,那意思就是720分频,也就是分频后的频率是之前的1/720,如下:
\frac{{72000000Hz}}{{720}} = 100000Hpsc=720 – 1,-1是内部寄存器规定的:0是不分频,1是二分频,2是三分频,依次类推
- arr 作用:规定“数到多少就触发定时器更新中断” 例:规定让计数器从0数到100,1ms数一次。这个“100”就是arr,arr=100。
所以自动重装载值arr的作用就是这个的计数器要记到多少才能进一次中断 arr=20000,那就是计数器计两万次才进一次中断,也就是0.00001 * 20000=0.2秒进一次中断。
![[屏幕截图 2025-03-30 231312.png|575]]
定时器的计数器随着时间计数,然后计数器达到arr值(TIMx_ARR)时,触发定时器更新中断,然后计数器清零继续重新计数。
五、STM32标准库
STM32F10x_FWLib 文件夹即为STM32标准库的库函数 “stm32f10x_”这个前缀,代表这套库函数可用于STM32F1的所有系列
“stm32f10x_gpio.c”
“stm32f10x_gpio.c”就是操作gpio相关的库函数,比如之前用的: ![[屏幕截图 2025-03-31 230921.png|625]] 这些凡是操作GPIO的函数全是。 因为我们go to definition以上的任意一个函数,都会跳转到“stm32f10x_gpio.c”里去。
“stm32f10x_rcc.c”
![[屏幕截图 2025-03-31 231027.png]]
这句就是它的函数,RCC分配外设时钟。
“stm32f10x_usart.c”
串口相关的库函数
“misc.c”
Misc是Miscellaneous(各种各样的)的缩写,杂项文件,里面主要装的是NVIC相关文件。也就是中断优先级分配相关,以后做项目几乎必用中断,所以提前加。
- 以用到单片机的什么外设就加什么库的原则,这样可以节省单片机的内存。
六、PWM控制舵机
PWM:
新的参数:TIMx_CCRx CCR寄存器的全称是CCR(Capture/Compare Register)的缩写,即:捕获/比较寄存器。
![[屏幕截图 2025-03-31 232005.png|575]]
**PWM的频率与arr和psc有关**
CNT<CCR时,PWM是高电平; CNT>CCR时,PWM是低电平 CCR寄存器有比较的功能,CCR寄存器确定了PWM的占空比。占空比英文是“duty”:
duty = \frac{{TIM\_CCRx}}{{TIM\_ARR}}舵机: 一般的舵机(特指模拟舵机)有三根线,VCC,GND和SIG(signal信号) 分别是电源线,地线和信号线,信号线需要接具有定时器PWM输出通道的引脚(复用)
代码: ![[屏幕截图 2025-04-01 143327.png|500]]
TIM1_PWM_Init:
先看TIM1_PWM_Init: 两个形参是arr自动重装载值和psc预分频系数 舵机的PWM周期必须为20ms(50Hz),所以先配置arr和psc这两个参数,让PWM的周期正好是20ms。 设置自动重装载值arr=20000,预分频系数psc=72 则定时器自动装载一次的周期为:1/(72000000/72) * 20000 = 0.02s = 20ms,符合要求。注意这里,arr设置得越大,PWM占空比的精度越高,因为:
duty = \frac{{TIM_CCRx}}{{TIM_ARR}}- 多了新结构体TIM_OCInitTypeDef TIM_OCInitStructure TIM_OCInitTypeDef 是定时器输出通道配置结构体,带OC字样的都是其相关的配置,OC(Output Channel),输出通道。
看代码,首先: ![[屏幕截图 2025-04-01 144711.png|625]] 三个结构体:GPIO配置、定时器配置、定时器输出通道配置。 两个RCC外设时钟使能:使能TIM1和GPIOA的外设。TIM1和GPIOA都在APB2总线上
接着相应的IO使能: ![[屏幕截图 2025-04-01 144943.png|600]]
A8~A11,TIM1的四个PWM通道。 GPIO_Mode_AF_PP==复用推挽输出模式
推挽输出和复用推挽输出的不同在于: 推挽输出是GPIO自己通过函数控制电平高低,比如PCout(13)=1; 复用推挽输出是GPIO把电平高低的控制权给了外设,这里就是给了TIM1,GPIO不管了,电平高还是低让TIM1自己去控制。
定时器的参数配置
输出通道参数配置 TIM_OCMode,配置为PWM模式1:TIM_OCMode_PWM1 PWM模式1:递增计数时,当TIMx_CNT(计数值)<TIMx_CCR(捕获/比较值)时,输出为有效电平,否则为无效电平。 PWM模式2:递增计数时,当TIMx_CNT(计数值)< TIMx_CCR(捕获/比较值)时,输出为无效电平,否则为有效电平。 ![[屏幕截图 2025-04-02 134915.png|525]] 输出极性高:有效电平为高电平 输出极性低:有效电平为低电平 实际输出的电平是由输出极性和电平是否有效共同决定的
TIM_OutputState是输出模式是否开启,开启才能输出PWM,填TIM_OutputState_Enable。
TIM_Pulse,意思是TIM_CCRx的初始值,一般都统一设为0。
然后四句就是输出通道1~4的配置参数上传函数。
![[屏幕截图 2025-04-02 135424.png|575]]
以上是可以不管的,写上即可
最后一句TIM_Cmd(TIM1, ENABLE),开启定时器
TIM_SetComparex
![[屏幕截图 2025-04-02 140019.png|575]] TIM_SetCompare1(TIM1,500)的意思就是设置定时器1的通道1的CCR值为500。 TIM_SetCompare2(TIM1,500)的意思就是设置定时器1的通道2的CCR值为500。
占空比公式:
duty = \frac{{TIM_CCRx}}{{TIM_ARR}}2025.4.2,还是没能明白占空比与舵机角度之间的对应关系,明白了回来还愿
七、仿真&实物debug
看教程即可。
八、OLED显示
先看代码: ![[屏幕截图 2025-04-02 162732.png|600]]
OLED_Clear 作用:清屏
OLED_ShowString(x,y,内容,字体大小),xy代表坐标值 x的范围(0,127) y的范围: 如果字体高度为8,行数的范围是0到7。 如果文字高度为16,那只能塞下四行,对应的行数就只有0,2,4,6这四个值。 内容:注意(u8 * ) 字体大小:8 / 16
OLED_Init函数 ![[屏幕截图 2025-04-02 163421.png|625]]
PA6,PA7接口可以随意更换:GPIO初始化换成任意其他的两个IO后,在oled.h更改宏定义
sprintf函数
利用sprintf可以轻松处理一个的字符串,让一些变量值插入字符串。
sprintf(text,"you have %d yuan!", your_money);
此函数的作用就是将your_money变量的值插入字符串,并且将加工好的字符串赋值给text变量,text[20]是一个字符串。
之后就可以使用OLED屏幕显示信息了
九、红外寻迹传感器
- 红外传感器原理: 红外发射管不断发射红外线,红外接收管不断接收是否有红外线反射回来。 当红外线没有被反射回来,红外接收管关断,此时模块的输出端为低电平。 当红外线被反射回来,红外接收管导通,此时对应的输出端就是高电平。
- 即: 如果某个传感器检测到白色,那就输出高电平“1”; 如果某个传感器检测到黑色,那就输出低电平“0”;
看代码: ![[屏幕截图 2025-04-02 231445.png|525]] extern:外部调用,定义一个全局变量 不在任何一个函数里,添加一句:extern int track_error;
- track_Init: ![[屏幕截图 2025-04-02 231924.png|650]]
初始化了五个GPIO,浮空输入,用于检测循迹传感器的OUT1-OUT5。
浮空输入完全取决于外部输入信号,外部输入是1那就是1,外部输入是0就是0,外部输入是高阻态那就电平不确定。
- track.h: ![[屏幕截图 2025-04-02 232117.png|600]]
GPIO_ReadInputDataBit的作用就是读取一个IO口电平的高低,高电平即1,低电平即0
##define OUT1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_4) ##define OUT1 PAin(4) 以上两句是功能完全一样的,两种不同写法。
十、超声波测距 & 定时器计时
*1. 会使用超声波测距 2. 会使用定时器进行计时 3. 提升C语言算法功底
HC-SR04
VCC:电源线,3-5.5V宽电压输入 GND:地线,连接单片机的接地(GND) TRIG:触发控制信号输入 ECHO:回响信号输出
- 工作原理: (1)TRIG口 触发测距,给TRIG口最少 10us 的高电平。 (2)模块内部自动发送 8 个 40khz 的方波,自动检测是否有声波返回;(这一步我们了解即可,这是超声波传感器内部在干的事,无需我们操作) (3)若有信号返回,经过模块内部运算后,通过 ECHO口 输出一个高电平,高电平持续的时间就是超声波从发射到返回的时间。
测量距离=(高电平时间 声速)/2*
所以只需要:
给TRIG口一个10us以上的高电平,去触发测量任务。 把随便一个IO口搞成推挽输出,接到TRIG上去,然后电平置高,delay_us(11),延时十微秒以上即可
传感器的测量任务进行完之后,ECHO会发出一段时间的高电平,这段高电平的持续时间经过换算就能得到待测距离。 需要测量高电平持续时间 1. 测量高电平: 使用GPIO_ReadInputDataBit函数不断检测ECHO口电平,一旦电平变化就开始计时。 2. 测量高电平的持续时间 在高电平到来瞬间打开定时器开始计数,等到高电平结束关闭定时器。 CNT清零的同时会进定时器更新中断,我们可以在定时器中断里记录,每进一次定时器中断,记录清零次数+1。 再就是把ARR的值设置得尽量大一点,降低定时器更新中断的触发频率,比如arr=10和arr=65535,如果arr=10,程序会不停进入定时器更新中断以至于大量资源被占用,main函数while(1)的东西可能都执行不了了。
看代码:
- main: ![[屏幕截图 2025-04-03 232633.png|600]]
选用定时器4。 psc=0=1-1,也就是计数频率是72MHz,不分频,为了让计数器的计数精度足够高 arr=65535,arr能取到的最大值就是65535。
- HC_SR04_Init: ![[屏幕截图 2025-04-03 232459.png|650]]
Trig口推挽,echo口浮空 置低trig口,保持低电平,因为trig置高是开始测距的信号
- Senor_Using ![[屏幕截图 2025-04-08 223418.png|625]]
第一句 extern int overcount 来自TIM4的中断服务函数,代表TIM4_CNT清零的次数。
while1 中: 前三句用于操作trig口触发超声波开始测距 while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_12) RESET);== 只要PA12是低电平,循环就一直进行 也就是PA12(ECHO)一旦变成高电平,马上跳出这个循环 然后给相关变量清零: overcount=0; TIM4->CNT=0(给TIM4的计数器清零) 再TIM_Cmd(TIM4,ENABLE);开启定时器,定时器计数器就会开始递增,CNT到最大值清零并触发中断,中断内通过overcount记录清零次数。
再看后半段程序: 变量i记录第几次测距,测五次取平均值(均值滤波) while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_12) SET);== 一旦检测到echo口变低,说明返回的超声波已经接收到了,就跳出循环 TIM_Cmd(TIM4,DISABLE);//关闭定时器,计数器也会停止计数。 使用TIM_GetCounter函数得到定时器4计数器目前的值,这句代码等同于:tim=TIM4->CNT;
length计算公式: tim+overcount * 65536就是定时器这段时间总共计了多少个数 高电平时间=总计数值 * 计数周期= (tim+overcount * 65536) * ( 1/72000000)
十一、直流有刷电机控制
L298N
L298N芯片内部集成有两个H桥电机驱动电路 ![[屏幕截图 2025-04-10 081014.png|500]] 中间这个圆圈代表直流电机。 已知S8050型号的三极管是NPN型三极管,S8550型号的三极管是PNP型三极管。 (对于NPN型三极管,IN端给高电平开关导通;给低电平开关关断。PNP型三极管完全相反)
- 分析: 当IN1=1,IN2=0时: Q1,Q4导通,Q2,Q3关断,电流流向如下图,定义此时电机正转: ![[屏幕截图 2025-04-10 081425.png|500]]
当IN1=0,IN2=1时:电机电流与上图方向相反,所以电机反转。 ![[屏幕截图 2025-04-10 081530.png|500]]
控制转速: 把IN1和IN2换成两路PWM控制 比如当IN1占空比是0,IN2占空比是100%时,电机全速转 但是当IN1占空比是0,IN2占空比是50%时,电机以全速的1/2转
L298N引脚 ![[屏幕截图 2025-04-10 081913.png|500]]
‘’通道A使能”、“通道B使能”、“板载5V使能”三个跳线帽都不能拔掉 电机A/B通道无法使能的话,对应通道的电机就不会转; 板载5V跳线帽拔掉,5V输出就没有了 用12V锂电池供电
- L298N电路 ![[屏幕截图 2025-04-10 082354.png|500]]
右边蓝色的八个二极管作用: 四个钳位电路,将连接电机输出所有通道的电压值钳位在0-Vss(0-12V之间) 因为电机是一种特别容易产生干扰的电气设备,电动机反过来就是发电机,如果不通电,手摇电机,就会产生不小的反电动势;如果发生电机急停等,电路电压就会产生很大的尖峰等干扰,电压一下会波动高于12V,如果不做钳位,可能就会对L2998N芯片等产生损害。
- 代码分析: ![[屏幕截图 2025-04-10 082656.png|500]]
使输出PWM赫兹为24kHz(芯片推荐的PWM频率)
注意不同的电机有不同的额定电压
额定电压是12V的电机,用12V电池供电,那PWM占空比就可以开到100%,让电机两端电压在12V。
但是如果电机额定电压是8V,大于8V会烧坏电机。所以如果还是使用12V电池供电,PWM控制占空比最大只能开到66.6%。
十二、电机编码器测速
编码器
首先我们要知道,电机是电机,编码器是编码器,这两个东西是各自独立的 13线霍尔款编码器原理如下图: ![[屏幕截图 2025-04-10 083844.png|500]]
- 上图这个刻度盘安装在电机转轴上,随着电机一起旋转;
- 刻度盘上有一条条等间距的缝隙;
- 右边是个小红外灯(发光元件),一直向左边的光敏元件发射激光,但是刻度盘会把光挡住,激光只能穿过刻度盘的一条条缝隙射给光敏元件,如下图: ![[屏幕截图 2025-04-10 084120.png|500]] 刻度盘一直转,光敏元件就能得到一个循环被照和不被照的类似PWM的信号,得到一个如下图A相那样的波: ![[屏幕截图 2025-04-10 084245.png|500]] 如果说刻度盘上共有25个缝隙。那么我们可以统计一段时间内A波形所有高电平的数量来得出刻度盘一共转了多少个缝隙,也就是多少个刻度。并且能算出刻度盘转到了什么位置。 比如我们测得,10秒内,高电平共来了67个,那么也就是这个电机十秒转了67/25=2.68转,也就是十秒内的平均转速是0.268r/s
这样我们就实现编码器测速了。
- 但是这样有个问题,不知道盘转的方向,正转反转都是这样一个波!而且如果电机就来回正反转打哆嗦,这样始终就是同一个缝隙来回被光穿过,这样好像也能得到A波,这咋办呢?
解决方法就是把刻度盘的内圈再刻上一圈与原缝隙错开90°的缝隙,然后再拿一个光敏元件和发光元件!如下图:![[屏幕截图 2025-04-10 084644.png|500]]这样就会得到如下两条锯齿波: ![[屏幕截图 2025-04-10 084733.png|500]]
A和B两组波形相位差是90º,可以按照下图判断出旋转方向: ![[屏幕截图 2025-04-10 084827.png|500]] 并且再进一步:通过观察上面AB相的波我们发现,圆盘向某个方向一直旋转,AB两相就是这四个状态周而复始。 ![[屏幕截图 2025-04-10 085148.png|500]]
- 由于该编码器为13线,也就是线数为13ppr,也就是刻度盘上每相都有13个缝隙; 电机每转一圈,A或B相的脉冲数都是13;也就是电机转一圈A相和B相就分别有13个脉冲。 而这个电机的减速器减速比为1:30: 也就是电机转30圈,输出转轴转一圈;编码器轴是连接在里面的电机轴上的,不是接在输出转轴上的。 综合编码器线数和减速器减速比,得出最外面的电机输出转轴转1圈,里面的霍尔编码器A或B相会有13 * 90 = 390 个脉冲。
- 看代码: ![[屏幕截图 2025-04-10 085540.png|525]]
使用了三个定时器
先看TIM4_ENCODE_Init: 编码器正交解码用的定时器4。 多了一个IC结构体。(input channel, 输出通道)![[屏幕截图 2025-04-10 085754.png|500]]![[屏幕截图 2025-04-10 085759.png|750]] 以上代码只需要复制黏贴就好了,除了第一句函数的第一个参数 第一句是一个非常重要的库函数:编码器模式设置函数。 TIM_EncoderMode_TI1代表仅TI1计数; TIM_EncoderMode_TI2代表仅TI2计数; TIM_EncoderMode_TI12代表TI1 TI2都计数。 TI1就是输入通道1,就是PB6,就是编码器A相; TI2就是输入通道2,就是PB7,就是编码器B相。 这个参数的意思具体看下面这两个图:![[屏幕截图 2025-04-10 090236.png|550]] 我们使用的模式就是TI12都计数,也就是:![[屏幕截图 2025-04-10 090418.png|275]]观察上面三个图可知,只有正确的连续四个AB状态才会成功进行下一次计数, 一旦有一个毛刺,发现顺序不对了就退回到上次顺序正确时候的计数值。 —————————————————————————————————————————— 再来看定时器的配置:![[屏幕截图 2025-04-10 090609.png|575]] 为了保证最高的采样精度,psc不分频,72MHz。 arr的值是综合了编码器线数、减速器减速比和通道采样方式的一个值: 13ppr的编码器,1:30的减速器下,电机外转轴转一圈A或B相会有390个脉冲,但并不是A或B相来一个脉冲计数器计一次数,我们设置了TI12都计数,那就是每个AB相的正交脉冲组合都算作一次计数:四个组合都算作计一次数,也就是全部的arr需要390 * 4=1560。 最后得出电机输出转轴转一圈会有1560个状态,定时器的计数器计1560个数。
定时器4的中断服务函数: ![[屏幕截图 2025-04-10 090908.png|550]] 每当更新中断(计数器溢出)来临,要先判断定时器是向上溢出还是向下溢出: “TIM4->CR1>>4&0x01”这一堆就是调用了定时器4相关寄存器的相应位, 结果=0就是向上溢出,结果=1就是向下溢出。 向上溢出就是计数器计数到了1557、1558、1559,然后清零,0。 向下溢出就是4,3,2,1,0,然后CNT=arr,1559,1558,1557。
定时器3的中断服务函数: ![[屏幕截图 2025-04-10 091205.png|700]] speed就是速度,单位是转每秒 TIM4->CNT的值就是定时器4计数器当前时刻的值,范围是0-1559。 Last_num变量存着上次采样时TIM4->CNT的值,last_count存着上次采样的圈数,这次的和上次的一减,就是△X,△X/△t就是速度,△t是通过定时器3的psc和arr决定的,我们配置的是0.1秒,△t=0.1s。
用到的三个定时器都是用作了不同的功能: 定时器1用于生成PWM控制电机。 定时器3用于多线程任务,计算电机转速。 定时器4用于编码器正交解码。
十三、电机PID闭环控制
- 先看代码: ![[屏幕截图 2025-04-23 172219 1.png|550]] 定时器1:生成PWM 定时器2:生成0.1s的定时器中断PID计算 定时器3:生成0.1s的定时器中断电机编码器测速 定时器4:正交解码
PID算法的原理和用法
PID(Proportional-Integral-Derivative)(比例-积分-微分)算法是一种常用的反馈控制算法,广泛应用于自动控制系统中。它通过测量系统的误差(目标值与实际值之间的差异)来调整控制器的输出,以使系统的响应满足特定的要求。
比例项P
PID(位置式):
u = P*e + I*\int e + D*\mathop e\limits^ \bullet比例控制P越大,接近目标值的速度越快,但是提升控制速度的代价是超调和震荡会随着P的增大而增大;P过大会使系统失稳。 ![[屏幕截图 2025-04-23 174358.png|425]]
微分项D
这时候就要引入D了,微分:
u = P*e + D*\mathop e\limits^ \bulletu = P*e + D*(e - {e_{last}}) = P*e + D*\Deltau = P*e + I*\int e + D*\mathop e\limits^ \bulletu = P*e + I*\sum e + D*\Delta e位置式PID和增量式PID
连续的PID公式:
u = Pe + I\int e + D*\mathop e\limits^ \bulletu(k) = Pe(k) + I\sum\limits_{i = 1}^k {e(i)} + D*[e(k) - e(k - 1)增量式PID: 增量的意思就是本次输出量和上次输出量的差值,比如第k次PID:
u(k) = Pe(k) + I\sum\limits_{i = 1}^k {e(i)} + D*[e(k) - e(k - 1)u(k - 1) = Pe(k - 1) + I\sum\limits_{i = 1}^{k - 1} {e(i)} + D*[e(k - 1) - e(k - 2)\Delta u(k) = u(k) - u(k - 1)\Delta u(k) = P*[e(k) - e(k - 1)] + Ie(k) + D[e(k) - 2*e(k - 1) + e(k - 2)两种PID的应用场合: (1)位置式PID控制器适用于需要快速响应和精确控制的场景。比如,在电机控制中,需要控制电机的转速或位置,使其能够快速响应并准确控制,这时可以使用位置式PID控制器。又如,在温度控制中,需要控制温度的精度和稳定性,这时也可以使用位置式PID控制器,同时位置式PID中,I项是会产生滞后,增加准确性的作用,为了系统的快速性,有可能I项直接不用,只用PD控制。
(2)增量式PID控制器适用于需要较强稳定性和对测量噪声较高的场景。比如,在机器人控制中,需要控制机器人的运动轨迹,这时可以使用增量式PID控制器,因为机器人在运动过程中,往往受到各种干扰,如摩擦力、惯性力等,增量式PID控制器对这些干扰有较强的稳定性;又如,在液位控制中,需要控制液位的高度和稳定性,这时也可以使用增量式PID控制器,因为在液位测量中,测量信号通常存在一定的噪声和波动,增量式PID控制器可以较好地抵抗这些干扰。
- **位置式PID代码:
#include "pid.h"
static float err_last=0; //上次的误差
static float err=0; //本次的误差
static float Integral_err=0; //累计的误差
static float P=0;// //P
static float I=0;// //I
static float D=0;// //D
float a_m=0.0; //输出值
float pid_control(float speed)
{
err=(aim_v - now_v); //获得本次误差
Integral_err += err; //获得累加误差
a_m = P * err + I * Integral_err + D * (err - err_last); //位置式PID公式
a_m = a_m<10000?a_m:10000; //对输出值进行限幅
a_m = a_m>-10000?a_m:-10000;
return a_m; //输出a_m
}
a_m = a_m<10000?a_m:10000; 输出限幅很重要 a_m = a_m>-10000?a_m:-10000;
- **增量式PID代码:
#include "pid.h"
static float err_last_last=0; //上上次的误差
static float err_last=0; //上次的误差
static float err=0; //本次的误差
static float P=0; //参数P
static float I=0; //参数I
static float D=0; //参数D
static float off_cal = 0;
float a_m=0.0; //输出值
float pid_control(float speed)
{
err_last_last=err_last; //更新上上次的误差
err_last=err; //更新上次的误差
err=(aim_v - now_v); //更新本次误差
off_cal = P * (err - err_last) + I * err + D * (err + err_last_last - 2 * err_last); //增量式PID的公式
a_m+= off_cal; //得到输出值
a_m = a_m<10000?a_m:10000; //对输出值进行限幅
a_m = a_m>-10000?a_m:-10000;
return a_m; //输出a_m
}
- 电机速度闭环控制及调参方法
1.
把P、I、D三个参数都调成0
2.
先调P,P从0开始缓慢增大,比如先让P=0.1,观察速度变化,一旦电机一上电,速度超过了目标值,比如2.5r/s,说明有超调了,这时候快速性已经没问题了。
3.
然后慢慢增大D,增加阻尼,减小超调,稍微增大D,观察速度变化,让电机上电后快速稳定在2r/s,即参数调试完成。
4.
速度闭环控制中,积分项I的作用不大,因为发现并没有静态误差,就没有调积分项。
十四、Ozone变量可视化
看教程即可。
十五、I2C底层通信
[[SPI、UART、RS232、RS485、IIC 通信总线协议]]
i2c.c: ![[屏幕截图 2025-04-24 175723.png|575]] 引脚的初始化,两个推挽输出。SCL是时钟信号,STM32是主机,负责发出时钟信号,所以IO模式使用推挽输出。
i2c.h: ![[屏幕截图 2025-04-24 180116.png|600]]
直接控制了STM32的寄存器,没有任何库函数
位操作
- STM32F1xx的中文参考手册: ![[屏幕截图 2025-04-24 180512.png|650]] 32位寄存器,管GPIO低八位的模式,比如是GPIOA,那就管着GPIOA.0 – GPIOA.7这八个脚。
& | << >> ~ ^ 这六个属于位运算符, 分别代表“与运算”,“或运算”,“左移”,“右移”,“按位取反”,“按位异或”。
GPIOA->CRL&=0XFF0FFFFF: 0XFF0FFFFF 十六进制转二进制为: “1111 1111 0000 1111 1111 1111 1111 1111” GPIOA->CRL&=0XFF0FFFFF也就是GPIOA->CRL=GPIOA->CRL&0XFF0FFFFF 对比上图,也就是把GPIOA_CRL寄存器的23~20位全部置0,也就是让5号口的所有配置全部置0
GPIOA->CRL|=(u32)8<<20; ,这个“(u32)8”,8是十进制,被强制转换成了无符号32位,那就是: (u32)8等价于“0000 0000 0000 0000 0000 0000 0000 1000”。 “<<20”就是把(u32)8左移20位: “0000 0000 0000 0000 0000 0000 0000 1000”左移20位变成了: “0000 0000 1000 0000 0000 0000 0000 0000” 左移之后再或运算“|”,这其实就是把23~20位,也就是A5口的这四个位配置为:![[屏幕截图 2025-04-24 181803.png|319]]1000,对照表可知: ![[屏幕截图 2025-04-24 182008.png|675]] 1000对应着上/下拉输入。 具体是上拉还是下拉,用PAout(5)这个函数决定
上拉输入的IO口在外部接IO口的电路(外部输入电路)为低电平时,IO口可以测到明确的低电平;
上拉输入的IO口在外部接IO口的电路(外部输入电路)为高电平时,IO口可以测到明确的高电平;
上拉输入的IO口在外部接IO口的电路(外部输入电路)为高阻态时,IO口可以测到明确的高电平;
下拉输入的IO口在外部接IO口的电路(外部输入电路)为低电平时,IO口可以测到明确的低电平;
下拉输入的IO口在外部接IO口的电路(外部输入电路)为高电平时,IO口可以测到明确的高电平;
下拉输入的IO口在外部接IO口的电路(外部输入电路)为高阻态时,IO口可以测到明确的低电平;
IIC通信函数:
- 开始位
- 结束位
- 主机回应从机ack
- 主机回应从机nack
- 主机等待从机回应
- 主机发送一个字节
- 主机接收一个字节
一、开始位 ![[屏幕截图 2025-04-24 182557.png|500]] 开始位:SCL保持高电平,SDA由高电平变为低电平后,延时(>4.7us),SCL变为低电平。为啥说了要大于4.7us,但是要延时2us呢? 因为代码运行也需要一定时间,加上代码运行时间即可大于4.7us。
二、停止位 ![[屏幕截图 2025-04-24 182806.png|575]] 停止信号:SCL保持高电平。SDA由低电平变为高电平。
IIC信号在数据传输过程中,当SCL=1高电平时,数据线SDA必须保持稳定状态,不允许有电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
SCL=1时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。
三、应答信号和不应答信号 ![[屏幕截图 2025-04-24 183156.png|600]] 应答信号:主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答。应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
四、等待从机应答 ![[屏幕截图 2025-04-24 183406 1.png|375]]
五、主机发送一个字节 ![[屏幕截图 2025-04-24 183516.png|450]]
六、主机读取一个字节 ![[屏幕截图 2025-04-24 183712.png|500]]
十六、逻辑分析仪
看教程即可。
十七、MPU6050(陀螺仪/加速度计)
注意!使用MPU6050时需要水平放置!不要把芯片竖着放,会导致芯片内部的陀螺仪初始化不成功!
MPU6050的本质是一个IIC通信的传感器模块,也就是一个传感器: MPU6050=陀螺仪+加速度计+数字运动处理器DMP(digital motion processor) 陀螺仪可以测量以空间坐标系x、y、z轴为转轴的三轴角速度;加速度计可以测量x、y、z三轴方向的加速度;3轴+3轴=6轴,所以是六轴运动处理传感器。
![[屏幕截图 2025-04-26 142914.png|575]] 只需要接VCC、GND、SCL、SDA四个脚即可 传感器的作用:获取当前的三轴角速度和三轴加速度。
- 看代码: ![[屏幕截图 2025-04-26 143245.png|550]] IIC通信接口的初始化,随便两个口就行 MPU6050初始化,DMP初始化,不需要看,只需要会用 只需要在合适的地方添加Read_DMP函数即可,为读出:三轴姿态角,三轴角速度,三轴加速度
三轴姿态角就是Pitch、Roll、Yaw,俯仰角,横摆角,航向角 三轴角速度就是gyro[0]、gyro[1]、gyro[2] 三轴加速度就是accel[0]、accel [1]、accel [2]
每次调用Read_DMP函数,角速度和加速度的相关变量就自动赋值了,我们引用下面这个头文件之后直接用就行 ![[屏幕截图 2025-04-26 144016.png|600]]
十八、完成平衡小车
放手去做!