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范围的问题:
网上所说的修改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灯的亮灭来展示):
c
#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;
}
c
#ifndef __KEY_H
#define __KEY_H
#include "main.h"
void DealKey(void);
#endif
2、开关机业务
这里暂时通过LED灯亮灭来模拟,后续可增加底板电路的控制、蜂鸣器的控制等,基本都是通过控制IO口高低电平方式来控制的:
c
#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);
}
}
c
#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字符通过串口写入:
c
#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,大循环中调用按钮监听处理:
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日志库:
c
#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
}
c
#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即可添加,之后按如下方式接线:
串口TX、RX和之前接法一样,还需要配置其波特率,也和之前一样,虚拟终端这里有一些区别,上节我们是TX、RX是和串口对应一致的,但是这里我们要显示串口发送的内容,所以RX接入串口的TX,不要搞错了,而且由于是显示日志的打印串口,我们配置的只发送,所以理论上RX是可以不接的。至于其它的接线基本没有什么注意的,和之前的大体一致,主要注意控制开关的高低电平即可,一端接电源,另一端设置低电平即导通,电流流过。
七、仿真测试结果
按下按键根据key.c中的处理逻辑就会来回判断进行开关机业务处理了,这里模拟开关机通过LED灯的亮灭来展示,串口的TX发送接入虚拟终端RX将发送的日志直接显示出来了,串口工具在Windows上打开对端的串口也是可以看到对应的信息的(快去试一下吧):
八、最后
下一节我们来试下ADC和蜂鸣器的使用吧,蜂鸣器这种简单的音频提示元器件也是用的比较广泛的,比较常见的像小区的门禁基本都会加蜂鸣器提示刷卡是否成功等,ADC接口则常常用来读取一些传感器的信息或者电压信息等,也是比较常用的。最近的一些总结基本都是用为主,对于这些IO口的更深层次的配置及控制原理我们暂时不做深入分析,只需要了解怎么用即可,这些东西感觉总结C51的时候来分析总结更合适一些,C51的应用相对简单一些,STM32的应用开发已经比较接近Linux应用开发这种层级,很多时候不用太考虑底层的实现,只需要会用接口开发比较复杂的应用即可(所以这种应用开发实时操作系统的学习就比较重要了),等到解决一些疑难杂症时,再对其做深入了解和理解,新手直接深入容易被劝退。