zl程序教程

您现在的位置是:首页 >  其他

当前栏目

STM32 printf 方法重定向到串口UART

2023-03-07 09:39:27 时间

在嵌入式系统中调试代码是很麻烦的一件事, 如果能方便地输出调试信息(与调试者交互), 能使极大加快问题排查的过程. 串口在嵌入式领域是一个比较重要的通讯接口. 因为没有显示设备, 在单片机的程序里调用printf()打印内容是不可见的,但我们可以利用它的外设来实现printf(),比如串口, 串口基本上大多数单片机都有, 通常用串口来打印内容. 通过重写fputc()函数来实现. fputc()是printf()的底层函数, 通过它把要打印的数据发送到串口上去.

Keil ARM 编译环境

不使用 MicroLib的普通方式

  1. 禁用半主机模式, 禁用了半主机模式才能使用标准库函数printf()打印信息到串口
    说明: 半主机模式是ARM单片机的一种调试机制,跟串口调试不一样,它需要通过仿真器来连接电脑,并调用相应的指令来实现单片机向电脑显示器打印信息(或者从电脑键盘读取输入)。这种方法比串口调试更复杂, 需要用仿真器实现.
  2. include头文件 #include "stdio.h"
  3. 重写 fputc方法
  4. 重新定义 __FILE, __stdout, __stdin这三个变量, 以及重写_sys_exit()
#pragma import(__use_no_semihosting_swi)

// Change it if you use different USART port
#define USARTx USART1

struct __FILE { int handle; };
FILE __stdout;
FILE __stdin;

int fputc(int ch, FILE *f) {
  while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET){}
  USART_SendData(USARTx, ch);
  return(ch);
}

int fgetc(FILE *f) {
  char ch;
  while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET){}
  ch = USART_ReceiveData(USARTx);
  return((int)ch);
}

int ferror(FILE *f) {
  return EOF;
}

void _ttywrch(int ch) {
  while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET){}
  USART_SendData(USARTx, ch);
}

void _sys_exit(int return_code) {
  while (1); /* endless loop */
}

使用 MicroLib (micro-library)

如果使用了keil uvsion开发环境, 可以用microlib简化这一过程. MicroLib是一个定制(精简)的stdio替代库, 提供无缓冲的stdin, stdout 和 stderr,当使用微库时,就默认关闭了半主机模式, 不需要#pragma注释. 使用MicroLib之后, 只需要修改fputc()使其重定向

1. 在KEIL-MDK中开启 Use MicroLIB 选项

打开配置面板, 定位到Target标签页, 勾选Use MicroLIB.

2. 将 fputc 方法的输出重定向

在 MicroLib 的 stdio.h 头文件中, fputc() 方法的prototype为

int fputc(int ch, FILE* stream)

这个方法原本是将ch输出到strem这个文件类型指针指向的文件, 现在将其替换为串口1

#include <stdio.h>
int fputc(int ch, FILE* stream)
{
    USART_SendChar(USART1, (uint8_t)ch);
    return ch;
}

3. 重写fgetc方法

同样的

/*
** Rewrite fgetc function and make scanf function work
**/
int fgetc(FILE* file)
{
    while((USART1->ISR & UART_IT_RXNE) == RESET);
    return USART1->RDR;
}

注意要include stdio.h, 否则会报FILE类型未定义.

ARM GCC 环境 gcc-arm-none-eabi

重写 __io_putchar

在对printf重定向之前,不能调用printf

在main函数之前加上如下代码对串口进行重定向,当然,串口一定要初始化之后再用printf,否则程序虽然不会飞,但是printf也不会有结果

下面代码公共使用的宏判断, 注意引用头文件#include "stdio.h"

#ifdef __GNUC__
  /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
     set to 'Yes') calls __io_putchar() */
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

下面的三种实现根据使用的库选择即可

SPL库实现

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the USART */
  USART_SendData(USART1, (uint8_t) ch);

  /* Loop until the end of transmission */
  while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET)
  {}

  return ch;
}

HAL库实现

/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

寄存器实现

PUTCHAR_PROTOTYPE
{
    //具体哪个串口可以更改USART1为其它串口
    while ((USART1->SR & 0X40) == 0); //循环发送,直到发送完毕
    USART1->DR = (uint8_t) ch;
    return ch;
}

重写 _write

SPL库实现

