stm32标准库和HAL库的对比学习6.《学习UART串口通信(阻塞,中断,DMA)以及简单数据包分析》
本人是大一的学生,学习了一段时间的stm32,此系列博客为个人的学习笔记,方便个人复习,如有错误或问题,非常非常欢迎大家来大力指正。
USART(Universal Synchronous/Asynchronous Receiver/Transmitter),通用同步/异步串行接收/发送器,其中Synchronous是同步,Asynchronous是异步。而我们串口通信是用异步通信的,所以也称为UART
同步和异步:同步是双方传输数据的时钟信号是一样的,在规定事件读取一个段数据,这个数据就是一个数据包。一般有一条时钟线,使双方的时钟相等
异步的时钟信号频率是不一样的,所以为了确定数据什么时候开始收发,就要用到电平信息去告诉什么时候开始接收,什么时候是读数据,什么时候读完。
波特率和比特率:
比特率(Bitrate)表示,每秒钟传输的二进制位数, 单位为比特每秒(bit/s)。
波特率”(Baudrate),它表示每秒钟传输了多少个码元。
如果是二进制的高低电平,波特率=比特率
同步靠时钟线,异步靠比特率
详细原理参考江科大的stm32教学
下面就是UART的图

TX为传输口,RX为接收口,所以传输口接接收口,gnd相接是使两机的参考低电平相同
在51单片机中要根据时序写代码的,但是在stm32直接调用库函数即可。
阻塞,中断,DMA是三种串口通信的传输方式
阻塞为当数据传输时,cpu会等待数据的传输完毕再去工作
中断:每接收/发送一个数据,就会触发一次中断,将正在工作的cpu叫过来传输数据,结束后cpu回去继续工作
DMA:DMA是一个数据搬运通道,可以代替cpu完成数据的搬运,省下cpu的占用
标准库的实现:阻塞
按照惯例,①初始化RCC②初始化GPIO模式③定义USART的参数
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);//开始usart时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开始GPIO时钟
GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_9;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_AF_PP;//输出脚复用推挽
GPIO_Init(GPIOA,&GPIO_Initstructure);
GPIO_Initstructure.GPIO_Pin=GPIO_Pin_10;
GPIO_Initstructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Initstructure.GPIO_Mode=GPIO_Mode_IPU;//输入脚输入模式
GPIO_Init(GPIOA,&GPIO_Initstructure);
USART_InitTypeDef USART_Initstructure;
USART_Initstructure.USART_BaudRate=9600;
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_Mode=USART_Mode_Tx|USART_Mode_Rx;//开启TX,RX脚
USART_Initstructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;;//选择流控
USART_Init(USART1,&USART_Initstructure);
USART_Cmd(USART1,ENABLE);
要自己写传输模块(参考江科大)
发送一个字节
void serialsend(uint8_t Byte)
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==0);//获取TDR发送寄存器空标志位,TXE,当没有标志位时为0,一直循环等到有标志位产生
}
发送一个数组
void serialsends(uint8_t *array,uint16_t length)
{
uint16_t i;
for(i=0;i<length;i++)
{
serialsend(array[i]);
}
}
发送一个字符串,用标志位判断
void serialsendstring(char *string)
{
uint8_t i;
for(i=0;string[i]!=0;i++)//空标志位,因为空位为0,或者'\0'所以不为0就会循环下去
{
serialsend(string[i]);
}
}
输出数字(将数字一位位分为字符发出)
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; //设置结果初值为1
while (Y --) //执行Y次
{
Result *= X; //将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'); //依次调用Serial_SendByte发送每位数字
}
}
从定义printf输出到串口
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
也可以用sprintf()
char num[100];
sprintf(num,"num:%d",666);
serialsendstring(num);
接收:
轮询:不用定义NVIC
void serialsend(uint8_t RxData)
if (Serial_GetRxFlag() == 1) //检查串口接收数据的标志位
{
RxData = Serial_GetRxData(); //获取串口接收的数据
Serial_SendByte(RxData); //串口将收到的数据回传回去,用于测试
}
中断实现:开启NVIC
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
写串口接收模块
uint8_t Serial_GetRxFlag(void) //下面的函数,如果收到数据,就RXFlay置一,表示接收到
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0; //这里是手动清零,但是DR被读取到也会自动清零,没读取到就要自己清,个人感觉没有这个函数也可以
return 1; //则返回1,并自动清零标志位
}
return 0; //如果标志位为0,则返回0
}
uint8_t Serial_GetRxData(void)
{
return Serial_RxData; //返回接收的数据变量
}
void USART1_IRQHandler(void)
{
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
Serial_RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
Serial_RxFlag = 1; //置接收标志位变量为1
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除USART1的RXNE标志位
//读取数据寄存器会自动清除此标志位
//如果已经读取了数据寄存器,也可以不执行此代码
}
}
个人认为江科大是为了更好教学而这样写的,我们实际使用的时候就可以不这样写了,可以按照自己的思维写,我认为这会更清晰点
DMA的实现(TX和RX的原理一样的,这里用RX示范)
DMA的定义
//memory是存储器,peripheralinc是初始
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_BufferSize=4;//缓冲区大小
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;//传输方向
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;//硬件触发还是软件触发,外设使用硬件
DMA_InitStructure.DMA_Mode=DMA_Mode_Circular;//是否使用自动重装
DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)RXDATA;//外设地址
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;//数据宽度//因为是32位的,所以如果要使用16位,用半位中断
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&(USART1->DR);//32位的地址
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;//字节宽度(类型)
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//地址是否自增
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel5,&DMA_InitStructure);
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);
DMA_Cmd(DMA1_Channel5, ENABLE);
HAL库的实现:
阻塞:
①打开RCC
②打开USART

