【秋招自用】【持续日更ing】STM32总结笔记【含PID小车、飞控等项目】【4.19更新至定时器测霍尔编码器速度】-程序员宅基地

技术标签: stm32  笔记  嵌入式硬件  

一.前言(必须知道的一些单片机基础芝士)

嵌入式单片机开发一定没有我们想象的那么复杂,我认为嵌入式开发中,查资料看Datasheet的能力和写代码是同样重要的,当然如果你也和我一样是发烧友,那么和嵌入式密不可分的硬件开发你也一定会感兴趣,后面的项目中也有我对硬件开发的理解。

【引】【简单例子1】LED小灯的亮灭

这是通过STM32通过GPIO外设,控制某一个连接LED小灯的引脚的高低电平,来实现控制LED的亮灭的非常简单的例程。

【问】那么首先第一个问题:这是什么开发模式?

1.STM32开发模式

  • 库函数开发:例程中使用的就是库函数开发。基于寄存器进行了函数的封装,封装之后可以根据函数名字就能明白代码作用,容易记忆,使用方便。
  • HAL库函数开发:通常搭配STM32Cube IDE使用,相比于标准库更加深入的封装,有句柄、回调函数等概念,因此相对于标准库模式有更好的可移植性
  • 寄存器开发:操作寄存器,通常需要对应查看Datasheet,通过位操作,对相应寄存器的位进行赋1和置0的操作,实现对应功能。

本文使用标准库函数进行开发,寄存器开发虽然用得不多,但是作为一个嵌入式工程师,对寄存器的与、或、移位、对寄存器清0,置1的操作也一定是必须要掌握的。

【问】第二个问题:为什么使用GPIO外设需要开启GPIO时钟?

STM32每个外设都有独立的时钟,时钟就是心脏,想要使用想要的外设,就要打开对应的时钟。

2.开启外设时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

例程中,我们根据这行代码可以推测出:使能(打开)了APB2总线上的一个外设GPIOA的时钟。

APB1和APB2的区别

APB1上挂载的都是低速外设,包括电源接口、CAN、USB、I2C1、I2C2、UART2、UART3 等。APB2上挂载的都是高速外设,包括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通GPIO 口(PA~PE)等。

【问】第三个问题:GPIO配置的过程中,为什么配置GPIO_Mode为推挽输出模式?

3.GPIO八种工作模式

详细分析见本人另一篇博客:(更新ing)里面关于推挽输出有极其详细的推理

  ​​​​​​​GPIO的八种工作模式

看不懂的话以下是本人粗鄙的理解:

1.浮空输入:通常用于按键检测

2.上拉:默认高电平         下拉:默认低电平

(这两种都是需要你去查硬件原理图的,当你确定好你想要实现的功能:这个GPIO引脚的输出需要保持逻辑电平高还是低?再去选择吧)

3.推挽输出:可以输出高和低

4.开漏输出:常用于 IIC 通讯或其它需要进行电平转换的场景。

5.模拟输入:显然是使用ADC或者DAC外设的时候才用的到

【其他一些知识】

(更新ing)

二.理论知识

1.GPIO(嵌入式点灯总工程师)

【例程2】跑马灯

写代码前,我们查看一下硬件原理图(正点原子F103C8T6),两个LED的连接情况:

图中我们可以看出LED0连接的是引脚PB5,PB5上拉接的是VCC。

【问】那么第一个问题:IO口应该配置为什么模式?

【问】别急,首先,输出还是输入模式?

输出模式:因为我要控制LED的亮灭

【问】然后选择什么GPIO模式?推挽输出可以吗?

可以,推挽输出模式驱动能力强,可以输出高低电平,非常适合。

因为PB5接的是VCC,所以输出0就亮,输出1就灭。

【问】开漏输出可以吗?

让我们回顾一下开漏,只能输出0和高阻态。但是我们发现PB5接的是VCC,那我们就不需要去管他不能输出1这个特点了。所以输出0,LED亮。输出1,高阻态,但是因为外接VCC,所以灯灭。

然后我们写LED初始化的代码:

