在微控制器程序中使用printf时最常见的耙

在我的项目中,我有时需要将printf与串行端口(UART或模拟串行端口的USB抽象)结合使用。 而且,像往常一样,它的应用程序之间要花费很多时间,我设法完全忘记了所有需要考虑的细微差别,以便它在大型项目中正常工作。

在本文中,我总结了自己在微控制器程序中使用printf时出现的细微差别,从最明显到完全不明显的证据分类。

简要介绍


实际上,为了在微控制器程序中使用printf,就足够了:
  • 在项目代码中包含头文件;
  • 重新定义_write系统函数以输出到串行端口;
  • 描述链接器需要的系统调用的存根(_fork,_wait和其他);
  • 在项目中使用printf调用。

实际上,并非一切都那么简单。

描述所有存根,而不仅仅是用过的。


最初,在项目的布局中存在一堆模糊的链接是令人惊讶的,但是在阅读了一点之后,就清楚了什么以及为什么。 在我所有的项目中,我都在连接此子模块 。 因此,在主项目中,我仅重新定义了我需要的方法(在这种情况下,只有_write),其余的保持不变。

重要的是要注意,所有存根都必须是C函数。 不是C ++(或包装在外部“ C”中)。 否则,布局将失败(请记住在使用G ++进行组装时名称更改)。

_write中有1个字符


尽管_write方法的原型的参数可以传递所显示消息的长度,但它的值为1(实际上,我们自己将其始终设置为1,但稍后会更多)。
int _write (int file, char *data, int len) { ... } 

在Internet上,您经常可以看到这种方法的这种实现
_write函数的频繁实现
 int uart_putc( const char ch) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); {} USART_SendData(USART2, (uint8_t) ch); return 0; } int _write_r (struct _reent *r, int file, char * ptr, int len) { r = r; file = file; ptr = ptr; #if 0 int index; /* For example, output string by UART */ for(index=0; index<len; index++) { if (ptr[index] == '\n') { uart_putc('\r'); } uart_putc(ptr[index]); } #endif return len; } 


这样的实现具有以下缺点:
  • 生产力低下;
  • 流不安全;
  • 无法将串行端口用于其他目的;


效能低下


性能下降的原因是使用处理器资源发送字节:您必须监视状态寄存器,而不是使用相同的DMA。 要解决此问题,可以预先准备好要发送的缓冲区,并在接收到行尾的字符(或填充缓冲区)时发送。 此方法需要一个缓冲存储器,但是通过频繁发送可以显着提高性能。
带有缓冲区的_write的示例实现
 #include "uart.h" #include <errno.h> #include <sys/unistd.h> extern mc::uart uart_1; extern "C" { //      uart. static const uint32_t buf_size = 254; static uint8_t tx_buf[buf_size] = {0}; static uint32_t buf_p = 0; static inline int _add_char (char data) { tx_buf[buf_p++] = data; if (buf_p >= buf_size) { if (uart_1.tx(tx_buf, buf_p, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } buf_p = 0; } return 0; } // Putty  \r\n    //    . static inline int _add_endl () { if (_add_char('\r') != 0) { return -1; } if (_add_char('\n') != 0) { return -1; } uint32_t len = buf_p; buf_p = 0; if (uart_1.tx(tx_buf, len, 100) != mc_interfaces::res::ok) { errno = EIO; return -1; } return 0; } int _write (int file, char *data, int len) { len = len; //   . if ((file != STDOUT_FILENO) && (file != STDERR_FILENO)) { errno = EBADF; return -1; } //     //   \n. if (*data != '\n') { if (_add_char(*data) != 0) { return -1; } } else { if (_add_endl() != 0) { return -1; } } return 1; } } 

这里,uart对象uart_1负责直接使用dma发送。 在从缓冲区发送数据(获取和返回互斥体)时,该对象使用FreeRTOS方法来阻止第三方对该对象的访问。 因此,没有人可以在从另一个线程发送消息时使用uart对象。
一些链接:
  • _ 在这里将功能代码写为真实项目的一部分
  • uart类接口在这里
  • 在stm32f4下实现uart类接口的实现
  • 这里将uart类实例化为项目的一部分


流不安全


由于没有人在相邻的FreeRTOS流中打扰开始向printf发送另一行,从而粉碎当前正在发送的缓冲区(uart中的互斥体保护该对象免于在不同的流中使用,但数据没有传输给它们,因此该实现也保持不受保护)。 ) 如果存在另一个线程的printf被调用的风险,则需要实现一个图层对象,该对象将完全阻止对printf的访问。 在我的特定情况下,只有一个线程与printf交互,因此其他复杂性只会降低性能(在层内部不断捕获和释放互斥锁)。

