本人是大一的学生,学习了一段时间的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的地址偏移

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