void LED_Init(void)
{
 
 GPIO_InitTypeDef  GPIO_InitStructure;
 
//使能PB,PE端口时钟
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);	 

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;				 //LED0-->PB.5 端口配置
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
 GPIO_Init(GPIOB, &GPIO_InitStructure);					 //根据设定参数初始化GPIOB.5
 GPIO_SetBits(GPIOB,GPIO_Pin_5);						 //PB.5 输出高

 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	    		 //LED1-->PE.5 端口配置, 推挽输出
 GPIO_Init(GPIOE, &GPIO_InitStructure);	  				 //推挽输出 ,IO口速度为50MHz
 GPIO_SetBits(GPIOE,GPIO_Pin_5); 						 //PE.5 输出高 
}
 

主函数中实现我们的跑马灯功能

int main(void)
{ 
 
	delay_init();		  //初始化延时函数
	LED_Init();		        //初始化LED端口
	while(1)
	{
			GPIO_ResetBits(GPIOB,GPIO_Pin_5);  //LED0对应引脚GPIOB.5拉低,亮 
			GPIO_SetBits(GPIOE,GPIO_Pin_5);   //LED1对应引脚GPIOE.5拉高,灭 
			delay_ms(300);  		   //延时300ms
			GPIO_SetBits(GPIOB,GPIO_Pin_5);	   //LED0对应引脚GPIOB.5拉高,灭 
			GPIO_ResetBits(GPIOE,GPIO_Pin_5); //LED1对应引脚GPIOE.5拉低,亮 
			delay_ms(300);                     //延时300ms
	}
} 

【例程3】按键控制LED和蜂鸣器

OK这个例程我们要实现的功能是:使用三个按键:

KEY_UP 控制蜂鸣器翻转,KEY1 控制 LED1翻转,KEY2 控制 LED0 翻转

老样子,首先我们要熟悉一下蜂鸣器,看一下原理图:

查资料我们可以知道:蜂鸣器的驱动电流为30mA,单个IO输出电流大概为25mA

【问】我们是否可以使用IO口直接连接蜂鸣器进行控制?

可以,但没必要。通常使用一个三极管驱动。

用一个 NPN 三极管(S8050)来驱动蜂鸣器,驱动信号通过 R36 和 R38 间的电压获得,芯片上电时默认电平为低电平,故上电时蜂鸣器不会直接响起。当 PB8 输出高电平的时候,蜂鸣器将发声,当 PB8 输出低电平的时候,蜂鸣器停止发声。

根据原理图,初始化蜂鸣器的GPIO引脚PB8,和例程1几乎一样的流程。

void BEEP_Init(void)
{
 
 GPIO_InitTypeDef  GPIO_InitStructure;
 	
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 //使能GPIOB端口时钟
 
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;				 //BEEP->PB8 端口配置
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	     //速度为50MHz
 GPIO_Init(GPIOB, &GPIO_InitStructure);	
 
 GPIO_ResetBits(GPIOB,GPIO_Pin_8);                       //输出0,关闭蜂鸣器输出

}

然后是这个例程的主角:按键登场。要了解按键,我们还是去看原理图:

【问】GPIO模式如何设置?

分析PA0,上拉接VCC,那么我们要使其正常工作(也就是产生电压差),所以PA0设置为下拉。

反之,分析PE2PE3PE4,设置为上拉。

那么初始化按键的函数也就写好了:

void KEY_Init(void) 
{ 
 	GPIO_InitTypeDef GPIO_InitStructure;
 
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOE,ENABLE);
   
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_4|GPIO_Pin_3;  //PE3和PE4
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;          //设置成上拉输入
 	GPIO_Init(GPIOE, &GPIO_InitStructure);                 

	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;            //PA0
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;         //设置成输入下拉	  
	GPIO_Init(GPIOA, &GPIO_InitStructure);

}

【问】然后呢?

我们来思考一下想实现的功能:

  • 按下KEY1(PE3),LED1电平翻转
  • 按下KEY2(PE2),LED2电平翻转
  • 按下KEY_UP(PA0),蜂鸣器响一下

那单片机怎么知道我按下了KEY1?

我们需要一个检测引脚电平的函数:GPIO_ReadInputDataBit

GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键0
GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键1
GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键3(WK_UP) 

【问】那是不是主函数中只需要不断地通过这个函数,检测电平,响应行动就可以了?

答案肯定是不行的。

首先按键的结构要求我们在编程的时候处理一下按键电平抖动的问题,也就是要加入防抖。因为按键结构的问题,说白了就是我按下按键的一瞬间,引脚内部的高低电平是不确定的。解决办法就是两个if语句即可:

u8 KEY_Scan()
{	 
	static u8 key_up=1;//按键按松开标志	  
	if(key_up&&(KEY0==0||KEY1==0||WK_UP==1))
	{
		delay_ms(10);//去抖动 
		key_up=0;
		if(KEY0==0)
			return KEY0_PRES;
		else if(KEY1==0)
			return KEY1_PRES;
		else if(WK_UP==1)
			return WKUP_PRES;
	}else if(KEY0==1&&KEY1==1&&WK_UP==0)
            key_up=1; 	    
 	return 0;// 无按键按下
}

别急,一句一句分析:

首先我定义一个标志位key_up,如果为1,说明按键松开了,反之为0,按键按下了。

初始化的值显然是1。

if(key_up&&(KEY0==0||KEY1==0||WK_UP==1))

如果三个按键有一个按下了,就进入这个if语句

这里用到了一些宏定义:


#define KEY0  GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_4)//读取按键0
#define KEY1  GPIO_ReadInputDataBit(GPIOE,GPIO_Pin_3)//读取按键1
#define WK_UP   GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键3(WK_UP) 

if语句进去 :

delay_ms(10);//去抖动 
key_up=0;

一进去就是一个delay,等待10ms,然后把标志位key_up置0,意思是按键按下了。

        if(KEY0==0)
			return KEY0_PRES;
		else if(KEY1==0)
			return KEY1_PRES;
		else if(WK_UP==1)
			return WKUP_PRES;
	}

如果在10ms后,这些引脚的电平依旧没有改变,那么单片机就可以确定引脚电平确实发生了改变,这里没有直接操作LED和蜂鸣器,而是又用了宏定义,把标志位作为函数的返回值,方便在主函数中判断:

#define KEY0_PRES 	1	//KEY0按下
#define KEY1_PRES	2	//KEY1按下
#define WKUP_PRES   3	//KEY_UP按下(即WK_UP/KEY_UP)

然后利用模块化编程的思想,写一下主函数:

int main(void)
 {
 	vu8 key=0;	
	delay_init();	    	 //延时函数初始化	  
	LED_Init();		  		//初始化与LED连接的硬件接口
	BEEP_Init();         	//初始化蜂鸣器端口
	KEY_Init();         	//初始化与按键连接的硬件接口
	LED0=0;					//先点亮红灯
	while(1)
	{
 		key=KEY_Scan(0);	//得到键值
	   	if(key)
		{						   
			switch(key)
			{				 
				case WKUP_PRES:	//控制蜂鸣器
					BEEP=!BEEP;
					break; 
				case KEY1_PRES:	//控制LED1翻转	 
					LED1=!LED1;
					break;
				case KEY0_PRES:	//同时控制LED0,LED1翻转 
					LED0=!LED0;
					LED1=!LED1;
					break;
			}
		}else delay_ms(10); 
	}	 
}

主函数的逻辑就是我们实现的功能,模块化编程的方式使得代码阅读一清二楚。

(当然代价就是源码要跳来跳去查看比较复杂吧)

2.外部中断(真的非常重要)

首先关于中断,我通俗易懂的解释,CPU是STM32的大脑,时刻都在活动和思考,前面的例程非常简单,我们的需求就是按下按键,灯亮了,CPU全程在做这种很无聊的事情,他一直循环在等,等一个心爱的按键电平变化,然后去做对应的动作。

【问】但是假如我现在在做一个STM32小车,寻迹、避障、不断使用PID算法稳定行走、抓取东西等等等等。用到的外设库库多,我的CPU还能把他所有精力,去一直等一个小小的按键电平变化吗?

显然是不行的吧?那我们怎么办呢?

这就用到了外部中断

【问】外部中断是怎么去做的呢?

我先通俗易懂的用一个实际例子解释:

在STM32PID小车项目中,红外检测模块用到了中断。

这个模块是用来寻迹的(让小车沿着黑色的线走),这个模块检测到了黑色,就会发出一个电平信号,然后我根据这个电平信号,去调整我的小车的行进方向(具体的后面再说)。

那么我的CPU总不能一直啥也不干,就一直去等着这个模块的信号吧?

