文章目录
USART串口中断
在我们之前的实验中,就使用到了阻塞函数,导致程序不能正常的进行,在[STM32单片机学习(15) ------ PC串口通信实验]一文中我们进行控制LED和向OLED屏幕输出时,因为函数的阻塞会使得程序卡住,那么本文就来讲解一下USART串口外设的中断如何编写。
USART串口中断编程
我们对中断的需求是:
当 USART 外设检测到接收事件(即接收数据寄存器非空)时,应当立即触发中断,由中断服务程序完成指令接收与处理。
所以:
我们需要USART1外设来产生一个中断,即USART外设中断。
下面我们首先来了解一下,USART外设中断的产生机制。
USART外设中断产生机制
USART外设如何才能产生一个中断请求,如何才能发送一个中断请求给NVIC?
-
每一个USART外设,不管它内部是什么情况,最终只能产生一个全局中断。
比如USART1外设,它只能产生一个全局中断,发出一个全局中断请求
-
USARTx外设内部,TXE,TC,RXNE灯中断标志位置位后,都可以发出一个中断请求,也只能发送一个中断请求。尤其需要注意下图中展示的标志位,不是普通状态标志位,而是中断标志位
USART外设内部是可以产生中断请求,它产生中断的原理框图如下图所示:

图中有两个月牙状的"逻辑或门"电路符号。
表示:任何一个中断标志位置位都会触发产生全局中断。
首先说明一下图中标志位的问题:
-
TXE、TC、RXNE等都是标志位,在中断系统中,我们把这些标志位叫做"中断标志位"。
-
"中断标志位"和"普通状态标志位"的又有什么区别呢
中断标志位有一个RXNE,状态标志位也有一个RXNE有什么区别呢?
粗略的讲,一个同名的中断标志位,需要在状态标志位的基础上,再额外设置一些东西,然后在状态标志位的前提下,中断标志位才会置位,才会发送中断请求。这个过程我们称之为:开启标志位的中断源。
比如:
RXNE中单标志位,它想要置位有两个条件:
- 接收数据寄存器非空
- 开启此标志位的中断源,这样才能让中断标志位置位,发送中断请求。
总之:
中断标志位置位了,就一定发送中断请求给NVIC。但不做特殊处理,不开启标志位中断源,中断标志位不会置位的
-
我们需记住:对于USART外设来说,状态标志位和中断标志位的置位复位是同步进行的。
举一个例子:
- PC端向USART1外设发数据,此时接收数据寄存器非空,于是中断标志位RXNE自动置位。
- 程序员调用Receive库函数完成数据接收,中断标志位RXNE自动复位。
- 这和前面讲状态标志位RXNE的使用是完全一致的。
- 只不过在SPL库中,必须区分中断标志位和状态标志位,因为它们使用的函数是不同的。
除此标志位的问题外,你应该还注意到图中有很多"小开关"。
这些小开关的作用是:
USART外设必须开启相应中断,才能够在中断标志位置位时产生全局中断请求。
举个例子:
- 设置开启了RXNE标志位中断,等到RXNE中断标志位置位时,USART外设就会发中断请求。
- 没有开启TXE标志位中断,即便TXE中断标志位置位了,USART外设也不会发中断请求。
其实也很好理解:
发中断请求也需要"耗电",没事就发请求,不设置就能发请求,也太奇怪了!
最后再总结一下这张图:
- USART外设必须开启相应标志位的中断,USART外设才会发送中断请求。
- 在开启中断的前提下,TXE、TC、RXNE、PE这四个中断标志位任何一个置位1时,都会产生一个USART中断请求,发送给NVIC。
- 任何一个USARTx外设同时只能产生一个中断,即USARTx全局中断。
- 比如USART1外设无论如何只能产生一个中断请求,这个中断叫做USART1全局中断。
- 也就是说,无论USART1内部有多少种条件产生中断,但在NVIC看来,都只有一个全局中断。
USART外设中断的使用流程
结合前面,我们讲过的所有知识点,描述一下USART1外设中断的使用流程(以接收数据触发中断为例):
- 完成USART1外设的基本初始化配置工作(包括引脚和USART1外设本身)
- 开启RXNE标志位中断,使得USART1外设在RXNE中断标志位置位时,可以发送中断请求给NVIC。
- 在NVIC中配置USART1全局中断的优先级**(注意配置优先级前要设置分组)**
- 在NVIC中开启USART1全局中断。
- 手动编写USART1全局中断的中断服务程序,也就是编写对应中断处理函数。
以上流程完成后,中断的触发、执行等都由中断系统自动完成。如此,就实现了真正意义上,实时处理串口数据接收!
最后总结一下整个过程:
- USART1外设,负责在接收数据寄存器非空时,产生中断请求。
- NVIC负责居中调度管理。
- 中断服务程序,也就是中断处理函数,负责处理实时数据接收!
那么到这里,万事俱备,剩下的就是学习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外设名,比如 USART1、USART2。
函数的第二个参数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去设置这个中断源(中断号),并且为此中断源设置优先级等。
所以处理外设中断的操作步骤就是:
- 开启外设中断源,使得外设能够在特定条件下产生一个中断请求。
- 初始化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。
注意:
- 重复调用
NVIC_PriorityGroupConfig会覆盖之前的设置,重新配置抢占优先级和子优先级位数的分配,可能会带未知的后果。 - 所以建议仅在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_IRQn、EXTI0_IRQn) |
NVIC_IRQChannelPreemptionPriority |
抢占优先级(值越小优先级越高,可以打断低优先级中断) |
NVIC_IRQChannelSubPriority |
子优先级(同级抢占优先级时,子优先级越低,越先执行) |
NVIC_IRQChannelCmd |
ENABLE (开启此中断)或 DISABLE(禁用此中断) |
剩下的:
- 抢占优先级和子优先级这两个成员的赋值非常简单,可以直接根据优先级分组,填入一个合适的整数值即可。
- 最后一个成员表示开启或禁用
IRQChannel对应的中断,根据使用目的选择即可。
注意:NVIC外设只需要初始化即可使用,不用考虑开启时钟,也没有时钟开启功能。
中断号/中断通道
中断号的问题(重点!)
上述NVIC_InitTypeDef结构体,在给第一个成员赋值时,需要填入一个中断号,那么什么是中断号呢?
中断号(IRQn,Interrupt Request Number)是 NVIC中,用于标识每个外设产生中断的唯一性编号。

