STM32串口通信(寄存器与hal库实现)
一、串口通信协议
1. 串口介绍
波特率
“波特率”(Baudrate),它表示每秒钟传输了多少个码元。在二进制的世界码元和位是等价的。用每秒传输的比特数表示波特率。
STM32提供了串口异步通讯,异步通讯中由于没有时钟信号,所以两个通讯设备之间需要约定好波特率,即每个码元的长度,以便对信号进行解码。常见的波特率为 4800、9600、115200等。
空闲位
串口协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平,表示当前线路上没有数据。
通讯的起始位
每开始一次通信时发送方先发出一个逻辑“0”的信号(低电平),表示传输字符的开始。因为总线空闲时为高电平所以开始一次通信时先发送一个明显区别于空闲状态的信号即低电平。
通讯的停止位
停止信号可由 0.5、1、1.5 或 2个逻辑1的数据位表示,只要双方约定一致即可。
有效数据位
在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效数据的长度常被约定为 5、6、7 或8位长。构成一个字符(一般都是8位)。先发送最低位,最后发送最高位,使用低电平表示‘0’高电平表示‘1’完成数据位的传输。
校验位
数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。串口校验分几种方式:
无校验(no parity)。
奇校验(odd parity):如果数据位中“1”的数目是偶数,则校验位为“1”,如果“1”的数目是奇数,校验位为“0”。
偶校验(even parity):如果数据为中“1”的数目是偶数,则校验位为“0”,如果为奇数,校验位为“1”。
在STM32串口通信中,一般如何处理变长数据的接收?
一般利用串口通信时的空闲帧来识别这次数据是否接收完毕。
当接收到连续的10个高电平(字长为8、停止位为1),就认为遇到一个空闲帧。STM32收到空闲帧会产生空闲中断,就表示这次数据传输完毕。
2. USART外设
STM32提供了USART(Universal Synchronous Asynchronous Receiver and Transmitter)通用同步异步收发器。是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。还有UART相比USART去掉了同步通讯功能。
一共提供5个串口供开发者选择。
波特率的产生
发送器和接收器的波特率是一致的,都是通过设置BRR寄存器来得到。
这里的fck是给外设的时钟(usart1在APB2上一般是72MHz,usart2,3,4,5在APB1上一般为36MHz)。
以usart1为例,fck = 72000000,要想配置波特率为115200,可得USARTDIV=39.0625。把这个值写入到BRR寄存器中。39.0625的小数部分:0.0625 * 16 = 1, 整数部分是:39(0x27)。
所以写入到BRR寄存器的值是:0x0271。
二、串口案例:计算机与串口通讯
需求描述:电脑(串口助手)通过串口向stm32发送数据,stm32接收数据后再发送回来。
TX PA9 使用推挽复用输出 ,主动驱动线路,输出稳定的高低电平信号。
RX PA10 使用浮空输入 / 带上拉输入 ,被动接收信号,避免内部驱动影响外部信号;带上拉输入可提高抗干扰能
1. 轮询方式接收
1.1 usart.c
#include "usart.h"void USART1_init(void)
{// 1. 开启时钟// 1.1 串口外设时钟RCC->APB2ENR |= RCC_APB2ENR_USART1EN;// 1.2 GPIO时钟RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;// 2. 配置GPIO工作模式// PA9=TX 复用推挽 CNF=10 MODE=11 PA10=RX 浮空输入 CNF=01 MODE=00GPIOA->CRH |= GPIO_CRH_CNF9_1;GPIOA->CRH &= ~GPIO_CRH_CNF9_0;GPIOA->CRH |= GPIO_CRH_MODE9;GPIOA->CRH &= ~GPIO_CRH_CNF10_1;GPIOA->CRH |= GPIO_CRH_CNF10_0;GPIOA->CRH &= ~GPIO_CRH_MODE10;// 3. 串口的参数配置// 3.1 配置波特率 115200USART1->BRR = 0x271;// 3.2 串口使能 使能接收和发送USART1->CR1 |= USART_CR1_UE;USART1->CR1 |= USART_CR1_TE;USART1->CR1 |= USART_CR1_RE;// 3.3 配置字长 8位USART1->CR1 &= ~USART_CR1_M;// 3.4 不需要检验位USART1->CR1 &= ~USART_CR1_PCE;// 3.5 停止位USART1->CR2 &= ~USART_CR2_STOP;
}void USART1_SendChar(uint8_t byte)
{// 等待发送寄存器为空 TXE=1while ((USART1->SR & USART_SR_TXE) == 0){}// 数据写到数据寄存器USART1->DR = byte;
}void USART1_SendString(uint8_t *str, uint16_t len)
{for (uint16_t i = 0; i < len; i++){USART1_SendChar(str[i]);}
}uint8_t USART1_ReceiveChar(void)
{// 等待读数据寄存器非空while ((USART1->SR & USART_SR_RXNE) == 0){}return USART1->DR;
}// buffer 存放接收到的数据
// *len 存放收到数据的字节的长度
void USART1_ReceiveString(uint8_t buffer[], uint8_t *len)
{uint8_t i = 0;// 不停接收下一个字符while (1){// 判断当前数据帧是否接收完毕while ((USART1->SR & USART_SR_RXNE) == 0){// 判断当前是否检测到空闲帧if (USART1->SR & USART_SR_IDLE){// 字符串接收完毕*len = i;return;}}// 接收到的数据放入缓冲区buffer[i] = USART1->DR;i++;}
}
上面接收字符和字符串函数也可以这样实现
uint8_t USART1_ReceiveChar(void)
{// 等待读数据寄存器非空while ((USART1->SR & USART_SR_RXNE) == 0){// 增加判断空闲帧的条件if (USART1->SR & USART_SR_IDLE){return 0;}}return USART1->DR;
}// buffer 存放接收到的数据
// *len 存放收到数据的字节的长度
void USART1_ReceiveString(uint8_t buffer[], uint8_t *len)
{uint8_t i = 0;while ((USART1->SR & USART_SR_IDLE) == 0){buffer[i] = USART1_ReceiveChar();i++;}// 清除IDLE位置// USART1->SR;USART1->DR;// USART1_ReceiveChar最后返回了0 导致多了'\0'字符*len = --i;
}
1.2 main.c
#include "led.h"
#include "delay.h"
#include "usart.h"uint8_t buff[100] = {0};
uint8_t len = 0;int main(void)
{USART1_init();USART1_SendChar('a');while (1){USART1_ReceiveString(buff, &len);USART1_SendString(buff, len);}
}
2. 中断方式接收
USART提供了多个中断事件
2.1 usart.c
需要在串口初始化中使能串口的各种中断
#include "usart.h"void USART1_init(void)
{// 1. 开启时钟// 1.1 串口外设时钟RCC->APB2ENR |= RCC_APB2ENR_USART1EN;// 1.2 GPIO时钟RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;// 2. 配置GPIO工作模式// PA9=TX 复用推挽 CNF=10 MODE=11 PA10=RX 浮空输入 CNF=01 MODE=00GPIOA->CRH |= GPIO_CRH_CNF9_1;GPIOA->CRH &= ~GPIO_CRH_CNF9_0;GPIOA->CRH |= GPIO_CRH_MODE9;GPIOA->CRH &= ~GPIO_CRH_CNF10_1;GPIOA->CRH |= GPIO_CRH_CNF10_0;GPIOA->CRH &= ~GPIO_CRH_MODE10;// 3. 串口的参数配置// 3.1 配置波特率 115200USART1->BRR = 0x271;// 3.2 串口使能 使能接收和发送USART1->CR1 |= USART_CR1_UE;USART1->CR1 |= USART_CR1_TE;USART1->CR1 |= USART_CR1_RE;// 3.3 配置字长 8位USART1->CR1 &= ~USART_CR1_M;// 3.4 不需要检验位USART1->CR1 &= ~USART_CR1_PCE;// 3.5 停止位USART1->CR2 &= ~USART_CR2_STOP;// 3.6 使能串口的各种中断USART1->CR1 |= USART_CR1_RXNEIE; // 接收非空中断USART1->CR1 |= USART_CR1_IDLEIE; // 空闲中断// 4. 配置NVIC// 4.1 优先级组NVIC_SetPriorityGrouping(4);// 4.2 优先级NVIC_SetPriority(USART1_IRQn, 2);// 4.3 使能中断NVIC_EnableIRQ(USART1_IRQn);
}void USART1_SendChar(uint8_t byte)
{// 等待发送寄存器为空 TXE=1while ((USART1->SR & USART_SR_TXE) == 0){}// 数据写到数据寄存器USART1->DR = byte;
}void USART1_SendString(uint8_t *str, uint16_t len)
{for (uint16_t i = 0; i < len; i++){USART1_SendChar(str[i]);}
}extern uint8_t buff[100];
extern uint8_t len;
extern uint8_t isOverFlag;void USART1_IRQHandler(void)
{// 数据接收寄存器非空if (USART1->SR & USART_SR_RXNE){buff[len] = USART1->DR;len++;}else if (USART1->SR & USART_SR_IDLE){// 字符串接收完毕// 清除空闲中断标志位// USART1->SR;USART1->DR;isOverFlag = 1;}
}
2.2 main.c
#include "led.h"
#include "delay.h"
#include "usart.h"uint8_t buff[100] = {0};
uint8_t len = 0;
uint8_t isOverFlag = 0; // 字符串接收完毕标志位int main(void)
{USART1_init();USART1_SendChar('a');while (1){if (isOverFlag){USART1_SendString(buff, len);len = 0;isOverFlag = 0;}}
}
3. HAL库轮询方式收发
使用STM32CubeMx进行配置,串口配置如下
接着在main.c中添加需要的代码
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint8_t buff[100] = {0};
uint16_t size = 0; // 数据实际长度
/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
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 */HAL_UART_Transmit(&huart1, "begin\r\n", 7, HAL_MAX_DELAY);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */// 接受定长字符串,然后原样发回去// if (HAL_UART_Receive(&huart1, buff, 10, HAL_MAX_DELAY) == HAL_OK)// {// HAL_UART_Transmit(&huart1, buff, 10, HAL_MAX_DELAY);// }// 接受变长字符串if (HAL_UARTEx_ReceiveToIdle(&huart1, buff, 100, &size, HAL_MAX_DELAY) == HAL_OK){HAL_UART_Transmit(&huart1, buff, size, HAL_MAX_DELAY);}}/* USER CODE END 3 */
}
轮询方式会占用大量CPU时间,在等待接收和等待发送完毕时,CPU只能空等,运行效率低。
4. HAL库中断方式接收(定长数据)
使用STM32CubeMx打开串口中断
stm32f1xx_hal_uart.c有中关于中断回调函数的描述
*** Interrupt mode IO operation ***===================================[..](+) Send an amount of data in non blocking mode using HAL_UART_Transmit_IT()(+) At transmission end of transfer HAL_UART_TxCpltCallback is executed and user canadd his own code by customization of function pointer HAL_UART_TxCpltCallback(+) Receive an amount of data in non blocking mode using HAL_UART_Receive_IT()(+) At reception end of transfer HAL_UART_RxCpltCallback is executed and user canadd his own code by customization of function pointer HAL_UART_RxCpltCallback(+) In case of transfer Error, HAL_UART_ErrorCallback() function is executed and user canadd his own code by customization of function pointer HAL_UART_ErrorCallback
/*** @brief Rx Transfer completed callbacks.* @param huart Pointer to a UART_HandleTypeDef structure that contains* the configuration information for the specified UART module.* @retval None*/
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{/* Prevent unused argument(s) compilation warning */UNUSED(huart);/* NOTE: This function should not be modified, when the callback is needed,the HAL_UART_RxCpltCallback could be implemented in the user file*/
}
/*** @brief This function handles USART1 global interrupt.*/
void USART1_IRQHandler(void)
{/* USER CODE BEGIN USART1_IRQn 0 *//* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(&huart1);/* USER CODE BEGIN USART1_IRQn 1 *//* USER CODE END USART1_IRQn 1 */
}
HAL_UART_IRQHandler
根据不同情况调用不同回调函数,我们需要在接收完毕时候触发中断,所以需要重写HAL_UART_RxCpltCallback
extern uint8_t isOver;void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if (huart->Instance == USART1){isOver = 1;}
}
main 函数中使用中断方式接收数据,判断接收完毕标志位,接收完毕便把数据再发送出去
/* USER CODE BEGIN 0 */
uint8_t isOver = 0;
uint8_t buff[100];
/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
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 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */// 定长数据接收HAL_UART_Receive_IT(&huart1, buff, 10);if (isOver){HAL_UART_Transmit(&huart1, buff, 10, HAL_MAX_DELAY);isOver = 0;}}/* USER CODE END 3 */
}
5. HAL库中断方式接收(不定长数据)
同样需要打开串口中断
stm32f1xx_hal_uart.c文件中查看相关函数的说明
[..] This subsection also provides a set of additional functions providing enhanced receptionservices to user. (For example, these functions allow application to handle use caseswhere number of data to be received is unknown).(#) Compared to standard reception services which only consider number of receiveddata elements as reception completion criteria, these functions also consider additional eventsas triggers for updating reception status to caller :(+) Detection of inactivity period (RX line has not been active for a given period).(++) RX inactivity detected by IDLE event, i.e. RX line has been in idle state (normally high state)for 1 frame time, after last received byte.(#) There are two mode of transfer:(+) Blocking mode: The reception is performed in polling mode, until either expected number of data is received,or till IDLE event occurs. Reception is handled only during function execution.When function exits, no data reception could occur. HAL status and number of actually received data elements,are returned by function after finishing transfer.(+) Non-Blocking mode: The reception is performed using Interrupts or DMA.These API's return the HAL status.The end of the data processing will be indicated through thededicated UART IRQ when using Interrupt mode or the DMA IRQ when using DMA mode.The HAL_UARTEx_RxEventCallback() user callback will be executed during Receive processThe HAL_UART_ErrorCallback()user callback will be executed when a reception error is detected.(#) Blocking mode API:(+) HAL_UARTEx_ReceiveToIdle()(#) Non-Blocking mode API with Interrupt:(+) HAL_UARTEx_ReceiveToIdle_IT()(#) Non-Blocking mode API with DMA:(+) HAL_UARTEx_ReceiveToIdle_DMA()
中断方式接收不定长数据使用HAL_UARTEx_ReceiveToIdle_IT
,对应的回调函数是HAL_UARTEx_RxEventCallback
extern uint8_t isOver;
extern uint8_t size;// Size是实际接收到数据的长度
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart->Instance == USART1){isOver = 1;size = Size;}
}
/* USER CODE BEGIN 0 */
uint8_t isOver = 0;
uint8_t buff[100];uint8_t size = 0;
/* USER CODE END 0 *//*** @brief The application entry point.* @retval int*/
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 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */// 不定长数据接收HAL_UARTEx_ReceiveToIdle_IT(&huart1, buff, 100);if (isOver){HAL_UART_Transmit(&huart1, buff, size, HAL_MAX_DELAY);isOver = 0;}}/* USER CODE END 3 */
}
三、串口案例:重定向printf
我们常使用printf来打印调试信息,在单片机中,我们可以重定向printf,把数据打印到串口,在电脑端串口助手中查看信息。
1. 寄存器方式实现
调用printf时候,底层会调用fputc函数来执行,可以重写fputc函数达到我们目的。
void USART1_SendChar(uint8_t byte)
{// 等待发送寄存器为空 TXE=1while ((USART1->SR & USART_SR_TXE) == 0){}// 数据写到数据寄存器USART1->DR = byte;
}int fputc(int ch, FILE *file)
{ // 向串口中发送字符USART1_SendChar(ch);return ch;
}
int main(void)
{USART1_init();printf("hello world!\r\n");while (1){}
}
注意需要在keil中勾选Use MicroLIB
2. HAL库方式实现
与寄存器方式相同,重写fputc,在fputc中向串口发送字符。同样,需要勾选Use MicroLIB。
#include "stdio.h"
int fputc(int ch, FILE *file)
{HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);return ch;
}