【解决方法】我们把这个模块的引脚和外部中断进行绑定并且设置一些参数,这样CPU就不用管它了,CPU自己做自己的更重要的事情,一旦这个模块检测到信号电平变化,他就会通知CPU大哥,此时CPU就会放下手里的事情,专门进入一个这个模块对应的中断处理函数,去做相应的事情完成我们的需求。

有了中断大概的应用场景,然后让我们开始这节的知识

【中断优先级】

有了上面对的例子铺垫就好说了。

NVIC就是CPU的小助手,例子中我只说了一个信号绑定中断,那么假如有两个信号,三个,四个信号绑定了中断怎么办?

那每个模块的中断是不是也有紧急和非紧急之分呢?

所以就需要我们去设置中断优先级:(数字越小,优先级越高哦,不要记错

抢占优先级:优先级高的能打断优先级低
响应优先级:当抢占优先级相同时,响应优先级高的先执行

(一层优先级是不够的,复杂项目中用到很多不同模块的中断,所以要有两个优先级)

【NVIC】向量嵌套中断控制器

【问】那又是谁来管理中断优先级的???

就是他:向量嵌套中断控制器(拗口)

结构如上。

【例程4】按键通过外部中断控制LED

实现功能:通过外部中断的方式让开发板上的KEY_UP(PA0) 控制 蜂鸣器响一下(简单一点)

还是之前那张图。

【中断的配置】

void EXTIX_Init(void)
{
   	EXTI_InitTypeDef EXTI_InitStructure;
 	NVIC_InitTypeDef NVIC_InitStructure;

    KEY_Init();	 //	按键端口初始化

  	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	//使能复用功能时钟

    //PA0中断线以及中断初始化配置:上升沿触发  PA0 WK_UP
 	GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0); 

  	EXTI_InitStructure.EXTI_Line=EXTI_Line0;
  	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
  	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
  	EXTI_Init(&EXTI_InitStructure);		

  	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键WK_UP所在的外部中断通道
  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2, 
  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;		 //子优先级3
  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				 //使能外部中断通道
  	NVIC_Init(&NVIC_InitStructure);
}

也是非常复杂,所以我们一句句分析:

首先配置中断,就要配置三个东西:EXTI结构体,NVIC结构体和中断线的配置。

中断分组

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  • NVIC_PRIORITYGROUP_0: 0位抢占式优先级,4位响应优先级;
  • NVIC_PRIORITYGROUP_1: 1位抢占式优先级,3位响应优先级;
  • NVIC_PRIORITYGROUP_2: 2位抢占式优先级,2位响应优先级;
  • NVIC_PRIORITYGROUP_3: 3位抢占式优先级,1位响应优先级;
  • NVIC_PRIORITYGROUP_4: 4位抢占式优先级,0位响应优先级;

一般选择2位抢占2位响应优先级就行,不用太纠结

【开启AFIO复用功能时钟】

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	//使能复用功能时钟

【问】AFIO又是什么??

还是通俗易懂的说:

STM32中的大部分的 GPIO引脚 都有复用功能,比如图中的PA1,他首先是GPIO引脚,然后可以复用为其他功能的引脚,有很多。

【问】这个外部中断的例程为什么要开启AFIO时钟??

STM32 的所有GPIO都引入到 EXTI 外部中断线上,使得所有的 GPIO 都能作为外部中断的输入源。所以如果把 GPIO 用作 EXTI 外部中断时,还需要开启 AFIO 时钟。

【问】哪几种情况一定要开启AFIO时钟??

在用到 外部中断端口重映射 的时候要使能AFIO时钟!!!!!!!!!!!

端口重映射后面再说吧。

【中断线】

GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);

【问】可以根据代码意思猜到意思吗?

这行代码配置了外部中断线路的GPIO引脚。将GPIOA的第0个引脚配置为外部中断线

这意味着:当GPIOA的第0个引脚(PA0)发生外部事件(产生由低到高的电平)时,将触发相应的外部中断。

【EXTI结构体】

    EXTI_InitStructure.EXTI_Line=EXTI_Line0;
  	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;	
  	EXTI_Init(&EXTI_InitStructure);		

第一个参数】是外部中断线:STM32一共有16个外部中断线

【问】怎么记住那个引脚对应哪条中断线?

PA0对应EXTI_Line0,PA1对应EXTI_Line1,PA2对应EXTI_Line2....

