不使用JMP或LCALL调用任意函数的最佳策略

时间:2009-06-15 10:05:39

标签: c embedded function-pointers

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

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

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

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

既不优雅,也不构成......你的方法/建议是什么?

3 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:1)

如果可以,请使用struct“interfaces”:

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

称之为:

dev->read_temp(&ret, dev);

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

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结构,您仍然可以节省空间并在链接时将它们遗漏。

如果您需要不同类型的参数或更少的参数,请忘记它。这意味着您无法为要更改的内容设计通用接口(在任何意义上),但如果没有通用接口,则无法实现可更改的实现。您可以使用编译时多态性(例如,typedefs&单独的编译单元用于不同的实现,其中一个将在您的二进制文件中链接),但它仍然必须至少与源代码兼容,即在同样的方式。

答案 2 :(得分:0)

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

另一方面,如果你输入了unsigned long milliKelvin,那么typedef unsigned char (*EvalTemperature)(milliKelvin *out);会更加清晰。对于每一个功能,它都应该被清楚地包装 - 通常是一个微不足道的功能。

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