======Внешняя шина с последовательным ECC в 1986ВЕ8Т====== По отзывам службы техподдержки возникает много вопросов о том, как проинициализировать внешнюю память, чтобы не возникали ошибки ECC. Рассмотрим эту тему на примере включения микроконтроллера 1986ВЕ8Т с микросхемой памяти (асинхронного статического ОЗУ) [[http://ic.milandr.ru/products/mikroskhemy_pamyati/operativnye_zapominayushchie_ustroystva/1645ru5u/|1645RU5U]] на штатной отладочной плате. //(Проект по статье доступен на [[https://github.com/StartMilandr/4.1-ExtBus_VE8/tree/master|GitHub]])// В проекте используется три кнопки, при нажатии на которые происходит: - **Key1**: Портится согласованное состояния данных и ECC. Выключается режим ECC и вся память (область данных и область значений ЕСС) прописывается индексами. - **Key2**: В режиме с ECC инициализируется память данных, область ЕСС при этом инициализируется автоматически. Последующее считывание показывает верность данных и отсутствие увеличений счетчиков ошибок. - **Key3**: В режиме без ECC вносится одинарная и двойная ошибка. Затем в режиме с ECC проверяется, что одинарная ошибка исправляется при чтении автоматически, при этом увеличивается счетчик одинарных ошибок. Двойная ошибка не может быть исправлена. После этого обе ошибки перезаписываются в режиме с ЕСС записью 32-битных значений. Успешность операций выводится на светодиоды, которые выключаются перед запуском операции. По окончании операции светодиоды загораются если: - **VD7**: Загорается всегда - исполнение закончено. - **VD8**: Тест по //Key2// или //Key3// прошли успешно. - **VD9**: Рассогласование данных по //Key1// выполнено. =====Начальная информация===== Микроконтроллер 1986ВЕ8Т реализован для эксплуатации при жестких внешних условиях, поэтому вместо flash памяти внутри реализована однократно программируемая (прожигаемая) память. Биты каждого слова максимально разнесены по площади кристалла так, что при точечном внешнем воздействии пострадает не все слово, а только часть бит различных слов. Введение кодирования по Хэммингу делает возможным в таком случае восстановить единичные сбои, что позволяет сохранить работоспособность микроконтроллера. Этот же подход позволяет обнаруживать ошибки при сбоях или наводках на шинах и прочих компонентах микросхемы. Для возможности восстановления слова данных, для каждой комбинации адреса и данных по этому адресу высчитывается специальное слово - ECC. Для вычисления ЕСС необходим адрес 32-бита и данные 32-бита. В итоге получается 8 битное значение ECC. По значению ECC можно узнать какой бит в данных или адресе окажется ложным в случае одиночной ошибки. Если ложными окажутся несколько битов, то это тоже определяется по ECC. При работе по внешней шине ЕСС может располагаться как последовательно вместе с данными, так и в отдельной области памяти. В нашем примере реализуем вариант, когда выделяется отдельная область для хранения ECC. Сами же данные будут лежать друг за другом, как это происходит в обычном режиме. Важно запомнить, что ECC высчитывается только для 32-битного слова данных. Т.е. если потребуется записать только один байт по какому-то адресу, то это произойдет в два этапа: * Считывание данных - По 32-битному адресу будет считано 32-х разрядное слово - Аппаратно посчитается ECC для этой комбинации адреса и данных, которое назвем ECC_right. - Затем будет считано 8-ми битное ECC - Если ECC не равно ECC_right, то увеличатся счетчики ошибок в регистре EXT_BUS_CNTR->RGNn_ECCS - одинарные или двойные. - Если одинарная ошибка в данных, то верные данные будут восстановлены. Если ошибка двойная, то восстановление данных не возможно. Программист должен написать обработчик на такой случай. - Если какая-либо ошибка обнаружена в адресе, то восстановление адреса смысла не имеет, поскольку нас интересуют только данные. * Запись данных - Вот только теперь процессор запишет байт в считанное и, возможно, восстановленное слово. - Высчитает новое ECC для комбинации адреса и новых данных. - Данные будут записаны по указанному адресу, а ECC записано в свою область памяти, отведенную для ECC. Но в случае, если писать на шину сразу 32-х битные данные, то для них не требуется считывание, поскольку новое ECC может быть высчитано сразу. Этим необходимо пользоваться для того, чтобы проинициализировать начальное состояние внешней памяти. То есть, при записи всего объема памяти будут записаны все данные и соответствующие им верные ЕСС. После этого с памятью можно работать в любом режиме - считывать/писать байты, 16-разрядные слова или 32-битные значения. Ошибки возникать не будут и счетчики ошибок перестанут расти. Если бы память не была проинициализирована, то при считывании постоянно возвращались бы несогласующиеся с ECC данные, генерились бы флаги ошибок и данные пытались восстанавливаться. В независимости от разрядности шины данных, начальная инициализация памяти в режиме с ECC должна производиться **32-х разрядными данными**! Контроллер шины сам разобъет эти 32-битные записи на необходимые транзакции с разрядностью шины данных (серия записей по 8, 16 или 32 бита). В проекте это выглядит так: // Функция записи 32-х разрядными значениями индекса void Fill_Data32_ByInd(uint32_t starAddr, uint32_t count) { uint32_t* addr; uint32_t i = 0; addr = (uint32_t*)starAddr; for (i = 0; i < count; ++i) *addr++ = i; } #define RGN0_StartAddr 0x10000000 // Начало региона данных с контролем ECC #define RGN0_EccAddr 0x10030000 // Начало области хранения значений ECC #define ECC_NUM_BYTES 0x30000 // Количество данных в регионе в байтах #define ECC_NUM_WORDS (ECC_NUM_BYTES >> 2) // Количество данных в 32-битных значениях int main(void) { ... // Вызов в main, инициализация памяти по нажатию на кнопку Key2. Fill_Data32_ByInd(RGN0_StartAddr, ECC_NUM_WORDS); Fill_Data32_ByInd(RGN0_StartAddr, ECC_NUM_WORDS); // Вторичная инициализация ... } Обнаружилось, что после первой инициализации данных, счетчик ошибок увеличивается, если начинать работать с данными. При втором проходе счетчик спонтанно увеличивается на +1, после этого увеличений больше не происходит. Наверное это какой-то баг в микроконтроллере и он будет исправлен в следующих ревизиях. Для устранения роста счетчика ошибок требуется **двойная инициализация** памяти в режиме с ECC! =====Включение микроконтроллера и настройка выводов===== От того как произведено включение микроконтроллера и микросхем памяти зависит код настройки выводов и контроллера шины. На демо-плате включение выполнено так: {{prog:extbus:ve8_extbus.png}} По рисунку видно, какие выводы микроконтроллера в какие функции должны быть настроены. Код настройки выводов GPIO таков: #define UNLOCK_KEY 0x8555AAA1 void ExtBus_InitPins_A19_D8(void) { PORT_InitTypeDef PORT_InitStructure; CLKCTRL_PER0_CLKcmd(CLKCTRL_PER0_CLK_MDR_PORTC_EN, ENABLE); CLKCTRL_PER0_CLKcmd(CLKCTRL_PER0_CLK_MDR_PORTD_EN, ENABLE); CLKCTRL_PER0_CLKcmd(CLKCTRL_PER0_CLK_MDR_PORTE_EN, ENABLE); PORTC->KEY = UNLOCK_KEY; PORTD->KEY = UNLOCK_KEY; PORTE->KEY = UNLOCK_KEY; PORT_StructInit(&PORT_InitStructure); // DATA BUS // Data[0..1] PORT_InitStructure.PORT_Pin = (PORT_Pin_30|PORT_Pin_31); PORT_InitStructure.PORT_SFUNC = PORT_SFUNC_2; PORT_InitStructure.PORT_SANALOG = PORT_SANALOG_DIGITAL; PORT_InitStructure.PORT_SPWR = PORT_SPWR_10; PORT_Init(PORTD, &PORT_InitStructure); // Data[2..7] PORT_InitStructure.PORT_Pin = (PORT_Pin_0|PORT_Pin_1|PORT_Pin_2|PORT_Pin_3|PORT_Pin_4|PORT_Pin_5); PORT_InitStructure.PORT_SFUNC = PORT_SFUNC_2; PORT_InitStructure.PORT_SANALOG = PORT_SANALOG_DIGITAL; PORT_InitStructure.PORT_SPWR = PORT_SPWR_10; PORT_Init(PORTE, &PORT_InitStructure); // ADDR BUS // Addr[0..1] PORT_InitStructure.PORT_Pin = (PORT_Pin_30|PORT_Pin_31); PORT_InitStructure.PORT_SFUNC = PORT_SFUNC_2; PORT_InitStructure.PORT_SANALOG = PORT_SANALOG_DIGITAL; PORT_InitStructure.PORT_SPWR = PORT_SPWR_10; PORT_Init(PORTC, &PORT_InitStructure); // Addr[2..18] PORT_InitStructure.PORT_Pin = ( PORT_Pin_0 |PORT_Pin_1 |PORT_Pin_2 |PORT_Pin_3 | PORT_Pin_4 |PORT_Pin_5 |PORT_Pin_6 |PORT_Pin_7 | PORT_Pin_8 |PORT_Pin_9 |PORT_Pin_10|PORT_Pin_11| PORT_Pin_12|PORT_Pin_13|PORT_Pin_14|PORT_Pin_15|PORT_Pin_16 ); PORT_InitStructure.PORT_SFUNC = PORT_SFUNC_2; PORT_InitStructure.PORT_SANALOG = PORT_SANALOG_DIGITAL; PORT_InitStructure.PORT_SPWR = PORT_SPWR_10; PORT_Init(PORTD, &PORT_InitStructure); // CTRL BUS // PD19 = nCS, PD23 = nOE, PD24 = nWE PORT_InitStructure.PORT_Pin = (PORT_Pin_19|PORT_Pin_23|PORT_Pin_24); PORT_InitStructure.PORT_SFUNC = PORT_SFUNC_2; PORT_InitStructure.PORT_SANALOG = PORT_SANALOG_DIGITAL; PORT_InitStructure.PORT_SPWR = PORT_SPWR_10; PORT_Init(PORTD, &PORT_InitStructure); }; Как всегда сначала включается тактирование портов. Затем происходит разблокировка портов, необходимо записать определенное значение в регистры PORTх->KEY. В этом особенность данного МК: без записи ключа настройка портов не сработает. Далее настраиваются необходимые выводы, участвующие в работе шины. Параметры инициализирующей структуры тут также несколько другие, чем в прочих микроконтроллерах, про которые мы писали ранее. Здесь особенность в том, что параметры, начинающиеся на **//S//** обозначают слово //Set//. Параметры, начинающиеся с **//C//** обозначают //Clear//. То есть управление задается масками - //Set// параметры устанавливают единицы, а параметры //Clear// их стирают. Например, при одной и той же маске, выражение //PORT_SFUNC = 0х2// приведет к установлению 1-го бита в необходимом регистре, а //PORT_СFUNC = 0х2// сбросит этот бит. =====Настройка шины===== После настройки выводов настраиваются параметры самой шины: #define RGN_WS_TIME 4 void ExtBus_Init_RGN0_D8(FunctionalState Ecc_EN, uint32_t baseECC) { EBC_RGN_InitTypeDef EBC_RGNx_IS; CLKCTRL_PER0_CLKcmd(CLKCTRL_PER0_CLK_MDR_EBC_EN, ENABLE); EXT_BUS_CNTR->KEY = UNLOCK_KEY; EXT_BUS_CNTR->RGN0_ECCBASE = baseECC; // 0x10030000; EXT_BUS_CNTR->RGN0_ECCS |= (3 << 4); // set FIX_SECC and FIX_DECC bit EBC_RGNx_StructInit(&EBC_RGNx_IS); //EBC_RGNx_IS.RGN_DIVOCLK = RGN_WS_TIME; EBC_RGNx_IS.RGN_WS_HOLD = RGN_WS_TIME; EBC_RGNx_IS.RGN_WS_SETUP = RGN_WS_TIME; EBC_RGNx_IS.RGN_WS_ACTIVE = RGN_WS_TIME; EBC_RGNx_IS.RGN_MODE = 2; //EBC_MODE_8X; EBC_RGNx_IS.RGN_ECCEN = Ecc_EN; if (Ecc_EN) { EBC_RGNx_IS.RGN_ECCMODE = ENABLE; EBC_RGNx_IS.RGN_READ32 = ENABLE; } EBC_RGNx_Init(RGN0, &EBC_RGNx_IS); EBC_RGNx_Cmd(RGN0, ENABLE); } Сначала включается тактирование, затем производится разблокировка. На внешней шине выделено 8 регионов адресов, для каждого региона есть возможность задать различные настройки. Мы будем работать с первым регионом (RGN0), его адреса составляют диапазон 0x1000_0000 - 0x17FF_FFFF. Оставим под данные адреса с 0x1000_0000 по 0x1002_FFFF, а с адреса 0x1003_0000 начнется область хранения значений ECC для этих данных (RGN0_ECCBASE = baseECC). Для того чтобы в регистрах ECCADR, ECCDATA, ECCECC отображалась информация о зафиксированной одинарной или двойной ошибке необходимо выставить биты FIX_SECC или FIX_DECC в регистре RGN0_ECCS. В регистре ECCADR будет содержаться адрес последней обнаруженной ошибки, в ECCDATA - считанные данные до исправления, в ECCECC - считанное значение ECC. Значения этих регистров помогают в отладке, позволяют понять, что реально было считано с шины. Временные параметры должны рассчитываться от скорости работы ядра, но здесь значения выбраны случайно (RGN_WS_TIME = 4). Поскольку задача достижения максимальной скорости работы шины не стоит. Значение RGN_MODE = 2 определяет режим работы - восьмибитная шина данных. В SPL я не нашел строкового определения этого значения. Есть только маска EBC_MODE_8X, но это уже значение для записи в регистр и для передачи в структуру не подходит. Значения этого поля могут быть такими, RGN_MODE: * 0 - 32 битная шина данных * 1 - 16 битная шина данных * 2 - 8 битная шина данных * 3 - 64 битная шина данных Контроль ECC возможен, только если разрешена работа 32-х битными значениями по шине, поэтому параметр RGN_READ32 должен быть включен. Если этот параметр выключен, то при работе по шине значение ECC формироваться не будет. =====Пояснения по тесту Key1===== Думаю, что код программы достаточно понятен, я опущу описание настройки тактирования, кнопок, светодиодов и прочего. Далее лишь некоторые пояснения по коду. По кнопке Key2 мы тестируем инициализацию внешней памяти, следовательно, нам необходима процедура, которая приводит эту память к рассогласованному с ECC состоянию. Состоянию, аналогичному тому, что образуется при включении питания внешнего ОЗУ. Ведь если мы один раз проинициализировали память по Key2, то последующие запуски Key2 будут работать с уже согласованной памятью, и смысла в тесте Key2 нет. Поэтому перед запуском теста по Key2 необходимо всегда запускать процедуру по Key1. При нажатии на Key1 временно выключается режим работы шины с ЕСС, а вся память прописывается индексами. Но можно забить ее и нулями или любыми случайными данными, это не важно. В процедуре по Key1 мы "портим" обе области - область данных и область ECC - процедура FillData_NoECC(). На время исполнения процедуры гаснут все светодиоды, по окончании загораются VD7 //(Completed)// и VD9 //(Память "испорчена")//. =====Пояснения по тесту Key2===== При нажатии на Key2 сначала вызывается дважды функция инициализации внешней памяти Fill_Data32_ByInd(RGN0_StartAddr, ECC_NUM_WORDS). Почему дважды - описано вначале статьи, видимо это баг в микроконтроллере. Возможно позднее появится более эффективное решение, чем двойная запись. Затем запускается функция TestRD_ECC(), которая считывает записанные значения, сверяет данные и проверяет, что счетчик ошибок не изменился. uint32_t TestRD_ECC(void) { uint32_t* addr; uint32_t i = 0; uint32_t rdValue, errCnt; uint32_t eccErrStart = EXT_BUS_CNTR->RGN0_ECCS; // Read and Check Data addr = (uint32_t*)RGN0_StartAddr; errCnt = 0; for (i = 0; i < ECC_NUM_WORDS; ++i) { rdValue = *addr++; if(rdValue != i) errCnt++; } return (errCnt == 0) && (EXT_BUS_CNTR->RGN0_ECCS == eccErrStart); } При запуске теста гаснут все светодиоды, по окончании теста загорается VD7 (Completed). VD8 загорается в случае, если тест TestRD_ECC() прошел успешно. =====Пояснения по тесту Key3===== По нажатию на кнопку Key3 запускается функция ErrorTest(), в которой сначала записываются одинарная и двойная ошибки. Это происходит в функции Write_Errors(), которая временно отключает ECC. Затем запускаются тесты на каждую ошибку TestRD_Err1() и TestRD_Err2(). Все, что имеет в названии Err1 относится к одинарной ошибке, а Err2 - соответственно к двойной. Эти тесты считывают регион адресов вокруг адреса внесенной ошибки, находят ее и проверяют счетчики ошибок. После этого ошибки затираются правильными значениям, и запускается функция TestRD_ECC() - тест чтения памяти, который уже использовался в Key2. Этот тест показывает, что память полностью согласована с ECC и ошибок не содержит. Индикация на светодиодах в тесте по Key3 поэтому такая-же, как и в тесте по Key2. uint32_t ErrorTest(void) { uint32_t val; // Write Errors Write_Errors(); // Check single error fixed if (!TestRD_Err1()) return 0; // Check double error found if (!TestRD_Err2()) return 0; // OverWrite Single Error Data - Double Write val = ADDR(RGN0_Addr_Err1); ADDR(RGN0_Addr_Err1) = val; ADDR(RGN0_Addr_Err1) = val; // Restore Double Error ADDR(RGN0_Addr_Err2) = Err2_ValueSrc; ADDR(RGN0_Addr_Err2) = Err2_ValueSrc; // Test Read all Memory without RGN0_ECCS increment return TestRD_ECC(); } // Внесение ошибок инвертированием одного и двух бит в разных словах void Write_Errors(void) { // ECC Off ExtBus_Init_RGN0_D8(DISABLE, RGN0_EccAddr); // Single Error Err1_ValueSrc = ADDR(RGN0_Addr_Err1); Err1_Value = Err1_ValueSrc ^ 1; ADDR(RGN0_Addr_Err1) = Err1_Value; // Double Error Err2_ValueSrc = ADDR(RGN0_Addr_Err2); Err2_Value = Err2_ValueSrc ^ 3; ADDR(RGN0_Addr_Err2) = Err2_Value; // Restore ECC ON ExtBus_Init_RGN0_D8(ENABLE, RGN0_EccAddr); } //Реализация отключения ECC с помощью функции ExtBus_Init_RGN0_D8 избыточна, можно использовать что-то более легковесное. Но для сокращения кода примера я ее оставил.// Обратите внимание, что перезапись битых слов делается также дважды. При этом одинарную ошибку можно перезаписать считанным словом, поскольку верное значение восстановилось по ЕСС при чтении. Двойную ошибку так исправить нельзя, необходимо знать исходные данные, чтобы восстановить значение. ====Тест одинарной ошибки==== Тест одинарной ошибки реализован в функции TestRD_Err1(). Адрес ошибки RGN0_Addr_Err1 мы выбрали рядом с началом области данных, поэтому производим чтение данных, начиная со стартового адреса и количеством ERR_AREA_SIZE чтений. При этом в цикле чтения ошибочных данных не обнаруживается (errCnt == 0), но счетчик ошибок инкрементируется (значение Err1Cnt > 0), поскольку одиночная ошибка есть, но значение было восстановлено. Двойных ошибок не обнаруживается (Err2Cnt == 0), а регистры ECC_ADDR, ECC_DATA, ECC_ECC возвращают ожидаемые данные по ошибке. #define RGN0_StartAddr 0x10000000 #define RGN0_Addr_Err1 0x10000010 #define ERR_AREA_SIZE 20 uint32_t TestRD_Err1(void) { uint32_t* addr; uint32_t i = 0; uint32_t rdValue, errCnt; uint32_t eccErrStart; uint32_t Err1Cnt, Err2Cnt, eccLogOK; // Начальное значение регистра ошибок eccErrStart = EXT_BUS_CNTR->RGN0_ECCS; // Чтение и проверка данных addr = (uint32_t*)RGN0_StartAddr; errCnt = 0; for (i = 0; i < ERR_AREA_SIZE; ++i) { rdValue = *addr++; if(rdValue != i) errCnt++; } // Проверка регистров локализации ошибки LogEccRegs(); eccLogOK = (regECC_ADDR == RGN0_Addr_Err1) && (regECC_DATA == Err1_Value) && (GetECC(regECC_ADDR, Err1_ValueSrc) == regECC_ECC); // Счетчики ошибок Err1Cnt = (EXT_BUS_CNTR->RGN0_ECCS >> 16) - (eccErrStart >> 16); Err2Cnt = ((EXT_BUS_CNTR->RGN0_ECCS >> 8) & 0xFF) - ((eccErrStart >> 8) & 0xFF); // Результат return (errCnt == 0) && (Err1Cnt < 2) && (Err2Cnt == 0) && eccLogOK; } Для вычисления значения ЕСС я воспользовался модифицированной функцией с форума, она выполняется быстрее. Но можно пользоваться и функцией из спецификации. const unsigned long long H[8] = { (unsigned long long) 0x0738C808099264FF, (unsigned long long) 0x38C808099264FF07, (unsigned long long) 0xC808099264FF0738, (unsigned long long) 0x08099264FF0738C8, (unsigned long long) 0x099264FF0738C808, (unsigned long long) 0x9264FF0738C80809, (unsigned long long) 0x64FF0738C8080992, (unsigned long long) 0xFF0738C808099264 }; // Модифицированная функция вычисления ЕСС с форума unsigned int GetECC(unsigned int data, unsigned int adr) { unsigned int* ptr_H; int i, j; unsigned int res; unsigned int ecc; unsigned int datai; unsigned int adri; ecc =0; ptr_H = (unsigned int*)(&H); for (i=0; i<8; i++) { datai = *ptr_H; ptr_H++; adri = *ptr_H; ptr_H++; datai &= data; adri &= adr; res = 0; for (j=0; j < 32; j++) { res ^= adri >> j; res ^= datai >> j; } res &= 0x1; res <<= i; ecc |= res; } return ecc; } ====Тест двойной ошибки==== Тест двойной ошибки в функции TestRD_Err2() аналогичен предыдущему тесту. Разница лишь в том, что здесь в цикле обнаружится одно неправильное значение при чтении. Ведь двойные ошибки не восстанавливаются, поэтому errCnt == 1. Счетчик двойных ошибок ожидаемо увеличился Err2Cnt > 0. Но увеличился также и счетчик одинарных ошибок Err1Cnt > 0, не понятно почему. Регистры ECC_ADDR, ECC_DATA, ECC_ECC снова возвращают ожидаемые данные по ошибке. Адрес двойной ошибки я выбрал вдалеке от адреса одинарной, поэтому необходимо высчитать стартовый адрес для опроса региона с ошибкой. Так же необходимо высчитать значение в стартовой ячейке i_offs, чтобы от него инкрементировать значения по i. Но это значение i_offs можно было бы и не высчитывать, а считать напрямую из стартового адреса, ведь мы знаем, что в данном адресе ошибки нет и значение вернется верное. #define RGN0_Addr_Err2 0x10010000 #define ERR_AREA_SIZE 20 uint32_t TestRD_Err2(void) { uint32_t* addr; uint32_t i = 0; uint32_t rdValue, i_offs, errCnt; uint32_t eccErrStart; uint32_t Err1Cnt, Err2Cnt, eccLogOK; // Начальное значение регистра ошибок eccErrStart = EXT_BUS_CNTR->RGN0_ECCS; // Вычисления стартового адреса чтения региона с ошибкой addr = (uint32_t*)RGN0_Addr_Err2 - ERR_AREA_SIZE / 2; // Значение данных по данному адресу i_offs = (RGN0_Addr_Err2 / 4 - ERR_AREA_SIZE / 2) & 0xFFFFF; errCnt = 0; // Чтение и проверка данных for (i = 0; i < ERR_AREA_SIZE; ++i) { rdValue = *addr++; if(rdValue != (i + i_offs)) errCnt++; } // Проверка регистров локализации ошибки LogEccRegs(); eccLogOK = (regECC_ADDR == RGN0_Addr_Err2) && (regECC_DATA == Err2_Value) && (GetECC(regECC_ADDR, Err2_ValueSrc) == regECC_ECC); // Счетчики ошибок Err1Cnt = (EXT_BUS_CNTR->RGN0_ECCS >> 16) - (eccErrStart >> 16); Err2Cnt = ((EXT_BUS_CNTR->RGN0_ECCS >> 8) & 0xFF) - ((eccErrStart >> 8) & 0xFF); // Результат return (errCnt == 1) && (Err1Cnt < 2) && (Err2Cnt < 2) && eccLogOK; } ====Итого==== На данном примере мы ознакомились с работой внешней шины МК 1986ВЕ8Т в 8-битном режиме. Кроме этого разобрались с работой последовательно организованной ЕСС.