stm32学习总结:5、Proteus8+STM32CubeMX+MDK仿真串口并使用串口打印日志(注意重定向printf到串口打印的问题)

发布时间:2024年01月03日

stm32学习总结:5、Proteus8+STM32CubeMX+MDK仿真串口并使用串口打印日志(注意重定向printf到串口打印的问题)

一、前言

上一节模拟实现了串口收发打印,一般我们裸机打印日志通过串口或者JLINK工具等带的RTT打印,对于仿真,我们选择使用串口打印再合适不过了,这里总结一下重定向printf到串口打印日志的过程;期间,尝试了CLion+arm gcc的方式,发现stm32f10x的flash还是支撑不起来未裁剪的标准库,只要使用stdio相关标准库编译时就很容易flash超标。

二、资料收集

https://blog.csdn.net/m0_54490453/article/details/128921674
https://www.cnblogs.com/pianist/p/3315801.html
https://blog.51cto.com/u_13682052/5670642
STM32串口使用printf打印日志:
https://community.st.com/t5/stm32-mcus-products/how-to-get-printf-example-working-in-another-project/m-p/392014
https://community.st.com/t5/stm32-mcus-products/stm32g0-redirect-printf-to-write-and-use-uart-how-to/m-p/66318

三、注意事项

如果是使用arm gcc编译器的,尽量不要使用printf,这会引入标准库,而对应库不像mdk的microlib做了裁剪,它是比较占用flash的,而stm3210x的flash最多只有32KB,很容易在编译时出现section .rodata’ will not fit in region FLASH'也就是超出flash范围的问题:
image.png
网上所说的修改xxx.ld配置文件这些方法很多时候是无效的,不能盲目去修改flash配置。
可以使用比如RTT打印等方式来打印日志,也可以换一些资源比较丰富的板子,也许官方可以出一些裁剪过的利用arm-none-eabi gcc编译的标准库(后面有机会的话我会来尝试一下,用stm32F10x的话arm gcc基本上没办法用printf,引入标准库加上一两个简单的外设接口就肯定会flash超标,用mdk原有的编译器就不会有这个问题)。

四、STM32CubeMX配置

这次彻底精简一下相关配置:

  • 1、一个按钮BUTTON,PA1配置GPIO OUTPUT用来接入按钮,使用默认配置即可,默认低电平,未拉高拉低,添加用户标签BUTTON:

image.png

  • 2、五个LED,PA4-PA8配置GPIO OUTPUT用来接入LED,使用默认配置即可,默认低电平,未拉高拉低,添加用户标签LED_1到LED_5:

image.png

  • 3、开启USART1,PA9\PA10来作为打印的串口,配置只发送,波特率设置为9600,不需要配置全局中断,我们使用该串口作为打印串口,只需要发送即可,所以不需要配置中断方式来接收:

image.png
然后生成代码即可。

五、MDK工程相关代码

1、非中断方式的按键处理

通过读取IO口的电平判断是否按下按钮,之后通过全局变量确认按下松开以及长短按,这种方式在理解上比较直观(按键这里的处理逻辑是判断LED灯1的电平变化来确定是否开关机,开关机的逻辑我们通过控制LED灯的亮灭来展示):

#include "gpio.h"
#include "key.h"
#include "pwr.h"
//#include "log.h"

// 按键的键值
#define KEY_Press 1

// 读取IO口的电平
#define KEY_PWR HAL_GPIO_ReadPin(GPIOA, BUTTON_Pin)

uint8_t key_old, count;

uint8_t ScanKey(void)
{
    if (GPIO_PIN_RESET == KEY_PWR) {
        HAL_Delay(40);//延时10-20ms,防抖

        if (GPIO_PIN_SET == KEY_PWR) {
            count++;
            return KEY_Press;
        }
    } else {
        HAL_Delay(40);
    }

    return 0;
}