是不是巨简单?

注意PB0和PA0不能公用一条中断线EXTI_Line0,后面项目大了以后要注意避免。

第二个参数】是设置触发方式是上升沿触发??还是下降沿触发??双边沿触发??

  • 上升沿触发(Rising edge trigger):当引脚由电平变为电平时触发中断。
  • 下降沿触发(Falling edge trigger):当引脚由电平变为电平时触发中断。
  • 双边沿触发(Both edge trigger):当引脚上升沿或下降沿发生时触发中断。

那么看一下原理图,我需要让引脚PA0(设置为下拉了)的电平从0变为1的时候,控制灯点亮。

所以我们选择上升沿触发

第三个参数】中断模式的选择:一种是事件触发,一种是中断触发。

事件是一种可以导致中断发生的事件,中断是因为中断事件的发生而导致的后续行为过程。事件与中断事件是包含关系。(有点绕,尽量理解,理解不了也没事,我只用到过中断,好像没用到过事件)

【NVIC结构体】

    NVIC_InitTypeDef NVIC_InitStructure;
    
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;             //外部中断0
  	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2, 
  	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03;		 //子优先级3
  	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				 //使能外部中断通道
  	NVIC_Init(&NVIC_InitStructure);
}

第一个参数】选择NVIC的中断源,PA0所以EXTIO,PB2就是EXTI2,可以理解吗?

下面就是配置抢占优先级2和响应优先级3(中断少的时候无所谓,中断要是数量多了就得好好权衡一下了,哪个最重要?那个第二级重要?)

最后一个参数就是使能一下,打开外部中断通道。

【中断处理函数】(重中之重重重重)

//外部中断0服务程序 
void EXTI0_IRQHandler(void)
{
	delay_ms(10);//消抖			 
	BEEP=!BEEP;	
	EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位  
}

首先,我们根据引脚PA0选择的外部中断线是EXTI0,那么对应的中断处理函数就是EXTIO_IRQHandler,名字是定死的,在32的固件库里是定死的,不能随便修改。

当外部中断检测到PA0的上升沿电平,他就会自动跳进这个中断处理函数中,实现我们写的代码的动作。

这里没用上一节的按键扫描函数,因为代码比较简单。

最后不要忘了清除一下标志位。

main函数

int main(void)
 {		
	delay_init();	    	 //延时函数初始化	

    //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	BEEP_Init();		 	//初始化蜂鸣器IO
	EXTIX_Init();         	//初始化外部中断输入 
	while(1)
	{	    
		delay_ms(1000);	  
	}	 
}

最后至于主函数就不用管了,while循环中随便写几行代码让他不要卡死就行了。

3.USART串口通信(通信协议入门)(看不懂可以先跳过会用即可)

通信的一些知识

通信是什么 ?? 通俗的讲:两个设备,一个收,一个发。

通信的分类

  • 单工:要么收,要么发(一根线)
  • 半双工:不可以同时收和发(一根线)
  • 双工:同时收和发(两根线)

串行和并行

同步和异步

同步通信:双方根据一个共同的时钟信号进行通信(时钟信号一样,自然就“同步”了)

异步通信:通信双方有各自的时钟,通信过程不同步。

我们这节学习的串口是串行的、异步半双工通信。

UART连线

两根数据线,一条收一条发,简单明了

UART通信协议

通讯协议,通俗的讲,两个设备要传数据,就必然要保证数据格式的正确,还要规定什么时候开始,什么时候结束,以及数据的校验等等。

以上就是UART的数据帧格式

【例程5】UART串口实现单片机和上位机通信

实现功能:LED不断闪烁,STM32通过USART1和上位机通信,上位机发送字符串给STM32(回车换行结束),然后32再把相同字符串发回上位机。

老样子,还是先看硬件连接:

由图可以知道我们需要用到PA9的复用功能USART1_TX,PA10的复用功能USART1_RX。

【问】这个例程会用到中断吗?

这是数据手册中NVIC_IRQChanne中断通道,本章例程使用的是USART1中断。那么和上一节例程类似的,也一定会有一个叫IRQ什么的中断处理函。

【问】自己想一下代码的流程?

  • 初始化两个引脚的GPIO、NVIC、UART结构体
  • UART的接收和发送
  • UART的中断处理函数
