27 KiB
[TOC]
1. 串口通信概述
1.1. 计算机通信
问题:
- 我们知道的通信方式有哪些?
- 理解通信技术的发展。
- 通信的要素:以烽火为例子。
通信要素:
- 硬件:通信介质
- 软件:
- 语法:格式
- 语义:格式中信息的具体含义
1.1.1. 并行通信
优势:
- 一次传输1个字节;
- 速度快;
- 信息表达相对简单;
劣势:
- 只适合短距离传输;
- 成本高。
问题:那些地方采用并行通信?
1.1.2. 串行通信
优势:
- 适合长距离传输;
- 成本低;
劣势:
- 速度相对较慢;
- 信息表达(通信控制)相对复杂;
问题:
- 那些设备是串行通信?
- 是否可以结合串行和并行的优势?如何结合?
1.2. 串行通信的基本概念
1.2.1. 同步和异步
- 通信双方需要专门又时钟线来同步双方的时钟,并保持严格一致;
- 通信速度相对较高;
- 如 SPI、I2C通信;
- 不需要时钟同步;
- 通信双方对时间的理解差异可以有少量的偏差;
- 每通信一帧(固定数量的bit位),会进行一次同步(如上图的START和STOP电平);
- 例如USB、UART(串口);
1.2.2. 异步串行通信的特点
- 以固定的长度(位)传输一组数据(帧);对于串口来说,一帧一般是一个字符(注意不是字节,以前一个字符是7个位;现在几乎都是8个位,和字节是对应的);
- 通过帧之间的间隔来进行时间的同步;
- 如果通信双方对时间的理解差异较小,那就可以在一帧的时间片内检查到有效数量的0和1的序列(帧),这样不需要通信双方对时间的理解完全一致,因为在一帧内的时间差异几乎可以忽略。
字符格式:
一般由起始位、数据位、校验位、停止位组成。对于连续的通信来说,停止也就是起始,因此往往设置停止就可以了。
- 空闲情况下,通信线路的电平为高;
- 当需要传输数据的时候,发送方把电位拉低,持续一段时间(开始位);
- 然后连续发送一个字符和校验位;
- 最后拉高电平至少持续一段时间(停止位),表示该帧数据发送完成;
从上述的通信过程得知,空闲(不传输数据)为高电平;一旦低电平后,表示通信开始;字符传输完成后,拉高电平;因此起始其实是可以不要的。
奇偶校验:
奇校验:如果数据中1的个数为奇数,则奇校验位0,否则为1。
偶校验:如果数据中1的个数为偶数,则偶校验位0,否则为1。
波特率:
波特率是每秒传输位的单位(bps),波特率越高,表示通信速率越快。注意,在任何通信场景中,通信速率的都应该用bps表示;例如100M的以太网,表示 100Mbps,也就是每秒大约传输10M字节(本来应该是100M/8,但是可能有停止位和校验,因此大约估算是100M/10)。
1.2.3. 异步串口通信的数据接收过程
我们假设采样时钟是波特率的16倍,具体的数据接收过程如下:
- 接收过程由起始位的下降沿启动。
- 接收端等待8个时钟周期,以便建立一个接近起始位周期中点的采样点。
- 接收端再次等待 16个时钟周期,使其进入第一个数据位周期的中点。
- 第一个数据位被采样并存储在接收寄存器中。
- 串口模块在采样第二个数据位之前,等待另外16个时钟周期。
- 重复此过程,直到所有的数据位都被采样和存储。
- 由停止位的上升沿使数据线返回到空闲状态。
1.2.4. 串行通信的传输方向
1.3. STM32串口通信
数据寄存器:
- TDR 发送寄存器,结合发送位移寄存器,把数据以电平的方式用时间进行排序发送到TX引脚;
- RDR 接收寄存器,通过接收位移寄存器组成的一个字符装载到RDR;
通信状态标志位:
轮询方式可以通过读取上述的状态来进行控制:
- RXNE表示有新的数据(字符);
- TXE:表示可以向TDR写入新的数据;
- TC:发送完成;
中断方式也会有对应的三种方式。
1.4. 硬件结构
串口通信的示意图是这样的:
但是目前的很多计算机没有串口,因此在开发板上有一个USB转串口的芯片:
因此实际的情况是这样:
这个芯片在开发板的位置是:
2. HAL库外设初始化过程
2.1. 串口外设句柄
stm32f1xx_hal_uart.h 的160行;
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行;
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
Parity
Mode
HwFlowCtl
注意:一般串口通信都使用三根线,不会用到硬件流控。
OverSampling
2.3. 串口初始化过程
- MX_USART2_UART_Init:该函数位于main.c,有框架自动生成;用于初始化串口句柄;
- HAL_UART_Init:位于 stm32f1xx_hal_uart.c;这是硬件抽象的初始化代码,并不关心具体的硬件,其中调用了HAL_UART_MspInit,用于实现真正的串口初始化功能 ;该文件中有个 HAL_UART_MspInit 函数的 weak定义;
- 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所示。
3.1.2. 串口轮询方式发送丽数(HAL UART_Transmit)
该函数用于在轮询方式下发送指定数量的数据。具体描述如表9-10所示
3.1.3. 串口轮询方式接收函数(HAL UART_Receive)
该函数用于在轮询方式下接收指定数量的数据。具体描述如表9-11所示。
3.2. 固定长度的数据收发
- 开发板串口使用PA2和PA3:
点击任意一个端口:
知道使用的是串口2
- 设置串口2的参数:
问题:
-
Hardware Flow Control 是什么意思?
-
Over Sampling (过采样)是什么意思?
-
代码编写:
/* 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所示:
4.1.2. 串口中断方式接收函数(HALUART_Receive_IT)
该函数用于在中断方式下接收指定数量的数据,具体描述如表9-13 所示:
4.1.3. 串口中断通用处理函数(HAL_UART_IRQHandler)
该函数是所有串口中断发生后的通用处理程序。任何一个串口的相关中断(如发送中断或接收中断)发生后,都会通过中断向量表中的串口中断服务程序USARTx_IRQHandler()调用该函数。在函数内部会完成具体的数据收发,最后调用不同的回调函数来完成后续的中断处理任务。具体描述如表 9-14所示:
4.1.4. 串口发送中断回调函数(HAL_UART_TxCpltCallback)
该函数用于处理在中断方式下发送完指定数量数据后的后续任务。任何一个串口发送完指定数量数据后,都会调用发送中断回调函数。因此,在函数内部需要判断是哪一个串口产生的本次发送中断回调,然后再执行具体的中断处理任务。具体描述如表9-15所示:
4.1.5. 串口接收中断回调函数(HAL_UART_RxCpltCallback)
该函数用于处理在中断方式下接收完指定数量数据后的后续任务。任何一个串口接收完指定数量数据后,都会调用接收中断回调函数。因此,在函数内部需要判断是哪一个串口产生的本次接收中断回调,然后再执行具体的中断处理任务。具体描述如表9-16所示:
4.1.6. 串口中断使能函数(__HAL_UART_ENABLE_IT)
该函数用于使能对应的串口中断类型,采用带参数的宏实现(宏函数),具体描述如表 9-17 所示:
4.1.7. 串口中断标志查询函数(__HAL_UART_GET_FLAG)
该函数用于查询对应的串口中断标志是否置位,采用带参数的宏实现(宏函数),具体描述如表 9-18 所示:
4.1.8. 空闲中断标志清除函数( __HAL_UART_CLEAR_IDLEFLAG)
该函数用于清除串口的空闲中断标志,采用带参数的宏实现(宏函数),具体描述如表 9-19 所示:
4.2. 任务:使用中断方式实现简单的通信协议(p289)
- 与查询方式一样的设置串口,需要打开串口的全局中断:
- 设置PA6为输出,Label为LED1;PA7为输出,Label为LED2
- 完善代码:
/* 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 */
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 */
}
思考:
- 这个简单的格式解析是通信当中的语法和意义;但是还有些不完善,会存在什么缺陷?
- 应该如何解决这些问题?
5. DMA方式的串口通信
轮询方式是主动的查询串口的状态,根据寄存器状态来进行收发的操作;这种方式效率比较低。中断方式效率较高,串口当有状态变化的时候会产生中断(中断函数的优先级永远高于主函数),进行对数据进行处理。但是中断方式其实还是一个字节一个字节的收发(看看STM32串口通信),没发送或者是接收一个字节都会产生中断。CPU在每个字节的收发后都要进行处理,这样的方式效率还是不够高。因此有了DMA方式。简单说来,DMA(Direct Memory Access)方式是外设与内存之间建立了一个联系,当有一组数据接收或者发送完成的时候才通知CPU进行处理(中断),这样会大家提高效率,减少CPU的占用时间。
5.1. DMA控制器概述
参考手册143页,描述了芯片有2个DMA控制器。
- 具备两个DMA控制器:DMA1和DMA2,每个控制器有8个数据流,每个数据流可以响应16个来自外设或存储器的 DMA 请求。
- 数据流(stream)是用于连接传输源和传输目标的数据通路,每个数据流可以配置为不同的传输源和传输目标。
- DMA 控制器通过总线仲裁器来协调各个 DMA 请求的优先级。
- 支持三种数据传输方向:从外设到存储器、从存储器到外设以及从存储器到存储器。
- 具备16字节的 FIFO。使能 FIFO功能后,源数据先送人FIFO,达到 FIFO 的触发阈值后,再传送到目标地址。
5.2. DMA的接口函数
5.2.1. 串口DMA 方式发送函数(HAL_UART_Transmit_DMA)
5.2.2. 串口DMA方式接收函数(HAL_UART_Receive_DMA)
5.2.3. DMA数据项数读取函数(__HAL_DMA_GET_COUNTER)
5.2.4. 串口DMA传输停止函数(HAL_UART_DMAStop
5.3. 利用空闲中断和 DMA 实现不定长数据的接收
串口拉低进行通信;每个字符间有一个停止位(高);当检查到停止位有两个以上的长度时表示线路Idle;或者说数据不连续。
任务描述:
使用空闲中断,接收一组连续的数据,然后把这些数据返回。因为框架并没有提供idle中断,因此需要修改一下中断处理函数。
5.3.1. 开发板配置
- 设置串口,与以前的一致,需要使能串口2:
- 设置DMA
点击1位置,添加RX的DMA支持(2条目),该配置后面的(Channel,Direction,Priority)保持缺省就可以了。Channel是固定的,这里不能调整;Direction,因为是读取,肯定是外设到内存,因此也不能调整;Priority是用于DMA仲裁的,因为这里只有一个DMA,因此任意值都没有关系。
3位置用于设置 DMA 工作模式,分为Normal和 Circular 两种模式。在Normal模式下,当DMA传输结束时,即指定数量的数据已经完成传输,将不再产生 DMA操作。如果需要再次传输数据,需要在关闭DMA数据流的情况下,重新启动DMA 传输。这种方式相当于单次传输方式。
在 Circular 模式下,当前的 DMA 传输结束时,传输数据的数量将自动重载为初值,数据缓冲区重新指向首地址,只要有数据就会继续DMA传输。这种方式相当于连续传输方式。Circular模式主要用于连续、大批量的数据传递,如 A/D转换中的连续转换模式。而这里的不定长数据接收任务,无法预知一帧数据的字节数。当利用 IDLE 中断接收完一帧数据后,需要暂停 DMA 传输,获取该帧数据的字节数,然后重新启动 DMA 传输,因此选择Normal 模式。
4、5位置的Increment Address:用于设置地址递增。外设作为单一设备,地址固定,因此,地址设置为不递增。而存储器包括多个存储单元,完成一个数据传输后,地址应该自动递增,指向下一个待传输数据的地址,因此设置为自动增加。
5位置的数据宽度:用于设置数据宽度,有Byte(字节)HalfWord(2字节)和Word(4字节)三种选择。由于串口一次只能收发一字节,因此选择 Byte。
- 设置串口2的全局中断,以便可以捕获IDLE中断。
5.3.2. 代码
在main.c的大约22行加入:
/* USER CODE BEGIN Includes */
#include <string.h>
/* USER CODE END Includes */
因为等一下要用到 strlen函数,因此加入 string.h 的引用。
在main.c的大约60行加入:
/* USER CODE BEGIN 0 */
#define LENGTH 100 // 接收缓冲的长度,需要不小于最大帧(连续数据)的长度
uint8_t RxBuffer[LENGTH]; // 接收缓冲,也用于发送
uint8_t RxCount = 0; // 接收到的数据长度
volatile uint8_t RxFlag = 0; // 自定义 Idle 中断标志
const char *MSG1 = "Hello";
/* USER CODE END 0 */
这里主要是变量的定义,注意 RxFlag 这个变量设置成 volatile ,因为在中断中需要对这个变量进行写,在main.c中需要对这个变量进行读。
main.c的大约100行的main函数中:
/* USER CODE BEGIN 2 */
HAL_UART_Transmit(&huart2, (uint8_t*) MSG1, strlen(MSG1), 100); // 发送提示
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart2, RxBuffer, LENGTH); // 开启DMA接收
/* USER CODE END 2 */
首先发送欢迎消息,然后启动串口2的IDLE中断,最后启用串口的DMA的接收。
在main函数的循环中:
while (1) {
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if (RxFlag) { // 坚持到IDLE中断,表示一帧数据接收完成
RxFlag = 0; // 重新设置自定义中断标志
HAL_UART_DMAStop(&huart2); // 停止DMA 接收,因为DMA可能没有接收满
RxCount = LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 计算收到数据的长度
HAL_UART_Transmit(&huart2, RxBuffer, RxCount, 100); // 发送接收到的数据
// 书上有把缓冲区重新填写为0的代码,其实是不需要的
HAL_UART_Receive_DMA(&huart2, RxBuffer, LENGTH); // 重新开启DMA接收
}
}
循环判断 RxFlag 标志,该标志是在串口中断服务中设置;检查到该标识表示已经接收完连续的一组数据;然后关闭DMA,发送刚刚收到的数据,最后在开始DMA读取。
以上是main.c的代码,下面看看stm32f1xx_it.c的代码。
大约在60行左右:
/* USER CODE BEGIN EV */
extern volatile uint8_t RxFlag; // RxFlag 在 main.c 中定义,这里只是引用
/* USER CODE END EV */
因为FxFlag是在main.c中定义的,这里也是需要使用的,所以声明成外部变量。
在大约204行的串口中断服务中加入以下代码:
/* USER CODE BEGIN USART2_IRQn 1 */
if (__HAL_UART_GET_FLAG(&huart2,UART_FLAG_IDLE) != RESET) {
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
RxFlag = 1;
}
/* USER CODE END USART2_IRQn 1 */
这里判断串口的IDLE标志,如果是真,就设置FxFlag为1,以便main函数使用。