【秋招自用】【持续日更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

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法