【问题标题】:Best strategy to call an arbitrary function without using JMP or LCALL不使用 JMP 或 LCALL 调用任意函数的最佳策略
【发布时间】:2009-06-15 10:05:39
【问题描述】:

在嵌入式 C 中很自然地有一些固定/通用算法,但不止一种可能的实现。这是由于几个产品展示,有时是选项,有时只是产品路线图策略的一部分,例如可选 RAM、不同 IP 集 MCU、升级频率等。

在我的大多数项目中,我通过将核心内容、算法和逻辑架构与实现外部状态评估、计时、内存存储等的实际功能解耦来处理这个问题。

当然,我使用 C 函数指针机制,并为这些指针使用一组有意义的名称。例如

unsigned char (*ucEvalTemperature)(int *);

将温度存储在一个int中,返回是OK。

现在想象一下,对于产品的特定配置,我有一个

unsigned char ucReadI2C_TMP75(int *);

从 TMP75 设备读取 I2C 总线上温度的函数和

unsigned char ucReadCh2_ADC(unsigned char *);

一种读取二极管压降的函数,由 ADC 读取,这是一种在非常广泛的范围内评估温度的方法。

这是相同的基本功能,但在不同的选项集产品上。

在某些配置中,我会将 ucEvalTemperature 设置为 ucReadI2C_TMP75,而在其他配置中,我将使用 ucReadCh2_ADC。在这种温和的情况下,为避免出现问题,我应该将参数类型更改为 int,因为指针的大小始终相同,但函数签名不是,编译器会抱怨。好吧……那不是杀手。

这个问题在可能需要不同参数集的函数上变得很明显。签名永远不会正确,编译器将无法解析我的 Fpointers。

所以,我有三种方法:

  • 使用全局参数栈,所有函数都是unsigned char Func(void);
  • 对每个实现都使用一个帮助函数,让我可以打开正确的分配来进行/调用;
  • 使用 JMP / LCALL 程序集调用(这当然很糟糕),可能会导致调用堆栈出现重大问题。

既不优雅,也不沉稳……你的方法/建议是什么?

【问题讨论】:

  • 当参数变化时,调用者如何知道要提供哪些参数?
  • 我了解您正在考虑将可变数量的 args 放入同一个函数中,但这里的问题更多的是具有不同数量的 args 的不同函数。

标签: c embedded function-pointers


【解决方案1】:

我通常更喜欢分层架构。与硬件的通信是通过“驱动程序”实现的。算法层调用由驱动程序实现的函数 (readTemp)。关键是需要定义一个接口,并且所有驱动程序实现都必须遵守这一点。

高层应该对如何读取温度一无所知(使用 TMP75 或 ADC 无关紧要)。驱动程序架构的缺点是您通常无法在运行时切换驱动程序。对于大多数嵌入式项目,这不是问题。如果您想这样做,请定义指向驱动程序公开的函数(遵循公共接口)而不是实现函数的函数指针。

【讨论】:

  • 我们都使用这种架构。您提到的缺点在编译时也是如此......必须手动干预才能更改实现,这就是我们想要避免的。相同的基本固件必须在每个部分使用不同的选项编译 100 次,这是非常乏味的。这不是 PC,这是与硬件一起使用的 FW,具有许多不同的可能实现(出于无数原因,FW 应尽可能保持不变)
  • 如果仔细配置构建系统,每个部分只需要编译一次,链接100次,速度更快。我了解您希望为所有不同的硬件配置提供一个可执行文件。这确实是一个很难解决的问题。我相信,如果所有驱动程序实现都遵循一个通用接口,那么找到一个优雅的解决方案会更容易。
【解决方案2】:

如果可以,请使用 struct "interfaces":

struct Device {
  int (*read_temp)(int*, Device*);
} *dev;

叫它:

dev->read_temp(&ret, dev);

如果您需要其他参数,请将它们打包到 Device 中

struct COMDevice {
  struct Device d;
  int port_nr;
};