注:UART通信协议的代码其实比较复杂,对新手不友好,但我尽量通俗易懂

UART的配置

1.打开时钟

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);	

 2.GPIO结构体配置

  GPIO_InitTypeDef GPIO_InitStructure; //USART1_TX   GPIOA.9

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
   
  //USART1_RX	  GPIOA.10初始化
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
  GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10  

【问】GPIO模式怎么设置?

 PA9是发送数据引脚,所以设置为复用推挽输出模式,可以增强输入信号的驱动能力。

PA10是接收数据引脚,浮空输入就行。

3.NVIC结构体设置

  NVIC_InitTypeDef NVIC_InitStructure;

  //Usart1 NVIC 配置
  NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;  //抢占优先级3
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
  NVIC_Init(&NVIC_InitStructure);	
  

 上面分析过了,中断源选择USART1_IRQn

4.UART结构体设置

  USART_InitTypeDef USART_InitStructure;
  
  //USART 初始化设置
  USART_InitStructure.USART_BaudRate = bound;//串口波特率
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
  USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
  USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式

  USART_Init(USART1, &USART_InitStructure); //初始化串口1

 其实对于串口,这些参数比较固定,这里不赘述了,注释也比较清楚。

 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
 USART_Cmd(USART1, ENABLE);                    //使能串口1 

初始化串口之后还要开启串口接收中断,以及使能串口1。

USART_IT_RXNE为接收中断标志位,也就是说,串口一接收到数据,这个标志位会被置位。

UART中断服务程序

串口每接收一个字符,就会进入这个函数,对数据进行处理

u16 USART_RX_STA=0;       //接收状态标记	  
u8 USART_RX_BUF[USART_REC_LEN];     //接收缓冲,最大USART_REC_LEN个字节.

void USART1_IRQHandler(void)                	//串口1中断服务程序
{
	u8 Res;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  
    //接收中断(接收到的数据必须是0x0d 0x0a结尾)
		{
		  Res =USART_ReceiveData(USART1);	//读取接收到的数据
		
		if((USART_RX_STA&0x8000)==0)//接收未完成
			{
			  if(USART_RX_STA&0x4000)//接收到了0x0d
				{
				   if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
				   else USART_RX_STA|=0x8000;	//接收完成了 
				}
			  else //还没收到0X0D
				{	
				   if(Res==0x0d)USART_RX_STA|=0x4000;
				   else
				   {
					USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
					USART_RX_STA++;
					if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;
                       //接收数据错误,重新开始接收	  
				   }		 
				}
			}   		 
     } 

一句一句分析:

if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  

USART_GetITStatus() 函数用于检测指定串口的指定标志位是否置位。

这里就是检测USART1串口的接收数据寄存器非空中断的常量:USART_IT_RXNE

如果检测到被置位,表示已经接收到数据,可以从USART接收数据缓冲区读取数据。

Res =USART_ReceiveData(USART1);	//读取接收到的数据

然后定义一个8位无符号char类型的返回值,使用USART_ReceiveData()来接收数据。

if((USART_RX_STA&0x8000)==0)     //接收未完成

USART_RX_STA这是我们自己设计的一个接收协议:

【问】那他是用来干嘛的呢

如图:

【问】为什么最后两位是0x0D和0x0a

ASCAII码规定的就是0D回车,0A换行。

不规定这样子的接收协议的话,我们无法判断数据帧什么时候结束。

【问】USART_RX_STA&0x8000什么意思?

USART_RX_STA是我们定义的一个16位的变量,0x8000是十六进制,换为二进制是:

1000 0000 0000 0000

那么USART_RX_STA  &  1000 0000 0000 0000  ==  0 的意思就是:

判断USART_RX_STA的第16位是不是0,并且屏蔽其他15位数据,是的话继续接收数据。

if(USART_RX_STA&0x4000)//接收到了0x0d

【问】这句是判断USART_RX_STA的第几位是不是0?

第15位,因为0x4000换为二进制是0100 0000 0000 0000

最后把整个逻辑注释写了一下:

【主函数】

	while(1)
	{
		if(USART_RX_STA&0x8000)
		{					   
			len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度0011 0000 0000 0000
			printf("\r\n您发送的消息为:\r\n\r\n");
			for(t=0;t<len;t++)
			{
				USART_SendData(USART1, USART_RX_BUF[t]);//向串口1发送数据
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET);//等待发送结束
			}
			printf("\r\n\r\n");//插入换行
			USART_RX_STA=0;
		}else
		{
		    printf("\r\n串口\r\n");
			delay_ms(10);   
		}
	}	 
 }

