快速上手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}} = 100000H
分频后的频率是100000Hz,也就是100KHz,也就是定时器3的计数器的计数频率是100KHz。 100KHz也就是一个周期0.00001秒,也就是定时器3的计数器每0.00001秒记一个数。

psc=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}}
PWM的频率由arr和psc决定; PWM的占空比由CCR决定
  • 舵机: 一般的舵机(特指模拟舵机)有三根线,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}}
![[屏幕截图 2025-04-01 143943.png|550]]![[屏幕截图 2025-04-01 143951 2.png|550]] 比定时器中断少了中断服务函数TIMx_IRQHandler,也没有了NVIC的相关配置。main函数内也少了中断优先级分组,因为生成PWM用不到定时器中断。

  • 多了新结构体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}}
所以TIM_SetCompare1(TIM1,500);这句话的作用就是把TIM1_CH1的占空比改为:500/20000=2.5%,对应舵机的0°位置; 同理,后面TIM_SetCompare1(TIM1,2500)就是把占空比改为: 2500/20000=12.5%,对应舵机的180°位置

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*

所以只需要:

  1. 给TRIG口一个10us以上的高电平,去触发测量任务。 把随便一个IO口搞成推挽输出,接到TRIG上去,然后电平置高,delay_us(11),延时十微秒以上即可

  2. 传感器的测量任务进行完之后,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]]

  1. 上图这个刻度盘安装在电机转轴上,随着电机一起旋转;
  2. 刻度盘上有一条条等间距的缝隙;
  3. 右边是个小红外灯(发光元件),一直向左边的光敏元件发射激光,但是刻度盘会把光挡住,激光只能穿过刻度盘的一条条缝隙射给光敏元件,如下图: ![[屏幕截图 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、I、D三个参数是确定不变的:也就是在自动控制中,这三个参数最初设定是多少就永远是多少;* *上式中,u就是output,输出值;* *e就是error,误差值,作为输入。*

比例控制P越大,接近目标值的速度越快,但是提升控制速度的代价是超调震荡会随着P的增大而增大;P过大会使系统失稳。 ![[屏幕截图 2025-04-23 174358.png|425]]

微分项D

这时候就要引入D了,微分:

u = P*e + D*\mathop e\limits^ \bullet
e上加一个点代表是e的微分。微分离散化之后就是e-e_last,e是这一次的误差,e_last是上一次的误差,于是公式就变成了:
u = P*e + D*(e - {e_{last}}) = P*e + D*\Delta
D项相当于一个阻尼项,可以让控制更加平缓,不会有过猛过冲的情况,同理,D项最重要的作用就是消除超调量![[屏幕截图 2025-04-23 174422.png|625]] #### *积分项I*
u = P*e + I*\int e + D*\mathop e\limits^ \bullet
积分项I就是专门来消除静态误差的,加入积分项,PID就全了。 跟前面一样,把公式离散化:
u = P*e + I*\sum e + D*\Delta e
**但是积分项I过大也会引起超调增大以及失稳,而且I过大会导致系统滞后,也就是调节效果慢半拍,降低系统快速性。**

位置式PID和增量式PID

连续的PID公式:

u = Pe + I\int e + D*\mathop e\limits^ \bullet
将其离散化,得到离散位置式PID公式:
u(k) = Pe(k) + I\sum\limits_{i = 1}^k {e(i)} + D*[e(k) - e(k - 1)
K代表第k次PID调节,u(k)就是本次的输出值。

增量式PID: 增量的意思就是本次输出量和上次输出量的差值,比如第k次PID:

u(k) = Pe(k) + I\sum\limits_{i = 1}^k {e(i)} + D*[e(k) - e(k - 1)
那上次就是k-1次PID:
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)
△u(k)是输出值的增量,也就是上一次的输出值加上△u(k)就是这一次的输出值。

两种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通信函数:

  1. 开始位
  2. 结束位
  3. 主机回应从机ack
  4. 主机回应从机nack
  5. 主机等待从机回应
  6. 主机发送一个字节
  7. 主机接收一个字节

一、开始位 ![[屏幕截图 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]]


十八、完成平衡小车

放手去做!