无法将串行端口用于其他目的


由于我们仅在接收到整个字符串(或缓冲区已满)之后发送,而不是uart对象,因此您可以调用converter方法到某个顶级接口以进行后续的数据包传输(例如,根据类似于数据包的传输协议保证传输交易Modbus)。 这将使您可以使用一个uart既用于显示调试信息,又可以用于例如用户与管理控制台的交互(如果设备上有一个)。 在接收方写一个解压缩器就足够了。

默认情况下,float输出不起作用


如果使用newlib-nano,则默认情况下,printf(以及它们的所有派生,例如sprintf / snprintf ...等等)都不支持浮点值的输出。 通过将以下链接器标志添加到项目中,可以轻松解决此问题。
 SET(LD_FLAGS -Wl,-u,vfprintf; -Wl,-u,_printf_float; -Wl,-u,_scanf_float; "_") 

这里查看标志的完整列表。

该程序冻结在printf的肠中某处


这是链接器标志中的另一个缺陷。 为了使用所需的库版本配置固件,必须明确指定处理器参数。
 SET(HARDWARE_FLAGS -mthumb; -mcpu=cortex-m4; -mfloat-abi=hard; -mfpu=fpv4-sp-d16;) SET(LD_FLAGS ${HARDWARE_FLAGS} "_") 

这里查看标志的完整列表。

printf迫使微控制器陷入硬故障


可能至少有两个原因:
  • 堆叠问题;
  • _sbrk问题;

堆栈问题


当使用FreeRTOS或任何其他OS时,此问题确实很明显。 问题是使用缓冲区。 第一段说,_write中每个有1个字节。 为了做到这一点,您必须在第一次使用printf之前禁止在代码中使用缓冲。
 setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); 

从功能描述中可以得出,可以用相同的方式设置以下值之一:
 #define _IOFBF 0 /* setvbuf should set fully buffered */ #define _IOLBF 1 /* setvbuf should set line buffered */ #define _IONBF 2 /* setvbuf should set unbuffered */ 

但是,这可能导致任务堆栈溢出(如果您突然是从中断调用printf的非常坏的人,则可能导致中断)。

纯粹从技术上讲,可以为每个流非常仔细地排列堆栈,但是这种方法需要仔细计划,并且很难捕获其所携带的错误。 一个更简单的解决方案是每个接收一个字节,将其存储在其自己的缓冲区中,然后以所需的格式输出(前面已解析)。

_sbrk问题


就我个人而言,这个问题是最隐含的。 那么,我们对_sbrk有什么了解?
  • 需要实现另一个存根以支持相当一部分标准库;
  • 需要在堆上分配内存;
  • 由malloc等各种库方法使用,免费。

就个人而言,在我的项目中,有95%的情况下我都使用FreeRTOS,并使用重新定义的方法new / delete / malloc,它们使用了许多FreeRTOS。 因此,当我分配内存时,我确定分配是在FreeRTOS堆上的,该堆在bss区域中占用了预定的内存量。 您可以在这里查看图层。 因此,纯粹从技术上讲,应该没有问题。 根本不应调用函数。 但是让我们想想,如果她打电话,那么她将在哪里尝试获得记忆?

