Вместо привычной флеш памяти в микроконтроллере 1986ВЕ8Т реализована однократно программируемая память ОТР - (One-Time Programable). Такая память бывает двух типов - Fuse и AntiFuse. Fuse переводится как предохранитель, что намекает на способ программирования. В памяти типа Fuse программирование бита сводится к пережиганию перемычки, а в памяти AntiFuse - к созданию пробоя подзатворного диэлектрика.
Красивые картинки про память и многое полезное можно найти в данной статье - The Benefits Of Antifuse OTP.
В обоих случаях изначально биты имеют значения 0, а прожигаются только те биты, которые должны стать "1". Согласно приведенной статье, прожиг бита памяти Fuse можно сделать только один раз. Если после единичного прожига бит не стал читаться как "1", то память бракуется. В отличие от Fuse, бит памяти типа AntiFuse можно прожигать несколько раз. При прожиге происходит пробой подзатворного диэлектрика, если пробой будет небольшой, то его можно и нужно "прожигать" повторно, чтобы образовался надежный контакт. Если контакт оставить плохой, то память не сможет работать на заявленных скоростях.
Все вышесказанное объясняет алгоритм программирования памяти ОТР в микроконтроллере 1986ВЕ8Т. Согласно спецификации, сначала необходимо провести первичный прожиг бит равных "1" - "фаза программирования". После этого запускается "фаза допрограммирования", когда считываются текущие значения в ячейках и если где-то "1" не прописалась, то запускается процедура программирования для данного бита. После окончания допрограммирования необходимо проверить, на сколько качественно прожглись каналы в подзатворном диэлектрике. Для этого запускается этап "верификации", который на максимально заявленной для памяти частоте сверяет значения ячеек памяти. Если каналы будут слабые, то при высокой скорости обращения будут возникать сбои, память будет читаться с ошибками, т.е. не будет совпадать со значениями, которые в нее были записаны. Описанная выше процедура составляет один цикл программирования памяти. Гарантированный компанией "Миландр" коэффициент программируемости составляет 0,7. Т.е. при программировании 100 микросхем будут успешно запрограммированы только 70, а 30 микросхем запрограммировать не удастся - будут ошибки. Но это гарантируемый коэффициент, на самом деле процент программируемых микросхем гораздо выше. Каждый производитель оставляет себе некоторый запас.
В случае, если в первом цикле программирования верификация выявила наличие ошибок, то есть возможность запустить повторный цикл. Но во вторичном цикле необходимо исключить операцию программирования. Это связано с тем, что вторичный прожиг уже пробитого бита не желателен. Поэтому второй цикл программирования должен состоять только из этапов допрограммирования и верификации. Производитель не рекомендует использовать более двух циклов программирования, предлагая в случае неудачи второго цикла браковать микроконтроллер.
Стоит отметить, что если в фазе допрограммирования сразу проверять значение только что про прожженного бита, то считать "0" вместо положенного "1" считается нормальным. Дело в том, что для памяти есть настройки, которые задают "стойкость" значений к считыванию. Эти параметры называются репликами и задаются в регистре TUNING блока MDR_OTP. В данном регистре можно задать, какое из значений "0" или "1" будет читаться "легче". Так, например, если задать, чтобы легче читался "0", то чтение "1"-цы будет затруднено. К сожалению, я не обладаю знаниями, чем эти "легче/тяжелее" обусловлены физически, но смысл здесь в том, что для прожига необходимо выставить наибольшее сопротивление к чтению, чтобы записанный бит читался "1"-цей только в том случае, когда уже диэлектрик пробит основательно. Это гарантирует, что при обычных репликах чтения, которые используются на этапе верификации, память будет читаться на максимальной частоте успешно. В итоге, только этап верификации показывает, было ли программирование успешно!
Кроме этого, чип бракуется, если на этапе допрограммирования будет выявлено, что какой-то из битов, который должен быть нулем оказался вдруг "1"-ей. Это может произойти, например, если данные начинают писаться в память, которая уже была записана раннее другими данными. Когда вместо нового "0"-ля, в памяти уже находится старая "1"-ца, то исправить такое положение нельзя. Обратите, пожалуйста, внимание, что Нули в памяти находятся изначально, записываются только "1"-цы!
(Дополнительную информацию можно найти здесь - Программирование 1645РТ3У)
Сравнивая описание этапов программирования и допрограммирования, можно заметить, что они мало отличаются. Собственно, различий всего два:
Если свести описания этапов на одну блок-схему, то получается такая картина:
По схеме видно, что различия крайне незначительны и можно реализовать оба этапа одной функцией. При этом "Различие 2" решается передачей в функцию количества повторов программирования бита - 1 цикл для программирования и 40 для допрограммирования. Остается "различие 1" с присутствием чтения текущего значения при допрограммировании. Но будет ли хуже, если программирование тоже будет читать текущее значение ячейки и писать только, если есть незаписанные 1-цы?
Изначально предполагается, что чип чист, и вся память 0-вая, поэтому нет чтения в программировании. В допрограммировании же важно исключить вторичный прожиг уже пробитого бита, поэтому чтение необходимо. Судя по всему, лишнее чтение ничем этапу программирования не навредит. Репликами чтение 0-ей выставлено максимально легким, т.е. нет шансов, что "0" друг прочтется "1"-ей и будет пропущено программирование какого-либо бита из-за того, что добавилось предварительное чтение. Кроме этого, если по ошибке произойдет запуск программирования для уже записанной памяти, то это исключит повторную запись уже пробитого бита.
В целом, я не вижу причины делать две отдельные функции для этапов программирования и допрограммирования. Достаточно реализовать их в одной функции, задав на вход лишь количество допустимых повторов. В библиотеке Pack_V6 это сделано так:
// Функция программирования слова OTP bool MDR_OTP_ProgWordEx(uint32_t addr, uint32_t data, uint32_t cycleCount); // Программирование слова - 1 цикл __STATIC_INLINE void MDR_OTP_ProgWord(uint32_t addr, uint32_t value) { MDR_OTP_ProgWordEx(addr, value, 1); } // Допрограммирование слова - 40 циклов #define MDR_OTP_REPROG_CNT 40 __STATIC_INLINE void MDR_OTP_RepProgWord(uint32_t addr, uint32_t value) { MDR_OTP_ProgWordEx(addr, value, MDR_OTP_REPROG_CNT); }
Листинг самой функции я приводить не буду, исходники доступны на GitHub, драйвер MDR_OTP - MDR_OTP_VE8x.h и MDR_OTP_VE8x.c .
С помощью драйвера MDR_OTP запись памяти выглядит так, отрывок из проекта описанного ниже:
void OPT_WriteTestData(void) { MDR_OTP_Enable(); // - Выключение доступа к контроллеру OTP, KEY MDR_OTP_ProgBegin_ByClockCPU_Hz(freqCPU_Hz); // - Расчет задержек, выставление минимального DUcc, // задержка HV_PE, выставление реплик в TUNING MDR_OTP_ProgWord(OTP_TEST_PROG_ADDR, OTP_TEST_PROG_DATA); // Программирование слова ... ... ... MDR_OTP_ProgEnd(); // - реплики и DUcc в состояние по умолчанию MDR_OTP_Disable(); // - выключение доступа к контроллеру OTP, KEY }
Внешнее напряжение программирования 7,2 В должно быть задано до исполнения данного кода! Повышенное напряжение необходимо, чтобы пробить диэлектрик, для этих же целей в функции MDR_OTP_ProgBegin() понижается питание самой микросхемы через подстройку внутренних LDO. После модификации LDO выдерживается необходимая задержка между подачей питания HV и входом в режим программирования - бит PE.
Данный пример был реализован, чтобы проверить работоспособность драйвера MDR_OTP. В примере используются все три кнопки на отладочной плате:
Для индикации используется три светодиода:
Слова в проекте пишутся, начиная с конца памяти ОТР, чтобы оставить начальные адреса для рабочей программы. Пишется всего одно слово, на нем проверяется, как отрабатывает запись и чтение. Для записи следующего слова необходимо исправить в коде значение OTP_TEST_TEST_IND, потому что оно задает адрес:
// Увеличивать после каждой записи - сдвиг на чистую память с конца ОТР. // Минимальное значение = 1 #define OTP_TEST_TEST_IND 5 #define OTP_TEST_ADDR_END 0x01020000 #define OTP_TEST_PROG_ADDR (OTP_TEST_ADDR_END - (OTP_TEST_TEST_IND * 4))
В тесте был опробован прожиг от трех источников тактирования, данные, записываемые при каждом прожиге, тоже менялись:
// Значения записываемые/записанные в память в порядке OTP_TEST_TEST_IND // 1: OTP_TEST_TEST_IND = 1 от HSE0_PLL_Max // #define OTP_TEST_PROG_DATA 0x13768421 // 2: OTP_TEST_TEST_IND = 2 от HSE0_PLL_Max // #define OTP_TEST_PROG_DATA 0x87245687 // 3: OTP_TEST_TEST_IND = 3 от HSI_Trim (задержки точные) // #define OTP_TEST_PROG_DATA 0xABCDEFED // 4: OTP_TEST_TEST_IND = 4 от HSI без Trim (задержки не точные) // #define OTP_TEST_PROG_DATA 0x12345678 // 5: OTP_TEST_TEST_IND = 5 от HSE1 Gen 25MHz #define OTP_TEST_PROG_DATA 0x9ABCDEF
В сущности не важно, на какой частоте происходит прошивка, все варианты отработали с первого раза - с первого нажатия на кнопку Key1. Важно правильно выдержать необходимые задержки в циклах программирования! Рекомендуемая частота прошивки ~24МГц необходима лишь для проведения этапа верификации. Ведь читать память необходимо на ее максимальной рабочей частоте. В данном примере это возможно только в варианте тактирования от внешнего генератора.
Необходимые задержки указаны в спецификации на стр. 215, таблица 36:
Название | Имя | Мин значение | Кол. тактов | |||
---|---|---|---|---|---|---|
мкс | 8 МГц | 25 МГц | 40 МГц | 64 МГц | ||
Режим записи - бит PE | ||||||
Предустановка HV перед PE | t_HV_PE | 10000 | 80000 | 250000 | 400000 | 640000 |
Предустановка PE перед DATA | t_PE_D | 300 | 2400 | 7500 | 12000 | 19200 |
Предустановка ADR перед DATA | t_A_D | 300 | 2400 | 7500 | 12000 | 19200 |
Удержание ADR после DATA | t_D_A | 5 | 40 | 125 | 200 | 320 |
Время программирования | t_Prog | 3000 | 24000 | 75000 | 120000 | 192000 |
Задержка между битами | t_LD | 5 | 40 | 125 | 200 | 320 |
Удержание PE после DATA | t_D_PE | 0 | 0 | 0 | 0 | 0 |
Режим чтения - бит SE | ||||||
Предустановка ADR перед SE | t_A_SE | 0.005 | 0,04 | 0,125 | 0,2 | 0,32 |
Удержание ADR после SE | t_SE_A | 0 | 0 | 0 | 0 | 0 |
Длительность HSE | t_SE | 0.01 | 0,08 | 0,25 | 0,4 | 0,64 |
По таблице видно, что для всех допустимых частот ядра достаточно лишь четырех задержек - на 10мс, 3мс, 300мкс и 5мкс. Остальные задержки составляют меньше чем один такт ядра, поэтому заботиться об их выдержке нет необходимости.
Задержка 5мкс между записью нескольких бит подряд не пригодилась. Дело в том, что на блок-схемах в спецификации описан режим программирования, в котором каждый бит записывается и тут же читается 5 раз, чтобы убедиться, что он прошит. Если бит не прошит, то он снова прожигается, вплоть до 30 раз. На диаграмме же сигналов в спецификации "Рисунок 57 – Временная диаграмма процесса программирования OTP памяти" представлен режим, в котором биты записываются подряд, а затем видимо целиком происходит чтение всего слова. По тексту спецификации подобный режим не описан, но реализован у Vasili с форума в его программе прошивки OTP по UART - PRG_OTP_UART.
Драйвер MDR_OTP был реализован согласно блок схем из спецификации и примера полученного от тех-поддержки. Диаграмма задержек при записи одного бита получается такая:
На диаграмме есть не упоминающиеся в спецификации задержки, это:
Для расчета задержек в функцию MDR_OTP_ProgBegin_ByClockCPU_Hz() передается текущая частота процессора, которая определяется функцией MDR_CPU_GetFreqHz(). Важно лишь в MDR_ConfigVE8.h прописать правильные частоты для подключенных внешних HSE0 и HSE1.
В качестве альтернативы, расчет задержек можно сделать отдельной функцией MDR_OTP_GetProgDelays() и получить из нее структуру с задержками, которая затем подается на вход MDR_OTP_ProgBegin(). Все тоже самое делает инлайн функция MDR_OTP_ProgBegin_ByClockCPU_Hz(), чтобы не писать это отдельно.
#define MDR_OTP_DELAY_US_HV_PE 10000 #define MDR_OTP_DELAY_US_A_D 300 #define MDR_OTP_DELAY_US_PROG 3000 typedef struct { uint32_t delay_HV_PE; uint32_t delay_A_D; uint32_t delay_Prog; } MDR_OTP_ProgDelays; // Вычисление задержек MDR_OTP_ProgDelays MDR_OTP_GetProgDelays(uint32_t freqCPU_Hz); // Инициализация программирования - снижение DUcc, выставление Tuning void MDR_OTP_ProgBegin(const MDR_OTP_ProgDelays *progDelays); // Все описанное вместе __STATIC_INLINE void MDR_OTP_ProgBegin_ByClockCPU_Hz(uint32_t freqCPU_Hz) { MDR_OTP_ProgDelays delays = MDR_OTP_GetProgDelays(freqCPU_Hz); MDR_OTP_ProgBegin(&delays); }
Задержки рассчитываются под функцию MDR_Delay(), реализация которой была описана ранее в данной статье - "Функция задержки и особенности её реализации в ассемблере".
Значения в таблице определяют минимальные величины задержек, следовательно, их важно не сделать короче. Самая важная задержка - это время программирования. Она единственная, для которой определено максимальное значение. Это необходимо для того, чтобы ограничить время воздействия на ячейку повышенного напряжения. Иначе, вероятно, может случиться деградация структур в топологии, тогда могут пострадать и соседние ячейки. Другие же задержки лишь означают, что следующий по диаграмме сигнал необходимо выставить не раньше, чем к этому будет готов контроллер памяти.
Для того чтобы проверить, как отрабатываются задержки данной функцией в проекте, используется кнопка KEY3. При нажатии на данную кнопку, на светодиоде LED3 начинают переключатся уровни напряжений с заданными задержками. Вот, например, какие возникают задержки при тактировании от HSI со значением Trim по умолчанию:
Как видно, данные задержки превышают минимальные значения, т.е. по-существу, являются допустимыми. Время программирования t_Prog по спецификации составляет от 3 до 7мс. Что тоже не нарушается при таких задержках. Поэтому при одном из запусков проекта прошивались ячейки при тактировании от HSI без подстройки. Для полного соответствия проводилась прошивка и с подстроенным HSI, задержки тут имеют более точные значения:
После вторичного нажатия на кнопку KEY3 режим вывода задержек отключается.
Для запуска проекта к демо-плате необходимо подключить источник питания на 7,2В - это середина из разрешенного диапазона 7,0-7,4В. В нашем случае это был источник, непосредственная подача питания от которого осуществляется по нажатию кнопки. Согласно порядку подачи напряжений, сначала необходимо запитать плату от рабочего источника, а уже затем, когда дело дойдет до прошивки памяти, подать на плату напряжение программирования. Порядок снятия напряжений обратный - сначала необходимо отключить источник на 7,2В, потом выключить плату.
Для подачи напряжения программирования на плате есть специальный разъем HV:
Перед прошивкой значений в память, для уверенности, можно проверить как отрабатывают в функциях такие операции как установление минимального питания DUcc и выдержка задержек.
Прежде чем подать на плату повышенное напряжение, можно убедиться, что функция MDR_OTP_ProgBegin_ByClockCPU_Hz() действительно понижает напряжение DUcc. Для этого необходимо (см. код ниже):
void OPT_WriteTestData(void) { MDR_OTP_Enable(); // Breakpoint сюда, чтобы увидеть начальный уровень DUcc MDR_OTP_ProgBegin_ByClockCPU_Hz(freqCPU_Hz); // MDR_OTP_ProgWord(OTP_TEST_PROG_ADDR, OTP_TEST_PROG_DATA); - отключаем программирование MDR_OTP_ProgEnd(); // Breakpoint сюда, чтобы увидеть снижение DUcc MDR_OTP_Disable(); }
Код библиотеки может меняться, могут поменяться функции настройки частоты, или компилятор сформирует прошивку несколько по другому. Все это может привести к тому, что данный проект перестанет отрабатывать так, как он работает сейчас, при написании данной статьи. Поэтому для каждого источника тактирования следует проверить, что задержки формируются правильно!
В проекте проверяется прошивка при тактировании от источников:
Выбор частоты для которой собирается проект делается условной компиляцией. Необходимо чтобы был выбран только один вариант из:
#define OTP_PROG_WITH_HSE0_MAX_CLOCK 1 #define OTP_PROG_WITH_HSI 0 #define OTP_PROG_WITH_HSE1_GEN_25MHZ 0
Библиотека должна знать, какие частоты подключены на HSE0 и HSE1, ведь снаружи могут быть подключены источники и с другими частотами. Значения внешних частот должны быть указаны в файле MDR_ConfigVE8.h. Это необходимо чтобы задержки из реальных величин времени пересчитывались в циклы задержки функции MDR_Delay() правильно - MDR_Delay - Функция задержки и особенности её реализации в ассемблере.
MDR_ConfigVE8.h //================ Параметры источников частоты ================ // Internal Generators #define HSI_FREQ_HZ 8000000UL // External Generators #define HSE0_FREQ_HZ 10000000UL ... #define HSE1_FREQ_HZ 25000000UL ...
Чтобы измерить задержки:
Если задержки получились неправильные необходимо проверить правильно ли указана частота в MDR_ConfigVE8.h и такая ли частота подана снаружи. Если все правильно, то необходимо проверить пересчет задержек для MDR_Delay() и значения вроде DELAY_LOOP_CYCLES_ASM. Можно перейти на вариант задержек через DWT - см. MDR_Delay - Функция задержки и особенности её реализации в ассемблере.
Если задержки правильные, то можно переходить в программированию ячейки ОТР.
Для прошивки ячейки памяти в проекте предлагается такой алгоритм действий:
При запусках проекта мы не столкнулись с ошибками при записи, память прописывалась с первого раза, на какой бы частоте мы ее не прописывали. Но в данном проекте нет как такового этапа верификации. Память здесь читается через регистровый доступ чтобы не было выхода в HardFault из-за ошибок ЕСС, при этом она фактически читается не при 25МГц и не через необходимые шины. Поэтому в данном примере качество "прожига" оценить нельзя.
Для прошивки следующего слова необходимо увеличить OTP_TEST_TEST_IND и можно поменять слово данных для разнообразия.
Не стоит пытаться дописать новые биты "1" в эту же ячейку памяти, потому что для нового значения слова изменится и ЕСС, для которого скорее всего потребуется изменить уже прописанный "0" на "1", что невозможно. Но с другой стороны, если дописываемый бит будет всего один, то это даст сбой по единичной ошибке ЕСС, которая будет аппаратно парирована в случае чтения данного слова по шине. Это можно использовать для проверки того, как отрабатывает контроллер ошибок FT_CNTRL, как генерируются события ошибки, прерывания и прочее.
Запуск проекта показал, что:
Следующим шагом будет написать на основе функций драйвера MDR_OTP несколько FLM и прошить какой-нибудь проект, тоже куда-нибудь в конец памяти - статья, "OTP_FLM_VE8 - Реализация FLM для OTP в 1986ВЕ8Т"
Ссылка на проект - GitHub