STM32单片机学习(17) —— 串口外设中断

文章目录

USART串口中断

在我们之前的实验中,就使用到了阻塞函数,导致程序不能正常的进行,在[STM32单片机学习(15) ------ PC串口通信实验]一文中我们进行控制LED和向OLED屏幕输出时,因为函数的阻塞会使得程序卡住,那么本文就来讲解一下USART串口外设的中断如何编写。

USART串口中断编程

我们对中断的需求是:

当 USART 外设检测到接收事件(即接收数据寄存器非空)时,应当立即触发中断,由中断服务程序完成指令接收与处理。

所以:

我们需要USART1外设来产生一个中断,即USART外设中断。

下面我们首先来了解一下,USART外设中断的产生机制。

USART外设中断产生机制

USART外设如何才能产生一个中断请求,如何才能发送一个中断请求给NVIC?

  1. 每一个USART外设,不管它内部是什么情况,最终只能产生一个全局中断。

    比如USART1外设,它只能产生一个全局中断,发出一个全局中断请求

  2. USARTx外设内部,TXE,TC,RXNE灯中断标志位置位后,都可以发出一个中断请求,也只能发送一个中断请求。尤其需要注意下图中展示的标志位,不是普通状态标志位,而是中断标志位

USART外设内部是可以产生中断请求,它产生中断的原理框图如下图所示:

图中有两个月牙状的"逻辑或门"电路符号。

表示:任何一个中断标志位置位都会触发产生全局中断。

首先说明一下图中标志位的问题:

  1. TXE、TC、RXNE等都是标志位,在中断系统中,我们把这些标志位叫做"中断标志位"。

  2. "中断标志位"和"普通状态标志位"的又有什么区别呢

    中断标志位有一个RXNE,状态标志位也有一个RXNE有什么区别呢?

    粗略的讲,一个同名的中断标志位,需要在状态标志位的基础上,再额外设置一些东西,然后在状态标志位的前提下,中断标志位才会置位,才会发送中断请求。这个过程我们称之为:开启标志位的中断源。

    比如:

    RXNE中单标志位,它想要置位有两个条件:

    1. 接收数据寄存器非空
    2. 开启此标志位的中断源,这样才能让中断标志位置位,发送中断请求。

    总之:

    中断标志位置位了,就一定发送中断请求给NVIC。但不做特殊处理,不开启标志位中断源,中断标志位不会置位的

  3. 我们需记住:对于USART外设来说,状态标志位和中断标志位的置位复位是同步进行的。

举一个例子:

  1. PC端向USART1外设发数据,此时接收数据寄存器非空,于是中断标志位RXNE自动置位。
  2. 程序员调用Receive库函数完成数据接收,中断标志位RXNE自动复位。
  3. 这和前面讲状态标志位RXNE的使用是完全一致的。
  4. 只不过在SPL库中,必须区分中断标志位和状态标志位,因为它们使用的函数是不同的。

除此标志位的问题外,你应该还注意到图中有很多"小开关"。

这些小开关的作用是:

USART外设必须开启相应中断,才能够在中断标志位置位时产生全局中断请求。

举个例子:

  1. 设置开启了RXNE标志位中断,等到RXNE中断标志位置位时,USART外设就会发中断请求。
  2. 没有开启TXE标志位中断,即便TXE中断标志位置位了,USART外设也不会发中断请求。

其实也很好理解:

发中断请求也需要"耗电",没事就发请求,不设置就能发请求,也太奇怪了!

最后再总结一下这张图:

  1. USART外设必须开启相应标志位的中断,USART外设才会发送中断请求。
  2. 在开启中断的前提下,TXE、TC、RXNE、PE这四个中断标志位任何一个置位1时,都会产生一个USART中断请求,发送给NVIC。
  3. 任何一个USARTx外设同时只能产生一个中断,即USARTx全局中断。
  4. 比如USART1外设无论如何只能产生一个中断请求,这个中断叫做USART1全局中断。
  5. 也就是说,无论USART1内部有多少种条件产生中断,但在NVIC看来,都只有一个全局中断。

USART外设中断的使用流程

