[TOC] # 1. 串口通信概述 ## 1.1. 计算机通信 问题: 1. 我们知道的通信方式有哪些? 2. 理解通信技术的发展。 3. 通信的要素:以烽火为例子。 通信要素: 1. 硬件:通信介质 2. 软件: 1. 语法:格式 2. 语义:格式中信息的具体含义 ### 1.1.1. 并行通信 ![image-20240926082752190](./img/image-20240926082752190.png) 优势: 1. 一次传输1个字节; 2. 速度快; 3. 信息表达相对简单; 劣势: 1. 只适合短距离传输; 2. 成本高。 问题:那些地方采用并行通信? ### 1.1.2. 串行通信 ![image-20240926083220154](./img/image-20240926083220154.png) 优势: 1. 适合长距离传输; 2. 成本低; 劣势: 1. 速度相对较慢; 2. 信息表达(通信控制)相对复杂; 问题: 1. 那些设备是串行通信? 2. 是否可以结合串行和并行的优势?如何结合? ## 1.2. 串行通信的基本概念 ### 1.2.1. 同步和异步 ![image-20240926105411531](./img/image-20240926105411531.png) 1. 通信双方需要专门又时钟线来同步双方的时钟,并保持严格一致; 2. 通信速度相对较高; 3. 如 SPI、I2C通信; ![image-20240926105432567](./img/image-20240926105432567.png) 1. 不需要时钟同步; 2. 通信双方对时间的理解差异可以有少量的偏差; 3. 每通信一帧(固定数量的bit位),会进行一次同步(如上图的START和STOP电平); 4. 例如USB、UART(串口); ### 1.2.2. 异步串行通信的特点 ![image-20240926110126910](./img/image-20240926110126910.png) 1. 以固定的长度(位)传输一组数据(帧);对于串口来说,一帧一般是一个字符(注意不是字节,以前一个字符是7个位;现在几乎都是8个位,和字节是对应的); 2. 通过帧之间的间隔来进行时间的同步; 3. 如果通信双方对时间的理解差异较小,那就可以在一帧的时间片内检查到有效数量的0和1的序列(帧),这样不需要通信双方对时间的理解完全一致,因为在一帧内的时间差异几乎可以忽略。 **字符格式:** ![image-20240926110639835](./img/image-20240926110639835.png) 一般由起始位、数据位、校验位、停止位组成。对于连续的通信来说,停止也就是起始,因此往往设置停止就可以了。 1. 空闲情况下,通信线路的电平为高; 2. 当需要传输数据的时候,发送方把电位拉低,持续一段时间(开始位); 3. 然后连续发送一个字符和校验位; 4. 最后拉高电平至少持续一段时间(停止位),表示该帧数据发送完成; 从上述的通信过程得知,空闲(不传输数据)为高电平;一旦低电平后,表示通信开始;字符传输完成后,拉高电平;因此起始其实是可以不要的。 ![alt text](img/uart1.drawio.png) 奇偶校验: 奇校验:如果数据中1的个数为奇数,则奇校验位0,否则为1。 偶校验:如果数据中1的个数为偶数,则偶校验位0,否则为1。 ![image-20240926114933398](./img/image-20240926114933398.png) **波特率:** 波特率是每秒传输位的单位(bps),波特率越高,表示通信速率越快。注意,在任何通信场景中,通信速率的都应该用bps表示;例如100M的以太网,表示 100Mbps,也就是每秒大约传输10M字节(本来应该是100M/8,但是可能有停止位和校验,因此大约估算是100M/10)。 ### 1.2.3. 异步串口通信的数据接收过程 ![image-20240926115059698](./img/image-20240926115059698.png) 我们假设采样时钟是波特率的16倍,具体的数据接收过程如下: 1. 接收过程由起始位的下降沿启动。 2. 接收端等待8个时钟周期,以便建立一个接近起始位周期中点的采样点。 3. 接收端再次等待 16个时钟周期,使其进入第一个数据位周期的中点。 4. 第一个数据位被采样并存储在接收寄存器中。 5. 串口模块在采样第二个数据位之前,等待另外16个时钟周期。 6. 重复此过程,直到所有的数据位都被采样和存储。 7. 由停止位的上升沿使数据线返回到空闲状态。 ### 1.2.4. 串行通信的传输方向 ![image-20240926120728340](./img/image-20240926120728340.png) ## 1.3. STM32串口通信 ![image-20240926120759519](./img/image-20240926120759519.png) **数据寄存器:** 1. TDR 发送寄存器,结合发送位移寄存器,把数据以电平的方式用时间进行排序发送到TX引脚; 2. RDR 接收寄存器,通过接收位移寄存器组成的一个字符装载到RDR; **通信状态标志位:** ![image-20240926163850067](./img/image-20240926163850067.png) 轮询方式可以通过读取上述的状态来进行控制: 1. RXNE表示有新的数据(字符); 2. TXE:表示可以向TDR写入新的数据; 3. TC:发送完成; 中断方式也会有对应的三种方式。 ## 1.4. 硬件结构 串口通信的示意图是这样的: ![alt text](img/uart2.drawio.png) 但是目前的很多计算机没有串口,因此在开发板上有一个USB转串口的芯片: ![image-20240926165734028](./img/image-20240926165734028.png) 因此实际的情况是这样: ![alt text](img/uart3.drawio.png) 这个芯片在开发板的位置是: ![image-20240926170851906](./img/image-20240926170851906.png) # 2. HAL库外设初始化过程 ## 2.1. 串口外设句柄 stm32f1xx_hal_uart.h 的160行; ```c typedef struct __UART_HandleTypeDef { USART_TypeDef *Instance; /*!< UART registers base address */ UART_InitTypeDef Init; /*!< UART communication parameters */ const 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 */ __IO HAL_UART_RxEventTypeTypeDef RxEventType; /*!< Type of Rx Event */ 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; ``` 和定时器的代码组织方式是一样的,包含和串口的属性以及操作的函数指针。基本解释可以参考教科书269页。 ## 2.2. 串口初始化数据类型(p270) stm32f1xx_hal_uart.h 的46行; ```c typedef struct { uint32_t BaudRate; /*!< This member configures the UART communication baud rate. The baud rate is computed using the following formula: - IntegerDivider = ((PCLKx) / (16 * (huart->Init.BaudRate))) - FractionalDivider = ((IntegerDivider - ((uint32_t) IntegerDivider)) * 16) + 0.5 */ uint32_t WordLength; /*!< Specifies the number of data bits transmitted or received in a frame. This parameter can be a value of @ref UART_Word_Length */ uint32_t StopBits; /*!< Specifies the number of stop bits transmitted. This parameter can be a value of @ref UART_Stop_Bits */ uint32_t Parity; /*!< Specifies the parity mode. This parameter can be a value of @ref UART_Parity @note When parity is enabled, the computed parity is inserted at the MSB position of the transmitted data (9th bit when the word length is set to 9 data bits; 8th bit when the word length is set to 8 data bits). */ uint32_t Mode; /*!< Specifies whether the Receive or Transmit mode is enabled or disabled. This parameter can be a value of @ref UART_Mode */ uint32_t HwFlowCtl; /*!< Specifies whether the hardware flow control mode is enabled or disabled. This parameter can be a value of @ref UART_Hardware_Flow_Control */ uint32_t OverSampling; /*!< Specifies whether the Over sampling 8 is enabled or disabled, to achieve higher speed (up to fPCLK/8). This parameter can be a value of @ref UART_Over_Sampling. This feature is only available on STM32F100xx family, so OverSampling parameter should always be set to 16. */ } UART_InitTypeDef; ``` StopBits ![image-20240929154747976](./img/image-20240929154747976.png) Parity ![image-20240929154807419](./img/image-20240929154807419.png) Mode ![image-20240929154830676](./img/image-20240929154830676.png) HwFlowCtl ![image-20240929154857111](./img/image-20240929154857111.png) 注意:一般串口通信都使用三根线,不会用到硬件流控。 OverSampling ![image-20240929154951411](./img/image-20240929154951411.png) ## 2.3. 串口初始化过程 ![image-20240929162341424](./img/image-20240929162341424.png) 1. MX_USART2_UART_Init:该函数位于main.c,有框架自动生成;用于初始化串口句柄; 2. HAL_UART_Init:位于 stm32f1xx_hal_uart.c;这是硬件抽象的初始化代码,并不关心具体的硬件,其中调用了HAL_UART_MspInit,用于实现真正的串口初始化功能 ;该文件中有个 HAL_UART_MspInit 函数的 weak定义; 3. HAL_UART_MspInit:位于 stm32f1xx_hal_msp.c;这里才是真正关于串口的初始化; 因此:stm32f1xx_hal_uart.c 中定义的是操作硬件的接口(类似Java的接口,所有的函数,常量等,编程一般也只需要和该文件打交道);stm32f1xx_hal_msp.c 是真正的实现,实现具体的硬件层面的操作。这样的设计就可以不改变接口的情况下,适配多个不同硬件。 # 3. 轮询方式的串口通信 ## 3.1. 轮询方式的接口函数 ### 3.1.1. 串口初始化函数(HAL_UART_Init) 该函数用于串口的初始化。将串口外设句柄中的参数写人对应的寄存器,并调用MCU硬件初始化函数HAL_UART_MspInit()完成时钟、引脚和中断等系统级初始化操作。具体描述如表 9-9所示。 ![image-20240929151719021](./img/image-20240929151719021.png) ### 3.1.2. 串口轮询方式发送丽数(HAL UART_Transmit) 该函数用于在轮询方式下发送指定数量的数据。具体描述如表9-10所示 ![image-20240929151812868](./img/image-20240929151812868.png) ### 3.1.3. 串口轮询方式接收函数(HAL UART_Receive) 该函数用于在轮询方式下接收指定数量的数据。具体描述如表9-11所示。 ![image-20240929151902186](./img/image-20240929151902186.png) ## 3.2. 固定长度的数据收发 1. 开发板串口使用PA2和PA3: ![image-20240929152437856](./img/image-20240929152437856.png) 点击任意一个端口: ![image-20240929152348644](./img/image-20240929152348644.png) 知道使用的是串口2 2. 设置串口2的参数: ![image-20240929152813604](./img/image-20240929152813604.png) 问题: 1. Hardware Flow Control 是什么意思? 2. Over Sampling (过采样)是什么意思? 3. 代码编写: ```c /* USER CODE BEGIN 2 */ uint8_t uartBuf[5]; /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if (HAL_UART_Receive(&huart2, uartBuf, 5, 100) == HAL_OK) { HAL_UART_Transmit(&huart2, uartBuf, 5, 100); } } /* USER CODE END 3 */ } ``` 下载程序后,连接TYPE-C到计算机的USB(可能需要安装驱动),然后打开串口通信软件,设置波特率等参数与开发板一致;发送5个字符,会收到开发板的回应(必须是一次性发送5个字符,不能分开发送)。 # 4. 中断方式的串口通信 中断的效率更高。 ## 4.1. 中断方式的接口函数 ### 4.1.1. 串口中断方式发送函数(HAL_UART_Transmit IT) 该函数用于在中断方式下发送指定数量的数据,具体描述如表9-12所示: ![image-20240929163645757](./img/image-20240929163645757.png) ### 4.1.2. 串口中断方式接收函数(HALUART_Receive_IT) 该函数用于在中断方式下接收指定数量的数据,具体描述如表9-13 所示: ![image-20240929163815598](./img/image-20240929163815598.png) ### 4.1.3. 串口中断通用处理函数(HAL_UART_IRQHandler) 该函数是所有串口中断发生后的通用处理程序。任何一个串口的相关中断(如发送中断或接收中断)发生后,都会通过中断向量表中的串口中断服务程序USARTx_IRQHandler()调用该函数。在函数内部会完成具体的数据收发,最后调用不同的回调函数来完成后续的中断处理任务。具体描述如表 9-14所示: ![image-20240929163945133](./img/image-20240929163945133.png) ### 4.1.4. 串口发送中断回调函数(HAL_UART_TxCpltCallback) 该函数用于处理在中断方式下发送完指定数量数据后的后续任务。任何一个串口发送完指定数量数据后,都会调用发送中断回调函数。因此,在函数内部需要判断是哪一个串口产生的本次发送中断回调,然后再执行具体的中断处理任务。具体描述如表9-15所示: ![image-20240929164054977](./img/image-20240929164054977.png) ### 4.1.5. 串口接收中断回调函数(HAL_UART_RxCpltCallback) 该函数用于处理在中断方式下接收完指定数量数据后的后续任务。任何一个串口接收完指定数量数据后,都会调用接收中断回调函数。因此,在函数内部需要判断是哪一个串口产生的本次接收中断回调,然后再执行具体的中断处理任务。具体描述如表9-16所示: ![image-20240929164202400](./img/image-20240929164202400.png) ### 4.1.6. 串口中断使能函数(__HAL_UART_ENABLE_IT) 该函数用于使能对应的串口中断类型,采用带参数的宏实现(宏函数),具体描述如表 9-17 所示: ![image-20240929164333113](./img/image-20240929164333113.png) ### 4.1.7. 串口中断标志查询函数(__HAL_UART_GET_FLAG) 该函数用于查询对应的串口中断标志是否置位,采用带参数的宏实现(宏函数),具体描述如表 9-18 所示: ![image-20240929164437770](./img/image-20240929164437770.png) ### 4.1.8. 空闲中断标志清除函数( __HAL_UART_CLEAR_IDLEFLAG) 该函数用于清除串口的空闲中断标志,采用带参数的宏实现(宏函数),具体描述如表 9-19 所示: ![image-20240929164531359](./img/image-20240929164531359.png) ## 4.2. 任务:使用中断方式实现简单的通信协议(p289) 1. 与查询方式一样的设置串口,需要打开串口的全局中断: ![image-20240929181629409](./img/image-20240929181629409.png) 2. 设置PA6为输出,Label为LED1;PA7为输出,Label为LED2 3. 完善代码: ```c /* USER CODE BEGIN 0 */ #define LENGTH 4 // 接收帧的长度 uint8_t RxBuffer[LENGTH]; // 接收缓冲 volatile uint8_t RxFlag = 0; // 是否收到数据的标志 const char *MSG1 = "Please enter instruction."; const char *MSG2 = "Head->0xAA Device->0x01 Operation->0x00/0x01 Tail->0x55."; const char *ERRMSG = "Communication Error! Please try again!"; // 中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { RxFlag = 1; // 设置帧标志,主函数处理这个标志 HAL_UART_Receive_IT(&huart2, RxBuffer, LENGTH); // 再次启动中断接收 } } /* USER CODE END 0 */ ``` ```c int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_USART2_UART_Init(); /* USER CODE BEGIN 2 */ HAL_UART_Transmit(&huart2, (uint8_t*) MSG1, strlen(MSG1), 100); // 发送提示 HAL_UART_Transmit(&huart2, (uint8_t*) MSG2, strlen(MSG2), 100); // 发送提示 HAL_UART_Receive_IT(&huart2, RxBuffer, LENGTH); // 启动中断接收 /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ if (RxFlag) { // 判断帧接收标志 RxFlag = 0; // 清除帧接收标志 if (RxBuffer[0] == 0xAA && RxBuffer[3] == 0x55) { // 判断头尾数据是否合法 switch (RxBuffer[1]) { case 1: // 灯1 if (RxBuffer[2]) HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); else HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); break; case 2: // 灯2 if (RxBuffer[2]) HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_SET); else HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET); break; } } else { // 非法格式,返回错误 HAL_UART_Transmit(&huart2, (uint8_t*) ERRMSG, strlen(ERRMSG), 100); } } } /* USER CODE END 3 */ } ``` 思考: 1. 这个简单的格式解析是通信当中的语法和意义;但是还有些不完善,会存在什么缺陷? 2. 应该如何解决这些问题? # 5. DMA方式的串口通信