如何在STM32上使用printf函数?


19

我试图弄清楚如何使用printf函数打印到串行端口。

我当前的设置是STM32CubeMX生成的代码和带有STM32F407发现板的 SystemWorkbench32 。

我在stdio.h中看到printf原型定义为:

int _EXFUN(printf, (const char *__restrict, ...)
               _ATTRIBUTE ((__format__ (__printf__, 1, 2))));

这是什么意思?该函数定义的确切位置在哪里?找出如何使用这种功能输出的一般要点是什么?


5
我认为您需要编写自己的“ int _write(int文件,char * ptr,int len)”,以便将标准输出发送到此处的串行端口。我相信通常是在名为Syscalls.c的文件中完成的,该文件处理“系统调用重新映射”。尝试使用Google搜索“ Syscalls.c”。
Tut 2015年

1
这不是电子问题。请参阅您的编译器库的手册。
奥林·拉斯罗普

您是要通过半主机(通过调试器)还是仅通过printf进行printf调试?
丹尼尔(Daniel)

就像@Tut所说的,_write()功能就是我所做的。详情请见下文。
cp.engr

这是一个很棒的教程:youtu.be/Oc58Ikj-lNI
Joseph Pighetti

Answers:


19

我从此页面上得到的第一种方法适用于STM32F072。

http://www.openstm32.org/forumthread1055

如此处所述,

我使用printf(以及所有其他面向控制台的stdio函数)的方式是通过创建低级I / O函数(如_read()和)的自定义实现_write()

GCC C库调用以下函数来执行低级I / O:

int _read(int file, char *data, int len)
int _write(int file, char *data, int len)
int _close(int file)
int _lseek(int file, int ptr, int dir)
int _fstat(int file, struct stat *st)
int _isatty(int file)

这些功能在GCC C库中作为带有“弱”链接的存根例程实现。如果上述任何函数的声明出现在您自己的代码中,则您的替代例程将覆盖库中的声明,并代替默认(非功能)例程使用。

我使用STM32CubeMX将USART1(huart1)设置为串行端口。因为我只想要printf(),所以只需要填充_write()函数,就可以按照以下步骤进行操作。通常包含在中syscalls.c

#include  <errno.h>
#include  <sys/unistd.h> // STDOUT_FILENO, STDERR_FILENO

int _write(int file, char *data, int len)
{
   if ((file != STDOUT_FILENO) && (file != STDERR_FILENO))
   {
      errno = EBADF;
      return -1;
   }

   // arbitrary timeout 1000
   HAL_StatusTypeDef status =
      HAL_UART_Transmit(&huart1, (uint8_t*)data, len, 1000);

   // return # of bytes written - as best we can tell
   return (status == HAL_OK ? len : 0);
}

如果我使用Makefile而不是IDE怎么办?在我的Makefile项目中无法正常工作...有人可以帮忙吗?
Tedi

@Tedi,您在使用GCC吗?这是特定于编译器的答案。您尝试了什么,结果如何?
cp.engr

是的,我使用GCC。我发现了问题。调用_write()函数。问题出在我发送字符串的代码中。我滥用了Segger的RTT库,但现在工作正常。谢谢。现在都在工作。
Tedi

4

_EXFUN是一个宏,可能包含一些有趣的指令,这些指令告诉编译器应检查格式字符串与printf兼容,并确保printf的参数与格式字符串匹配。

要学习如何使用printf,我建议使用手册页并进行更多的谷歌搜索。编写一些使用printf的简单C程序,然后在尝试过渡到在Micro上使用它之前在计算机上运行它们。

有趣的问题是“打印的文本在哪里?”。在类似Unix的系统上,它会“标准输出”,但微控制器则没有这种东西。CMSIS调试库可以将printf文本发送到arm半主机调试端口,即发送到您的gdb或openocd会话中,但是我不知道SystemWorkbench32会做什么。

