Здравствуйте, уважаемые читатели.
В ходе изучения данной стати вы узнаете: как создавать устройства на базе ардуино, как читать и применять документацию на микросхемы, как принимать стратегические решения в рамках установленной задачи, как работать с ROM памятью, как использовать доступные ресурсы максимально эффективно в установленных рамках, как собирать полезные устройства на макетке и многое другое, приятного чтения.
Задача: есть микросхема памяти ROM 27С512 емкостью 512 бит или 64 Кбайта, в нее нужно прошить готовый дамп размером 16 Кбайт, понадобится нам такой ROM с дампом для сборки ZX Spectrum совместимого компьютера (о нем в следующих статьях), а сегодня рассмотрим создание программатора и процессы чтения и прошивки ROM памяти. Прошить данный ROM нам понадобится скорее всего пару раз в жизни, покупать и ждать программатор смысла нет, особенно когда есть коробочка с ардуинками, цветными проводками и макеткой.

Первым делом изучаем даташит на ROM.

Данный ROM имеет шину адреса 16 бит и шину данных 8 бит и прошивается он при подача напряжения на вывод VPP напряжения +12.5В, установкой необходимого адреса и кратковременной подачей низкого уровня на вывод CE.
Определим количество необходимых выводов для того чтобы прошить 16 Кбайт данных за один раз. Нам понадобиться 14 выводов для шины адреса чтобы адресовать 16 Кбайт (разряды A14 и A15 будем подключать к GND или VCC в зависимости от того какие 16 Кбайт микросхемы ROM мы планируем прошивать), 8 выводов чтобы задать 1 байт данных, 1 вывод CE и 1 вывод VPP и того 26 выводов, смотрим в схему подключения Arduino Nano и понимаем что легко мы не отделаемся (у нее всего 19 портов ввода вывода), также хотелось иметь UART для управления с ПК, следовательно используем то что имеем и берем две Arduino Nano или подобные на чипе ATmega328.