结合前面,我们讲过的所有知识点,描述一下USART1外设中断的使用流程(以接收数据触发中断为例):

  1. 完成USART1外设的基本初始化配置工作(包括引脚和USART1外设本身)
  2. 开启RXNE标志位中断,使得USART1外设在RXNE中断标志位置位时,可以发送中断请求给NVIC。
  3. 在NVIC中配置USART1全局中断的优先级**(注意配置优先级前要设置分组)**
  4. 在NVIC中开启USART1全局中断。
  5. 手动编写USART1全局中断的中断服务程序,也就是编写对应中断处理函数。

以上流程完成后,中断的触发、执行等都由中断系统自动完成。如此,就实现了真正意义上,实时处理串口数据接收!

最后总结一下整个过程:

  1. USART1外设,负责在接收数据寄存器非空时,产生中断请求。
  2. NVIC负责居中调度管理。
  3. 中断服务程序,也就是中断处理函数,负责处理实时数据接收!

那么到这里,万事俱备,剩下的就是学习SPL库函数调用了。下面逐一介绍这些函数:

USART_ITConfig函数

USART_ITConfig() 是 STM32 标准外设库中提供的用于开启或关闭 USART 标志位中断源的函数

它用于配置 USART 外设是否可以产生中断请求,以及哪个中断标志位会产生中断请求。

其函数名字中的"IT"是中断"Interrupt"的缩写,连上Config配置,表示此函数用于配置USART的中断源信息。

此函数的声明如下:

复制代码
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);

函数没有返回值。

函数的第一个参数USARTx也没什么值得说的,用于指示配置中断源的USART外设名,比如 USART1USART2

函数的第二个参数USART_IT用于指示产生中断源的标志位。

它使用一系列宏定义来指示各种标志位中断源,比如:

标志位名称 USART_IT 传参宏定义 开启标志位中断源后的作用
TXE USART_IT_TXE 当发送数据寄存器为空时,产生中断请求
TC USART_IT_TC 当发送/移位寄存器全部为空,数据发送完成时,产生中断请求
RXNE USART_IT_RXNE 接收数据寄存器非空,有数据可读时,产生中断请求
PE USART_IT_PE 奇偶校验错误时,产生中断请求

这些中间是"IT"的标志位,我们称之为中断标志位,它是SPL库特有的一套宏定义体系!

函数的最后一个参数则非常简单,只需要传参 ENABLE 表示开启此标志位中断,设为 DISABLE 则表示禁用。

USART_ITConfig()函数开启了USART 外设的中断源,使得USART外设可以产生一个中断请求。

但这个中断请求还需要交给NVIC处理,所以接下来还需要初始化NVIC,让NVIC去设置这个中断源(中断号),并且为此中断源设置优先级等。

所以处理外设中断的操作步骤就是:

  1. 开启外设中断源,使得外设能够在特定条件下产生一个中断请求。
  2. 初始化NVIC,通过设置中断号来开启此中断、设置优先级等,也就是让NVIC来管理这个外设中断请求,允许CPU相应处理此中断源。

NVIC_PriorityGroupConfig函数

在初始化NVIC时,核心的操作就是设置抢占优先级和子优先级,而在设置两个优先级之前,首先需要做的事情就是设置优先级分组。

这就需要调用NVIC_PriorityGroupConfig()函数来实现了。

此函数的声明也很简单,如下:

复制代码
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

其形参NVIC_PriorityGroup,用于指定优先级分组。

形参的具体传参使用一组宏定义,你可以在misc.h头文件中找到它们。

不同的宏定义取值决定了不同的优先级分组,具体细节参考下列表格:

宏定义(分组) 抢占优先级位数 子优先级位数 抢占优先级范围 子优先级范围
NVIC_PriorityGroup_0 0 4 无或者固定0 0~15
NVIC_PriorityGroup_1 1 3 0~1 0~7
NVIC_PriorityGroup_2 2 2 0~3 0~3
NVIC_PriorityGroup_3 3 1 0~7 0~1
NVIC_PriorityGroup_4 4 0 0~15 无或者固定0

所以在设置某个中断的优先级之前,需要先调用NVIC_PriorityGroupConfig函数来设置它的分组。