void DealKey(void)
{
    uint8_t key_value = 0;
    //获取键值
    key_value = ScanKey();

    if (key_value != key_old) {
        //与上一次的键值比较 如果不相等,表明有键值的变化,开始计时
        key_old = key_value;
        count = 0;
    } else {
        //如果没有键值的改变 说明没有新按键按下或松开
        key_value = 0;
    }

    if (key_value)// 短按处理
    {
        switch(key_value) {
            case 1 : {
                //LOG(LOG_DEBUG, "KEY1 switch");
                if (GPIO_PIN_SET == HAL_GPIO_ReadPin(GPIOA, LED_1_Pin)) {
                    //LOG(LOG_DEBUG, "pwr on");
                    PWROn();
                } else {
                    //LOG(LOG_DEBUG, "pwr off");
                    PWROff();
                    key_old = key_value;
                }
            }
                break;
            case 2 : {
//                LOG(DEBUG, "KEY2 switch");
                }
                break;
        }

        key_value = 0;
    }
    
    return;
}

#ifndef __KEY_H
#define __KEY_H

#include "main.h"

void DealKey(void);

#endif

2、开关机业务

这里暂时通过LED灯亮灭来模拟,后续可增加底板电路的控制、蜂鸣器的控制等,基本都是通过控制IO口高低电平方式来控制的:

#include "pwr.h"
#include "log.h"
#include "gpio.h"

void TurnOnLED(int flag)
{
    switch(flag)
    {
        case 1:
            HAL_GPIO_WritePin(GPIOA, LED_1_Pin, GPIO_PIN_RESET);
            break;
        case 2:
            HAL_GPIO_WritePin(GPIOA, LED_2_Pin, GPIO_PIN_RESET);
            break;
        case 3:
            HAL_GPIO_WritePin(GPIOA, LED_3_Pin, GPIO_PIN_RESET);
            break;
        case 4:
            HAL_GPIO_WritePin(GPIOA, LED_4_Pin, GPIO_PIN_RESET);
            break;
        case 5:
            HAL_GPIO_WritePin(GPIOA, LED_5_Pin, GPIO_PIN_RESET);
            break;
    }
}

void TurnOffLED(int flag)
{
    switch(flag)
    {
        case 1 :
            HAL_GPIO_WritePin(GPIOA, LED_1_Pin, GPIO_PIN_SET);
            break;
        case 2 :
            HAL_GPIO_WritePin(GPIOA, LED_2_Pin, GPIO_PIN_SET);
            break;
        case 3 :
            HAL_GPIO_WritePin(GPIOA, LED_3_Pin, GPIO_PIN_SET);
            break;
        case 4 :
            HAL_GPIO_WritePin(GPIOA, LED_4_Pin, GPIO_PIN_SET);
            break;
        case 5 :
            HAL_GPIO_WritePin(GPIOA, LED_5_Pin, GPIO_PIN_SET);
            break;
    }
}

void PWROn(void)
{
    LOG(LOG_DEBUG, "PWROn LED blink...");
    for (int i = 1; i< 6; i++) {
        HAL_Delay(100);
        TurnOnLED(i);
    }
}

void PWROff(void)
{
    LOG(LOG_DEBUG, "PWROn LED off...");
    for (int i = 1; i< 6; i++) {
        HAL_Delay(100);
        TurnOffLED(i);
    }
}
#ifndef __PWR_H
#define __PWR_H

#include "main.h"

void PWROn(void);

void PWROff(void);

#endif

3、printf重定向到串口打印

通过stm32的官方论坛和一些参考文章发现是通过对printf进行重写来将发送到终端输出设备的内容通过串口发送出来,不同的编译器链接的库的printf底层调用方式可能有差异,需要注意一下。目前官方论坛上给到的方式是通过宏PUTCHAR_PROTOTYPE控制,我尝试去寻找对应printf的源码,mdk库的printf源码没有找到,据网上说其裁剪的标准库实现的printf是用fputc来将字符发送出去的,而gun c标准库的printf最终查看源码发现是通过调用write函数写入到显示设备的,而该函数为系统调用,系统通过驱动调用硬件IO口去控制显示设备将写入的内容显示,对于没有系统的stm32裸机来说其封装了一个__io_putchar的类似系统调用(其文件名字就叫syscalls.c)接口来让我们重写输出方式,这样我们重写PUTCHAR_PROTOTYPE即可,目前在main.c中添加对应宏控制并重写将写入的ch字符通过串口写入:

#ifdef __GNUC__

/* With GCC, 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__ */

PUTCHAR_PROTOTYPE

{

    /* Place your implementation of fputc here */

    /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */

    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);

    return ch;

}

整体main.c,大循环中调用按钮监听处理:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "key.h"
#include "stdio.h"

/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#ifdef __GNUC__

/* With GCC, 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__ */

PUTCHAR_PROTOTYPE
{
    /* Place your implementation of fputc here */

    /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */

    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);

    return ch;
}
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    DealKey();
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

4、日志打印封装

然后我们将printf函数稍微封装一下,也可以使用easylogger日志库:

#include "log.h"
#include "usart.h"

char* get_log_level_str(const int level)
{
	if (level == LOG_DEBUG) {
		return "DEBUG";
	}
	else if (level == LOG_INFO) {
		return "INFO";
	}
	else if (level == LOG_WARN) {
		return "WARN";
	}
	return "UNLNOW";
}

void my_log(const int level,const char* fun, const int line ,const char* fmt, ...)
{
#ifdef OPEN_LOG
	va_list arg;
	va_start(arg, fmt);
	char buf[50] = { 0 };
	vsnprintf(buf, sizeof(buf), fmt, arg);
	va_end(arg);
	if (level >= LOG_LEVEL)
		printf("[%-5s] [%-20s%4d] %s \r\n", get_log_level_str(level), fun, line, buf);
#endif
}

#ifndef __LOG_H_
#define __LOG_H_

#include <stdarg.h>
#include <stdio.h>
#define OPEN_LOG 1
#define LOG_LEVEL LOG_DEBUG

typedef enum
{
	LOG_DEBUG = 0,
	LOG_INFO,
	LOG_WARN
}E_LOGLEVEL;

void my_log(const int level, const char* fun, const int line, const char* fmt, ...);
#define LOG(level,fmt,...) my_log(level,__FUNCTION__,__LINE__,fmt, ##__VA_ARGS__)

#endif

这样我们调用LOG()进行日志打印就可以了,和常规软件开发的日志打印接口基本就一致了。

六、Proteus项目配置

Proteus创建串口和虚拟终端、LED灯、电阻灯我们之前已经总结过了,不再重复,这里再添加一个按键即可,按下P,搜索BUTTON即可添加,之后按如下方式接线:
image.png
串口TX、RX和之前接法一样,还需要配置其波特率,也和之前一样,虚拟终端这里有一些区别,上节我们是TX、RX是和串口对应一致的,但是这里我们要显示串口发送的内容,所以RX接入串口的TX,不要搞错了,而且由于是显示日志的打印串口,我们配置的只发送,所以理论上RX是可以不接的。至于其它的接线基本没有什么注意的,和之前的大体一致,主要注意控制开关的高低电平即可,一端接电源,另一端设置低电平即导通,电流流过。

七、仿真测试结果

按下按键根据key.c中的处理逻辑就会来回判断进行开关机业务处理了,这里模拟开关机通过LED灯的亮灭来展示,串口的TX发送接入虚拟终端RX将发送的日志直接显示出来了,串口工具在Windows上打开对端的串口也是可以看到对应的信息的(快去试一下吧):
GIF.gif

八、最后

下一节我们来试下ADC和蜂鸣器的使用吧,蜂鸣器这种简单的音频提示元器件也是用的比较广泛的,比较常见的像小区的门禁基本都会加蜂鸣器提示刷卡是否成功等,ADC接口则常常用来读取一些传感器的信息或者电压信息等,也是比较常用的。最近的一些总结基本都是用为主,对于这些IO口的更深层次的配置及控制原理我们暂时不做深入分析,只需要了解怎么用即可,这些东西感觉总结C51的时候来分析总结更合适一些,C51的应用相对简单一些,STM32的应用开发已经比较接近Linux应用开发这种层级,很多时候不用太考虑底层的实现,只需要会用接口开发比较复杂的应用即可(所以这种应用开发实时操作系统的学习就比较重要了),等到解决一些疑难杂症时,再对其做深入了解和理解,新手直接深入容易被劝退。

文章来源:https://blog.csdn.net/weixin_39510813/article/details/135371253
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。