首先判断数据接收是否完成,完成了就把sta的前14位数据赋值给len,然后for循环发送USART_RX_BUF的数据给串口,打印出来。

后面还判断了一下串口数据发送完成标志位:USART_FLAG_TC。

这样我们的实验代码基本就写完了。

【结】

其实编程核心还是串口中断服务函数的代码,用到了很多比较复杂难懂的知识,这样的编程方式其实利用了状态机思想,增强稳定性,减少错误,我们以后是会经常遇到的。

4.定时器(定时中断+输入捕获+输出比较)

(内容太多就分章节引用了)

【定时中断】

定时器(定时中断)icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137818021?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22137818021%22%2C%22source%22%3A%22Xiaoxuexxxxx%22%7D

【输出比较】

STM32定时器(输出PWM波控制小车电机转速)icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137864527?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22137864527%22%2C%22source%22%3A%22Xiaoxuexxxxx%22%7D

 【输入捕获】

STM32定时器(输入捕获:超声波传感器测距离)icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137917873?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22137917873%22%2C%22source%22%3A%22Xiaoxuexxxxx%22%7D

【编码器模式】

STM32定时器编码器模式测霍尔编码器速度icon-default.png?t=N7T8https://blog.csdn.net/Xiaoxuexxxxx/article/details/137937083?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22137937083%22%2C%22source%22%3A%22Xiaoxuexxxxx%22%7D

持续更新ing   : 日期 4.18

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Xiaoxuexxxxx/article/details/137509422

智能推荐

2022黑龙江最新建筑八大员(材料员)模拟考试试题及答案_料账的试题-程序员宅基地

文章浏览阅读529次。百分百题库提供建筑八大员(材料员)考试试题、建筑八大员(材料员)考试预测题、建筑八大员(材料员)考试真题、建筑八大员(材料员)证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。310项目经理部应编制机械设备使用计划并报()审批。A监理单位B企业C建设单位D租赁单位答案:B311对技术开发、新技术和新工艺应用等情况进行的分析和评价属于()。A人力资源管理考核B材料管理考核C机械设备管理考核D技术管理考核答案:D312建筑垃圾和渣土._料账的试题

chatgpt赋能python:Python自动打开浏览器的技巧-程序员宅基地

文章浏览阅读614次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python自动打开浏览器

Linux中安装JDK-RPM_linux 安装jdk rpm-程序员宅基地

文章浏览阅读545次。Linux中安装JDK-RPM方式_linux 安装jdk rpm

net高校志愿者管理系统-73371,计算机毕业设计(上万套实战教程,赠送源码)-程序员宅基地

文章浏览阅读25次。免费领取项目源码,请关注赞收藏并私信博主,谢谢-高校志愿者管理系统主要功能模块包括页、个人资料(个人信息。修改密码)、公共管理(轮播图、系统公告)、用户管理(管理员、志愿用户)、信息管理(志愿资讯、资讯分类)、活动分类、志愿活动、报名信息、活动心得、留言反馈,采取面对对象的开发模式进行软件的开发和硬体的架设,能很好的满足实际使用的需求,完善了对应的软体架设以及程序编码的工作,采取SQL Server 作为后台数据的主要存储单元,采用Asp.Net技术进行业务系统的编码及其开发,实现了本系统的全部功能。

小米宣布用鸿蒙了吗,小米OV对于是否采用鸿蒙保持沉默,原因是中国制造需要它们...-程序员宅基地

文章浏览阅读122次。原标题:小米OV对于是否采用鸿蒙保持沉默,原因是中国制造需要它们目前华为已开始对鸿蒙系统大规模宣传,不过中国手机四强中的另外三家小米、OPPO、vivo对于是否采用鸿蒙系统保持沉默,甚至OPPO还因此而闹出了一些风波,对此柏铭科技认为这是因为中国制造当下需要小米OV几家继续将手机出口至海外市场。 2020年中国制造支持中国经济渡过了艰难的一年,这一年中国进出口贸易额保持稳步增长的势头,成为全球唯一..._小米宣布用鸿蒙系统

