Здравствуйте. Хочу поделиться алгоритмом и программной реализацией счетчика времени наработки изделия на микроконтроллере 1986BE92QI на языке Си.
Очень часто появляется необходимость отсчитывать время, отработанное некоторым устройством. Для ведения счетчика наработки необходимо периодически с определенным интервалом времени, например каждую минуту, обновлять значение, хранящееся в ячейке энергонезависимой памяти EEPROM. К сожалению, ресурс циклов записи и стирания этих ячеек памяти обычно мал и составляет около 10.000 циклов (по оценке производителя). Значит, если стирать и перезаписывать значение в одну и туже ячейку памяти с интервалом в 1 минуту, то ресурс ячейки будет израсходован примерно за неделю. Для увеличения этого времени можно использовать не одну ячейку, а все ячейки некоторой, свободной страницы памяти, например последней. Это даст нам 1024 * 10.000 запас циклов записи и стирания, что эквивалентно, примерно 19 годам при ежеминутной перезаписи значений счетчика. Или использовать даже две страницы, в зависимости от требований. В микроконтроллере 1986BE92QI доступны 32 страницы энергонезависимой памяти для записи программы и 1 страница информационной энергонезависимой памяти по 4 кбайт. Каждая страница поделена на 4 сектора (SECTOR_A, SECTOR_B, SECTOR_C, SECTOR_D). Разбитие страницы на сектора дает возможность стирать данные страницы не целиком, а поблочно по 256 четырех байтных слова в четыре этапа.
Итак, для ведения счетчика наработки можно использовать следующий алгоритм. Значение счетчика записывается в первую ячейку первого сектора (SECTOR_A). Через минуту происходит инкремент счетчика и его значение записывается во вторую ячейку первого сектора и так до конца сектора. Затем осуществляется переход на следующий сектор (SECTOR_B), но перед записью значений происходит стирание этого сектора и выполняется запись значения в первую ячейку сектора B, затем во вторую ячейку и так далее до конца страницы. Заполнение всей страницы данными происходит за 1024 минуты. Когда вся страница заполнена осуществляется переход на первый сектор SECTOR_A, он предварительно очищается, т.к. сохранившиеся там данные нам уже не нужны, ведь самые актуальные значения находятся в секторе D. И продолжается запись данных уже в первый сектор.
Каждое записываемое в ячейку памяти значение состоит из четырех байт. В первых трех байтах находится само значение счетчика минут. В четвертом байте 8 битная, посчитанная для этих трех байт контрольная сумма CRC8. Эта контрольная сумма позволяет определять испорченность записанных данных. Ниже приведена структура записываемого в память значения счетчика с тремя байтами данных uint8_t val[3] и байтом контрольной суммы uint8_t crc.
typedef struct
{
union
{
struct
{
uint8_t val[3];
uint8_t crc;
};
uint32_t value;
};
} counter_value_t;
Если записанная контрольная сумма не будет совпадать с подсчитанной контрольной суммой, то это означает, что данные в данной ячейке испорчены и их использовать нельзя и необходимо взять предыдущее значение минут. Потеря одной-двух минут не будет критичной, если надо подсчитать несколько лет наработки.
Испортить записываемое значение также возможно, если произойдет отключение питания микроконтроллера во время записи в ячейку EEPROM. Наличие контрольной суммы также позволяет определять испорченные значения при записи.
При первом запуске микроконтроллер проходится последовательно по каждой ячейке памяти страницы, считывает значение счетчика и ищется максимальное не испорченное значение. С адреса следующего за этим значением продолжится ведение записи счетчика в память.
В листинге ниже представлен код на языке Си для реализации описанного алгоритма.
#include <MDR32Fx.h>
#include <stdlib.h>
#include <crc8.h>
#include <MDR32F9Qx_eeprom.h>
#include <stdint.h>
#include <MDR32F9Qx_config.h>
#include <MDR32F9Qx_eeprom.h>
// Стартовый адрес страницы для записи.
#define PAGE 0x0801F000
#define SECTORA (PAGE)
#define SECTORB (PAGE + 0x04)
#define SECTORC (PAGE + 0x08)
#define SECTORD (PAGE + 0x0C)
// Кол-во секторов в странице
#define SECTORS 4
// Кол-во 32 битных слов в странице
#define WORDS 256
// EEPROM Bank Selector
#define BANK_SELECT EEPROM_Main_Bank_Select
#define EEPROM_REG_ACCESS_KEY ((uint32_t)0x8AAA5551)
#define DELAY_LOOP_CYCLES (8UL)
#define GET_US_LOOPS(N) ((uint32_t)((float)(N) * FLASH_PROG_FREQ_MHZ / DELAY_LOOP_CYCLES))
#pragma anon_unions
typedef struct
{
union
{
struct
{
uint8_t val[3]; // Значение счетчика
uint8_t crc; // Контрольная сумма
};
uint32_t value;
};
} counter_value_t;
typedef enum {
RC_OK = 0,
RC_EMPTY = 1,
RC_CRCERR = 2
}rc_mem_t;
const uint32_t sectors[4]={
SECTORA,
SECTORB,
SECTORC,
SECTORD
};
uint32_t last_eeprom_word = 0; // Последнее прочитанное значение
uint32_t eeprom_word = 0; // Текущее прочитанное значение
uint32_t sector = 0; // Текущий сектор
uint32_t position = 0; // Текущая позиция в секторе
/**----------------------------------------------------------------------------
* @brief Program delay.
* @param loops: Number of the loops.
* @retval None.
*/
__RAMFUNC static void ProgramDelay(uint32_t loops)
{
volatile uint32_t i = loops;
for (; i > 0; i--)
{
}
}
/**-----------------------------------------------------------------------------
* @brief Стирание одного сектора.
* @param adress: начальный адрес сектора.
* @param bankSelector: выбранный банк памяти.
* @retval None.
*/
__RAMFUNC void EEPROM_EraseSector(uint32_t address, uint32_t bankSelector)
{
uint32_t Command;
assert_param(IS_EEPROM_BANK_SELECTOR(BankSelector));
MDR_EEPROM->KEY = EEPROM_REG_ACCESS_KEY;
Command = (MDR_EEPROM->CMD & EEPROM_CMD_DELAY_Msk) | EEPROM_CMD_CON;
Command |= (bankSelector == EEPROM_Info_Bank_Select) ? EEPROM_CMD_IFREN : 0;
MDR_EEPROM->CMD = Command;
MDR_EEPROM->ADR = address; // Page Address
MDR_EEPROM->DI = 0;
Command |= EEPROM_CMD_XE | EEPROM_CMD_ERASE;
MDR_EEPROM->CMD = Command;
ProgramDelay(GET_US_LOOPS(5)); // Wait for 5 us
Command |= EEPROM_CMD_NVSTR;
MDR_EEPROM->CMD = Command;
ProgramDelay(GET_US_LOOPS(40000)); // Wait for 40 ms
Command &= ~EEPROM_CMD_ERASE;
MDR_EEPROM->CMD = Command;
ProgramDelay(GET_US_LOOPS(5)); // Wait for 5 us
Command &= ~(EEPROM_CMD_XE | EEPROM_CMD_NVSTR);
MDR_EEPROM->CMD = Command;
ProgramDelay(GET_US_LOOPS(1)); // Wait for 1 us
Command &= EEPROM_CMD_DELAY_Msk;
MDR_EEPROM->CMD = Command;
MDR_EEPROM->KEY = 0;
}
/**-----------------------------------------------------------------------------
* @brief Получение значения.
* @param adress: адресс ячеки для чтения.
* @param bankSelector: выбранный банк памяти.
* @param value: указатель на переменную для чтения значения из памяти.
* @retval Результат операции.
*/
rc_mem_t GetWord(uint32_t address, uint32_t bankSelector, uint32_t * value)
{
rc_mem_t ret = 0;
counter_value_t count;
uint8_t crc = 0;
address -= address % 4;
__disable_irq();
count.value = EEPROM_ReadWord (address, bankSelector);
__enable_irq();
crc = getCRC8byTable( count.val, 3);
if (count.value == 0xFFFFFFFF)
ret = RC_EMPTY;
else if (count.crc != crc)
ret = RC_CRCERR;
else
{
ret = RC_OK;
*value = count.value & 0x00FFFFFF;
}
return ret;
}
/**-----------------------------------------------------------------------------
* @brief Запись значения в память.
* @param adress: адресс ячеки для записи.
* @param bankSelector: выбранный банк памяти.
* @param value: значение для записи в ячейку памяти.
* @retval Результат операции.
*/
void SetWord(uint32_t address, uint32_t bankSelector, uint32_t value)
{
counter_value_t count;
address -= address%4;
memcpy( count.val, &value, 3);
count.crc = getCRC8byTable( count.val, 3);
__disable_irq();
EEPROM_ProgramWord ( address, bankSelector, count.value);
__enable_irq();
}
/**-----------------------------------------------------------------------------
* @brief Ведение счетчика.
* @param Нет.
* @retval Нет.
*/
void Moto()
{
static uint8_t isFirst = 1;
uint32_t i = 0;
rc_mem_t ret = 0;
// поиск текущего сектора
for (i = sector, last_eeprom_word = 0; i < 4; i++)
{
ret = GetWord(sectors[i], BANK_SELECT, &eeprom_word);
if ( ret == RC_EMPTY || ret == RC_CRCERR || eeprom_word < last_eeprom_word)
break;
else
{
sector = i;
last_eeprom_word = eeprom_word;
}
}
// поиск текущей позиции в секторе
for (i = 0; i < WORDS; i++)
{
ret = GetWord(sectors[sector] + i * 16, BANK_SELECT, &eeprom_word);
if (ret == RC_EMPTY)
break;
else if (ret != RC_EMPTY)
last_eeprom_word = eeprom_word + 1;
}
position = i;
if (isFirst)
{
isFirst = 0;
return;
}
// стираем следующий сектор, если начинать сначала
if (position == WORDS)
{
position = 0;
sector++;
if(sector == SECTORS )
sector = 0;
__disable_irq();
EEPROM_EraseSector(sectors[sector], BANK_SELECT);
__enable_irq();
}
// записываем значение + 1
SetWord(sectors[sector] + position * 16, BANK_SELECT, last_eeprom_word);
}
/**----------------------------------------------------------------------------
* @brief Получение значения.
* @param Нет.
* @retval Значение счетчика.
*/
uint32_t GetMoto()
{
return last_eeprom_word;
}
/**-----------------------------------------------------------------------------
* @brief Установка значения счетчика.
* @param value: Значение счетчика.
* @retval Нет.
*/
void SetMoto(uint32_t value)
{
__disable_irq();
EEPROM_ErasePage (PAGE, BANK_SELECT);
__enable_irq();
sector = 0;
position = 0;
last_eeprom_word = value;
SetWord(sectors[sector] + position * 16, BANK_SELECT, value);
}
Стоит обратить внимание, что при записи в EEPROM блокируются прерывания. Это может быть довольно критичным недостатком для приложений, в которых реализованы различные защиты на других прерываниях, например, прерываниях по компаратору.
Данная реализация счетчика содержит только минимальный необходимый набор проверок и контроля ошибок и не претендует на максимальную полноту охвата решаемой проблемы. Можно придумать и добавить дополнительные проверки, например, проверку сохраненного значения в памяти непосредственно после записи и т.д.
И, на всякий случай, реализация алгоритма вычисления контрольной суммы CRC8 табличным методом.
#include <stdint.h>
const uint8_t crc8tab[256] =
{
0x00, 0x31, 0x62, 0x53, 0xC4, 0xF5, 0xA6, 0x97,
0xB9, 0x88, 0xDB, 0xEA, 0x7D, 0x4C, 0x1F, 0x2E,
0x43, 0x72, 0x21, 0x10, 0x87, 0xB6, 0xE5, 0xD4,
0xFA, 0xCB, 0x98, 0xA9, 0x3E, 0x0F, 0x5C, 0x6D,
0x86, 0xB7, 0xE4, 0xD5, 0x42, 0x73, 0x20, 0x11,
0x3F, 0x0E, 0x5D, 0x6C, 0xFB, 0xCA, 0x99, 0xA8,
0xC5, 0xF4, 0xA7, 0x96, 0x01, 0x30, 0x63, 0x52,
0x7C, 0x4D, 0x1E, 0x2F, 0xB8, 0x89, 0xDA, 0xEB,
0x3D, 0x0C, 0x5F, 0x6E, 0xF9, 0xC8, 0x9B, 0xAA,
0x84, 0xB5, 0xE6, 0xD7, 0x40, 0x71, 0x22, 0x13,
0x7E, 0x4F, 0x1C, 0x2D, 0xBA, 0x8B, 0xD8, 0xE9,
0xC7, 0xF6, 0xA5, 0x94, 0x03, 0x32, 0x61, 0x50,
0xBB, 0x8A, 0xD9, 0xE8, 0x7F, 0x4E, 0x1D, 0x2C,
0x02, 0x33, 0x60, 0x51, 0xC6, 0xF7, 0xA4, 0x95,
0xF8, 0xC9, 0x9A, 0xAB, 0x3C, 0x0D, 0x5E, 0x6F,
0x41, 0x70, 0x23, 0x12, 0x85, 0xB4, 0xE7, 0xD6,
0x7A, 0x4B, 0x18, 0x29, 0xBE, 0x8F, 0xDC, 0xED,
0xC3, 0xF2, 0xA1, 0x90, 0x07, 0x36, 0x65, 0x54,
0x39, 0x08, 0x5B, 0x6A, 0xFD, 0xCC, 0x9F, 0xAE,
0x80, 0xB1, 0xE2, 0xD3, 0x44, 0x75, 0x26, 0x17,
0xFC, 0xCD, 0x9E, 0xAF, 0x38, 0x09, 0x5A, 0x6B,
0x45, 0x74, 0x27, 0x16, 0x81, 0xB0, 0xE3, 0xD2,
0xBF, 0x8E, 0xDD, 0xEC, 0x7B, 0x4A, 0x19, 0x28,
0x06, 0x37, 0x64, 0x55, 0xC2, 0xF3, 0xA0, 0x91,
0x47, 0x76, 0x25, 0x14, 0x83, 0xB2, 0xE1, 0xD0,
0xFE, 0xCF, 0x9C, 0xAD, 0x3A, 0x0B, 0x58, 0x69,
0x04, 0x35, 0x66, 0x57, 0xC0, 0xF1, 0xA2, 0x93,
0xBD, 0x8C, 0xDF, 0xEE, 0x79, 0x48, 0x1B, 0x2A,
0xC1, 0xF0, 0xA3, 0x92, 0x05, 0x34, 0x67, 0x56,
0x78, 0x49, 0x1A, 0x2B, 0xBC, 0x8D, 0xDE, 0xEF,
0x82, 0xB3, 0xE0, 0xD1, 0x46, 0x77, 0x24, 0x15,
0x3B, 0x0A, 0x59, 0x68, 0xFF, 0xCE, 0x9D, 0xAC
};
//-----------------------------------------------------------------------------
//
// getCRC8byTable
//
// Calculation of the CRC-8
//
// Parametric model of CRC-8 algorithm:
// Name : CRC-8
// Poly : 0x31 x^8 + x^5 + x^4 + 1
// Init : 0xFF
// Revert: false
// XorOut: 0x00
// Check : 0xF7 ("123456789")
// MaxLen: 15 bytes(127 bits) - detection of single, dual,
// triple and all odd errors
//
uint8_t getCRC8byTable( uint8_t* arr, uint16_t len )
{
uint8_t crc8 = 0xff;
unsigned int i;
for( i = 0; i< len; i++)
{
crc8 = crc8tab[ crc8 ^ arr[i] ];
}
return crc8;
}
//-----------------------------------------------------------------------------
//
// getCRC8
//
// calculation of the CRC directly
//
// Parametric model of CRC-8 algorithm:
// Name : CRC-8
// Poly : 0x31 x^8 + x^5 + x^4 + 1
// Init : 0xFF
// Revert: false
// XorOut: 0x00
// Check : 0xF7 ("123456789")
// MaxLen: 15 bytes(127 bits) - detection of single, dual,
// triple and all odd errors
//
uint8_t getCRC8(uint8_t *pcBlock, uint16_t len )
{
uint8_t crc = 0xff;
unsigned int i;
while (len--)
{
crc ^= *pcBlock++;
for (i = 0; i < 8; i++)
crc = crc & 0x80 ? (crc << 1) ^ 0x31 : crc << 1;
}
return crc;
}
Комментарии (31)
le2
04.08.2021 11:11+2Это не так обычно делается. Просто счетчики износа оборудования считают от нуля и ведут в ОЗУ. И каждые минут десять или ещё как-то по желанию добавляют к основным счётчикам в ПЗУ. Для задач бизнеса и для любых других технических это более чем достаточно. Погрешность при сбросе конечно есть, но это мизерная погрешность для реально эксплуатируемого изделия.
EEPROM (раньше по крайней мере) ещё жрала при записи много, да и экзотика это в эпоху ARM.le2
04.08.2021 11:13+1также встречается тамперная память — небольшая оперативка в контроллерах под напряжением батарейки часов. Туда можно.
le2
04.08.2021 11:15А для жадных извращенцев, как я, могу рекомендовать писать в память программ. Там 10 000 циклов. Тоже хорошо.
Gengenid
04.08.2021 11:22+2Так в статье и идет речь о памяти программ, т.е. флеше. Нет в упомянутом контроллере EEPROM.
Ravenwolf Автор
04.08.2021 13:27Производитель память программ в даташите называет EEPROM PROGRAM областью. И контроллер Flash-памяти программ называет MDR_EEPROM. Есть определенная путаница в формулировках, конечно.
da-nie
04.08.2021 19:47+1Судя по отечественному контроллеру и задаче подсчёта ресурса, автор делает что-то для военки. А туда засунуть батарейку, думаю, будет не так просто. Я для подобной задачи очень хотел применить 1666РЕ014 — её можно перезаписывать бесконечно практически. Но в список МОП она так и не попала (говорят, там кристалл импортный).
amphasis
04.08.2021 12:38+2Еще можно записывать накопленные данные в EEPROM по приходу power down event, технически обеспечив достаточное для записи время работы контроллера после выключения питания. Делал так в одном любительском проекте.
stripe
05.08.2021 20:23Ага, вешаешь на питание конденсатор микрофарад на 500, и заводишь внешнее питание на прерывание. Как раз хватает несколько ячеек eeprom записать
YDR
04.08.2021 11:24+2иногда можно писать побитно. т.е. 0xFF -> 0xFE -> 0xFC -> 0xF8 -> 0xF0 -> ..., и 4096 байт превращяются в 32768 бит. раз в минуту -> 32768 минут = почти 23 дня. * 10000...
Попробуйте. При записи в ячейку возможно обнулять биты. (в единички - только стиранием).
на самом деле, первые 2-4 байта можно использовать как счетчик перезаписей (+KC), или еще один сектор использовать для записи битов количеств перезаписей основного сектора. Или чередовать секторы - на что хватит фантазии.
Gengenid
04.08.2021 11:35+2Мне кажется на эту тему почти у всех производителей есть аппноуты. AVR109, STM AN2594 и прочие
barbos98
04.08.2021 16:28Согласно Миландру данный контроллер аналог STM32F103X, который имеет backup domain. Так и чухаем его регистры и по прерыванию питания сливаем в энергонезависимую. Так будет кошернее. К стати, бекап домен имеет свое питание, соответственно проблема стремится к нулю.
Ravenwolf Автор
04.08.2021 16:38Надо будет опробовать ваше решение. Только батареечное питание ставить не вариант, но можно ионистор прикрутить.
le2
04.08.2021 20:24иногда ставят электролит (не очень большой, хотя бы 470uF на 6.3V). Компаратором или АЦП ловят просадку по питанию и за несколько секунд — сотен милисекунд и даже десятков милисекунд можно очень много всего забекапить и корректно завершить все критические секции.
le2
04.08.2021 20:30ионисторы сильно разные по утечкам. Разнятся даже в зависимости от партии. У некоторых большой саморазряд. Да и вроде влагу из воздуха сосут, что саморазряд ускоряется со временем. Из-за этого некоторые вендоры начали заливать их компаундом.
Не думаю что стоит всерьез надеятся на них если требуется целостность данных больше чем несколько часов.
Внешняя микросхема eeprom или даже более дешевая serial-flash будет лучше и дешевле.
devprodest
04.08.2021 22:30На самом то деле, что мешает держать счётчик в ОЗУ и при событии снижения питания (детектирование любым удобным способом) или просто с более длительным периодом записывать в ПЗУ?
Переменную можно разместить в неинициализируемой области (настраиваем скрипт для линкера) и перезагрузки не страшны.
Нужна лишь ёмкость по питанию чуть больше чем обычно. Можно сказать почти бесплатное решение.
Ravenwolf Автор
05.08.2021 10:56Да, мы пробовали всякие супервизоры питания. В диапазоне температур -50 +50 и термоциклировании и утечек отечественных электролитов сложно говорить о надёжности супервизоров. В итоге решение периодически записывать в энергонезависимую память, тем более, что она доступна непосредственно в МК оказалось самым надёжным.
checkpoint
05.08.2021 00:53+2Оперативной памяти доверять нельзя. Ионистор не защитит Вас от броска по питанию или статического разряда (ESD) и все ваши накопления за несколько дней будут утеряны. Писать надо регулярно в EEPROM с последовательным «размазыванием», с сохранением контрольной суммы и номера серии, желательно не реже чем раз в сек. Номер серии нужен для того, что бы при перезапуске устройства найти последнее правильное значение. Данное решение выстрадано и проверено годами.
garus_ru
05.08.2021 09:02Что будет, когда счетчик переполнится?
То есть, в память уже попало максимальное значение, после включения питания вы его нашли, затем счетчик обнулился, но с этого момента прошло менее 1024 минут, требуемых на перезапись всех страниц, и аппаратуру выключили.
Пока не преодолеем 1024 минуты непрерывной работы после достижения максимального значения, учет времени вестись не будет?
Andy_Big
06.08.2021 00:11Это произойдет лишь через 12 лет :)
3epg
18.08.2021 10:3412 лет ничто для электронной техники ИМХО. У меня дома комп 2010 года трудится, как раз 12 лет. Я бы очень опечалился, если он внезапно из-за такой мелочи превратился в тыкву.
Andy_Big
18.08.2021 10:51Так это же смотря какая электроника. Поди сейчас найди у кого-то смартфон такого возраста :)
3epg
18.08.2021 11:05Но всё таки нельзя исключать, что такие есть. Правда смартфоны слегка не корректное сравнение, тк на старые просто не ставятся новые приложухи и юзеры вынужденны их менять из-за этого обстоятельства.
ЗЫ айфон шесть, с которого я пишу выпускался с 14го года, у товарища есть в пользовании пятый и брал он его новым один из первых, ему уже девять лет и менять он его ни на что не хочет, говорит это последний из телефонов, следующие - одни лопаты. ))
Andy_Big
18.08.2021 11:57Я вовсе не исключаю, что есть владельцы таких смартфонов, но их придется поискать :)
Что касается описываемого Вами случая со счетчиком, то если даже по прошествии 12 лет работы он 10 раз подряд будет включаться на время, меньшее, чем 1024 минуты (около 17 часов), то это все равно будет слишком несущественно на фоне общего времени наработки. Если это какой-то не супер критический прибор, то я, например, считаю допустимым это.
garus_ru
21.08.2021 12:57+1Ага, понятно )))
- так, ребята... "науке" больше не наливать... топлива...
Ravenwolf Автор
21.08.2021 09:28Немножко не понял вопроса. Страница разбита на 4 сектора по 256 4-х байтных слов. И каждый из этих секторов стирается отдельно. Т.е. для перехода на запись в следующий сектор его предварительно очищают. Когда у вас все 4 сектора заполнены происходит стирание первого сектора и переход от четвертого сектора к первому. При этом предыдущие данные сохраняются в четвертом секторе.
garus_ru
21.08.2021 12:56У вас наступает момент, когда в память записывается последнее число, которое является максимальным. После него через минуту счетчик переполняется и принимает нулевое значение, которое тоже записывается в память. Затем, допустим, оборудование до истечения (256*3+1) минут - согласен, не 1023, - выключают. Таким образом, у нас в памяти присутствует максимально возможное значение и куча всяких других.
После включения вы ищете по всем страницам наибольшее значение и оно оказывается равным максимально возможному. Через минуту снова записывается нулевое значение и т.д.. И, если после включения оборудование проработало менее (256*3+1) минут и его выключили, то максимально возможное значение осталось незатертым.
Если бы вы при переполнении (обнуления) счетчика произвели бы стирание всех страниц, то такого зазора в (256*3+1) минут, в котором не происходит подсчет моточасов, не было бы.
lamerok
А на С++ можно сделать, чтобы запись была очень юзерфрендли, без знания секторов и адресов EEPROM
тут вот описал https://habr.com/ru/post/561678/
Ravenwolf Автор
Спасибо за ссылку, очень интересная статья!)