Первая будет задавать только шину адреса 14 разрядов. Работать будет в режиме счетчика, иметь вход счета и вход сброса и выход 14 разрядной шины адреса. Да, такой счетчик можно собрать из двух микросхем К561ИЕ9, которые также нужно заказывать и ждать, либо ехать в ближайший "круглосуточный" радио-магазин, но ардуино у нас есть здесь и сейчас.
Вторая будет задавать 8 бит данных на шине данных, управлять выводами VPP и CE, управлять счетчиком адреса (первой адруинкой) управлять выводами счета и сброса и управляться по UART с ПК. Также в Arduino Nano имеется FLASH память объемом 32 Кбайт, соответственно для упрощения программатора в целом, как программно аппаратного комплекса, дамп размером 16 Кбайт, который мы будем прошивать в ROM, мы будем хранить во FLASH памяти второй ардуино, и это сильно упростит нам жизнь, а именно не потребуется создание приложения на ПК для передачи дампа для прошивки по UART. Эта мера вполне оправданна, так как нам надо прошить одну микросхему ROM один раз.
Начнем с ардуино - счетчика адреса, для первых 8 бит шины адреса (A0 - A7) будем использовать весь порт D (PD0 - PD7 или порты ардуино с 0 по 7), для последующих 6 бит шины адреса (A8 - A13) будем использовать весь порт B (PB0 - PB5 или порты ардуино с 8 по 13 ) он имеет 6 выводов и этого нам достаточно. В итоге мы получим полное соответствие номера разряда шины адреса микросхемы ROM номеру вывода ардуино, будет удобно собирать все на макетке. Вывод 14 (PC0) будет входом сброса, вывод 15 (PC1) будет входом счета. Чтобы не устанавливать лишние компоненты, делаем их подтянутыми к питанию директивой PULL UP для помехоустойчивости и активный уровень будем использовать низкий (при подтяжке вывода к GND будет выполняться действие).
// address counter
//
// A0/PC0- reset
// A1/PC1 - count
// 0-13 - address bus A0-A13
byte count_pin, count_pin_store;
void setup() {
DDRD = 0xFF;
DDRB = 0b00111111;
PORTC = 0b00000011;
}
void loop() {
if(!(PINC & 0b00000001)) {
PORTB = 0x00;
PORTD = 0x00;
} else {
count_pin = PINC & 0b00000010;
if (count_pin != count_pin_store) {
count_pin_store = count_pin;
if(!count_pin) {
if (PORTD == 0xFF) {
PORTD = 0x00;
PORTB++;
} else {
PORTD++;
}
}
}
}
}
Код ардуино - счетчика адреса получился быстрый и простой. В переменной count_pin хранится считанное значение бита 1 порта С, в переменной count_pin_store хранится предидущее значение бита 1 порта С. Текущий адрес будем хранить прямо в портах D и B ардуино. В стартовых настройках порты D и B определяем как выходы, биты 0 и 1 порта C как входы в режиме PULL UP.
В основном цикле проверяем бит 0 порта C если он равен 0 то мы обнуляем данные прямо в портах D и B, в противном случае считываем бит 1 порта C в переменную count_pin и сравниваем его с предидущим значением, которое хранится в переменной count_pin_store, если они не равны, то сохраняем считанное значение в переменную count_pin_store. Далее проверяем считанное значение бита 1 порта C и если оно равно 0 то нам необходимо перейти на следующий адрес. Для этого проверяем значение порта D и если оно уже равно 0xFF (в десятичном виде 255), то при добавлении 1 значение порта D переполнится и обнулится, следовательно прибавляем 1 к порту B, и уже после этого обнуляем значение порта D. Если значение порта D еще не достигло 255 то просто прибавляем к нему 1. Код не является идеалом но работает стабильно и достаточно быстро для поставленных задач. Можно было использовать прерывания, флаги процессора, ассемблерные вставки и довести его до идеала, но нам надо прошить одну микросхему ROM сегодня, а не делать из ардуино идеальный 14-разрядный счетчик.
Вторая ардуино будет заниматься всей основной работой, управлять ардуино-счетчиком адреса, формировать шину данных, управлять выводами микросхемы ROM и общаться с пользователем по UART.
Будет удобно для формирования шины данных разрядностью 8 бит использовать один порт ардуино, но у ардуино всего один порт D который имеет разрядность 8 бит. Порты B и C имеют по 6 бит (если не считать вывод RESET/PC6). В связи с тем, что мы будем использовать UART, то выводы RX и TX (выводы 0 и 1 ардуино) будут заняты, они в свою очередь являются битами 0 и 1 порта D, следовательно порт D для шины данных мы использовать не можем. Для удобства формирования шины данных для младших 4-х бит шины мы будем использовать младшие 4 бита порта B, а для старших 4-х бит шины - младшие 4 бита порта C

int read_data() {
cs_down();
delay(1);
byte l = (PINB & 0b00001111);
byte h = (PINC & 0b00001111);
cs_up();
return l | (h << 4) ;
}
Изучив даташит микросхемы ROM а именно временную диаграмму процедуры чтения реализуем собственно процедуру чтения данных. Для этого опускаем вывод CS микросхемы ROM на время чтения данных, читаем младшие 4 бита с портов B и С в переменные l и h соответственно, собираем из переменных полный бит данных и возвращаем его как результат. Немного забегая вперед вывод OE микросхемы ROM будет использоваться только для прошивки как вывод VPP и на него в момент прошивки будет подаваться +12.5В, в режиме чтения он будет притянут к земле по умолчанию.

