| 
 
| 前言 在微控制器的开发中,串口打印文本的功能非常重要!在每一款微控制器都会有这样的功能,在我们的开发中,串口具有举足轻重的地位。另外STM32F4里面拥有“USART模块”,即异步同步收发器,除了可以串口输出/接受数据,还可以使用在RS232、RS485、智能卡、LIN协议、调制解调等功能。本文硬件基于正点原子STM32F4探索者开发板,采用STM32F407ZGT6主控芯片,本文的代码运行环境为STM32CubeIDE,实现了串口打印和接收数据,并实现了printf和scanf的函数功能,可以酌情移植到其他硬件电路中。
 
 串口配置
 串口配置分以下步骤完成。
 
 串口通信配置
 
 
   
 这里采用异步通信模式,没有使用硬件流控制,波特率为115200,8位数据位,没有奇偶校验,1位停止位,接收和发送都打开,采用16位的过采样模式。
 关于过采样,实际操作的寄存器是USART_CR1的Bit15,实际原理可以看一下参考手册,这里不再赘述。
 
 
   
 这里根据原理图的连接状态,选择引脚重映射。目前没有使用DMA,但开启了中断。
 
 重映射引脚配置
 
 
   
 中断配置
 如果需要使用中断接受或发送,可以使能中断,并配置优先级。
 
 
   
 
   
 将中断打开,优先级设置的比滴答定时器低一些。(数字越小,优先级越高。能不能打断别的中断,看的是抢占优先级,数字越小越优先执行)
 至此,串口的配置结束。
 
 测试代码
 先测试一下简单的发送数据
 
 
   
 在main.c里面自动生成了串口和GPIO的初始化操作,之后我们会分析串口初始化里面的代码,GPIO的初始化简单,就不在这里赘述了。
 
 先说明一下main.h里面定义的宏
 #define STANDBY_MODE_TEST 0/*Notice: If you use PA0 as a KEY_UP button, This value mast be 0*/
 #define UART_RXBUFFER_SIZE (uint16_t)1 //When usart receive ONE Byte, save data to a variable
 #define USART_RX_TOTALLY_BUFFER 100
 
 
 main函数执行之前的全局变量定义
 在main函数之前,定义全局变量:
 
 uint8_t USART_RX_BUFFER[USART_RX_TOTALLY_BUFFER];//Totally Save RX datas
 uint8_t aRxBuffer[UART_RXBUFFER_SIZE];//When usart receive ONE Byte, save data here! Notice: this array only has ONE element.
 extern uint16_t USART_RX_STA;/*Use this variable as a register; Bit15--have received complete or NOT; Bit14--receive 0x0d or NOT; Bit13-Bit0--the subscript of receiving array*/
 
 
 在usart.c里面定义的变量
 
 extern uint8_t aRxBuffer[UART_RXBUFFER_SIZE];//When usart receive ONE Byte, save data here! Notice: this array only has ONE element.
 extern uint8_t USART_RX_BUFFER[USART_RX_TOTALLY_BUFFER];//Totally Save RX datas
 uint16_t USART_RX_STA = 0;/*Use this variable as a register; Bit15--have received completely or NOT; Bit14--receive 0x0d or NOT; Bit13-Bit0--the subscript of receiving array*/
 
 
 初始化串口和GPIO之后打开中断接收使能
 在main函数中,进入while(1)死循环之前,打开串口的中断接收。
 
 HAL_UART_Receive_IT(&huart1, (uint8_t *)aRxBuffer, UART_RXBUFFER_SIZE);
 
 
 下面讲一下HAL_UART_Receive_IT函数的参数:&huart1就是USART1的句柄,aRxBuffer指针就是存放接收收到数据的地址,最需要前面注意的参数是:UART_RXBUFFER_SIZE(前面定义此宏为1)。意味着,接收到1个字节的数据,就要执行一次接收完成回调函数HAL_UART_RxCpltCallback(huart)。
 【注意:每接收到一个字节数据,就会触发中断,执行USART1_IRQHandler(void) 中断处理函数,但我们可以人为设置,当接收到多少字节数据的时候,微控制器STM32调用并执行到HAL_UART_RxCpltCallback(huart) 函数,这个函数是需要我们重写的,以此达到通知我们的目的。这个过程是HAL库的内部逻辑完成的。】
 
 由上面这行代码,告诉HAL库,接收到1字节的数据就要调用HAL_UART_RxCpltCallback(huart) 函数来通知我们。而这个函数我们是可以重定义的,里面写我们自己的逻辑。
 
 串口打印数据测试
 写在while(1)死循环的代码:
 
 char send[] = "Hello World\r\n";
 HAL_UART_Transmit(&huart1, (uint8_t *)send, sizeof(send), 1000);
 
 
 或者我们直接操作寄存器:
 
 USART1->DR = 'A';
 while(USART1->SR & 0x40 == 0)
 {
 /*wait*/
 }
 
 
 
 这里的0x40是在检测是否发完了。
 
 
   
 
   
 这时候,使用TTL转USB模块,连接电脑,就可以看到串口调试助手上接收到数据了。
 
 将接收到的数据发送回去
 编写接收完成回调函数
 在usart.c里添加代码:
 
 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
 {
 if(huart->Instance == USART1)
 {
 if((USART_RX_STA & 0x8000) == 0)//have NOT received completely
 {
 if(USART_RX_STA & 0x4000)//接收到了0x0d,意味着下一个字符应该是0x0a。回车由这两个字符组成
 {
 if(aRxBuffer[0] != 0x0a)
 {
 USART_RX_STA = 0;//receive ERROR! receive again
 }
 else
 {
 USART_RX_STA |= 0x8000;//receive completely
 }
 }
 else
 {
 if(aRxBuffer[0] == 0x0d)//received 0x0d
 {
 USART_RX_STA |= 0x4000;
 }
 else
 {
 /*Have NOT receive "ENTER" */
 USART_RX_BUFFER[USART_RX_STA & 0x3FFF] = aRxBuffer[0];/*Store data to receiving array, the subscript is given by USART_RX_STA[13-0]*/
 USART_RX_STA++;/*the subscript given by USART_RX_STA[13-0] plus ONE*/
 
 /*At this time the Bit15 and Bit 14 are both 0*/
 if(USART_RX_STA > USART_RX_TOTALLY_BUFFER - 1 )/*receive overflow*/
 {
 USART_RX_STA = 0;/*receive again*/
 }
 }
 }
 }
 else
 {
 /* The handle code is in main function, when Receve Complete*/
 }
 }
 HAL_UART_Receive_IT(&huart1, (uint8_t *)aRxBuffer, UART_RXBUFFER_SIZE);
 }
 
 
 
 有没有留意到,这个函数就是前面提到的——接收完成回调函数?由于我们设置,接收到1字节数据就执行此函数一次,那么我们在这里写了逻辑,来完成我们的目标:
 如果串口接收到很长的字符串,我们可以将其保存在一个字符数组中,以备下一步将其发送出去。
 【这里参考了正点原子的代码】
 
 详解代码逻辑
 在讲解下面的内容之前,先分析一下上面在中断回调函数里的代码逻辑,方便大家理解和使用这里的代码。
 
 首先,在开启接收中断的时候,大家应该还记得,我们希望STM32接收到1字节的数据就调用中断回调函数来通知我们。所以,我们就在这里处理这刚刚接收到的1字节数据,它存储在指针aRxBuffer所指向的1字节内存中。
 USART_RX_STA 这个变量在这里当寄存器使用了,它的BIT15代表的意思是接收字符串已经完成;BIT14代表接收到“半个回车”,也就是接收到了0x0d。所谓“半个回车”,是说,回车符是由两个字符组成的。没错,你在串口调试助手上点击发送,并且已经勾选了“发送新行”,那就会自动在字符串后面发送两个字符:0x0d 0x0a
 
 
   
 当然我们在串口调试助手上发送的字符串不只是一个回车。设想一个场景,我们在串口调试助手上发送了:ab。那么在我们的STM32的串口上,会受到ab+0x0d+0x0a,一共4个字符。收到a的时候,判断一下,之前没有收到“半个回车”,那么这个字符是普通字符,就存储在我们的字符数组USART_RX_BUFFER。
 至于存储在这个数组的那个下标的位置呢?由USART_RX_STA 的BIT0~BIT13里面存储的数字决定。可想而知,这是2的14次方个下标的容量,已经完全满足当下标的条件了,是不是?简单计算一下,2^14 = 16384,我们最大可以使用到USART_RX_BUFFER[16383],但我们不会定义这么大的数组。
 一旦放进字符数组USART_RX_BUFFER中,就把下标加1,也就是USART_RX_STA 加1。直到收到回车为止。当收到了回车,USART_RX_STA 的BIT15就是1,就不会再存储数据进去了。下面就是在主函数中检查这个BIT是不是1,如果是1,那么就可以从字符数组中取出来接收的字符串,然后进行处理了,你想发送回去也行,想进一步处理代表什么含义也可以,完全由你!
 
 main.c函数里加入逻辑代码
 在进入while死循环之前,定义times变量用于计数。
 
 uint16_t times=0;
 
 
 在while(1)死循环中,写如下代码:
 
 if((USART_RX_STA&0x8000) != 0)
 {
 len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度
 uint8_t i = len;
 while(i--)
 {
 char send0[] = "\r\nwhat you send is:";
 HAL_UART_Transmit(&huart1,(uint8_t*)send0,sizeof(send0),1000);        //发送接收到的数据
 HAL_UART_Transmit(&huart1,(uint8_t*)USART_RX_BUFFER,len,1000);        //发送接收到的数据
 
 }
 USART_RX_STA=0;
 }
 else
 {
 times++;
 if(times%5000==0)
 {
 times = 0;
 
 char sss[] = "\r\n学而时习之, 不亦说乎!\r\n";
 HAL_UART_Transmit(&huart1, (uint8_t *)sss, sizeof(sss),1000);
 
 }
 }
 
 
 
 至此,已经将代码编写完毕!
 
 实验现象
 
 
   
 prinf和scanf函数的功能实现
 下面增加printf和scanf函数的实现代码。在Keil中,通过写下面的代码,并且在Use MicoLib打勾来实现。但在STM32CubeIDE里不行。
 
 #pragma import(__use_no_semihosting)
 //标准库需要的支持函数
 struct __FILE
 {
 int handle;
 };
 
 FILE __stdout;
 //定义_sys_exit()以避免使用半主机模式
 void _sys_exit(int x)
 {
 x = x;
 }
 //重定义fputc函数
 int fputc(int ch, FILE *f)
 {
 while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
 USART1->DR = (uint8_t) ch;
 return ch;
 }
 
 
 
 下面是方法:
 新建两个文件,retarget.c和retarget.h。下面是代码:
 
 retarget.c文件
 // All credit to Carmine Noviello for this code
 // https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/src/retarget/retarget.c
 
 #include <_ansi.h>
 #include <_syslist.h>
 #include <errno.h>
 #include <sys/time.h>
 #include <sys/times.h>
 #include <limits.h>
 #include <signal.h>
 //#include <../Inc/retarget.h>
 #include <retarget.h>
 #include <stdint.h>
 #include <stdio.h>
 
 #if !defined(OS_USE_SEMIHOSTING)
 
 #define STDIN_FILENO  0
 #define STDOUT_FILENO 1
 #define STDERR_FILENO 2
 
 UART_HandleTypeDef *gHuart;
 
 void RetargetInit(UART_HandleTypeDef *huart) {
 gHuart = huart;
 
 /* Disable I/O buffering for STDOUT stream, so that
 * chars are sent out as soon as they are printed. */
 setvbuf(stdout, NULL, _IONBF, 0);
 }
 
 int _isatty(int fd) {
 if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
 return 1;
 
 errno = EBADF;
 return 0;
 }
 
 int _write(int fd, char* ptr, int len) {
 HAL_StatusTypeDef hstatus;
 
 if (fd == STDOUT_FILENO || fd == STDERR_FILENO) {
 hstatus = HAL_UART_Transmit(gHuart, (uint8_t *) ptr, len, HAL_MAX_DELAY);
 if (hstatus == HAL_OK)
 return len;
 else
 return EIO;
 }
 errno = EBADF;
 return -1;
 }
 
 int _close(int fd) {
 if (fd >= STDIN_FILENO && fd <= STDERR_FILENO)
 return 0;
 
 errno = EBADF;
 return -1;
 }
 
 int _lseek(int fd, int ptr, int dir) {
 (void) fd;
 (void) ptr;
 (void) dir;
 
 errno = EBADF;
 return -1;
 }
 
 int _read(int fd, char* ptr, int len) {
 HAL_StatusTypeDef hstatus;
 
 if (fd == STDIN_FILENO) {
 hstatus = HAL_UART_Receive(gHuart, (uint8_t *) ptr, 1, HAL_MAX_DELAY);
 if (hstatus == HAL_OK)
 return 1;
 else
 return EIO;
 }
 errno = EBADF;
 return -1;
 }
 
 int _fstat(int fd, struct stat* st) {
 if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) {
 st->st_mode = S_IFCHR;
 return 0;
 }
 
 errno = EBADF;
 return 0;
 }
 
 #endif //#if !defined(OS_USE_SEMIHOSTING)
 
 
 
 
 retarget.h文件
 // All credit to Carmine Noviello for this code
 // https://github.com/cnoviello/mastering-stm32/blob/master/nucleo-f030R8/system/include/retarget/retarget.h
 
 #ifndef _RETARGET_H__
 #define _RETARGET_H__
 
 #include "stm32f4xx_hal.h"
 #include <sys/stat.h>
 
 void RetargetInit(UART_HandleTypeDef *huart);
 int _isatty(int fd);
 int _write(int fd, char* ptr, int len);
 int _close(int fd);
 int _lseek(int fd, int ptr, int dir);
 int _read(int fd, char* ptr, int len);
 int _fstat(int fd, struct stat* st);
 
 #endif //#ifndef _RETARGET_H__
 
 
 main.c文件中增加的代码
 #include "retarget.h"
 
 
 完成初始化串口和GPIO之后,就可以调用如下函数:
 
 RetargetInit(&huart1);
 
 
 之后在while(1)里面写如下代码测试:
 
 printf("\r\n输入三个数字,以空格间隔:\r\n");
 scanf("%d %d %d", &a, &b, &c);
 printf("\r\n输入的数字为:%d, %d, %d\r\n", a, b, c);
 
 
 测试结果
 
 
   
 参考文献
 [1]关于printf和scanf函数的实现,参考文章https://blog.csdn.net/qq_42212961/article/details/105803129
 [2]关于接收串口信息并将其发送回去的代码,参考正点原子的官方例程
 ————————————————
 
 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
 
 原文链接:https://blog.csdn.net/smart_boy__/article/details/148319101
 
 
 | 
 |