Лучшая стратегия для вызова произвольной функции без использования JMP или LCALL - PullRequest
3 голосов
/ 15 июня 2009

Во встроенном C вполне естественно иметь некоторый фиксированный / универсальный алгоритм, но более одной возможной реализации. Это происходит из-за нескольких презентаций продукта, иногда опций, в других случаях это просто часть стратегий стратегии продукта, таких как опциональное ОЗУ, другой MCU с IP-набором, повышенная частота и т. Д.

В большинстве моих проектов я имею дело с этим, отделяя основные компоненты, алгоритм и архитектуру логики от реальных функций, которые реализуют внешнюю оценку состояния, хранение времени, память и т. Д.

Естественно, я использую механизм указателей на функции C, и я использую набор значимых имен для этих указателей. Е.Г.

unsigned char (*ucEvalTemperature)(int *);

Тот хранит температуру в целых числах, а возвращаемое значение в порядке.

Теперь представьте, что для конкретной конфигурации продукта у меня есть

unsigned char ucReadI2C_TMP75(int *);

функция, которая считывает температуру на шине I2C с устройства TMP75 и

unsigned char ucReadCh2_ADC(unsigned char *);

функция, которая считывает падение напряжения на диоде, считываемое АЦП, что позволяет оценивать температуру при очень широких движениях.

Это та же базовая функциональность, но на разных продуктах с набором опций.

В некоторых конфигах у меня будет настройка ucEvalTempera на ucReadI2C_TMP75, а в других - ucReadCh2_ADC. В этом легком случае, чтобы избежать проблем, я должен изменить тип аргумента на int, потому что указатель всегда имеет одинаковый размер, но сигнатура функции не совпадает, и компилятор будет жаловаться. Хорошо ... это не убийца.

Проблема становится очевидной для функций, которым может потребоваться другой набор аргументов. Подписи никогда не будут правильными, и компилятор не сможет разрешить мои Fpointers.

Итак, у меня есть три способа:

  • использовать глобальный стек аргументов, и все функции имеют тип unsigned char Func (void);
  • использовать вспомогательную функцию для каждой реализации, которая позволяет мне включить правильное назначение для make / call;
  • использовать вызовы сборки JMP / LCALL (что, конечно, ужасно), что может вызвать серьезные проблемы в стеке вызовов.

Ни элегантно, ни составно ... каков ваш подход / совет?

Ответы [ 3 ]

2 голосов
/ 15 июня 2009

Я обычно предпочитаю иметь многоуровневую архитектуру. Связь с аппаратным обеспечением достигается с помощью «драйверов». Слои алгоритмов вызывают функции (readTemp), которые реализуются драйвером. Ключевым моментом является то, что интерфейс должен быть определен, и это должно соблюдаться всеми реализациями драйверов.

Более высокий уровень не должен знать о том, как считывается температура (не имеет значения, используете ли вы TMP75 или АЦП). Недостатком архитектуры драйверов является то, что вы обычно не можете переключать драйвер во время выполнения. Для большинства встроенных проектов это не проблема. Если вы хотите это сделать, определите указатели на функции, предоставляемые драйвером (которые следуют общему интерфейсу), а не на функции реализации.

1 голос
/ 15 июня 2009

Если вы можете, используйте 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
};

Вы передадите структуру устройства для функции, которая должна считывать температуру.

Это (или что-то подобное) используется в ядре Linux, за исключением того, что они не помещают указатели на функции внутри структур, но создают для них специальную статическую структуру и хранят указатель на эту структуру внутри структуры Device. Это почти точно так, как объектно-ориентированные лингваты, такие как C ++, реализуют полиморфизм.

Если вы поместите функции в отдельный модуль компиляции, включая структуру Device, которая ссылается на них, вы все равно можете сэкономить место и оставить их при связывании.

Если вам нужны разные типы аргументов или меньше аргументов, просто забудьте об этом. Это означает, что вы не можете спроектировать общий интерфейс (в любом смысле) для вещей, которые вы хотите изменить, но без общего интерфейса никакая изменяемая реализация невозможна. Вы можете использовать полиморфизм времени компиляции (например, typedefs и отдельные модули компиляции для разных реализаций, одна из которых будет связана в вашем двоичном файле), но он все равно должен быть по крайней мере совместимым с исходным кодом, то есть вызываться в том же способ.

0 голосов
/ 15 июня 2009

Правильный способ - использовать вспомогательную функцию. Конечно, unsigned char ucReadCh2_ADC(unsigned char *); может выглядеть так, как будто он сохраняет результат как значение [0,255]. Но кто сказал, что фактический диапазон составляет [0,255]? И даже если бы это произошло, что бы эти значения представляли ?

С другой стороны, если вы наберетеdef unsigned long milliKelvin, то typedef unsigned char (*EvalTemperature)(milliKelvin *out); будет намного понятнее. И для каждой функции становится ясно, как ее следует обернуть - довольно часто тривиальная функция.

Обратите внимание, что я удалил префикс "uc" из typedef, так как функция все равно не возвращала беззнаковый символ. Возвращает логическое значение, ОК. (Поклонники Кольберта могут использовать поплавок для обозначения правдивости ;-))

...