目录
Preface:
(一)CUBEMX配置串口
(二)轮询方式
(三)中断 + DMA
串口通信协议简单,因此被广泛应用;串口有UART(Universal Asynchronous Receiver Transmitter,通用异步收发传输器),USART(Universal Synchronous Asynchronous Receiver Transmitter,通用同步异步收发传输器),在物理层,常用的有TTL(晶体管-晶体管逻辑,0V表示逻辑0,5V表示逻辑1),RS232(+3V~+15V表示逻辑0,-15V~-3V表示逻辑1),RS485(差分线);而这三者在数据链路层使用的是相同的协议,也就是常说的串口协议,基本参数有:数据位、停止位、奇偶校验位、波特率、过采样参数等等;
首先,根据串口原理图可知,外部的TX和RX分别接到了PA10(MCU的RX)和PA9(MCU的TX)也就是你发我收我发你收:
接下来进入cubemx,设置好调试,时钟之后,打开Connectivity,选择USART1-串口1,一般都用异步通信,因为同步通信需要连时钟线比较麻烦;选择异步通信后,默认选好了PA9和PA10,当然如果PA9或PA10被占用的话也可以自己选择重映射引脚;然后看到参数设置选项(左上角的!可以显示当前参数的description),首先看到波特率,可以发现范围为1.283K~5.25MBit/s,也就是可以写1283~5250000;字长可以是8bit或9bit(包含奇偶校验位),奇偶校验可选奇校验、偶校验或无校验;停止位有1bit或2bit;数据传输方向可选仅接收、仅发送或双向;过采样可选8次采样和16次采样,用于确定有效的起始位,8次采样速度更快(单位时间采8次,次数少速度快),但容错性差,16次采样速度慢,但容错性好,默认使用16次采样;其实参数选择是根据通信双方来定的,当然没定的话大家都保持默认即可,也不需要改参数;设置好之后直接生成代码即可:
轮询就是cpu一直死循环检测串口,等待数据到来,极其浪费cpu资源,一般来说都不会用这种方式;下面实现了一个轮询检测串口,收到数据后再通过串口发回去:
my_uart.h:
#ifndef UART_MY_UART_H
#define UART_MY_UART_H
#ifdef __cplusplus
extern "C" {
#endif
#include "main.h"
HAL_StatusTypeDef UART_SendData(UART_HandleTypeDef* uart, char* str, uint16_t size);
HAL_StatusTypeDef UART_ReceiveData(UART_HandleTypeDef* uart, uint8_t* pdata, uint16_t size);
#ifdef __cplusplus
}
#endif
#endif //UART_MY_UART_H
my_uart.cpp(实际上,对于接收数据函数就是把收到的数据传给发送函数,然后让发送函数通过串口发送回去;而对于发送函数,首先new一片内存,用来存放接收数据函数传过来的数据,然后用sprintf函数copy过来;why?为什么要copy?这不是多此一举吗?其实不然,这里仅仅发送字符串看不出sprintf的作用;事实上,如果还要通过串口发送数字之类的变量,就可以在发送之前全部用sprintf打包进一个字符数组中,一次性发完,个人感觉方便很多;;;最后不要忘记delete掉申请的内存,并将指针置空,以防野指针错误):
#include "my_uart.h"
#include "cstdio"
//串口发送数据函数
HAL_StatusTypeDef UART_SendData(UART_HandleTypeDef* uart, char* str, uint16_t size) {
HAL_StatusTypeDef status = HAL_OK;
char *temp = new char[size]; //申请内存
std::sprintf(temp, "%s", str); //copy数据
status = HAL_UART_Transmit(uart, (uint8_t*)temp, size, 0xffff);//阻塞直到发送完成
delete[] temp;
temp = nullptr; //释放内存并置空指针
return status;
}
//串口接收数据函数
HAL_StatusTypeDef UART_ReceiveData(UART_HandleTypeDef* uart, uint8_t* pdata, uint16_t size) {
HAL_StatusTypeDef status = HAL_OK;
status = HAL_UART_Receive(uart, pdata, size, 0xffff);//阻塞直到接收完成
return status;
}
主函数主要部分:
结果:
还有分析代码执行过程;首先打一个断点在接收函数中,并进入调试,如下图:
然后点击执行下一句,会发现系统进入阻塞状态,等待串口输入数据;当然如果过了太久没输入,就会超时,也会结束接收:
接下来,在串口助手中输入想输入的字符串(当然只能读取15个,因为前面指定了读15个;并且要在超时时间内输入):
我这里输入了“0123456789abc”(共13个,加上回车&换行一共15个)可以观察到数据已经被成功接收了,并且状态为HAL_OK,表示接收成功;也可以根据地址去内存看数据,如下:
接下来,进入发送函数,如下图(箭头指向的是待执行语句),可以发现字符串已经被正确地传过去了:
然后执行分配内存new语句(可以发现刚分配好内存时,其中的值是随机的),再执行字符串copy语句sprintf,并去内存中查看是否成功;如下图:
然后就是将数据发送出去了,最后别忘了释放内存(继续查看内存会发现,释放了内存而内存中该地址的值却没有变,只有前面的两个字节变了,我猜测这两个字节应该是表示后面这段内存是否已被分配),不然就会产生内存泄漏:
以上就是串口轮询的分析过程~~~
工程链接:https://pan.baidu.com/s/1XDGoVidEDXZgnw1muTyaWQ
提取码:0xFF
事实上,串口有许多个中断,不太可能全部列出来,我认为最常用的就是RXNE(接收缓冲非空中断)、IDLE(空闲中断)、TC(发送完成中断)了;然后就是DMA,可以直接从外设把数据传送到内存(或者反过来)而不需要cpu干预,只需要在数据开始发送的时候指定参数,当数据传输完毕就会产生一个DMA传输完成的中断给cpu,让cpu来进行接下来的处理;那为啥要把中断和DMA放一起?主要是一般来说,数据接收者预先都不知道会收到多少数据,用轮询是不太能做得到的;而如果用中断一个字符一个字符地中断接收的话又导致cpu被频繁中断,拉低了性能;那能不能说收完数据再产生一次中断了,这样明显更合理,而这就是串口空闲中断+DMA;用DMA接收数据,当接收完数据之后,等到了总线空闲状态,串口就会产生一个IDLE空闲中断;那什么是空闲状态呢?空闲就是总线上在一个字节的时间内没有再接收到数据。空闲中断是从检测到有数据被接收开始,总线上在一个字节的时间内没有再接收到数据的时候发生的。一般就只有一个数据帧发送完成的时候,总线在会在一个字节时间内没有接收到数据,所以串口的空闲中断也叫帧中断。
我之前有写过一个串口空闲中断+DMA来接收不定长数据的,在这里;但其实,还是有一点bug的;比如说:
1、连着接收两帧数据,但是这两帧数据间隔很近,不够一个字节的空闲时间,这就导致只产生一次空闲中断,出现错误;
2、正常的一问一答数据交流是没有问题,只要保证每次接收不同帧的数据间隔大于一个字节的空闲时间即可,也就是保证一帧数据对应一个空闲中断;但是如果是MCU要接收很大的数据量,并且是连着接收,实时性又高(相当于bug 1中的升级版吧),就会导致:在所有数据传输完之前,一次空闲中断都不会发生,cpu不知道DMA已经收了多少数据,势必会导致出错。
暂时没有想到很好的解决办法......
完~
以上均为个人学习心得,如有错误,请不吝赐教~
THE END