C(嵌入式):为源文件提供唯一ID

8

我正在开发一个紧凑型的调试输出/单元测试工具,用于嵌入式系统。

我创建了一个系统,可以以紧凑的方式通过串口将消息输出到PC。为了节省内存空间/串口带宽,我通过给它们分配唯一的16位ID从嵌入式系统中剥离了消息字符串。

这很容易实现,因为我将所有消息都放在一个列表中。一些宏会将其转换成枚举:

projectdefs.h:

#define MESSAGE_TABLE(MSG) \
    MSG(HELLO, "Hello World!") \
    MSG(TEST, "Second message #ID 1") \
    MSG(TEST2, "Third message #ID 2")

messages.h:

#define MACRO_STR_CONCAT(a,b) a##b
#define MESSAGE_ENUM(codeName, str) MACRO_STR_CONCAT(MSG_, codeName)    

typedef enum messageNumbers_e {
    MESSAGE_TABLE(MESSAGE_ENUM),
    MESSAGE_COUNT
};

#define MESSAGE(codeName) messageSend(MACRO_STR_CONCAT(MSG_, codeName), __LINE__, file_number);

串行端口传输的唯一数据是消息ID、行号和文件号(注意:不是字符串!)。

我的问题在于,如何使用C预处理器/编译器为每个文件分配唯一的ID。我不想在嵌入式程序中存储每个文件名字符串。这会占用过多的内存或者串行端口带宽。

我的想法是,在每个源文件顶部使用宏定义来定义常数file_number。我将使用以下定义:

#define ASSIGN_FILENUMBER() enum { file_number = __COUNTER__ };

然而,由于每个文件都是单独编译的,这意味着__COUNTER__语句被调用时始终从0开始,并不知道其他文件或自己的ID。
另一个考虑因素是编辑MakeFile(脚本)并在其中添加文件ID号码。然而,这将使项目构建/功能与我的IDE配置紧密耦合,这是不可取的。此外,我对当前IDE(XC16/XC32编译器,Mplab X IDE或IAR嵌入式工作台)的可能性不确定。
我想知道是否有其他创造性的方法可以让标准的C预处理器接管任务?