如果您不在调试器中工作,则使用sprintf格式化要打印的字符串,然后通过串行端口或您可能附加的任何显示器发送这些字符串可能会更有意义。

当心:printf及其相关代码非常大。在32F407上,这可能无关紧要,但这在闪存很少的设备上确实是个问题。


IIRC _EXFUN标记要导出的功能,即要在用户代码中使用的功能,并定义调用约定。在大多数(嵌入式)平台上,可以使用默认的调用约定。但是在带有动态库的x86上,可能需要定义函数__cdecl以避免错误。至少在newlib / cygwin上,_EXFUN仅用于设置__cdecl
erebos 2015年

4

@AdamHaun的答案就是您所需要的,sprintf()创建一个字符串然后发送它很容易。但是,如果您确实想要printf()自己的函数,则可以使用可变参数函数(va_list)

具有va_list自定义打印功能的外观如下所示:

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

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 my_printf(const char *fmt, ...) // custom printf() function
{
    va_list argp;
    va_start(argp, fmt);
    vprint(fmt, argp);
    va_end(argp);
}

用法示例:

uint16_t year = 2015;
uint8_t month = 12;
uint8_t day   = 18;
char* date = "date";

// "Today's date: 2015-12-18"
my_printf("Today's %s: %d-%d-%d\r\n", date, year, month, day);

请注意,虽然此解决方案为您提供了方便的使用功能,但它比发送原始数据或使用even更慢sprintf()。对于高数据速率,我认为这还不够。


另一个选择,也许是更好的选择,是与ST-Link Utility一起使用ST-Link,SWD调试器。并通过SWO viewer使用Printf,这是ST-Link Utility的手册,相关部分从第31页开始。

通过SWO Viewer的Printf显示从目标通过SWO发送的printf数据。它允许在正在运行的固件上显示一些有用的信息。


您不使用va_arg,如何逐步查看变量参数?
iouzzr

1

(通常)printf()是C标准库的一部分。如果您的库版本随附源代码,则可能会在其中找到实现。

使用sprintf()生成字符串,然后使用另一个函数通过串行端口发送字符串可能会更容易。这样,所有困难的格式化工作都将为您完成,而您不必破解标准库。


2
是的,printf()通常是库的一部分,是的,但是直到有人设置了输出到所需硬件的基本功能之后,它才能任何事情。那不是“黑客”这个库,而是按计划利用它。
克里斯·斯特拉顿

正如Bence和William在他们的回答中所建议的,它可能被预先配置为通过调试器连接发送文本。(当然,这不是提问者想要的。)
Adam Haun

通常,这仅是由于您选择链接以支持您的主板的代码而发生的,但是无论您通过定义自己的输出函数来进行更改。你的建议的问题是,它不是基于理解的图书馆是如何意味着使用-而不是做你提出了一个多步骤的过程对每个输出中,其中除其他外将使任何现有的便携式乱七八糟以正常方式执行操作的代码。
克里斯·斯特拉顿

STM32是一个独立的环境。编译器不需要提供stdio.h-它是完全可选的。但是,如果确实提供stdio.h,则必须提供所有库。确实实现printf的独立系统编译器往往将其作为UART通信来实现。
伦丁

1

对于那些苦苦挣扎的人,请将以下内容添加到syscalls.c中:

extern UART_HandleTypeDef huart1; // access huart1 instance
//extern int __io_putchar(int ch) __attribute__((weak)); // comment this out

__attribute__((weak)) int __io_putchar(int ch)
{
    HAL_StatusTypeDef status = HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
    return (status == HAL_OK ? ch : 0);
}

通过腻子连接到您的COM端口,您应该会很好。


1

如果您想使用其他方法,而又不想覆盖函数或声明自己的函数,则可以简单地用于snprintf实现相同的目标。但这并不那么优雅。

char buf[100];
snprintf(buf, 100, "%X %X", val1, val2);
HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), 1000);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.