int write_verify_data(int data) {
byte l = (data & 0b00001111);
byte h = (data & 0b11110000) >> 4;
DDRB |= 0b00001111;
DDRC |= 0b00001111;
PORTB &= 0b11110000;
PORTC &= 0b11110000;
PORTB |= 0b00001111 & l;
PORTC |= 0b00001111 & h;
delay(1);
vpp_up();
delay(1);
cs_down();
delay(1);
cs_up();
delay(1);
vpp_down();
delay(1);
PORTB &= 0b11110000;
PORTC &= 0b11110000;
DDRB &= 0b11110000;
DDRC &= 0b11110000;
return read_data();
}
Изучив временную диаграмму процедуры записи становится ясно, как нужно управлять выводами CS и VPP. Логика получается следующая: формируем адрес и данные на соответствующих шинах, подаем +12.5В на вывод VPP и опускаем вывод CS, запись произошла, далее в обратном порядке поднимаем вывод CS и отключаем напряжение +12.5В на выводе VPP, после записи производим проверочное чтение данных по уже установленному адресу и возвращаем считанные данные в качестве результата.
Формирование данных на шине данных будет происходить следующим образом, разбиваем 8 бит данных на 4 младшие и старшие бита сохраняя их в соответствующие переменные l и h, перенастраиваем 4 младшие бита портов B и C в режим вывод и записываем в них по 4 младшие бита из переменных соответственно, в 4 младшие бита порта B записываем 4 младшие бита из переменной l и в 4 младшие бита порта C записываем 4 младшие бита из переменной h. Данные на шине данных сформированы. После произведения записи обратно перенастраиваем 4 младшие бита портов B и C в режим входа.
void cs_up() {
PORTD |= 0b10000000;
}
void cs_down() {
PORTD &= 0b01111111;
}
void vpp_up() {
PORTD |= 0b00010000;
}
void vpp_down() {
PORTD &= 0b11101111;
}
Для управления выводами CE и VPP, просто задаем соответствующие биты порта D
void addr_count() {
addr++;
PORTD &= 0b10111111;
delayMicroseconds(10);
PORTD |= 0b01000000;
delay(1);
}
void addr_reset() {
addr = 0;
PORTD &= 0b11011111;
delayMicroseconds(10);
PORTD |= 0b00100000;
delay(100);
}
Процедуры управления адреса изменяют соответствующий бит порта D на время, достаточное для отработки команды ардуино-счетчика адреса и соответственно меняют значение переменной addr, в ней хранится текущий адрес, он нам понадобится как для обращения к дампу так и для вывода его в консоль.
void cs_up() {
PORTD |= 0b10000000;
}
void cs_down() {
PORTD &= 0b01111111;
}
void vpp_up() {
PORTD |= 0b00010000;
}
void vpp_down() {
PORTD &= 0b11101111;
}
Для управления выводами CE и VPP, просто задаем соответствующие биты порта D
void loop() {
if (Serial.available()) {
byte data, dump_byte, data_byte;
byte string_count = 0;
byte stop = 0;
unsigned int error_count = 0;
int inByte = Serial.read();
switch (inByte) {
case '8':
Serial.println("");
Serial.println("8 - compare_data_dump");
addr_reset();
print_addr_hex();
Serial.print(" | ");
while(addr <= addr_end) {
dump_byte = pgm_read_byte(&dump[addr]);
data_byte = read_data();
print_data_hex(data_byte);
if (dump_byte == data_byte) {
Serial.print("==");
} else {
Serial.print("!=");
error_count ++;
}
print_data_hex(dump_byte);
string_count++;
Serial.write(' ');
addr_count();
if (string_count == 16) {
Serial.println("");
print_addr_hex();
Serial.print(" | ");
string_count = 0;
}
}
Serial.println("");
Serial.print("error_count=");
Serial.println(error_count);
print_menu();
break;
case '9':
Serial.println("");
Serial.println("9 - write_dump");
addr_reset();
print_addr_hex();
Serial.print(" | ");
while(addr <= addr_end) {
dump_byte = pgm_read_byte(&dump[addr]);
data_byte = write_verify_data(dump_byte) ;
print_data_hex(data_byte);
if (dump_byte == data_byte) {
Serial.print("==");
} else {
Serial.print("!=");
error_count ++;
}
print_data_hex(dump_byte);
string_count++;
Serial.write(' ');
addr_count();
if (string_count == 16) {
Serial.println("");
print_addr_hex();
Serial.print(" | ");
string_count = 0;
}
}
Serial.println("");
Serial.print("error_count=");
Serial.println(error_count);
print_menu();
break;
}
}
}
Организуем управление программатором через UART в основном цикле, пользователь будет отправлять номер команды и получать результат выполнения через консоль UART.
В основном цикле проверяем наличие входящих данных в интерфейсе UART, в случае поступления данных определяем команду и выполняем ее.
Для прошивки дампа в микросхему ROM используется команда 9. Она выполняет побайтовую запись всего дампа с проверочным чтением, подсчитывает количество ошибок записи и выводит весь процесс в консоль UART.
Для проверки записанных данных в микросхеме ROM используется команда 8. Она выполняет побайтовое чтение данных из микросхемы ROM и сравнение с дампом, подсчитывает количество ошибок и выводит весь процесс в консоль UART.
Скрытый текст
case '0':
Serial.println("addr_reset");
addr_reset();
print_addr_hex();
Serial.println("");
print_menu();
break;
case '1':
Serial.println("addr_count");
addr_count();
print_addr_hex();
Serial.println("");
print_menu();
break;
case '2':
Serial.println("addr_print");
print_addr_hex();
Serial.println("");
print_menu();
break;
case '3':
data = read_data();
print_data_hex(data);
Serial.print(" | ");
print_data_bin(data);
Serial.println("");
print_menu();
break;
case '4':
Serial.println("");
Serial.println("4 - data_print");
addr_reset();
print_addr_hex();
Serial.print(" | ");
while(addr <= addr_end) {
data = read_data();
print_data_hex(data);
string_count++;
addr_count();
Serial.write(' ');
if (string_count == 16) {
Serial.println("");
print_addr_hex();
Serial.print(" | ");
string_count = 0;
}
}
Serial.println("");
print_menu();
break;
case '5':
Serial.println("");
Serial.println("5 - data_arduino_print");
addr_reset();
while(addr <= addr_end) {
data = read_data();
Serial.print("0x");
print_data_hex(data);
Serial.print(", ");
string_count++;
if (string_count == 16) {
Serial.println("");
string_count = 0;
}
addr_count();
}
Serial.println("");
print_menu();
break;
case '6':
Serial.println("");
Serial.println("6 - dump_print");
addr_reset();
print_addr_hex();
Serial.print(" | ");
while(addr <= addr_end) {
data = pgm_read_byte(&dump[addr]);
print_data_hex(data);
string_count++;
addr_count();
Serial.write(' ');
if (string_count == 16) {
Serial.println("");
print_addr_hex();
Serial.print(" | ");
string_count = 0;
}
}
Serial.println("");
print_menu();
break;
case '7':
Serial.println("");
Serial.println("7 - dump_arduino_print");
addr_reset();
while(addr <= addr_end) {
data = pgm_read_byte(&dump[addr]);
Serial.print("0x");
print_data_hex(data);
Serial.print(", ");
string_count++;
if (string_count == 16) {
Serial.println("");
string_count = 0;
}
addr_count();
}
Serial.println("");
print_menu();
break;Остальные команды 0, 1, 2, 3, 4, 5, 6 и 7 являются сервисными и служат для наладки программатора, при прошивке мы их использовать не будем, скрыл их под спойлер.
void print_menu() {
Serial.println("MENU:");
Serial.println("0 - addr_reset");
Serial.println("1 - addr_count");
Serial.println("2 - addr_print");
Serial.println("3 - data_current_addr_print");
Serial.println("4 - data_print");
Serial.println("5 - data_arduino_print");
Serial.println("6 - dump_print");
Serial.println("7 - dump_arduino_print");
Serial.println("8 - compare_data_dump");
Serial.println("9 - write_dump");
}
Процедура вывода меню выводит в консоль UART допустимые команды программатора.
void print_addr_hex() {
Serial.print((addr & 0b1111000000000000) >> 12, HEX);
Serial.print((addr & 0b111100000000) >> 8, HEX);
Serial.print((addr & 0b11110000) >> 4, HEX);
Serial.print(addr & 0b1111, HEX);
}
Процедура вывода шеснадцатиричного адреса выводит в консоль текущий адрес в HEX формате длиной строго в 4 символа, нужно это для того, что метод Serial.print(0x000f, HEX) выведет в консоль не 000F как мы ожидаем а только один символ F, и нас при выводе дампа в консоль UART все будет кривое, косое и не читаемое.
void print_data_bin(int data) {
Serial.print((data & 0b10000000) >> 7, BIN);
Serial.print((data & 0b1000000) >> 6, BIN);
Serial.print((data & 0b100000) >> 5, BIN);
Serial.print((data & 0b10000) >> 4, BIN);
Serial.print((data & 0b1000) >> 3, BIN);
Serial.print((data & 0b100) >> 2, BIN);
Serial.print((data & 0b10) >> 1, BIN);
Serial.print(data & 0b1, BIN);
}
void print_data_hex(int data) {
Serial.print((data & 0b11110000) >> 4, HEX);
Serial.print(data & 0b1111, HEX);
}
Аналогичная проблема будет и при выводе байта данных в консоль UART в двоичном и шестнадцатеричном виде, также выводим их посимвольно.
unsigned int addr_end = 0x3fff;
const byte dump[] PROGMEM = {
0xf3, 0xaf, 0x11, 0xff, 0xff, 0xc3, 0xcb, 0x11,
0x2a, 0x5d, 0x5c, 0x22, 0x5f, 0x5c, 0x18, 0x43, ........};
Дамп у нас будет храниться во FLASH памяти ардуино в переменной dump[], соответственно читаться он будет побайтово встроенной функцией pgm_read_byte(&dump[addr]), адрес последнего байта данных дампа хранится в переменной addr_end (для 16 Кбайт соответствует 0x3fff).
Для заполнения массива dump[] из файла дампа нам необходимо конвертировать файл дампа в массив данных для ардуино. Воспользуемся языком Python.
file_name = 'sos48k_only.bin'
byte_in_string = 0
byte_count = 0
print('file_name', file_name)
print()
with open(file_name, 'rb') as file:
byte = file.read(1)
while byte:
int_byte = int.from_bytes(byte, byteorder='big')
hex_value = hex(int_byte)
byte_count += 1
if byte_in_string < 15:
print(hex_value + ', ', end="")
byte_in_string += 1
else:
print(hex_value + ', ')
byte_in_string = 0
byte = file.read(1)
print('byte_count', byte_count)
print('end_addr', hex(byte_count-1))
Открываем файл дампа и читаем его побайтово, приводим его в соответствие синтексиса массива для ардуино и выводим в консоль, после окончания конвертации копируем его в буфер обмена и вставляем в код в переменную dump[]. Для удобства также в консоль выдается адрес последнего байта данных для занесения его в переменную end_addr.
cmd /k python dump_convertor.py
Для удобства запуска конвертора дампа можно создать в директории с ним файл run_dump_convertor.cmd в который прописать указанную строку, она будет открывать командную консоль Windows и запускать в ней конвертор дампа.
И так, даташиты изучены, дампы конвертированы, прошивки написаны пора переходить к составлению схемы и сборке устройства.