Kafka Eagle_kafka eagle git-程序员宅基地

文章浏览阅读1.3k次。1.Kafka Eagle实现kafka消息监控的代码细节是什么?2.Kafka owner的组成规则是什么?3.怎样使用SQL进行kafka数据预览?4.Kafka Eagle是否支持多集群监控?1.概述在《Kafka 消息监控 - Kafka Eagle》一文中,简单的介绍了 Kafka Eagle这款监控工具的作用,截图预览,以及使用详情。今天_kafka eagle git

随便推点

Eva.js是什么(互动小游戏开发)-程序员宅基地

文章浏览阅读1.1k次,点赞29次,收藏19次。Eva.js 是一个专注于开发互动游戏项目的前端游戏引擎。:Eva.js 提供开箱即用的游戏组件供开发人员立即使用。是的,它简单而优雅!:Eva.js 由高效的运行时和渲染管道 (Pixi.JS) 提供支持,这使得释放设备的全部潜力成为可能。:得益于 ECS(实体-组件-系统)架构,你可以通过高度可定制的 API 扩展您的需求。唯一的限制是你的想象力!_eva.js

OC学习笔记-Objective-C概述和特点_objective-c特点及应用领域-程序员宅基地

文章浏览阅读1k次。Objective-C概述Objective-C是一种面向对象的计算机语言,1980年代初布莱德.考斯特在其公司Stepstone发明Objective-C,该语言是基于SmallTalk-80。1988年NeXT公司发布了OC,他的开发环境和类库叫NEXTSTEP, 1994年NExt与Sun公司发布了标准的NEXTSTEP系统,取名openStep。1996_objective-c特点及应用领域

STM32学习笔记6:TIM基本介绍_stm32 tim寄存器详解-程序员宅基地

文章浏览阅读955次,点赞20次,收藏16次。TIM(Timer)定时器定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断16位计数器、预分频器、自动重装寄存器的时基单元,在 72MHz 计数时钟下可以实现最大 59.65s 的定时,59.65s65536×65536×172MHz59.65s65536×65536×721​MHz不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。_stm32 tim寄存器详解

前端基础语言HTML、CSS 和 JavaScript 学习指南_艾编程学习资料-程序员宅基地

文章浏览阅读1.5k次。对于任何有兴趣学习前端 Web 开发的人来说,了解 HTML、CSS 和JavaScript 之间的区别至关重要。这三种前端语言都是您访问过的每个网站的用户界面构建块。而且,虽然每种语言都有不同的功能重点,但它们都可以共同创建令人兴奋的交互式网站,让用户保持参与。因此,您会发现学习所有三种语言都很重要。如果您有兴趣从事前端开发工作,可以通过多种方式学习这些语言——在艾编程就可以参与到学习当中来。在本文中,我们将回顾每种语言的特征、它们如何协同工作以及您可以在哪里学习它们。HTML vs C._艾编程学习资料

三维重构(10):PCL点云配准_局部点云与全局点云配准-程序员宅基地

文章浏览阅读2.8k次。点云配准主要针对点云的:不完整、旋转错位、平移错位。因此要得到完整点云就需要对局部点云进行配准。为了得到被测物体的完整数据模型,需要确定一个合适的坐标系变换,将从各个视角得到的点集合并到一个统一的坐标系下形成一个完整的数据点云,然后就可以方便地进行可视化,这就是点云数据的配准。点云配准技术通过计算机技术和统计学规律,通过计算机计算两个点云之间的错位,也就是把在不同的坐标系下的得到的点云进行坐标变..._局部点云与全局点云配准

python零基础学习书-Python零基础到进阶必读的书藉:Python学习手册pdf免费下载-程序员宅基地

文章浏览阅读273次。提取码:0oorGoogle和YouTube由于Python的高可适应性、易于维护以及适合于快速开发而采用它。如果你想要编写高质量、高效的并且易于与其他语言和工具集成的代码,《Python学习手册:第4 版》将帮助你使用Python快速实现这一点,不管你是编程新手还是Python初学者。本书是易于掌握和自学的教程,根据作者Python专家Mark Lutz的著名培训课程编写而成。《Python学习..._零基础学pythonpdf电子书