当然你也可以不调用这个函数来设置分组,默认采用的分组是NVIC_PriorityGroup_0,当然建议最好还是设置分组,比如使用分组2。

注意:

  1. 重复调用 NVIC_PriorityGroupConfig 会覆盖之前的设置,重新配置抢占优先级和子优先级位数的分配,可能会带未知的后果。
  2. 所以建议仅在main函数的while死循环上面,调用此函数一次就可以了。并且要在NVIC_Init初始化函数之前调用。

NVIC_Init函数

初始化NVIC就需要使用NVIC_Init函数,和我们之前初始化外设使用的"Init"函数,它也需要给一个结构体赋值来完成NVIC初始化。

NVIC_Init函数的声明很简单,只有一个形参:

复制代码
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

此函数需要传入一个NVIC_InitTypeDef类型的结构体对象指针,该结构体类型的声明如下:

复制代码
typedef struct {
 uint8_t NVIC_IRQChannel;                      // 要配置的中断通道(IRQ 号)
 uint8_t NVIC_IRQChannelPreemptionPriority;    // 抢占优先级
 uint8_t NVIC_IRQChannelSubPriority;           // 响应优先级(子优先级)
 FunctionalState NVIC_IRQChannelCmd;           // 开启(ENABLE)或禁用(DISABLE)
} NVIC_InitTypeDef;

其各个成员的解释如下:

IRQ:是中断请求(Interrupt Request)的缩写。

IRQChannel:直译是中断请求的通道,用于指定要配置的具体中断源,可以直接用中断号给这个成员赋值。

