4

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

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

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

unsigned char (*ucEvalTemperature)(int *);

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

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

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 程序集调用(这当然很可怕),可能会导致调用堆栈出现重大问题。

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

4

3 回答 3

2

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

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

于 2009-06-15T10:43:59.157 回答
2

如果可以,请使用结构“接口”:

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 和单独的编译单元,其中一个将链接到您的二进制文件中),但它仍然必须至少与源代码兼容,也就是说,在相同的情况下调用方法。

于 2009-06-15T10:21:29.430 回答
0

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

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

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

于 2009-06-15T11:08:05.993 回答