嵌入式学习笔记——STM32的USART收发字符串及串口中断
前言
上一篇中,介绍了串口收发相关的寄存器,通过代码实现了一个字节的收发,本文接着上面的内容,通过功能函数实现字符串的收发,然后引入中断解决收发过程中while()死等的问题。
字符串的收发
发送一个字符串
根据昨天的字符发送函数,只需要稍作修改即可实现发送函数了,一个字符串的结尾会有一个’\0’作为结束符,所以再发送过程中,只需要判断当前发送的字符是不是结束符即可,如果不是结束符就将该位发送至电脑的串口调试助手,如果是结束符,那就意味着一个字符串发送完毕了。具体代码如下:
/*******************************************
*函数名 :Usart1_Send_Str
*函数功能 :串口1发送一个字符串函数
*函数参数 :u8 *str
*函数返回值:无
*函数描述 :
*********************************************/
void Usart1_Send_Str(u8 *str)
{
while(*str != '\0')
{
Usart1_Send_Byte(*str);
str++;
}
}
接收字符串
发送字符串相对容易,接收这边,就需要借用C语言中的数组来帮忙了,因为数据是一个字符一个字符的发送过来的,每一次只能接收一个字符,所以需要使用一个数组来存接收到的位,而且串口助手在发送字符串的时候是不会给单片机发送结束符,所以还需要编程者自己规定结束符,当然,后面引入空闲中断之后就不需要这样操作了。这里笔者使用的是‘#’作为结束标志。具体实现代码如下:
/*******************************************
*函数名 :Usart1_Receive_Str
*函数功能 :串口1接收一个字节函数
*函数参数 :void
*函数返回值:u8 str
*函数描述 :
*********************************************/
void Usart1_Receive_Str(void)
{
static u8 i=0;
//等待接收完成
while(!(USART1->SR & (1<<5)));
//将数据寄存器的数据读取到数组
Str_Buff[i] = USART1->DR;
i++;
if(Str_Buff[i-1]=='#')//如果检测到结束标志‘#’
{
Str_Buff[i-1]= '\0';//手动给字符串添加‘\0’结束符
i=0;
Usart1_Receive_Str_Flag=1;//接收完成的标志位置一
Usart1_Send_Str(Str_Buff);//将接受的数组再发回串口助手
}
}
需求
使用串口调试助手发送11打开1号小灯,10关闭一号小灯,21打开二号小灯,20关闭二号小灯。效果如下:

主函数代码:

利用串口实现printf
在C语言的学习中,使用频率最高的输出函数就是printf了,这个函数在单片机上也同样适用,只是要改一下输出的方向,所以也叫重定向。
在C中printf函数-----输出函数
输出方向:PC机------>屏幕
在单片机中printf-------输出函数
输出方向: 单片机---->PC机
关于具体的修改其实KEIL已经帮我们做好了,需要我们修改的只有一个,就是将“stdio.h”内的fputc,也就是字符输出函数,修改到和我们的字符串输出函数关联即可。具体代码如下:
//printf的重定向函数
//fputc-----专门发送字符的函数h
int fputc(int c, FILE * stream)
{
Usart1_Send_Byte(c);
return c;
}
将此代码放到USART1.c中即可,不需要调用也不需要声明。

然后就是勾选KEIL的库,如下图所示,依次选择魔法棒、Target、然后将3所在位置的复选框锁定。

然后再在USART1.h中添加stdio.h。

最后在主函数中调用printf即可,printf主要是方便我们的后期调试,实用语法与C一致。

实际输出效果:

到这里,已经实现了字符串的收发,但是存在两个问题,
1是上位机发送数据到板子上必须要设置结束符,类似笔者此处的‘#’;
2是此代码在接收时会阻塞在等待接受完毕的while,这会导致其他的模块工作不正常,在while(1)内,一定要尽力避免死等的出现。
很明显,现在这个代码还不太令人满意,那么要怎么修改呢,在修改代码之前,需要先去了解一个新的东西——中断。

中断
首先,需要知道中断是个什么东西,它有什么作用,具体怎么使用,下面一一来进行介绍。
中断是什么
中断嘛,按照名字的第一反应是终止一件事,打断某些东西的感觉,实际上也差不多是这个意思,它终止和打断的就是前面编写的main函数里面运行的东西。

