一例离奇的STM32Cube串口数据乱码解决过程分析

使用的开发环境是STM32CubeMX+VSCode-PlatformIO,用CubeMX初始化外设,然后导入到PlatformIO中开发实际应用。


引脚配置图

中断配置图

时钟配置图

结果出师不利,前后折腾将近一整天,始终输出乱码。


乱码

尝试的方法如下:

  1. 降低波特率
    低波特率一般不容易出错,容错率较高。


    image.png

    依旧是乱码。

  2. 降低系统频率
    过高的CPU主频会降低系统稳定性,是不是CPU跑飞了?加上LED指示灯代码,显示依然是正常的。继续修改系统频率(原本设置为满频率168Mhz,后降为84Mhz)


    乱码
  3. 尝试避免使用中断方式发送
    用HAL_UART_Transmit()函数发送,依然乱码。

  4. 尝试换用外接TTL转换器
    去掉PA9、PA10到USB的跳线帽,连接外部USB-TTL,


    image.png

结果:


乱码
  1. 怀疑是不是片内UART部分硬件烧坏了?
    用示波器检测PA9脚上波形,测到的结果:
    image.png

    似乎看不出什么所以然来,后继续查阅前人提问资料,翻到这样一条:
    串口通信,显示乱码!-CSDN论坛
image.png

乱码就一种可能,波特率不对。如果有示波器可以用示波器看一下。收一个最小的脉冲,他的脉宽就是实际的波特率的倒数。

让我们记住这句话。


image.png

放大看一个最小波形宽度,得到的结果是 27.1us. 按照wackestar老哥的计算方法,对应的波特率就是1/27.1*1e6=36900,似乎并不怎么对劲,我不是设置的115200🐴?

写进串口监视器配置里看看输出:


image.png
image.png

问题解决。

...

原因呢?
算一下115200和36900之间的倍率,刚好3.12倍。。。好像没啥规律呀。


image.png

测试一下附近的标准波特率,38400,同样可以准确解码。而115200和38400之间刚好差了3倍。然而还是没搞懂哪里用错了。翻看一下使用手册:


image.png

也并没有弄明白为什么。
image.png

查看HAL认为的系统频率,发现是这个数:262.50Mhz。而设定值为84Mhz,将这个数字乘3,为252Mhz。这个值是比较接近输出值的。会不会是因为外部时钟设置没有生效呢?
image.png

查看stm32f4xx_hal_conf.h文件,发现是有设置时钟频率的。但是没有导入?


image.png

image.png

这是HAL库的默认配置文件,按道理来说应该是采用项目中的配置,但不清楚什么原因始终导入的是库文件的配置。
image.png

修改为8Mhz
image.png

修改后的输出:
image.png

这次又出现了乱码,不过不用慌,因为这时的波特率已经发生变化,修改为115200:
image.png

完美解决!

分析原因

由于stm32f4xx_hal_conf.h文件错误导入,导致STM32Cube库错误的计算了系统时钟震荡频率,进而算出了错误的串口时钟,从而在PC端上使用预定的波特率无法正常接收消息,表现为数据乱码。
修改系统库内配置文件为正确的晶振频率后,输出一切正常。

使用printf函数进行串口输出?

顺便提一下(也是本次探索的副产品)如何使用print函数来实现串口输出吧,毕竟printf函数在调试当中非常方便。
由于PlatformIO IDE使用的编译器比较特别,是用的arm-eabi-none标准嵌入式设备GCC编译器,所以和STM32CubeIDE以及Keil MDK编译器特性都不相同,所以无论网上多见的fputc()函数,或者__io_putchar()乃至_write()函数都无法实现print函数的串口重定向。顺便放出来供参考(亲测对PlatformIO附带的编译器无效):

#include <stdio.h>
__IO ITStatus USART1Ready = RESET;

/* USER CODE BEGIN PV */
#ifdef __GNUC__
    #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
    #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit_IT(&huart1 , (uint8_t *)&ch, 1);
    while (USART1Ready != SET)
  {
  }
    USART1Ready = RESET;
    return ch;
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart){
  if(huart->Instance==USART1)
  {
    USART1Ready = SET;
    // HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
  }
}
/* USER CODE END PV */
int _write(int fd, char *ptr, int len)  
{
  UNUSED(fd);
  HAL_UART_Transmit_IT(&huart1 , (uint8_t *)ptr, len);
  while (USART1Ready != SET) {
  }
  USART1Ready = RESET;
  HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_10);
  return 0;
}