结构体成员 含义
NVIC_IRQChannel 需要配置的中断号(如 USART1_IRQnEXTI0_IRQn
NVIC_IRQChannelPreemptionPriority 抢占优先级(值越小优先级越高,可以打断低优先级中断)
NVIC_IRQChannelSubPriority 子优先级(同级抢占优先级时,子优先级越低,越先执行)
NVIC_IRQChannelCmd ENABLE (开启此中断)或 DISABLE(禁用此中断)

剩下的:

  1. 抢占优先级和子优先级这两个成员的赋值非常简单,可以直接根据优先级分组,填入一个合适的整数值即可。
  2. 最后一个成员表示开启或禁用IRQChannel对应的中断,根据使用目的选择即可。

注意:NVIC外设只需要初始化即可使用,不用考虑开启时钟,也没有时钟开启功能。

中断号/中断通道

中断号的问题(重点!)

上述NVIC_InitTypeDef结构体,在给第一个成员赋值时,需要填入一个中断号,那么什么是中断号呢?

中断号(IRQn,Interrupt Request Number)是 NVIC中,用于标识每个外设产生中断的唯一性编号。

现在,我们知道USART会产生一个全局中断请求,那么这个中断请求的中断号是什么呢?在哪里可以查这个中断号呢?

这个中断号切记不能随便填写,也尽量不要依靠记忆填写,建议的方式是------抄。

在头文件stm32f10x.h的一开头,定义了一个叫做IRQn/IRQn_Type的枚举类型,这个枚举类型中的枚举值就是全部中断号定义。

再加上我们使用的芯片是STM32F103C8T6,Flash闪存容量是64KB,属于中等密度(MD)芯片。

所以外设中断的中断号主要看这部分内容:

后续假如我们要配置别的中断请求,查找中断号时,也是来这里查。这些中断号的命名都非常显著,很好找。

到这里为止,除了中断服务程序还没有编写,其它流程涉及到的函数我们都学习了。

总结

  1. 初始化NVIC外设就是配置某个中断请求
  2. 其中的成员NVIC_IRQChannel,就是配置终端的唯一性编号
  3. 这个标号不要手写和记忆,要去stmf10x.h的头文件找到枚举类型IRQn_Type进行复制粘贴

中断服务程序的编写

现在我们已经实现了下列功能:

  1. 配置开启了USART外设的标志位中断源。
  2. 配置开启了NVIC的USART外设中断请求。

在完成上面两步后,只要主程序在运行的过程中,触发了中断源,NVIC 会自动调用对应的中断服务程序。

也就是NVIC 会自动调用对应的中断处理函数。

注意:官方的叫法是中断服务程序(ISR),但我更喜欢叫中断处理函数。它们是一回事,只是名字有区别罢了。

后续文档,一律不再区分中断服务程序和中断处理函数。

那么中断处理函数应该怎么写呢?

注意中断处理函数由NVIC自动调用,为了让NVIC能够找到这个函数,对某个中断源触发的中断处理函数,有明确的要求:

  1. 所有中断处理函数的返回值类型都是void,不允许自定义返回值类型!
  2. 所有中断处理函数的形参列表也必须是void,不允许自定义形参列表!
  3. 某个外设的中断处理函数,采用固定的函数名,不允许自定义函数名!
  4. 中断处理函数的具体实现内容可以由程序员自己决定,通常可以是任意实现!

那么,既然中断处理函数的名字是固定的,那么应该去哪里找这个名字呢?

中断处理函数的名字(重点)

在上面的章节中,我们提到过《中断向量表》的概念。

所谓中断向量表,就是一张"函数地址表",每一个中断类型的中断服务程序(ISR)都具有唯一对应的函数入口地址(函数指针)

下面这张截图来自《参考手册》,其中的地址是相对基准地址的偏移地址,并不是真实的内存地址。

那么在STM32的中断系统在运行的过程中,是如何找到对应的中断处理函数的呢?

思考过程:

函数地址也叫做函数指针,在C语言的时候我们学习过《函数指针》

我们知道:在C语言代码中,函数名就代表函数指针,就代表函数的地址。

那么在SPL库的代码中,有没有这样一段记录中断处理函数函数名(函数指针)的代码呢?

答:

当然有。

就在启动文件"startup_stm32f10x.s"中,几乎就在启动文件的一开头,存在下面的汇编代码片段:

这里实际上是《中断向量表》的函数名(函数指针)形式,和《参考手册》中的是完全对应的。

因此任何中断服务程序的函数名都是固定的,必须严格与启动文件中定义的名称保持一致,否则 CPU 在中断发生时就无法找到对应函数了。

中断服务程序就无法执行了。

所以:

如果需要编写中断处理函数,就去启动文件中找函数名,然后复制粘贴,禁止自行编写函数名!!!

完成串口中断服务程序的编写

在上面讲解USART外设中断机制时,我们已经知道:

整个USART1外设都只能产生一个全局中断USART1_IRQn(在stm32f10x.h头文件中找到,中断号在枚举类型IRQn_Type中定义)

它对应的中断服务程序的函数名为:USART1_IRQHandler(在 startup_stm32f10x.s启动文件中找到,就在此汇编代码文件的开头)

于是可以写出这个中断服务程序的函数声明:

c 复制代码
void USART1_IRQHandler(void);

中断服务程序的编写要求

中断时对主程序的补充,是处理必要且紧急情况的特殊手段,所以中断不能喧宾夺主,中断应该短平快,尽快执行完毕,不要让程序陷入中断执行,所以应当遵守以下几点:

  1. 中断服务程序的逻辑应当清晰明了,能短则短。能不放在中断处理的逻辑就不要放在中断来处理
  2. 中断服务程序的执行时间要短,应当能快则快
  3. 中断服务程序中禁止出现阻塞,不能延时,不能做循环,不能等待
  4. 中断服务程序中,应当正确判断中断源,以及要正确清楚中断标志位
  5. 主程序、中断服务程序乃至于硬件都共同处理的数据,最好使用volatile 关键字声明,避免出现并发访问问题。

中断服务程序不是用来完成特殊功能的,而是用来"及时处理紧急事件并尽快退出"的,而且优先级越高的中断,越要更快的完成并且退出。

USART全局中断应当判断中断源

  1. USART外设只能产生一个全局中断,但此全局中断的触发条件有很多,比如RXNE、TXE、TC等。
  2. 如果不对触发源进行判断,怎么知道是哪一个寄存器置位触发的中断呢?
  3. 如果不知道是哪个中断源触发的中断,就没有办法正确的处理发生事件。

此时就需要使用函数:USART_GetITStatus,该函数用于获取某个USART外设的中断标志位是否被置为1。

也就是获取下图当中的中断标志位的状态:

该函数的声明如下:

c 复制代码
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);

