[STM32F4] STM32F4串口打印功能的实现及拓展

[复制链接]
1300|0
 楼主| xiaoqizi 发表于 2025-6-13 14:59 | 显示全部楼层 |阅读模式
前言
  在微控制器的开发中,串口打印文本的功能非常重要!在每一款微控制器都会有这样的功能,在我们的开发中,串口具有举足轻重的地位。另外STM32F4里面拥有“USART模块”,即异步同步收发器,除了可以串口输出/接受数据,还可以使用在RS232、RS485、智能卡、LIN协议、调制解调等功能。本文硬件基于正点原子STM32F4探索者开发板,采用STM32F407ZGT6主控芯片,本文的代码运行环境为STM32CubeIDE,实现了串口打印和接收数据,并实现了printf和scanf的函数功能,可以酌情移植到其他硬件电路中。

串口配置
  串口配置分以下步骤完成。

串口通信配置

51511684b6f11bed5b.png

  这里采用异步通信模式,没有使用硬件流控制,波特率为115200,8位数据位,没有奇偶校验,1位停止位,接收和发送都打开,采用16位的过采样模式。
  关于过采样,实际操作的寄存器是USART_CR1的Bit15,实际原理可以看一下参考手册,这里不再赘述。

80379684b6f0c4c9e6.png

  这里根据原理图的连接状态,选择引脚重映射。目前没有使用DMA,但开启了中断。

重映射引脚配置

61947684b6f05444e1.png

中断配置
  如果需要使用中断接受或发送,可以使能中断,并配置优先级。

69809684b6eff1a7da.png

54689684b6ef8aa210.png

  将中断打开,优先级设置的比滴答定时器低一些。(数字越小,优先级越高。能不能打断别的中断,看的是抢占优先级,数字越小越优先执行)
  至此,串口的配置结束。

测试代码
先测试一下简单的发送数据

78767684b6ef0b600f.png

  在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是在检测是否发完了。

78241684b6ee08edd9.png

2742684b6edad7f56.png

  这时候,使用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

26614684b6ebfc5cbd.png

  当然我们在串口调试助手上发送的字符串不只是一个回车。设想一个场景,我们在串口调试助手上发送了: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);
               
        }               
}



至此,已经将代码编写完毕!

实验现象

13252684b6eb4c8c9f.png

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);


测试结果

72454684b6ea1a7104.png

参考文献
[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

您需要登录后才可以回帖 登录 | 注册

本版积分规则

130

主题

4344

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部