网站诊断,如何选择一个优质网站建设公司,怎样做自己的 优惠卷网站,网站推广页文章目录#xff1a;
前言
一、准备工作
1、接线
2、新建工程
二、CubeMX的配置
1、USART1 配置 异步通信
2、通信协议参数
3、打开DMA发送、接收
三、发送操作、代码解释
四、printf 重定向到USART1
五、接收代码的编写
1、定义一个结构体变量
前言
一、准备工作
1、接线
2、新建工程
二、CubeMX的配置
1、USART1 配置 异步通信
2、通信协议参数
3、打开DMA发送、接收
三、发送操作、代码解释
四、printf 重定向到USART1
五、接收代码的编写
1、定义一个结构体变量存放接收的字节数、数据
2、开启DMA让硬件自动接收数据
3、重写DMA空闲中断回调函数
4、接收的使用示范 前言 本篇详细地用截图解释 CubeMX对USART1 的配置HAL函数使用和收发程序的编写。 收、发机制DMA发送 DAM空闲中断接收。 DMA空闲中断的搭配相当高效而且最大地节省芯片运行资源。 这里不讲解串口通信原理但通信配置、操作的图解过程比较详细因此全文的阅读会有点费时间全程需要15分钟左右。 如有错漏欢迎指正。 新手先行扫盲超简单的串口通讯的工作原理 一、准备工作 1、接线 目前绝大部分的开发板都板载了USB转TTL用于与电脑通信方便输出调试信息。 虽然各家使用的转换芯片各有不同如CH340、CP2102等但是使用上是一致的。 开发板的USB转TTL的电路大家已约定俗成基本使用USART1TX-PA9 RX-PA10。 本篇操作通用STM32绝大部分芯片因为CubeMX本就为跨芯片而生 2、新建工程 为了减少篇幅长度我们使用之前已建立的工程GPIO--推挽输出模式 点亮LED灯。 复制它整个文件夹、重新修改文件夹名称如USART1 -- DMA发送DMA空闲中断接收。 技巧只能修改工程文件夹名称不要修改工程文件的名称否则CubeMX无法重新生成。 技巧复制已有工程是老司机的日常操作能延用已写好的代码和配置大大减少开发时间。 传送门 新建一个工程(STM32F103) 新建一个工程(STM32F407) GPIO 推挽输出模式点亮LED灯 二、CubeMX的配置 倘若使用标准库进行串口的DMA收发编写那是相当耗时的需要不断调试排错。 现在有了CubeMX, 配置过程极度的简便只需三项选择1分钟也用不了。 我们先打开CubeMX配置文件。 1、USART1 配置 异步通信 在选择异步通信后将会使用默认引脚TX-PA9, RX-PA10. 我们无需对引脚进行任何配置CubeMX帮我们自动配置好 2、通信协议参数 本篇使用USART1的常用配置115200-None-8-1。 下图中4个主要的通信协议参数一般只需修改波特率。蓝色的3项基本万年不动。 3、打开DMA发送、接收 如图操作DMA Settings / ADD / Select / 添加USART_RX “USART_TX; 网上很多教程只使用了USART_RX。 除非TX所用的DMA通道已被其它设备占用了否则干它 添加完成后的状态 4、设置RX引脚上拉 在选择异步通信后CubeMX会自动配置引脚的工作模式。 但是默认配置不打开上下拉。这会使引脚在悬空时电平不确定容易产生误接收。 我们把RX接收引脚修改为上拉Pull-up)给引脚固定一个弱上拉以避免悬空时产生误接收 好了就这么简单。 中断配置 默认就行。优先级配置默认就行。DMA配置默认就行。 然后点击 GENERATE CODE生成工程吧 三、发送操作、代码解释 我们打开生成后的Keil工程。 在main.c文件能看到已增加了DMA和USART1的初始化代码。 初始化部分CubeMX都已帮我们编写好了发送部分的底层处理、逻辑CubeMX也编写好了。 在工程中现在就能直接使用下面这 3个 函数发送任何数据
HAL_UART_Transmit (huart1, uint8_t *pData, uint16_t Num, 超时值);
HAL_UART_Transmit_IT (huart1, uint8_t *pData, uint16_t Num);
HAL_UART_Transmit_DMA (huart1, uint8_t *pData, uint16_t Num); 先上板测试后面再解释 在/* USER CODE BEGIN 2 */ 与 /* USER CODE END 2 */ 之间敲入以下发送代码 /* USER CODE BEGIN 2 *//* 用户代码必须写在配对的BEGIN与END之间 */static char strTem[100] Hello World!\r; // 定义一个数组也可以是其它的数据如结构体等HAL_UART_Transmit (huart1, (uint8_t*)strTem, strlen(strTem), 0xFFFF); // 发送方式2HAL_UART_Transmit(), 不推荐使用; 阻塞式发送当调用后程序会一直死等不干其它事了(中断除外)直到发送完毕HAL_UART_Transmit_IT (huart1, (uint8_t*)strTem, strlen(strTem)); // 发送方式3HAL_UART_Transmit_IT(), 推荐使用; 利用中断发送非阻塞式大大减少资源占用; 注意当上次的调用还没完成发送下次的调用会直接返回(放弃)所以要想连接发送两行调用间要么判断串口结构体gState的值要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时, 用时计算1/(波特率*11*前一帧字节数) while((huart1)-gState ! HAL_UART_STATE_READY); // 等待上条发送结束; 也可以用HAL_Delay延时法但就要计算发送用时; 两种方法都是死等法程序暂时卡死不会往下运行; 如果两次发送间隔时间大如大于100ms, 就不用判断语句了。HAL_UART_Transmit_DMA (huart1,(uint8_t*)strTem, strlen(strTem)); // 发送方式4HAL_UART_Transmit_DMA()推荐使用; 利用DMA发送非阻塞式最大限度减少资源占用; 注意当上次的调用还没完成发送下次的调用会直接返回(放弃); 所以要想连接发送两行调用间要么判断串口结构体gState的值要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时用时计算1/(波特率*11*前一帧字节数)/* USER CODE END 2 */ 打开电脑的串口助手。 编译、烧录。串口助手马上有显示 如果你那边烧录后没有显示要么是串口号错了要么是没有打勾keil的自动复位。 下面对3个函数的使用逐一解释不建议新手跳过有避坑干货。 1、HAL_UART_Transmit (huart1, uint8_t *pData, uint16_t Num, 超时值); 阻塞式发送。参数串口数据地址发送的字节数ms超时值 每发送一个字节死等好了继续发下一个再死等不断重复。 就是以前标准库种那最普通的死等法只是它增加了一个超时值。 超时值如果指定时间内没发送完毕就直接返回防止卡死。数据发送通信需时 1秒 ÷ 波特率 × 字节数 × 10 × 1000ms。举例115200波特率100字节大约用时 9ms。 新手如果不会计算直接把超时值填大一点如50ms。 2、HAL_UART_Transmit_IT (huart1, uint8_t *pData, uint16_t Num); 利用中断发送。参数串口数据地址发送的字节数 向寄存器填入一个字节程序就继续干其它的事去当一个字节发送完成后会产生发送中断CubeMX生成的回调函数自动填入下一个字节不断重复不用干预。 非阻塞式发送。能大大地减少程序运行时间的占用。 有一点要注意当连续地调用本中断发送函数时调用的间隔时间小于通信所需的用时按上这时后面那条函数调用会直接返回放弃发送。 因为函数内部在发送前会判断串口的忙状态如果在忙还在发送上一包数据就放弃本包数据返回。 解决的方法有两个 ① 最常用的两行中断发送函数间插入HAL_Delay(10)原理参考上面的发送需时。 ② 两行中断发送函数间插入 while((huart1)-gState ! HAL_UART_STATE_READY); 和 HAL_Delay() 一样都是死等但能省了那么一点点运行时间。 3、HAL_UART_Transmit_DMA (huart1, uint8_t *pData, uint16_t Num); DMA发送。参数串口数据地址发送的字节数。 上面的中断发送函数100个字节会产生100次中断。这个DMA发送函数全程只产生一次中断。 调用后函数给DMA数据地址DMA就自动开始搬砖它会把数据逐字节搬运到串口的DR寄存器上等串口发送完这个字节了再自动搬运下一个过程完全不占用程序运行资源。搬完了就产生一个中断给程序打个招呼。通常我们程序上把这个“招呼”也省略了不用理会它。 3个发送函数中推荐使用这个DMA发送函数发送的最优解。 同样的两行DMA发送函数间注意发送间隔否则放弃发送直接返回。处理方法同上。 四、printf 重定向到USART1 约定俗成地常使用printf函数输出一些调试信息。它能很灵活地控制输出字符串的格式。 约定俗成地printf 常通过 USART1 输出数据到串口助手而非USART2、3...。 要使用 printf 需要做两个事 ① 在文件头插入 #include stdio.h ; ② 重定向 printf, 使它能通过 USART1 输出。 把下面代码复制到 main.c的 BEGIN 4 与 END 4 注释行之间即可使用。 无需打勾“Use MicroLIB。
#include stdio.h
#pragma import(__use_no_semihosting)struct __FILE
{int handle;
}; // 标准库需要的支持函数FILE __stdout; // FILE 在stdio.h文件
void _sys_exit(int x)
{x x; // 定义_sys_exit()以避免使用半主机模式
}int fputc(int ch, FILE *f) // 重写fputc函数使printf的输出由UART1实现, 这里使用USART1
{// 注意不能使用HAL_UART_Transmit_IT(), 机制上会冲突; 因为调用中断发送函数后如果上次发送还在进行就会直接返回它不会继续等待也不会数据填入队列排队发送HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0x02); // 使用HAL_UART_Transmit相等于USART1-DR ch, 函数内部加了简单的超时判断(ms)防止卡死return ch;
} 现在试试我们的 printf 输出效果 在刚才敲入代码的位置在3个发送函数之前添加一行printf, 尝试输出系统运行时钟的值。 整体如下: /* USER CODE BEGIN 2 *//* 用户代码必须写在配对的BEGIN与END之间 */printf(\r系统运行时钟%d MHz\r, SystemCoreClock/1000000); // 发送方式1使用printf发送它有灵活的格式化很适合处理字符串; 注意printf需要重定向fputc函数才能使用否则程序会卡死; 本示例已重写fputc, 在main.c的底部附近 static char strTem[100] Hello World!\r; // 定义一个数组也可以是其它的数据如结构体等HAL_UART_Transmit (huart1, (uint8_t*)strTem, strlen(strTem), 0xFFFF); // 发送方式2HAL_UART_Transmit(), 不推荐使用; 阻塞式发送当调用后程序会一直死等不干其它事了(中断除外)直到发送完毕HAL_UART_Transmit_IT (huart1, (uint8_t*)strTem, strlen(strTem)); // 发送方式3HAL_UART_Transmit_IT(), 推荐使用; 利用中断发送非阻塞式大大减少资源占用; 注意当上次的调用还没完成发送下次的调用会直接返回(放弃)所以要想连接发送两行调用间要么判断串口结构体gState的值要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时, 用时计算1/(波特率*11*前一帧字节数) while((huart1)-gState ! HAL_UART_STATE_READY); // 等待上条发送结束; 也可以用HAL_Delay延时法但就要计算发送用时; 两种方法都是死等法程序暂时卡死不会往下运行; 如果两次发送间隔时间大如大于100ms, 就不用判断语句了。HAL_UART_Transmit_DMA (huart1,(uint8_t*)strTem, strlen(strTem)); // 发送方式4HAL_UART_Transmit_DMA()推荐使用; 利用DMA发送非阻塞式最大限度减少资源占用; 注意当上次的调用还没完成发送下次的调用会直接返回(放弃); 所以要想连接发送两行调用间要么判断串口结构体gState的值要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时用时计算1/(波特率*11*前一帧字节数)/* USER CODE END 2 */ 串口助手的输出效果如下图能正常输出了 然后我们来试个错把printf这行剪切到3个发送函数之下。再编译烧录运行。 怎样没有输出了吧 原因、解决的方法如上面发送函数所述不再重述。 上述3个函数小编在测试时发现还有两三处暂时没理解透的现象但是发生的场景较少这里就先不啰嗦了等以后有更深入的理解时再更新上来。 五、接收代码的编写 发送数据可以调用现成的函数而接收数据现成函数不太好用。 接收也有3个函数和发送的3个函数相对应
HAL_UART_Receive (huart1, uint8_t *pData, uint16_t Num, 超时值);
HAL_UART_Receive_IT (huart1, uint8_t *pData, uint16_t Num);
HAL_UART_Receive_DMA (huart1, uint8_t *pData, uint16_t Num); 一般大家都不使用这三个函数太TM的难用了有兴趣的可csdn搜它们的使用优劣分析。 我们利用HAL库现成的资源另敲十来行代码令串口的接收机制更实用、更灵活。 完成后整个接收过程1个结构体 1个HAL库函数 1个回调函数全程自动接收。 共分4小项下面将有详细操作图解 ① 定义一个结构体变量存放接收的字节数、数据数组。 ② 开启DMA让硬件自动接收数据放到缓存 ③ 重写回调函数当一帧数据接收好了把缓存的数据转存到全局结构体变量里备用。 ④ 在需要使用串口接收的地方如在while中判断接收字节数0, 即为接收到新一帧数据了。 1、定义一个结构体变量存放接收的字节数、数据 ① 首先在main.h文件新建一个结构体类型 在 /* USER CODE BEGIN ET */ 与 /* USER CODE END ET */ 之间新建一个结构体类型。
/* USER CODE BEGIN ET */
/* 所有用户代码必须写在配对的BEGIO与END注释行之间否则重新生成时会被删除 */typedef struct // 声明一个结构体方便管理变量
{uint16_t ReceiveNum; // 接收字节数uint8_t ReceiveData[512]; // 接收到的数据uint8_t BuffTemp[512]; // 接收缓存; 注意这个数组只是一个缓存用于DMA逐个字节接收当接收完一帧后数据在回调函数中转存到 ReceiveData[ ] 存放。即双缓冲有效减少单缓冲的接收过程新数据覆盖旧数据
} xUSATR_TypeDef;extern xUSATR_TypeDef xUSART1 ; // 定义结构体方便管理变量。也可以不用结构体用单独的变量/* USER CODE END ET */它有3个成员 uint16_t ReceiveNum; // 接收字节数只要字节数0即为接收到新一帧数据 uint8_t ReceiveData[512]; // 接收到的数据 uint8_t BuffTemp[512]; // 临时缓存在DMA空闲中断中将把一帧数据复制到ReceivedData[ ] 有些教程还有一个Flag变量用来标记是否接收到数据。我们直接用ReceiveNum判断更简单。 在定义结构体类型的下面一行代码中用extern声明了一个结构体变量它将在main.c中定义。 技巧如果希望定义的结构体类型工程全局可用就要在h文件中定义其它文件引用这个h文件。 技巧如果希望定义的变量能被工程全局调用就在h文件中用extern声明然后在某个c文件中定义。 ② 回到main.c定义结构体变量 在 /* USER CODE BEGIN 0 */ 与 /* END 0 */ 之间使用新建的结构体类型定义我们的结构体变量。
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* 所有用户代码必须写在配对的BEGIO与END注释行之间否则重新生成时会被删除 */xUSATR_TypeDef xUSART1 {0}; // 定义结构体方便管理变量。也可以不用结构体用单独的变量/* USER CODE END 0 */ 现在我们拥有一个了全局变量xUSART1。 以后的其它文件如蓝牙模块驱动、串口屏驱动只要在文件中引用main.h就能通过这个结构体变量使用串口1接收的数据了。 2、开启DMA让硬件自动接收数据 我们整个接收过程仅使用到1个HAL库函数 只需在main()函数的初始化部分调用HAL库函数 HAL_UARTEx_ReceiveToIdle_DMA (串口、缓存、字节数) ; 参数串口、接收缓存区、最大接收字节数 作用使能DMA、使能串口的空闲中断正式进入接收状态。 操作在 main.c的 /* USER CODE BEGIN 2 */ 与 /* END 2 */ 之间插入函数
HAL_UARTEx_ReceiveToIdle_DMA(huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp)); // 开启DMA空闲中断 插入后的位置如下图 调用函数后硬件就会立刻进入自动接收状态从RX引脚接收到的数据会逐个字节顺序存放到指定缓存中这里我们指定的缓存是xUSART1.BuffTemp。 因为函数内部开启了DMA中断、空闲中断所以达成下列两个条件之一就会触发中断 ① DMA接收的字节数达到了参数中的最大值 ② 串口发生空闲中断即RX引脚超过1字节的时间没有新信号。 当上述中断产生时硬件自动调用其相关的中断服务函数再继而调用回调函数。 CubeMX生成的代码已编写好上述两个中断服务函数还定义了一个它俩最终调用的回调函数。注意这个回调函数是一个弱函数。 因此我们不用管中断服务函数只需重写这个回调函数就能实现对接收数据的处理。 3、重写DMA空闲中断回调函数 DMA完成中断、空闲中断所调用的回调函数 HAL_UARTEx_RxEventCallback串口接收到的字节数); 弱函数定义在stm32xx_hal_gpio.c文件的底部。 现在我们对它进行重写以实现对接收数据的处理。 在main.c的底部/* USER CODE BEGIN 4 */ 与 /* END 4 */ 之间新建函数并编写其代码
/* USER CODE BEGIN 4 */
/* 所有用户代码必须写在配对的BEGIN与 END之间 *//******************************************************************************* 函 数 HAL_UARTEx_RxEventCallback* 功 能 DMA空闲中断回调函数* 参 数 UART_HandleTypeDef *huart // 触发的串口* uint16_t Size // 接收字节* 返回值 无* 备 注 1这个是回调函数不是中断服务函数。技巧使用CubeMX生成的工程中中断服务函数已被CubeMX安排妥当我们只管重写回调函数* 2触发条件当DMA接收到指定字节数时或产生空闲中断时硬件就会自动调用本回调函数无需进行人工调用;* 2必须使用这个函数名称因为它在CubeMX生成时已被写好了各种函数调用、函数弱定义(在stm32xx_hal_uart.c的底部); 不要在原弱定义中增添代码而是重写本函数* 3无需进行中断标志的清理它在被调用前已有清中断的操作;* 4生成的所有DMA空闲中断服务函数都会统一调用这个函数以引脚编号作参数* 5判断参数传进来的引脚编号即可知道是哪个串口接收收了多少字节
******************************************************************************/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if (huart huart1) // 判断串口{__HAL_UNLOCK(huart); // 解锁串口状态xUSART1.ReceiveNum Size; // 把接收字节数存入结构体xUSART1.ReceiveNum以备使用memset(xUSART1.ReceiveData, 0, sizeof(xUSART1.ReceiveData)); // 清0前一帧的接收数据memcpy(xUSART1.ReceiveData, xUSART1.BuffTemp, Size); // 把新数据从临时缓存中复制到xUSART1.ReceiveData[], 以备使用HAL_UARTEx_ReceiveToIdle_DMA(huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp)); // 再次开启DMA空闲中断; 每当接收完指定长度或者产生空闲中断时就会来到这个}
}/* USER CODE END 4 */ 上面代码我们重点解释后四行 ① xUSART1.ReceiveNum Size; 把接收的字节数存入结构体 xUSART1.ReceiveNum以备使用 。 在程序的其它地方判断 ReceivNum 0 就能知道是否收到新一帧数据了。 ② memset(xUSART1.ReceivedData, 0, sizeof(xUSART1.ReceivedData)); 清0前一帧的数据缓存 ③ memcpy(xUSART1.ReceivedData, xUSART1.BuffTemp, Size); 把新数据从临时缓存中复制到xUSART1.ReceivedData[], 以备使用 从结构体和这段回调函数中可以发现这是一个双缓存的操作思路。 .ReceivedData用于存放接收后完整的一帧数据对外使用 。 .BuffTemp用于DMA接收过程是一个中间缓存。 ④ HAL_UARTEx_ReceiveToIdle_DMA(huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp)); 再次开启DMA空闲中断进入接收状态。 我们在main()函数的初始化部分已调用过这个函数了为什么要在回调函数中再次调用 因为在DMA的中断服务函数里会关闭DMA即只接收一次。所以在接收完一帧后再次调用函数就能让DMA开始工作接收下一帧。在这个位置调用 能让DMA不断地循环工作。 其实在CubeMX配置中DMA有一个选项 Mode的circular, 可以让DMA进行连续地的工作接收完成后无需在回调函数里再次开启DMA 。但是目前的CubeMX版本(V6.10这个参数的选择会使我们上面的DMA接收与发送相冲突。那我们二选一好了自行手工调用。 注意一点本篇的处理是保存最后一帧数据。当有新一帧数据来了会自动盖掉旧帧数据。 至此接收工作已准备妥当。程序运行后硬件会自动接收并把接收的帧数据存放到结构体中。
4、接收的使用示范 我们来试试使用的效果吧 ① 在main.c的while函数里编写接收判断代码 /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ /* 用户代码必须写在配对的BEGIN与END之间 */ if (xUSART1.ReceiveNum) // 判断字节数 { printf(\r USART1 接收到一帧数据 \r); // 提示 printf(字节数%d \r, xUSART1.ReceiveNum); // 显示字节数 printf(ASCII : %s\r, (char *)xUSART1.ReceiveData); // 显示数据以ASCII方式显示即以字符串的方式显示 printf(16进制: ); // 显示数据以16进制方式显示每一个字节的值 for (uint16_t i 0; i xUSART1.ReceiveNum; i) // 逐个字节输出 printf(0x%X , xUSART1.ReceiveData[i]); // 以16进制显示 printf(\r\r); // 显示换行 xUSART1.ReceiveNum 0; // 清0接收标记 } } /* USER CODE END 3 */ ② 工程编译烧录
③ 打开串口助手参数设置 115200-None-8-1, 打开对应的串口端口。 按一下板子右下角的复位键串口输出如下图 ③ 在串口的发送区输入字符串 天气不错喔~~或者其它数据。 点击发送串口助手将通过PA10发送到开发板。在程序的while函数中那段代码判断接收到数据后为了方便观察将通过USART1的PA9发出数据串口助手接收后显示如下 ④ 试试16进制数据的发送。 发送区打勾16进制发送输入随意16进制值不用加0x用空格作间隔。 注意16进制的值不一定是ASCII码表的显示范围值所以在ASCII显示中会出现乱码正常现象。 至此USART1的收发已完整地展示完毕。
如有错漏欢迎留言指正修改~~