也就是说在程序正常运行过程中,出现了不正常的事件(异常),CPU会优先去处理这个异常,处理完之后再回到正常的程序中。这个异常事件就是中断。
中断的目的:由外设或者CPU创造一个异常事件(紧急事件)
紧急事件发生的时间和地点:未知
紧急事件是实时响应的,紧急事件不能执行太久(里面不能有延时 循环 阻塞程序)
下面我们来看一张图:
代码正常运行的时候是如下图所示的蓝色箭头方向,首先会依次向下执行初始化代码,然后进入while循环,始终在任务1与任务2之间循环运行。
------->任务2
如果我们在初始化中,初始化了中断,就会有一个对应的中断服务函数,当中断的条件满足了,程序就会暂时终止main函数里面的内容,去把中断服务函数里面的内容执行完毕了再回来运行main里面的内容。

同时,从上图可以看出,当中断被初始化后,任何时刻都可能满足中断的条件,也就是在任何位置都可能会跳出main里面的内容去执行中断服务函数里面的内容。因此说它产生的时间和地点是未知的。
此时代码的运行顺序就不固定了可能是从任务1跳出去执行任务3任务4后再回来执行任务2
也有可能是直接就先去执行任务3,任务4后再回来执行任务1和任务2,等等还有多种可能,也就是只要初始化中断后,任何时刻,只要中断信号满足了,CPU就需要优先解决中断事件。
这正是中断的意义所在,类似上面的串口接收,没有中断的话,需要一直死等,这会严重影响其他函数的运行,引入中断后,就可以通过中断来判断是否接收完成了。那么具体是怎么配置的呢。
通过上面的流程图,大致分析一下,首先,肯定需要在初始化中初始化中断,然后是需要一个中断服务函数来存放异常事件,此外还需要判断对应的中断事件有没有产生。
串口的接收中断以及空闲中断
本文主要是接着串口先体验一下中断的作用,关于STM32中断系统的详细介绍放到下一篇中。
在配置串口的接收和发送的时候,串口相关的有很多寄存器是直接跳过了的,这里面就有一部分是和中断相关的,
首先,来看控制寄存器1(USART1-CR[0])的第四到第八位都是中断相关的使能位,也就是说,只要在代码中配置了相关的位,就可以在其描述的时刻产生对应的中断信号,这里面常用的就是第4位的空闲中断和第5位的接收完成中断;第4位的中断信号是在接收完成后一段时间内再也没有数据接收了就会被触发,所以叫做空闲中断的使能;而第5是接收中断,也就是每次接收完一个字符后就会触发一次中断;编程者可以使用这两个中断实现非阻塞的串口数据接收。当接收中断产生的时候就读取接受的字符到数组,当产生空闲中断的时候就在数组末尾添加结束标志。这样就省去了结束符,也解决了接收死等的问题。

串口的接收中断:
触发条件是:串口的DR寄存器接收到了值,接收到触发信号后,会将信号传递给NVIC控制器,NVIC控制器会将CPU调到中断服务函数中使用。
串口的空闲中断:
触发条件:在串口接收数据过程中,如果有一段事件串口停止工作了会产生触发信号。
在串口工作结束的时候产生一次中断请求
接收到字符串的时候没有结束条件,可以使用空闲中断作为判断字符串结束的条件
只要使用串口的模块,大部分都需要接收数据,但是如果使用基础的串口函数都会造成阻塞
实现代码
首先来分析一下实现流程,伪代码:
串口1的初始化函数
{
//打开时钟
//GPIO控制器配置
//USART1的控制器配置
加上串口的接收中断使能
//NVIC控制器的配置
优先级分组 ------ 主函数的初始化之上
优先级合成
优先级分配
使能中断源
}
在nvic.c中写中断服务函数即可
Void 串口1的中断服务函数名(void)
{
If(USART1->SR & (1<<5)) //开启了CR1中的接收中断使能
{
//清除标志位 //读取DR寄存器 同一个步骤
}
}
首先,肯定是需要实现串口的基础收发功能的,这个步骤在之前已经实现了,然后再其基础上开启串口的接收中断使能和空闲中断使能,也就是使能对应寄存器的第4位和第5位。
操作代码如下:
USART1->CR1 |= (1<<5); //使能接收中断
USART1->CR1 |= (1<<4); //使能空闲中断
需要将这两行代码添加到如下图所示的位置。