当你使用它的时候,只需向下转换。

然后,您将为您的设备创建函数:

int foo_read_temp(int* ret, struct Device*)
{
  *ret = 100;
  return 0;
}

int com_device_read_temp(int* ret, struct Device* dev)
{
  struct COMDevice* cdev = (struct COMDevice*)dev; /* could be made as a macro */
  ... communicate with device on com port cdev->port_nr ...
  *ret = ... what you got ...
  return error;
}

并像这样创建设备:

Device* foo_device= { .read_temp = foo_read_temp };
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
  .port_nr = 0x3e8
};
COMDevice* com_device_1= { .d = { .read_temp = com_read_temp },
  .port_nr = 0x3f8
};

您需要将 Device 结构传递给需要读取温度的函数。

Linux 内核中使用了这个(或类似的东西),除了它们没有将函数指针放在结构体中,而是为它创建一个特殊的静态结构体并将指向该结构体的指针存储在 Device 结构体中。这几乎就是 C++ 等面向对象语言实现多态性的方式。

如果您将函数放在单独的编译单元中,包括引用它们的 Device 结构,您仍然可以节省空间并在链接时将它们排除在外。

如果您需要不同类型的参数,或者需要更少的参数,请忘记它。这意味着你不能为你想要改变的东西设计一个通用接口(在任何意义上),但是没有一个通用接口,就不可能有可更改的实现。您可以使用编译时多态性(例如,针对不同实现的 typedef 和单独的编译单元,其中一个将链接到您的二进制文件中),但它仍然必须至少与源代码兼容,即在相同的情况下调用方式。

【讨论】:

  • C 结构可以具有变量可以具有的任何类型的成员,包括函数指针。 (如果您不相信,请尝试一下,或查看 Linux 源代码。)您对 C++ struct/class 所说的几乎是真的,但与您的问题和我的回答完全无关——不知道您为什么提到它.
  • 抱歉,我误读了您的代码,它表明您在该结构中有一个函数,而不是函数指针。被调用函数如何知道您封装在第二个结构上的附加参数?您关于通用接口是未更改实现的要求的观点是正确的。所以,我在这里尝试的是找到一种方法来超越函数的“正常”C 签名(返回、参数数量及其类型)并提出更灵活的方法(但可能不太明显)跨度>
  • 被调用函数如何知道参数:它会知道参数,因为它与带有参数的特定结构紧密耦合(即一种结构 - 一个函数)
【解决方案3】:

正确的方法是使用辅助函数。当然,unsigned char ucReadCh2_ADC(unsigned char *); 可能看起来像是将结果存储为值 [0,255]。但是谁说实际范围是 [0,255] ?即使是这样,这些值代表什么

另一方面,如果你要 typedef unsigned long milliKelvin,那么 typedef unsigned char (*EvalTemperature)(milliKelvin *out); 会更清晰。对于每个函数,它应该如何包装变得很清楚——通常是一个微不足道的函数。

请注意,我从 typedef 中删除了“uc”前缀,因为该函数无论如何都没有返回无符号字符。它返回一个布尔值,OK-ness。 (科尔伯特粉丝可能想要使用浮点数来表示真实性;-))

【讨论】:

  • 实际上花了一些时间。在选择布尔值之前,我考虑过编写 C 类型的 _Bool。正是因为它不是 C 类型,但具有明确、正确的语义。 // 至于科尔伯特的参考,他是“乔恩·斯图尔特每日秀”中的一个类型,他在报道麦凯恩/奥巴马选举时以“真实”着称,以涵盖其中所说的所有半真半假的事实。
  • 哦,那个科尔伯特!我以为你可能会指着他,但因为我已经一年多没看这个节目了,所以没有得到那个具体的术语。我想念我破裂的卫星电视......
猜你喜欢
  • 1970-01-01
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-25
  • 2014-04-02
  • 2012-04-25
  • 1970-01-01
相关资源
最近更新 更多