串口通信是单片机学习和应用中非常重要而且非常实用的一个外设。在项目开发中,常常需要使用到串口调试,来查看程序运行的状态。
一、简述:
首先来看一看NUCLEO_F767开发板的串口原理图。
以往开发板上串口常用USART1的PA9和PA10,这里却没有将这两个引脚继续作为串口,而是将他们作为USB的引脚来使用。
查看用户手册,对于USART的相关信息如图所示:
Nucleo板没有使用USART1作为串口,而是使用了USART3,但也没有直接将这两个引脚引出来,而是默认连接到了ST-Link上面,和USB结合做出了一个虚拟串口。也可以根据自己的需要将接口外接,不过需要手动的改动锡桥。
在这里可以注意到:USART3的串口引脚引到ST-Link的时候,在CN5这个端子上留了两个插针,可以利用这两个针脚来外接串口线和其他设备通信。但应当看清楚原理图里的引脚对应关系,CN5上面印的“TX和RX”其实指的是ST-Link上的,F767上的引脚和这个丝印标注恰好是相反的。
将开发板连接电脑,打开“设备管理器”可以看到“端口(COM和LPT)"一栏下有一个"STMicroelectronics STLink Virtual COM Port"
二、查询模式
打开上一节CubeMX的配置,添加下列内容。
USART3配置成异步模式,并且选择PD8和PD9的串口复用功能。
配置 USART1 的初始化参数,包括波特率停止位等等。这里将 USART3 配置为:波特率 115200,8 位字长模式,无奇偶 校验位,1 个停止位,发送/接收均开启。生成代码
在"usart.c"文件下,有初始化的函数:
/* USART3 init function */
void MX_USART3_UART_Init(void)
{
huart3.Instance = USART3;
huart3.Init.BaudRate = 115200;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
huart3.Init.Mode = UART_MODE_TX_RX;
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart3.Init.OverSampling = UART_OVERSAMPLING_16;
huart3.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart3.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart3) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
}
为了使用printf函数,在"usart.c"文件的下面用户代码区添加如下重定向程序:
/* USER CODE BEGIN 1 */
#ifdef __GNUC__
/* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
set to 'Yes') calls __io_putchar() */
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
* @brief Retargets the C library printf function to the USART.
* @param None
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
/* USER CODE END 1 */
在主循环下添加如下代码:
/* USER CODE BEGIN 2 */
printf("Hello,Nucleo!\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//超时时间是以ms为单位计数的,此处时间太长,会让程序一直停在这里判断,
//导致串口状态一直是忙,外部中断回调函数里的发送程序无法正常执行
//缩短Timeout的时间。
if(HAL_UART_Receive(&huart3, &data, 1, 1) == HAL_OK)
{
printf("USART Receive data: %c\n",data);
}
HAL_Delay(500);
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
/* USER CODE END WHILE */
将上一节的HAL_GPIO_EXTI_Callback函数修改为如下代码:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static uint8_t Led_Set=0;
if( HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == GPIO_PIN_SET )
{
while(HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == GPIO_PIN_SET){}
Led_Set++;
if(Led_Set>=255) Led_Set=1;
if(Led_Set%2 == 1)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin);
printf("LED2 Toggle\n");
}
else
{
HAL_GPIO_TogglePin(LED3_GPIO_Port,LED3_Pin);
printf("LED3 Toggle\n");
}
}
}
编译下载到开发板:打开串口调试助手:按下按键,可以收到F767发来的信息,发送一个字符'h',就可以收到“USART Receive data: h ”:
说明查询模式下实现了串口的发送和接收。
三、中断方式
在刚刚 CubeMX工程的基础上做下修改:
开启串口中断。
配置USART3的中断分组,抢占优先级设为1,响应优先级设为2 。生成代码。
在主函数里添加如下代码:
定义存储 发送接收 数据的数组,大小为10 。
/* USER CODE BEGIN 0 */
uint8_t data_TX[10]="abcdefghi\n";
uint8_t data_RX[10]={0};
/* USER CODE END 0 */
主函数里在用户代码区使用下面的代码:
/* USER CODE BEGIN 2 */
printf("Hello,Nucleo!\n");
HAL_UART_Receive_IT(&huart3, data_RX, 10); //配置串口接收中断
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(500);
HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);
/* USER CODE END WHILE */
在主函数下面添加:
/* USER CODE BEGIN 4 */
//按键外部中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
static uint8_t Led_Set=0;
if( HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == GPIO_PIN_SET )
{
while(HAL_GPIO_ReadPin(KEY_GPIO_Port,KEY_Pin) == GPIO_PIN_SET){}
Led_Set++;
if(Led_Set>=255) Led_Set=1;
if(Led_Set%2 == 1)
{
HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin);
HAL_UART_Transmit_IT(&huart3, data_TX, 10 );
}
else
{
HAL_GPIO_TogglePin(LED3_GPIO_Port,LED3_Pin);
printf("LED3 Toggle\n");
}
}
}
//串口发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
printf("Send OK!\n");
}
//串口接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit_IT(&huart3, data_RX, 10 );
HAL_UART_Receive_IT(&huart3, data_RX, 10);
}
/* USER CODE END 4 */
程序复位之后,首先会发送一个“Hello,Nucleo!”来测试串口是否正常,接着开启串口接收中断,指定存储区,确定要接 收的数据的长度。当接受完这些长度的数据之后,就会进入中断,最终接收完成回调函数,来讲刚刚接收到的数据发送出 去。发送的函数同样也开启了中断,当发送完成后,同样进入回调函数,输出一个“Send OK!\n”。按下按键同样也会触 发发送。
四、DMA方式。
在刚刚的CubeMX工程里,将串口DMA开启:
生成代码,在"usart.c"里就有了配置DMA的过程:
/* USART3 DMA Init */
/* USART3_RX Init */
hdma_usart3_rx.Instance = DMA1_Stream1;
hdma_usart3_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart3_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart3_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart3_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart3_rx.Init.Mode = DMA_NORMAL;
hdma_usart3_rx.Init.Priority = DMA_PRIORITY_LOW;
hdma_usart3_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart3_rx) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart3_rx);
主函数里添加如下代码:
/* USER CODE BEGIN 2 */
printf("Hello,Nucleo!\n"); //打印提示信息
HAL_UART_Receive_DMA(&huart3,data_RX,10); //开启接收DMA,有数据直接存入内存
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(data_RX[0] != 0 )//判断存储接收数据的数组首位是否为空
{
HAL_Delay(10); //保障数据完整,等待DMA接收完成
printf("DMA receive: %s\n",data_RX); //输出
data_RX[0]=0; //清楚数居首位
while( HAL_DMA_GetState(&hdma_usart3_rx) != HAL_DMA_STATE_READY ){;} //检测DMA状态
HAL_UART_Receive_DMA( &huart3, data_RX, 10); //开启接收DMA,有数据直接存入内存
}
/* USER CODE END WHILE */
另外删除中断接收方式中的发送完成、接收完成回调函数。编译下载:
2019/4/14日补充:
DMA方式接收串口数据详细配置过程,此次配置不使用F767,而使用F103C8T6进行:
在CubeMX(此时使用的版本为最新版:5.1.0)里面配置好串口引脚,我这里配置了两个串口,分别是串口1和串口3
两个串口的模式配置成这个样子:
配置两个串口的DMA通道、优先级、模式和字长。
开启两个DMA的中断,之后生成代码,打开工程。
可以在usart.c文件的用户代码区加入串口重定向的代码段,这样就可以使用printf函数进行串口输出了
为了确认程序正常执行,可以在主函数开始的地方加一句输出提示:
主函数中添加串口输出,检查串口工作是否正常:
接着打开DMA接收,这里设置DMA接收定长数据包,为10个字节
在stm32f1xx_hal_uart.h中有这么一个函数:
在main文件中定义,写如下代码,因为开启了两个DMA,所以在进入函数时需要先判断一下是哪个串口接收到了数据,这里判断串口1,
下载到板子上,现象如下: