# STM32_UART **Repository Path**: lewque/stm32_-uart ## Basic Information - **Project Name**: STM32_UART - **Description**: 本仓库主要是学习stm32串口通信的知识。代码编译运行后是不定长接收模式,其他模式的已经注释,可以通过阅读笔记,修改后尝试其他版本。 - **Primary Language**: C - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2024-05-28 - **Last Updated**: 2024-05-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 串口通信(UART) ## 一、概述 串口通信是一种通用==串行==数据总线,用于==异步==通信,可以实现==全双工==通信。 ###### 串行:使用单一通道来传输多个数据位 与之相对的是并行:使用多个通道同时来传输多个数据位 ###### 异步:发送信息,不会关注对方是否准备好接收以及是否接收成功。 ###### 同步:通信过程中,双方需要有相应的应答的信号,保持双方处于同步状态。 ###### 全双工:在总线上,具有发送和接收的双向功能,并且能够同时进行发送和接收 ###### 半双工:在总线上,可以发送和接收,但不能同时进行发送和接收 ###### 单工:在总线上,只有一种传输方向,要么发送,要么接收 ## 二、传输过程 ### 发送数据 1. 当刚初始化完成,且没有数据传输时,总线保持高电平输出逻辑1,作为==空闲位==。 2. 当要发送数据时,发送端输出逻辑0作为==起始位==。 3. 接着开始输出数据位,发送端从==最低位==D0开始传输数据,然后D1,以此类推,直到数据==最高位==。 4. 如果设置有奇偶校验,发送端发送奇偶校验位 5. 最后发送端输出1作为停止位。 6. 如果没有信息继续发送,则发送端输出1作为空闲位,当有数据传输时,则跳回步骤2 ### 接收数据 1. 当数据线处于空闲状态时,接收到逻辑1。当检测到信号线由1变为0时,即起始位,开始接收数据。 2. ==每隔T时间==,就会对输入信号进行检测,接收数据,并作为最低位D0。 3. 接着继续每隔T时间检测输入信号,把接收到的数据作为D1,以此类推,直到接收到最高位 4. 再过T时间,接收奇偶校验位 5. 当接收到奇偶校验位后,接收端希望接收到停止位1,如果说没有接收到停止位1,则说明错误。如果成功接收到了停止位1后,进行奇偶校验,校验没有错误后,则把数据存储进寄存器中。 6. 接收完此次信息后,数据线就继续传输空闲位 ## 三、串口波特率 ==描述串口传输速度快慢==。其单位为bps,意思就是一秒能传输几个bit。串口波特率设置越高,数据传输速度越快,但并不是越高越好。==太高会导致数据传输不稳定==。 ==同时要注意,发送端和接收端的波特率必须保持一致,才能正确接收到数据== ## 四、数据格式 ![数据帧格式](pic/%E6%95%B0%E6%8D%AE%E5%B8%A7%E6%A0%BC%E5%BC%8F.jpg) 1. 起始位:低电平0 2. 数据位:一个字符占一个字节,而一个字节一般由8位二进制来组成。也有6,7位的情况。==低位在前,高位在后== 3. 奇偶校验位:在标准ASCII码中,D7位作为奇偶校验位。奇偶校验:用来检测数据传输过程中是否出现错误的。如果是奇校验,那么这一个字节数据中,数据位为1的个数必须是奇数,如果是非奇数,则在D7添1。相反,偶校验,1的个数必须是偶数,如果非偶数,则在最后一位添1。 4. 停止位:高电平1。停止位一般有三种规定长度,分别为1,1.5,2个单位时间长度。 ## 五、stm32+hal+cubemx实现串口通信 #### 开发环境:stm32f103C8T6 keil5 cubemx usb转ttl #### cubemx配置 ![cubemx配置](pic/cubemx%E9%85%8D%E7%BD%AE.jpg) ###### USART实际上即可以同步也可以异步,所以上面我们选择第一个,为异步通信,第二个为同步通信 ###### Baud Rate:波特率 word length:数据位长度,包括8和9(包括校验位) ###### parity:奇偶校验位,选择none不校验,也可以配置奇或者偶(使用奇偶校验的话,word length要配置为9) ###### stop bits:停止位的位数,一般使用1 #### 代码编写 串口通信收发分为阻塞式和dma中断。 #### printf输出 首先我们先实现最简单的形式,也是我们在C语言中最常用的输出数据格式,printf ###### 使用printf需要导入stdio.h头文件,并且需要进行重定向输出。可以在usart.c或者main.c中添加函数 ```c int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff); return ch; } ``` ###### 同时还需要在keil5魔术棒里面勾选micro lib ![keil5](pic/keil5.png) 这样设置后,就可以像在C语言里面一样使用printf作为输出。在这里推荐一个免费的上位机vofa+。如果使用这个上位机,需要注意其读取的格式,需要在pirntf输出的字符串最后加上\n或者\r\n才能正确显示 #### 能否使用scanf输入呢 可以的,但是并不推荐。需要和printf一样进行重定向,代码如下 ```c int fgetc(FILE *f) { uint8_t ch = 0; HAL_UART_Receive(&huart1, &ch, 1, 0xffff); return ch; } ``` 不推荐的原因:因为scanf输入属于阻塞式,会一直占用cpu资源,阻塞会让其他程序无法执行。同时也无法确定什么时候有数据的输入,所以一般不使用这种形式,来接收输入。 #### 中断式接收 中断式接收,又分为定长接收和不定长接收。 首先,先看一下串口收发两个hal库函数 ```c /** * @brief Receives an amount of data in blocking mode. (阻塞式接收数据) * @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01), * the received data is handled as a set of u16. In this case, Size must indicate the number * of u16 available through pData. * @param huart Pointer to a UART_HandleTypeDef structure that contains * the configuration information for the specified UART module.(串口句柄) * @param pData Pointer to data buffer (u8 or u16 data elements).(接收数据的数组或者指针,必须为u8) * @param Size Amount of data elements (u8 or u16) to be received.(接收数据占用的字节数) * @param Timeout Timeout duration(等待时间,0XFFFF相当于一直等待) * @retval HAL status(返回传输的结果,HAL_OK,HAL_BUSY,HAL_ERROR,HAL_TIMEOUT) */ HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) ``` ``` @note 奇偶校验如果没有设置,而数据位长度设置为9位,此时接收数据会处理为u16(uint16_t)。 ``` ```C /** * @brief Sends an amount of data in blocking mode. * @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01), * the sent data is handled as a set of u16. In this case, Size must indicate the number * of u16 provided through pData. * @param huart Pointer to a UART_HandleTypeDef structure that contains * the configuration information for the specified UART module. * @param pData Pointer to data buffer (u8 or u16 data elements). * @param Size Amount of data elements (u8 or u16) to be sent * @param Timeout Timeout duration * @retval HAL status */ HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout) ``` 基本与上面同理 ```c uint8_t buf[2]={23,24,‘\n’} ;//十进制 HAL_UART_Transmit(&huart1, buf, 2, 0xFFFF); uint8_t buf[12]={"Hello World"} ; HAL_UART_Transmit(&huart1, buf, sizeof(buf), 0xFFFF); ``` 实际上,数据传输时,十进制的数字会被转换为二进制。而上位机vofa+如果开启的是字符串输出模式,显示的会是根据ASCCI码转换后的字符值。使用16进制输出,可以看到输出的就是十进制对应的十六进制值。不过,如果使用printf来输出,只要格式写对,像%d,%c等等,就可以在字符串模式下,看到正确的值。 数据的接收也是类似的,但是由于这个函数属于阻塞式发送,所以不推荐使用。 ##### 定长中断式接收 ![串口中断](pic/%E4%B8%B2%E5%8F%A3%E4%B8%AD%E6%96%AD.png) 需要使能串口全局中断(上面两个dma中断,是在不定长接收的时候使用的) 定长:就是已经确定发进来的数据有多长 ###### 步骤: 1. 先在main.c启动接收中断(参数说明跟前面实际上是相同的,区别就在于不会阻塞等待数据) ```C /** * @brief Receives an amount of data in non blocking mode. * * @param huart Pointer to a UART_HandleTypeDef structure that contains * the configuration information for the specified UART module. * @param pData Pointer to data buffer (u8 or u16 data elements). * @param Size Amount of data elements (u8 or u16) to be received. * @retval HAL status */ HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) ``` 2.在stm32f1xx_hal.c中,会生成一个中断函数`USART1_IRQHandler()`,可以在这个函数里面直接做处理,但实际上,hal库定义一个虚函数`void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)`作为串口回调函数,==可以在工程中任意位置定义,但是一定要保证函数名一样==。 ```c void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1);//这个函数里面就会调用中断回调函数 /* USER CODE BEGIN USART1_IRQn 1 */ HAL_UART_Receive_IT(&huart1,data,2); //继续启动接收中断 接收data一般需要使用extern声明 /* USER CODE END USART1_IRQn 1 */ } ``` ```C void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) //判断中断的来源 {printf("\n收到数据:%s \r\n",data);} //我们想要进行的处理,这里是把输入数据重新输出 } ``` ##### 不定长接收 无法确定接收的信息有多长,以及每次发送的信息的长度会发生变化。==这种方法是最适合平常开发使用的== ###### DMA:Direct Memory Access 直接存储器存储 直接存储器存取用来提供在外设和存储器之间或者存储器和存储器之间的高速数 据传输。无须 CPU 任何干预,通过 DMA 数据可以快速地移动。这就节省了 CPU 的资源来做其他操作.从下图可以看到,DMA挂载在系统总线上,可以直接访问CPU以及其他外设 ![DMA](pic/DMA.png) ###### 步骤 1. cubemx中配置串口开启DMA通道,会自动使能中断 ![DMA中断](pic/dma%E4%B8%AD%E6%96%AD.png) 2. 在main函数while前面,初始化的地方加上 ```C HAL_UART_Receive_DMA(&huart1,data,255); //启动DMA中断接收 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);//配置为空闲中断 ``` 3. 在中断回调函数编写处理代码 ```C void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(RESET !=__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)){ //判断触发的中断类型 __HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除中断标志位 HAL_UART_DMAStop(&huart1); //暂停DMA uint8_t data_length = 255-__HAL_DMA_GET_COUNTER(huart1.hdmarx); //计算接收数据的长度 HAL_UART_Transmit_DMA(&huart1,data,data_length);//把接收到的数据发回上位机 memset(data,0,255); //清空数组 HAL_UART_Receive_DMA(&huart1,data,255);//进行下一次接收 } } ``` 注意:hal库生成的代码,在初始化的地方可能有坑。==DMA的初始化需要放在UART初始化的前面==,有时候生成的代码会反过来,如果接收到的数据只有最后一位,大概率就是出现了这个错误。 理解使用DMA空闲中断接收的好处:如果使用定长接收,当传输比较大的数据时,会多次进入中断,占用CPU资源。而使用DMA空闲中断,当有数据传入时,DMAA会快速把数据进行存储,等到通信结束,进入空闲状态时,再触发空闲中断,对数据进行读取。有效避免多次进入中断,浪费CPU资源。 同时要注意,每帧的长度不能大于启动时定义的size,否则会把超出来的数据,覆盖到开头 #### 拓展数据处理 从上面可以看到,接收到的数据一般都是使用uint8_t的数组来接收的,数组中的每个元素保存的实际上是一个字符。如果要把字符拼接成整型或者浮点数,可以使用atoi(转换为整型) atof(转换成浮点数) ###### 注意数据的长度,选择适合的数据类型,否则会导致接收的数据不正确 ```C uint8_t unsigned char //0-255 uint16_t unsigned short int ```