其实到这里有关串口中断的配置已经完成了,但是为了方便后面的对中断的管理,还需要对中断进行分组和设置优先级,原因是在整个系统中,中断不可能只有一个,为了避免中断之间的冲突,就需要对中断的优先级进行管理,也正是为了方便管理,stm32的所有中断都有固定的命名,具体的管理和分组留到下一篇中在做介绍,在本文只需要知道这一段代码的作用就是设置中断分组、优先级,还有就是打开对应的中断使能即可。
代码如下:
//NVIC控制器配置
u32 pri=NVIC_EncodePriority(7-2,0,0);//设置抢占优先级为0,响应优先级为0
NVIC_SetPriority(USART1_IRQn,pri); //将对应的优先级设置映射到对应的中断
NVIC_EnableIRQ(USART1_IRQn); //使能中断
此段代码的位置如下:

除此之外,还需要添加一句在所有的初始化之前的的中断分组函数
NVIC_SetPriorityGrouping(7-2);//设置中断的优先组别(111-110-101-100)

至此,有关串口中断的初始化部分就搞定了,然后就是中断服务函数,中断服务函数的名称也是固定的,必须使用.s文件内的命名才可以,否则会找不到的,而且中断服务函数在使用的时候不需要调用、声明,也没有返回值、形参。只需要按照.s的函数名命名即可。
具体代码如下:
还是需要在对应文件夹下新建Nvic.c和Nvic.h。并将Nvic.c添加到工程,在主函数的main.h添加Nvic.h的头文件。
#include "Nvic.h"
UsartStruct Usart1_Receive;//声明结构体
/*******************************
函数名:USART1_IRQHandler
函数功能:串口一的中断服务函数函数
函数形参:无
函数返回值:void
备注:
********************************/
void USART1_IRQHandler(void)
{
if(USART1->SR & (1<<5))//判断接收中断
{
Usart1_Receive.Rebuf[Usart1_Receive.ReBytes] = USART1->DR;
Usart1_Receive.ReBytes++;
}
if(USART1->SR & (1<<4))//判断空闲中断
{
USART1->SR;
USART1->DR;//清除标志位
Usart1_Receive.Rebuf[Usart1_Receive.ReBytes]='\0';
Usart1_Receive.ReBytes=0;
Usart1_Receive.UsartFlag=1;
}
}
Nvic.h
#ifndef _NVIC_H__
#define _NVIC_H__
#include "stm32f4xx.h"
#define RBUF_MAX 50
typedef struct
{
void (*Usart_Recelve_Process)(void);//串口数据处理函数
u8 Rebuf[RBUF_MAX]; //串口接收数据数组
u16 ReBytes; //串口接收数据长度
u8 UsartFlag; //串口接收到数据标志位
}UsartStruct;
extern UsartStruct Usart1_Receive;//外部声明
#endif
实现效果
为了方便看见效果,这里写了一个小需求,使用串口调试助手发送“open”,打开LED1和LED2,并通过printf返回小灯状态,发送“close”,关闭LED1和LED2。
注意需要使用到“String.h”里面的字符串处理函数,要将这个头文件添加到main.h
具体的main.c内容如下:
#include "main.h"
int main(void)
{
/*------------------变量定义区--------------------------*/
/*------------------初始化外设区------------------------*/
NVIC_SetPriorityGrouping(7-2);//设置中断的优先组别(111-110-101-100)
Led_Init();
Key_Init();
Usart1_Init(115200);
printf("系统初始化完毕\r\n");
/*------------------单次运行区--------------------------*/
while(1)//防止程序跑飞
{
/*------------------主循环区--------------------------*/
if(Usart1_Receive.UsartFlag==1)
{
Usart1_Receive.UsartFlag=0;
if(strcmp((const char*)Usart1_Receive.Rebuf,"open")==0 )
{
LED_1_ON;
LED_2(1);
printf("小灯已打开\r\n");
}
if(strcmp((const char*)Usart1_Receive.Rebuf,"close")==0)
{
LED_1_OFF;
LED_2(0);
printf("小灯已关闭\r\n");
}
}
}
}
最终运行效果:


总结
本文主要是进一步完善串口的收发功能函数,并引出串口中断的相关使用办法,由于基础的串口接收时会有阻塞作用,会导致while(1)内的其他功能无法正常运行,就像流水灯、按键扫描这些都会收到影响,所以就引入了串口中断,当对应的中断信号到了,说明已经接收到了数据,此时CPU跳出去先把数据接收,再回来继续运行while(1)的内容,这样就巧妙地回避了一直等待接受的过程。这一篇中代码有些多,写的也有些混乱,大家有建议可以指出,以错误之处也欢迎提出。
M4系列目录
1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)