然后生成代码
HAL_UART_Receive(&huart2,recevice,1,HAL_MAX_DELAY);//这是接收
HAL_UART_Transmit(&huart2,(uint8_t*)recevice,strlen(recevice),100);//这个是发送
中断:
中断传输的函数使用是一样的,都是立即执行所以接收函数直接改为
HAL_UART_Transmit_IT();//这个是发送
HAL_UART_Receive_IT();//这个是接收
但是接收函数也是非堵塞,可能当数据没接收完就下一步了。所以这里要用到中断处理函数
1.标准库的串口阻塞,中断,DMA的实现
void USART2_IRQHandler(void),但是有很多中断都是调用这个函数的,所以在这个函数里面有一个回调函数
中断事件不能写在USART2_IRQHandler,因为uart很多事件都是用这个触发的,不知道是哪个。要用回调函数。每次接收一段函数都会触发中断,用回调是全部字节发生中断才进行
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
在接收完成时会产生中断去执行这串代码。(其实每接收一个字节都中断一次,回调函数是全部字节接收完毕才中断)
所以我们写一条接收函数到main中(while外)
然后函数写成
}*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_IT(&huart2,recevice,1);
中间写自己的判断代码
HAL_UART_Receive_IT(&huart2,recevice,1);//
}
中断接收函数不写在while里面,所以我们就在执行回调函数时再引用一次 HAL_UART_Receive_IT(&huart2,recevice,1);即可
dma:要去cubemx开通道,将it改为dma就行了用的是DMA完成中断处理函数

Direction是搬运的方向,数据宽度和模式暂时不定义。
将之前的_IT改为_DMA就可以了
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_DMA(&huart2,recevice,1);
中间写自己的判断代码
HAL_UART_Receive_DMA(&huart2,recevice,1);//
}
接收不定长数据:串口空闲中断,空闲中断就产生,代表一帧数据传输完毕
需要将接收代码改为
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,recevice,sizeof());
sizeof是最大数据长度
触发的中断函数为
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart==&huart2)
{
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,recevice,size);
}
可以用it/dma/阻塞,这里使用了DMA。
dma会有传输过半的中断也会调用上述函数,又时要用到,但是用不到可以关闭
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT);
长数据包
用一下代码作为讲解
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart==&huart2)//判断是否为uart2触发
{
HAL_UART_Transmit_DMA(&huart2,recevice,Size);
if(recevice[0]==0xAA){//验证包头是否正确
uint8_t sum=0;
for(int i=0;i<Size-1;i++)//计算校验和,全部的数据加起来,和校验位比较,看收到的数据是否正确,校验位要自己手动算
{
sum+=recevice[i];
}
if(sum==recevice[Size-1]){//判断校验位,如果正确,就是全部正确
for(int i=2;i<Size-1;i=i+2)//对数据进行判断,+2是AA00这样第一个数据和第二个隔了2个字
{
if(recevice[i]==0x00)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);
}else if(recevice[i]==0x01)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);
}
}
}
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,recevice,10);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT);
}
如果要发送的数据快间隔小,就要设立缓冲区,解析数据步骤搬出中断,在主循环处理//uart_command
小知识:&ADC1->DR就是ADC1的地址+DR的地址偏移
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)