现在,我们知道USART会产生一个全局中断请求,那么这个中断请求的中断号是什么呢?在哪里可以查这个中断号呢?
这个中断号切记不能随便填写,也尽量不要依靠记忆填写,建议的方式是------抄。
在头文件stm32f10x.h的一开头,定义了一个叫做IRQn/IRQn_Type的枚举类型,这个枚举类型中的枚举值就是全部中断号定义。
再加上我们使用的芯片是STM32F103C8T6,Flash闪存容量是64KB,属于中等密度(MD)芯片。
所以外设中断的中断号主要看这部分内容:

后续假如我们要配置别的中断请求,查找中断号时,也是来这里查。这些中断号的命名都非常显著,很好找。
到这里为止,除了中断服务程序还没有编写,其它流程涉及到的函数我们都学习了。
总结

- 初始化NVIC外设就是配置某个中断请求
- 其中的成员NVIC_IRQChannel,就是配置终端的唯一性编号
- 这个标号不要手写和记忆,要去stmf10x.h的头文件找到枚举类型IRQn_Type进行复制粘贴
中断服务程序的编写
现在我们已经实现了下列功能:
- 配置开启了USART外设的标志位中断源。
- 配置开启了NVIC的USART外设中断请求。
在完成上面两步后,只要主程序在运行的过程中,触发了中断源,NVIC 会自动调用对应的中断服务程序。
也就是NVIC 会自动调用对应的中断处理函数。
注意:官方的叫法是中断服务程序(ISR),但我更喜欢叫中断处理函数。它们是一回事,只是名字有区别罢了。
后续文档,一律不再区分中断服务程序和中断处理函数。
那么中断处理函数应该怎么写呢?
注意中断处理函数由NVIC自动调用,为了让NVIC能够找到这个函数,对某个中断源触发的中断处理函数,有明确的要求:
- 所有中断处理函数的返回值类型都是void,不允许自定义返回值类型!
- 所有中断处理函数的形参列表也必须是void,不允许自定义形参列表!
- 某个外设的中断处理函数,采用固定的函数名,不允许自定义函数名!
- 中断处理函数的具体实现内容可以由程序员自己决定,通常可以是任意实现!
那么,既然中断处理函数的名字是固定的,那么应该去哪里找这个名字呢?
中断处理函数的名字(重点)
在上面的章节中,我们提到过《中断向量表》的概念。
所谓中断向量表,就是一张"函数地址表",每一个中断类型的中断服务程序(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);
中断服务程序的编写要求
中断时对主程序的补充,是处理必要且紧急情况的特殊手段,所以中断不能喧宾夺主,中断应该短平快,尽快执行完毕,不要让程序陷入中断执行,所以应当遵守以下几点:
- 中断服务程序的逻辑应当清晰明了,能短则短。能不放在中断处理的逻辑就不要放在中断来处理
- 中断服务程序的执行时间要短,应当能快则快
- 中断服务程序中禁止出现阻塞,不能延时,不能做循环,不能等待
- 中断服务程序中,应当正确判断中断源,以及要正确清楚中断标志位
- 主程序、中断服务程序乃至于硬件都共同处理的数据,最好使用volatile 关键字声明,避免出现并发访问问题。
中断服务程序不是用来完成特殊功能的,而是用来"及时处理紧急事件并尽快退出"的,而且优先级越高的中断,越要更快的完成并且退出。
USART全局中断应当判断中断源
- USART外设只能产生一个全局中断,但此全局中断的触发条件有很多,比如RXNE、TXE、TC等。
- 如果不对触发源进行判断,怎么知道是哪一个寄存器置位触发的中断呢?
- 如果不知道是哪个中断源触发的中断,就没有办法正确的处理发生事件。
此时就需要使用函数: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。
中断标志位的清零问题(重点)
学到这里,我们应该能够意识到以下事实:
- 当外设的中断标志位置位,且对应中断源处于开启状态时,此外设就会发送中断请求给NVIC。
- 在满足1的前提下,如果NVIC配置开启了此中断,那么就会调度执行对应中断服务程序。
那么你能否想到这样的一个问题:
如果中断标志位一直处于置位状态,始终没有复位清零,会发生什么情况呢?
答:
- 如果中断标志位一直处于置位状态,那么中断请求条件就始终成立。
- 当CPU执行完该中断服务程序后,就会再次检测到该中断请求,于是不断重新进入同一个中断服务程序。
- 主程序就得不到机会执行了,此时程序就卡死在中断服务程序里了。
这显然是不合理的。
那怎么办呢?
当然是在中断服务程序中,在合适的位置清理复位中断标志位,这样中断就不会重复触发了。
换句话说:
中断服务程序的核心职责之一,就是在适当的时机清除中断标志位,否则中断将无法"结束"。
那么中断服务程序的什么位置适合清零中断标志位呢?
通常来说,在已经确定处理完本次中断事件之后,再清除中断标志位。
举例:
RXNE中断标志位置位的条件是:接收数据寄存器非空。
所以,只要确定读完了数据,就可以将该标志位置0清除。
但这里存在一个巧妙的硬件设计:
- 读数据,需要调用USART_ReceiveData函数。
- 此函数调用,会读接收数据寄存器。
- 而接收数据寄存器硬件的设计,使得读此寄存器就可以自动将RXNE标志位复位!!!
所以在串口RXNE中断的处理过程中,不需要手动复位RXNE标志位!
c
// 这一行代码既完成了读数据,也完成了清零RXNE中断标志
uint8_t ReceiveCmd = (uint8_t)USART_ReceiveData(USART1);
需要特别注意的是,这种"自动清除中断标志位"的机制并不是所有外设、所有中断都具备的。
如果某个中断标志位在处理中不会被硬件自动清零,那么就必须在中断服务程序中,由软件手动将该标志位清除,否则中断将会反复触发。
在后续的学习中,我们马上就会遇到这种必须手动清零中断标志位的典型场景。
总结:使用中断的好处
计算机系统为什么要使用中断?使用中断有什么好处呢?中断在计算机系统中有何意义?
这三个问题的答案都可以通过上面的,一个简单的串口中断的案例得出。
计算机系统(无论嵌入式系统还是通用计算机系统)中:
- CPU的运行速度是最快的,其次是存储器,外设运行速度最慢。
- 当外设与CPU进行数据交换时,由于彼此数据处理速度上的差异,导致CPU的大部分时间都在等待外设,大大降低了CPU效率。
中断机制的出现解决了这个问题:
- 传统的实现方式,需要使用CPU持续轮询外设状态,导致大量时间浪费在等待慢速外设上。
- 采用中断方式:外设就绪时主动触发中断,CPU仅在需要时响应,其余时间处理其他任务,实现同步工作,显著减少空闲等待。
这就是使用中断的第一个好处:显著提升了CPU的利用率以及效率!
除此之外在嵌入式系统中,对实时性的要求很高,中断机制使CPU能立即处理紧急事件(如传感器信号、用户输入、错误故障等),满足实时控制系统的时效性需求。
总之,中断在计算机系统特别是嵌入式系统中占有极其重要的地位,中断机制可以说嵌入式系统的基石,是核心机制!
总结:外设中断开发的一般流程
以上,我们就学习了外设中断开发的所有前置知识和用到的函数,这里进行一个总结。
以 USART1 外设发出中断请求,NVIC 处理并执行中断 为例,完整的外设中断开发流程如下:
- 第一步:了解USART外设触发中断的条件与机制。
- 第二步:使用USART_ITConfig函数开启USART1标志位中断源,使得USART1外设可以在特定情况下向NVIC发送中断请求。
- 第三步:使用NVIC_PriorityGroupConfig函数设置优先级分组,为设置NVIC设置优先级做准备。
- 第四步:使用NVIC_Init函数设置USART1中断请求的优先级,并开启USART1中断,使得CPU可以响应执行此中断。
- 第五步:在
startup_stm32f10x.s启动文件中找到 USART1 的中断处理函数名称,并编写 USART1_IRQHandler() 进行实际的中断处理。 - 第六步:编写中断服务程序时要注意判断中断源,以及注意中断标志位的复位问题。
当主程序运行过程中,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;
}
}
}