回顾微控制器“经典”项目的RAM布局:
  • .data;
  • .bss;
  • 空的空间
  • 初始堆栈。

在数据中,我们具有全局对象(变量,结构和其他全局项目字段)的初始数据。 在bss中,全局字段具有初始零值,并小心地包含一堆FreeRTOS。 它只是内存中的一个数组。 然后可以使用heap_x.c文件中的方法。 接下来是空白空间,之后是堆栈(或者从末尾开始)。 因为 FreeRTOS在我的项目中使用,然后仅在调度程序启动之前使用此堆栈。 因此,在大多数情况下,它的使用仅限于collobyte(实际上通常是100个字节的限制)。

但是,使用_sbrk在哪里分配内存呢? 查看链接器脚本中使用的变量。
 void *__attribute__ ((weak)) _sbrk (int incr) { extern char __heap_start; extern char __heap_end; ... 

现在,我们在链接程序脚本中找到了它们(我的脚本与st提供的脚本略有不同,但是此处的部分大致相同):
 __stack = ORIGIN(SRAM) + LENGTH(SRAM); __main_stack_size = 1024; __main_stack_limit = __stack - __main_stack_size; ...  flash,    ... .bss (NOLOAD) : ALIGN(4) { ... . = ALIGN(4); __bss_end = .; } >SRAM __heap_start = __bss_end; __heap_end = __main_stack_limit; 

也就是说,它使用堆栈(从0x20020000开始为1 kb,使用128 kb RAM)之间的内存。

明白了 但是他重新定义了malloc,free和其他方法。 毕竟不需要使用_sbrk吗? 事实证明,这是必须的。 此外,此方法不使用printf,而是用于设置缓冲模式的方法-setvbuf(或_malloc_r,在库中未声明为弱函数。与malloc不同,可以轻松替换)。

由于我确定未使用sbrk,所以我在堆栈附近放置了一堆FreeRTOS(bss部分)(因为我确定堆栈使用量比要求的少10倍)。

问题3的解决方案
  • 在bss和堆栈之间缩进;
  • 覆盖_malloc_r,以便不调用_sbrk(从库中分离一种方法);
  • 通过malloc和free重写sbrk。

我选择了第一个选项,因为无法安全地替换标准的_malloc_r(位于libg_nano.a(lib_a-nano-mallocr.o)内)(该方法未声明为__attribute__((weak)),但只能从双库中排除单个函数我没有成功链接)。 我真的不想重写一个电话的sbrk。

最终的解决方案是在RAM中为初始堆栈和_sbrk分配单独的分区。 这样可以确保在设置阶段各部分不会相互堆叠。 在sbrk内部,还有一个检查是否有超出部分的检查。 我必须进行一些小的更正,以便在检测到国外过渡时,流程将挂在while循环中(因为sbrk的使用仅发生在初始化的初始阶段,应在调试设备的阶段进行处理)。
修改的内存
 MEMORY { FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 1M CCM_SRAM (RW) : ORIGIN = 0x10000000, LENGTH = 64K SRAM (RW) : ORIGIN = 0x20000000, LENGTH = 126K SBRK_HEAP (RW) : ORIGIN = 0x2001F800, LENGTH = 1K MAIN_STACK (RW) : ORIGIN = 0x2001FC00, LENGTH = 1K } 


对section.ld的更改
 __stack = ORIGIN(MAIN_STACK) + LENGTH(MAIN_STACK); __heap_start = ORIGIN(SBRK_HEAP); __heap_end = ORIGIN(SBRK_HEAP) + LENGTH(SBRK_HEAP); 

您可以在此提交中查看我的沙箱项目中的 mem.ldsection.ld

UPD 07/12/2019:修复了带有float值的有效printf的标志列表。 我用更正的编译和布局标志更正了到工作CMakeLists的链接(存在细微差别的事实是,标志应一一并通过“;”列出,而在一行或另一行上则无关紧要)。

Source: https://habr.com/ru/post/zh-CN459420/


All Articles