void USART_Put(uint8_t ch)
{
    #if (DEBUG == DEBUG_UART1)
    USART_SendData(USART1, (uint8_t)ch);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
    #elif (DEBUG == DEBUG_UART2)
    USART_SendData(USART2, (uint8_t)ch);
    while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
    #elif (DEBUG == DEBUG_UART3)
    USART_SendData(USART3, (uint8_t)ch);
    while (USART_GetFlagStatus(USART3, USART_FLAG_TC) == RESET);
    #endif
}

#if defined(__GNUC__)
  int _write(int file, char *ptr, int len) {
    int n = len;
    switch (file) {
      case STDOUT_FILENO: /* stdout */
      case STDERR_FILENO: /* stderr */
          while (len--) {
              USART_Put(*ptr++ & (uint16_t)0x01FF);
          }
          break;
      default:
          errno = EBADF;
          return -1;
    }
    return n;
  }
#elif defined (__ICCARM__)
  #include "LowLevelIOInterface.h"
  size_t __write(int handle, const unsigned char * buffer, size_t size)
  {
    int len = size;
    while (len--) {
      USART_Put(*buffer++ & (uint16_t)0x01FF);
    }
    return size;
  }
#elif defined (__CC_ARM)
  int fputc(int ch, FILE *f)
  {
    rturn USART_Put(ch);
  }
#endif

HAL库实现

#if defined(__GNUC__)
int _write(int fd, char * ptr, int len)
{
  HAL_UART_Transmit(&huart1, (uint8_t *) ptr, len, HAL_MAX_DELAY);
  return len;
}
#elif defined (__ICCARM__)
#include "LowLevelIOInterface.h"
size_t __write(int handle, const unsigned char * buffer, size_t size)
{
  HAL_UART_Transmit(&huart1, (uint8_t *) buffer, size, HAL_MAX_DELAY);
  return size;
}
#elif defined (__CC_ARM)
int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
#endif

在代码中启用UART

根据可用的pin脚, USART1可以使用PA9, PA10组合, 或者PB6, PB7组合.

stm32f103

void UARTmain_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  USART_InitTypeDef USART_InitStructure;

  // 打开GPIO和USART时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
  
  // 将USART1 Tx@PA9的GPIO配置为推挽复用模式
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  // 将USART1 Rx@PA10的GPIO配置为浮空输入模式
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
  GPIO_Init(GPIOA, &GPIO_InitStructure);

  /* 配置USART1参数
      波特率   = 115200
      数据长度 = 8
      停止位   = 1
      校验位   = No
      禁止硬件流控(即禁止RTS和CTS)
      使能接收和发送
  */
  USART_InitStructure.USART_BaudRate = 115200;
  USART_InitStructure.USART_WordLength = USART_WordLength_8b;
  USART_InitStructure.USART_StopBits = USART_StopBits_1;
  USART_InitStructure.USART_Parity = USART_Parity_No;
  USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
  USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
  USART_Init(USART1, &USART_InitStructure);

  // 使能 USART1
  USART_Cmd(USART1, ENABLE);
}

stm32f401

void UARTmain_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct;
  USART_InitTypeDef USART_InitStruct;
  
  /**
   * Enable clock for GPIOB
   * Enable clock for USART1 peripheral
   */
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

  /**
   * 串口1对应引脚复用映射
   * STM32F4xx USART1 为PA9/PB6对应USART1的TX, PA10/PB7对应USART1的RX
   * Tell pins PB6 and PB7 which alternating function you will use
   * @important Make sure, these lines are before pins configuration!
   */
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_USART1);
  GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_USART1);
  // Initialize pins as alternating function
  GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
  GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF;
  GPIO_InitStruct.GPIO_OType=GPIO_OType_PP;
  GPIO_InitStruct.GPIO_PuPd=GPIO_PuPd_UP;
  GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStruct);

  /**
   * Set Baudrate to value you pass to function
   * Disable Hardware Flow control
   * Set Mode To TX and RX, so USART will work in full-duplex mode
   * Disable parity bit
   * Set 1 stop bit
   * Set Data bits to 8
   *
   * Initialize USART2
   * Activate USART2
   */  
  USART_InitStruct.USART_BaudRate=115200;
  USART_InitStruct.USART_WordLength=USART_WordLength_8b;
  USART_InitStruct.USART_StopBits=USART_StopBits_1;
  USART_InitStruct.USART_Parity=USART_Parity_No;
  USART_InitStruct.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
  USART_InitStruct.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;
  USART_Init(USART1, &USART_InitStruct);
  USART_Cmd(USART1, ENABLE);
}

参考