Транзисторы для подачи напряжения на вывод VPP подойдут любые соответствующей полярности, номиналы резисторов также указаны примерно. В целом схему подачи напряжения VPP можно собрать из других компонентов, например я использовал вместо транзистора С945 оптрон PC817 только потому, что он у меня был.

Питание VCC +5В осуществляется по USB через ардуино, напряжение +12.5В я брал с литий ионного аккумулятора, состоящего из трех банок 18650, в качестве него подойдет любой источник питания 12 В и током не менее 100 мА.

Стирание микросхемы ROM перед прошивкой выполняется с замыканием всех выводов микросхемы и выдержкой под ультрафиолетовым светом, я использовал бактерицидный для местного применения, моему ROM хватил 5 минут, чтобы очиститься. После очистки данные микросхема ROM читаются как FF, при прошивке биты переводятся в состояне 0.
Итоги: За вечер мы изготовили программатор из компонентов, которые были под рукой и прошили им микросхему ROM. Поставленная задача полностью выполнена. В процессе прокачали навыки в чтении технической документации, программировании микроконтроллеров и на языке Python, изучили битовые операции и форматы данных, а самое главное, мы сэкономили свое время и получили удовлетворение от процесса.
Все файлы можно получить по ссылке Репозиторий проекта
Прошить таким программатором можно микросхемы ROM, FLASH или EEPROM разных объемов, возможно с минимальными доработками, для примера 27С128, 27С256, 27С512, W27C512 и подобные, главное сверяйтесь с даташитом.
Спасибо что дочитали, добро пожаловать в комментарии и с удовольствием буду рад видеть вас других моих новых постах. (Это кстати первый мой пост на хабре)
zbot
Ну я бы вместо счетчиков ИЕ9 использовал пару 74HC595 (потому что они лежат в наличии), ну или ту-же вторую ардуино подключенную по spi в качестве этих же сдвиговых регистров (с соответствующей прошивкой получил пару байт по spi - выдал на порты 14 бит (или все 16) из них в качестве адреса).
Можно конечно и типа через счетчик организовать на второй ардуине те-же пару выводов для связи задействовать первый CLK (счетчик +1) и второй RST (сброс счетчика в ноль).
А так как говорится почему бы и не да )))