stm32学习总结:5、Proteus8+STM32CubeMX+MDK仿真串口并使用串口打印日志(注意重定向printf到串口打印的问题)
文章目录
- stm32学习总结:5、Proteus8+STM32CubeMX+MDK仿真串口并使用串口打印日志(注意重定向printf到串口打印的问题)
- 一、前言
- 二、资料收集
- 三、注意事项
- 四、STM32CubeMX配置
- 五、MDK工程相关代码
- 1、非中断方式的按键处理
- 2、开关机业务
- 3、printf重定向到串口打印
- 4、日志打印封装
- 六、Proteus项目配置
- 七、仿真测试结果
- 八、最后
一、前言
上一节模拟实现了串口收发打印,一般我们裸机打印日志通过串口或者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范围的问题:
网上所说的修改xxx.ld配置文件这些方法很多时候是无效的,不能盲目去修改flash配置。
可以使用比如RTT打印等方式来打印日志,也可以换一些资源比较丰富的板子,也许官方可以出一些裁剪过的利用arm-none-eabi gcc编译的标准库(后面有机会的话我会来尝试一下,用stm32F10x的话arm gcc基本上没办法用printf,引入标准库加上一两个简单的外设接口就肯定会flash超标,用mdk原有的编译器就不会有这个问题)。
四、STM32CubeMX配置
这次彻底精简一下相关配置:
- 1、一个按钮BUTTON,PA1配置GPIO OUTPUT用来接入按钮,使用默认配置即可,默认低电平,未拉高拉低,添加用户标签BUTTON:
- 2、五个LED,PA4-PA8配置GPIO OUTPUT用来接入LED,使用默认配置即可,默认低电平,未拉高拉低,添加用户标签LED_1到LED_5:
- 3、开启USART1,PA9\PA10来作为打印的串口,配置只发送,波特率设置为9600,不需要配置全局中断,我们使用该串口作为打印串口,只需要发送即可,所以不需要配置中断方式来接收:
然后生成代码即可。
五、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 printfset 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 printfset 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_LOGva_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_DEBUGtypedef 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即可添加,之后按如下方式接线:
串口TX、RX和之前接法一样,还需要配置其波特率,也和之前一样,虚拟终端这里有一些区别,上节我们是TX、RX是和串口对应一致的,但是这里我们要显示串口发送的内容,所以RX接入串口的TX,不要搞错了,而且由于是显示日志的打印串口,我们配置的只发送,所以理论上RX是可以不接的。至于其它的接线基本没有什么注意的,和之前的大体一致,主要注意控制开关的高低电平即可,一端接电源,另一端设置低电平即导通,电流流过。
七、仿真测试结果
按下按键根据key.c中的处理逻辑就会来回判断进行开关机业务处理了,这里模拟开关机通过LED灯的亮灭来展示,串口的TX发送接入虚拟终端RX将发送的日志直接显示出来了,串口工具在Windows上打开对端的串口也是可以看到对应的信息的(快去试一下吧):
八、最后
下一节我们来试下ADC和蜂鸣器的使用吧,蜂鸣器这种简单的音频提示元器件也是用的比较广泛的,比较常见的像小区的门禁基本都会加蜂鸣器提示刷卡是否成功等,ADC接口则常常用来读取一些传感器的信息或者电压信息等,也是比较常用的。最近的一些总结基本都是用为主,对于这些IO口的更深层次的配置及控制原理我们暂时不做深入分析,只需要了解怎么用即可,这些东西感觉总结C51的时候来分析总结更合适一些,C51的应用相对简单一些,STM32的应用开发已经比较接近Linux应用开发这种层级,很多时候不用太考虑底层的实现,只需要会用接口开发比较复杂的应用即可(所以这种应用开发实时操作系统的学习就比较重要了),等到解决一些疑难杂症时,再对其做深入了解和理解,新手直接深入容易被劝退。