这里定义USART1Ready 变量起到防止同步读写出现乱码的作用。
而且printf代码是包含在lib库中的,所以无法看到其具体实现,因此也很难追溯其调用中间步骤。
所以才有了这里的曲线救国方案:

#include <stdio.h>
#include <stdarg.h>
#include <string.h>
void print(const char *fmt, ...);

void vprint(const char *fmt, va_list argp)
{
    char string[200];
    if(0 < vsprintf(string, fmt, argp)) // build string
    {
        HAL_UART_Transmit(&huart1, (uint8_t*)string, strlen(string), 0xffffff); // send message via UART
    }
}

void print(const char *fmt, ...) // custom printf() function
{
    va_list argp;
    va_start(argp, fmt);
    vprint(fmt, argp);
    va_end(argp);
}

测试代码:

while (1)
  {
    /* USER CODE END WHILE */
    HAL_Delay(500);
    HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_9);
    HAL_Delay(250);
    // uint8_t str[] = {32, 33, 34, 35, 36};
    uint8_t str[] = "Hello, 嘤嘤嘤。\n";
    // HAL_UART_Transmit_IT(&huart1, str, sizeof(str));
    print("Hello\n");
    print((char*)str);
    
    print("double: %lf\n", a=a+0.01);
    print("SysClock Freq: %.2f Mhz\n", (float)HAL_RCC_GetSysClockFreq()/(float)1e6);
    print("HCLK1 Freq: %.2f Mhz\n", (float)HAL_RCC_GetHCLKFreq()/(float)1e6);
    print("PCLK1 Freq: %.2f Mhz\n", (float)HAL_RCC_GetPCLK1Freq()/(float)1e6);
    print("PCLK2 Freq: %.2f Mhz\n", (float)HAL_RCC_GetPCLK2Freq()/(float)1e6);

    uint32_t pllm, pllvco, pllp, sysclockfreq;
    pllm = RCC->PLLCFGR & RCC_PLLCFGR_PLLM;
    if(__HAL_RCC_GET_PLL_OSCSOURCE() != RCC_PLLSOURCE_HSI)
    {
      /* HSE used as PLL clock source */
      pllvco = (uint32_t) ((((uint64_t) HSE_VALUE * ((uint64_t) ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> RCC_PLLCFGR_PLLN_Pos)))) / (uint64_t)pllm);
    }
    else
    {
      /* HSI used as PLL clock source */
      pllvco = (uint32_t) ((((uint64_t) HSI_VALUE * ((uint64_t) ((RCC->PLLCFGR & RCC_PLLCFGR_PLLN) >> RCC_PLLCFGR_PLLN_Pos)))) / (uint64_t)pllm);
    }
    pllp = ((((RCC->PLLCFGR & RCC_PLLCFGR_PLLP) >> RCC_PLLCFGR_PLLP_Pos) + 1U) *2U);

    sysclockfreq = pllvco/pllp;
    print("HSE_VALUE, pllm, pllvco, pllp, sysclockfreq = %u \t %u \t %u \t %u \t %u\n", HSI_VALUE, pllm, pllvco, pllp, sysclockfreq);
    print("sysclockfreq: %.2f Mhz\n", (float)sysclockfreq/(float)1e6);

    /* USER CODE BEGIN 3 */
  }

如何print输出浮点数float以及double?

这里又有一个坑...因为正常情况你的输出格式应该长这样:


image.png

也就是无法对浮点数进行格式输出,这是由于printf库内容很大,所以嵌入式版本默认进行了缩减,所以直接使用是无法进行浮点数格式化的。
解决办法是引入一个浮点数printf库,而这个库在编译器中是自带的,只是需要选择性加载进来:

build_flags = 
    -Wl,-u_printf_float

platformio.ini文件中添加上面的代码,就会看到前面的输出结果了。实测可以很好的进行浮点数格式处理,包括截断处理、符号处理、补全处理等等,同时也支持双精度小数的输出,到目前为止基本使用起来得心应手了。

小彩蛋

测试得到的36900的波特率与设置的波特率115200刚好差3.12倍。
而测试得到的系统频率262.50Mhz与设定的频率84.00Mhz相差也是3.125倍。
晶振设置频率25000000 Hz与实际的8000000 Hz相差也是 25/8=3.125倍。

如果对你有所帮助,欢迎点赞。❤

推荐阅读更多精彩内容