目录
总览
串口是常用的设备与设备之间发送和接受信息的工具,下面我们将使用STM32F103C8T6和CUBEMX, Keil 讲解如何使用CUBEMX配置串口异步通信下的三种模式(轮询模式、中断模式、DMA模式)以及相关HAL库函数。以及使用HAL库拓展函数实现串口不定长数据的接收
使用CUBEMX创建工程的基本配置
CUBEMX中的配置
打开软件,选择芯片类型,可以在左上角搜索芯片型号,选择好后点击Start Project创建一个新工程
首先在左栏找到System Core->SYS->Debug中,将其更改为Serial Wire
再来到RCC中修改高速外部时钟为外部晶振
接下来点击Clock Configuation配置时钟树,使用PLLCLK并将其时钟源修改为HSE,最后在HCLK中修改频率为72MHz,点击键盘上回车,Cubemx就会帮你将剩下的参数自己配置好
接下来来到Project Manager配置工程路径以及生成工程类型,这里由于我们使用的是Keil,所有在IDE选项中选择MDK-ARM即可
点击Code Generator,将第一项修改为只复制必要的文件,可以节省项目所占空间,接着在第二个选项中勾选生成独立的.c/.h代码,方变我们进行修改
最后一步,配置一个串口:回到Pinout&Configuration,在Connectivity中找到USART1,将其Mode修改为Asynchronous即异步通信模式
完成后我们就可以看到串口输出输入管脚分辨对应单片机上PA9以及PA10,RX为输入(即接受Receive)TX为输出(即发送Transmit)
配置好一个串口就是这样了,如果还需要中断、DMA模式可以返回继续配置
接下来点击右上角Generate Code就可以生成keil工程了,点击弹出的Open Project就可以打开新建的工程了
Keil中的配置
首先选择烧录器,我使用的是ST-link,点击魔术棒,在USE中修改其为St Link Debugger
接着点击Unilities选项,点击Settings,在弹出的选项卡中勾选Reset and Run
接着在Pack选项卡中取消勾选Enable,这样我们在烧录过后就可以不用手动复位了
keil的基本配置到此也结束了
实物连接
这里需要一些必要的驱动还需要USB转TTL模块:
CH340串口驱动,下载地址:
CH340 Drivers for Windows, Mac and Linux (gogo.co.nz)
ST-Link驱动,下载地址:
https://www.st.com/en/development-tools/stsw-link009.html
我用的串口软件是逐飞助手,下载地址:
【软件】逐飞助手上位机 · SeekFree/逐飞助手 - 码云 - 开源中国 (gitee.com)
具体的下载安装就不阐述了,基本都是跟着步骤来就好
连接的时候:将USB-TTL模块上的,A9->RX;A10->TX,如下图
串口轮询模式
轮询模式HAL库函数
//串口发送函数 HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); //参数分别为串口句柄,发送数据地址,发送数据大小,超时时间,举例 uint8_t TxBuffer[5]={1,2,3,4,5}; HAL_UART_Transmit(&huart1,TxBuffer,sizeof(TxBuffer),50);
//串口接收函数 HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); //参数分别为串口句柄,发送数据地址,发送数据大小,超时时间,举例 uint8_t RxBuffer[5]={0}; HAL_UART_Receive(&huart1,RxBuffer,sizeof(RxBuffer),50);
特点
只能接受或发送指定长度,或者在超时时间内接受或者发送,该等待时间内CPU阻塞
实验一:发送数据给单片机并让其返回相同值
打开刚刚创建的keil工程
1. 打开Application/User文件夹中的main.c文件
2. 下滑找到main函数,将代码写进主函数,代码在图片后面(将代码写在begin和end块之中可以防止再次生成时所写代码不被覆盖)
3. 点击③所示按钮进行编译,下方无error即可
4. 点击load将编译后生成的hex文件下载进单片机
uint8_t RxBuffer[5] = {0}; if(HAL_UART_Receive(&huart1,RxBuffer,sizeof(RxBuffer),1000)==HAL_OK) HAL_UART_Transmit(&huart1,RxBuffer,sizeof(RxBuffer),50);
接着打开串口调试助手,按照如图所示连接好串口,选择带有CH340的即可
接着给单片机发送数据,由于RxBuffer大小仅有5字节,所以一次只给单片机发送5字节数据,观察是否会返回。可以看到,在接收到数据后,单片机又将发送的数据返回
串口重定向
有c语言基础的同学都知道,我们常用接收和发送信息的函数是scanf和printf,进行串口重定向后,我们也可以在工程中使用该函数,步骤如下
首先,打开魔术棒,勾选微库
下拉main.c找位置重新改写fputc和fgetc函数
在上面引用函数库
接着添加改写后的函数
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; } int fgetc(FILE *f) { uint8_t ch; HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY); return ch; }
接着回到主函数while()中修改原来的代码
编译、下载后打开逐飞助手,就可以得到以下结果
串口中断模式
在CUBEMX中打开串口中断
回到cubemx界面,在刚刚的USART1界面找到NVIC Settings 勾选中断选项后点击生成代码
中断模式HAL库函数
//串口中断发送函数 HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //参数与轮询模式差不多,但比轮询模式少了超时时间,中断情况下不需要使程序阻塞在此等待,使用例子如下 uint8_t TxBuffer[10] = {0}; HAL_UART_Transmit_IT(&huart1, TxBuffer, sizeof(TxBuffer));
//串口中断接收函数 HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //使用和上面的大致相同,这里不做过多阐述
这里需要注意的是:串口中断接收函数尽量不要放到while循环里,以免上一次接受还没完成就开启了下一次接受,出现BUG
串口中断接收函数在接收完成后会产生一次中断,我们可以在对应的中断回调函数中处理接收到的数据以及开启下一次接收,该中断回调函数需要我们自己重定义,模板如下
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){ if(huart == &huart1){ //若有多个串口,接收完成后依然调用该中断回调函数,所以在书写中断回调函数时养成加判断的习惯 //用户书写处理接收数据函数 } HAL_UART_Receive_IT(huart, pData, Size); //再次启动串口接受中断用于接受下一次的数据 }
发送完成只有一样有其对应的中断回调函数,该函数为:
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
由于不经常使用,所以这里就不过多阐述,大致使用方式与接受完成回调函数相似
特点
解决了CPU阻塞问题,但仍然无法实现不定长数据接受
实验二:使用中断回调完成实验一
按照上述方法配置完CUBEMX后,接下来进行keil代码的编写,首先将上个实验中自己添加的代码删除。
由于需要在多个函数中调用RxBuffer,将其提升为全局变量,可以写在自带的PV(Private Varibles)注释对中
接下来在主函数中打开串口接收中断,注意,不要放在While()中
接下来在USER CODE BEGIN 4注释对中编写中断回调函数,代码及其功能详解在图片下方代码块中
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){ //接收中断回调函数 if(huart == &huart1){ //判断返回串口 HAL_UART_Transmit_IT(huart, RxBuffer, sizeof(RxBuffer)); //开启发送中断打印接收到的信息 } HAL_UART_Receive_IT(huart, RxBuffer, sizeof(RxBuffer)); //启用接收中断准备下一次数据的接收 } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){ //发送中断回调函数 if(huart == &huart1) { uint8_t SucMsg[11] = "\nsuccessful"; HAL_UART_Transmit(huart, SucMsg, sizeof(SucMsg), HAL_MAX_DELAY); //发送发送成功的信息,注意不要使用中断的形式,否则会反复调用该中断 } }
注:代码块successful中增加了一个"\n"是为了换行好观察数据
接下来重复编译烧录的过程,然后打开串口助手,发送一个10字的消息,得到结果如下
中断模式下的使用就到此为止了
串口DMA模式
Direct Memory Access-直接内存访问,在创建完DMA通道后,它会在合适的时机自动将数据转移,不占用CPU进行转移,且在转移完成后触发一次中断表明转移完成
CUBEMX中添加DMA路径
点击DMA settings->Add中添加如图所示两条路径,配置默认即可
设置好后点击generator code重新更新代码,(注意之前的NVIC中的中断也要打开)
DMA模式HAL库函数
//串口DMA发送函数 HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //格式与使用方法与中断模式 uint8_t TxBuffer[10] = {0}; HAL_UART_Transmit_DMA(&huart1, TxBuffer, sizeof(TxBuffer));
//串口DMA接收函数 HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); //使用和上面的大致相同,这里不做过多阐述
特点
进一步减少了对CPU的占用,仍无法实现不定长数据接受
实验三:使用DMA模式完成实验一
DMA传输完成后也是调用TxCpltCallback与RxCpltCallback函数
故我们只需要在原keil代码中把IT换成DMA即可
编译下载后打开串口助手,调试结果如下
串口不定长数据接收
串口空闲中断
空闲中断发生时,表明一帧数据包接受完成,在此时对数据进行分析处理即可
串口接受HAL库扩展函数
HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout);//阻塞轮询 HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//中断 HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);//DMA
注意:由于接受的是不定长数据,可以把用于接收的数组长度写大一点 Size不表示期望接受的数据长度,而是一次可接受的数据的最大长度,一般填写接受数组的长度
串口中断空闲回调函数
HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);
//一般需要用户重定义,且完整书写用户自定义回调函数格式应为: void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){ if(huart == &huart2){ //用户书写处理接收数据函数 } HAL_UARTEx_ReceiveToIdle_DMA(huart, pData, Size); //再次启动串口接受中断用于接受下一次的数据,此处采用DMA,采用中断模式也可以 __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); //关闭DMA传输过半中断,如果上面采用中断模式则不需要 }
注意:比RxCpltCallback函数多出参数Size,该参数表示接收到的数据长度 使用ReceiveToIdle函数后,不再调用RxCpltCallback回调 使用DMA模式的ReceiceToIdle会触发传输过半中断(接收到Size一半的数据),如果想接受完整数据,则需要将其关闭
特点
结合DMA模式后,实现了对CPU的最小占用程度和对不定长数据的接收和处理
实验四:使用扩展函数完成实验一并实现不定长数据接收
由于我们在CUBEMX中已经打开DMA以及中断,所以CUBEMX中可以不用再做配置
打开keil,删除上次实验写的代码
首先添加一个比较大的接受数组,然后添加一个了来自外部文件usart.c的句柄
然后在主函数中开启一次接受,跟上面一样,不要放在while()中
最后在注释对中添加中断回调函数
所有添加的代码及注释都在这里啦
uint8_t RxBuffer[50] = {0}; extern DMA_HandleTypeDef hdma_usart1_rx; HAL_UARTEx_ReceiveToIdle_DMA(&huart1,RxBuffer,50); __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); //关闭过半中断 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size){ if(huart == &huart1){ HAL_UART_Transmit(huart, RxBuffer, sizeof(RxBuffer),HAL_MAX_DELAY);//传输回接受的数据 for(uint8_t i = 0;i < sizeof(RxBuffer); i++) RxBuffer[i] = 0;//将上一次的数据清零,若不清零,下一次的数据若小于上一次,则会保留上一次多出来的数据 } HAL_UARTEx_ReceiveToIdle_DMA(huart, RxBuffer, 50); //再次打开接受 __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); //关闭过半中断,以获得完整数据 }
编译下载后打开串口助手,最后的结果就是这样啦
以上就是串口的基本常用用法以及其实践了,欢迎大家讨论