通讯协议之路主要分为两部分,第一部分从理论上面讲解各类协议的通讯原理以及通讯格式,第二部分从具体运用上讲解各类通讯协议的具体应用方法。
后续文章会同时发表在个人博客(jason1016.club)、CSDN;视频会发布在bilibili(UID:399951374)
本文前缀:
通讯协议专栏:通讯协议_JASON丶LI的博客-CSDN博客
UART理论部分:通讯协议学习之路:UART协议理论-CSDN博客
具体实践方案选择:
1、轮询模式
程序必须轮询状态位以检查是否已收到新字符并以足够快的速度读取它以获得所有字节
优点
很容易实现,但在真正项目中的应用很少
缺点
在突发数据中很容易错过接收到的字符
仅适用于低波特率
应用程序必须非常快速地检查是否收到新的数据
2、中断模式
UART触发中断,CPU跳转到服务程序处理数据接收
优点
目前程序中最常用的方法
在低速率下工作良好,115200 波特
缺点
为每个接收到的字符执行中断服务程序
可能会在具有许多中断的高性能 MCU 中停止其他任务
一次接收突发数据时可能会停止操作系统
3、DMA模式
🍀🍀🍀注意这里的DMA模式接收不定长数据时是检测IDLE空闲中断标志位来判断DMA接收是否完成的,但是本人在使用proteus仿真中,IDLE一直都不会挂起导致仿真的DMA接收无法实现,后续会想方案解决
DMA 用于在硬件级别将数据从 USART RX 数据寄存器传输到用户存储器。 除了在必要时由应用程序处理接收到的数据,此时不需要应用程序交互
优点
i.从 USART 外设到内存的传输是在硬件完成的,无需 CPU干涉
ii.可以很容易地与操作系统一起工作
iii.针对最高波特率 > 1Mbps 和低功耗应用进行了优化
vi.在大量数据突发的情况下,增加数据缓冲区大小可以改进功能
缺点
i.DMA 硬件必须事先知道要传输的字节数
ii.如果通信失败,DMA 可能不会通知应用程序所有传输的字节
本文仅关注接收未知数据长度的 DMA 模式。
开发实践
对于usart的开发实践,其实并没有学习理论时预想的那么负责,因为目前市面上绝大部分单片机芯片内核都已经配备了完整的U(S)ART固件,相当于厂家已经配置好了对应的协议传输方案,我们要做的就是简单地配置一下其已存在的固件以及对应数据传输的规则即可。
本文以STM32F103C8为例,分两种方式进行usart协议通讯的配置,分别给标准库用户和HAL库用户详细的配置解决方案(ps:寄存器开发没有,作者寄存器开发不太熟练...)
一、标准库
中断模式:
单片机知识巩固
流控:【STM32学习笔记】USART 硬件流控 - 知乎 (zhihu.com)
usart协议的使用核心在与配置与数据的收发处理,对于标准库而言,USART的配置核心在与IO口的配置,而数据的收发核心在于数据的发送格式和接收缓存标注位,接收数据筛选。
1、UART的配置
USART协议硬件通道,在单片机的配置,本人有自己的一点想法。之前在理论中提到了单片机拟人化的概念,在这章我将继续延续这个概念进行介绍。先让我们回忆一下:之前提到晶振就是单片机的心跳信号,每实现一次晶振的跳变信号,单片机就执行一次指令周期;而各类的总线就是血管和供血,各类的IO口和IO协议就是单片机感知世界与世界交流的感官。
不知道大家有没有听过捕食者效应,那就是当你饥饿的时候,你会发现你的感官会变得更加灵敏(比如我考试之前一般都不吃饭的),这是什么原因呢?原因就是当我们空腹时,我们的消化系统就不需要工作,血液的占用就少了,但是血液的总量是不变的,因此心脏每一次泵血,血液就可以流向人身体内更需要他的地方。
回归单片机,正常单片机这么多IO口,当他每一个都开启工作的时候,是不是就得持续给他们供电以及随时随地管理信号呀,这会导致单片机的能耗变得非常高,那怎么解决呢?因此单片机所有的外设默认都是失能的,什么意思呢,就像刚刚提到的空腹状态,就是单片机不需要消化这个外设通讯,因此我们就可以失能消化系统这类的外设时钟,就是不给他供血了。那当我们发现我们需要消化食物了,那我们就重新开启这个外设的时钟就行了。这种按需开启,有利于单片机大大节省能耗以及降低CPU占用率,通常我们都是用啥就开啥,其他的就不管了。
而单片机GPIO、USART、SPI、NVIC等这类型外设的配置,以及各类的配置选项,就需要大家各自去掌握了,这里附上STM32F103的库函数编程文档供参考。
配置阶段分为:
- 使能GPIO时钟
- 使能USART时钟
- 配置GPIO口
- 配置USART口
- 开启USART中断模式
- 配置NVIC中断模式
- 使能usart
具体配置方案以及配置原因参考下列代码。
void USART1_Init(u32 bound) //USART1初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1(ck=PB8,TX=PA9,RX=PA10)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA
GPIO_InitTypeDef GPIO_Initstructure; //定义GPIO结构体
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP; //设置GPIO为复用推挽输出
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_9; //设置为Pin9
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz; //设置为50MHZ速度
GPIO_Init(GPIOA, &GPIO_Initstructure); //按照上述结构体配置初始化GPIOA
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_IPU; //设置GPIO为浮空输入
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_10; //设置为Pin9
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz; //设置为50MHZ速度
GPIO_Init(GPIOA, &GPIO_Initstructure); //按照上述结构体配置初始化GPIOA
USART_InitTypeDef USART_Initstructure; //定义UASRT结构体
USART_Initstructure.USART_BaudRate = bound; //配置波特率
USART_Initstructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //配置流控
USART_Initstructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //配置通讯方式,一般选择双控
USART_Initstructure.USART_Parity = USART_Parity_No; //配置是否需要校验位(需要对应下面的数据长度WordLength)
USART_Initstructure.USART_StopBits = USART_StopBits_1; //配置停止位
USART_Initstructure.USART_WordLength = USART_WordLength_8b; //配置数据长度(可选8位和9位,若设置校验位则配置9位,否则配置8位即可)
USART_Init(USART1, &USART_Initstructure); //按照上述结构体配置初始化USART1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能USART中断
NVIC_InitTypeDef NVIC_Initstructure; //定义NVIC结构体
NVIC_Initstructure.NVIC_IRQChannel = USART1_IRQn; //设置NVIC管理USART1中断
NVIC_Initstructure.NVIC_IRQChannelCmd = ENABLE; //确定使能
NVIC_Initstructure.NVIC_IRQChannelPreemptionPriority = 1; //设置抢占优先级为0
NVIC_Initstructure.NVIC_IRQChannelSubPriority = 1; //设置等候优先级为0
NVIC_Init(&NVIC_Initstructure); //按照上述结构体配置初始化NVIC
USART_Cmd(USART1, ENABLE); //使能UASRT1
}
库函数配置关键点:
- @param USART_FLAG:指定要检查的标志。
- @arg USART_FLAG_CTS: CTS更改标志(不适用于UART4和UART5)
- @arg USART_FLAG_LBD: LIN中断检测标志
- @arg USART_FLAG_TXE:传输数据寄存器空标志
- @arg USART_FLAG_TC:传输完成标志
- @arg USART_FLAG_RXNE:接收数据寄存器不空标志
- @arg USART_FLAG_IDLE:空闲线检测标志
- @arg USART_FLAG_ORE:超限错误标志
- @arg USART_FLAG_NE:噪声错误标志
- @arg USART_FLAG_FE:帧错误标志
- @arg USART_FLAG_PE:奇偶校验错误标志
- @param USART_IT:指定USART中断源要启用或禁用。
- @arg USART_IT_CTS: CTS更改中断(不适用于UART4和UART5)
- @arg USART_IT_LBD: LIN中断检测中断
- @arg USART_IT_TXE:传输数据寄存器空中断
- @arg USART_IT_TC:传输完成中断
- @arg USART_IT_RXNE:接收数据寄存器不空中断
- @arg USART_IT_IDLE:空闲线检测中断
- @arg USART_IT_PE:奇偶校验错误中断
- @arg USART_IT_ERR:中断错误(帧错误,噪声错误,溢出错误)
2、UART的发送
USART发送发本质就是调用USART_SendData()函数发送信息,各类的信息发送都是基于该函数变形而得的。
void Serial_SendByte(uint8_t Byte) //编写发送函数
{
USART_SendData(USART1, Byte); //发送字节函数
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //检验发送是否完成
}
//发送一个数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
//发送一个字符串
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != 0; i++)
{
Serial_SendByte(String[i]);
}
}
//取X的Y次方(用于下列运算)
uint32_t Serial_Pow(uint32_t X,uint32_t Y)
{
uint32_t Result =1;
while (Y--)
{
Result *=X;
}
return Result;
}
//发送一串数字(数字需要针对每一位数字进行对应的除法和求余)
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number/Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
//重写fputc函数
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
//封装printf可变参数格式
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
3、UART的接收
UART数据接收的本质
当已使能的UART信道接收到信息的时候触发usart中断,
然后在中断事件中调用USART_GetITStatus()函数检查接收数据寄存器是否为空
若检查到数据则将数据转移到Serial_RxData中进行数据的存储,同时挂起标志位Serial_RxFlag,方便后续要轮询模式中进行数据的打印或调用操作.
最后调用USART_ClearITPendingBit()函数进行中断挂起状态的清除,退出中断并方便下次再次进入中断.
uint8_t Serial_RxData; //定义UASRT1_RX数据缓存
uint8_t Serial_RxFlag; //定义USART1_RX接收标志位,用于后续TX特定数据的接收
char Serial_RxPacket[100];
//调用标志位和重置标志位
uint8_t Serial_GetFlag(void)
{
if (Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData;
}
//无限制接收
//void USART1_IRQHandler(void)
//{
// if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
// {
// Serial_RxData = USART_ReceiveData(USART1);
// Serial_RxFlag = 1;
// USART_ClearITPendingBit(USART1, USART_IT_RXNE);
// }
//}
//接收筛选,接收数据包
void USART1_IRQHandler(void) //编写USART1中断函数处理接收事件
{
//static为静态变量,只需定义一次(只能再规定函数中执行的全局变量)
static uint8_t RxStare = 0;
static uint8_t pRxPacket = 0;
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if (RxStare == 0) //状态1
{
if (RxData == '@' && Serial_RxFlag == 0)
{
RxStare = 1;
pRxPacket = 0;
}
}
else if (RxStare == 1) //状态2
{
if (RxData == '\r')
{
RxStare =2;
}
else
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
}
}
else if (RxStare == 2) //状态3
{
if (RxData == '\n')
{
RxStare = 0;
Serial_RxPacket[pRxPacket] = '\0';
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1, USART_FLAG_RXNE);
}
}
DMA模式:
参考文档:
STM32 DMA串口发送模式配置及使用简单分享 - 知乎 (zhihu.com)
STM32 | 串口DMA很难?其实就是如此简单!(超详细、附代码)-CSDN博客
对于DMA转运,核心就在于DMA通道的配置与选取,发送不需要用到cpu,接收需要进入cpu的中断模式进行数据的处理
对于DMA的发送,核心在于调用DMA_Cmd(DMA1_Channel7, ENABLE);函数,将原先设定好的CNDTR(数据长度)的CMAR(数据地址)数据发送出去
对于DMA的接收,分为定长与不定长两种接收方式
定长方式采用DMA1_Channel6_IRQHandler中断方式,当接收数据缓存满了之后就会发生中断,将数据缓存内的所有数据读取。
不定长方式采用USART2_IRQHandler(IDLE方式)的中断方式,接收串口空闲标志位,当数据接收完存入数据缓存之后就会触发IDLE中断,这是我们将读取数据缓存内的数据,同时检查数据缓存内的剩余容量,这样数据长度就等于数据缓存总容量-数据缓存剩余容量,当得知数据长度与数据地址后就可以实现不定长数据的接收了。
配置:
#include "usart2.h"
//USART2_MAX_TX_LEN和USART2_MAX_RX_LEN在头文件进行了宏定义,分别指USART2最大发送长度和最大接收长度
u8 USART2_TX_BUF[USART2_MAX_TX_LEN]; //发送缓冲,最大USART2_MAX_TX_LEN字节
u8 u1rxbuf[USART2_MAX_RX_LEN]; //发送数据缓冲区1
u8 u2rxbuf[USART2_MAX_RX_LEN]; //发送数据缓冲区2
u8 witchbuf=0; //标记当前使用的是哪个缓冲区,0:使用u1rxbuf;1:使用u2rxbuf
u8 USART2_TX_FLAG=0; //USART2发送标志,启动发送时置1
u8 USART2_RX_FLAG=0; //USART2接收标志,启动接收时置1
void Initial_UART2(unsigned long baudrate)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB2Periph_GPIOA, ENABLE); //使能USART2,GPIOA时钟
//USART2_TX GPIOA.2初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //PA.2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //GPIO速率50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA.2
//USART2_RX GPIOA.3初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; //PA.3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA.3
//USART 初始化设置
USART_InitStructure.USART_BaudRate = baudrate; //串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No ; //无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART2, &USART_InitStructure); //初始化串口2
//中断开启设置
USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); //开启检测串口空闲状态中断
USART_ClearFlag(USART2,USART_FLAG_TC); //清除USART2标志位
USART_Cmd(USART2, ENABLE); //使能串口2
NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; //NVIC通道设置
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 8; //抢占优先级8
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
DMA1_USART2_Init(); //DMA1_USART2初始化
}
void DMA1_USART2_Init(void)
{
DMA_InitTypeDef DMA1_Init;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE); //使能DMA1时钟
//DMA_USART2_RX USART2->RAM的数据传输
DMA_DeInit(DMA1_Channel6); //将DMA的通道6寄存器重设为缺省值
DMA1_Init.DMA_PeripheralBaseAddr = (u32)(&USART2->DR); //启动传输前装入实际RAM地址
DMA1_Init.DMA_MemoryBaseAddr = (u32)u1rxbuf; //设置接收缓冲区首地址
DMA1_Init.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取到内存
DMA1_Init.DMA_BufferSize = USART2_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA1_Init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA1_Init.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA1_Init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA1_Init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA1_Init.DMA_Mode = DMA_Mode_Normal; //工作在正常模式
DMA1_Init.DMA_Priority = DMA_Priority_High; //DMA通道 x拥有高优先级
DMA1_Init.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel6,&DMA1_Init); //对DMA通道6进行初始化
//DMA_USART2_TX RAM->USART2的数据传输
DMA_DeInit(DMA1_Channel7); //将DMA的通道7寄存器重设为缺省值
DMA1_Init.DMA_PeripheralBaseAddr = (u32)(&USART2->DR); //启动传输前装入实际RAM地址
DMA1_Init.DMA_MemoryBaseAddr = (u32)USART2_TX_BUF; //设置发送缓冲区首地址
DMA1_Init.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存发送到外设
DMA1_Init.DMA_BufferSize = USART2_MAX_TX_LEN; //DMA通道的DMA缓存的大小
DMA1_Init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA1_Init.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA1_Init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA1_Init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA1_Init.DMA_Mode = DMA_Mode_Normal; //工作在正常模式
DMA1_Init.DMA_Priority = DMA_Priority_High; //DMA通道 x拥有高优先级
DMA1_Init.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel7,&DMA1_Init); //对DMA通道7进行初始化
//DMA1通道6 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel6_IRQn; //NVIC通道设置
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
//DMA1通道7 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel7_IRQn; //NVIC通道设置
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3 ; //抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化NVIC寄存器
DMA_ITConfig(DMA1_Channel6,DMA_IT_TC,ENABLE); //开USART2 Rx DMA中断
DMA_ITConfig(DMA1_Channel7,DMA_IT_TC,ENABLE); //开USART2 Tx DMA中断
DMA_Cmd(DMA1_Channel6,ENABLE); //使DMA通道6停止工作
DMA_Cmd(DMA1_Channel7,DISABLE); //使DMA通道7停止工作
USART_DMACmd(USART2, USART_DMAReq_Tx, ENABLE); //开启串口DMA发送
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); //开启串口DMA接收
}
发送函数
//DMA 发送应用源码
void DMA_USART2_Tx_Data(u8 *buffer, u32 size)
{
while(USART2_TX_FLAG); //等待上一次发送完成(USART2_TX_FLAG为1即还在发送数据)
USART2_TX_FLAG=1; //USART2发送标志(启动发送)
DMA1_Channel7->CMAR = (uint32_t)buffer; //设置要发送的数据地址
DMA1_Channel7->CNDTR = size; //设置要发送的字节数目
DMA_Cmd(DMA1_Channel7, ENABLE); //开始DMA发送
}
void USART2_printf(char *format, ...)
{
//VA_LIST 是在C语言中解决变参问题的一组宏,所在头文件:#include <stdarg.h>,用于获取不确定个数的参数。
va_list arg_ptr; //实例化可变长参数列表
while(USART2_TX_FLAG); //等待上一次发送完成(USART2_TX_FLAG为1即还在发送数据)
va_start(arg_ptr, format); //初始化可变参数列表,设置format为可变长列表的起始点(第一个元素)
// USART2_MAX_TX_LEN+1可接受的最大字符数(非字节数,UNICODE一个字符两个字节), 防止产生数组越界
vsnprintf((char*)USART2_TX_BUF, USART2_MAX_TX_LEN+1, format, arg_ptr); //从USART2_TX_BUF的首地址开始拼合,拼合format内容;USART2_MAX_TX_LEN+1限制长度,防止产生数组越界
va_end(arg_ptr); //注意必须关闭
DMA_USART2_Tx_Data(USART2_TX_BUF,strlen((const char*)USART2_TX_BUF)); //发送USART2_TX_BUF内容
}
接收函数
//处理DMA1 通道6的接收完成中断
void DMA1_Channel6_IRQHandler(void)
{
u8 *p;
if(DMA_GetITStatus(DMA1_IT_TC6)!= RESET) //DMA接收完成标志
{
DMA_ClearITPendingBit(DMA1_IT_TC6); //清除中断标志
USART_ClearFlag(USART2,USART_FLAG_TC); //清除USART2标志位
DMA_Cmd(DMA1_Channel6, DISABLE ); //关闭USART2 TX DMA1 所指示的通道
if(witchbuf) //之前用的u2rxbuf,切换为u1rxbuf
{
p=u2rxbuf; //先保存前一次数据地址再切换缓冲区
DMA1_Channel6->CMAR=(u32)u1rxbuf; //切换为u1rxbuf缓冲区地址
witchbuf=0; //下一次切换为u2rxbuf
}else //之前用的u1rxbuf,切换为u2rxbuf
{
p=u1rxbuf; //先保存前一次数据地址再切换缓冲区
DMA1_Channel6->CMAR=(u32)u2rxbuf; //切换为u2rxbuf缓冲区地址
witchbuf=1; //下一次切换为u1rxbuf
}
DMA1_Channel6->CNDTR = USART2_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA_Cmd(DMA1_Channel6, ENABLE); //使能USART2 TX DMA1 所指示的通道
//******************↓↓↓↓↓这里作数据处理↓↓↓↓↓******************//
DMA_USART2_Tx_Data(p,USART2_MAX_RX_LEN);
//******************↑↑↑↑↑这里作数据处理↑↑↑↑↑******************//
}
}
//DMA1通道7中断
void DMA1_Channel7_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC7)!= RESET) //DMA接收完成标志
{
DMA_ClearITPendingBit(DMA1_IT_TC7); //清除中断标志
USART_ClearFlag(USART2,USART_FLAG_TC); //清除串口2的标志位
DMA_Cmd(DMA1_Channel7, DISABLE ); //关闭USART2 TX DMA1 所指示的通道
USART2_TX_FLAG=0; //USART2发送标志(关闭)
}
}
//串口2中断函数
void USART2_IRQHandler(void)
{
u8 *p;
u8 USART2_RX_LEN = 0; //接收数据长度
if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET) //串口2空闲中断
{
USART_ReceiveData(USART2); //清除串口2空闲中断IDLE标志位
USART_ClearFlag(USART2,USART_FLAG_TC); //清除USART2标志位
DMA_Cmd(DMA1_Channel6, DISABLE ); //关闭USART2 TX DMA1 所指示的通道
USART2_RX_LEN = USART2_MAX_RX_LEN - DMA1_Channel6->CNDTR; //获得接收到的字节数
if(witchbuf) //之前用的u2rxbuf,切换为u1rxbuf
{
p=u2rxbuf; //先保存前一次数据地址再切换缓冲区
DMA1_Channel6->CMAR=(u32)u1rxbuf; //切换为u1rxbuf缓冲区地址
witchbuf=0; //下一次切换为u2rxbuf
}else //之前用的u1rxbuf,切换为u2rxbuf
{
p=u1rxbuf; //先保存前一次数据地址再切换缓冲区
DMA1_Channel6->CMAR=(u32)u2rxbuf; //切换为u2rxbuf缓冲区地址
witchbuf=1; //下一次切换为u1rxbuf
}
DMA1_Channel6->CNDTR = USART2_MAX_RX_LEN; //DMA通道的DMA缓存的大小
DMA_Cmd(DMA1_Channel6, ENABLE); //使能USART2 TX DMA1 所指示的通道
//******************↓↓↓↓↓这里作数据处理↓↓↓↓↓******************//
DMA_USART2_Tx_Data(p,USART2_RX_LEN);
//******************↑↑↑↑↑这里作数据处理↑↑↑↑↑******************//
}
USART_ClearITPendingBit(USART2,USART_IT_ORE); //清除USART2_ORE标志位
}
二、HAL库
这里先放上HAL库的串口句柄
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART registers base address */
UART_InitTypeDef Init; /*!< UART communication parameters */
uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */
uint16_t TxXferSize; /*!< UART Tx Transfer size */
__IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */
uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */
uint16_t RxXferSize; /*!< UART Rx Transfer size */
__IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
__IO HAL_UART_RxTypeTypeDef ReceptionType; /*!< Type of ongoing reception */
DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management
and also related to Tx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO uint32_t ErrorCode; /*!< UART Error code */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Half Complete Callback */
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Complete Callback */
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Half Complete Callback */
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Complete Callback */
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Error Callback */
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Complete Callback */
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Receive Complete Callback */
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Wakeup Callback */
void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos); /*!< UART Reception Event Callback */
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp Init callback */
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp DeInit callback */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef;
1、串口发送/接收函数
HAL_UART_Transmit():串口发送数据,使用超时管理机制
HAL_UART_Receive():串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT():串口中断模式发送
HAL_UART_Receive_IT():串口中断模式接收
HAL_UART_Transmit_DMA():串口DMA模式发送
HAL_UART_Transmit_DMA():串口DMA模式接收
串口发送数据 HAL_UART_Transmit
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
功能:串口发送指定长度的数据。如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)。
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
- *pData 需要发送的数据
- Size 发送的字节数
- Timeout 最大发送时间,发送数据超过该时间退出发送
举例: HAL_UART_Transmit(&huart1, (uint8_t *)ZZX, 3, 0xffff); //串口发送三个字节数据,最大传输时间0xffff
中断接收数据 HAL_UART_Receive_IT
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口中断接收,以中断方式接收指定长度数据。
大致过程是,设置数据存放位置,接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。
再然后,串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断。(只触发一次中断)
参数:
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
*pData 接收到的数据存放地址
Size 接收的字节数
举例: HAL_UART_Receive_IT(&huart1,(uint8_t *)&value,1); //中断接收一个字符,存储到value中
2、串口中断函数
HAL_UART_IRQHandler(UART_HandleTypeDef *huart); //串口中断处理函数
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //串口发送中断回调函数
HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); //串口发送一半中断回调函数(用的较少)
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //串口接收中断回调函数
HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//串口接收一半回调函数(用的较少)
HAL_UART_ErrorCallback();串口接收错误函数
a.串口中断服务函数 USART1_IRQHandler(void)【不需要配置】
USART1_IRQHandler(void);
功能:当我们使能了中断并且中断发生时就会执行这里的中断服务函数。
这个函数在MX配置后会自行进行下面所说的一系列判断,不需要额外配置,因此HAL库不同于标准库,标准库是在这个函数里面进行响应的中断处理事件的配置,而标准库则在这里面进行接收和发送两个事件类型的判断,再跳转到对应的函数进行对应的处理。
b.串口中断处理函数 HAL_UART_IRQHandler(UART_HandleTypeDef *huart)【不需要配置】
HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
功能:对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用
如果接收数据,则会进行接收中断处理函数
/* UART in mode Receiver ---------------------------------------------------*/
if((tmp_flag != RESET) && (tmp_it_source != RESET))
{
UART_Receive_IT(huart);
}
如果发送数据,则会进行发送中断处理函数
/* UART in mode Transmitter ------------------------------------------------*/
if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
{
UART_Transmit_IT(huart);
return;
}
c.❤️串口接收中断回调函数 HAL_UART_RxCpltCallback(huart)【需要配置】
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
功能:HAL库的中断进行完之后,并不会直接退出,而是会进入中断回调函数中,用户可以在其中设置代码,串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改,
参数:
- UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
举例: HAL_UART_RxCpltCallback(&huart1){ //用户设定的代码 }
3、串口查询函数
HAL_UART_GetState(); 判断UART的接收是否结束,或者发送数据是否忙碌
举例:
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX) //检测UART发送结束
三、 HAL库具体实现过程
1、发送函数(重定向)
引入printf重定向代码块
代码最适合加在CubeMX自动生成后的usart.c文件的 / * USER CODE BEGIN 0 * / 和 / * USER CODE END 0 * / 中间
/* USER CODE BEGIN 0 */
#include <stdio.h>
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int _io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__*/
/******************************************************************
*@brief Retargets the C library printf function to the USART.
*@param None
*@retval None
******************************************************************/
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch,1,0xFFFF);
return ch;
}
/* USER CODE END 0 */
添加#include<stdio.h>
比较全局的办法就是将#include直接加入main.h中,因为Cube生成文件大部分都是包含了main.h的,所以除了自建文件几乎都可以全局包含到stdio.h,而且自建文件也可以直接包含main.h,我的习惯是把工程用的共性的概率高的头文件都放在main.h里面,具体位置如下:
//main.h
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include<stdio.h>
/* USER CODE END Includes */
在自建文件使用printf函数时记得#include
main.c内测试的代码:
while (1)
{
/* USER CODE END WHILE */
printf("串口打印测试\n");
HAL_Delay(1000);
/* USER CODE BEGIN 3 */
}
注意!!!使用此代码时还要在魔术棒那个选项中打勾“UseMicroLIB”,否则stdio.h是编译不了的,但它又不会报错。
参考文章:STM32-HAL库-printf函数重定向(USART应用实例)_hal库printf重定向_Calvin Haynes的博客-CSDN博客
2、⭐接收函数(中断)
因为中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再调用一次中断接收函数
具体流程:
1、初始化串口
2、在main中第一次调用接收中断函数
3、进入接收中断,接收完数据 进入中断回调函数
4、修改HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,
5 、回调函数中要调用一次HAL_UART_Receive_IT函数,使得程序可以重新触发接收中断
函数流程图:
HAL_UART_Receive_IT(中断接收函数) -> USART2_IRQHandler(void)(中断服务函数) -> HAL_UART_IRQHandler(UART_HandleTypeDef *huart)(中断处理函数) -> UART_Receive_IT(UART_HandleTypeDef *huart) (接收函数) -> HAL_UART_RxCpltCallback(huart);(中断回调函数)
HAL_UART_RxCpltCallback函数就是用户要重写在main.c里的回调函数。
代码实现:
在main.c中添加下列定义:
#include <string.h>
#define RXBUFFERSIZE 256 //最大接收字节数
char RxBuffer[RXBUFFERSIZE]; //接收数据
uint8_t aRxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
在main()主函数中,调用一次接收中断函数
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
/* USER CODE END 2 */
在main.c下方添加中断回调函数
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer));
HAL_UART_Transmit(&huart1, (uint8_t *)"数据溢出", 10,0xFFFF);
}
else
{
RxBuffer[Uart1_Rx_Cnt++] = aRxBuffer; //接收数据转存
if((RxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(RxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart1, (uint8_t *)&RxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(RxBuffer,0x00,sizeof(RxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1); //再开启接收中断
}
/* USER CODE END 4 */
参考文章:【STM32】HAL库 STM32CubeMX教程四---UART串口通信详解_hal_uart_transmit-CSDN博客
3、发送函数(DMA转运)
UART DMA函数库
HAL_UART_Transmit();串口发送数据,使用超时管理机制
HAL_UART_Receive();串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT();串口中断模式发送
HAL_UART_Receive_IT();串口中断模式接收
HAL_UART_Transmit_DMA();串口DMA模式发送
HAL_UART_Transmit_DMA();串口DMA模式接收
HAL_UART_DMAPause() 暂停串口DMA
HAL_UART_DMAResume(); 恢复串口DMA
HAL_UART_DMAStop(); 结束串口DMA
在main.C中添加:
/* USER CODE BEGIN Init /
uint8_t Senbuff[] = "\r\n*** Serial Output Message by DMA **\r\n UART DMA Test \r\n Zxiaoxuan"; //定义数据发送数组
/ USER CODE END Init */
while循环:
while (1)
{
/* USER CODE END WHILE */
HAL_UART_Transmit_DMA(&huart1, (uint8_t )Senbuff, sizeof(Senbuff));HAL_Delay(1000);/ USER CODE BEGIN 3 */
}
参考文章:【STM32】HAL库 STM32CubeMX教程十一---DMA (串口DMA发送接收)_hal库dma串口接收-CSDN博客
4、接收函数(DMA)
STM32的IDLE的中断产生条件:在串口无数据接收的情况下,不会产生,当清除IDLE标志位后,必须有接收到第一个数据后,才开始触发,一但接收的数据断流,没有接收到数据,即产生IDLE中断
使用DMA+串口接受空闲中断 实现将接收的数据完整发送到上位机的功能
uart.c
volatile uint8_t rx_len = 0; //接收一帧数据的长度
volatile uint8_t recv_end_flag = 0; //一帧数据接收完成标志
uint8_t rx_buffer[100]={0}; //接收数据缓存数组
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
//下方为自己添加的代码
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); //使能IDLE中断
//DMA接收函数,此句一定要加,不加接收不到第一次传进来的实数据,是空的,且此时接收到的数据长度为缓存器的数据长度
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);
}
uart.h
extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
/* USER CODE BEGIN Private defines */
#define BUFFER_SIZE 100
extern volatile uint8_t rx_len ; //接收一帧数据的长度
extern volatile uint8_t recv_end_flag; //一帧数据接收完成标志
extern uint8_t rx_buffer[100]; //接收数据缓存数组
main.c
/*
*********************************************************************************************************
* 函 数 名: DMA_Usart_Send
* 功能说明: 串口发送功能函数
* 形 参: buf,len
* 返 回 值: 无
*********************************************************************************************************
*/
void DMA_Usart_Send(uint8_t *buf,uint8_t len)//串口发送封装
{
if(HAL_UART_Transmit_DMA(&huart1, buf,len)!= HAL_OK) //判断是否发送正常,如果出现异常则进入异常中断函数
{
Error_Handler();
}
}
/*
*********************************************************************************************************
* 函 数 名: DMA_Usart1_Read
* 功能说明: 串口接收功能函数
* 形 参: Data,len
* 返 回 值: 无
*********************************************************************************************************
*/
void DMA_Usart1_Read(uint8_t *Data,uint8_t len)//串口接收封装
{
HAL_UART_Receive_DMA(&huart1,Data,len);//重新打开DMA接收
}
while循环
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(recv_end_flag == 1) //接收完成标志
{
DMA_Usart_Send(rx_buffer, rx_len);
rx_len = 0;//清除计数
recv_end_flag = 0;//清除接收结束标志位
// for(uint8_t i=0;i<rx_len;i++)
// {
// rx_buffer[i]=0;//清接收缓存
// }
memset(rx_buffer,0,rx_len);
}
HAL_UART_Receive_DMA(&huart1,rx_buffer,BUFFER_SIZE);//重新打开DMA接收
}
main.c中的 HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if(huart == &huart2)//目前用不了IDLE标志位在Proteus中不知道为什么无法检测
{
uint32_t tmp_flag = 0;
uint32_t temp;
tmp_flag =__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE); //获取IDLE标志位
if((tmp_flag != RESET))//idle标志被置位
{
LED_turn(GPIO_PIN_2);
__HAL_UART_CLEAR_IDLEFLAG(&huart2);//清除标志位
//temp = huart1.Instance->SR; //清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能
//temp = huart1.Instance->DR; //读取数据寄存器中的数据
//这两句和上面那句等效
HAL_UART_DMAStop(&huart2); // 停止DMA传输,防止
temp = __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 获取DMA中未传输的数据个数
//temp = hdma_usart1_rx.Instance->NDTR;// 读取NDTR寄存器,获取DMA中未传输的数据个数,
rx_len = BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数
recv_end_flag = 1; // 接受完成标志位置1
}
}
}