简介
串口通讯顾名思义就是一种串行通讯方式,下面我们将从物理层和协议层对其进行分析。
物理层
电平标准:
根据电平标准的不同,串口通讯可以分为TTL标准以及RS-232标准。
RS-232有更强抗干扰能力
RS-232信号不能直接被控制器识别,必须转换成TTL信号
串口线中的 RTS、CTS、DSR、DTR 及 DCD 信号,使用逻辑 1 表示信号有效,逻辑 0 表示信号无效。
协议层:
数据包:
启始停止信号:
数据包的起始信号由一个逻辑0表示,停止信号由0.5 ,1.5,或2个逻辑1表示
波特率:
两个通讯设备需要规定好波特率,即每个码元长度,位0-位7就是有效数据,用虚线分开的每一格代表一个码元。
有效数据:
数据包的有效数据长度规定在5-8位
数据校验:
由于数据通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。
奇校验:
要求有效数据和校验位中的“1”的个数为奇数,比如8位长数据01110010中“1”的个数为偶数,为了凑奇数必须将校验位置为1,最后就是9位。
偶校验:
要求有效数据和校验位中的“1”的个数为偶数
USART框图:
TX:数据输出引脚
RX:数据接收引脚
SW_RX:数据接收引脚,用于单线和智能卡模式
nRTS:请求以发送,n表示低电平有效,当USART准备发送数据时nRTS变为低电平
nCTS:请求以接收
数据寄存器:
USART_DR 实际是包含了两个寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。串行通信是一个位一个位传输的,发送时把TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。
控制器:
发送器:
当 USART_CR1 寄存器的发送使能位 TE 置 1 时,启动数据发送,发送移位寄存器的数据会在 TX引脚输出,低位在前,高位在后。如果是同步模式 SCLK 也输出时钟信号。
**一个字符帧发送需要三个部分:起始位 + 数据帧 + 停止位。**起始位是一个位周期的低电平,位周期就是每一位占用的时间;数据帧就是我们要发送的 8 位或 9 位数据,数据是从最低位开始传输的;停止位是一定时间周期的高电平。
停止位时间长短是可以通过 USART 控制寄存器 2(USART_CR2) 的 STOP[1:0] 位控制,可选 0.5个、1 个、1.5 个和 2 个停止位。默认使用 1 个停止位。2 个停止位适用于正常 USART 模式。
当发送使能位 TE 置 1 之后,发送器开始会先发送一个空闲帧 (一个数据帧长度的高电平),接下来就可以往 USART_DR 寄存器写入要发送的数据。在写入最后一个数据后,需要等待 USART 状态寄存器 (USART_SR) 的 TC 位为 1,表示数据传输完成,如果 USART_CR1 寄存器的 TCIE 位置1,将产生中断。
接收器:
如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,收完成后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置 1,同时如果USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。
代码层:
用cubemx配置串口:
typedef struct
{
uint32_t BaudRate; //波特率
uint32_t WordLength; //字长 可选8/9位
uint32_t StopBits; //停止位 可选 0.5 1 1.5 2
uint32_t Parity; //校验位
uint32_t Mode;//模式
uint32_t HwFlowCtl;//硬件流控制
uint32_t OverSampling; // 过采样模式
} UART_InitTypeDef;
初始化结构体
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
/* USER CODE BEGIN USART1_Init 0 */
/* USER CODE END USART1_Init 0 */
/* USER CODE BEGIN USART1_Init 1 */
/* USER CODE END USART1_Init 1 */
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;//无检验位
huart1.Init.Mode = UART_MODE_TX_RX;//发送接收模式
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN USART1_Init 2 */
/* USER CODE END USART1_Init 2 */
}
生成的初始化函数
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();//开USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE();//开GPIOA的时钟
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;//复用输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;//复用输入,和普通输入一样所以可以直接用普通输入
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 interrupt Init */
//抢占优先级0,子优先级0
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
//使能中断通道
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
GPIO初始化
HAL库UART函数
uart结构体定义,包含了所有的功能函数等。
发送接收函数:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
发送函数
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
接收函数
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
发送中断函数
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
接收中断函数
typedef enum
{
HAL_OK = 0x00U,
HAL_ERROR = 0x01U,
HAL_BUSY = 0x02U,
HAL_TIMEOUT = 0x03U
} HAL_StatusTypeDef;
这些函数的输入参数大体一样,就调两个分析一波;
HAL_StatusTypeDef
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
这个函数是发送函数,第一个参数填入的是HAL库定义的结构体变量
第二个参数填入要发送的数组或变量,第三个直接sizeof(变量)即可,也就是填入参数长度,第四个就是发送时间,如果超过这个时间还没发送成功则直接停止。
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
这里需要讲一下中断函数的运行流程:
一开始会先执行HAL_UART_Transmit_IT这个函数,接着进入到USART1_IRQHandler这个中断服务函数,然后就是中断处理函数,最后再进入回调函数
接收中断代码:
uint8_t Rx_str;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)
{
if(Rx_str=='1')
{
HAL_UART_Transmit(&huart1, Tx_str2, sizeof(Tx_str2), 500);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_5);
}
if(Rx_str=='2')
{
HAL_UART_Transmit(&huart1, Tx_str, sizeof(Tx_str), 500);
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
}
}
}
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_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
while (1)
{
HAL_UART_Receive_IT(&huart1, &Rx_str, 1);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
重定向printf函数:
在 C 语言 HAL 库中,fputc 函数是 printf 函数内部的一个函数,功能是将字符 ch 写入到文件指针f 所指向文件的当前写指针位置,简单理解就是把字符写入到特定文件中。我们使用 USART 函
数重新修改 fputc 函数内容,达到类似“写入”的功能。
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return (ch);
}
IN 3 /
}
/ USER CODE END 3 */
}
#### 重定向printf函数:
在 C 语言 HAL 库中,fputc 函数是 printf 函数内部的一个函数,功能是将字符 ch 写入到文件指针f 所指向文件的当前写指针位置,简单理解就是把字符写入到特定文件中。我们使用 USART 函
数重新修改 fputc 函数内容,达到类似“写入”的功能。
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return (ch);
}
将这段代码放入文件中并头文件调用#include <stdio.h>,勾选魔术棒里的Micro lib即可。