第一个形参很好理解,需要填入要查询的USART1外设具体的名字,比如直接填"USART1"。

第二个形参当中的IT是单词"Interrupt"的缩写,它表示各种USART外设中断标志位,可以取以下一些值:

中断标志 说明
USART_IT_RXNE 当接收数据寄存器非空时,该中断标志位置为1
USART_IT_TXE 当发送数据寄存器为空时,该中断标志位置为1
USART_IT_TC 所有数据发送完成时,该中断标志位置为1
USART_IT_PE 当出现奇偶校验错误时,该中断标志位置为1

具体的内容的大家可以自行查阅相关的手册,或者可以直接阅读源码注释。

此函数的返回值就很好理解了,返回的是此中断标志位的状态,也就是0和1两个状态。

ITStatus和FlagStatus其实是同一个枚举类型,这个枚举类型专门用来指示标志位状态的1和0。

c 复制代码
typedef enum {RESET = 0, SET = !RESET} FlagStatus, ITStatus;

此类型的取值只有两个:

返回值 类型 说明
SET ITStatus 置位,中断源条件已满足,可以发送中断请求
RESET ITStatus 复位,中断源条件未满足,不会发中断请求

中断标志位和普通状态标志位,在取值上没有任何区别,都是SET和RESET。

中断标志位的清零问题(重点)

学到这里,我们应该能够意识到以下事实:

  1. 当外设的中断标志位置位,且对应中断源处于开启状态时,此外设就会发送中断请求给NVIC。
  2. 在满足1的前提下,如果NVIC配置开启了此中断,那么就会调度执行对应中断服务程序。

那么你能否想到这样的一个问题:

如果中断标志位一直处于置位状态,始终没有复位清零,会发生什么情况呢?

答:

  1. 如果中断标志位一直处于置位状态,那么中断请求条件就始终成立。
  2. 当CPU执行完该中断服务程序后,就会再次检测到该中断请求,于是不断重新进入同一个中断服务程序
  3. 主程序就得不到机会执行了,此时程序就卡死在中断服务程序里了。

这显然是不合理的。

那怎么办呢?

当然是在中断服务程序中,在合适的位置清理复位中断标志位,这样中断就不会重复触发了。

换句话说:

中断服务程序的核心职责之一,就是在适当的时机清除中断标志位,否则中断将无法"结束"。

那么中断服务程序的什么位置适合清零中断标志位呢?

通常来说,在已经确定处理完本次中断事件之后,再清除中断标志位。

举例:

RXNE中断标志位置位的条件是:接收数据寄存器非空。

所以,只要确定读完了数据,就可以将该标志位置0清除。

但这里存在一个巧妙的硬件设计:

  1. 读数据,需要调用USART_ReceiveData函数。
  2. 此函数调用,会读接收数据寄存器。
  3. 而接收数据寄存器硬件的设计,使得读此寄存器就可以自动将RXNE标志位复位!!!

所以在串口RXNE中断的处理过程中,不需要手动复位RXNE标志位!

c 复制代码
// 这一行代码既完成了读数据,也完成了清零RXNE中断标志
uint8_t ReceiveCmd = (uint8_t)USART_ReceiveData(USART1);

需要特别注意的是,这种"自动清除中断标志位"的机制并不是所有外设、所有中断都具备的。

如果某个中断标志位在处理中不会被硬件自动清零,那么就必须在中断服务程序中,由软件手动将该标志位清除,否则中断将会反复触发。

在后续的学习中,我们马上就会遇到这种必须手动清零中断标志位的典型场景。

总结:使用中断的好处

计算机系统为什么要使用中断?使用中断有什么好处呢?中断在计算机系统中有何意义?

这三个问题的答案都可以通过上面的,一个简单的串口中断的案例得出。

计算机系统(无论嵌入式系统还是通用计算机系统)中:

  1. CPU的运行速度是最快的,其次是存储器,外设运行速度最慢。
  2. 当外设与CPU进行数据交换时,由于彼此数据处理速度上的差异,导致CPU的大部分时间都在等待外设,大大降低了CPU效率。

