对于串口通信在这里就不再多做介绍了,有需要的可以自行查询资料,本章主要是完成串口的收发实验
一、轮询方式
1 、STM32CubeMX设置
RCC设置外接HSE,时钟设置为168M
USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位,其他保持默认
- 重载printf
C语言中的标准库中所用的标准输入输出函数,默认的输出设备是显示器,要实现串口或LCD的输出,必须重新定义标准库函数里与输出函数相关的函数。例如:printf输出到串口,需要将fputc里面的输出指向串口(重定向),方法如下:只要自己添加一个int fputc(int ch, FILE *f)函数,能够输出字符就可以了。
在usart.c文件后面添加如下代码
/* USER CODE BEGIN 0 */
#include "stdio.h"
/* USER CODE END 0 */
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE *f){
//HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,1000);//发送串口
return ch;
}
/* USER CODE END 1 */
我在 usart.c重定义了这个fputc函数,但用不了printf,一用printf就会导致单片机无法启动,不知道是哪里的原因。
以上问题是由于microlib没有开启导致的,microlib 进行了高度优化以使代码变得很小。它的功能比缺省 C 库少,并且根本不具备某些 ISO C 特性。 某些库函数的运行速度也比较慢,如果要使用printf(),必须开启。
2024.3.8更新
因此我使用了另外一种重定义方法:定义完后用法是跟printf一样的,但是需要多定义个数组。
这里是使用sprintf函数将发送的内容放进tx_buf数组,而且sprintf会返回发送内容的长度,因此这里的重定义可以实现。
#include "stdio.h"
unsigned char tx_buf[256];
#define printf1(...) HAL_UART_Transmit(&huart1,tx_buf,sprintf((char*)tx_buf,__VA_ARGS__),0xffff);
main函数如下,使用printf注意包含stdio.h头文件
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,"HAL_UART_Transmit Test...",25,0xffff);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_RESET); //LED0亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_SET); //LED0灭
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_RESET); //LED1亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_SET); //LED1灭
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
printf("Hello STM32 %d\r\n",123);
HAL_Delay(500);
}
/* USER CODE END 3 */
}
将程序下载验证:
轮询收发:
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 */
unsigned char data[16]= {0};
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_RESET); //LED0亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_SET); //LED0灭
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_RESET); //LED1亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_SET); //LED1灭
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// HAL_UART_Transmit(&huart1,"HAL_UART_Transmit Test...",25,0xffff);
HAL_UART_Receive(&huart1, data, 16, 0xffff);//接收16个字符
HAL_UART_Transmit(&huart1, data, 16, 0xffff);//发送16个字符
// printf("\r\n printf test...%d\r\n",25);
//printf("Hello STM32\r\n");
HAL_Delay(500);
}
/* USER CODE END 3 */
}
HAL_UART_Transmit(,,,)//传输函数,将数据发送出去
第一个函参,目串口结构体(可以到usart.c中查看,记得在前面加取址符&)
第二个函参,发送的数组地址
第三个函参,要发送的字符个数
第四个函参,阻塞时间,超时跳出函数
相关的函数可以在stm32f4xx_uart.c文件中查找。
串口发送指定长度的数据。如果超时没发送完成,则不再发送,返回超时标志(HAL_TIMEOUT)。
下载验证:
轮询方式:CPU不断查询IO设备,如设备有请求则加以处理。例如CPU不断查询串口是否传输完成,如传输超过则返回超时错误。轮询方式会占用CPU处理时间,效率较低。
二、中断方式
采用中断的方式进行数据传输,需要打开串口中断。
把串口中断的优先级设置为1,防止跟其他中断干扰。
打开中断后生成代码,在usart.c文件中会自动生成中断初始化代码
/*****usart.c文件中的UART初始化函数以及IO口配置函数*****/
void MX_USART1_UART_Init(void){
//....该函数与轮询方式的UART初始化函数相同....
}
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();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**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 */
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
在usart.c中自定义该回调函数 __weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart),并编写中断函数代码,HAL库的中断进行完之后,并不会直接退出,而是会进入中断回调函数中,用户可以在其中设置代码, 串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改,因为中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再调用一次中断接收函数
extern uint8_t RxMsg[20];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart->Instance == USART1){
HAL_UART_Transmit(&huart1,RxMsg,10,0xffff); //将接收的数据通过串口1发送回去
HAL_UART_Receive_IT(&huart1,RxMsg,10); //再次开启接收中断
}
}
HAL_UART_Receive_IT(&huart1,RxMsg,10);//因为串口中断接收,以中断方式接收指定长度数据。大致过程是,设置数据存放位置,接收数据长度,然后使能串口接收中断。接收到数据时,会触发串口中断。再然后,串口中断函数处理,直到接收到指定长度数据,而后关闭中断,进入中断接收回调函数,不再触发接收中断,(只触发一次中断),因此需要在回调函数中再次启动接收中断。
还有其他的一些中断相关函数:
HAL_UART_IRQHandler(UART_HandleTypeDef *huart); //串口中断处理函数
HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //串口发送中断回调函数
HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); //串口发送一半中断回调函数(用的较少)
HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); //串口接收中断回调函数
HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//串口接收一半回调函数(用的较少)
HAL_UART_ErrorCallback();串口接收错误函数
串口中断处理函数
HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
功能:对接收到的数据进行判断和处理 判断是发送中断还是接收中断,然后进行数据的发送和接收,在中断服务函数中使用
如果接收数据,则会进行接收中断处理函数
如果发送数据,则会进行发送中断处理函数
串口查询函数
HAL_UART_GetState(); 判断UART的接收是否结束,或者发送数据是否忙碌
举例:
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX) //检测UART发送结束
在main.c中定义数据并启动接收中断,在main函数中需要先启动接收中断。
/*****main.c文件中编写相关代码*****/
/* USER CODE BEGIN PV */
uint8_t RxMsg[20];
/* USER CODE END PV */
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1,RxMsg,10);//必须启动串口接收中断,要不然中断会没反应
/* USER CODE END 2 */
while (1){
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_RESET); //LED0亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_SET); //LED0灭
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_RESET); //LED1亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_SET); //LED1灭
HAL_Delay(500);
/* USER CODE END WHILE */
}
下载验证:当单片机接收到10个字符时触发中断,然后进入回调函数,在回调函数中将数据发送出去,并启动下一次中断。
值得注意的是当你发送的数据超过10个字符时,会只读取前10个字符,剩下的会在下一次中断中接收,(有时候可能会导致中断卡死)需要重启复位才能恢复原来的正常数据
三、DMA模式
如果要打开DMA,必须先打开中断
这里DMA模式选择普通模式,其他默认即可
把DMA的中断优先级也改成1.
将DMA和中断也进行重定义:
unsigned char tx_buf[256];
#define printf1(...) HAL_UART_Transmit(&huart1,tx_buf,sprintf((char*)tx_buf,__VA_ARGS__),0xffff);//普通模式
#define DMA_printf1(...) HAL_UART_Transmit_DMA(&huart1,tx_buf,sprintf((char*)tx_buf,__VA_ARGS__));//DMA模式
#define IT_printf1(...) HAL_UART_Transmit_IT(&huart1,tx_buf,sprintf((char*)tx_buf,__VA_ARGS__));//中断模式
DMA模式的使用方法与中断的使用方法一致,在main函数添加以下程序,测试DMA的发送功能:
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_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
unsigned char data[16]= {0};
HAL_UART_Transmit(&huart1,"HAL_UART_Transmit Test...",strlen("HAL_UART_Transmit Test..."),0xffff);
HAL_UART_Transmit_IT(&huart1,TxMsg,sizeof(TxMsg));
HAL_UART_Receive_IT(&huart1,RxMsg,10);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_RESET); //LED0亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_13,GPIO_PIN_SET); //LED0灭
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_RESET); //LED1亮
HAL_Delay(500); //延时500ms
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_14,GPIO_PIN_SET); //LED1灭
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
// HAL_UART_Transmit(&huart1,"HAL_UART_Transmit Test...",25,0xffff);
// HAL_UART_Receive(&huart1, data, 16, 0xffff);
// HAL_UART_Transmit(&huart1, data, 16, 0xffff);
// printf("\r\n printf test...%d\r\n",25);
//printf("Hello STM32\r\n");
printf1("Hello STM32 %d\r\n",123);
HAL_Delay(500);
DMA_printf1("DMA_printf1 %d\r\n",234);
HAL_Delay(500);
IT_printf1("IT_printf1 %d\r\n",456);
// HAL_Delay(500);
}
/* USER CODE END 3 */
}
下载验证:
以上介绍了三种串口通信的三种模式,三种模式的处理速度也是不一样的。
首先是轮询模式,由于存在阻塞时间,所以处理速度最慢,但数据是完整的。
其次是中断模式,由于中断是利用自身的资源来进行的,而DMA不占用自身资源,所以中断会比DMA慢上一点。
最快是DMA模式,DMA模式是非阻塞状态,且不占据自身资源,处理速度很快,但可能存在数据丢失的情况。
HAL_UART_Transmit();//串口发送数据,使用超时管理机制
HAL_UART_Receive();//串口接收数据,使用超时管理机制
HAL_UART_Transmit_IT();//串口中断模式发送
HAL_UART_Receive_IT();//串口中断模式接收
HAL_UART_Transmit_DMA();//串口DMA模式发送
HAL_UART_Transmit_DMA();//串口DMA模式接收
以上讲解的都是定长接收数据的方法,下一章将讲解不定长数据的收发。
H2Z20Str: 普通中断使用前需要在main函数中先调用一次中断,其他的CubeMX会自动生成的。
陆俊屹: 普通中断接收按这个抄了后,完全没任何反应,这是怎么回事,是不是没包含什么表头?
H2Z20Str: 好的,谢谢指出问题
可可托海的牧羊人-高: 增大栈空间,图片缺配的是增大堆空间
H2Z20Str: 应该是你按下按键后触发的捕获中断导致了单片机死机