多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

STM32CubeMX系列04——串口(查询、中断、DMA、不定长接收、重定向)

STM32CubeMX系列04——串口(查询、中断、DMA、不定长接收、重定向) 文章目录1. 所用硬件2. 生成工程2.1. 创建工程选择主控2.2. 系统配置2.3. 配置工程目录2.4. 配置用到的外设3. 查询模式3.1. 配置串口3.2. 生成代码3.3. 编写代码3.4. 效果验证4. 中断模式4.1. 配置串口4.2. 生成代码4.3. 运行原理及代码分析4.4. 效果验证5. DMA模式5.1. 配置串口5.2. 生成代码5.3. 编写代码5.4. 效果验证6. 串口重定向6.1. 配置原理6.2. 实现6.2.1. 使用MicroLIB库6.2.2. 不使用MicroLIB库7. 进阶——接收不定长数据7.1. 串口中断函数、回调函数的补充明⭐⭐⭐7.1. DMA空闲中断7.2. 中断环形缓冲区 文章汇总有代码汇总 1. 所用硬件正点原子Mini板主控 STM32F103RCT6.用到的外设LED、串口1PA9、PA10。原理图2. 生成工程2.1. 创建工程选择主控2.2. 系统配置配置时钟源配置debug模式如果需要ST-Link下载及调试可以勾选配置时钟树可以直接在HCLK那里输入72然后敲回车会自动配置2.3. 配置工程目录勾选上会单独生成 c文件和头文件2.4. 配置用到的外设PA8LED0。3. 查询模式3.1. 配置串口3.2. 生成代码3.3. 编写代码编写main.c代码intmain(void){/* USER CODE BEGIN 1 */charlight_on[11]Light on \r\n;charlight_off[12]Light off \r\n;charrec_buf[1]{0};/* USER CODE END 1 *//* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE */while(1){// 串口1、接收数据的数组、接收的长度、等待时间if(HAL_OKHAL_UART_Receive(huart1,(uint8_t*)rec_buf,1,0xFFFF)){if(rec_buf[0]A){// 串口1、发送的数据、发送的长度单位字节、超时时间HAL_UART_Transmit(huart1,(uint8_t*)light_on,11,0xFFFF);HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_RESET);}else{// 串口1、发送的数据、发送的长度单位字节、超时时间HAL_UART_Transmit(huart1,(uint8_t*)light_off,12,0xFFFF);HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_SET);}}/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}3.4. 效果验证编译、烧录、查看结果。效果使用串口调试助手电脑发送一个字符A单片机收到后 发送 Light on 给电脑同时LED亮。电脑发送一个字符B(其他字符)单片机收到后 发送 Light off 给电脑同时LED灭。4. 中断模式4.1. 配置串口配置中断4.2. 生成代码4.3. 运行原理及代码分析和分析中断的过程一样。打开工程在stm32f1xx_it.h中可以看到函数USART1_IRQHandler显然这是串口中断处理函数在中断处理函数中又调用了函数HAL_UART_IRQHandler(huart1);然后去看看函数HAL_UART_IRQHandler(huart1);都干了什么事。在HAL_UART_IRQHandler(huart1);函数中考虑了很多情况从注释可以看出不出错误的话会调用函数UART_Receive_IT(huart);然后就返回了。因此我们看看函数UART_Receive_IT(huart);在这个函数中也分了很多很多情况我们直接看最后最后有个函数HAL_UART_RxCpltCallback(huart);从注释可以看出这是个接收完成的回调函数。学完配置以后一定要看看7.1小节关于这个函数的触发条件说明然后再去看看这个回调函数。还是在这个文件中定义如下。可以看到这个函数是个弱函数用户可以再次定义该函数。也就是说我们可以重新定义这个函数并在函数中编写我们处理中断的逻辑。注释写的也很清楚当需要回调时不应修改此函数HAL_UART_RxCpltCallback可以在用户文件中实现这段放在哪都行。大的工程可以创建一个文件放进去这里直接放在main.c中了。/* USER CODE BEGIN PV */charlight_on[11]Light on \r\n;charlight_off[12]Light off \r\n;charrec_buf[1]{0};/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/voidSystemClock_Config(void);/* USER CODE BEGIN PFP */voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){// 判断是不是串口1 中断if(huart-InstanceUSART1){if(rec_buf[0]A){// 串口1、发送的数据、发送的长度单位字节、超时时间HAL_UART_Transmit(huart1,(uint8_t*)light_on,11,0xFFFF);HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_RESET);}else{// 串口1、发送的数据、发送的长度单位字节、超时时间HAL_UART_Transmit(huart1,(uint8_t*)light_off,12,0xFFFF);HAL_GPIO_WritePin(LED0_GPIO_Port,LED0_Pin,GPIO_PIN_SET);}// 重新使能接收中断HAL_UART_Receive_IT(huart1,(uint8_t*)rec_buf,1);}}/* USER CODE END PFP */intmain(void){/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_USART1_UART_Init();/* Infinite loop *//* USER CODE BEGIN WHILE */// 开启串口接收中断HAL_UART_Receive_IT(huart1,(uint8_t*)rec_buf,1);// 发送开始提示信息HAL_UART_Transmit(huart1,(uint8_t*)start...\r\n,10,0xFFFF);while(1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}4.4. 效果验证编译、烧录、查看结果。效果使用串口调试助手复位开发板电脑收到 start…电脑发送字符 A单片机收到后 发送 Light on 给电脑同时 LED 亮电脑发送字符 B其他字符也可单片机收到后 发送 Light off 给电脑同时 LED 灭。5. DMA模式5.1. 配置串口配置DMADMA (Direct Memory Access)直接存储器访问可以不使用CPU将数据在存储器与外设之间传输也可以从存储器 到 存储器。STM32 最多有 2 个 DMA 控制器DMA2 仅存在大容量产品中STM32F103RCT6 有两个 DMA 控制器 DMA1 和 DMA2DMA1 有 7 个通道。DMA2 有 5 个通道。接收通道设置为循环接收发送通道设置为普通模式DMA RequestUSART1_TX表示在发送数据的时候使用 DMA通道。USART1_RX表示在接收数据的时候使用 DMA通道。ModeNormal 模式表示 CPU 发起 DMA 传输请求就把数据通过DMA通道传输出去传输完成之后会触发中断Circular模式表示会循环把数据传输出去传输完成之后从头再来。Increment Address这里在内存上勾选没勾选外设因为外设这里是串口地址是固定的只需要把数据的 起始地址传输过去地址不断累加就可以了。勾选中断DMA中断优先级设置的比串口中断要低不过其实不低也能运行。5.2. 生成代码5.3. 编写代码/* USER CODE BEGIN PV */charbuf[13]running... \r\n;/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/voidSystemClock_Config(void);/** * brief The application entry point. * retval int */intmain(void){/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE */// 开启DMA接收数据通道将串口数据存到buf数组。// 这里设置的循环模式会不断接收串口数据到内存。HAL_UART_Receive_DMA(huart1,(uint8_t*)buf,13);// 串口其他 DMA 相关函数// HAL_UART_DMAResume(huart1); 恢复串口DMA// HAL_UART_DMAPause(huart1) 暂停串口DMA// HAL_UART_DMAStop(huart1); 结束串口DMAwhile(1){// 开启DMA发送传输通道// 这里设置的普通模式发送一次就关闭了。延迟一段时间后重新打开发送。HAL_UART_Transmit_DMA(huart1,(uint8_t*)buf,13);HAL_Delay(1000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}这里有个bug。MX_DMA_Init();MX_USART1_UART_Init();这两个初始化必须DMA在前USART在后否则编译通过但是运行会卡死。如果一开始就设置了DMA和串口一般没啥问题。但是如果一开始只设置了串口生成代码后又加了DMA就容易出现 DMA在后面的情况。另外可以通过设置来设置先后顺序。5.4. 效果验证编译、烧录、查看结果。效果使用串口调试助手初始化后串口助手每隔一秒收到 字符串 running…使用串口助手向开发板发送 任意字符比如 1234567890ab该字符会被存储到buf数组中之后串口助手会不断收到 发送的任意字符串如果发送的长度不足13个字符则只会替换前几个字符后面几个还是原来的。6. 串口重定向为了方便发送数据我们使用printf函数发送数据但是使用此函数需要先进行配置。6.1. 配置原理printf函数定义在stdio.h头文件中printf 函数根据 format 字符串给出的格式打印输出到 std::out标准输出中。printf 函数会调用更底层的 I/O 函数fputc去逐个字符打印。fputc 也定义于头文件 stdio.h中。因此我们如果想用 printf 打印数据到串口只需要重新定义fputc函数在fputc的函数中将数据通过串口发送称之为fputc重定向或者printf重定向。6.2. 实现配置还是上面三种的配置随便啦。6.2.1. 使用MicroLIB库MicroLib是对标准C库进行了高度优化之后的库供MDK默认使用相比之下MicroLIB的代码更少资源占用更少。步骤一在MDK中使用MicroLib重定向printf步骤二在生成的usart.c中添加重定向代码。这里放到最后/* USER CODE BEGIN 1 *//**********************printf重定向****************************/// 需要调用stdio.h文件#includestdio.hintfputc(intch,FILE*f){HAL_UART_Transmit(huart1,(uint8_t*)ch,1,0xffff);returnch;}/* USER CODE END 1 */main.cwhile(1){printf(Hello, %s \r\n,world);// 字符串printf(Test int: i %d \r\n,100);// 整数printf(Test float: i %f \r\n,1.234);// 浮点数printf(Test hex: i 0x%2x \r\n,100);// 16进制printf(中文测试 \r\n);// 中文HAL_Delay(2000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}效果验证编译、烧录、查看结果。效果使用串口调试助手每隔两秒发送数据。6.2.2. 不使用MicroLIB库不用勾选 Use MicroLIB直接在 usart.c 文件中添加以下代码。// 需要调用stdio.h文件#includestdio.h//取消ARM的半主机工作模式#pragmaimport(__use_no_semihosting)//标准库需要的支持函数struct__FILE{inthandle;};FILE __stdout;void_sys_exit(intx)//定义_sys_exit()以避免使用半主机模式{xx;}intfputc(intch,FILE*f){HAL_UART_Transmit(huart1,(uint8_t*)ch,1,0xffff);returnch;}上面这个默认使用的是AC5编译器如果改成AC6编译不过添加兼容AC5和AC6版本的。#ifEnable_RTTViewer1intfputc(intch,FILE*f){SEGGER_RTT_Write(0,ch,1);return(ch);}#else#includestdio.h// 1. 根据编译器版本自动选择禁用半主机模式的语法#ifdefined(__CC_ARM)||(defined(__ARMCC_VERSION)(__ARMCC_VERSION6010050))#if(__ARMCC_VERSION6010050)// AC6 编译器使用内联汇编__asm(.global __use_no_semihosting\n);__asm(.global __ARM_use_no_argv \n);#else// AC5 编译器使用 #pragma#pragmaimport(__use_no_semihosting)struct__FILE{inthandle;};#endif#endif// 2. 提供底层空壳函数防止链接报错FILE __stdout;void_sys_exit(intx){xx;}int_ttywrch(intch){chch;returnch;}// 3. 重定向 fputcintfputc(intch,FILE*f){USART3-TDRch;while((USART3-ISRUSART_ISR_TC)0);returnch;}#endifmain.cwhile(1){printf(Hello, %s \r\n,world);// 字符串printf(Test int: i %d \r\n,100);// 整数printf(Test float: i %f \r\n,1.234);// 浮点数printf(Test hex: i 0x%2x \r\n,100);// 16进制printf(中文测试 \r\n);// 中文HAL_Delay(2000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}效果验证每隔两秒发送数据。PS在其他文件中使用printf会有警告unction “printf” declared implicitly出现不影响使用。如果不想看到警告可以在调用的文件中添加#include stdio.h头文件即可。7. 进阶——接收不定长数据7.1. 串口中断函数、回调函数的补充明⭐⭐⭐串口中断函数每接收到一个字节就会进入一次串口中断函数。如果一个数据帧发送了多个字节就会连续进入多次中断处理函数。比如通过串口助手给单片机发送0123456789不要勾选发送新行则单片机会连续进入10次void USART1_IRQHandler(void)中断处理函数。接收完成回调函数每接收完成一次会执行一次接收完成回调函数。前面中断章节中说了接收完毕后会执行接收完成回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)但是没有具体说明什么时候才算接收完成。可以看下面这行代码显然这里是想接收10个字节并把它放到rec_buf数组中。那么接收够10个字节就表示接收完成了。// 开启串口接收中断HAL_UART_Receive_IT(huart1,(uint8_t*)rec_buf,10);那如果我只发送5个字节呢那就只会进入5次中断不会执行接收完成回调函数当再次发送5个字节的时候就会在执行第5次中断的时候执行接收完成回调函数。再如果发送8个字符呢那就只会进入8次中断不会执行接收完成回调函数当再次发送8个字节的时候会在执行2次中断并执行回调函数然后再次执行一次中断函数就卡死了…示例串口配置和中断章节的配置一样打开串口勾选中断即可。main.ccharrec_buf[10]{0};volatileuint8_tnum0;// 记录进入中断的次数volatileuint8_tnumback0;// 记录进入回调的次数voidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){// 判断是不是串口1 中断if(huart-InstanceUSART1){printf(已经进入了 %d 次中断函数了 \r\n,num);printf(第 %d 次进入回调 \r\n,numback);// 重新开启接收HAL_UART_Receive_IT(huart1,(uint8_t*)rec_buf,10);}}intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_USART1_UART_Init();// 开启串口接收中断HAL_UART_Receive_IT(huart1,(uint8_t*)rec_buf,10);while(1){printf(已经进入了 %d 次中断函数了 \r\n,num);HAL_Delay(1000);}}stm32f1xx_it.cexternvolatileuint8_tnum;voidUSART1_IRQHandler(void){num;// 记录进入中断的次数HAL_UART_IRQHandler(huart1);}发送“1234567890”10个字符10个字节可以看到会进入10次中断在最后一次中断时刚好接收够10个字节也就执行了回调函数。而当发送“12345”5个字符5个字节时进入了5次中断但是并没有接收完成如果再次发送5个字符则再进入5次中断再第5次中断时接收完成执行了回调函数。而如果发送“12345678”8个字符8个字节时进入了8次中断但是并没有接收完成如果再次发送8个字符第二次中断会执行回调函数并再次进入中断之后再点发送就没用了。。。总之前面的三种方案只能处理数据长度比较固定的数据。7.1. DMA空闲中断先说要一下串口的中断。主要说两个串口接收中断、串口空闲中断。串口接收中断UART_IT_RXNE一帧数据是表示一个或多个字节组成的有含义的字符串比如前面案例中发送的一个字符串。在一帧数据发送过程中每接收到一个字节就会进入一次到串口中断函数。如果满足接收完成的条件在中断函数中还会执行了发送完成的回调函数。串口空闲中断UART_IT_IDLE上面发送一个字符串一帧数据可能包含多个字节会不断进入接收中断函数因此发送两个字节之间的时间很短串口不能认为是空闲的。但是整个字符串一帧数据发送完成之后串口在可以发送一个字节的时间内没有再次发送数据此时就会发生空闲中断。总结接收中断空闲中断处理函数USARTx_IRQHandler()USARTx_IRQHandler()回调函数HAL_UART_RxCpltCallback()HAL库没有提供USART状态寄存器中的位UART_IT_RXNEUART_IT_IDLE触发条件接受到一个字节数据触发一次接受完一帧数据又过了一个字节时间没有接收到数据在上面的程序中当DMA串口接收开始后DMA通道会不断的将发送来的数据转移到内存也就是说接收程序一直在运行那该如何判断串口接收是否完成从而及时关闭DMA通道如何知道接收到数据的长度以便于接收不定长的呢答案便是使用串口空闲中断。解决办法串口收到数据DMA不断传输数据到内存。一帧数据发送完毕串口暂时空闲触发串口空闲中断。在中断服务函数中根据空闲中断的标志位判断是否发生空闲中断可以计算刚才收到了多少个字节的数据。存储接收到的数据清除标志位开始下一帧接收。代码配置就是DMA模式的配置main.c/* USER CODE BEGIN PV */// 自己定义两个变量uint8_trx_buffer[100];// 接收数据的数组volatileuint8_trx_len0;// 接收数据的长度/* USER CODE END PV *//* Private function prototypes -----------------------------------------------*/voidSystemClock_Config(void);/** * brief The application entry point. * retval int */intmain(void){/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_DMA_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE *///开启空闲中断__HAL_UART_ENABLE_IT(huart1,UART_IT_IDLE);// 开启DMA接收数据通道将串口数据存到buf数组。HAL_UART_Receive_DMA(huart1,(uint8_t*)rx_buffer,100);while(1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */}stm32f1xx_it.c/* USER CODE BEGIN PV */// 定义在main.c中在此处声明externuint8_trx_buffer[100];// 接收数据的数组externvolatileuint8_trx_len;// 接收数据的长度/* USER CODE END PV *//** * brief This function handles USART1 global interrupt. */voidUSART1_IRQHandler(void){/* USER CODE BEGIN USART1_IRQn 0 *//* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(huart1);/* USER CODE BEGIN USART1_IRQn 1 */// 获取IDLE状态uint8_ttmp_flag__HAL_UART_GET_FLAG(huart1,UART_FLAG_IDLE);if((tmp_flag!RESET))// 判断接收是否结束{__HAL_UART_CLEAR_IDLEFLAG(huart1);// 清除空闲中断标志HAL_UART_DMAStop(huart1);// 停止DMA通道// 查询DMA剩余传输数据个数uint8_ttemp__HAL_DMA_GET_COUNTER(hdma_usart1_rx);// 最大接收长度 - 剩余长度 已发送长度rx_len100-temp;/* 在下面写自己的处理逻辑注意这里是中断尽量简短 */// 发送接收到的数据HAL_UART_Transmit_DMA(huart1,rx_buffer,rx_len);/* 在上面写自己的处理逻辑注意这里是中断尽量简短 */// 重新开启DMAHAL_UART_Receive_DMA(huart1,rx_buffer,100);}/* USER CODE END USART1_IRQn 1 */}效果验证不管给开发板发送多长的数据都会重新发送回来。7.2. 中断环形缓冲区代码有点长代码链接在主页代码有详细注释和使用说明。
返回列表