中断机制的出现解决了这个问题:

  1. 传统的实现方式,需要使用CPU持续轮询外设状态,导致大量时间浪费在等待慢速外设上。
  2. 采用中断方式:外设就绪时主动触发中断,CPU仅在需要时响应,其余时间处理其他任务,实现同步工作,显著减少空闲等待。

这就是使用中断的第一个好处:显著提升了CPU的利用率以及效率!

除此之外在嵌入式系统中,对实时性的要求很高,中断机制使CPU能立即处理紧急事件(如传感器信号、用户输入、错误故障等),满足实时控制系统的时效性需求。

总之,中断在计算机系统特别是嵌入式系统中占有极其重要的地位,中断机制可以说嵌入式系统的基石,是核心机制!

总结:外设中断开发的一般流程

以上,我们就学习了外设中断开发的所有前置知识和用到的函数,这里进行一个总结。

USART1 外设发出中断请求,NVIC 处理并执行中断 为例,完整的外设中断开发流程如下:

  1. 第一步:了解USART外设触发中断的条件与机制。
  2. 第二步:使用USART_ITConfig函数开启USART1标志位中断源,使得USART1外设可以在特定情况下向NVIC发送中断请求。
  3. 第三步:使用NVIC_PriorityGroupConfig函数设置优先级分组,为设置NVIC设置优先级做准备。
  4. 第四步:使用NVIC_Init函数设置USART1中断请求的优先级,并开启USART1中断,使得CPU可以响应执行此中断。
  5. 第五步:在startup_stm32f10x.s启动文件中找到 USART1 的中断处理函数名称,并编写 USART1_IRQHandler() 进行实际的中断处理。
  6. 第六步:编写中断服务程序时要注意判断中断源,以及注意中断标志位的复位问题。

当主程序运行过程中,USART1中断源条件满足,触发中断,NVIC 立即调用 USART1_IRQHandler() 自动执行中断处理逻辑。

实验:通过USART串口通信操控PC13指示灯

我们在此之前已经遇到过很多次阻塞函数的情况,存在死循环,导致我们的程序卡死,没有办法正常推进。那么现在我们就用中断来解决如何规避到阻塞的情况。下面我们使用一个实验来演示如何使用中断。

实验要求

通过USART串口输入控制PC13指示灯,

输入1:点亮

输入2:熄灭

输入3:慢闪烁

输入4:快闪烁

点亮和熄灭不会涉及到阻塞,但是灯的闪烁状态我们一般会用一个死循环来实现

框架设计

c 复制代码
// 我们使用到USART1外设和PC13指示灯
// 所以要先初始化PA9 PA10引脚 和USART1外设 以及PC13指示灯
void init_RxTx_PC13();
void init_USART();
// 我们用到中断就一定需要NVIC
// 因为外设提交中断到NVIC,只有NVIC开启了中断才可以使用中断
// 初始化NVIC
void init_NVIC();
// 我们通过USART1接收到的数据来实现控制灯的效果
// 但是TXE、TC、RXNE、PE这四个中断标志位任何一个置位1时,都会产生一个USART中断请求,发送给NVIC
// 我们开启外设中断源以及规定哪一个中断标志位可以触发中断
// 所以当有数据可读入时,我们触发中断,所以标志位设置RXNE
void USART_RXNEConfig();
// 中断执行后,需要告诉单片机需要做什么所以还需要一个中断服务程序
// 注意这里的函数名需要到启动文件中去找
void USART1_IRQHandler();
// 到这里我们的总体框架就搭建完了,但是需要额外说一点
// 在设置优先级数值之前,需要在main函数上面设置优先级分组
// 这可以放在init_NVIC()里面,也可以放在main()函数中,只要注意顺序即可

// 另外我们定义一个枚举类型,用来表示PC13指示灯的状态,以及设置一个变量来存储它的状态
typedef enum{
	On,				// 点亮
	Off,			// 熄灭
	Blink,			// 闪烁
	BlinkPlus		// 快闪说
} PC13State;

static PC13State pc13 = On;

代码实现

c 复制代码
#include "stm32f10x.h"
#include "../tools/Delay.h"
// 设置PC13状态
typedef enum {
    On,
    Off,
    Blink,
    BlinkPlus
} PC13State;
static PC13State pc13 = On;