1
你的编译器支持预编译头文件吗?预编译头文件可以保留__COUNTER__值,至少在MSVC文档中是这样写的。 - Alex F
你觉得使用O(1)算法动态生成唯一ID可以吗?或者我没有理解这个问题:o - user123
我试过手动预编译一个带有ASSIGN_FILENUMBER宏的头文件。我的编译器可能是基于gcc的,但并不太确定。 我可以看到预编译的头文件已被拾取(-H输出),但是处理器特定的定义无法编译:invalid attribute space, __sfr__, __unsafe__等被忽略。 由于使用C语言编写的嵌入式软件是完全静态的程序,因此在设备内部动态分配/计算ID没有意义。此外,这将更难以跟踪主机。 - Hans
由于C预处理器不知道有多少文件将被编译以制作您的程序,我认为C预处理器将很难提供帮助。 - brian beuning
1
虽然不是C预处理器,但您可能可以让makefile运行一个shell/python/perl脚本,在构建代码之前重新生成列表和常量。但更常见的选项是仅转储代码地址(以及可能的版本或构建时代),并使用引用映射文件或类似工具链调试输出的工具将其转换回开发机器上的文件/源代码(您需要保留,但不需要实际加载到目标系统上)。 - Chris Stratton
我已经实现了预构建脚本选项。在MPLAB X中运行得非常好,需要看看在其他IDE上的表现如何。 XC16的预编译文件头最终失败了。我摆脱了无效属性警告(包括预编译头文件中有问题的xc.h头文件),但是遇到了“内部编译器错误;分段错误”的难题。 :( 因此,C预处理器似乎被排除在外了。 - Hans
3个回答

6
我不确定在程序中存储源代码文件名对内存来说是否重要。如果你能够得到它们,那么你可以声明一个变量,如const char *fname = __FILE__;,然后计算某种校验和、哈希或CRC,并传输该值而不是文件名。在接收端,您可以将哈希值与文件名之一进行匹配。但需要确保文件名哈希不会发生冲突。
另一种方法是使用Makefile、Perl或其他工具来维护文件计数器,并将其作为宏输入到gcc中,例如gcc [someparams1] -DFILENUMBER=%FILECOUNTER% somefile.c [someparams2],其中%FILECOUNTER%是任何可以展开为文本形式的文件计数器变量值的表达式。在此处,您可能需要引入源文件编译的固定顺序。
您可以组合这两种方法,并将哈希值而不是计数器传递给gcc,例如-DFILENAMEHASH=%HASH%,其中%HASH%会扩展为数字常量,您的编译脚本将根据文件名生成该常量。

2
在桌面上存储字符串并不是什么大问题,但这个问题询问的是嵌入式应用程序,通常需要保持非常紧凑。 - Chris Stratton
我已经完成了你和Chris建议的事情。我添加了一个预构建程序(一个小的C#控制台程序,但可以是任何东西...),它读取makefile,找到所有的构建命令并添加了一个-DFILENUMBER参数。我在我的调试输出程序中也解析了makefile,所以我知道文件号是哪个文件。不幸的是,没有(可靠的)C解决方案,但这个方法可行并且不太麻烦。 - Hans

1
你是否考虑根据从FILE获取的信息创建一个16位哈希数?它可能不是完美的(会发生冲突),但对于你的需求而言可能已经足够了。如果可以接受,你需要一个外部查找表,以便将哈希编号映射到相应的文件。
希望这个想法能有所帮助。

这听起来像是一个运行时解决方案,这与保持字符串不出现在编译代码中的目标不兼容。 - Chris Stratton
如果可以通过某种宏在编译时完成,那么这将是一个选项。但我不认为宏可以进行这种算术运算? 我想要的是在构建/编译时生成静态文件ID。 - Hans

0

我建议在编译期间计算文件名的哈希值。您可以使用以下宏(请参见http://lolengine.net/blog/2011/12/20/cpp-constant-string-hash):

// Compile time hash computation. Size of input string must be <256 bytes.
#define   H1(s, i, x)    (x * 65599u + (uint8_t)s[(i) < strlen(s) ? strlen(s) - 1 - (i) : strlen(s)])
#define   H4(s, i, x)  H1(s, i,  H1(s, i +  1,  H1(s, i +   2,  H1(s, i +   3, x))))
#define  H16(s, i, x)  H4(s, i,  H4(s, i +  4,  H4(s, i +   8,  H4(s, i +  12, x))))
#define  H64(s, i, x) H16(s, i, H16(s, i + 16, H16(s, i +  32, H16(s, i +  48, x))))
#define H256(s, i, x) H64(s, i, H64(s, i + 64, H64(s, i + 128, H64(s, i + 192, x))))
#define HASH(s) ((uint32_t)(H256(s, 0, 0) ^ (H256(s, 0, 0) >> 16)))

然后,由于__COUNTER____FILE_NAME__,您可以轻松编写一个宏,通过唯一的数字替换跟踪:

#define __LOG(val, msg, ...) \
    do {                                                    \
        if (USE_BINARY_TRACES)                              \
            print_as_bin(val, ##__VA_ARGS__);               \
        else                                                \
            print_as_ascii(msg, ##__VA_ARGS__);             \
    } while(0)

#define LOG(msg, ...) __LOG(HASH(__FILE_NAME__) + __COUNTER__, msg, ##__VA_ARGS__)

然而,你可以更进一步。你可以将跟踪的字符串版本及其关联的ID存储在二进制文件的特定部分中:

struct message {
    uint32_t id;
    const char content[];
};

#define __LOG(val, msg, ...) \
    do {                                                        \
        static const __attribute__((used, section(".rodata_traces"))) struct message __trace = { val, msg }; \
        fake_printf(msg, ##__VA_ARGS__);                        \
        if (USE_BINARY_TRACES)                                  \
            send_to_uart_bin(val, ##__VA_ARGS__);               \
        else                                                    \
            send_to_uart_ascii(msg, ##__VA_ARGS__);             \
    } while(0)

#define LOG(msg, ...) __LOG(HASH(__FILE_NAME__) + __COUNTER__, msg, ##__VA_ARGS__)

因此,与数字相关联的字符串存储在二进制文件的.rodata_traces部分中:

$ objdump -j .rodata_traces -s binary_logs

binary_logs:     file format elf64-x86-64

Contents of section .rodata_traces:
  2008 d6caf741 576f726c 64000000 d5caf741  ...AWorld......A
  2018 48656c6c 6f00                        Hello.

现在您可以编写一个脚本来查找此表并翻译您的跟踪信息。显然,此表对运行时并不是必需的,您可以使用objcopy(就像处理调试符号一样)来削减它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接