Структура параметров EEPROM для небольшого встроенного устройства
Основная проблема, которую я решаю при редизайне небольшого встроенного устройства (PID-контроллер), - это хранение параметров устройства. Старое решение, которое я частично представил здесь, было эффективным с точки зрения пространства, но неуклюжим для поддержки при добавлении новых параметров. Он был основан на идентификаторе параметра устройства, который должен совпадать с адресом EEPROM, как в примере, приведенном ниже:
// EEPROM variable addresses
#define EE_CRC 0 // EEPROM CRC-16 value
#define EE_PROCESS_BIAS 1 // FLOAT, -100.00 - 100.00 U
#define EE_SETPOINT_VALUE 3 // FLOAT, -9999 - 9999.9
#define EE_SETPOINT_BIAS 5 // CHAR, -100 - 100 U
#define EE_PID_USED 6 // BYTE, 1 - 3
#define EE_OUTPUT_ACTION 7 // LIST, DIRE/OBRNU
#define EE_OUTPUT_TYPE 8 // LIST, GRIJA/MOTOR
#define EE_PROCESS_BIAS2 9 // FLOAT, -100.00 - 100.00 U
#define EE_SETPOINT_VALUE2 11 // FLOAT, -9999 - 9999.9
#define EE_SETPOINT_BIAS2 13 // CHAR, -100 - 100 U
#define EE_PID_USED2 14 // BYTE, 1 - 3
#define EE_OUTPUT_ACTION2 15 // LIST, DIRE/OBRNU
#define EE_OUTPUT_TYPE2 16 // LIST, GRIJA/MOTOR
#define EE_LINOUT_CALIB_ZERO 17 // FLOAT, -100.0 - 100.0
#define EE_LINOUT_CALIB_GAIN 19 // FLOAT, -2.0 - 2.0
Каждый адрес был жестко закодирован, и следующий адрес был определен в зависимости от предыдущего размера данных (обратите внимание на неравномерное расстояние между адресами). Это было эффективно, так как хранилище данных EEPROM не было потрачено впустую, но его было трудно расширить, не внося ошибок.
В других частях кода (например, меню HMI, хранение данных...) код будет использовать список параметров, соответствующий только что указанным адресам, что-то вроде следующего:
// Parameter identification, NEVER USE 0 (zero) as ID since it's NULL
// Sequence is not important, but MUST be same as in setparam structure
#define ID_ENTER_PASSWORD_OPER 1
#define ID_ENTER_PASSWORD_PROGRAM 2
#define ID_ENTER_PASSWORD_CONFIG 3
#define ID_ENTER_PASSWORD_CALIB 4
#define ID_ENTER_PASSWORD_TEST 5
#define ID_ENTER_PASSWORD_TREGU 6
#define ID_PROCESS_BIAS 7
#define ID_SETPOINT_VALUE 8
#define ID_SETPOINT_BIAS 9
#define ID_PID_USED 10
#define ID_OUTPUT_ACTION 11
#define ID_OUTPUT_TYPE 12
#define ID_PROCESS_BIAS2 13
...
Затем в коде, использующем эти параметры, например в приведенных ниже структурах пользовательского меню, я создал элементы, используя свой собственный тип PARAM (структуру):
struct param { // Parametar decription
WORD ParamID; // Unique parameter ID, never use zero value
BYTE ParamType; // Parametar type
char Lower[EDITSIZE]; // Lowest value string
char Upper[EDITSIZE]; // Highest value string
char Default[EDITSIZE]; // Default value string
BYTE ParamAddr; // Parametar address (in it's media)
};
typedef struct param PARAM;
Теперь список параметров построен в виде массива структур:
PARAM code setparam[] = {
{NULL, NULL, NULL, NULL, NULL, NULL}, // ID 0 doesn't exist
{ID_ENTER_PASSWORD_OPER, T_PASS, "0", "9999", "0", NULL},
{ID_ENTER_PASSWORD_PROGRAM, T_PASS, "0", "9999", "0", NULL},
{ID_ENTER_PASSWORD_CONFIG, T_PASS, "0", "9999", "0", NULL},
{ID_ENTER_PASSWORD_CALIB, T_PASS, "0", "9999", "0", NULL},
{ID_ENTER_PASSWORD_TEST, T_PASS, "0", "9999", "0", NULL},
{ID_ENTER_PASSWORD_TREGU, T_PASS, "0", "9999", "0", NULL},
{ID_PROCESS_BIAS, T_FLOAT, "-100.0", "100.0", "0", EE_PROCESS_BIAS},
{ID_SETPOINT_VALUE, T_FLOAT, "-999", "9999", "0.0", EE_SETPOINT_VALUE},
{ID_SETPOINT_BIAS, T_CHAR, "-100", "100", "0", EE_SETPOINT_BIAS},
{ID_PID_USED, T_BYTE, "1", "3", "1", EE_PID_USED},
{ID_OUTPUT_ACTION, T_LIST, "0", "1", "dIrE", EE_OUTPUT_ACTION},
{ID_OUTPUT_TYPE, T_LIST, "0", "1", "GrIJA", EE_OUTPUT_TYPE},
{ID_PROCESS_BIAS2, T_FLOAT, "-100.0", "100.0", "0", EE_PROCESS_BIAS2},
...
По сути, каждый параметр имеет свой уникальный идентификатор, и этот идентификатор должен совпадать с жестко закодированным адресом EEPROM. Поскольку параметры не были фиксированы по размеру, я не мог использовать сам идентификатор параметра в качестве адреса EEPROM (или другого носителя). Организация EEPROM в приведенном выше примере была 16-битным словом, но это не имеет значения в принципе (больше места тратится на символы, поэтому в любом случае я бы предпочел 8-битную организацию в будущем)
Вопрос:
Есть ли более элегантный способ сделать это? Какая-то хеш-таблица, хорошо известный шаблон, стандартное решение для подобных проблем? ЭСППЗУ теперь намного больше по размеру, и я не возражаю против использования фиксированного размера параметра (тратя 32 бита на логический параметр) в обмен на более элегантное решение. Похоже, с параметрами фиксированного размера, я мог бы использовать идентификатор параметра в качестве адреса. Есть ли очевидный недостаток в этом методе, который я не вижу?
Сейчас я использую распределенное HW (HMI, I/O и главный контроллер разделены), и я хотел бы использовать структуру, в которой все устройства знают об этой структуре параметров, так что, например, удаленный ввод / вывод знает, как масштабировать входные значения, и HMI знает, как отображать и форматировать данные, все это основано только на ID параметра. Другими словами, мне нужно единственное место, где будут определены все параметры.
Я провел исследование Google, очень мало можно найти для небольших устройств, не считая некоторых баз данных. Я даже думал о каком-то определении XML, которое генерировало бы некоторый C-код для моих структур данных, но, может быть, было какое-то элегантное решение, более подходящее для небольших устройств (до 512 K Flash, 32 K RAM)?
3 ответа
Я не уверен, что это на самом деле лучше, чем у вас, но вот идея. Для упрощения обслуживания рассмотрите возможность инкапсуляции информации об адресах EEPROM в объект "eeprom". Прямо сейчас у вас есть объект параметра, и каждый экземпляр знает, где хранятся его данные в физической EEPROM. Возможно, было бы легче поддерживать, если бы объект параметров не знал EEPROM. И вместо этого отдельный объект EEPROM отвечал за взаимодействие между физической EEPROM и экземплярами объекта параметров.
Кроме того, рассмотрите возможность добавления номера версии данных EEPROM к данным, сохраненным в EEPROM. Если микропрограмма устройства обновляется и формат данных EEPROM изменяется, то этот номер версии позволяет новой микропрограмме распознавать и преобразовывать старую версию данных EEPROM.
Если вас не беспокоит совместимость между изменениями или процессорами, вы можете просто скопировать структуру между ОЗУ и ЭСППЗУ и получить доступ только к отдельным элементам копии ОЗУ.
Вы также можете относительно легко создать инструмент, который скомпилирует список определений из структуры и известных правил упаковки вашего компилятора, если вы хотите сделать явный доступ к отдельным членам непосредственно в EEPROM.
Вот что я хотел бы сделать.
Я бы создал typedef структуры с переменными, которые вы хотите иметь в EEPROM.
На вашем примере это будет выглядеть примерно так:
typedef struct eeprom_st
{
float process_biass;
float setpoint_value;
char setpoint_bias;
....
} eeprom_st_t;
Чем бы я создал определение смещения, чтобы отметить, где структура должна храниться в EEPROM.
И я бы добавил указатель на этот тип, чтобы использовать его как фиктивный объект:
#define EEPROM_OFFSET 0
eeprom_st_t *dummy;
Чем бы я использовал offsetof, чтобы получить смещение конкретной переменной мне нужно вот так:
eeprom_write( my_setpoint_bias, EEPROM_OFFSET + offsetof(eeprom_st_t,setpoint_bias),
sizeoff(dummy->setpoint_bias));
Чтобы сделать его более элегантным, я бы также превратил подпрограмму записи EEPROM в макрос.