// 初始化PA9 PA10引脚 PC13指示灯
void init_RxTx_PC13() {
    // 开时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC, ENABLE);
    // 初始化PA9引脚(可不初始化)
    GPIO_InitTypeDef GPIO_InitStruct;
    // 复用推挽默默是
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    // 默认设置为高电平
    GPIO_SetBits(GPIOA, GPIO_Pin_9);
	
    //	初始化PA10
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

	//初始化PC13
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(GPIOC, &GPIO_InitStruct);
}

// 初始化USART1
void init_USART1() {
    // 开时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    USART_InitTypeDef USART_InitStruct;
    // 设置波特率
    USART_InitStruct.USART_BaudRate = 115200;
    // 不使用硬件控制流
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    // 设置读写(可以只设置写)
    USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    // 无校验
    USART_InitStruct.USART_Parity = USART_Parity_No;
    // 使用1位终止符
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    // 数据位8位
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
  	// 初始化USART1
    USART_Init(USART1, &USART_InitStruct);
    // 使能开启USART1
    USART_Cmd(USART1, ENABLE);
}

// 初始化NVIC
void init_NVIC() {
    // 设置分组
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    // 初始化NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    // 设置中断号,中断号从stm32f10x.h头文件中抄,不要自行手写
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    // 开启USART1外设全局中断
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    // 设置抢占优先级
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
    // 设置子优先级
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
    NVIC_Init(&NVIC_InitStruct);
}

// 设置中断标志位
void USART_RXNEConfig() {
    // RXNE置位时,触发中断 --> 有新数据可读
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}

// 中断服务程序
void USART1_IRQHandler() {
    // 中断标志位置位时触发
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) {
        // 记录并清空数据寄存器
        char new_word = USART_ReceiveData(USART1);
		// 根据输入设置pc13指示灯状态
        switch (new_word) {
        case '1':
            pc13 = On;
            break;

        case '2':
            pc13 = Off;
            break;

        case '3':
            pc13 = Blink;
            break;

        case '4':
            pc13 = BlinkPlus;
            break;

        default:
            break;
        }
    }
}


int main() {
    init_RxTx_PC13();
    init_USART();
    init_NVIC();
    USART_RXNEConfig();
    while (1) {

        switch (pc13) {
        case On:
            GPIO_ResetBits(GPIOC, GPIO_Pin_13);
            break;

        case Off:
            GPIO_SetBits(GPIOC, GPIO_Pin_13);
            break;

        case Blink:
            GPIO_ResetBits(GPIOC, GPIO_Pin_13);
            Delay_S(1);
            GPIO_SetBits(GPIOC, GPIO_Pin_13);
            Delay_S(1);
            break;

        case BlinkPlus:
            GPIO_ResetBits(GPIOC, GPIO_Pin_13);
            Delay_Ms(200);
            GPIO_SetBits(GPIOC, GPIO_Pin_13);
            Delay_Ms(200);
            break;
        }
    }

}
相关推荐
hhcgchpspk7 小时前
easyx按键游戏
c++·stm32·单片机·游戏·easyx
魔法阵维护师7 小时前
从零开发游戏需要学习的c#模块,第二十一章(精灵动画 —— 让角色走起来)
学习·游戏·c#
行走的大喇叭7 小时前
Linux kernel目录、配置文件介绍
linux·单片机·嵌入式硬件
xian_wwq7 小时前
【学习笔记】探讨大模型应用安全建设系列6——合规备案:大模型备案与监管合规实操
笔记·学习·安全
0南城逆流07 小时前
【网站分享】常用网站分享四:STM32常用外设链接
stm32·单片机·嵌入式硬件
yu85939587 小时前
STM32 控制 W5500 以太网传输程序
stm32·单片机·嵌入式硬件
xian_wwq7 小时前
【学习笔记】探讨大模型应用安全建设系列7——安全评测与红队测试
笔记·学习·安全
LCG元7 小时前
STM32实战:基于STM32F103的车内防窒息系统(红外检测+GSM报警)
stm32·单片机·嵌入式硬件
_李小白7 小时前
【android opencv学习笔记】Day 21: 形态学开运算与闭运算
android·opencv·学习