PDA

Просмотр полной версии : ROM-плеер на ардуино



Improver
07.02.2018, 12:42
В качестве альтернативы проекту "SD-ROM Картридж (http://zx-pk.ru/threads/24473-sd-rom-kartridzh.html)", можно сделать аналогичное устройство, которое будет загружать программы с SD-карты в Вектор-06ц, но только будет это делать через магнитофонный вход. Этот вариант, хоть и более медленный, но имеет свои плюсы -- такой плеер будет работать с любой моделью Вектора, вне зависимости от того, какой в нём прошит загрузчик. Конечно, таким способом можно загружать программы и при помощи простого mp3-плеера, и напрямую со звуковой карты ПК, но пусть будет и эта альтернатива, возможно устройство получится более удобное в использовании.

Пока что я ставлю себе ближайшую цель создать плеер, который будет с SD-карты выводить данные в формате ROM, а цель "на перспективу" -- создать аналог проекта "Магнитофон для Спектрума на ATMega128 (http://zx-pk.ru/threads/25622-magnitofon-dlya-spektruma-na-atmega128.html)", но для Вектора и на ардуино, без необходимости изготовления плат, пайки и в полностью открытых исходниках (пусть будет GPL...).

Итак, на текущий момент у меня готов первый вариант скетча, который выводит заранее подготовленные данные в формате ROM. Данный скетч протестирован на китайском аналоге Arduino Nano v3.

Для компиляции использовалась IDE версии 1.0.5 под Ubuntu, никаких дополнительных библиотек не потребовалось. Размер скетча в двоичном коде -- чуть больше 4Кб.
Вкратце, скетч работает так: сначала выводится короткий звуковой сигнал и через 1 секунду начинается воспроизведение заголовка и данных. Дублирование блоков не делал -- думаю, при качественном сигнале в этом не будет необходимости. Далее пауза на 15 секунд и всё повторяется сначала.


// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x39, 0x30, 0x31, 0x31, 0x38, // дата 190118
0x74, 0x65, 0x73, 0x74, 0x74, 0x70, 0x20, 0x20, // testtp.....
0x20, 0x20, 0x20, 0x00, 0x00 };
// собственно, сама программа -- тест техпрогона
byte St[1024] = {
0xC3, 0x3D, 0x01, 0x77, 0x65, 0x6B, 0x74, 0x6F, 0x72, 0x2D, 0x30, 0x36, 0x63, 0x54, 0x45, 0x53,
0x54, 0x20, 0x56, 0x28, 0x31, 0x2E, 0x31, 0x29, 0x20, 0x20, 0x72, 0x61, 0x7A, 0x72, 0x61, 0x62,
0x2E, 0x74, 0x65, 0x6D, 0x69, 0x72, 0x61, 0x7A, 0x6F, 0x77, 0x20, 0x64, 0x2E, 0x61, 0x2E, 0x20,
0x2C, 0x73, 0x6F, 0x6B, 0x6F, 0x6C, 0x6F, 0x77, 0x20, 0x61, 0x2E, 0x61, 0x2E, 0x3E, 0x80, 0xD3,
0x04, 0x3E, 0xC3, 0x32, 0x00, 0x00, 0x3E, 0xC9, 0x32, 0x38, 0x00, 0x21, 0x01, 0x00, 0x22, 0xE6,
0x04, 0x21, 0x3D, 0x01, 0x22, 0x01, 0x00, 0x21, 0x00, 0x05, 0x11, 0x01, 0x00, 0xAF, 0x77, 0x23,
0x77, 0x23, 0x77, 0x23, 0x77, 0x19, 0xD2, 0x5E, 0x01, 0x31, 0x00, 0x01, 0x3E, 0x88, 0xD3, 0x00,
0xCD, 0xD7, 0x02, 0x3E, 0x03, 0xD3, 0x02, 0x3E, 0x02, 0xD3, 0x01, 0x3E, 0xFF, 0xD3, 0x03, 0xCD,
0xB4, 0x03, 0x21, 0x55, 0x9E, 0x06, 0x04, 0x0E, 0x08, 0x3E, 0xFF, 0xCD, 0x71, 0x03, 0x7C, 0xD6,
0x04, 0x67, 0x0D, 0xC2, 0x89, 0x01, 0x7C, 0xC6, 0x20, 0x67, 0x7D, 0xC6, 0x20, 0x6F, 0x05, 0xC2,
0x87, 0x01, 0x06, 0x80, 0x2A, 0xE6, 0x04, 0xE5, 0x7C, 0x21, 0x34, 0x8A, 0xF5, 0xCD, 0x38, 0x03,
0xF1, 0x21, 0x34, 0xCA, 0xCD, 0x38, 0x03, 0xE1, 0x7D, 0x21, 0x34, 0x94, 0xF5, 0xCD, 0x38, 0x03,
0xF1, 0x21, 0x34, 0xD4, 0xCD, 0x38, 0x03, 0x21, 0x00, 0x05, 0x78, 0x07, 0x47, 0x4E, 0x70, 0x7E,
0x71, 0xB8, 0xCA, 0xFF, 0x01, 0xCD, 0x04, 0x03, 0xE5, 0x21, 0x1A, 0x9E, 0xCD, 0x38, 0x03, 0xE1,
0xE5, 0x7C, 0x21, 0x1A, 0xAA, 0xCD, 0x38, 0x03, 0xE1, 0xE5, 0x7D, 0x21, 0x1A, 0xB4, 0xCD, 0x38,
0x03, 0x3A, 0x9E, 0x04, 0x3C, 0x32, 0x9E, 0x04, 0x21, 0x0C, 0xB4, 0xCD, 0x38, 0x03, 0xE1, 0x7D,
0xB7, 0xC2, 0x23, 0x02, 0xE5, 0x7C, 0xD3, 0x07, 0x21, 0x27, 0xCA, 0xCD, 0x38, 0x03, 0xE1, 0xE5,
0x7D, 0xD3, 0x06, 0x21, 0x27, 0xD4, 0xCD, 0x38, 0x03, 0x78, 0xD3, 0x05, 0x21, 0x27, 0xFE, 0xCD,
0x38, 0x03, 0xE1, 0x23, 0x7C, 0xFE, 0x00, 0xC2, 0xCD, 0x01, 0x3E, 0x80, 0xB8, 0xC2, 0xA4, 0x01,
0xE5, 0x2E, 0x00, 0xCD, 0x99, 0x02, 0xCD, 0x99, 0x02, 0x21, 0xDC, 0x05, 0x3E, 0x36, 0xD3, 0x08,
0x7D, 0xD3, 0x0B, 0x7C, 0xD3, 0x0B, 0x2E, 0xFF, 0xCD, 0x99, 0x02, 0x21, 0xEE, 0x02, 0x3E, 0x76,
0xD3, 0x08, 0x7D, 0xD3, 0x0A, 0x7C, 0xD3, 0x0A, 0x2E, 0x01, 0xCD, 0x99, 0x02, 0x21, 0xF4, 0x01,
0x3E, 0xB6, 0xD3, 0x08, 0x7D, 0xD3, 0x09, 0x7C, 0xD3, 0x09, 0x2E, 0x00, 0xCD, 0x99, 0x02, 0x21,
0xFF, 0xFF, 0x3E, 0x00, 0xD3, 0x02, 0x2B, 0xD3, 0x01, 0x7C, 0xB5, 0xC2, 0x76, 0x02, 0x3E, 0x03,
0xD3, 0x02, 0x2A, 0xE6, 0x04, 0x23, 0x22, 0xE6, 0x04, 0xE1, 0x3E, 0x30, 0xD3, 0x08, 0x3E, 0x70,
0xD3, 0x08, 0x3E, 0xB0, 0xD3, 0x08, 0xC3, 0xA4, 0x01, 0x26, 0x00, 0x3E, 0xFF, 0xFB, 0x76, 0xD3,
0x03, 0xF5, 0xE5, 0x21, 0x78, 0x02, 0x2B, 0x7C, 0xB5, 0xC2, 0xA6, 0x02, 0x2E, 0x0F, 0x26, 0x52,
0x7D, 0xD3, 0x02, 0x25, 0xC2, 0xB3, 0x02, 0x2D, 0xC2, 0xAE, 0x02, 0x3E, 0x03, 0xD3, 0x02, 0xE1,
0xF1, 0x85, 0x24, 0xC2, 0x9D, 0x02, 0xC9, 0xF6, 0xA4, 0x52, 0x00, 0xC0, 0x80, 0x40, 0x00, 0x20,
0x10, 0x08, 0x00, 0x04, 0x02, 0x01, 0x24, 0xFB, 0x76, 0xF5, 0xE5, 0xD5, 0x11, 0x0F, 0x10, 0x21,
0xC7, 0x02, 0x7B, 0xD3, 0x02, 0x7E, 0xD3, 0x0C, 0x23, 0xD3, 0x0C, 0x1D, 0xD3, 0x0C, 0x15, 0xD3,
0x0C, 0x14, 0xD3, 0x0C, 0x15, 0xD3, 0x0C, 0x14, 0xD3, 0x0C, 0x15, 0xD3, 0x0C, 0xC2, 0xE2, 0x02,
0xD1, 0xE1, 0xF1, 0xC9, 0xE5, 0xC5, 0x4F, 0x2E, 0x35, 0x7C, 0xE6, 0x60, 0x67, 0x7D, 0xC6, 0x20,
0x6F, 0x7C, 0xD6, 0x20, 0x67, 0xF2, 0x0D, 0x03, 0x79, 0xA8, 0x26, 0xBE, 0x47, 0x7C, 0xC6, 0x04,
0x67, 0xFE, 0xE2, 0xCA, 0x34, 0x03, 0x78, 0x1F, 0x47, 0xD2, 0x1D, 0x03, 0x3E, 0x55, 0xCD, 0x71,
0x03, 0xC3, 0x1D, 0x03, 0x79, 0xC1, 0xE1, 0xC9, 0xC5, 0xD5, 0xE5, 0x06, 0x08, 0x4F, 0x78, 0xFE,
0x04, 0xC2, 0x45, 0x03, 0x25, 0x79, 0x1F, 0x4F, 0xD2, 0x51, 0x03, 0x11, 0x94, 0x04, 0xC3, 0x54,
0x03, 0x11, 0x8A, 0x04, 0xCD, 0x60, 0x03, 0x25, 0x05, 0xC2, 0x3E, 0x03, 0xE1, 0xD1, 0xC1, 0xC9,
0xC5, 0xD5, 0xE5, 0x06, 0x0A, 0x1A, 0x77, 0x2D, 0x13, 0x05, 0xC2, 0x65, 0x03, 0xE1, 0xD1, 0xC1,
0xC9, 0xC5, 0xD5, 0xE5, 0xFE, 0xFF, 0xCA, 0x93, 0x03, 0x11, 0xA4, 0x03, 0x4C, 0x21, 0xAC, 0x03,
0xF5, 0x1A, 0xB9, 0x46, 0x13, 0x23, 0xC2, 0x81, 0x03, 0xF1, 0xE1, 0xE5, 0x60, 0x11, 0x72, 0x04,
0xC3, 0x96, 0x03, 0x11, 0x5A, 0x04, 0x06, 0x18, 0x1A, 0x77, 0x13, 0x2D, 0x05, 0xC2, 0x98, 0x03,
0xE1, 0xD1, 0xC1, 0xC9, 0xC2, 0xC6, 0xCA, 0xCE, 0xD2, 0xD6, 0xDA, 0xDE, 0xC6, 0xC2, 0xCE, 0xCA,
0xD6, 0xD2, 0xDE, 0xDA, 0x06, 0x18, 0x3E, 0xFF, 0x26, 0x88, 0x0E, 0x38, 0x2E, 0xF8, 0x77, 0x2D,
0x0D, 0xC2, 0xBE, 0x03, 0x24, 0x05, 0xC2, 0xBA, 0x03, 0x21, 0xE0, 0xEA, 0x3E, 0x01, 0xCD, 0x32,
0x04, 0x24, 0x3C, 0xFE, 0x10, 0xC2, 0xCE, 0x03, 0x21, 0xF0, 0x81, 0xCD, 0xE1, 0x03, 0x21, 0xF0,
0x99, 0x06, 0x03, 0x11, 0x9F, 0x04, 0xE5, 0x3E, 0x04, 0xF5, 0xE5, 0x0E, 0x09, 0x1A, 0x77, 0x2B,
0x13, 0x0D, 0xC2, 0xED, 0x03, 0xE1, 0xF1, 0x24, 0x3D, 0xC2, 0xE9, 0x03, 0xE1, 0x11, 0xF0, 0x20,
0x19, 0x05, 0xC2, 0xE3, 0x03, 0xD5, 0xE5, 0xC5, 0x21, 0xEB, 0x8B, 0x11, 0xC3, 0x04, 0xCD, 0x27,
0x04, 0x21, 0xEB, 0x8F, 0xCD, 0x27, 0x04, 0x21, 0xEB, 0x93, 0xCD, 0x27, 0x04, 0x21, 0xEB, 0x97,
0xCD, 0x27, 0x04, 0xC1, 0xE1, 0xD1, 0xC9, 0x06, 0x09, 0x1A, 0x77, 0x13, 0x2B, 0x05, 0xC2, 0x29,
0x04, 0xC9, 0xF5, 0xE5, 0x57, 0x0E, 0x04, 0x06, 0x18, 0xE5, 0x7A, 0x0F, 0x57, 0xD2, 0x45, 0x04,
0x3E, 0xFF, 0xC3, 0x46, 0x04, 0xAF, 0x77, 0x2D, 0x05, 0xC2, 0x46, 0x04, 0xE1, 0xD5, 0x11, 0x00,
0xE0, 0x19, 0xD1, 0x0D, 0xC2, 0x37, 0x04, 0xE1, 0xF1, 0xC9, 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81,
0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x85,
0x81, 0xFF, 0xFF, 0x81, 0x85, 0xA9, 0x91, 0xBD, 0x81, 0x81, 0xBD, 0xA9, 0xA9, 0x9D, 0x81, 0x91,
0xA9, 0xA9, 0xBD, 0x81, 0x81, 0xAD, 0xAD, 0xBD, 0x81, 0xFF, 0x7C, 0xC6, 0xCE, 0xCE, 0xD6, 0xD6,
0xE6, 0xE6, 0xC6, 0x7C, 0x04, 0x0C, 0x1C, 0x3C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x3E, 0x00, 0x00,
0x7F, 0x5D, 0x1C, 0x1C, 0x1C, 0x1C, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x63, 0x7F, 0x60, 0x3E,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x60, 0x3E, 0x03, 0x7E, 0x00, 0x00, 0x08, 0x18, 0x7F, 0x18, 0x18,
0x19, 0x0E, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x78, 0x6C, 0x66, 0x00, 0x00, 0x3C, 0x66, 0x6E,
0x60, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x66,
0x66, 0x24, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

int p = 3; // номер пина, на который будет вывод сигнала

void setup() //процедура setup
{
pinMode(p, OUTPUT); //объявляем пин как выход
}

void loop() //процедура loop
{
tone(p, 500, 100); //включаем на 500 Гц на 100 мс
delay(1000); //ждем 1 с

byte BLs = 0x01; // начальный блок
byte BLe = 0x04; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
int Nbt = 0; // номер байта из массива

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
SendByte(0x00);
}
for (j=0; j<=24; j++){
SendByte(0x55);
}
}

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных
CSz = BLs;
CSz += BLe;
CSz += BLt;
for (j=0; j<=15; j++){ // 00h*16
SendByte(0x00);
}
for (j=0; j<=3; j++){ // 55h*4
SendByte(0x55);
}
SendByte(0xE6); // E6h*1
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
SendByte(SB[j]);
}
SendByte(BLs); // начальный блок
SendByte(BLe); // конечный блок
SendByte(BLt); // осталось блоков
SendByte(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}
SendByte(0xE6); // E6h*1
CSs = Nst;
SendByte(Nst); // номер строки
CSs += CSz;
SendByte(CSz); // контр.сумма заголовка

for (j=0; j<=31; j++){ // собственно, строка данных
CSs += St[Nbt];
SendByte(St[Nbt]);
Nbt++;
}
SendByte(CSs); // контр.сумма строки
}
}

for (j=0; j<=15; j++){ // 00h*16 -- завершение вывода программы (?)
SendByte(0x00);
}

delay(15000); // ждем 15 с
}

void SendByte(byte SBt){ // Подпрограмма вывода байта
byte Pd=PORTD;
byte i=8;
do{ // Выводим биты начиная со старшего
i--;
if ((bitRead(Pd, p))^(bitRead(SBt, i))){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p=3
PORTD = Pd; // вывод в порт p
}
else{
delayMicroseconds(128); // Задержка для выравнивания длительности сигнала
}
delayMicroseconds(256); // Задержка первого полупериода сигнала
Pd ^= (1 << p); // инвертируем бит в позиции p
PORTD = Pd; // вывод в порт p
delayMicroseconds(256); // Задержка второго полупериода сигнала
}
while (i>0);
}


В качестве выхода выбран порт D3 на ардуино, к нему можно подключить для теста пьезоэлемент (второй вывод на GND), либо подключить непосредственно ко входу Вектора через конденсатор. Я пробовал его подключать напрямую ко входу звуковой карты ПК и записать сигнал в WAV-файл (пример: 64142), полученный файл без ошибок распознаётся программой wav2rom на скорости 6-8. На реальном железе ещё пока не протестил...

В общем, если тема интересна -- давайте её развивать. Исходник скетча в виде файла для скачки: 64141

zebest
07.02.2018, 14:48
Почемууу бы в известный проект Tapeduino/Tzxduino не добавить функционал по выигрыванию/проигрыванию ром файлов? Или свой велосипед за всегда ближе?
Ну или хотя бы железо по пинам совместимые, чтобы два раза не вставать
https://github.com/sadken/TZXDuino

Improver
07.02.2018, 16:23
Почемууу бы в известный проект Tapeduino/Tzxduino не добавить функционал по выигрыванию/проигрыванию ром файлов? Или свой велосипед за всегда ближе?
Свой велосипед интереснее. :-) А если серьёзно -- то на данном этапе ещё пока не имеет значения, на основе какого проекта строить свой, т.к. пока что, фактически, есть только кодировщик в rom-формат. Хотя я заметил, что в этом проекте сначала файл с программой преобразуется в wav, а потом проигрывается -- я не вижу пока в этом преобразовании особого смысла. Но я совершенно не возражаю против того, чтобы "взять лучшее" из Tapeduino/Tzxduino.


Ну или хотя бы железо по пинам совместимые, чтобы два раза не вставать
Так... Подключение SD-картридера и экрана в любом случае будут совпадать, ибо там используется аппаратные интерфейсы, кнопки без проблем можно сделать так же, если очень хочется, а вот выход... Там это сделано на пин D9, он же PB1, и на том же порту PB висит обращение к картридеру -- тут надо ещё протестить, не будут ли неожиданные глюки или тормоза при выводе данных.

ivagor
07.02.2018, 16:37
Improver, генерацию стоит доработать. Наблюдается перекос длительностей. В идеале "длинные" интервалы д.б. в 2 раза длиннее "коротких", а в Test_16~~.wav они примерно на четверть длиннее чем нужно. Легко заметить на преамбуле - нули нормальные, а 55h слишком долгие. В эмуляторе стандартные загрузчики не могут прочитать такой файл, прочитал только таким (http://zx-pk.ru/threads/8778-varianty-zagruzchikov.html?p=854237&viewfull=1#post854237) и только в VV. Есть большое сомнение, что реал лучше воспримет такой wav.

svofski
07.02.2018, 16:47
Improver, ты генерируешь сигнал таймером-компарером?

Improver
07.02.2018, 17:06
Improver, генерацию стоит доработать. Наблюдается перекос длительностей. В идеале "длинные" интервалы д.б. в 2 раза длиннее "коротких", а в Test_16~~.wav они примерно на четверть длиннее чем нужно. Легко заметить на преамбуле - нули нормальные, а 55h слишком долгие.Да, сейчас глянул -- так и есть... Надо уменьшить значение в строке "delayMicroseconds(128)" в подпрограмме вывода байта.

- - - Добавлено - - -


Improver, ты генерируешь сигнал таймером-компарером?Незнаю, что это, но наверно нет. :-) Сигнал создаётся изменением состояния порта в подпрограмме SendByte, там же делается задержка длительности каждого состояния.

svofski
07.02.2018, 17:36
Незнаю, что это, но наверно нет. :-)
Хардверные таймеры позволяют отмерять точные промежутки времени и аппаратно генерируют волновую форму на выходе. Я кроме даташитов на атмеги ничего не читал, но может быть вот тут неплохо это в ардуинный контекст помещено:
https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM

С другой стороны полностью софтовый генератор проще переносить на альтернативные Ардуины. Так что наверное нету смысла заморачиваться.

Improver
07.02.2018, 18:06
ivagor, вот, уменьшил значение до 16, и тогда пропорции сигнала соблюдены, вроде... Посмотри: 64143
Опять же, проверил через wav2rom -- распознаётся легко...

- - - Добавлено - - -


Хардверные таймеры позволяют отмерять точные промежутки времени и аппаратно генерируют волновую форму на выходе.А, про это... Да, в начале у меня была идея использовать аппаратную генерацию, но нам ведь на выходе, фактически, нужно получать один импульс в нужной фазе на каждый бит, и мне показалось, что это будет слишком заморочено...

Я сейчас бегло почитал ту статью про PWM, там есть интересные вещи, надо будет изучить этот вопрос.

zebest
07.02.2018, 18:22
Подключение SD-картридера и экрана в любом случае будут совпадать, ибо там используется аппаратные интерфейсы, кнопки без проблем можно сделать так же, если очень хочется, а вот выход...
http://s2.micp.ru/w85VZ.jpg
с год назад собирал ради интереса и тестировал на тап-ках, ну и разрисовал, для сеяб.. Пищало вполне прилично :)

ivagor
07.02.2018, 18:23
Test_17~.wav спокойно читается в эмуляторах штатными загрузчиками, длительности выровнялись.

svofski
07.02.2018, 18:39
мне показалось, что это будет слишком заморочено
У страха глаза велики =) С таймерами самые невероятные вещи можно творить.

Но на самом деле ради переносимости я бы правда отложил эту идею. Монотонность импульсов очень просто получить, если генерировать поток BPSK в виде битовых пар. А процедура вывода будет этот поток бит за битом брать, выводить и ждать. Тогда получается очень простая процедура с одним выводом и одной задержкой, которая не зависит от собственно значения очередного бита, фазы и, что самое интересное, даже от вида модуляции: все разновидности BPSK, BFSK, MFM, (m,n) RLL и GCR так представимы.

Improver
09.02.2018, 11:16
Итак, продолжаем развивать проект...
Подключил SD-картридер, схема подключениея такая, как в сообщении #19 (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=949439&viewfull=1#post949439) zebest, только пока без кнопок и экрана. Да, и вывод сигнала остался на D3.

В корень SD-карты положить файл "cybernoi.rom". Если карта не обнаружена, то подаётся сигнал 200Гц/100мс, если не обнаружен файл, то сигнал 200Гц/300мс.

/*
* SD-картридер подключяется к выводам ардуино:
** MOSI - D11
** MISO - D12
** CLK - D13
** CS - D10
*
* Выход - D3
*/
#include <SD.h>

File romFile;

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x39, 0x30, 0x31, 0x31, 0x38, // дата: 190118
0x74, 0x65, 0x73, 0x74, 0x74, 0x70, 0x20, 0x20, // testtp.....
0x20, 0x20, 0x20, 0x00, 0x00 };

int p = 3; // номер пина, на который будет вывод сигнала

void setup() //процедура setup
{
pinMode(p, OUTPUT); //объявляем пин как выход
pinMode(10, OUTPUT);

while (!SD.begin(10)){ // SD-карта готова?
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
}

void loop() //процедура loop
{
tone(p, 500, 100); //включаем на 500 Гц на 100 мс
delay(1000); //ждем 1 с

romFile = SD.open("cybernoi.rom"); // открываем файл
if (romFile) { // открылся?
byte BLs = 0x01; // начальный блок
unsigned int Nbt = romFile.size(); // всего байтов
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0){ // корректировка количества блоков, если размер файла не кратен 256
BLe++;
}

// Начинаем вывод
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
SendByte(0x00);
}
for (j=0; j<=24; j++){
SendByte(0x55);
}
}

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных
CSz = BLs;
CSz += BLe;
CSz += BLt;
for (j=0; j<=15; j++){ // 00h*16
SendByte(0x00);
}
for (j=0; j<=3; j++){ // 55h*4
SendByte(0x55);
}
SendByte(0xE6); // E6h*1
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
SendByte(SB[j]);
}
SendByte(BLs); // начальный блок
SendByte(BLe); // конечный блок
SendByte(BLt); // осталось блоков
SendByte(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}
SendByte(0xE6); // E6h*1
CSs = Nst;
SendByte(Nst); // номер строки
CSs += CSz;
SendByte(CSz); // контр.сумма заголовка

for (j=0; j<=31; j++){ // собственно, строка данных
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
CSs += St;
SendByte(St);
}
SendByte(CSs); // контр.сумма строки
}
}

for (j=0; j<=15; j++){ // 00h*16 -- завершение вывода программы (?)
SendByte(0x00);
}
// close the file:
romFile.close();
}
else {
tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс
}
delay(15000); // ждем 15 с
}

void SendByte(byte SBt){ // Подпрограмма вывода байта
byte Pd=PORTD;
byte i=8;
do{ // Выводим биты начиная со старшего
i--;
if ((bitRead(Pd, p))^(bitRead(SBt, i))){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p=3
PORTD = Pd; // вывод в порт p
}
else{
delayMicroseconds(16); // Задержка для выравнивания длительности сигнала
}
delayMicroseconds(256); // Задержка первого полупериода сигнала
Pd ^= (1 << p); // инвертируем бит в позиции p
PORTD = Pd; // вывод в порт p
delayMicroseconds(256); // Задержка второго полупериода сигнала
}
while (i>0);
}
Для проверки взял один из самых больших ROM-файлов, игру Cybernoid, на выгрузку ушло примерно 4 минуты. Всё, вроде, отработало здорово, но при распознавании в wav2rom вылезло 45 ошибок. :-( Причём все ошибки в первом байте блока (не каждого -- в 45 из 159 блоков):
64163

Просмотр сигнала в той же программе выявил ошибку:
64164

Где-то возникают тормоза -- или прерывание срабатывает, или чтение с карты тормозит... Причём эти ошибки есть даже если выгружаемая программа короткая, например тот же тест техпрогона. В общем, надо ещё подумать...

Вот скетч в архиве: 64165, WAV-файл не выкладываю -- нет смысла...

- - - Добавлено - - -


У страха глаза велики =) С таймерами самые невероятные вещи можно творить.

Но на самом деле ради переносимости я бы правда отложил эту идею.Да, оставим на потом. А с другой стороны, с выводом без всяких таймеров легко справлялся проц КР580ВМ80А -- и ардуина тоже должна справится. :-)

svofski
09.02.2018, 12:15
St = romFile.read(); // читаем очередной байт из файла
Предполагаю, что эта процедура берет очередной байт из буфера, а когда буфер заканчивается, читает целиком следующий сектор, или несколько, если надо свериться с FAT, что и вызывает задержку.

Не знаком с драйвером SD от ардуины. Думаю, что вряд ли там можно висеть колбеком на чтение байта. Формат Вектора предполагает ресинхронизацию между блоками. Значит, если рассчитать размер буфера так, чтобы буфер всегда подсасывался на границе блока, можно эту проблему замести под ковер.

Improver
09.02.2018, 14:01
svofski, я сейчас попробовал сделать предварительную считку байта до того, как блок начал выводиться, при этом задержка вылезла в заголовок. Если там допустима некоторая рассинхронизация, то это решение... Как я понимаю, задержка может происходить до начала заголовка блока (00h*16, 55h*4...)?

svofski
09.02.2018, 16:05
Это мое предположение, да. По идее после последнего байта любого блока можно сделать безболезненную паузу. Не знаю, как на самом деле на это реагирует загрузчик.

Параллельно я бы все-таки поинтересовался недрами драйвера, чтобы научиться читать поток без задержек. Это пригодится для форматов без блоков.

Improver
09.02.2018, 17:10
ivagor, svofski, можете протестить на железе (или эмуляторах) такой вариант: 64167?
Это всё тот же тест техпрогона, но с sd-карты. В wav2rom он (и киберноид тоже) распознаются без ошибок, но в тесте есть два фриза, перед первым блоком и ближе к концу, наверно после второго или третьего блока:
64168

KTSerg
09.02.2018, 17:12
На сколько я помню, загрузчик начинает принимать новый блок только когда сможет принять байт Е6h, либо его инверсию 19h. Так что паузы между блоками действительно должны проходить безболезненно.
Но покопать дровишки лишним не будет...

svofski
09.02.2018, 17:48
У меня сейчас место расчищено для других дел, так что реал ждет своей очереди на полке. В эмулятор грузится хорошо.

- - - Добавлено - - -

В Тапире видно пропуски:
https://i.imgur.com/go613sV.png
Для стандартного загрузчика безболезненно можно сделать полупериод 5. Это будет не огромное ускорение, то все же.

Improver
14.02.2018, 18:14
Использованием библиотеки SdFat из проекта TZXDuino и переделкой подпрограммы вывода байта SendByte удалось сократить провалы в передаче сигнала раза в полтора, но всё равно она сейчас составляет примерно шесть полупериодов сигнала. Пробовал предварительно вычитывать 256 байт (1 блок) в буфер, но толку от этого нет -- всё равно задержки...
Вот что имеем в последней версии программы:

/*
* SD-картридер подключяется к выводам ардуино:
** MOSI - D11
** MISO - D12
** CLK - D13
** CS - D10
*
* Выход - D3
*/
#include <SdFat.h>

SdFat sd;
SdFile romFile;

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x74, 0x65, 0x73, 0x74, 0x74, 0x70, 0x20, 0x20, // testtp.....
0x20, 0x20, 0x20, 0x00, 0x00 };

int p = 3; // номер пина, на который будет вывод сигнала
unsigned long Tzd = 0; // переменная для расчёта задержки
const int Tpp = 256; // Длительность задержки сигнала в микросекундах (один полупериод)

void setup() //процедура setup
{
pinMode(p, OUTPUT); //объявляем пин как выход
pinMode(10, OUTPUT);

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
sd.chdir(); // устанавливаем корневую директорию SD
}

void loop() //процедура loop
{
tone(p, 500, 100); //включаем на 500 Гц на 100 мс
delay(1000); //ждем 1 с

if (romFile.open("cybernoi.rom",O_READ)) { // открываем файл. Открылся?
byte BLs = 0x01; // начальный блок
unsigned int Nbt = romFile.fileSize(); // всего байт
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0){ // корректировка количества блоков, если размер файла не кратен 256
BLe++;
}

// Начинаем вывод
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
SendByte(0x00);
}
for (j=0; j<=24; j++){
SendByte(0x55);
}
}

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных
CSz = BLs;
CSz += BLe;
CSz += BLt;
// упреждающее чтение первого байта блока данных с карты (он всегда есть)
St = romFile.read(); // читаем байт из файла
Nbt--;

for (j=0; j<=15; j++){ // 00h*16
SendByte(0x00);
}
for (j=0; j<=3; j++){ // 55h*4
SendByte(0x55);
}
SendByte(0xE6); // E6h*1
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
SendByte(SB[j]);
}
SendByte(BLs); // начальный блок
SendByte(BLe); // конечный блок
SendByte(BLt); // осталось блоков
SendByte(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++){ // 00h*4
SendByte(0x00);
}
SendByte(0xE6); // E6h*1
CSs = Nst;
SendByte(Nst); // номер строки
CSs += CSz;
SendByte(CSz); // контр.сумма заголовка

CSs += St; // начинаем вывод строки данных
SendByte(St); // предварительно считанный байт

for (j=0; j<=30; j++){ // остальные 31 байт
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
SendByte(St); // передаём считанный байт
CSs += St;
}
SendByte(CSs); // контр.сумма строки
if (Nst !=0x87) {
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
}
}
}

for (j=0; j<=15; j++){ // 00h*16 -- завершение вывода программы (?)
SendByte(0x00);
}
// close the file:
romFile.close();
}
else {
tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс
}
delay(15000); // ждем 15 с
}

void SendByte(byte SBt){ // Подпрограмма вывода байта
byte Pd=PORTD;
byte i=8;
do{ // Выводим биты начиная со старшего
i--;
Tzd = micros() - Tzd; // Вычисляем, какая была задержка
if (Tzd < Tpp) { // Если прошло времени меньше, чем установленная задержка
delayMicroseconds(Tpp - Tzd); // Задержка второго полупериода сигнала (предыдущего)
}
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
PORTD = Pd; // вывод в порт p
delayMicroseconds(Tpp); // Задержка первого полупериода сигнала
Tzd = micros(); // начало отсчёта длительности задержки
if ((bitRead(Pd, p))^(bitRead(SBt, i))){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
PORTD = Pd; // вывод в порт p
}
}
while (i>0);
}

И он же вместе с библиотекой в архиве: 64244
WAV-ка с примером вывода: 64245
Будем оптимизировать дальше... :-)

svofski
14.02.2018, 21:29
По-моему тут оптимизировать уже нечего, в рамках принятой концепции все на месте. Чтобы избавиться от задержек совсем, надо менять концепцию.

Чтобы было максимально просто и не заморачиваться на специфическое железо, я бы сделал так:

* Создаются 2 пинг-понг буфера. Один на запись, второй на чтение. В идеале конечно надо библиотеке SD объяснить, что читать надо прямо в них, а не копировать по байту. Иначе очень жирно будет буферов, что для AVR особенно критично.

* Прерывание от таймера настраивается на полупериод кассетного бита. Оно выводит один полубит и сдвигает указатель на следующий полубит. Когда кончается бит, на следующий бит, когда кончается байт, на следующий байт. Когда закончится буфер А, переключается на чтение буфера Б и ставит флажок "нужен буфер".

* Основной цикл слушает флажок "нужен буфер" и запускает чтение буфера А, пока выводится Б и наоборот.

zebest
14.02.2018, 21:30
а елси весь файл в буфер зачитать а потом уже транслировать?? емкости ардуны не хватит?? а если Мегу взять ?

Improver
15.02.2018, 11:31
По-моему тут оптимизировать уже нечего, в рамках принятой концепции все на месте. Чтобы избавиться от задержек совсем, надо менять концепцию.

Чтобы было максимально просто и не заморачиваться на специфическое железо, я бы сделал так:

* Создаются 2 пинг-понг буфера. ...
Интересная идея. А если сделать один буфер? Допустим, так:
- Основная программа подготавливает данные и сливает их в массив, допустим, на 256 байт. И есть две переменных, первая -- индекс, откуда сейчас читаем, а вторая -- куда пишем.
- Как буфер заполнится, запускаем таймер на время, равное периоду вывода 1 байта (8 бит). По прерыванию с него выводим байт с позиции чтения, делаем индекс чтения +1.
- Далее основная программа заполняет с максимальной скоростью буфер до состояния "позиция записи" = ("позиция чтения" - 1), а по таймеру идёт непрерывный вывод данных из буфера. (Естественно, при достижении конца буфера запись циклически начнётся с его начала.)

Нормально так будет?


а елси весь файл в буфер зачитать а потом уже транслировать?? емкости ардуны не хватит?? а если Мегу взять ?Не, там оперативки всего-то 2 Кб, на меге -- 8 Кб... Ну если только во флеш переносить.

KTSerg
15.02.2018, 11:48
Другими словами:
- Буфер закольцован.
- Программа вызываемая из прерывания, занимается отправкой с текущей позиции чтения из буфера.
- Основная программа следит за разностью между индексами чтения и записи в буфер, и подчитывает, заполняя буфер.

svofski
15.02.2018, 13:32
Кольцо удобно когда поставщик и потребитель неизвестно в каком темпе работают: буфер последовательного канала итп. А тут все известно, чтение происходит порциями фиксированного размера в буфер, поэтому пинг-понг проще:

* не надо считать никаких голов и хвостов
* чтение всегда линейное, то есть можно пользоваться стандартными библиотеками для чтения прямо в буфер
* просто гарантировать то, что запись не происходит туда, откуда еще не все прочитано

Если очень хочется именно кольцо, то достаточно представить себе пинг-понг буфер кольцом длиной 2, где каждый элемент — это буфер ;)

- - - Добавлено - - -


Как буфер заполнится, запускаем таймер на время, равное периоду вывода 1 байта (8 бит). По прерыванию с него выводим байт с позиции чтения, делаем индекс чтения +1
Так нельзя делать. Если обработчик все время будет выводить байт, то как только он выйдет, он тут же будет вызван снова, потому что пришел черед следующего байта. Получается, что наша программа просто переехала в прерывание, а времени на чтение как не было, так и нет.

Смысл прерывания в том, что основное свободное время, которое у нас есть, находится между полубитами. Время вывода полубитов так же является критичным для задачи. Поэтому прерывание выводит только один полубит. Это происходит по таймеру, поэтому мы знаем, что вывод работает максимально точно. После вывода полубита обработчик обязательно должен вернуться, чтобы во-первых иметь возможность быть вызванным снова вовремя и, во-вторых, чтобы дать время основному циклу для поддержания заполненности буферов.

KTSerg
15.02.2018, 16:11
Ну, если задержки вызывает именно чтение, то не обращать на них внимание. Читать стандартными функциями по 32 или 256 байт (как нравится) в буфер, до начала вывода преамбулы (строки 32 Байт) и не обращать внимание на паузы, т.к. Вектор их (в этом месте) спокойно пережуёт.

Improver
15.02.2018, 16:31
svofski, пожалуй, Вы правы -- надо делать таймер на полупериод. Это получается, что чётные разы его срабатывания в программе будет просто инверсия состояния порта, а нечётные -- состояние будет меняться (или не меняться) в зависимости от выводимого бита... Ок, попробую это реализовать.

- - - Добавлено - - -


Ну, если задержки вызывает именно чтение, то не обращать на них внимание. Читать стандартными функциями по 32 или 256 байт (как нравится) в буфер, до начала вывода преамбулы (строки 32 Байт) и не обращать внимание на паузы, т.к. Вектор их (в этом месте) спокойно пережуёт.В последнем варианте почти так и есть, но хотелось бы лучшего... :-) А буфер, как я догадываюсь, имеет размер одного кластера SD-карты.

svofski
15.02.2018, 18:30
Посмотрел бегло на SdFat в TZXDuino. Ключевой момент, где подкачивается буфер:
https://github.com/sadken/TZXDuino/blob/d74e9ab2a688e235ee4beb21da98ac3b23b0bc7e/Libraries/SdFat/utility/FatFile.cpp#L760

Кеш там - это буфер 512 байт, который используется для всего на свете. Для текущих данных, но так же и для таблиц FAT и каталогов. Это круто, но делает переделку этой библиотеки под пинг-понг буфер для данных задачей непропорционально премудрой.

Будь это мой проект, я бы эту библиотеку отправил туда, где ей место. А вместо нее взял бы адекватную, написанную без вселенских замашек. Но, пытаясь поставить себя на место Improver-a,

я бы плюнул и оставил эту библиотеку как есть, раз она удобна в использовании и проверена. А данные читал бы в два буфера по 256 байт, только не read(void) а сразу read(uint8_t * dst, size_t count). Вот если памяти перестанет хватать, тогда уже можно подумать, как сделать оптимальней.

KTSerg
15.02.2018, 18:33
...
В последнем варианте почти так и есть, но хотелось бы лучшего... :-) А буфер, как я догадываюсь, имеет размер одного кластера SD-карты.
Просто насколько я понимаю, стандартными средствами до того буфера (считанного кластера), нет доступа. Библиотека предоставляет лишь функцию, которая предоставляет пользователю данные из считанного кластера. Вот я и думаю, что вызывать стандартную функцию библиотеки для чтения одного байта, это избыточно, нужно сразу получить в свой буфер например 32 байта и спокойно их отправить, потом запросить следующие... правда это лишний расход памяти...

Improver
16.02.2018, 14:07
Очередная версия готова, вот:

Для скетча требуется библиотека TimerOne (https://github.com/PaulStoffregen/TimerOne) и, также, как и для предыдущей версии, библиотека SdFat из проекта TZXDuino (https://github.com/sadken/TZXDuino).

/*
* SD-картридер подключяется к выводам ардуино:
** MOSI - D11
** MISO - D12
** CLK - D13
** CS - D10
*
* Выход - D3
*/
#include <SdFat.h>
#include <TimerOne.h>

SdFat sd;
SdFile romFile;

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x74, 0x65, 0x73, 0x74, 0x74, 0x70, 0x20, 0x20, // testtp.....
0x20, 0x20, 0x20, 0x00, 0x00 };

int p = 3; // номер пина, на который будет вывод сигнала

const int Tpp = 256; // Длительность задержки сигнала в микросекундах (один полупериод)

void setup() //процедура setup
{
pinMode(p, OUTPUT); // объявляем пин как выход
pinMode(10, OUTPUT); // CS для SD-картридера

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
sd.chdir(); // устанавливаем корневую директорию SD
}

void loop() //процедура loop
{
tone(p, 500, 100); //включаем на 500 Гц на 100 мс
delay(1000); //ждем 1 с

if (romFile.open("cybernoi.rom",O_READ)) { // открываем файл. Открылся?
byte BLs = 0x01; // начальный блок
unsigned int Nbt = romFile.fileSize(); // всего байт
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0){ // корректировка количества блоков, если размер файла не кратен 256
BLe++;
}

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[CWB] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[CWB] = 0x55;
CWB++;
}
}

Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++){ // 00h*16
ToBUFF(0x00);
}
for (j=0; j<=3; j++){ // 55h*4
ToBUFF(0x55);
}
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++){ // 00h*4
ToBUFF(0x00);
}

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков
ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++){ // 00h*4
ToBUFF(0x00);
}
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
}
ToBUFF(CSs); // контр.сумма строки
}
}
// close the file:
romFile.close();

for (j=0; j<=15; j++){ // 00h*16 -- завершение вывода программы (?)
ToBUFF(0x00);
}
}
else {
tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс
}
while (CRB < CWB) { // Ждём опустошения буфера
delay(Tpp);
}
Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
delay(15000); // ждем 15 с
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
while (CWB > CRB + 255) {// Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
// if ((bitRead(Pd, p))^(bitRead(BUFF[lowByte(CRB)], bBit/2))){ // Старый вариант...
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // нечётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
}
else{
bBit = 15;
CRB++;
}
}

В итоге, решил всё-таки сделать на одном закольцованном буфере, а не на двух типа "пинг-понг" -- всё ради экономии памяти... Размер буфера 256 байт, что при таймере в 256 мкс даёт его ёмкость чуть больше секунды. В дебужной версии запись в буфер "догоняет" чтение и уходит в паузу примерно раз в секунду-полторы. На том же "Киберноиде" ни одного провала или ошибки в выводимом сигнале не обнаружено, даже при сокращении времени таймера до 160 мкс, что уже можно считать успехом. :v2_dizzy_punk:
Кстати, по таймеру -- если я правильно посчитал, то при 256 мкс "несущая" частота должна быть примерно 2 кГц, а сколько максимум может вытянуть Вектор?

Ещё такой ньюанс -- тут (в конце) (http://radioprog.ru/post/116) пишут, что "при обращении к общим переменным прерывания, как правило, должны быть отключены", но я пока сделал без отключения -- посмотрим, если будет работать без глюков, то может и так оставим...

Скетч со всеми нужными библиотеками в архиве: 64264
Пример выводимой wav-ки: 64265

- - - Добавлено - - -


Вот я и думаю, что вызывать стандартную функцию библиотеки для чтения одного байта, это избыточно, нужно сразу получить в свой буфер например 32 байта и спокойно их отправить, потом запросить следующие... правда это лишний расход памяти...Пробовал я так сделать, и при этом задержки получаются даже немного больше... С буфером на SD не всё так просто: во-первых, в некоторых статьях упоминается о наличии в SD-картах аппаратного буфера чтения/записи, который может быть разного размера в зависимости от модели карты (пруфов не сохранил), а во-вторых, сами карты, в принципе, не умеют читать/писать информацию иначе, чем блоками размером в кластер. Поэтому, я думаю, чтение первого байта происходит с большей задержкой, чем остальных в пределах одного кластера. В общем, сейчас эта проблема решена использованием таймера и таким псевдо-распараллеливанием процессов чтения-передачи, что-то ещё делать с библиотекой я не вижу особого смысла.

- - - Добавлено - - -


я бы плюнул и оставил эту библиотеку как есть, раз она удобна в использовании и проверена.Так и сделал. :-)

svofski
16.02.2018, 14:22
Поздравляю! Это уже красиво.

Маленькое замечание — delay() для ожидания изменения бита не имеет смысла, потому что внутри его такой же цикл.

ToBUFF() умеренно безопасен, пока получается поддерживать разрыв между указателями.

KTSerg
16.02.2018, 14:48
Кстати, если в программе только одно прерывание и оно используется для формирования выходного WAV, то должно быть глубоко фиолетово, сколько буферов и какие они... главное, чтобы были не пустыми. Поскольку прерывание на вывод очередного фронта прервёт общение с SD-картой (оно ведь не на прерываниях)...

Improver
16.02.2018, 17:32
Маленькое замечание — delay() для ожидания изменения бита не имеет смысла, потому что внутри его такой же цикл.Там немного сокровенный смысл: во время отработки цикла в delay(Tpp) не трогается переменная CRB, которая может неожиданно измениться в подпрограмме обработки прерывания, просто ждём 1000 циклов таймера, и за это время чтение догонит запись на 125 байт (почти половину буфера). :-)

svofski
16.02.2018, 18:03
Когда что-то поменяется, что-то добавится, или просто пройдет достаточное время, это навернется и поймать проблему будет очень трудно.

Для while() { delay(); } я бы завел булевый флажок, который устанавливается по такому же условию, но из прерывания.

Операция BUFF[lowByte(CWB++)] = SBb должна быть атомарной, то есть в скобке из noInterrupts() - interrupts().

Таким образом уйдет сокровенность и не останется мест, где зависимая переменная может измениться во время проверки условия. Останется бездушный автомат =)

Improver
19.02.2018, 09:32
Для while() { delay(); } я бы завел булевый флажок, который устанавливается по такому же условию, но из прерывания.Не вижу смысла, т.к.:
1. Это увеличит код подпрограммы прерывания, что не есть гуд.
2. Обращение к этой булевой переменной в основной программе также должно быть внутри noInterrupts / interrupts.
Лучше сделать нечто такое:

noInterrupts();
CRB_temp = CRB;
interrupts();
и далее в программе используем CRB_temp, как, собственно рекомендуют по ссылкам выше... И хотя вероятность одновременного чтения и записи переменной CRB крайне низка, это защитит нас от глюка на 100%.


Операция BUFF[lowByte(CWB++)] = SBb должна быть атомарной, то есть в скобке из noInterrupts() - interrupts().Ну может оно и так, но если принять за аксиому то, что разработчики компиляторов для ардуин -- разумные люди, то запись одного элемента массива ни в коем разе не должна затрагивать как-то другие элементы. А попадание чтения и записи в одну ячейку исключено программно задержкой, поэтому особого смысла выделять эту команду я не вижу.

KTSerg
19.02.2018, 10:20
А запрет прерывания не приведёт к потере прерывания и соответственно удлинениям импульсов ?

svofski
19.02.2018, 23:44
1. Это увеличит код подпрограммы прерывания, что не есть гуд.
2. Обращение к этой булевой переменной в основной программе также должно быть внутри noInterrupts / interrupts.

Увеличит на то же количество инструкций, которое эта же операция займет внутри секции с запрещенными прерываниями в основном цикле. И это пренебрежимо малое время в любом случае.

Опрос такого флага в основной программе не нужно прятать в критическую секцию, потому что она только читается, это флажок - один байт, суть чтение атомарное, и никаких попыток одновременной модификации не делается. Запрет прерывания вокруг проверки просто ничего не изменит.

Вариант с копированием переменной в критической секции тоже будет работать, но он не лучше. Это просто другой способ сделать то же самое.


разработчики компиляторов для ардуин -- разумные люди
Дело не в компиляторе, а в том, что эта простая строчка на C++ прячет в себе много операций, каждая из которых может быть прервана:
- вычисление смещения
- запись
- инкремент
- запись
каждая из этих операций тоже не атомарна, 16-битные инкременты, например, и тоже может быть разбита прерыванием.

Если вычисление флажка делать в прерывании, то операция записи должна быть в критической секции, чтобы флажок не вычислился с непонятным значением CWB. Если проверка условия c копированием итд в основном цикле, то операция копирования должна быть в критической секции. В принципе это дело вкуса.

Но все это из совершенно другой категории, чем подгонка буферов через delay(). delay() просто старается обойти проблему стороной, а правильная реализация взаимодействия между потоками исключает конфликтную ситуацию в принципе. То есть, если есть желание разбираться, то начинать надо с убирания delay().


А запрет прерывания не приведёт к потере прерывания и соответственно удлинениям импульсов ?
Флаг запроса прерывания висит, пока его не сбросят вручную, или автоматом при вызове обработчика (лучше мне не верить, а почитать даташыт, страница 112, Timer/Counter Interrupt Flag Register – TIFR).

Improver
20.02.2018, 10:09
Увеличит на то же количество инструкций, которое эта же операция займет внутри секции с запрещенными прерываниями в основном цикле. И это пренебрежимо малое время в любом случае.Не совсем на то же -- в основной программе можно вычисление этой булевой переменной вынести за секцию с запрещением прерывания. Я приводил пример выше, и по ссылке тоже так рекомендуют... Плюс к тому же не требуется выносить в критические секции все обращения к переменной CWB. Да, это будет всего несколько десятков циклов процессора, но всё же...


Если вычисление флажка делать в прерывании, то операция записи должна быть в критической секции, чтобы флажок не вычислился с непонятным значением CWB. Если проверка условия c копированием итд в основном цикле, то операция копирования должна быть в критической секции. В принципе это дело вкуса.Ещё, если вычисление флажка делать в подпрограмме прерывания, то он будет вычисляться каждый полуцикл, а если в основной программе -- то только перед записью в буфер, и в случае "паузы" он вычисляться не будет... А в остальном -- да, различия минимальны и это дело вкуса, но мне почему-то больше нравится второй вариант.


То есть, если есть желание разбираться, то начинать надо с убирания delay().Как? Заменить на millis()/micros() -- в данном случае это неимоверно усложнит программу... Честно -- я не представляю, как это сделать. Можно пример кода, или ссылку на то, как можно замедлить (синхронизировать) поток записи без применения delay() или другого подобного цикла ожидания?

- - - Добавлено - - -


А запрет прерывания не приведёт к потере прерывания и соответственно удлинениям импульсов ?Безусловно да, и в худшем случае импульс будет удлинён на время исполнения кода внутри секции с запретом прерывания, но при частоте процессора в 16Мгц, задержка на десяток циклов будет пренебрежительно мала. Хотя, это не избавляет от необходимости сокращения до минимума кода, исполняемого с запретом прерываний.

KTSerg
20.02.2018, 10:24
Можно ещё одно "извращение" сделать...
В прерывании, при изменении индекса CRB (или просто в конце прерывания), устанавливать флаг, а в программе ждать установки этого флага и только тогда переходить к проверке (сравнению индексов).
Контроль такого флага даст уверенность, что сравнение индексов происходит сразу по окончании прерывания и не будет "прервано"...
Непосредственно перед циклом ожидания флага, обязательно его (флаг) сбросить.

svofski
21.02.2018, 00:19
Как? Заменить на millis()/micros() -- в данном случае это неимоверно усложнит программу... Честно -- я не представляю, как это сделать. Можно пример кода, или ссылку на то, как можно замедлить (синхронизировать) поток записи без применения delay() или другого подобного цикла ожидания?
Задержка — это просто пустой цикл, который ничем не отличается от цикла, который эту задержку вызывает. Сейчас структура основного цикла условно говоря такая:



while (в буфере нет места) {
задержка(); /* на самом деле просто вложенный пустой цикл */
}
записать_в_буфер;



Задержка тут не является никакой частью механизма синхронизации. В последней версии скетча она выполняла "сокровенную функцию", то есть оттягивала фиаско. Синхронизация же обеспечивается любым из рассмотренных раньше способов, суть которых сводится к атомарной установке флага в одном месте и проверке его в другом. Если она сделана и исправно работает, то никаких ухищрений в виде задержек больше не потребуется:



while(в буфере нет места) { /* ничего */ };
записать_в_буфер;


Вместо двух вложенных циклов будет просто цикл.



В прерывании, при изменении индекса CRB (или просто в конце прерывания), устанавливать флаг, а в программе ждать установки этого флага и только тогда переходить к проверке (сравнению индексов).
Контроль такого флага даст уверенность, что сравнение индексов происходит сразу по окончании прерывания и не будет "прервано"...
Непосредственно перед циклом ожидания флага, обязательно его (флаг) сбросить.

Если будет чтение сектора, потом копирование большого числа байтов, прерывание несколько раз случится за это время.

Еще можно сразу после заполнения буфера загонять ардуину в сон (тут надо долго читать, какой именно из режимов сна правильно будет работать). Она будет просыпаться только на прерывание и забивание буфера, остальное время потреблять какой-то там наноампер.

KTSerg
21.02.2018, 07:47
Я вот чет притормозил... А зачем вообще необходима атомарность в BUFF[lowByte(CWB)] = SBb; ?
В прерывании вообще нет обращения к CWB, его модификация не происходит (где-то в другом месте кода), ну разорвётся эта запись на прерывание, и чем это грозит? (если не считать, случай когда CRB догнал CWB...), Но опять-же, индекс CWB ведь не изменяется до окончания записи в буфер, значит и проблемы не должно быть...

Improver
21.02.2018, 09:31
Вместо двух вложенных циклов будет просто цикл.Можно и так, это изменение имеет свои плюсы и минусы: цикл записи всегда будет "идти по пятам" чтения, т.е. если случится событие "в буфере нет места", то этот цикл начнёт с максимальной для ардуины скоростью проверять условие, опрашивать переменную CRB и запись продолжится, как только CRB увеличится на единицу. А если учесть, что, для безопасности, в основной программе все обращения к CRB должно быть внутри noInterrupts/interrupts, то получится совсем скверно для работы таймера и прерываний. Мне кажется, наличие второго вложенного цикла с ожиданием -- это тот костыль, который помогает. :-) Либо, опять же, можно вынести проверку условия "в буфере нет места" в подпрограмму обработки прерывания, но тогда окружать noInterrupts/interrupts прийдётся переменную CWB.


Еще можно сразу после заполнения буфера загонять ардуину в сон (тут надо долго читать, какой именно из режимов сна правильно будет работать). Она будет просыпаться только на прерывание и забивание буфера, остальное время потреблять какой-то там наноампер.А это уже интересная идея.

- - - Добавлено - - -


Я вот чет притормозил... А зачем вообще необходима атомарность в BUFF[lowByte(CWB)] = SBb; ?
В прерывании вообще нет обращения к CWB, его модификация не происходит (где-то в другом месте кода), ну разорвётся эта запись на прерывание, и чем это грозит? (если не считать, случай когда CRB догнал CWB...), Но опять-же, индекс CWB ведь не изменяется до окончания записи в буфер, значит и проблемы не должно быть...Да, именно так.
Интересное наблюдение -- в программе сейчас нет проверки на то, что чтение из буфера обгонит запись, хотя теоретически это возможно, например, в случае, если SD-карта не читается. Надо будет сделать проверку и на это событие...

Improver
21.02.2018, 14:33
В общем, не стоим на месте -- вот новая версия:
Кроме библиотеки TimerOne и SdFat, для работы теперь ещё требуется библиотека LiquidCrystal из стандартного комплекта IDE для ардуино.
Тут постарался исправить все найденные ошибки, добавил экран и кнопки. Кроме того, теперь в wav-ке в заголовках передаётся имя воспроизводимого файла.

/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Выход - D3

Подключение экрана 1602А (LCD Keypad Shield):
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
pinMode(10, OUTPUT); // CS для SD-картридера
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(currentFile); // переходим к первому файлу в директории
}

void loop() {
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

int button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
for (int i=0;12;i++){
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if ((sfileName[i+2]|0x20)=='o') { // == 'o'|'O'
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
if (PlayROM(sfileName, i)==0) { // Передаём короткое имя файла и позицию точки в качестве параметров
printtext("Done.",1); // Всё закончилось успешно.
}
else {
printtext("ERROR!",1); // Сообщение об ошибке
}
}
}
}
break;
}
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
printtext(" ",1); // очищаем строку 1
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
seekFile(currentFile);
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(currentFile);
break;
case BT_select: // пока что просто сообщение о нажатой кнопке, потом будет выход в настройки...
printtext("<Select>",0);
break;
case BT_none: // ничего не нажато
break;
}
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 100) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 400) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(" ");
lcd.setCursor(0,l);
lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.getName(fileName,filenameLength);
romFile.close();
maxFile++;
}
romFile.cwd()->rewind();
}

void seekFile(int pos) { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<=currentFile;i++) {
romFile.openNext(romFile.cwd(),O_READ);
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
romFile.close();
}
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
tone(p, 500, 100); // включаем на 500 Гц на 100 мс
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLs = 0x01; // начальный блок
unsigned int Nbt = romFile.fileSize(); // всего байт
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

int StName = 0;
for (i=14; i<=21; i++){ // заносим в SB имя файла
if (StName < pnt) { // имя ещё не закончилось?
SB[i] = FName[StName];
StName++;
}
else SB[i] = 0x20; // пробелы
}
StName = pnt;
for (i=22; i<=24; i++){ // заносим в SB расширение файла
StName++;
SB[i] = FName[StName];
}

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(" ");

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 1; // выход из ПП с ошибкой 1.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
while (CRB_temp < CWB) { // Ждём опустошения буфера
//delay(Tpp); // попробуем отключить этот delay... :-)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // обновляем значение CRB
interrupts(); // разрешение прерываний
}
Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
while (CWB > CRB_temp + 255) {// Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс)
noInterrupts();
CRB_temp = CRB; // обновляем значение CRB
interrupts();
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // нечётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
}
else{
bBit = 15;
CRB++;
}
}


Из трёх экранов для ардуин, завалявшихся у меня, решил использовать более-менее стандартный 1602, но вот только он у меня работает не по IIC-интерфейсу... Да и вообще это комплект из экрана и кнопок, именуемый "LCD Keypad Shield (https://yandex.ru/search/?text=LCD%20Keypad%20Shield&lr=38)", поэтому пришлось разрисовать схему подключения (как смог).
64310
На этой схеме подключение SD-карты смог изобразить только условно, уточнить по подключению карты можно на рисунке zebest (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=949439&viewfull=1#post949439) или в самом скетче. Кроме того, не расписаны номиналы сопротивлений (они стандартны для LCD Keypad Shield).
Если надо будет переделать на другое подключение кнопок, то это легко будет сделать минимальной правкой подпрограммы getPressedButton, для другого экрана править скетч понадобится немного больше...

Управление кнопками такое:
- кнопки "вверх" и "вниз" -- выбор файла/директории на SD
- кнопка "влево" -- перейти в выбранную директорию или запустить на воспроизведение файл
- кнопка "вправо" -- перейти в корневую директорию
- кнопка "select" пока не задействована, думаю потом добавить туда вызов настройки, например, изменение скорости воспроизведения.

На экране в верхне строке пишется имя выбранного файла, а в нижней идёт отсчёт секунд от включения ардуинки (для теста делал, потом можно будет убрать). Во время воспроизведения в нижней строке экрана пишется "Playing...", а справа от надписи выводится количество оставшихся блоков.

Скетч с библиотеками в одном архиве: 64311
Пример вывода: 64312

svofski
21.02.2018, 17:34
Это начинает походить на спор, причем немного с испорченным телефоном из-за форумности, отвлеченности на другие дела, отсуствия доски и салфетки для иллюстрации аргументов итд. Спорить охоты нет и особенно я не хочу, чтобы это выглядело, как посягательство на творческую свободу, или отнимало лишнее время. Я поддерживаю проект Improver-а в любом виде и надеюсь как-нибудь все-таки перекроить свою нану и попробовать его в деле.

Но на всякий случай обращу внимание на то, что:
* все циклы (пока не появится спящий режим) крутятся с максимальной для ардуины скоростью
* задержка систематически будет образовываться из-за отвлечения основного цикла на чтение сектора и тогда игра в догоняйку будет иметь место с зазором больше 1
* проверка булевого флажка никакого noInterrupts() не требует
* если быть совсем дотошным, вычисление этого флажка изнутри прерывания гарантировано происходит сильно задолго до следующего и абсолютно никакой лепты в джиттер не вносит
* атомарность не записи в буфер как таковой, а записи + инкремента указателя записи. А указатель фигурирует в проверке условия.

P.S. кстати об экранчиках, у меня вот такой, я его нашел в пицце у tnt23 в пакетике "DO NOT EAT" и как заказать второй такой же не знаю:
https://imgur.com/1Us47PA

Завелся он u8glib-ом так:


// HW SPI:
// 11: MOSI 13: SCK
U8G2_SH1106_128X64_NONAME_1_4W_HW_SPI u8g2(U8G2_R0, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);

Целиком немудреный скетч: https://pastebin.com/nfps6aE3

На таком экране можно было б рисовать огромное количество ненужной развлекательной информации, показывать назойливую рекламу во время загрузки ROM-файлов итд.

Improver
21.02.2018, 17:50
кстати об экранчиках, у меня вот такой, я его нашел в пицце у tnt23 в пакетике "DO NOT EAT" и как заказать второй такой же не знаю:
https://imgur.com/1Us47PA:-) У меня есть точно такой же, работает по 4SPI, но я почему-то решил, что это экзотика и использовал более распространённый 1602. И, кстати, да -- такие экранчики на том же али редко встречаются, в основном там с IIC-интерфейсом.


Завелся он u8glib-омЭтой библиотекой не пробовал пользоваться, но через Adafruit_SSD1306 он работал неплохо. Попробую его поставить...

KTSerg
21.02.2018, 17:57
А вот это разве не оно? Это первая ссылка, дальше не смотрел...
https://ru.aliexpress.com/item/free-shipping-0-96-inch-OLED-display-module-128X64-OLED-For-arduino-I2C-IIC-SPI-7p/32595065663.html
и гугля на "lcd 128x64 spi" много похожего даёт...

сам увидел, это i2c а не spi...

Не, это всё оно, интерфейс перемычками/резюками выбирается...
https://ru.aliexpress.com/item/0-96-0-96-OLED-128x64-oled-LED/32671614837.html

Improver
21.02.2018, 18:07
А вот это разве не оно? Это первая ссылка, дальше не смотрел...
https://ru.aliexpress.com/item/free-shipping-0-96-inch-OLED-display-module-128X64-OLED-For-arduino-I2C-IIC-SPI-7p/32595065663.html
и гугля на "lcd 128x64 spi" много похожего даёт...

сам увидел, это i2c а не spi...
Не, это тоже он. Они визуально отличаются количеством контактов -- есть на 7, на 6 и на 4. С интерфейсом 4SPI должно быть 7. На I2C или 3SPI он переключается перепайкой сопротивлений.

svofski
21.02.2018, 20:42
Ну вот, я думал, что это такой go to дисплей по умолчанию и даже развел свою плату расширения под него (хотя он там не нужен и я надеюсь, что плата будет полезной в основном без дисплея). А оказалось, что это какой-то редкоземельный уникальный экземпляр.

Библиотека AdaFruit была первым, что я нашел и она мне совсем не понравилась (признаюсь, что я очень предвзято и совершенно иррационально обостренно негативно отношусь к этой лавочке). Окончательно меня добило ее бредовое лицензионное требование тащить с собой сплешскрин. Независимо от моей оценки, я требования авторов уважаю и отправляю такие библиотеки в помойку.

tnt23
21.02.2018, 22:14
Экранчик, который попался svofski, брался тут:

https://www.aliexpress.com/item/OLED-1-3-inch-128-64-blue-SPI-and-IIC-Module-LCD-LED-Display-Module/32603484783.html

Если совсем пристально смотреть в корень, то от экранчика нужен собственно экранчик, подложка с джамперами не нужна. На плате разводится разъем для припайки шлейфа экранчика, нужные-ненужные ноги землятся или увешиваются конденсаторами, и все это подключается к ардуине по вкусу.

svofski
21.02.2018, 23:36
В моем случае экранчик вставляется перпендикулярно, для чего удобней подложки с пинами ничего не придумаешь.

Improver
22.02.2018, 12:48
Экран пока не поменял, но выкладываю очередную версию плеера:
Для работы требуются те же библиотеки, что и ранее: TimerOne, SdFat и LiquidCrystal.

/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Выход - D3

Подключение экрана 1602А (LCD Keypad Shield):
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
pinMode(10, OUTPUT); // CS для SD-картридера
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(currentFile); // переходим к первому файлу в директории
}

void loop() {
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

int button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
for (int i=0;12;i++){
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if ((sfileName[i+2]|0x20)=='o') { // == 'o'|'O'
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
int RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",1); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",1); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
default:
printtext("ERROR!",1); // Сообщение об ошибке
}
}
}
}
break;
}
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
printtext(" ",1); // очищаем строку 1
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
seekFile(currentFile);
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(currentFile);
break;
case BT_select: // пока что просто сообщение о нажатой кнопке, потом будет выход в настройки...
printtext("<Select>",0);
break;
case BT_none: // ничего не нажато
break;
}
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 100) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 400) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(" ");
lcd.setCursor(0,l);
lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.getName(fileName,filenameLength);
romFile.close();
maxFile++;
}
romFile.cwd()->rewind();
}

void seekFile(int pos) { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<=currentFile;i++) {
romFile.openNext(romFile.cwd(),O_READ);
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
romFile.close();
}
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
tone(p, 500, 100); // включаем на 500 Гц на 100 мс
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLs = 0x01; // начальный блок
unsigned int Nbt = romFile.fileSize(); // всего байт
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

int StName = 0;
for (i=14; i<=21; i++){ // заносим в SB имя файла
if (StName < pnt) { // имя ещё не закончилось?
SB[i] = FName[StName];
StName++;
}
else SB[i] = 0x20; // пробелы
}
StName = pnt;
for (i=22; i<=24; i++){ // заносим в SB расширение файла
StName++;
SB[i] = FName[StName];
}

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(" ");

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) {// Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // нечётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
}
else{
bBit = 15;
CRB++;
}
}

Изменил немногое, а именно:
- сделал остановку воспроизведения при нажатии любой кнопки
- немного изменил циклы задержки (которые тут много обсуждали), теперь там код выглядит так:

noInterrupts();
CRB_temp = CRB;
interrupts();
if (CWB > (CRB_temp + 255)) {
delay(Tpp);
}
т.е. теперь нет вложенных циклов, просто проверка условия и при его выполнении пустой цикл delay. Я просто вдруг заметил, что "while", стоявший там ранее, может быть исполнен максимум один раз, тогда какой в нём смысл?.. :-)

Исходники с библиотеками в одном архиве: 64320
WAV-ку не делал, т.к. думаю, что там без изменений...

И, напоследок, фотка, как сейчас выглядит плеер (для общего представления):
64321

svofski
23.02.2018, 04:29
Я опять с диверсией:

Поигрался сегодня впервые с ESP8266-12F. Он умеет быть ардуиной мощной, почти бесплатной и с вайфаем. Программировать его можно просто через адаптер usb to serial, а если залить в него OTA updater, то и эту пуповину можно оторвать и программировать из эфира. Флеша в моем попалось 32 мегабита, правда я не очень пока понимаю, сколько доступно под пользовательские данные, поскольку похоже вся фирмварь живет в ней и молотит прямо из нее, кешируясь. В Ардуинном IDE есть несколько примеров базовых вещей, в основном сетевого толка. GPIO там мало, но на SPI/I2C, Serial и еще чего-нибудь маленькое наберется. Процессор работает на 80 МГц, но не совсем понятно, какая производительность на самом деле, потому что он же обслуживает и сетевые все вещи и флеш работает серьезно медленней, чем IRAM, которого очень мало. Но думается, что если очень изловчиться, то можно было бы сделать из ESP12F и расширителя портов хитроумный эмулятор ROM-диска на ПУ. Хотя по-моему лучше сделать зашибенский магнитофон с веб-интерфейсом.

На usb-serial cp2102 набортный регулятор 3.3В 100мА и утверждается, что с ним не взлетит, но у меня с конденсатором на 2200µF стабильно летает. Потребляет больше всего, похоже, при загрузке, потом выходит на крейсерскую скорость. Потыкаться это ок, но похоже, что для гарантированной работы надо 200мА иметь.

В общем советую.

P.S. Просто SDK для нее я тоже собрал, но пока не дорос до того, чтобы выкинуть ардуино, которое для ознакомительных экспериментов самое то. Мой коварный план когда-нибудь в будущем конфигурировать с нее Циклон 4 вместо EPCS4.

KTSerg
23.02.2018, 06:29
Я опять с диверсией:

Поигрался сегодня впервые с ESP8266-12F. ...

Может в отдельную тему вынести...

Дерзай... у меня пара модулей ESP8266-12Q без дела пылятся...
А вот такой ресурс смотрел?
https://www.espressif.com/en/products/hardware/esp8266ex/resources

svofski
23.02.2018, 20:15
Я хотел именно предолжить Improver-у как интересную потенциальную платформу для эмулятора кассетофона. Я их собираюсь использовать в своей плате расширения ВУ и отвлекаться сейчас не буду.

Improver
23.02.2018, 22:40
Вообще да, ESP8266 -- штука интересная, но, думаю, сначала надо довести до ума то, что есть. Тем более, что с функцией "майфуна" справляется даже более слабая ATMega128 (в проекте "Магнитофон для Спектрума... (http://zx-pk.ru/threads/25622-magnitofon-dlya-spektruma-na-atmega128.html)"), в том числе и на запись. Возможностей же одной ESP8266 хватит, наверно, на всю периферию Вектора сразу... :-)

Improver
25.02.2018, 21:16
Хорошие новости. :-)
Во-первых, мой "направленный отрезок" заработал после более 15-и лет хранения, а во-вторых, ROM-плеер в последней редакции вполне уверенно, с первого раза и без ошибок загрузил и тест техпрогона, и даже Киберноида на скорости... точнее с таймером, установленным на период в 256 микросекунд. В общем, концепция верна, можно двигаться дальше. :-)

svofski
26.02.2018, 15:56
Поздравляю с расчехлением копья.

KTSerg
26.02.2018, 19:01
...
Исходники с библиотеками в одном архиве: ...
WAV-ку не делал, т.к. думаю, что там без изменений...

А чего нумерация строк токая:
"for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)"
по описанию протокола это вроде как нумерация повторов... :)

А "регулировку громкости" - не делал?
Или просто в схему выхода резюк-переменник воткнул?

Improver
27.02.2018, 17:20
Поздравляю с расчехлением копья.
Спасибо, хотя не всё там хорошо: "паралонки" на кнопках разложились -- клавиатура не работает, видео только ч/б, да и кадры плывут на современных теликах... Хотя, для тестов хватит, а потом буду восстанавливать по мере возможности.


А чего нумерация строк токая:
"for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)"
по описанию протокола это вроде как нумерация повторов... :)
Я ориентировался на это описание: ссылка (в конце страницы) (http://emu80.org/dev/dev_v.html), есть та же инфа в сканах печатных изданий, но так сразу не найду... В общем, 0x80-0x87 -- первый вывод блока, 0x88-0x8F -- повтор.


А "регулировку громкости" - не делал?
Или просто в схему выхода резюк-переменник воткнул?
В начале просто подключил массу и D3 ардуины на вход, потом сделал через конденсатор 0,33мКф. Кстати, случайно выбранная задержка в 256 мкс оказалась удачной -- без конденсатора на 248 мкс уже не грузится, с конденсатором чуть лучше. Эмпирическими расчётами прикинул, что максимальная скорость, с которой выводил данные Вектор, должна быть при полупериоде около 216 мкс, а наименьшая -- 400 мкс.

Интересное наблюдение: копировщик "copy v2.1" может грузить данные без ошибок на скорости от 232 мкс, в отличие от всех других загрузчиков, в том числе и начального, из ПЗУ. С чем это связано -- не понятно... Видимо, там код более оптимизированный. (Всем другим копировщикам нужно не меньше 264 мкс.)

Опять же, возможно на выходе надо собрать более приличную схемку, как в других "плеерах", или даже просто убрать "сопли", и скорости возрастут. Или попробовать подавать сигнал прямо на вывод 13 микросхемы Д30 (КР580ВВ55А).

KTSerg
27.02.2018, 18:36
Я ориентировался на это описание: ссылка (в конце страницы), есть та же инфа в сканах печатных изданий, но так сразу не найду... В общем, 0x80-0x87 -- первый вывод блока, 0x88-0x8F -- повтор.
Да, действительно...
Пардон, из другой "оперы" видать вылезло...

ivagor
27.02.2018, 18:49
копировщик "copy v2.1" может грузить данные без ошибок на скорости от 232 мкс, в отличие от всех других загрузчиков, в том числе и начального, из ПЗУ. С чем это связано -- не понятно
copy 2.1 более корректно определяет константу чтения, чем большинство ПЗУшных загрузчиков.

svofski
27.02.2018, 23:40
Я бы потолще конденсатор взял, 0.33 суховато как-то.


"паралонки" на кнопках разложились
Мой несостоявшийся проект века был конверсией поролоновой клавиатуры в настоящую ёмкостную, которая работает. Я уверен, что это возможно. Поролон пришлось бы все равно раскроить новый, правда.

KTSerg
28.02.2018, 05:59
Про клаву.

...
Мой несостоявшийся проект века был конверсией поролоновой клавиатуры в настоящую ёмкостную, которая работает. Я уверен, что это возможно. Поролон пришлось бы все равно раскроить новый, правда.

Я где-то уже рассказывал, что у меня есть (была) древняя, тяжеленная, настоящая ёмкостная клава от РС-ХТшки.
Когда я её разбирал и изучал "как работает", то обнаружил в поддоне стальную, заземленную пластину. Как только отключал заземление или поднимал от этой пластины плату клавиатуры, клава сразу переставала работать, из-за помех. Но в собранном состоянии работала отлично.

Improver
28.02.2018, 15:54
Новая версия плеера:
Для работы требуются те же библиотеки, что и ранее: TimerOne, SdFat и LiquidCrystal.

/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Выход - D3

Подключение экрана 1602А (LCD Keypad Shield):
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
pinMode(10, OUTPUT); // CS для SD-картридера
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(currentFile); // переходим к первому файлу в директории
}

void loop() {
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

int button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
if (Nbt != 0xFFFF) { // проверяем размер файла
for (int i=0;12;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
int RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",1); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",1); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
default:
printtext("ERROR!",1); // Сообщение об ошибке
}
}
}
}
break;
}
}
}
else {
printtext("File is too big",1);
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
printtext(" ",1); // очищаем строку 1
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
seekFile(currentFile);
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
seekFile(currentFile);
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(currentFile);
break;
case BT_select: // выход в настройки...
printtext("Period(mks):",0);
do {
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
delay(200);
button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_up:
if (Tpp<400) {
Tpp += 8;
}
break;
case BT_down:
if (Tpp>200) {
Tpp = Tpp - 8;
}
break;
}
lcd.setCursor(12,0);
lcd.print(Tpp);
}
while(button!=BT_select); // Цикл пока не будет снова нажата кнопка select
seekFile(currentFile);
break;
case BT_none: // ничего не нажато
break;
}
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 100) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 400) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(" ");
lcd.setCursor(0,l);
lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.close();
maxFile++;
}
}

void seekFile(int pos) { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
romFile.openNext(romFile.cwd(),O_READ);
romFile.close();
}
romFile.openNext(romFile.cwd(),O_READ); // читаем данные текущего файла
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
if (romFile.fileSize()<=49152) { // проверка размера файла
Nbt = romFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
romFile.close();
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
romFile.dirEntry(&d); // Считываем дату файла
uint16_t AAA = FAT_DAY(d.lastWriteDate); // Сохраняем дату файла в заголовке -- день
SB[8] = (AAA%100)/10 + '0'; // перевод из целого в символ
SB[9] = AAA%10 + '0';
AAA = FAT_MONTH(d.lastWriteDate); // месяц
SB[10] = (AAA%100)/10 + '0';
SB[11] = AAA%10 + '0';
AAA = FAT_YEAR(d.lastWriteDate); // последние две цифры года
SB[12] = (AAA%100)/10 + '0';
SB[13] = AAA%10 + '0';

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(" ");

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // нечётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
}
else{
bBit = 15;
CRB++;
}
}
Изменения:
- Сделал регулировку периода таймера: вызов через кнопку "select", изменение кнопками "вверх" и "вниз", применение изменений по кнопке "select". Период можно менять в пределах 200...400 мкс.
- Немного изменил процедуру вывода имени файла в заголовках блоков: теперь имена преобразуются в заглавные буквы и тильда -- в подчёркивание.
- Сделал проверку размера файла -- он должен быть не больше 48Кб.
- Сделал возможность вывода файлов с нулевого блока. Т.е. файлы с расширением "rom" теперь выводятся с первого блока, а с расширением "r0m" -- с нулевого.

По последнему пункту вопрос: есть ли какая-то система именования файлов с указанием номера блока, которую можно считать "де факто" стандартом? Например, "*.Rxx", где xx -- шестнадцатиричный номер первого блока...

А в остальном, фактически, проект готов -- осталось сделать варианты под другие экраны/кнопки, ну и, естественно, исправлять баги. :-)

Исходники с библиотеками в одном архиве: 64393

- - - Добавлено - - -


Я бы потолще конденсатор взял, 0.33 суховато как-то.Я попробовал несколько, но лучше всего пошло с этим (видимо те слишком большие были, или потому что электролиты). Думаю попробовать собрать схемку из проекта для спектрума (http://zx-pk.ru/threads/25622-magnitofon-dlya-spektruma-na-atmega128.html) (D3 подключать справа к сопротивлению R8 в этой схеме):

64394


Мой несостоявшийся проект века был конверсией поролоновой клавиатуры в настоящую ёмкостную, которая работает. Я уверен, что это возможно. Поролон пришлось бы все равно раскроить новый, правда.На основных клавишах я заменил разложившийся поролон на вспененный полиэтилен, а остальные пока не трогал -- надо найти трубку с внутренним диаметром ~13,5 мм для вырубки, а то ножницами кривовато получается...

KTSerg
28.02.2018, 16:00
... На основных клавишах я заменил разложившийся поролон на вспененный полиэтилен, а остальные пока не трогал -- надо найти трубку с внутренним диаметром ~13,5 мм для вырубки, а то ножницами кривовато получается...
Скрутить кольцо из полоски жести...

Improver
28.02.2018, 16:05
Скрутить кольцо из полоски жести...Как вариант -- да. Но это уже оффтоп, для этого есть отдельная тема по клавиатуре... :-)

svofski
28.02.2018, 16:42
По последнему пункту вопрос: есть ли какая-то система именования файлов с указанием номера блока, которую можно считать "де факто" стандартом? Например, "*.Rxx", где xx -- шестнадцатиричный номер первого блока...
Могу только сказать, что в моих эмуляторах есть только различие между r0m и rom (так же com/bin/vec) и всех устраивает. С кассеты можно грузить куда попало в какой угодно последовательности (см тему с кассетными экспериментами Tim0xa-и), но я никогда не слышал, чтобы это использовалось на практике.

- - - Добавлено - - -

Может электролит другой стороной поставить? По идее плюсом к ардуине. 10мКф из Спектрумофона мне больше нравится, к тому же 10мкф легко можно найти керамику. Аудиофилы не одобрят, но Вектор стерпит. Что делает R8 в схеме из проекта для Спектрума? Может быть есть комплементарный ему последовательный с ножкой ардуины, так что они вместе образуют делитель?

KTSerg
28.02.2018, 19:48
... С кассеты можно грузить куда попало в какой угодно последовательности (см тему с кассетными экспериментами Tim0xa-и), но я никогда не слышал, чтобы это использовалось на практике. ...
Помню, что попадалась "защита от копирования", часть программы грузилась прямо в экранную область. Загрузка прекращалась, когда загружаемые данные натыкались на стек, расположенный вверху справа. Я всё помню удивлялся, что "кубики" рисуемые загрузчиком "затирают" код программы, как она после этого работает... Оказалось, что программа после старта, сначала "собирает/склеивает" себя из кусочков, которые остались в промежутках загрузочной сетки...

Кстати, я собрал этот кусок выходного каскада, как в схеме. Только заменил резюк 3К на переменник, и к ползунку подключил этот резюк на 10. Должна получиться регулировка громкости. Правда ещё не пробовал. Адаптирую код под LPC-проц.

svofski
28.02.2018, 22:25
Прикольно про защиту. Было бы любопытно кассетные оригиналы этих защит найти. В архивах то все уже сломано, конечно.


Кстати, я собрал этот кусок выходного каскада, как в схеме. Только заменил резюк 3К на переменник, и к ползунку подключил этот резюк на 10. Должна получиться регулировка громкости.
В таком виде я понимаю, это делитель (и немножко регулятор тембра). А когда там просто 3К стоит, странно.

Ramiros
28.02.2018, 22:52
Прикольно про защиту. Было бы любопытно кассетные оригиналы этих защит найти.

Вот, есть у меня одна

KTSerg
28.02.2018, 23:33
У меня тож, заработал... пока без дисплея и кнопок, управление по СОМ-порту с РС...
Сильно не экскрементировал, получается с моей схемой выходного каскада, на "громкости" чуть меньше максимума уверенно грузит с периодом таймера 210мкс. При таймере 190 гарантированы "пропуски".
Отличие номиналов от схемы (собирал из того, что попалось под руку) резюк не 3К а 4.7К , 10Мкф(плюсом к процессору), кондёр пикушник не 10 а 120.

Пару часов промаялся, пока нашел прикол компилятора, раньше с таким не сталкивался...
Все переменные объявлены как BYTE, ожидал, что и результат вычисления тоже будет BYTE.
в строке : while((IndW+1)==IndR);
никогда не останавливался, хотя я ожидал, что будет крутиться пока IndW на 1 меньше чем IndR.
Заработал только в варианте: while((BYTE)(IndW+1)==IndR);

ivagor
01.03.2018, 08:02
Вот, есть у меня одна
Это результат работы TURBO-Copy (http://www.sensi.org/scalar/ware/719/) VladTru, таких можно много наделать.
Мне встречалась защита в виде уникального формата (для которого нет копировщиков) на сборнике игрушек из Кишинева. Тогда эти хитрые форматы меня не очень интересовали, зато нужна была возможность копирования и все игрушки оттуда я хакнул в обычный rom, даже не уверен, что осталась оригинальная запись. Как называлась "фирма" - не помню.
Еще у меня был BASIC-M с оригинальной защитой от кишиневского центра "Компьютер". И вроде его я не хакал, но у меня из другого источника была ломаная версия.
Центр "Компьютер", кстати, по крайней мере на коммерческом пике своей деятельности, писал игрушки и программы в каком-то хитром формате с хитрым загрузчиком. Вроде там при загрузке подобно спеку шли цветные полосы (в basic-m был другой загрузчик).

Improver
01.03.2018, 10:21
Может электролит другой стороной поставить? По идее плюсом к ардуине.
Тот, который у меня 0,33мКф -- не электролит... Хотя я пробовал электролиты тоже, и да, подключал плюсом на ардуину.


Что делает R8 в схеме из проекта для Спектрума? Может быть есть комплементарный ему последовательный с ножкой ардуины, так что они вместе образуют делитель?Не, ничего там нет, по той схеме (http://trolsoft.ru/ru/sch/zx-tapper) дальше подключается прямо на ногу атмеги.

- - - Добавлено - - -


У меня тож, заработал... пока без дисплея и кнопок, управление по СОМ-порту с РС...Замечательно! Поделись тут своим скетчем, для общего блага и вселенского счастья. :-)


Сильно не экскрементировал, получается с моей схемой выходного каскада, на "громкости" чуть меньше максимума уверенно грузит с периодом таймера 210мкс. При таймере 190 гарантированы "пропуски".Оу... Надо делать выходную схему, однозначно. И, возможно, тогда пределы изменения периода в скетче можно будет расширить.

Кстати, разглядывая схемы Вектора я заметил, что на некоторых входной каскад был сделан на компараторе К554СА3А и подтяжкой выхода к +5В, а на некоторых на операционном усилителе К554УД1А и сопротивлением на землю, с этим тоже может быть связано отличие в скоростях приёма данных.

KTSerg
01.03.2018, 11:01
...
Центр "Компьютер", кстати, по крайней мере на коммерческом пике своей деятельности, писал игрушки и программы в каком-то хитром формате с хитрым загрузчиком. Вроде там при загрузке подобно спеку шли цветные полосы (в basic-m был другой загрузчик).
Да, что-то припоминается, что и для Вектора было, на кассете перед программой шел "загрузчик" в 1 блок, он запускался, потом шла программа в "своём" формате. Но тоже, его форматом как-то не интересовался... Только было понятно, что там инфа была без повторов. Малейший дефект ленты и запись в ведро... Кстати, именно этот факт был стимулом "ломануть", поскольку магнитофоны очень любили жувать ленту... а терять купленное было жалко... Правда вроде-бы на кассету записывали такие проги два раза, но не сильно спасало...

- - - Добавлено - - -




У меня тож, заработал... пока без дисплея и кнопок, управление по СОМ-порту с РС...
Замечательно! Поделись тут своим скетчем, для общего блага и вселенского счастья. :-)

Пока изучаю библиотеку, на данный момент в коде "чётр ногу сломит". Немного разберусь, причешу, будет видно...
У меня вот ардуинный дисплей с тачем давно без дела лежит, может интерфейс на него выведу.

svofski
01.03.2018, 15:33
Кстати, разглядывая схемы Вектора я заметил, что на некоторых входной каскад был сделан на компараторе К554СА3А и подтяжкой выхода к +5В, а на некоторых на операционном усилителе К554УД1А и сопротивлением на землю, с этим тоже может быть связано отличие в скоростях приёма данных.
https://i.imgur.com/p5m2Kfs.png
Пытаюсь постичь замысловатую входную схему. RC фильтр из R38=3K3, C57=2200, где-то 22кГц получается частота среза. R41 и R40 для привязки к нулю? Почему R38 стоит перед C47?

Если входной фильтр c Fc=22КГц есть, то выходной с Fc=1.5МГц на кассетоимитаторе ничего, кроме труднопредсказуемых искажений внести не может (два пассивных каскада подряд -> меняется выходной импеданс первого каскада -> характеристика второго каскада плывет в зависимости от частоты). Это все конечно не существенно, поскольку 1.5МГц — это только снять фаску с высших гармоник и уменьшить эмиссию в эфир.

svofski
01.03.2018, 17:50
Все переменные объявлены как BYTE, ожидал, что и результат вычисления тоже будет BYTE.
в строке : while((IndW+1)==IndR);
никогда не останавливался, хотя я ожидал, что будет крутиться пока IndW на 1 меньше чем IndR.
Заработал только в варианте: while((BYTE)(IndW+1)==IndR);
Это интуитивно очевидно, но я не сразу вспомнил, как это правило называется.

Глава 2.8.1.1. Integral promotions.

http://publications.gbdirect.co.uk/c_book/chapter2/expressions_and_arithmetic.html

В общих чертах: все целочисленные выражения независимо от типа аргументов вычисляются в типе int, если таковой их в себя вмещает. То есть unsigned char + unsigned char == int.

Improver
02.03.2018, 10:47
Исправление ошибок, как и обещано :-)

/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Выход - D3

Подключение экрана 1602А (LCD Keypad Shield):
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
pinMode(10, OUTPUT); // CS для SD-картридера
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(); // переходим к первому файлу в директории
}

void loop() {
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

int button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
seekFile();
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
if (Nbt != 0xFFFF) { // проверяем размер файла
for (int i=0;12;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
int RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",1); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",1); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
default:
printtext("ERROR!",1); // Сообщение об ошибке
}
seekFile();
}
}
}
break;
}
}
}
else {
printtext("File is too big",1);
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
printtext(" ",1); // очищаем строку 1
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
seekFile();
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
seekFile();
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile();
break;
case BT_select: // выход в настройки...
printtext("Period(mks):",0);
do {
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
delay(200);
button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_up:
if (Tpp<400) {
Tpp += 8;
}
break;
case BT_down:
if (Tpp>200) {
Tpp = Tpp - 8;
}
break;
}
lcd.setCursor(12,0);
lcd.print(Tpp);
}
while(button!=BT_select); // Цикл пока не будет снова нажата кнопка select
seekFile();
break;
case BT_none: // ничего не нажато
break;
}
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 100) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 400) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(" ");
lcd.setCursor(0,l);
lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.close();
maxFile++;
}
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
romFile.openNext(romFile.cwd(),O_READ);
romFile.close();
}
romFile.openNext(romFile.cwd(),O_READ); // читаем данные текущего файла
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
if (romFile.fileSize()<=49152) { // проверка размера файла
Nbt = romFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
romFile.close();
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
romFile.dirEntry(&d); // Считываем дату файла
uint16_t AAA = FAT_DAY(d.lastWriteDate); // Сохраняем дату файла в заголовке -- день
SB[8] = (AAA%100)/10 + '0'; // перевод из целого в символ
SB[9] = AAA%10 + '0';
AAA = FAT_MONTH(d.lastWriteDate); // месяц
SB[10] = (AAA%100)/10 + '0';
SB[11] = AAA%10 + '0';
AAA = FAT_YEAR(d.lastWriteDate); // последние две цифры года
SB[12] = (AAA%100)/10 + '0';
SB[13] = AAA%10 + '0';

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(" ");

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // нечётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
}
else{
bBit = 15;
CRB++;
}
}

Исправлен глюк, от которого повторное воспроизведение того же файла не работало. Ну и плюс к тому, убрал ненужный параметр у процедуры seekFile().

Архив скетча в полном комплекте: 64421

- - - Добавлено - - -

По выходному каскаду: собрал я опубликованную тут чуть ранее (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=952287&viewfull=1#post952287) схемку, но с ней всё плохо... Скорости выше, чем 264мкс нет. Стал подбирать выходной конденсатор, и оказалось, что чем он меньше, тем лучше, начиная примерно от 1 мкФ идёт ухудшение скорости. Тот самый мой 0,33 мкФ, пожалуй, самый большой по ёмкости, с которым работает с задержкой от 232мкс. Ставил там даже 0,0068 мкФ -- работает не хуже, чем 0,33 мкФ. В общем, не понятно...

Вскрыл свой Вектор и глянул -- на входе там стоит вообще другая микросхема, не та, что в схеме, а К553УД2. Возможно они и взаимозаменяемы с К553УД1В. И ещё, интересный момент -- в даташитах на эти микросхемы пишут, что им нужно питание +/-15В, у нас же они запитаны от +/-5В, т.е. работают не в штатном режиме... Странно... Хотя в них может быть заложен больший потенциал, чем указано в документации и в военное время вообще весь Вектор можно было запитать от одной пальчиковой батарейки. :-)))

И ещё, по выходу rom-плеера: в ридми к проекту TZXDuino (https://github.com/sadken/TZXDuino) пишут, что на выход надо ставить простейший аналоговый усилитель, типа LM386 или подобного, надо попробовать что-нибудь такое. Но если уж и так ничего не выйдет -- не беда, пусть будет у меня 256 мкс.


Пытаюсь постичь замысловатую входную схему.
Думаю, надо эту схему (вместе с выходной плеера) сделать в эмуляторе и посмотреть, как она работает на разных частотах...

- - - Добавлено - - -


грузит с периодом таймера 210мкс. При таймере 190 гарантированы "пропуски".
Кстати, встречал в доках, что период таймера 1 желательно выставлять кратным 4, а лучше 8...

KTSerg
02.03.2018, 11:02
...

грузит с периодом таймера 210мкс. При таймере 190 гарантированы "пропуски".
Кстати, встречал в доках, что период таймера 1 желательно выставлять кратным 4, а лучше 8...
У меня макетка на проце LPC2146. В таймере два регистра деления частоты (с 60МГц) в первом поставил 59, во втором 210. Логгер показывает почти 210мкс.

svofski
02.03.2018, 14:45
в ридми к проекту TZXDuino пишут, что на выход надо ставить простейший аналоговый усилитель, типа LM386 или подобного, надо попробовать что-нибудь такое.
Если уменьшать размах сигнала делителем, то получившийся сигнал желательно усилить при подаче на вход с низким импедансом, иначе нагрузка будет перекашивать собой делитель. Для этого должно быть достаточно простого эмиттерного повторителя. Но зачем? КМОП-выход ардуины и так мощен, качает 20мА в любую сторону. Входной импеданс у схемы в Векторе при 5кгц порядка 20К, если я не ошибся. Если сделать выходной делитель из 1К резистора, то соотношение импедансов будет в районе 1:10. То есть нагрузка подкузьмит максимум на одну десятую, это вполне терпимо должно быть.


в даташитах на эти микросхемы пишут, что им нужно питание +/-15В, у нас же они запитаны от +/-5В, т.е. работают не в штатном режиме
Это наверное рекомендованный максимальный размах. Чем больше размах питания, тем шире динамический диапазон и тем шире диапазон допустимых входных напряжений. Поэтому в общем случае, если нет других ограничений, желательно иметь возможность сделать размах побольше. У нас же тут всего 1 бит динамики и уровень входного сигнала в пределах 1В, так что все должно быть нормально.

Improver
02.03.2018, 21:56
Сделал небольшой анализ того, что происходит на входе Вектора... Для начала, несколько скриншотов графиков того, что происходит по схеме, которую тут (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=952408&viewfull=1#post952408) запостил svofski, т.е. на компараторе К554СА3:
1. "Стандартная" ситуация -- входной сигнал прямоугольной формы с периодом 512 мкс и размахом +/-1,2В:
64434
* Зеленый график -- входной сигнал
* Синий график -- на выводе "-" компаратора
* Красный график -- на выводе "+" компаратора

2. Сигнал с rom-плеера с конденсатором 10 мкФ на выходе:
64435

3. И то же самое с конденсатором 0,1 мкФ (для информации):
64436

В этом варианте видно, что читаться информация явно не будет...


Теперь схема моего Вектора (фоткал из прилагаемой документации):
64433
Схема практически полностью отличается, что, собственно, объясняет различие в ёмкости выходных конденсаторов на rom-плеере. Вот что у меня получается по графикам:
1. Входной сигнал прямоугольной формы с периодом 512 мкс и размахом +/-1,2В:
64437
* Синий график -- входной сигнал
* Зеленый график -- на выводе "-" усилителя

2. Сигнал с rom-плеера с конденсатором 10 мкФ на выходе:
64438
Видно достаточно плавное понижение сигнала до симметричного относительно "0" положения. Подозреваю, что из-за этого на высоких скоростях передачи сигнал просто не успевает выйти в нужный режим для корректного определения скорости.

3. И, наконец, то же самое с конденсатором 0,33 мкФ:
64439
Выходит, что по каждой конкретной модели/версии Вектора на схему выхода rom-плеера надо будет немного менять... И, опять же, надо ещё попробовать поставить усилитель -- может это будет универсальное решение.

Все картинки в архиве (а то что-то форум их зажимает до неприличия): 64440

svofski
03.03.2018, 01:24
Мой вариант потешиться лишь бы бредборду не доставать. Мой любимый falstad.com на этой схеме совсем плохо себя показал, поэтому в EveryCircuit: http://everycircuit.com/circuit/5667966850170880/tapeout-tapein

Можно менять на ходу параметры. Для меня неожиданным оказалось то, как меняется постоянная составляющая в зависимости от соотношения C-R (100nF/1K). Если увеличивать C, то все смещается и становится несимметричным относительно нуля. Это более-менее в согласии с опытом Improver-а. Возможно эта асимметрия и оказывает пагубное влияние на загрузку.

Improver, у тебя на части схем C4 2.2нф, что соответствует обычной кишиневской схеме, а на других - 2.2мкф. 2.2мкф не многовато?

KTSerg
03.03.2018, 07:47
Мой вариант потешиться лишь бы бредборду не доставать. Мой любимый falstad.com на этой схеме совсем плохо себя показал, поэтому в EveryCircuit: http://everycircuit.com/circuit/5667966850170880/tapeout-tapein

Можно менять на ходу параметры. Для меня неожиданным оказалось то, как меняется постоянная составляющая в зависимости от соотношения C-R (100nF/1K). Если увеличивать C, то все смещается и становится несимметричным относительно нуля. Это более-менее в согласии с опытом Improver-а. Возможно эта асимметрия и оказывает пагубное влияние на загрузку.
...
Спасибо, круто.
В таком варианте мне показалось, наилучшим соотношением 1mF/10K.
А что за резистор 1К, после 100nF ?
Его вроде нет в схемах.
Можно как-то в симуляторе его удалить, и посмотреть результат?

svofski, а что, этот циркуит триал только 24 часа?

Improver
03.03.2018, 08:30
Improver, у тебя на части схем C4 2.2нф, что соответствует обычной кишиневской схеме, а на других - 2.2мкф. 2.2мкф не многовато?С4 = С57 на схеме, там написана ёмкость 2,2н? Что-то показалось, что 2,2м... Не беда -- поправим. :-)

- - - Добавлено - - -


в EveryCircuit: http://everycircuit.com/circuit/5667...tapeout-tapeinА можно хоть пару скриншотов оттуда с результатами? А то ставить хром ради этого нет никакого желания...

KTSerg
03.03.2018, 09:22
А можно хоть пару скриншотов оттуда с результатами? А то ставить хром ради этого нет никакого желания...
Вроде написано, что на Андроиде и на Яблоке будет работать...

KTSerg
03.03.2018, 13:58
Посидел с осциллографом, посмотрел, что с сигналом при разных номиналах кондёров.
Использовал Вектор06Ц.02, там стоит 554СА3А, со схемой совпадает.
ROM-плеер просто гнал частоту 2КГц.
В выходной каскад со схемы zx-tapper-а вносил изменения.

R8 - поменял на переменник, вроде-как должен выполнять функцию регулировки "громкости".

Добавлял переменник R* (в итоге, по результатам убрал).
Добавленный R* - вносил искажения, при уменьшении сопротивления.

С13 изначально у меня стоял 120n, осциллограф показал, что при уменьшении "громкости" на выходе СА3 меняется скважность сигнала - увеличивается продолжительность положительного импульса, за счет отрицательного.
Поменял С13 на 10n - "громкость" практически перестала влиять на скважность.

С12 - пробовал менять в диапазоне от 0.47мкФ до 10мкФ, по результатам поставил планарный 4.7мкФ. Малые емкости давали "пилу" на выходе плеера.

tnt23
03.03.2018, 14:54
По-моему, странное включение потенциометра R*. По идее сигнал хорошо бы снимать с его движка (средней точки), оставив крайние концы подключенными к сигналу и земле.

KTSerg
03.03.2018, 15:25
По-моему, странное включение потенциометра R*. По идее сигнал хорошо бы снимать с его движка (средней точки), оставив крайние концы подключенными к сигналу и земле.
Я не понял, что это за резистор в схеме, которую эмулировал svofski в EveryCircuit, решил проверить его влияние на сигнал. А переменник, что-бы проверить его разные значения.

svofski
03.03.2018, 18:14
Резистор на землю я поставил потому что иначе в симуляции получался сигнал перекошенный относительно нуля. У меня тут серьезные вопросы к симуляторам, потому что похоже, что им всем тут сносит башню, причем всем по разному (а до более-менее порядочного в этих вопросах LTSpice мне не дойти, не расчистив предварительно пол). Без редактора выкинуть резистор нельзя, но можно сделать ему значение какое-нибудь огромное.

EveryCircuit я знаю только в качестве Андроидного приложения и хромную версию сам впервые увидел вчера, когда расшарил. Он забавный, если расценивать его как игру типа "собери мультивибратор" и наверное стоит своих монет, но верить ему в вопросах столь важных, как загрузка Вектора с магнитофона, я бы не стал.

Improver
03.03.2018, 18:48
Итак, вот что даёт схема на к554са3 с исправленным конденсатором: 64452
В архиве скриншоты тех же трёх вариантов, что и в сообщении #79 (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=952607&viewfull=1#post952607) (картинки сюда не вставляю, т.к. нет особого смысла -- они всё равно будут сконвертированы до нечитабельного состояния).
В общем, на основе всего этого можно увидеть, что с компаратором схема явно работает лучше и выход rom-плеера можно сделать практически любым из предложенных ранее вариантов.


Я не понял, что это за резистор в схеме, которую эмулировал svofski в EveryCircuit, решил проверить его влияние на сигнал. А переменник, что-бы проверить его разные значения.
Такое подключение таит в себе опасность сделать короткое замыкание выхода на землю, если выкрутить его в крайнее положение...

- - - Добавлено - - -


Вроде написано, что на Андроиде и на Яблоке будет работать...Работает, но пишет, что нужно смотреть только из хрома... :-(

KTSerg
03.03.2018, 19:09
...
EveryCircuit я знаю только в качестве Андроидного приложения и хромную версию сам впервые увидел вчера, когда расшарил. Он забавный, если расценивать его как игру типа "собери мультивибратор" и наверное стоит своих монет, но верить ему в вопросах столь важных, как загрузка Вектора с магнитофона, я бы не стал.
Не знаю, есть ли в нём "синхронизация", как в осциллографе, на бегущем графике не видно характеристик сигнала... длительность импульсов/скважность...

Такое подключение таит в себе опасность сделать короткое замыкание выхода на землю, если выкрутить его в крайнее положение...
Там с обоих сторон резистора стоят последовательно конденсаторы, не думаю, что сразу что-то сгорит... для входа значения не имеет, а выход через 10 Ом и "громкость", так-что не коротнуть...

tnt23
03.03.2018, 21:24
(а до более-менее порядочного в этих вопросах LTSpice мне не дойти, не расчистив предварительно пол)

С некоторых пор Spice стали включать в девелоперскую версию KiCad, чтобы не заставлять людей почем зря пол расчищать.

svofski
03.03.2018, 22:23
tnt23, это тот же компьютер.

svofski
05.03.2018, 15:53
Разгреб пол и поиграл в LTSpice. Сколько я не бился, я не могу придумать, чтобы что-то объективно было бы лучше просто проводка от ардуины прямо в магнитофонный вход, безо всяких делителей и конденсаторов.

В симуляции в начале перекос скважности всегда, потому что переходный процесс в C47-(R41+R40). В жизни этот процесс устаканивается за долю секунды во время начального писка. Эта схема ведет себя не фонтан даже с идеальным источником на входе, так что я не представляю, что бы еще можно было улучшить.

На всякий случай мой .asc от LTSpice: https://pastebin.com/VTPqARzQ

Improver
06.03.2018, 08:59
Сколько я не бился, я не могу придумать, чтобы что-то объективно было бы лучше просто проводка от ардуины прямо в магнитофонный вход, безо всяких делителей и конденсаторов.Эти эксперименты сподвигли меня вчера провести свой -- я всё же попробовал подать цифровой сигнал с ардуины прямо на вход РС4 контроллера Д30 Вектора... То есть мимо всех конденсаторов, делителей, компараторов -- только цифра в чистом виде. И результат, скажу прямо, меня не порадовал: скорость передачи удалось поднять всего лишь на один шаг (8 мкс). А если к этому ещё прибавить опыт KTSerg, который собрал плеер на другой, более быстрой элементной базе, то можно сделать неутешительный вывод, что не так всё хорошо с выводом сигнала в rom-плеере. :-(

Думаю, надо будет снять ещё одну wav-ку сигнала, но с частотой дискредитации 96кГц, плюс такую же с живого Вектора и сравнить циклы "под микроскопом"...

KTSerg
06.03.2018, 09:32
Ну, есть ещё вариант, интегрировать в ROM-плеер "турбозагрузчик".
Что-бы при выборе файла для загрузки, делался анализ размера файла.
Для "больших" файлов, сначала грузить в Вектор "турбозагрузчик", а потом сам файл (соответственно в формате турбозагрузчика, а не ROM).
Но думаю возникнет аналогичная проблема с пополнением буфера отправки и своевременным "подчитыванием" данных с SD-карты. Ведь скорость выгрузки из буфера будет выше, а времени на чтение SD-карты меньше (т.к. прерывания чаще).
А может я и не прав...

Improver
06.03.2018, 10:59
Ну, есть ещё вариант, интегрировать в ROM-плеер "турбозагрузчик".
Можно пойти ещё дальше -- грузить свой загрузчик, который будет принимать данные по ПУ, о чём я уже упоминал тут (http://zx-pk.ru/threads/24473-sd-rom-kartridzh.html?p=949255&viewfull=1#post949255), тогда можно будет раз в десять обогнать и турбо-загрузчик... :-) Вообще, думаю, пока не стоит играться с загрузчиками -- оставим эту идею на потом.


Но думаю возникнет аналогичная проблема с пополнением буфера отправки и своевременным "подчитыванием" данных с SD-карты.Не, ардуина справится -- я тестировал с задержкой таймера в 64 мкс и опустошения буфера не было.

svofski
06.03.2018, 11:58
Improver, ты кажется говорил, что в какой-то копировщик с твоей ардуины грузится лучше. Может быть у KTSerg другой загрузчик в ПЗУ, оттого и получается разогнать быстрее.

Improver
06.03.2018, 12:50
svofski, да, так и есть -- в стандартный копировщик "COPY v2.1" может грузиться с таймингом от 232 мкс, и в случае прямого подключения, как показал вчерашний эксперимент, от 224 мкс, но это далеко от тех 190-200 мкс у KTSerg, которые практически совпадают с расчётными величинами для скорости "21" (или 16h) Вектора.

ivagor
06.03.2018, 17:50
Хакнул copy21. В VV теперь грузит на максимале, в emu без особых изменений. Как на реале - сложно сказать.

svofski
06.03.2018, 19:17
ivagor, а ты не хотел бы сделать новый 2к загрузчик, исправленный и дополненный?

ivagor
06.03.2018, 19:27
Можно попробовать, но есть большой вопрос - что именно исправлять и дополнять? Думаю, что у каждого векториста есть свое представление, что должно быть в идеальном загрузчике и как это должно работать. Ну и возможности и знания (мои или другого человека, который соберется править загрузчик) ограничены. Хотя непонятные моменты можно просто скопипастить.

svofski
06.03.2018, 20:45
Вот. Основная идея не всех победить, а получить такой сборный загрузчик, который можно собрать себе по вкусу. Например, ценность загрузки по ЛВС или 0-момеду мне сейчас кажется нулевой, потому что я не собираю исторический (или мифический) КУВТ. А загрузчик, который бы понимал твой турбо-формат без предварительной загрузки загрузчика, иметь было бы ценно — особенно при том, что я в обозримом будущем предвижу какую-то низкоуровневую отладку на железе. Ну, или хотя бы загрузчик, который стабильно точно определяет временную постоянную так, что можно разогнать стандартный формат чуть шустрее. Или еще вариант: вместо существующих сейчас альтернативных способов загрузки сделать свой новый, который будет проще поддержать простенькой ардуиной ибо это то, что сейчас людям проще и ближе.

- - - Добавлено - - -

P.S. подумалось, что вообще надо делать плату эмулятора ПЗУ, которая будет ставиться в ту же колодку, но гибко позволять себя конфигурировать и грузить все на свете, в том числе из эфира. Разумеется, у меня все мысли сразу в сторону esp8266, видимо в упряжке с какой-то цплдовиной для сопряжения с шиной.

ivagor
06.03.2018, 21:07
В качестве задачи первого этапа можно попробовать взять дизассемблирование и комментирование (хотя бы в важных местах) загрузчика Tim0xи (все же это самый современный вариант). В идеале загнать в условную компиляцию источники загрузки. Вполне подъемно, но не интересно (самый главный минус). Может займусь, или может еще кто захочет.
Но если ты сделаешь эмулятор ПЗУ, загружаемый из всего на свете, то смысл ковырять загрузчик вобще потеряется.

KTSerg
06.03.2018, 21:49
Вот. Основная идея не всех победить, а получить такой сборный загрузчик, который можно собрать себе по вкусу. Например, ценность загрузки по ЛВС или 0-момеду мне сейчас кажется нулевой...
P.S. подумалось, что вообще надо делать плату эмулятора ПЗУ, которая будет ставиться в ту же колодку, но гибко позволять себя конфигурировать и грузить все на свете, в том числе из эфира. Разумеется, у меня все мысли сразу в сторону esp8266, видимо в упряжке с какой-то цплдовиной для сопряжения с шиной.
Эмулятор ПЗУ подразумевает два подхода.
Первый - загрузка с эмулятора по протоколу ПЗУ небольшого БИОСа, и дальше грузить автоматом интерфейс для работы с файлами на эмуляторе уже по другому протоколу.
Второй - иметь очень быстрый доступ к файлу на эмуляторе что-бы успевать его заливать по стробам протокола ПЗУ, но иметь ограничение, что файл грузится только с адреса 0, и максимальный размер файла 32КБ.
При этом эмулятор контроллера ЛВС или 0-модема (протокол не смотрел, но думаю он должен быть гибче протокола ПЗУ) лишены этих недостатков. Поток синхронизируемый с ожиданием готовности данных, адрес загрузки указан в служебной части протокола, размер файла... сколько загрузчик позволит.
Так-что выбор в пользу протокола ПЗУ не так очевиден, по крайней мере для меня.

KTSerg
07.03.2018, 05:54
Посмотрел алгоритм ПУ-LPT.
Всё просто.
настройка портов Вектора: А,С - выход. В - вход.
Данные передаются полубайтами, сначала младший. Для данных используется младшие биты порта В. Один бит порта В (бит 5, "1"-данные готовы) на флаг готовности данных, один бит порта С (бит 4, "0"-данные приняты) на подтверждение приёма.
В потоке сначала передаются 55h, AAh, номер блока для загрузки, количество блоков. Далее сам файл. Контрольная сумма самого файла (которая вычисляется "исключающее ИЛИ" (XOR) контрольной суммы и принятого байта).

Одно смущает, мне показалось, что после приёма каждого байта файла, вызывается подпрограмма, которая рисует весь "квадратик" на сетке загрузки - типа принято 256Байт...

Можно будет в плеер добавить :)

Improver
07.03.2018, 11:31
Возвращаясь к rom-плееру... Вчера попробовал исследовать сигнал, за неимением осциллографа использовал аудиокарту ПК -- кривая сигнала получается "неописуемой красоты", все циклы чётко выдержаны, даже и не знаю, к чему там придраться... Единственное, что увидел, это то, что сигнал формируется в инверсном виде и, чисто теоретически, это может повлечь большее время обработки Вектором. Немного переделал скетч, но проверить на оборудовании его ещё не успел, хотя в тапире вав-ки для обоих вариантов выглядят хорошо.
Вот результаты, для иллюстрации проделанного: 64497

По-хорошему, надо бы ещё исследовать сигнал на выходе компаратора, но для этого, опять же, требуется осциллограф.

Improver
09.03.2018, 13:19
Итак, продолжаем... Снял wav-ку с Вектора на максимальной скорости вывода "copy v2.1", и, самое удивительное, напрямую с компа Вектор её грузит легко, в отличие от тех же сохранённых wav-ок с ардуины. Визуально они отличаются, вот картинка для сравнения (Вектор вверху):
64528

Причём заметил, что кроме среза верхних частот, Вектор далеко не всегда чётко выдерживает интервалы, и это, как видно, для него не критично.

Попробовал воссоздать схему выхода Вектора, там всего три сопротивления и два конденсатора:
64530
Правда, не нашёл конденсатор 0,068 мкФ, поставил вместо него в полтора раза больший 0,1 мкФ -- результат отрицательный, т.е. улучшений нет. Пробовал исправить эквалайзером одну из ранее полученных с ардуины wav-ок, игрался и так, и эдак, доводил её даже до состояния, близкого с чистой синусоиде -- Вектор понимает и такую, но, опять же, не на максимальной скорости. :-(

В общем, пока кроме поиска и установки правильного конденсатора, идей нет...

Вот wav-ка, полученная с Вектора (https://yadi.sk/d/mqLLf_VT3TBZuU), может, кто поймёт, в чём его секрет? :-)

svofski
09.03.2018, 22:02
Если вав записать с ардуины в компьютер и потом такую запись проиграть в Вектор? Если эту запись загрузить в эмулятор?

Improver
10.03.2018, 08:34
Если вав записать с ардуины в компьютер и потом такую запись проиграть в Вектор? Если эту запись загрузить в эмулятор?В эмулятор не пробовал, только на живой Вектор -- и с компа не грузится, даже после обработки записи фильтрами. Загружает только если снизить скорость воспроизведения до 75-80% от исходной, т.е. получается на тех же ~256 мкс.

Improver
10.03.2018, 22:27
Игры с фильтрами/конденсаторами ничего не дают... Но заметил интересную особенность: сигнал с Вектора имеет небольшие задержки в передаче на границе каждого байта, примерно по 5 сэмплов на 96кГц, что равно ~50 мкс. Это можно заметить и на скриншоте, который я опубликовал выше (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=953633&viewfull=1#post953633), примерно в середине. Ардуина же напротив, чётко выдерживает все периоды. Вот, думаю, может в этом причина? Вот только надо придумать, как организовать такие задержки? Править задержку таймера на каждом байте?

- - - Добавлено - - -

О, да -- именно так и есть! Сейчас попробовал взять самый первый скетч и добавить туда задержку между байтами, и Вектор стал грузить на скоростях с полупериодом до 176 мкс, такой по скорости сигнал не давал ни один из копировщиков (речь идёт о стандартном формате ROM, не турбо). :v2_dizzy_punk:

Теперь вопрос, как сделать это на таймере1?...

svofski
11.03.2018, 00:56
Занятно, кто б мог подумать. В Векторе и тут отличились :)

Таймеру1 можно период менять на ходу. Увеличивать его перед границей байта, потом обратно.

KTSerg
11.03.2018, 07:10
Игры с фильтрами/конденсаторами ничего не дают... Но заметил интересную особенность: сигнал с Вектора имеет небольшие задержки в передаче на границе каждого байта, примерно по 5 сэмплов на 96кГц, что равно ~50 мкс. ... Ардуина же напротив, чётко выдерживает все периоды. Вот, думаю, может в этом причина?
...
О, да -- именно так и есть! Сейчас попробовал взять самый первый скетч и добавить туда задержку между байтами, и Вектор стал грузить на скоростях с полупериодом до 176 мкс, такой по скорости сигнал не давал ни один из копировщиков (речь идёт о стандартном формате ROM, не турбо)....

Тут можно пойти от обратного...
Вектор грузит и выгружает не по таймеру, а по алгоритму с задержками.
Соответственно, нужно расписать тайминги команд алгоритма чтения, и будет видно, где какие задержки должны быть для "оптимальной" по скорости загрузки.
На границе байта задержка нужна для записи принятого байта в память, а при приёме 32 байт ещё и на отрисовку принятой строчки блока (квадратика) на экране. А после приёма всего блока (256Байт) ещё и на проверку все ли строки блока приняты без ошибок. А это "не хухры-мухры", там идёт преобразование адреса принимаемой строки (32Байт в ОЗУ) в адрес на экране, довольно много вычислений. И когда возвращается к приёму следующего байта... а он уже проскочил...
Но можно нарваться и на то, что в разных загрузчиках, "стандартный формт" может отличаться "авторской оптимизацией"... и тайминги для одного загрузчика не подойдут к таймингам в другом...
Я так думаю...

А можно и не маяться и не считать тайминги алгоритма, а просто подключить логгер на сигнал "ЧТВУ" и магнитофонный вход ВВ55. И посмотреть на эти самые тайминги...

KTSerg
11.03.2018, 12:49
Посмотрел логером соотношение импульсов на магнитофонном входе и чтение порта.
При 50мкс задержке между байтами конечно стало лучше. Но при увеличении интервала между байтами до 60мкс, программа стабильно грузится на стандартном заводском загрузчике в Вектор 06Ц.02, при полупериоде таймера... барабанная дробь...в 112мкс...!!!
Правда для вычисления загрузчиком задержек, я отключаю изменение значения таймера на время меандра. Но вроде и без этого отключения работало.
Программа в 21КБ грузится за 59сек, и это стандартный загрузчик...

Improver
11.03.2018, 18:29
Занятно, кто б мог подумать. В Векторе и тут отличились :)Да, почти тридцать лет прошло, а секреты у Вектора ещё остались. :-)


Таймеру1 можно период менять на ходу. Увеличивать его перед границей байта, потом обратно.Это хорошо, что так можно, иначе это было бы сложной проблемой... Так и сделал, и оно работает.


А можно и не маяться и не считать тайминги алгоритма, а просто подключить логгер на сигнал "ЧТВУ" и магнитофонный вход ВВ55. И посмотреть на эти самые тайминги...Можно, при его наличии и если об этом знать заранее. А так, внимательного рассмотрения и измерения wav-ки оказалось достаточно.


При 50мкс задержке между байтами конечно стало лучше. Но при увеличении интервала между байтами до 60мкс,После нескольких экспериментов установил, что для стабильного считывания нужно, чтобы задержка между байтами была в сумме равна около 256 мкс, т.е. при полупериоде 200 мкс в начале байта нужно добавить 56 мкс, 192 мкс -- +64 мкс и т.п.

По крайней мере, это на ардуино и моих конденсаторах, а сейчас у меня собрана не лучшая схема...


программа стабильно грузится на стандартном заводском загрузчике в Вектор 06Ц.02, при полупериоде таймера... барабанная дробь...в 112мкс...!!!Фантастический результат! Мне такой на Векторе не ".02" не удалось достичь даже близко... Минимум у меня -- 176 мкс, и то в девяти из десяти случаях Вектор не успевает распознать начало записи (00h/55h...).


Программа в 21КБ грузится за 59сек, и это стандартный загрузчик...У меня получилось загрузить тот же 40-ка килобайтный Киберноид за три минуты, таймер при этом был выставлен на 184 мкс. Тоже не плохо, я считаю.

- - - Добавлено - - -

Выкладываю улучшенную, по скорости вывода, версию:
Требования по библиотекам не изменились -- нужны TimerOne, SdFat и LiquidCrystal.
/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Выход - D3

Подключение экрана 1602А (LCD Keypad Shield):
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

Кнопки подключаются на вывод A0 (схема LCD Keypad Shield)
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

volatile int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
digitalWrite(p, LOW);
pinMode(10, OUTPUT); // CS для SD-картридера
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(); // переходим к первому файлу в директории
}

void loop() {
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины -- это для теста...

int button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
seekFile();
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
if (Nbt != 0xFFFF) { // проверяем размер файла
for (int i=0;12;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
int RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",1); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",1); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
default:
printtext("ERROR!",1); // Сообщение об ошибке
}
digitalWrite(p, LOW);
seekFile();
}
}
}
break;
}
}
}
else {
printtext("File is too big",1);
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
printtext(" ",1); // очищаем строку 1
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
seekFile();
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
seekFile();
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile();
break;
case BT_select: // выход в настройки...
printtext("Period(mks):",0);
do {
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
delay(200);
button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_up:
if (Tpp<400) Tpp += 8;
break;
case BT_down:
if (Tpp>176) Tpp = Tpp - 8;
break;
}
if (Tpp <= 264) { // Выставляем значение задержки на начало байта Tb
Tb = 16; // для полупериода от 248 до 264
if (Tpp <= 240) Tb = 264 - Tpp; // для полупериода меньше или равном 240
}
else Tb = 0; // для полупериода больше 264
lcd.setCursor(12,0);
lcd.print(Tpp);
}
while(button!=BT_select); // Цикл пока не будет снова нажата кнопка select
seekFile();
break;
case BT_none: // ничего не нажато
break;
}
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 100) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 400) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(" ");
lcd.setCursor(0,l);
lcd.print(text);
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.close();
maxFile++;
}
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
romFile.openNext(romFile.cwd(),O_READ);
romFile.close();
}
romFile.openNext(romFile.cwd(),O_READ); // читаем данные текущего файла
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
if (romFile.fileSize()<=49152) { // проверка размера файла
Nbt = romFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
romFile.close();
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
romFile.dirEntry(&d); // Считываем дату файла
uint16_t AAA = FAT_DAY(d.lastWriteDate); // Сохраняем дату файла в заголовке -- день
SB[8] = (AAA%100)/10 + '0'; // перевод из целого в символ
SB[9] = AAA%10 + '0';
AAA = FAT_MONTH(d.lastWriteDate); // месяц
SB[10] = (AAA%100)/10 + '0';
SB[11] = AAA%10 + '0';
AAA = FAT_YEAR(d.lastWriteDate); // последние две цифры года
SB[12] = (AAA%100)/10 + '0';
SB[13] = AAA%10 + '0';

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(" ");

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
if (CRB > 200) Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

Изменения:
- сделана дополнительная задержка между байтами при передаче данных, что позволило существенно повысить скорость чтения Вектором.

Примечание: для увеличения стабильности определения Вектором начала передачи и расчёта скорости, добавление задержек в начальный заголовок передаваемых данных не делается.

Исходники с библиотеками в одном архиве: 64571

KTSerg
11.03.2018, 18:41
... После нескольких экспериментов установил, что для стабильного считывания нужно, чтобы задержка между байтами была в сумме равна около 256 мкс, т.е. при полупериоде 200 мкс в начале байта нужно добавить 56 мкс, 192 мкс -- +64 мкс и т.п. ...
До такого я не допетрил, но заметил, что длительность полупериода должна быть кратной 16. Т.е. на 112, 128, 156, 172 ... таймингах нормально грузит, а на значениях между ними, даже с разницей 1-2 единицы от указанных величин, либо вообще не подхватывает, либо быстро ошибку ловит...

KTSerg
11.03.2018, 22:11
Наконец получилось с помощью объединённого ROM-плеера/SD-ROM картриджа считать заводской загрузчик, на SD-карту, не прибегая ни к каким припаиваниям, замыканиям, и прочим аппаратным ухищрениям. Просто загрузил программку, которая слила загрузчик на SD-карту.
Сравню с имеющимися загрузчиками, может есть отличия...

От загрузчика kish2 отличается.

ivagor
12.03.2018, 06:14
считать заводской загрузчик, на SD-карту, не прибегая ни к каким припаиваниям, замыканиям, и прочим аппаратным ухищрениям. Просто загрузил программку, которая слила загрузчик на SD-карту.
Как минимум автостарт же надо было реализовать, чтобы пзу не отключилось (типа как в ROM-REAPER/TURBO-COPY), или как?

KTSerg
12.03.2018, 07:12
Как минимум автостарт же надо было реализовать, чтобы пзу не отключилось (типа как в ROM-REAPER/TURBO-COPY), или как?
Не, нужно было не заставить ПЗУ включиться, а обмануть загрузчик и перехватить управление ДО выключения ПЗУ (до сброса).
Я тупанул, пошел по прямой дорожке, и потратил почти целый день на войну с загрузчиком, пытаясь перехватить у него управление.
Не мог понять почему читаю сплошные "00", даже схему изучал, думал, что прочитать инфу, из программы за пределами области памяти загрузчика, нельзя (ошибочное предположение, но другое в голову не приходило). Оказалось, что перед передачей управления моей (загруженной) программе, загрузчик успевал щелкнуть рэлюхой (её управление заведено на схему "сброса"), и ПЗУ отключалось.
Мог за пол часа, в эмуляторе сделать скриншот загрузочной сетки Вектора, вырезать из него экранную область, дорисовать квадратики в загрузочной сетке, поправить стек в адресах 0xDExxh, прикрепить полученное к своей программе ... и получить результат.

ivagor
12.03.2018, 07:17
Не, нужно было не заставить ПЗУ включиться, а обмануть загрузчик и перехватить управление ДО выключения ПЗУ (до сброса).
Насколько я знаю, чисто программно заставить ПЗУ включиться нельзя.
ROM-REAPER/TURBO-COPY как раз автостартуют, т.е. перехватывают управление до отключения ПЗУ.

KTSerg
12.03.2018, 07:26
Насколько я знаю, чисто программно заставить ПЗУ включиться нельзя.
ROM-REAPER/TURBO-COPY как раз автостартуют, т.е. перехватывают управление до отключения ПЗУ.
Может я его с чем-то путаю, но в картотеке есть описание выгружалки ПЗУ, там нужно в схему Вектора кнопку и диод допаять.

ivagor
12.03.2018, 07:29
С кнопкой это ROM-DUMPER (http://www.sensi.org/scalar/ware/777/)
А чисто программный - ROM-REAPER (http://www.sensi.org/scalar/ware/780/)

KTSerg
12.03.2018, 07:43
С кнопкой это ROM-DUMPER (http://www.sensi.org/scalar/ware/777/)
А чисто программный - ROM-REAPER (http://www.sensi.org/scalar/ware/780/)
Ясно.
Значит я не внимательно читал описание, и упомянутую идею Дампера, принял за необходимость аналогичной доработки. :(
Но с другой стороны, не имея большого желания записывать ROM файл через магнитофонный выход, и дальнейшую его обработку, сделал выгрузку содержимого ПЗУ сразу на SD-карту. Т.к. по любому собирался втыкать функции записи файла на SD-карту, принятого от Вектора.

Сорькаю, ТС-у за отклонение от темы.

Improver
12.03.2018, 09:54
До такого я не допетрил, но заметил, что длительность полупериода должна быть кратной 16. Т.е. на 112, 128, 156, 172 ... таймингах нормально грузит...Стоп, 112 и 128 кратно 16, а 156 и 172 -- уже нет... :-\ Ближайшие к ним числа, кратные 16 -- это 160 и 176.

Кстати, задержка между байтами одного размера на любых скоростях выше 256 мкс вполне логически объяснима, т.к. она скорее всего вызвана недостаточным быстродействием Вектора на обработку байта, которое остаётся постоянным при любой скорости.

Полагаю, можно будет выделить ещё задержки на отрисовку блоков и т.п., и, допустим, при передаче данных в пределах одной строки можно будет ещё немного сократить эти интервалы, но смысла в этом не вижу. Почему? А потому, что, даже в текущем состоянии, задержка в 56 мкс на каждый байт увеличивает общее время вывода 40 кб всего на ~3 секунды по сравнению с выводом без этих задержек, просто нет смысла вылавливать эти дополнительные микросекунды.


Сорькаю, ТС-у за отклонение от темы.Прощу, если наконец-то выложишь тут свои скетчи для общественности. ;-)

KTSerg
12.03.2018, 10:43
Стоп, 112 и 128 кратно 16, а 156 и 172 -- уже нет... :-\ Ближайшие к ним числа, кратные 16 -- это 160 и 176.
...Да, мой косяк, просто я с 200 сразу прыгнул на 128 и потом на 112, и промежуточные значения проверял на скорую руку, и значения писал не по записям, а что вспомнил.

Полагаю, можно будет выделить ещё задержки на отрисовку блоков и т.п., ... просто нет смысла вылавливать эти дополнительные микросекунды.
Я смотрел на задержки при переносе принятого в память и отрисовку блоков, на это тратятся 4 байта (точнее часть одного) "00" перед "Е6" в начале каждой строки.

Прощу, если наконец-то выложишь тут свои скетчи для общественности. ;-)
:)
Я ж говорил, это не скетч, т.к. не Ардуина. Пишу на Си в Keil. Будет более менее завершенный отладочный (с управлением по СОМ-порту с РС) проект выложу.
Вот в функциях ЛВС и ROM-плеера уже объединил переменные и буферы, т.к. алгоритмы идентичны.
Застрял пока на открытии файла для записи. Библиотека позволяет уже существующий файл открывать и в него писать, а новый только создаёт, но записывать в него отказывается, вываливает ошибку, буду пробовать...

ivagor
12.03.2018, 12:18
Насчет задержки между байтами. Ramiros реализовал эту фичу в своем ROM2WAV (2009 год). Правда не вынесена "на панель управления" возможность менять длительность задержки, но и так работает хорошо, и исходник доступен.
Насчет достижимых скоростей. Эмулятор это не реал, но просто для информации. ROM2WAV с устраненной по методу Tim0xи избыточностью и Fd=24 кГц преобразует файлик 33.25 Кб в wav длительностью примерно полторы минуты. В VV с хакнутым мною загрузчиком такой файлик грузится.

KTSerg
12.03.2018, 12:59
...
Насчет достижимых скоростей. Эмулятор это не реал, но просто для информации. ROM2WAV с устраненной по методу Tim0xи избыточностью и Fd=24 кГц преобразует файлик 33.25 Кб в wav длительностью примерно полторы минуты. В VV с хакнутым мною загрузчиком такой файлик грузится.
33.25 Кб за ~1.5 мин., это видимо эквивалент того, что получилось 21 Кб за ~1 мин, при полупериоде 112мкс. Но я все-же подозреваю, что от загрузчика будет зависеть...

ivagor
12.03.2018, 13:14
Ближе к вечеру попробую покрутить и посмотреть, что еще можно выжать и с хакнутым загрузчиком и с оригинальными.

Improver
12.03.2018, 13:40
Насчет задержки между байтами. Ramiros реализовал эту фичу в своем ROM2WAV (2009 год). Правда не вынесена "на панель управления" возможность менять длительность задержки, но и так работает хорошо, и исходник доступен.
Эх, если б об этом хоть кто-то намекнул ранее, то мне бы не пришлось изобретать велосипед... :-)
Сейчас глянул исходники ROM2WAV, как я понял, там добавляется 1 сэмпл на частоте 22 кГц при любой скорости, это получается по 45 мкс. А максимальная скорость там -- 2 сэмпла на полупериод на той же частоте, что выходит в ~91 мкс на полуцикл (между байтами по 136 мкс)... Интересно, в таком виде тоже грузилось?

ivagor
12.03.2018, 14:01
А максимальная скорость там -- 2 сэмпла на полупериод на той же частоте, что выходит в ~91 мкс на полуцикл (между байтами по 136 мкс)... Интересно, в таком виде тоже грузилось?
VV грузит, emu не грузит. Реал - не знаю.

Если увеличить частоту дискретизации без включения ресемплирования, то можно получить и более высокую скорость (для стандартных загрузчиков без разницы, они это уже не грузят). Как раз при увеличении частоты дискретизации задержку стоит увеличивать до 2.

Improver
19.03.2018, 11:19
В прошедшие выходные получил посылку с известного китайского сайта, заказывал "Data Logger Shield (https://yandex.ru/images/search?family=yes&text=Data%20Logger%20Shield)" с ардуиной уно -- этот шилд как раз подходит для ROM-плеера, есть место, куда можно впаять схему выхода и разъём, плюс бонусом имеет на борту часы. Вот что сейчас имеем:
Всё в сборе:
64678

И по отдельности:
64679

Бело-розовый проводок с разъёмом на фото -- это прямой выход с контакта D3, для опытов...
На всём этом скетч третьей версии загрузился и заработал без проблем, Вектор грузит всё, как и ранее. Но выявился небольшой глючёк -- как оказалось вывод D10 ардуины также задействован платой "LCD Keypad Shield" для управления яркостью подсветки экрана, и во время обращения к СД-карте подсветка становится немного темнее. :-( Решения тут два: первое -- откусить или выпаять соответствующий штырь контакта на шилде с экраном и кнопками, и второе -- считать это фичей, показывающей таким образом то, что происходит обращение к карте памяти. Склоняюсь ко второму варианту. :-)

И раз уж появился бонус в виде часов, решил его задействовать, вот новая версия скетча:
Для работы требуются библиотеки TimerOne, SdFat, а также стандартные LiquidCrystal, Wire и RTClib.

/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V
* Кнопки - A0
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

RTC_DS1307 RTC;

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile romFile;

#define filenameLength 100 // максимальная длина имени файла
char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[13]; // короткое имя текущего файла
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

volatile int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
digitalWrite(p, LOW);
pinMode(10, OUTPUT); // CS для SD-картридера
digitalWrite(10, HIGH);
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",0);
// following line sets the RTC to the date & time this sketch was compiled
RTC.adjust(DateTime(__DATE__, __TIME__));
delay(1000); // ждем 1 с
}
//RTC.adjust(DateTime(__DATE__, __TIME__));

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",0);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",0);

sd.chdir(); // устанавливаем корневую директорию SD
getMaxFile(); // получаем количество файлов в директории
seekFile(); // переходим к первому файлу в директории
}

void loop() {
DateTime now = RTC.now();
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
printtext(DT,1); // выводим время и дату

int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button)
{
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if(isDir==1) { //Если это директория, то переход в неё
sd.chdir(fileName, true);
getMaxFile();
currentFile=1;
}
else { // если не директория -- пробуем воспроизвести файл
if(romFile.cwd()->exists(sfileName)) {
printtext("Not ROM-file",1);
if (Nbt != 0xFFFF) { // проверяем размер файла
for (int i=0;12;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if ((sfileName[i+1]|0x20)=='r') { // проверяем следующий символ (расширения) == 'r'|'R'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
if ((sfileName[i+3]|0x20)=='m') { // == 'm'|'M'
printtext("Playing...",1);
int RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",1); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",1); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
default:
printtext("ERROR!",1); // Сообщение об ошибке
}
digitalWrite(p, LOW);
}
}
}
break;
}
}
}
else {
printtext("File is too big",1);
}
}
else {
printtext("No File Selected",1);
}
delay(1000); // ждем 1 с
}
break;
case BT_left: // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
getMaxFile();
currentFile=1;
break;
case BT_up: // вверх по файлам в директории
currentFile--;
if(currentFile<1) {
getMaxFile();
currentFile = maxFile;
}
break;
case BT_down: // вниз по файлам в директории
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
break;
case BT_select: // выход в настройки...
printtext("Setup",0);
printtext("Period(mks):",1);
do {
delay(300);
button = getPressedButton(); // какая кнопка нажата?
switch (button)
{
case BT_up:
if (Tpp<400) Tpp += 8;
break;
case BT_down:
if (Tpp>160) Tpp = Tpp - 8;
break;
}
if (Tpp <= 264) { // Выставляем значение задержки на начало байта Tb
Tb = 16; // для полупериода от 248 до 264
if (Tpp <= 240) Tb = 264 - Tpp; // для полупериода меньше или равном 240
}
else Tb = 0; // для полупериода больше 264
lcd.setCursor(12,1);
lcd.print(Tpp);
}
while(button!=BT_select); // Цикл пока не будет снова нажата кнопка select
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case BT_none: // ничего не нажато
delay(100);
break;
}
if (button != BT_none) seekFile();
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" ");
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
romFile.cwd()->rewind();
maxFile=0;
while(romFile.openNext(romFile.cwd(),O_READ)) {
romFile.close();
maxFile++;
}
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
romFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
romFile.openNext(romFile.cwd(),O_READ);
romFile.close();
}
romFile.openNext(romFile.cwd(),O_READ); // читаем данные текущего файла
romFile.getName(fileName,filenameLength);
romFile.getSFN(sfileName);
isDir = romFile.isDir();
if (romFile.fileSize()<=49152) { // проверка размера файла
Nbt = romFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
romFile.close();
printtext(sfileName,0); // вывод имени текущего файла
if (isDir==1) lcd.print('>'); // если это директория, добавляем в конце символ '>'
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
delay(1000); // ждем 1 с

if (romFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
romFile.dirEntry(&d); // Считываем дату файла
uint16_t AAA = FAT_DAY(d.lastWriteDate); // Сохраняем дату файла в заголовке -- день
SB[8] = (AAA%100)/10 + '0'; // перевод из целого в символ
SB[9] = AAA%10 + '0';
AAA = FAT_MONTH(d.lastWriteDate); // месяц
SB[10] = (AAA%100)/10 + '0';
SB[11] = AAA%10 + '0';
AAA = FAT_YEAR(d.lastWriteDate); // последние две цифры года
SB[12] = (AAA%100)/10 + '0';
SB[13] = AAA%10 + '0';

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 1); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = romFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
romFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
romFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
if (CRB > 200) Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

Изменения по отношению к предыдущей версии минимальны: добавлены часы вместо отсчёта секунд от включения и немного оптимизированы некоторые алгоритмы (не главные).
Исходники с библиотеками в одном архиве: 64680

Теперь, имея "генератор сигнала Вектора", можно двигаться дальше -- на второй ардуинке делать запись и сохранение данных... :-)

KTSerg
20.03.2018, 06:21
... плюс бонусом имеет на борту часы. ... добавлены часы вместо отсчёта секунд от включения ...
Идея интересная, только в скетче нужно предусмотреть вариант, когда часов нет (наиболее критично), они остановились, сбросились, или имеют не адекватные показания...

Improver
20.03.2018, 09:19
Идея интересная, только в скетче нужно предусмотреть вариант, когда часов нет (наиболее критично), они остановились, сбросились, или имеют не адекватные показания...Ситуацию отсутствия часов не проверял (проверю по возможности), а в остальных случаях на часах будет установлено время с компа, которое было на момент прошивки. А в случае, если их заведомо нет,можно использовать скетч третьей версии (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=954018&viewfull=1#post954018) -- он, фактически, и отличается отсутствием часов. А потом уж сделаем нечто объединённое, на "#ifdef...".

KTSerg
20.03.2018, 11:02
Две версии, возможно не очень удобно.
Если в программе нет принципиальных отличий, то наверное можно в одной версии объединить.
Если часы не отвечают, можно из функции чтения даты/времени возвращать константу, или возможно таймер запускать... .

Improver
22.03.2018, 17:45
Двигаемся дальше, теперь нужно добавить запись... Вот что я думаю: запись нужно делать полностью, со всеми заголовками, синхроимпульсами и прочей служебной информацией -- это даст возможность писать в любом формате, не только ROM, но и бейсика/монитора/ретекса и т.д. И вывод, соответственно, должен быть идентичен вводу.

Потом, при захвате сигнала принимаем за аксиому, что во всех форматах Вектора вывод начинается с группы нулей. (Это ведь так?) Это избавляет от необходимости поиска синхроимпульса и инвертирования сигнала "на лету".

И вот первый пробный вариант:
Сигнал в цифровом виде подаётся на вход D2 ардуино. Никаких дополнительных библиотек скетчу не нужно.

#define InputPIN 2

volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned int PPeriod_sred = 0; // среднее значение границы длинны полупериода
unsigned int Tpp = 0; // расчётное значение полупериода для вывода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile boolean Plong = false; // "длинный" полупериод

byte BUFF[512]; // буфер данных
unsigned int CWB = 0; // индекс записи в буфер
byte bBit = 14; // индекс записываемого полубита
byte A = 0; // записываемый байт
byte B = 0; // записываемый бит
int i0 = 0; // счётчик циклов без сигнала
//unsigned int j;

void setup() //процедура setup
{
pinMode(InputPIN, INPUT); //объявляем пин как вход
pinMode(13, OUTPUT); // объявляем пин как выход
attachInterrupt(0, TakeTime, CHANGE); // включаем обработку внешнего прерывания
Serial.begin(9600);
}

void loop() //процедура loop
{
if (Pik) { // Произошло изменение сигнала
Pik = false;
i0 = 0;

if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
}
}
// ===================================
if (bBit > 1) {
bBit--; // счётчик -1
}
else {
if (CWB < 512) BUFF[CWB] = A;
CWB++;
A = 0;
bBit += 15;
}
// ====================================
digitalWrite(13, HIGH); // зажигаем светодиод -- сигнал есть
}
else{ // изменения сигнала не было
if (i0 < 9) { // считаем 10 раз
i0++;
}
if (i0 == 8) { // на восьмой раз выводим тестовую инфу
digitalWrite(13, LOW); // гасим светодиод -- сигнала нет
Serial.println("--------");
for (int j = 0; j<=511; j++) {
Serial.print(BUFF[j], HEX);
Serial.print('-');
if (((j+1)%16) == 0) Serial.println('-');
}
Serial.println("--------");
Serial.println(PPeriod_sred); // граница короткий/длинный полупериод
if (((PPeriod_sred/3*2)%8) < 4) { // полупериод для последующего вывода...
Tpp = (PPeriod_sred/12)*8; // округление до кратного 8 в меньшую сторону
}
else {
Tpp = ((PPeriod_sred/12)+1)*8;// округление до кратного 8 в большую сторону
}
Serial.println(Tpp); // расчётное значение длинны полупериода для вывода
// сбрасываем счётчики -- сигнала слишьком долго нет
CWB = 0;
A = 0;
B = 0;
bBit = 14;
delay(100);
}
}
delayMicroseconds(128); // задержка...
}

void TakeTime()
{
PPeriod = micros() - iMicros_old;
iMicros_old += PPeriod;
if (PPeriod < 1000) { // началось...
if (PPeriod_sred != 0) {
if (!Plong) {
if (PPeriod > PPeriod_sred) Plong = true; // тек. полупериод стал длинный
}
else {
if (PPeriod < PPeriod_sred) Plong = false; // тек. полупериод стал короткий
}
// если ни то, ни другое -- они равны, т.е. Plong без изменений
if (!Plong) { // если полупериод короткий, пересчитываем среднее значение
PPeriod_sred = (PPeriod_sred + PPeriod/2*3)/2;
}
}
else {
PPeriod_sred = PPeriod/2*3; // если ещё нет статистики, берём текущее значение
}
Pik = true; // есть сигнал
}
else { // был перерыв в сигнале
if (PPeriod_sred != 0) {
PPeriod_sred = 0; // обнуляем среднее значение
Plong = false; // и считаем, что будет короткий полупериод
}
}
}
В этом варианте решил попробовать сделать захват сигнала через внешнее прерывание, которое на большинстве ардуин работает на порту D2. (Может у кого будут идеи по-лучше?)

До записи на карту пока не дошёл, скетч пока только определяет наличие сигнала, захватывает первые 512 байт в буфер и по завершении вываливает всё это в терминал, плюс в конце выводит размер полупериода входного сигнала в микросекундах. На живом Векторе пока не проверял, но две ардуины между собой уже передают информацию вполне уверенно.

Вот этот скетч в архиве: 64723

Improver
23.03.2018, 16:36
Продолжаем тему... Версия скетча с записью на SD-карту:
Для работы требуется библиотека SdFat

/*
SD-картридер подключяется к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Вход - D2

*/
#define InputPIN 2

#include <SdFat.h>

SdFat sd;
SdFile dataFile;
char FName[13] = "input000.vkt";

volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned int PPeriod_sred = 0; // среднее значение границы длинны полупериода
unsigned int Tpp = 0; // расчётное значение полупериода для вывода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс записываемого полубита. (Начинаем с 15, т.к. первый бит теряется)
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит
int i0 = 0; // счётчик циклов без сигнала

void setup() //процедура setup
{
pinMode(InputPIN, INPUT); //объявляем пин как вход
pinMode(10, OUTPUT);
Serial.begin(9600);

Serial.print("Initializing SD card...");
while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
Serial.println("SD-card failed!");
delay(3000); // ждем 3 с
}
Serial.println("SD-card is OK.");
sd.chdir(); // устанавливаем корневую директорию SD
}

void loop() //процедура loop
{
if (dataFile.open(FName, FILE_WRITE)) { // открываем файл на запись
Serial.print("Save data to file ");
Serial.print(FName);
Serial.print(" ... ");
i0 = 0;
CRB = 0;

attachInterrupt(0, TakeTime, CHANGE); // включаем обработку внешнего прерывания
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
}
Serial.print("start ... ");

noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
//====================================
do {
while ((CRB_temp <= CRB) & Pik)// пока индекс записи меньше или равен индексу чтения и есть сигнал
{ // Ждём заполнения буфера
i0++;
Pik = (i0 < 10); // что-то долго ждём...
delay(1); // задержка на ввод данных
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
if (!Pik) break;
i0 = 0;

dataFile.write(BUFF[lowByte(CRB)]); // пишем байт
CRB++;
}
while (Pik);
//=====================================
detachInterrupt(0); // отключаем обработку внешнего прерывания 0
if (((PPeriod_sred/3*2)%8) < 4) { // полупериод для последующего вывода...
Tpp = (PPeriod_sred/12)*8; // округление до кратного 8 в меньшую сторону
}
else {
Tpp = ((PPeriod_sred/12)+1)*8; // округление до кратного 8 в большую сторону
}
dataFile.write(0xFF);
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
dataFile.close(); // закрываем файл
Serial.println("done");
Serial.print("Speed: ");
Serial.println(Tpp); // расчётное значение длинны полупериода для вывода

}
else {
Serial.print("Error opening ");
Serial.println(FName);
}
while (1) {
} // тормозим ардуину
Serial.println("stop");
}

void TakeTime()
{
boolean Plong = false; // короткий полупериод по умолчанию
PPeriod = micros() - iMicros_old;
iMicros_old += PPeriod;
if (PPeriod < 1000) { // началось...
if (PPeriod_sred != 0) {
Plong = (PPeriod > PPeriod_sred); // = false, если тек. полупериод короткий
if (!Plong) { // если полупериод короткий, пересчитываем среднее значение
PPeriod_sred = (PPeriod_sred + PPeriod/2*3)/2;
}
}
else {
PPeriod_sred = PPeriod/2*3; // ещё нет статистики, берём текущее значение
}
}
else { // был перерыв в сигнале
if (PPeriod_sred != 0) {
PPeriod_sred = 0; // обнуляем среднее значение
CWB = 0; // начинаем запись с начала
A = 0;
B = 0;
}
}

if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
// корректировка счётчиков
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
CWB++;
A = 0;
bBit += 15; // = +16 -1
}
Pik = true; // есть сигнал!
}
Картридер подключается к тем же выводам, что и в ROM-плеере.

Собственно, с этим скетчем ардуина ждёт подачи сигнала на D2, а при получении записывает все данные в файл "input000.vkt" и подвисает... В конец файла также записывается маркер 0xFF и расчётный период таймера для вывода в микросекундах (два байта) -- придумал свой формат файла для удобства... :-) Если на карте такой файл уже есть, данные дописываются в конец. Пока всё это ещё сыровато, но для отладки технологии записи уже вполне подходит.

По схеме входа для подачи сигнала с Вектора тоже пока ещё не понятно, что будет лучше -- собрать схему на компараторе СА3, или можно будет всё упростить до предела...

Архив со скетчем и необходимой библиотекой: 64731

svofski
25.03.2018, 13:26
Может быть для сохранения окажется удобным vcd (value change dump), или что-то похожее на него.

KTSerg
26.03.2018, 22:41
Подкинуть идейку :)
В плеере уже есть и выгрузка и загрузка данных...
Можно избавиться от ардуинных дисплея и кнопок...
И использовать сам Вектор для интерфейса плеера... :)
Например вот-так (как на скриншоте). :)

Improver
27.03.2018, 10:02
Может быть для сохранения окажется удобным vcd (value change dump), или что-то похожее на него.
Посмотрел я описание этого формата в википедии (https://en.wikipedia.org/wiki/Value_change_dump)... Только ASCII-символы, заголовки секций в текстовом виде... Всё это прекрасно при неограниченных ресурсах большого ПК, но не на ардуине, где разбор текстовых строк -- тот ещё подарок... :-(



Подкинуть идейку :)
В плеере уже есть и выгрузка и загрузка данных...
Можно избавиться от ардуинных дисплея и кнопок...
И использовать сам Вектор для интерфейса плеера... :)Отличная идея -- сам обдумывал такой вариант. :-) Причём эта идея ещё интересна тем, что тогда ардуину с картридером можно будет разместить в корпусе Вектора (например, в пустой "ноге")... Но если начинать дальше продумывать интерфейс взаимодействия Вектор-ардуино, то сразу хочется сделать и передачу данных по этому же каналу, и тогда получим уже не "майфун", а совсем другое устройство, более близкое к ROM-картриджу. В общем, эту идею я решил пока отложить для другого проекта.

KTSerg
27.03.2018, 10:20
...
Отличная идея -- сам обдумывал такой вариант. :-) Причём эта идея ещё интересна тем, что тогда ардуину с картридером можно будет разместить в корпусе Вектора (например, в пустой "ноге")... ...
Если прятать в "ноге", значит вход/выход подключать нужно будет непосредственно в схему. Если оставлять мафонный вход/выход, нужно тумблер в разрыв втыкать, что-бы реальному мафону не мешать...

svofski
27.03.2018, 10:38
Не обязательно реализовывать vcd буквально. Взять просто идею сохранения времени изменения сигнала.

Improver
27.03.2018, 13:13
Если оставлять мафонный вход/выход, нужно тумблер в разрыв втыкать, что-бы реальному мафону не мешать...Не обязательно тумблер ставить, можно просто сделать несложную схемку аудио-развязки "два входа"-"один выход".

В такой конструкции есть ещё один неразрешённый вопрос: как сделать управление из других программ? Ну, например, как запустить запись из бейсика? А сохранение текста из RETEX? Кнопок-то нет... Хотя, в какой-то мере можно использовать сигнал управления реле Вектора, но на большинстве оно не устанавливалось...

- - - Добавлено - - -

Новая версия скетча, теперь банановый с записью:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire. Вход для записи данных -- D2.

/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V
* Кнопки - A0
*/

// include the library code:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

RTC_DS1307 RTC;

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

//#define filenameLength 100 // максимальная длина имени файла
//char fileName[filenameLength + 1]; // имя текущего файла
char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
unsigned int CRB_temp = 0; // временная переменная для CRB
int i0 = 0; // счётчик циклов без сигнала
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned int PPeriod_sred = 0; // среднее значение границы длинны полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала

// Заголовок
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x31, 0x34, 0x30, 0x32, 0x31, 0x38, // дата: 140218
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

char iFName[13] = "input000.vkt";

volatile int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0;
const int M_play_in = 10;
const int M_record = 1;
const int M_record_in = 11;
const int M_setup = 2;
const int M_setup_in = 12;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
digitalWrite(p, LOW);
pinMode(10, OUTPUT); // CS для SD-картридера
digitalWrite(10, HIGH);
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop();

printtext("BEKTOP-MF",0);
printtext("version 5.0",1);
delay(2000); // ждем 2 с для солидности :-)

Wire.begin();
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1);
// following line sets the RTC to the date & time this sketch was compiled
RTC.adjust(DateTime(__DATE__, __TIME__));
delay(1000); // ждем 1 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__));

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
tone(p, 200, 100); // нет -- включаем на 200 Гц на 100 мс
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);

sd.chdir(); // устанавливаем корневую директорию SD
// getMaxFile(); // получаем количество файлов в директории и переходим к первому файлу в директории
}

void loop() {
int RCrom = 0; // для кода возврата ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button)
{
case BT_up:
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down:
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right:
if (MLevel < 10) {
switch (MLevel)
{
case M_play:
printtext("Play file:",0);
getMaxFile();
break;
case M_record:
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) {
if (iFName[7] < '9') {
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') {
iFName[6]++;
}
else {
iFName[6] = '0';
iFName[5]++;
}
}
}
printtext(iFName,1);
break;
case M_setup:
printtext("Setup",0);
break;
}
MLevel += 10;
button = BT_none;
}
break;
case BT_left:
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel)
{
case M_play:
printtext("Play file ->",0);
printtime();
break;
case M_record:
printtext("Record data ->",0);
printtime();
break;
case M_setup:
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile();
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile();
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if (((sfileName[i+1]|0x20)=='r')&((sfileName[i+3]|0x20)=='m')) { // проверяем первый и третий символы расширения == 'r'|'R', == 'm'|'M'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
}
else { // проверяем расширение файла на "VKT"
if (((sfileName[i+1]|0x20)=='v')&((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t')) {
delay(2000);
RCrom = 0; // тут будет вызов ПП для формата VKT
}
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile();
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) {
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
do {
printtext("Waiting...",0);
i0 = 0; // Сбрасываем индексы
CRB = 0;
bBit = 14; // индекс записываемого полубита. (Начинаем с 15, т.к. первый бит теряется)

attachInterrupt(0, TakeTime, CHANGE); // включаем обработку внешнего прерывания
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (getPressedButton()!=BT_none) { // кнопка нажата?
break; // прерывание режима загрузки при нажатии кнопки
}
}
printtext("Start...",0);

noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
//====================================
do {
while ((CRB_temp <= CRB) & Pik)// пока индекс записи меньше или равен индексу чтения и есть сигнал
{ // Ждём заполнения буфера
i0++; // счиаем циклы
Pik = (i0 < 10); // что-то долго ждём...
delay(1); // задержка на ввод данных
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
if (!Pik) break; // выход из чтения т.к. сигнал закончился
i0 = 0;

dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
CRB++;
}
while (Pik); // пока есть сигнал
//=====================================
detachInterrupt(0); // отключаем обработку внешнего прерывания 0
if (((PPeriod_sred/3*2)%8) < 4) { // полупериод для последующего вывода...
Tpp = (PPeriod_sred/12)*8; // округление до кратного 8 в меньшую сторону
}
else {
Tpp = ((PPeriod_sred/12)+1)*8; // округление до кратного 8 в большую сторону
}
}
while (Tpp < 32); // проверка -- если было ложное срабатывание (задержка <32 мкс), то снова ждём...
dataFile.write(0xFF);
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) {
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
dataFile.close(); // закрываем файл
printtext("Done. Speed:",0);
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 0 в строке 1
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
}
else {
printtext("Error open file",0);
}
delay(1000);
MLevel = M_record;
}
else {
if (button == BT_left) MLevel = M_record;
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up:
if (Tpp<400) Tpp += 8;
break;
case BT_down:
if (Tpp>160) Tpp = Tpp - 8;
break;
case BT_left:
MLevel = M_setup;
}
lcd.setCursor(12,1);
lcd.print(Tpp);
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" ");
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1;
seekFile();
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
//dataFile.getName(fileName,filenameLength);
dataFile.getSFN(sfileName);
isDir = dataFile.isDir();
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close();
if (isDir!=1) {
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) {
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

int PlayROM(char FName[], int pnt) // функция вывода файла
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла
uint16_t AAA = FAT_DAY(d.lastWriteDate); // Сохраняем дату файла в заголовке -- день
SB[8] = (AAA%100)/10 + '0'; // перевод из целого в символ
SB[9] = AAA%10 + '0';
AAA = FAT_MONTH(d.lastWriteDate); // месяц
SB[10] = (AAA%100)/10 + '0';
SB[11] = AAA%10 + '0';
AAA = FAT_YEAR(d.lastWriteDate); // последние две цифры года
SB[12] = (AAA%100)/10 + '0';
SB[13] = AAA%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;

if (Tpp <= 264) { // Вычисляем значение задержки на начало байта Tb
Tb = 16; // для полупериода от 248 до 264
if (Tpp <= 240) Tb = 264 - Tpp;// для полупериода меньше или равном 240
}
else Tb = 0; // для полупериода больше 264

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else tone(p, 200, 300); // нет файла -- включаем на 200 Гц на 300 мс

do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);

Timer1.stop(); // Останавливаем таймер............
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
if (CRB > 200) Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

void TakeTime()
{
boolean Plong = false; // короткий полупериод по умолчанию
PPeriod = micros() - iMicros_old;
iMicros_old += PPeriod;
if (PPeriod < 1000) { // началось...
if (PPeriod_sred != 0) {
Plong = (PPeriod > PPeriod_sred); // = false, если тек. полупериод короткий
if (!Plong) { // если полупериод короткий, пересчитываем среднее значение
PPeriod_sred = (PPeriod_sred + PPeriod/2*3)/2;
}
}
else {
PPeriod_sred = PPeriod/2*3; // ещё нет статистики, берём текущее значение
}
}
else { // был перерыв в сигнале
if (PPeriod_sred != 0) {
PPeriod_sred = 0; // обнуляем среднее значение
CWB = 0; // начинаем запись с начала
A = 0;
B = 0;
}
}

if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
// корректировка счётчиков
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
CWB++;
A = 0;
bBit += 15; // = +16 -1
}
Pik = true; // есть сигнал!
}
Что сделано:
- Добавлена запись данных. Запись выполняется в файлы вида "inputXXX.vkt", где ХХХ -- это числа от 000 до 999, выбирается файл с наименьшим числом, которого ещё нет в корне на SD-карте.
- Увеличена граница максимального размера файла до 65534 байта.
- Добавлена проверка наличия часов, если их нет, то показывается вывод секунд от старта, как и ранее.
- Добавлено сохранение даты/времени записи файла (при наличии часов).
- В связи с перечисленными изменениями, переделан интерфейс. Теперь при старте нужно выбрать "чтение", "запись" или "настройки".

Назначение кнопок:
<UP>,<DOWN> -- выбор пункта меню, выбор файла, изменение значения и т.п.
<LEFT> -- возврат назад
<RIGHT> -- вход в пункты меню, запуск чтения/записи.
<SELECT> -- переход в корень меню. (фактически уже не нужна, но путь будет...)

Ремарка по сохранению данных: сделать первоначальный набор имени файла для сохранения можно, но делать это на пяти кнопках и двух строках экрана будет крайне не удобно, я думаю, гораздо проще переткнуть потом SD-карту в комп и переименовать файл по своему желанию...

В итоге, осталось только сделать вывод файлов в формате сохранения VKT... :) Ну и потом протестить на железе, в особенности на не-ROM-форматах.

Вот архив со скетчем и всеми библиотеками: 64814

KTSerg
27.03.2018, 17:36
... В такой конструкции есть ещё один неразрешённый вопрос: как сделать управление из других программ? Ну, например, как запустить запись из бейсика? А сохранение текста из RETEX? Кнопок-то нет ...
Да, о применении из программ я чёт совсем не подумал, "заклинило" на начальной загрузке.

Improver
29.03.2018, 12:42
Свежая версия "цифрового магнитофона" для Вектора:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.

/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - D2

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V
* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит
int i0 = 0; // счётчик циклов без сигнала

volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned int PPeriod_sred = 0; // среднее значение границы длинны полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x32, 0x39, 0x30, 0x33, 0x31, 0x38, // дата: 290318
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_record = 1; // запись
const int M_record_in = 11;
const int M_setup = 2; // настройки
const int M_setup_in = 12;

void setup() {
pinMode(p, OUTPUT); // объявляем пин как выход
digitalWrite(p, LOW);
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH);
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext("version 6.0",1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(1000); // ждем 1 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if (((sfileName[i+1]|0x20)=='r')&((sfileName[i+3]|0x20)=='m')) { // проверяем первый и третий символы расширения == 'r'|'R', == 'm'|'M'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
}
else { // проверяем расширение файла на "VKT"
if (((sfileName[i+1]|0x20)=='v')&((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t')) {
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
do {
printtext("Waiting...",0); // ожидание сигнала
i0 = 0; // Сбрасываем индексы
CRB = 0;
bBit = 14; // индекс записываемого полубита. (Начинаем с 15, т.к. первый бит теряется)

attachInterrupt(0, TakeTime, CHANGE); // включаем обработку внешнего прерывания
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (getPressedButton()!=BT_none) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Start...",0); // сигнал зафиксирован, записываем

noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
//====================================
do {
while ((CRB_temp <= CRB) & Pik)// пока индекс записи меньше или равен индексу чтения и есть сигнал
{ // Ждём заполнения буфера
i0++; // счиаем циклы
Pik = (i0 < 10); // что-то долго ждём...
delay(1); // задержка на ввод данных
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
if (!Pik) break; // выход из чтения т.к. сигнал закончился
i0 = 0;

dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
CRB++;
}
while (Pik); // пока есть сигнал
//=====================================
detachInterrupt(0); // отключаем обработку внешнего прерывания 0
if (((PPeriod_sred/3*2)%8) < 4) { // расчитываем полупериод для последующего вывода...
Tpp = (PPeriod_sred/12)*8; // округление до кратного 8 в меньшую сторону
}
else {
Tpp = ((PPeriod_sred/12)+1)*8; // округление до кратного 8 в большую сторону
}
}
while (Tpp < 32); // проверка -- если было ложное срабатывание (задержка <32 мкс), то снова ждём...
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
dataFile.close(); // закрываем файл
printtext("Done. Speed:",0);
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}
delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 264) {
Tb = 16; // для полупериода от 248 до 264
if (Tpp <= 240) Tb = 264 - Tpp;// для полупериода меньше или равном 240
}
else Tb = 0; // для полупериода больше 264
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 11; // не правильная метка
dataFile.seekSet(0); // на начало файла

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 250 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (unsigned int i=251; i<=Nbt-4; i++){ // всё остальное, кроме последних 3 байт
ToBUFF(dataFile.read());

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8);
lcd.print(' ');

if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}

dataFile.close(); // закрываем файл
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
if (CRB > 200) Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

void TakeTime() // ПП обработки внешнего прерывания -- определяет и заносит полученные биты в буфер
{
boolean Plong = false; // короткий полупериод по умолчанию
PPeriod = micros() - iMicros_old;
iMicros_old += PPeriod;
if (PPeriod < 1000) { // началось...
if (PPeriod_sred != 0) {
Plong = (PPeriod > PPeriod_sred); // = false, если тек. полупериод короткий
if (!Plong) { // если полупериод короткий, пересчитываем среднее значение
PPeriod_sred = (PPeriod_sred + PPeriod/2*3)/2;
}
}
else {
PPeriod_sred = PPeriod/2*3; // ещё нет статистики, берём текущее значение
}
}
else { // был перерыв в сигнале
if (PPeriod_sred != 0) {
PPeriod_sred = 0; // обнуляем среднее значение
CWB = 0; // начинаем запись с начала
A = 0;
B = 0;
}
}
// расшифровка данных
if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
// корректировка счётчиков
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
CWB++;
A = 0;
bBit += 15; // = +16 -1
}
Pik = true; // есть сигнал!
}
Изменения по отношению к предыдущей версии:
- добавлено воспроизведение записанных файлов.
- небольшие улучшения в программе.

Вопрос по схеме для входа пока открыт... Вижу три варианта:
1. Собрать схему на транзисторе, как в zx-tapper (http://trolsoft.ru/ru/sch/zx-tapper).
2. Подключаться "по цифре" непосредственно к выходу 14 микросхемы ВВ55А Вектора.
3. Собрать схему на компараторе К554СА3А, как в Векторе. Но этот вариант требует двуполярного питания, которого в ардуине нет... :-(

Будем пробовать, начиная с первого варианта. :)

Архив со скетчем и всеми библиотеками: 64817

Trol73
29.03.2018, 15:48
Как вариант, ещё можно попробовать использовать встроенный компаратор AVR-ки.

Improver
29.03.2018, 16:15
Как вариант, ещё можно попробовать использовать встроенный компаратор AVR-ки.Да, и это тоже вариант, вот только я пока ещё не разобрался, как это сделать... :( Есть хороший пример?

Trol73
29.03.2018, 16:30
С точки зрения схемотехники попадался такой простой вариант сделать из двуполярного напряжения однополярное:
64819
На сколько оно будет дружить с входящим сигналом - надо проверять экспериментально..

Improver
06.04.2018, 16:09
Итак, прикрутил я встроенный ардуиновский компаратор, вот скетч:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.

/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V
* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит
int i0 = 0; // счётчик циклов без сигнала

volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned long PPeriod_sred; // среднее значение границы длинны полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x32, 0x39, 0x30, 0x33, 0x31, 0x38, // дата: 290318
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_record = 1; // запись
const int M_record_in = 11;
const int M_setup = 2; // настройки
const int M_setup_in = 12;

void setup() {
pinMode(InputPIN, INPUT); //объявляем пин как вход
pinMode(p, OUTPUT); // объявляем пин как выход
digitalWrite(p, LOW);
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH);
lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext("version 7.0",1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(1000); // ждем 1 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1<<AIN1D)|(1<<AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if (((sfileName[i+1]|0x20)=='r')&((sfileName[i+3]|0x20)=='m')) { // проверяем первый и третий символы расширения == 'r'|'R', == 'm'|'M'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
}
else { // проверяем расширение файла на "VKT"
if (((sfileName[i+1]|0x20)=='v')&((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t')) {
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Waiting...",0); // ожидание сигнала
i0 = 0; // Сбрасываем индексы
CRB = 0;

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1<<ACME); // включить мультиплексор
ADMUX = (1<<MUX0) // включить вход на A1
|(1<<REFS1)|(1<<REFS0); // опорное напряжение АЦП (?)

ACSR = (1<<ACIE) // Разрешить прерывание аналогового компаратора
|(1<<ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(0)!=HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Start...",0); // сигнал зафиксирован, записываем

noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
//====================================
do {
while ((CRB_temp <= CRB) & Pik)// пока индекс записи меньше или равен индексу чтения и есть сигнал
{ // Ждём заполнения буфера
i0++; // счиаем циклы
Pik = (i0 < 10); // что-то долго ждём...
delay(1); // задержка на ввод данных
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
if (!Pik) break; // выход из чтения т.к. сигнал закончился
i0 = 0;

dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
CRB++;
}
while (Pik); // пока есть сигнал
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128

if (((PPeriod_sred/3*2)%8) < 4) { // расчитываем полупериод для последующего вывода...
Tpp = (PPeriod_sred/12)*8; // округление до кратного 8 в меньшую сторону
}
else {
Tpp = ((PPeriod_sred/12)+1)*8; // округление до кратного 8 в большую сторону
}

if (CRB > 0) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
printtext("Done. Speed:",0);
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
}
else {
printtext("Canceled.",0);
if (!dataFile.remove()) {
delay(1000);
printtext("Error del.file",0);
}
}
dataFile.close();
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}
delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 264) {
Tb = 16; // для полупериода от 248 до 264
if (Tpp <= 240) Tb = 264 - Tpp;// для полупериода меньше или равном 240
}
else Tb = 0; // для полупериода больше 264
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 11; // не правильная метка
dataFile.seekSet(0); // на начало файла

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 250 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (unsigned int i=251; i<=Nbt-4; i++){ // всё остальное, кроме последних 3 байт
ToBUFF(dataFile.read());

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8);
lcd.print(' ');

if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
if (CRB > 200) Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
boolean Plong = false; // короткий полупериод по умолчанию
PPeriod = micros() - iMicros_old;
iMicros_old += PPeriod;
if (Pik) { // началось...
Plong = (PPeriod > PPeriod_sred); // = false, если тек. полупериод короткий
if (!Plong) { // если полупериод короткий, пересчитываем среднее значение
if (CWB <= 255) PPeriod_sred = (PPeriod_sred*CWB + PPeriod*3/2 + 15)/(CWB+1);
}
}
else { // был перерыв в сигнале
PPeriod_sred = 1024; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
}
// расшифровка данных
if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
// корректировка счётчиков
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
CWB++;
A = 0;
bBit += 15; // = +16 -1
}
Pik = true; // есть сигнал!
}
Что изменилось:
- Так как компаратор в ардуине умеет работать только с выводами D6, D7 и А0-А7 (первые два заняты экраном и А0 -- кнопками), сигнал с Вектора теперь принимается на входе А1.
- Улучшил алгоритм расчёта значения средней задержки полупериода сигнала -- с компаратором эти значения сильно "скакали". И теперь для расчёта используются только данные с первых 256 байт.

В работе компаратор использует внутреннее опорное напряжение 1,1В, а напряжение на выходе Вектора, насколько я помню, не превышает его по амплитуде, поэтому входная схема всё-таки потребуется. Это может быть транзистор, как в схеме zx-tapper, или может даже будет достаточно схемки Trol73 -- пока сказать не могу, надо пробовать... По идее, идеальным будет вариант, когда центр сигнала будет находиться на уровне опорного напряжения 1,1В, ведь для записи Векторовского сигнала нужно только чётко отмерять временные промежутки, когда меняется фаза. Можно пойти и другим путём: задействовать внешнее опорное напряжение на выводе AREF и сделать его равным нулю (заземлить вывод). Какой вариант лучше -- покажет практика. :)

Архив в полном комплекте: 64923

Improver
13.04.2018, 12:46
Для общего понимания, нарисовал схему подключений "магнитофона для Вектора" на ардуино:

http://zx-pk.ru/attachment.php?attachmentid=64975

64976 в более высоком качестве
Но хочу отметить, что проще и удобнее всё то же самое собрать на трёх платках (http://zx-pk.ru/threads/28819-rom-pleer-na-arduino.html?p=955274&viewfull=1#post955274): Arduino UNO r3 (на Atmega328p) + "Data Logger Shield v1.0" + "LCD Keypad Shield". В этом случае нужно будет только впаять в платку сопротивления и конденсаторы на вход и выход.

Пробовал записывать сигнал с компа, прямо со звуковой карты -- в принципе, для входящего сигнала достаточно будет сопротивлений (как на картинке), но для хорошей записи надо поиграться громкостью и регулировкой сопротивления R8. Оптимально, получается так: подключаешь вход к ПК (без сигнала), включаешь ардуину и выставляешь R8 так, чтобы на выводе А1 было напряжение 0,9-1,1В. Точно на 1,1В лучше не выставлять, иначе компаратор будет срабатывать на шумы. Хинт для ностальгирующих: в этот момент можно представить, что крутишь головку магнитофона. :D Ну и дальше уже можно включать запись.

Ближайшее время хочу всё-таки проверить работу на реальном Векторе...

Improver
09.06.2018, 17:06
Почти два месяца я гонял по-всякому подпрограмму записи с Вектора, но, к сожалению, пока что стабильной работы плеера нет... Буду продолжать, но хочу поделиться промежуточными результатами, чтобы не забылось. Вот последняя версия скетча:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.
#define vers "version 9.38"
/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Часы реального времени (RTC DS1307) подключены:
* SDA - A4
* SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит
//int i0 = 0; // счётчик циклов без сигнала

//volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x32, 0x39, 0x30, 0x33, 0x31, 0x38, // дата: 290318
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile unsigned int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_record = 1; // запись
const int M_record_in = 11;
const int M_setup = 2; // настройки
const int M_setup_in = 12;

void setup() {
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext(vers,1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1<<AIN1D)|(1<<AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if (((sfileName[i+1]|0x20)=='r')&((sfileName[i+3]|0x20)=='m')) { // проверяем первый и третий символы расширения == 'r'|'R', == 'm'|'M'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
}
else { // проверяем расширение файла на "VKT"
if (((sfileName[i+1]|0x20)=='v')&((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t')) {
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...",0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...",0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1<<ACME); // включить мультиплексор
ADMUX = (1<<MUX0) // включить вход на A1
|(1<<REFS1)|(1<<REFS0); // опорное напряжение АЦП (?)

ACSR = (1<<ACIE) // Разрешить прерывание аналогового компаратора
|(1<<ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0)!=HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...",0); // сигнал зафиксирован, записываем
printtext("Bytes:",1);
delay(100); // задержка для накопления инфы...

//====================================
do { // пока есть сигнал
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
if (CRB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(200); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~78 байт или ~31 байт на минимальной
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
CRB++;
if (CRB%10 == 0) {
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
}
while (CRB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128

Tpp = ((PPeriod_sred[0]+PPeriod_sred[1])/384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp%8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp/8)*8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp/8+1)*8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
printtext("Done. Speed:",0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1]>(PPeriod_sred[0]+3840))|(PPeriod_sred[0]>(PPeriod_sred[1]+3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("",1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0]/192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1]/192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp<160)|(Tpp>512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.",0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file",0);
}
}
dataFile.close(); // закрываем файл
Tpp = 200; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 11; // не правильная метка
dataFile.seekSet(0); // на начало файла

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 250 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (unsigned int i=251; i<=Nbt-4; i++){ // всё остальное, кроме последних 3 байт
ToBUFF(dataFile.read());

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8);
lcd.print(' ');

if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
/*if (CRB > 200)*/ Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
boolean Plong = false; // короткий полупериод по умолчанию
if (PPeriod < 1024) { // началось...
if (bBit < 16) { // если это не последний полубит
Plong = (PPeriod > (PPeriod_sred[PP]>>7)); // sred/128, =false, если тек. полупериод короткий
if (CWB <= 127) { // расчёт среднего значения, если меньше 256 байт считано
if (!Plong) { // если полупериод короткий
PPeriod_sred[PP] = (PPeriod_sred[PP]*CWB + PPeriod*192)/(CWB+1); // "среднее значение" = 1,5*128 от короткого полупериода
}
// else { // если полупериод длинный, берём PPeriod/2
// PPeriod_sred = (PPeriod_sred*CWB + PPeriod*3/4)/(CWB+1);
// }
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
Plong = (PPeriod > (PPeriod_sred[PP]*25/2432)); // с учётом увеличения периода между байтами, граница будет =([1,5]*25/19)/128 =1,97*Tpp
//Plong = (PPeriod > ((PPeriod_sred[PP]>>7) + 96)); // с учётом увеличения периода между байтами на 96 мкс
}
PP ^= 1; // инвертируем бит признака чётности полупериода

// расшифровка данных
if (Plong) { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
else { // получен короткий полупериод
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
// корректировка счётчиков
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
CWB++; // счётчик байтов +1
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}

}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
Что сделано:
1. Как оказалось, расчёт среднего времени не очень удачен, и хотя в экзеле показывал хорошие значения, но из-за округления результатов целочисленного деления в ардуине всё было плохо... В связи с этим сохраняемое "среднее значение" полупериода пришлось увеличить на два порядка (точнее умножить на 128), в таком виде он работает лучше.
2. Опять же, для улучшения результатов, решил сохранять средние значения двух полупериодов отдельно -- верхней и нижней части сигнала. Эти значения также можно использовать для оценки настройки компаратора, если они близки или даже равны, то всё настроено хорошо.
3. Теперь при записи показывается количество считанных байт, это позволяет контролировать процесс и оценить качество записи.
4. Возможно у меня SD-карта уже подглючивает, но последнее время начало первой записи в файл подвисает секунд на 5-10, пришлось сделать для этого тестовое сохранение байта до старта записи.

Что ещё вызывает проблемы:
- Межбайтовые промежутки... Это просто что-то -- при воспроизведении с Вектора они могут достигать двойной длинны, причём "тапир (https://svofski.github.io/tapir/)" сохранённую wav-ку проглатывает со 100% результатом, а вот ардуина капризничает... Сделал корректировку на начало байта, но она не всегда спасает. Вот один из лучших вариантов записанного файла: 65480 -- input004.vkt -- что было записано последней версией скетча, input0xx.vkt -- что должно быть. Сбой идёт с 340-го байта. А это wav-ка, записанная непосредственно с Вектора, "для опытов": 65482.

Надо думать, как это можно победить в ардуине. Я не исключаю такой вариант, что внутренний компаратор вносит свои погрешности, возможно надо попробовать сделать на внешнем...

И, в конце, полный комплект последней версии скетча: 65481

svofski
09.06.2018, 17:39
Алгоритмы Тапира не особо мудреные, их можно подсмотреть. cas.js (https://github.com/svofski/tapir/blob/master/cas.js) разбивает сигнал на пары интервалов между инверсиями LL/LS/SL/SS, а scanner.js (https://github.com/svofski/tapir/blob/master/scanner.js) из парных символов делает биты.

Кстати, если я правильно понимаю, эта часть ROM-плеера никак не привязана к железу. Такие вещи проще отлаживать на писишке. Сделать тестовый проект, который легко запустить, увидеть результат, может быть в отладчике пощупать. А когда алгоритм уже работает железно, вот тогда и засовывать в железо.

Improver
13.06.2018, 09:29
Алгоритмы Тапира не особо мудреные, их можно подсмотреть. cas.js (https://github.com/svofski/tapir/blob/master/cas.js) разбивает сигнал на пары интервалов между инверсиями LL/LS/SL/SS, а scanner.js (https://github.com/svofski/tapir/blob/master/scanner.js) из парных символов делает биты.Да, это несколько другой подход к расшифровке сигнала, хотя можно подумать и в этом направлении. Трудность с ардуиной в том, что, из-за недостатка памяти, всё делать надо за один проход и сразу по факту получения сигнала. А, с другой стороны, Вектор с этой задачей справляется легко, значит есть шансы. :-)


Кстати, если я правильно понимаю, эта часть ROM-плеера никак не привязана к железу. Такие вещи проще отлаживать на писишке. Сделать тестовый проект, который легко запустить, увидеть результат, может быть в отладчике пощупать. А когда алгоритм уже работает железно, вот тогда и засовывать в железо.Да, можно попробовать и так. Для приближения к реальности алгоритма можно даже ардуиной записать подряд все временные промежутки реального Векторовского сигнала, а потом отрабатывать на полученных данных... Объём, конечно, возрастёт в ~32 раза (до 16 промежутков на байт данных Х 2 байта на запись, т.к. промежутки часто больше 256 мкс.), но ардуина должна справится с таким потоком. Пойду, подумаю...:v2_dizzy_botan:

svofski
13.06.2018, 13:24
Трудность с ардуиной в том, что, из-за недостатка памяти, всё делать надо за один проход и сразу по факту получения сигнала.
Для этого алгоритма это не трудность. Мудрености там минимум и больше одного символа за раз ему не надо. Предварительный проход там делается для оценки временных параметров. В случае вполне надежного источника для этого достаточно начального писка. Принципиального отличия от того, как читает Вектор, по-моему нет. Просто немного по разному смотрим на то, что происходит с сигналом.

Improver
13.06.2018, 14:35
Предварительный проход там делается для оценки временных параметров.Вот про это я и намекал, что этот предварительный проход, к сожалению, нельзя сделать на ардуине (по крайней мере не создавая промежуточного временного файла)... И в этом и есть основная трудность -- сделать "на лету" правильную оценку временных параметров. А расшифровка сигнала на их основе -- задача не сложная. :)

Improver
15.06.2018, 11:58
Очередная версия скетча:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.
#define vers "version 9.57"
/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Часы реального времени (RTC DS1307) подключены:
* SDA - A4
* SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит
//int i0 = 0; // счётчик циклов без сигнала

//volatile unsigned long PPeriod = 0; // текущее значение полупериода
volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x32, 0x39, 0x30, 0x33, 0x31, 0x38, // дата: 290318
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x20, 0x20, 0x20, 0x00, 0x00 };

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile unsigned int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_record = 1; // запись
const int M_record_in = 11;
const int M_setup = 2; // настройки
const int M_setup_in = 12;

void setup() {
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext(vers,1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1<<AIN1D)|(1<<AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
if (((sfileName[i+1]|0x20)=='r')&((sfileName[i+3]|0x20)=='m')) { // проверяем первый и третий символы расширения == 'r'|'R', == 'm'|'M'
if (((sfileName[i+2]|0x20)=='o')|((sfileName[i+2]|0x20)=='0')) { // == 'o'|'O'|'0'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
}
else { // проверяем расширение файла на "VKT"
if (((sfileName[i+1]|0x20)=='v')&((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t')) {
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...",0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...",0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1<<ACME); // включить мультиплексор
ADMUX = (1<<MUX0) // включить вход на A1
|(1<<REFS1)|(1<<REFS0); // опорное напряжение АЦП (?)

ACSR = (1<<ACIE) // Разрешить прерывание аналогового компаратора
|(1<<ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0)!=HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...",0); // сигнал зафиксирован, записываем
printtext("Bytes:",1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB%256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CRB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CRB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128

Tpp = ((PPeriod_sred[0]+PPeriod_sred[1])/384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp%8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp/8)*8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp/8+1)*8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
printtext("Done. Speed:",0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1]>(PPeriod_sred[0]+3840))|(PPeriod_sred[0]>(PPeriod_sred[1]+3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("",1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0]/192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1]/192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp<160)|(Tpp>512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.",0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file",0);
}
}
dataFile.close(); // закрываем файл
Tpp = 200; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 11; // не правильная метка
dataFile.seekSet(0); // на начало файла

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 250 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (unsigned int i=251; i<=Nbt-4; i++){ // всё остальное, кроме последних 3 байт
ToBUFF(dataFile.read());

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8);
lcd.print(' ');

if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // пробелы
}
for (i=1; i<=3; i++){ // заносим в SB расширение файла
if ((FName[pnt+i]>=0x61) && (FName[pnt+i]<=0x7A)) {
SB[i+21] = FName[pnt+i] - 0x20; // на заглавные буквы
}
else {
SB[i+21] = FName[pnt+i];
}
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
}
else return 3; // нет файла
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
/*if (CRB > 200)*/ Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP]>>7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP]*CWB + PPeriod*192)/(CWB+1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP]/97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
В общем, оптимизация алгоритмов захвата сигнала, сокращение количества вызовов секций noInterrupts/Interrupts, а также увеличение границы отличия "короткого" от "длинного" полупериода до значения ~1,98*Трр дало некоторый положительный результат -- с ПК данные считываются более-менее стабильно на разных скоростях. Теперь опять дело за проверкой на реальном железе.

З.Ы. Так получилось, что индексы полубитов в байте ведутся в таком порядке: 15, 14, 13, ... 2, 1, 16. Вроде бы не логично, но переправлять не стал...:v2_unsur:

Последняя версия скетча с библиотеками в архиве: 65533

Improver
10.07.2018, 13:10
Готова очередная версия "магнитофона", в новой версии добавлено воспроизведение форматов:

- CAS -- универсальный формат для файлов Бейсика, Монитора-отладчика и, может быть, других программ. Файлы на SD-карте должны иметь расширение "cas".

- Формат Монитора-отладчика. В связи с тем, что для этого формата нужно обязательно задать начальный и конечный адреса загрузки, решил использовать такой же метод, как и у многих файлов этого формата в библиотеке Базис (http://www.sensi.org/scalar) -- начальный адрес загрузки определяется по расширению файла, конечный рассчитывается по размеру файла. Это выглядит так: "<filename>.mXX", где ХХ -- это старший разряд начального адреса загрузки в шестнадцатиричном виде, например file1.m01 -- загрузка с адреса 0100h, file2.mA0 -- загрузка с адреса A000h и т.п.
Примечание: если загрузка должна начинаться с адреса не кратного 256 (например, 01F0h), то это можно будет сделать только конвертацией в формат CAS.

- Формат записи командами Бейсика CSAVE/CLOAD. В этом формате воспроизводятся файлы с расширением "bas".

- Формат записи командами Бейсика BSAVE/BLOAD. Тут тоже требуется задание начального/конечного адресов, не стал изобретать велосипед и сделал всё аналогично формату монитора, только расширение должно быть вида "<filename>.bXX", где ХХ -- это старший разряд начального адреса загрузки в шестнадцатиричном виде.

- Формат записи Ассемблера-Редактора. Файлы должны быть с расширением "asm", их содержимое -- текст с концами строк 0Dh.

Важное примечание: на все перечисленные стандарты записи Вектор имеет фиксированную (или установленную по-умолчанию) скорость, которая, по расчётам, должна быть примерно равна полупериоду в 376мкс, жёстко устанавливать это значение в скетче я пока не стал, поэтому не забудьте выставлять его в настройках перед воспроизведением.

Итак, сам скетч:
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.

#define vers "version 10.5"
/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Часы реального времени (RTC DS1307) подключены:
* SDA - A4
* SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт
unsigned int StartAddr; // стартовый адрес для MON

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x30, 0x39, 0x30, 0x37, 0x31, 0x38, // дата: 090718
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x52, 0x4F, 0x4D, 0x00, 0x00 }; // расширение "ROM" и нули

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile unsigned int Tpp = 200; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 64; // Дополнительная задержка сигнала на начало байта в микросекундах

const int p = 3; // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_record = 1; // запись
const int M_record_in = 11;
const int M_setup = 2; // настройки
const int M_setup_in = 12;

void setup() {
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext(vers,1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1<<AIN1D)|(1<<AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > 0) MLevel--;
else MLevel = 2;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < 2) MLevel++;
else MLevel = 0;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = M_play; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);
for (int i=0;8;i++){ // цикл по имени файла
if (sfileName[i]=='.'){ // ищем точку в имени файла
switch((sfileName[i+1]|0x20)) // следующий после точки символ
{
case 'r': // первый символ == 'r'|'R'
if ( (((sfileName[i+2]|0x20)=='o')|(sfileName[i+2]=='0'))&((sfileName[i+3]|0x20)=='m') ) { // второй символ == 'o'|'O'|'0' и третий == 'm'|'M'
if ((sfileName[i+2]|0x20)!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, i);// Передаём короткое имя файла и позицию точки в качестве параметров
}
break;
case 'v': // первый символ == 'v'|'V'
if ( ((sfileName[i+2]|0x20)=='k')&((sfileName[i+3]|0x20)=='t') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
break;
case 'c': // первый символ == 'c'|'C'
if ( ((sfileName[i+2]|0x20)=='a')&((sfileName[i+3]|0x20)=='s') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(sfileName, i, 0); // вызов ПП для формата CAS
}
break;
case 'b': // первый символ == 'b'|'B'
if ( ((sfileName[i+2]|0x20)=='a')&((sfileName[i+3]|0x20)=='s') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(sfileName, i, 1); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'm': // первый символ == 'm'|'M' (или 'b'|'B')
StartAddr = 0x100;
if ((sfileName[i+2]>='0')&(sfileName[i+2]<='9')) { // преобразуем второй символ расширения в HEX
StartAddr = (sfileName[i+2] - '0')*16;
}
else if (((sfileName[i+2]|0x20)>='a')&((sfileName[i+2]|0x20)<='f')) {
StartAddr = ((sfileName[i+2]|0x20) - 87)*16;
}
if ((sfileName[i+3]>='0')&(sfileName[i+3]<='9')) { // преобразуем третий символ расширения в HEX
StartAddr += sfileName[i+3] - '0';
}
else if (((sfileName[i+3]|0x20)>='a')&((sfileName[i+3]|0x20)<='f')) {
StartAddr += (sfileName[i+3]|0x20) - 87;
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(sfileName, i, 2); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'a': // первый символ == 'a'|'A'
if ( ((sfileName[i+2]|0x20)=='s')&((sfileName[i+3]|0x20)=='m') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(sfileName, i, 3); // вызов ПП для формата ASM
}
break;
}
break; // нашли точку -- прерываем цикл
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printtext("Play file:",0);
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...",0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...",0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1<<ACME); // включить мультиплексор
ADMUX = (1<<MUX0) // включить вход на A1
|(1<<REFS1)|(1<<REFS0); // опорное напряжение АЦП (?)

ACSR = (1<<ACIE) // Разрешить прерывание аналогового компаратора
|(1<<ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0)!=HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...",0); // сигнал зафиксирован, записываем
printtext("Bytes:",1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB%256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CRB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CRB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128

Tpp = ((PPeriod_sred[0]+PPeriod_sred[1])/384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp%8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp/8)*8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp/8+1)*8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
printtext("Done. Speed:",0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1]>(PPeriod_sred[0]+3840))|(PPeriod_sred[0]>(PPeriod_sred[1]+3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("",1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0]/192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1]/192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp<160)|(Tpp>512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.",0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file",0);
}
}
dataFile.close(); // закрываем файл
Tpp = 200; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
if (buttonValue < 80) return BT_right;
else if (buttonValue < 200) return BT_up;
else if (buttonValue < 380) return BT_down;
else if (buttonValue < 600) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 11; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt-4; // уменьшаем размер данных на метку

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 250 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt>>8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

return PlayFile(251, 0);// выводим данные из файла, возвращаем код возврата из ПП
}
return 3; // ошибка / нет файла
}

int PlayAll(char FName[], int pnt, byte FType) // функция вывода остальных типов файлов
// входные данные: имя файла, длина имени, тип файла: 0=CAS; 1=BAS(C); 2=MON и BAS(B); 3=ASM
{
int i;
byte SB;
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=255; CWB++){ // первые 255 байт
if ((FType!=3)|((CWB/64==1)|(CWB/64==3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

// Tpp = 376; // стандартная скорость вывода для MON/BAS/ASM
// Tb = 0;
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
return PlayFile(0, 0);// выводим данные из файла, возвращаем код возврата из ПП
case 1: // это BAS(C)
for (i=0;i<=3;i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i=0;i<=3;i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i=0;i<=3;i++) ToBUFF(0xE6); // 4x 0xE6
}

for (i=0;i<pnt;i++) { // вывод имени файла без расширения
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB = FName[i];
}
else {
SB = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
ToBUFF(SB); // заносим в буфер очередную букву имени
}
for (i=0;i<=2;i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType==1) {
for (i=0;i<=767;i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i=0;i<=2;i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
return PlayFile(0, 2);// выводим данные из файла, возвращаем код возврата из ПП
}

// это MON, BAS(B) или ASM
for (i=0;i<=255;i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType==2) { // это MON или BAS(B)
ToBUFF(lowByte(StartAddr)); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddr+(Nbt-1)/256));// старший байт адреса конца записи
ToBUFF(lowByte(Nbt-1)); // младший байт адреса конца записи
return PlayFile(0, 1);// выводим данные из файла, возвращаем код возврата из ПП
}

// это ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
return PlayFile(0, 2);// выводим данные из файла, возвращаем код возврата из ПП
}
return 3; // ошибка / нет файла
}

int PlayFile(int Strt, byte TypeCS) // Функция вывода битов файла
// Strt -- нач.позиция, TypeCS -- вид контрольной суммы
{
unsigned int CSF = 0;
byte SB;
for (unsigned int i=Strt; i<Nbt; i++){ // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (TypeCS>0) CSF += SB;

if ((Nbt-i)%256==0){ // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
// выводим контрольную сумму
if (TypeCS > 0) ToBUFF(lowByte(CSF)); // младший байт КС
if (TypeCS > 1) ToBUFF(highByte(CSF)); // старший байт КС

WaitBuffer(); // ожидаем окончания вывода
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if ((FName[i]>=0x61) && (FName[i]<=0x7A)) {
SB[i+14] = FName[i] - 0x20; // на заглавные буквы
}
else if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i];
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb){ // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP]>>7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP]*CWB + PPeriod*192)/(CWB+1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP]/97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
Архив со всеми библиотеками: 65711

З.Ы. К сожалению, запись на "живом" Векторе я так и не проверил, эта часть скетча осталась без изменений.

Improver
15.10.2018, 17:28
Свежая версия "магнитофона на ардуино":
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.

#define vers "version 11.1"
/*
Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
* MOSI - D11
* MISO - D12
* CLK - D13
* CS - D10

Часы реального времени (RTC DS1307) подключены:
* SDA - A4
* SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
* LCD RS pin - D8
* LCD Enable pin - D9
* LCD D4 pin - D4
* LCD D5 pin - D5
* LCD D6 pin - D6
* LCD D7 pin - D7
* LCD R/W pin - GND
* LCD 5V pin - 5V

* Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в лиректории (файлов и поддиректорий)
byte isDir = 0; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

byte BLs = 0x01; // начальный блок
unsigned int Nbt; // размер файла, байт
unsigned int StartAddr; // стартовый адрес для MON
unsigned int CSF; // контрольная сумма

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
unsigned int CRB_temp = 0; // временная переменная для CRB
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

// Заголовок блока
byte SB[27] = {
0x4E, 0x4F, 0x44, 0x49, 0x53, 0x43, 0x30, 0x30, // NODISK00
0x30, 0x39, 0x30, 0x37, 0x31, 0x38, // дата: 090718
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // имя программы
0x52, 0x4F, 0x4D, 0x00, 0x00 }; // расширение "ROM" и нули

// строка часов
char DT[15] = {
'0', '0', ':', '0', '0', ':', '0', '0', ' ',
'0', '0', '/', '0', '0', 0x00 };

// имя файла для записи
char iFName[13] = "input000.vkt";

volatile unsigned int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile int Tb = 16; // Дополнительная задержка сигнала на начало байта в микросекундах
unsigned int TppSTD = 376; // Стандартная длительность задержки сигнала для файлов BAS/MON/ASM...

const int p = 3; // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const int BT_none = 0; // константы -- коды нажатой кнопки
const int BT_right = 1;
const int BT_up = 2;
const int BT_down = 3;
const int BT_left = 4;
const int BT_select = 5;

int MLevel = 0; // текущий пункт меню
const int M_play = 0; // воспроизведение
const int M_play_in = 10;
const int M_dplay = 1; // воспроизведение в формате DOS
const int M_dplay_in = 11;
const int M_record = 2; // запись
const int M_record_in = 12;
const int M_setup = 3; // настройки
const int M_setup_in = 13;

void setup() {
//Serial.begin(9600);
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF",0); // вывод названия
printtext(vers,1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!",1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
if (now.month() > 12) DT[0]=0x00; // если часы не работают, обнуляем первый байт в качестве флага
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10,SPI_FULL_SPEED)){ // SD-карта готова?
printtext("SD-card failed!",1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.",1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1<<AIN1D)|(1<<AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
int RCrom = 0; // для кода возврата из ПП
int button = getPressedButton(); // какая кнопка нажата?
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > M_play) MLevel--;
else MLevel = M_setup;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < M_setup) MLevel++;
else MLevel = M_play;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
switch (MLevel) // выводим надписи меню
{
case M_play: // воспроизведение
printtext("Play file:",0);
getMaxFile();
break;
case M_dplay: // воспроизведение (DOS)
printtext("Play file (DOS):",0);
getMaxFile();
break;
case M_record: // запись
printtext("Record to file:",0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName,1); // выводим имя файла для записи
break;
case M_setup: // настройки
printtext("Setup",0);
break;
}
MLevel += 10; // заходим в подменю
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->",0);
printtime();
break;
case M_dplay: // воспроизведение (DOS)
printtext("Play file (D)->",0);
printtime();
break;
case M_record: // запись
printtext("Record data ->",0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->",0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
case M_dplay_in:
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if(currentFile<1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if(currentFile>maxFile) {
currentFile=1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = MLevel - 10; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir==1) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...",0);

CSF = 0;
for (int i=0; i<=12; i++){ // Переводим все буквы имени файла в заглавные
if ((sfileName[i]>=0x61) && (sfileName[i]<=0x7A)) {
sfileName[i] &= 0xDF; // на заглавные буквы
}
if (sfileName[i]=='.'){ // ищем точку в имени файла
CSF = i; // запоминаем позицию точки в неиспользуемой сейчас переменной, для этого не предназначенной
}
}

if (MLevel == M_dplay_in) { // если вывод в формате DOS
RCrom = PlayAll(sfileName, CSF, 4); // вызов ПП для формата DOS
}
else { // другие форматы
switch(sfileName[CSF+1]) // следующий после точки символ
{
case 'R': // первый символ == 'r'|'R'
if ( ((sfileName[CSF+2]=='O')|(sfileName[CSF+2]=='0'))&(sfileName[CSF+3]=='M') ) { // второй символ == 'o'|'O'|'0' и третий == 'm'|'M'
if (sfileName[CSF+2]!='0') { // проверка на вывод нулевого блока по расширению файла
BLs = 0x01; // с первого блока
}
else {
BLs = 0x00; // с нулевого блока
}
RCrom = PlayROM(sfileName, CSF);// Передаём короткое имя файла и позицию точки в качестве параметров
}
break;
case 'V': // первый символ == 'v'|'V'
if ( (sfileName[CSF+2]=='K')&(sfileName[CSF+3]=='T') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(sfileName); // вызов ПП для формата VKT
}
break;
case 'C': // первый символ == 'c'|'C'
if ( (sfileName[CSF+2]=='A')&(sfileName[CSF+3]=='S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(sfileName, CSF, 0); // вызов ПП для формата CAS
}
break;
case 'B': // первый символ == 'b'|'B'
if ( (sfileName[CSF+2]=='A')&(sfileName[CSF+3]=='S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(sfileName, CSF, 1); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'M': // первый символ == 'm'|'M' (или 'b'|'B')
StartAddr = 0x100;
if ((sfileName[CSF+2]>='0')&(sfileName[CSF+2]<='9')) { // преобразуем второй символ расширения в HEX
StartAddr = (sfileName[CSF+2] - '0')*16;
}
else if ((sfileName[CSF+2]>='A')&(sfileName[CSF+2]<='F')) {
StartAddr = (sfileName[CSF+2] - 55)*16;
}
if ((sfileName[CSF+3]>='0')&(sfileName[CSF+3]<='9')) { // преобразуем третий символ расширения в HEX
StartAddr += sfileName[CSF+3] - '0';
}
else if ((sfileName[CSF+3]>='A')&(sfileName[CSF+3]<='F')) {
StartAddr += (sfileName[CSF+3] - 55);
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(sfileName, CSF, 2); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'A': // первый символ == 'a'|'A'
if ( (sfileName[CSF+2]=='S')&(sfileName[CSF+3]=='M') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(sfileName, CSF, 3); // вызов ПП для формата ASM
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.",0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped",0); // Сообщение об остановке
while(getPressedButton()!=BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big",0); // большой файл
break;
case 11:
printtext("Unknown format",0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
case 12:
printtext("Bad speed mark",0); // метка скорости в VKT не правильная (больше или меньше границы)
break;
default:
printtext("ERROR!",0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
if (MLevel != M_dplay_in) { // если вывод в не формате DOS
printtext("Play file:",0);
}
else {
printtext("Play file (DOS):",0);
}
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...",0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...",0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1<<ACME); // включить мультиплексор
ADMUX = (1<<MUX0) // включить вход на A1
|(1<<REFS1)|(1<<REFS0); // опорное напряжение АЦП (?)

ACSR = (1<<ACIE) // Разрешить прерывание аналогового компаратора
|(1<<ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0)!=HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...",0); // сигнал зафиксирован, записываем
printtext("Bytes:",1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB%256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CRB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CRB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CRB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128
Pik = false; // сбрасываем флаг "есть сигнал"

Tpp = ((PPeriod_sred[0]+PPeriod_sred[1])/384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp%8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp/8)*8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp/8+1)*8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT[0] > 0x00) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE,now.year(),now.month() ,now.day(),now.hour(),now.minute(),now.second());
dataFile.timestamp(T_WRITE,now.year(),now.month(), now.day(),now.hour(),now.minute(),now.second());
}
printtext("Done. Speed:",0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1]>(PPeriod_sred[0]+3840))|(PPeriod_sred[0]>(PPeriod_sred[1]+3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("",1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0]/192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1]/192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp<160)|(Tpp>512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.",0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file",0);
}
}
dataFile.close(); // закрываем файл
Tpp = 256; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file",0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):",1);
switch (button)
{
case BT_up: // вверх
if (Tpp<400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp>160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12,1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
int getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
//Serial.println(buttonValue);
if (buttonValue < 60) return BT_right;
else if (buttonValue < 180) return BT_up;
else if (buttonValue < 330) return BT_down;
else if (buttonValue < 530) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, int l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0,l);
lcd.print(text);
lcd.print(" "); // очистка строки после текста
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT[0] > 0x00) { // если не обнулён первый байт -- часы работают
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour()/10 + '0'; // перевод из целого в символ
DT[1] = now.hour()%10 + '0'; // часы
DT[3] = now.minute()/10 + '0'; // минуты
DT[4] = now.minute()%10 + '0'; // минуты
DT[6] = now.second()/10 + '0'; // секунды
DT[7] = now.second()%10 + '0'; // секунды
DT[9] = now.day()/10 + '0'; // день
DT[10] = now.day()%10 + '0'; // день
DT[12] = now.month()/10 + '0'; // месяц
DT[13] = now.month()%10 + '0'; // месяц
lcd.print(DT); // выводим время и дату
lcd.print(" ");
}
else {
lcd.print(millis()/1000); // выводим количество секунд с момента влючения ардуины вместо времени
lcd.print(" ");
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile=0;
while(dataFile.openNext(dataFile.cwd(),O_READ)) {
dataFile.close();
maxFile++;
}
currentFile=1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for(int i=1;i<currentFile;i++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(),O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(),O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize()<=0xFFFE) {// проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (isDir!=1) { // это не директория?
printtext(sfileName,1); // вывод имени текущего файла
}
else {
for (int i=0;i<14;i++) { // ищем конец имени файла
if (sfileName[i]==0x00) {
sfileName[i]='>'; // если это директория, добавляем в конце символ '>'
sfileName[i+1]=0x00;
printtext(sfileName,1); // вывод имени текущей директории
sfileName[i]=0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void SwapTpp() // поменять местами значения переменных Tpp и TppSTD
{
Tpp = Tpp + TppSTD; // меняем местами значения Tpp и TppSTD
TppSTD = Tpp - TppSTD;
Tpp = Tpp - TppSTD;
CalcTb();
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
do{ // Ждём опустошения буфера
delay(Tpp/64); // задержка на вывод 1 байта (~Tpp*16/1000)
BUFF[lowByte(CWB+1)] = 0x00; // обнуляем следующий за последним байт -- иногда и он успевает прочитаться...
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_temp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

int PlayVKT(char FName[]) // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt-3); // на позицию -3 байт от конца файла
if (dataFile.read()!=0xFF) return 11; // метка не найдена
Tpp = dataFile.read()*256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160)|(Tpp > 511)) return 12; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt-4; // уменьшаем размер данных на метку (4) и предварительно считанные данные (251)

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB=0; CWB<=250; CWB++){ // первые 251 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt>>8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

int RCV = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (RCV == 0) WaitBuffer(); // ожидаем окончания вывода
return RCV; // выходим с кодом
}
return 3; // ошибка / нет файла
}

int PlayAll(char FName[], int pnt, byte FType) // функция вывода остальных типов файлов
// входные данные: имя файла, длина имени, тип файла:
// 0=CAS и VKT; 1=BAS(C); 2=MON и BAS(B); 3=ASM; 4=DOS
// Возвращаемые RC:
// = 0 -- всё ок
// = 1 -- прерывание по кнопке
// = 2 -- чтение обогнало запись в буфер
{
int i, rcp;
// byte SB; // выводимый байт
delay(1000); // ждем 1 с
if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;

// Начинаем наполнять буфер
for (CWB=0; CWB<=255; CWB++){ // первые 256 байт
if ((FType!=3)|((CWB/64==1)|(CWB/64==3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

SwapTpp(); // выставляем стандартную скорость вывода
// Tpp = 376; // стандартная скорость вывода
// Tb = 0;
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
rcp = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (rcp == 0) WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
case 1: // это BAS(C)
for (i=0;i<=3;i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i=0;i<=3;i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i=0;i<=3;i++) ToBUFF(0xE6); // 4x 0xE6
break;
case 4: // это DOS
ToBUFF(0x01);
ToBUFF(0x00); // 0x0100 -- начальный адрес (всегда)
ToBUFF(highByte(Nbt-1)+1); // старший байт конечного адреса (нач.адрес+длинна данных)
ToBUFF(0xFF); // младший байт конечного адреса (всегда)
rcp = PlayFile(true); // выводим байты из файла с подсчётом КС
if (rcp > 0) {
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
for (i=lowByte(Nbt-1);i<0xFF;i++) ToBUFF(0x00); // дополняем, если надо, нулями
ToBUFF(lowByte(CSF)); // выводим младший байт КС
}

byte SB; // выводимый байт
for (i=0;i<pnt;i++) { // вывод имени файла без расширения, DOS поддерживаются только 'A'-'Z','0'-'9'
if (FName[i]!=0x7E) { // не тильда
SB = FName[i]; // берём символ символ
}
else { // тильда -- часто встречабщийся символ, остальные можно переименовать вручную
if (FType!=4) {
SB = '_'; // меняем тильду на подчёркивание
}
else SB = '0'; // для DOS меняем тильду на нолик, иначе при загрузке будет ошибка
}
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву имени
}

// это DOS
if (FType==4) {
for (i=pnt;i<8;i++) { // дополняем имя пробелами до 8 байт
ToBUFF(0x20);
CSF += 0x20;
}
for (i=1;i<4;i++) { // выводим расширение файла
SB = FName[pnt+i];
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву расширени
}
ToBUFF(lowByte(CSF)); // выводим младший байт КС (данные + имя файла)
WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return 0; // выходим с кодом 0
}

for (i=0;i<=2;i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType==1) {
for (i=0;i<=767;i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i=0;i<=2;i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// это MON, BAS(B) или ASM
for (i=0;i<=255;i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType==2) { // это MON или BAS(B)
ToBUFF(lowByte(StartAddr)); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddr+(Nbt-1)/256));// старший байт адреса конца записи
ToBUFF(lowByte(Nbt-1)); // младший байт адреса конца записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// остался только ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(0xFF); // дополняем FFh в конце
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
return 3; // ошибка / нет файла
}

int PlayFile(boolean CSFp) // Функция вывода битов файла
// CSFp признак необходимости подсчёта контрольной суммы файла
{
byte SB;
CSF = 0; // обнуляем контрольную сумму
for (unsigned int i=0; i<Nbt; i++){ // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (CSFp) CSF += SB;

if ((Nbt-i)%256==0){ // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt-i)>>8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
return 0;
}

int PlayROM(char FName[], int pnt) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(FName,O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt/256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;

if (Nbt%256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

for (i=0; i<=7; i++){ // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if (FName[i]!=0x7E) { // не тильда
SB[i+14] = FName[i]; // заносим символ
}
else {
SB[i+14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i+14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate)/10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate)%10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate)/10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate)%10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate)%100)/10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate)%10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i=0; i<=3; i++){ // преамбула (4*(00H*25+55H*25))
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j=0; j<=24; j++){
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt=BLe; BLt>=1; BLt--){ // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j=0; j<=15; j++) ToBUFF(0x00);// 00h*16
for (j=0; j<=3; j++) ToBUFF(0x55);// 55h*4
ToBUFF(0xE6); // E6h*1
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4

for (j=0; j<=26; j++){ // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst=0x80; Nst<=0x87; Nst++){ // вывод строк (8 шт.)
for (j=0; j<=3; j++) ToBUFF(0x00);// 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j=0; j<=31; j++){ // цикл на 32 байта
if (Nbt > 0){ // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton()!=BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
if (CRB_temp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j=0; j<=31; j++) ToBUFF(0x00);// 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb) { // Подпрограмма записи байта в буфер
noInterrupts(); // запрет прерываний
CRB_temp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_temp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd=PORTD;
if (bBit & 1){ // проверка индекса полубитов на чётность
if (( (Pd >> p)^( BUFF[lowByte(CRB)] >> (bBit >> 1) ))&1){ // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else{ // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else{
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp+Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP]>>7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP]*CWB + PPeriod*192)/(CWB+1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A<<1)+B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP]/97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A<<1)+B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
Изменения по отношению к предыдущей:
- добавлена поддержка формата записи DOS. В связи с тем, что в этом формате можно загружать файлы с практически любыми именами и расширениями, для него сделан отдельный пункт меню.
- для всех форматов, кроме ROM и VKT, теперь автоматически выставляется стандартная скорость 376мкс.
- исправлен вывод в формате ассемблера: в конце файла, перед контрольной суммой, должен быть байт 0xFF, иначе загрузка файла не останавливается после окончания воспроизведения.
- небольшие изменения и улучшения по скетчу.

Архив со скетчем и библиотеками: 66569

То, что я писал тут (http://zx-pk.ru/threads/9532-vektor-06ts-sredstva-razrabotki.html?p=981705&viewfull=1#post981705) про формат DOS подтвержается экспериментами на эмуляторе, плюс ещё такие моменты:
- если выгружать данные размером не кратным 256 байтам, то всё равно на диске при загрузке файл будет округлён до этого значения.
- имя файла для выгрузки на ленту командами "3" и "savedos" может быть любым допустимым, но вот назад считаются только файлы с именами из заглавных латинских букв и цифр.

Improver
27.11.2018, 14:52
Обновление "магнитофона на ардуино":
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.
#define vers "version 11.3"
/*Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
MOSI - D11
MISO - D12
CLK - D13
CS - D10

Часы реального времени (RTC DS1307) подключены:
SDA - A4
SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
LCD RS pin - D8
LCD Enable pin - D9
LCD D4 pin - D4
LCD D5 pin - D5
LCD D6 pin - D6
LCD D7 pin - D7
LCD R/W pin - GND
LCD 5V pin - 5V

Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в директории (файлов и поддиректорий)
boolean isDir = false; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

unsigned int Nbt; // размер файла, байт
unsigned int CSF; // контрольная сумма

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

boolean DT_good = true; // Признак наличия часов

volatile unsigned int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile byte Tb = 16; // Дополнительная задержка сигнала на начало байта в микросекундах
unsigned int TppSTD = 376; // Стандартная длительность задержки сигнала для файлов BAS/MON/ASM...

#define p 3 // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const byte BT_none = 0; // константы -- коды нажатой кнопки
const byte BT_right = 1;
const byte BT_up = 2;
const byte BT_down = 3;
const byte BT_left = 4;
const byte BT_select = 5;

byte MLevel = 0; // текущий пункт меню
const byte M_play = 0; // воспроизведение
const byte M_play_in = 10;
const byte M_dplay = 1; // воспроизведение в формате DOS
const byte M_dplay_in = 11;
const byte M_record = 2; // запись
const byte M_record_in = 12;
const byte M_setup = 3; // настройки
const byte M_setup_in = 13;

void setup() {
//Serial.begin(9600);
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF", 0); // вывод названия
printtext(vers, 1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!", 1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
DT_good = !(now.month() > 12); // если часы не работают, выставляем признак
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10, SPI_FULL_SPEED)) { // SD-карта готова?
printtext("SD-card failed!", 1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.", 1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1 << AIN1D) | (1 << AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
byte RCrom = 0; // для кода возврата из ПП
unsigned int StartAddr = 0x100; // стартовый адрес для MON
char iFName[13] = "input000.vkt"; // имя файла для записи

byte button = getPressedButton(); // какая кнопка нажата?
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > M_play) MLevel--;
else MLevel = M_setup;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < M_setup) MLevel++;
else MLevel = M_play;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
MLevel += 10; // заходим в подменю
switch (MLevel) // выводим надписи меню
{
case M_play_in: // воспроизведение
case M_dplay_in: // воспроизведение (DOS)
printplay(); // вывод надписи "Play file..."
getMaxFile();
break;
case M_record_in: // запись
printtext("Record to file:", 0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName, 1); // выводим имя файла для записи
break;
case M_setup_in: // настройки
printtext("Setup", 0);
break;
}
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->", 0);
printtime();
break;
case M_dplay: // воспроизведение (DOS)
printtext("Play to DOS ->", 0);
printtime();
break;
case M_record: // запись
printtext("Record data ->", 0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->", 0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
case M_dplay_in:
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if (currentFile < 1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if (currentFile > maxFile) {
currentFile = 1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = MLevel - 10; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...", 0);

byte PNT = 0;
for (byte i = 0; i <= 12; i++) { // Переводим все буквы имени файла в заглавные
if ((sfileName[i] >= 0x61) && (sfileName[i] <= 0x7A)) {
sfileName[i] &= 0xDF; // на заглавные буквы
}
if (sfileName[i] == '.') { // ищем точку в имени файла
PNT = i; // запоминаем позицию точки в неиспользуемой сейчас переменной, для этого не предназначенной
}
}

if (MLevel == M_dplay_in) { // если вывод в формате DOS
RCrom = PlayAll(PNT, 4, 0); // вызов ПП для формата DOS
}
else { // другие форматы
switch (sfileName[PNT + 1]) // следующий после точки символ
{
case 'R': // первый символ == 'r'|'R'
if ( ((sfileName[PNT + 2] == 'O') | (sfileName[PNT + 2] == '0')) & (sfileName[PNT + 3] == 'M') ) { // второй символ == 'o'|'O'|'0' и третий == 'm'|'M'
if (sfileName[PNT + 2] != '0') { // проверка на вывод нулевого блока по расширению файла
RCrom = PlayROM(PNT, 0x01);// Передаём позицию точки в качестве параметра, вывод с первого блока
}
else {
RCrom = PlayROM(PNT, 0x00);// Передаём позицию точки в качестве параметра, вывод с нулевого блока
}
}
break;
case 'V': // первый символ == 'v'|'V'
if ( (sfileName[PNT + 2] == 'K') & (sfileName[PNT + 3] == 'T') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(); // вызов ПП для формата VKT
}
break;
case 'C': // первый символ == 'c'|'C'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(PNT, 0, 0); // вызов ПП для формата CAS
}
break;
case 'B': // первый символ == 'b'|'B'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(PNT, 1, 0); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'M': // первый символ == 'm'|'M' (или 'b'|'B')
//StartAddr = 0x100;
if ((sfileName[PNT + 2] >= '0') & (sfileName[PNT + 2] <= '9')) { // преобразуем второй символ расширения в HEX
StartAddr = (sfileName[PNT + 2] - '0') * 16;
}
else if ((sfileName[PNT + 2] >= 'A') & (sfileName[PNT + 2] <= 'F')) {
StartAddr = (sfileName[PNT + 2] - 55) * 16;
}
if ((sfileName[PNT + 3] >= '0') & (sfileName[PNT + 3] <= '9')) { // преобразуем третий символ расширения в HEX
StartAddr += sfileName[PNT + 3] - '0';
}
else if ((sfileName[PNT + 3] >= 'A') & (sfileName[PNT + 3] <= 'F')) {
StartAddr += (sfileName[PNT + 3] - 55);
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(PNT, 2, StartAddr); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'A': // первый символ == 'a'|'A'
if ( (sfileName[PNT + 2] == 'S') & (sfileName[PNT + 3] == 'M') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(PNT, 3, 0); // вызов ПП для формата ASM
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.", 0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped", 0); // Сообщение об остановке
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big", 0); // большой файл
break;
case 11:
printtext("Unknown format", 0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
case 12:
printtext("Bad speed mark", 0); // метка скорости в VKT не правильная (больше или меньше границы)
break;
default:
printtext("ERROR!", 0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printplay(); // вывод надписи "Play file..."
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...", 0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...", 0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1 << ACME); // включить мультиплексор
ADMUX = (1 << MUX0) // включить вход на A1
| (1 << REFS1) | (1 << REFS0); // опорное напряжение АЦП (?)

ACSR = (1 << ACIE) // Разрешить прерывание аналогового компаратора
| (1 << ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0) != HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...", 0); // сигнал зафиксирован, записываем
printtext("Bytes:", 1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
unsigned int CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB % 256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CWB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CWB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128
Pik = false; // сбрасываем флаг "есть сигнал"

Tpp = ((PPeriod_sred[0] + PPeriod_sred[1]) / 384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp % 8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp / 8) * 8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp / 8 + 1) * 8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT_good) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
dataFile.timestamp(T_WRITE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
}
printtext("Done. Speed:", 0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1] > (PPeriod_sred[0] + 3840)) | (PPeriod_sred[0] > (PPeriod_sred[1] + 3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("", 1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0] / 192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1] / 192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp < 160) | (Tpp > 512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.", 0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file", 0);
}
}
dataFile.close(); // закрываем файл
Tpp = 256; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file", 0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):", 1);
switch (button)
{
case BT_up: // вверх
if (Tpp < 400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp > 160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12, 1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
byte getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
//Serial.println(buttonValue);
if (buttonValue < 60) return BT_right;
else if (buttonValue < 180) return BT_up;
else if (buttonValue < 330) return BT_down;
else if (buttonValue < 530) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, byte l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0, l);
//byte WRB = lcd.print(text);
for (byte i = lcd.print(text); i < 16; i++) lcd.print(' '); // вывод текста и очистка строки до конца
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT_good) { // если часы работают
char DT[17];
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour() / 10 + '0'; // перевод из целого в символ
DT[1] = now.hour() % 10 + '0'; // часы
DT[2] = ':';
DT[3] = now.minute() / 10 + '0'; // минуты
DT[4] = now.minute() % 10 + '0'; // минуты
DT[5] = ':';
DT[6] = now.second() / 10 + '0'; // секунды
DT[7] = now.second() % 10 + '0'; // секунды
DT[8] = ' ';
DT[9] = now.day() / 10 + '0'; // день
DT[10] = now.day() % 10 + '0'; // день
DT[11] = '/';
DT[12] = now.month() / 10 + '0'; // месяц
DT[13] = now.month() % 10 + '0'; // месяц
DT[14] = ' ';
DT[15] = ' ';
DT[16] = 0x00;
lcd.print(DT); // выводим время и дату
}
else {
byte WRB = lcd.print(millis() / 1000); // выводим количество секунд с момента влючения ардуины вместо времени
for (byte i = WRB; i < 16; i++) lcd.print(' '); // очистка строки после текста
}
}

void printplay() {
if (MLevel != M_dplay_in) { // если вывод в не формате DOS
printtext("Play file:", 0);
}
else {
printtext("Play file (DOS):", 0);
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile = 0;
while (dataFile.openNext(dataFile.cwd(), O_READ)) {
dataFile.close();
maxFile++;
}
currentFile = 1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for (int X = 1; X < currentFile; X++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(), O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(), O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize() <= 0xFFFE) { // проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (!isDir) { // это не директория?
printtext(sfileName, 1); // вывод имени текущего файла
if (Nbt < 0xFFFF) { // если размер файла в допустимых пределах
lcd.setCursor(16, 1);
byte WRB;
if (Nbt >= 1000) { // если размер больше 1000 байт
WRB = lcd.print(Nbt / 1024); // подсчёт числа символов
lcd.setCursor(15 - WRB, 1);
lcd.print(Nbt / 1024); // вывод размера файла в кБ
lcd.print('k');
}
else {
WRB = lcd.print(Nbt); // подсчёт числа символов
lcd.setCursor(16 - WRB, 1);
lcd.print(Nbt); // вывод размера файла в байтах
}
}
}
else {
for (byte i = 0; i < 14; i++) { // ищем конец имени файла
if (sfileName[i] == 0x00) {
sfileName[i] = '>'; // если это директория, добавляем в конце символ '>'
sfileName[i + 1] = 0x00;
printtext(sfileName, 1); // вывод имени текущей директории
sfileName[i] = 0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void SwapTpp() // поменять местами значения переменных Tpp и TppSTD
{
Tpp = Tpp + TppSTD; // меняем местами значения Tpp и TppSTD
TppSTD = Tpp - TppSTD;
Tpp = Tpp - TppSTD;
CalcTb();
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
unsigned int CRB_tmp;
do { // Ждём опустошения буфера
delay(Tpp / 64); // задержка на вывод 1 байта (~Tpp*16/1000)
BUFF[lowByte(CWB + 1)] = 0x00; // обнуляем следующий за последним байт -- иногда и он успевает прочитаться...
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_tmp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

byte PlayVKT() // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt - 3); // на позицию -3 байт от конца файла
if (dataFile.read() != 0xFF) return 11; // метка не найдена
Tpp = dataFile.read() * 256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160) | (Tpp > 511)) return 12; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt - 4; // уменьшаем размер данных на метку (4) и предварительно считанные данные (251)

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 250; CWB++) { // первые 251 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt >> 8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

int RCV = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (RCV == 0) WaitBuffer(); // ожидаем окончания вывода
return RCV; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayAll(byte pnt, byte FType, unsigned int StartAddr) // функция вывода остальных типов файлов
// входные данные: длина имени, тип файла, начальный адрес. Тип файла это:
// 0=CAS и VKT; 1=BAS(C); 2=MON и BAS(B); 3=ASM; 4=DOS
// Возвращаемые RC:
// = 0 -- всё ок
// = 1 -- прерывание по кнопке
// = 2 -- чтение обогнало запись в буфер
{
int i, rcp;
// byte SB; // выводимый байт
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 255; CWB++) { // первые 256 байт
if ((FType != 3) | ((CWB / 64 == 1) | (CWB / 64 == 3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

SwapTpp(); // выставляем стандартную скорость вывода
// Tpp = 376; // стандартная скорость вывода
// Tb = 0;
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
rcp = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (rcp == 0) WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
case 1: // это BAS(C)
for (i = 0; i <= 3; i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i = 0; i <= 3; i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i = 0; i <= 3; i++) ToBUFF(0xE6); // 4x 0xE6
break;
case 4: // это DOS
ToBUFF(0x01);
ToBUFF(0x00); // 0x0100 -- начальный адрес (всегда)
ToBUFF(highByte(Nbt - 1) + 1); // старший байт конечного адреса (нач.адрес+длинна данных)
ToBUFF(0xFF); // младший байт конечного адреса (всегда)
rcp = PlayFile(true); // выводим байты из файла с подсчётом КС
if (rcp > 0) {
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
for (i = lowByte(Nbt - 1); i < 0xFF; i++) ToBUFF(0x00); // дополняем, если надо, нулями
ToBUFF(lowByte(CSF)); // выводим младший байт КС
}

byte SB; // выводимый байт
for (i = 0; i < pnt; i++) { // вывод имени файла без расширения, DOS поддерживаются только 'A'-'Z','0'-'9'
if (sfileName[i] != 0x7E) { // не тильда
SB = sfileName[i]; // берём символ символ
}
else { // тильда -- часто встречабщийся символ, остальные можно переименовать вручную
if (FType != 4) {
SB = '_'; // меняем тильду на подчёркивание
}
else SB = '0'; // для DOS меняем тильду на нолик, иначе при загрузке будет ошибка
}
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву имени
}

// это DOS
if (FType == 4) {
for (i = pnt; i < 8; i++) { // дополняем имя пробелами до 8 байт
ToBUFF(0x20);
CSF += 0x20;
}
for (i = 1; i < 4; i++) { // выводим расширение файла
SB = sfileName[pnt + i];
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву расширени
}
ToBUFF(lowByte(CSF)); // выводим младший байт КС (данные + имя файла)
WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return 0; // выходим с кодом 0
}

for (i = 0; i <= 2; i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType == 1) {
for (i = 0; i <= 767; i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i = 0; i <= 2; i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// это MON, BAS(B) или ASM
for (i = 0; i <= 255; i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType == 2) { // это MON или BAS(B)
ToBUFF(lowByte(StartAddr)); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddr + (Nbt - 1) / 256)); // старший байт адреса конца записи
ToBUFF(lowByte(Nbt - 1)); // младший байт адреса конца записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// остался только ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(0xFF); // дополняем FFh в конце
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayFile(boolean CSFp) // Функция вывода битов файла
// CSFp признак необходимости подсчёта контрольной суммы файла
{
byte SB;
unsigned int CWB_tmp;
CSF = 0; // обнуляем контрольную сумму
for (unsigned int i = 0; i < Nbt; i++) { // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (CSFp) CSF += SB;

if ((Nbt - i) % 256 == 0) { // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt - i) >> 8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CWB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB_tmp > CWB) { // проверка -- не обогнало ли воспроизведение запись в буфер?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
return 0;
}

byte PlayROM(int pnt, byte BLs) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt / 256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;
unsigned int CRB_tmp;

if (Nbt % 256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

// Заголовок блока
byte SB[25];
SB[0] = 0x4E;
SB[1] = 0x4F;
SB[2] = 0x44;
SB[3] = 0x49;
SB[4] = 0x53;
SB[5] = 0x43;
SB[6] = 0x30;
SB[7] = 0x30; // NODISK00
SB[22] = 0x52;
SB[23] = 0x4F;
SB[24] = 0x4D; // расширение "ROM"

for (i = 0; i <= 7; i++) { // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if (sfileName[i] != 0x7E) { // не тильда
SB[i + 14] = sfileName[i]; // заносим символ
}
else {
SB[i + 14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i + 14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate) / 10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate) % 10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate) / 10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate) % 10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate) % 100) / 10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate) % 10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i = 0; i <= 3; i++) { // преамбула (4*(00H*25+55H*25))
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt = BLe; BLt >= 1; BLt--) { // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j = 0; j <= 15; j++) ToBUFF(0x00); // 00h*16
for (j = 0; j <= 3; j++) ToBUFF(0x55); // 55h*4
ToBUFF(0xE6); // E6h*1
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4

for (j = 0; j <= 24; j++) { // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(0x00);
ToBUFF(0x00);
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst = 0x80; Nst <= 0x87; Nst++) { // вывод строк (8 шт.)
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j = 0; j <= 31; j++) { // цикл на 32 байта
if (Nbt > 0) { // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CRB_tmp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j = 0; j <= 31; j++) ToBUFF(0x00); // 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb) { // Подпрограмма записи байта в буфер
unsigned int CRB_tmp;
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_tmp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd = PORTD;
if (bBit & 1) { // проверка индекса полубитов на чётность
if (( (Pd >> p) ^ ( BUFF[lowByte(CRB)] >> (bBit >> 1) )) & 1) { // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else { // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else {
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp + Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP] >> 7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP] * CWB + PPeriod * 192) / (CWB + 1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A << 1) + B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP] / 97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
Изменения:
1. Оптимизация глобальных переменных. Запустил компиляцию в Arduino IDE 1.8.7, а она рекомендует освободить больше памяти для переменных для большей стабильности. Сделал, насколько это возможно, многие переменные из глобальных переделал в локальные...
2. При использовании магнитофона заметил, что будет удобно предварительно увидеть размер файла перед выгрузкой, добавил эту функцию. Теперь справа от имени файла показывается его размер в байтах или килобайтах (округляется в меньшую сторону до целых), на файлах больше 63Кб и директориях размер не показывается.

Архив, скетч + библиотеки: 67046

Improver
10.12.2018, 10:22
Ещё два небольших дополнения в "магнитофоне":
1. Теперь файлы с расширением "*.com" и "*.c0m" воспроизводятся "магнитофоном" в формате загрузчика, также, как и "*.rom" ("*.r0m").
2. При воспроизведении в формате ДОС файлов с расширением "*.rom" они сразу переименовываются в "*.com", чтобы потом не переименовывать их вручную. :)
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.
#define vers "version 11.5"
/*Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
MOSI - D11
MISO - D12
CLK - D13
CS - D10

Часы реального времени (RTC DS1307) подключены:
SDA - A4
SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
LCD RS pin - D8
LCD Enable pin - D9
LCD D4 pin - D4
LCD D5 pin - D5
LCD D6 pin - D6
LCD D7 pin - D7
LCD R/W pin - GND
LCD 5V pin - 5V

Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в директории (файлов и поддиректорий)
boolean isDir = false; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

unsigned int Nbt; // размер файла, байт
unsigned int CSF; // контрольная сумма

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

boolean DT_good = true; // Признак наличия часов

volatile unsigned int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile byte Tb = 16; // Дополнительная задержка сигнала на начало байта в микросекундах
unsigned int TppSTD = 376; // Стандартная длительность задержки сигнала для файлов BAS/MON/ASM...

#define p 3 // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const byte BT_none = 0; // константы -- коды нажатой кнопки
const byte BT_right = 1;
const byte BT_up = 2;
const byte BT_down = 3;
const byte BT_left = 4;
const byte BT_select = 5;

byte MLevel = 0; // текущий пункт меню
const byte M_play = 0; // воспроизведение
const byte M_play_in = 10;
const byte M_dplay = 1; // воспроизведение в формате DOS
const byte M_dplay_in = 11;
const byte M_record = 2; // запись
const byte M_record_in = 12;
const byte M_setup = 3; // настройки
const byte M_setup_in = 13;

void setup() {
//Serial.begin(9600);
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF", 0); // вывод названия
printtext(vers, 1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!", 1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
DT_good = !(now.month() > 12); // если часы не работают, выставляем признак
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10, SPI_FULL_SPEED)) { // SD-карта готова?
printtext("SD-card failed!", 1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.", 1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1 << AIN1D) | (1 << AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
byte RCrom = 0; // для кода возврата из ПП
unsigned int StartAddr = 0x100; // стартовый адрес для MON
char iFName[13] = "input000.vkt"; // имя файла для записи

byte button = getPressedButton(); // какая кнопка нажата?
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > M_play) MLevel--;
else MLevel = M_setup;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < M_setup) MLevel++;
else MLevel = M_play;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
MLevel += 10; // заходим в подменю
switch (MLevel) // выводим надписи меню
{
case M_play_in: // воспроизведение
case M_dplay_in: // воспроизведение (DOS)
printplay(); // вывод надписи "Play file..."
getMaxFile();
break;
case M_record_in: // запись
printtext("Record to file:", 0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName, 1); // выводим имя файла для записи
break;
case M_setup_in: // настройки
printtext("Setup", 0);
break;
}
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->", 0);
printtime();
break;
case M_dplay: // воспроизведение (DOS)
printtext("Play to DOS ->", 0);
printtime();
break;
case M_record: // запись
printtext("Record data ->", 0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->", 0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
case M_dplay_in:
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if (currentFile < 1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if (currentFile > maxFile) {
currentFile = 1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = MLevel - 10; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...", 0);

byte PNT = 0;
for (byte i = 0; i <= 12; i++) { // Переводим все буквы имени файла в заглавные
if ((sfileName[i] >= 0x61) && (sfileName[i] <= 0x7A)) {
sfileName[i] &= 0xDF; // на заглавные буквы
}
if (sfileName[i] == '.') { // ищем точку в имени файла
PNT = i; // запоминаем позицию точки в неиспользуемой сейчас переменной, для этого не предназначенной
}
}

if (MLevel == M_dplay_in) { // если вывод в формате DOS
RCrom = PlayAll(PNT, 4, 0); // вызов ПП для формата DOS
}
else { // другие форматы
switch (sfileName[PNT + 1]) // следующий после точки символ
{
case 'C': // первый символ == 'c'|'C'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(PNT, 0, 0); // вызов ПП для формата CAS
break;
} // если не CAS, то далее проверяем на COM/C0M
case 'R': // первый символ == 'r'|'R'
if ( ((sfileName[PNT + 2] == 'O') | (sfileName[PNT + 2] == '0')) & (sfileName[PNT + 3] == 'M') ) { // второй символ == 'o'|'O'|'0' и третий == 'm'|'M'
if (sfileName[PNT + 2] != '0') { // проверка на вывод нулевого блока по расширению файла
RCrom = PlayROM(PNT, 0x01);// Передаём позицию точки в качестве параметра, вывод с первого блока
}
else {
RCrom = PlayROM(PNT, 0x00);// Передаём позицию точки в качестве параметра, вывод с нулевого блока
}
}
break;
case 'V': // первый символ == 'v'|'V'
if ( (sfileName[PNT + 2] == 'K') & (sfileName[PNT + 3] == 'T') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(); // вызов ПП для формата VKT
}
break;
case 'B': // первый символ == 'b'|'B'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(PNT, 1, 0); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'M': // первый символ == 'm'|'M' (или 'b'|'B')
//StartAddr = 0x100;
if ((sfileName[PNT + 2] >= '0') & (sfileName[PNT + 2] <= '9')) { // преобразуем второй символ расширения в HEX
StartAddr = (sfileName[PNT + 2] - '0') * 16;
}
else if ((sfileName[PNT + 2] >= 'A') & (sfileName[PNT + 2] <= 'F')) {
StartAddr = (sfileName[PNT + 2] - 55) * 16;
}
if ((sfileName[PNT + 3] >= '0') & (sfileName[PNT + 3] <= '9')) { // преобразуем третий символ расширения в HEX
StartAddr += sfileName[PNT + 3] - '0';
}
else if ((sfileName[PNT + 3] >= 'A') & (sfileName[PNT + 3] <= 'F')) {
StartAddr += (sfileName[PNT + 3] - 55);
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(PNT, 2, StartAddr); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'A': // первый символ == 'a'|'A'
if ( (sfileName[PNT + 2] == 'S') & (sfileName[PNT + 3] == 'M') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(PNT, 3, 0); // вызов ПП для формата ASM
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.", 0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped", 0); // Сообщение об остановке
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big", 0); // большой файл
break;
case 11:
printtext("Unknown format", 0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
case 12:
printtext("Bad speed mark", 0); // метка скорости в VKT не правильная (больше или меньше границы)
break;
default:
printtext("ERROR!", 0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printplay(); // вывод надписи "Play file..."
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...", 0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...", 0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1 << ACME); // включить мультиплексор
ADMUX = (1 << MUX0) // включить вход на A1
| (1 << REFS1) | (1 << REFS0); // опорное напряжение АЦП (?)

ACSR = (1 << ACIE) // Разрешить прерывание аналогового компаратора
| (1 << ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0) != HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...", 0); // сигнал зафиксирован, записываем
printtext("Bytes:", 1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
unsigned int CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB % 256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CWB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CWB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128
Pik = false; // сбрасываем флаг "есть сигнал"

Tpp = ((PPeriod_sred[0] + PPeriod_sred[1]) / 384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp % 8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp / 8) * 8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp / 8 + 1) * 8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT_good) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
dataFile.timestamp(T_WRITE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
}
printtext("Done. Speed:", 0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1] > (PPeriod_sred[0] + 3840)) | (PPeriod_sred[0] > (PPeriod_sred[1] + 3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("", 1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0] / 192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1] / 192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp < 160) | (Tpp > 512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.", 0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file", 0);
}
}
dataFile.close(); // закрываем файл
Tpp = 256; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file", 0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):", 1);
switch (button)
{
case BT_up: // вверх
if (Tpp < 400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp > 160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12, 1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
byte getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
//Serial.println(buttonValue);
if (buttonValue < 60) return BT_right;
else if (buttonValue < 180) return BT_up;
else if (buttonValue < 330) return BT_down;
else if (buttonValue < 530) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, byte l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0, l);
//byte WRB = lcd.print(text);
for (byte i = lcd.print(text); i < 16; i++) lcd.print(' '); // вывод текста и очистка строки до конца
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT_good) { // если часы работают
char DT[17];
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour() / 10 + '0'; // перевод из целого в символ
DT[1] = now.hour() % 10 + '0'; // часы
DT[2] = ':';
DT[3] = now.minute() / 10 + '0'; // минуты
DT[4] = now.minute() % 10 + '0'; // минуты
DT[5] = ':';
DT[6] = now.second() / 10 + '0'; // секунды
DT[7] = now.second() % 10 + '0'; // секунды
DT[8] = ' ';
DT[9] = now.day() / 10 + '0'; // день
DT[10] = now.day() % 10 + '0'; // день
DT[11] = '/';
DT[12] = now.month() / 10 + '0'; // месяц
DT[13] = now.month() % 10 + '0'; // месяц
DT[14] = ' ';
DT[15] = ' ';
DT[16] = 0x00;
lcd.print(DT); // выводим время и дату
}
else {
byte WRB = lcd.print(millis() / 1000); // выводим количество секунд с момента влючения ардуины вместо времени
for (byte i = WRB; i < 16; i++) lcd.print(' '); // очистка строки после текста
}
}

void printplay() {
if (MLevel != M_dplay_in) { // если вывод в не формате DOS
printtext("Play file:", 0);
}
else {
printtext("Play file (DOS):", 0);
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile = 0;
while (dataFile.openNext(dataFile.cwd(), O_READ)) {
dataFile.close();
maxFile++;
}
currentFile = 1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for (int X = 1; X < currentFile; X++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(), O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(), O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize() <= 0xFFFE) { // проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (!isDir) { // это не директория?
printtext(sfileName, 1); // вывод имени текущего файла
if (Nbt < 0xFFFF) { // если размер файла в допустимых пределах
lcd.setCursor(16, 1);
byte WRB;
if (Nbt >= 1000) { // если размер больше 1000 байт
WRB = lcd.print(Nbt / 1024);// подсчёт числа символов
lcd.setCursor(15 - WRB, 1);
lcd.print(Nbt / 1024); // вывод размера файла в кБ
lcd.print('k');
}
else {
WRB = lcd.print(Nbt); // подсчёт числа символов
lcd.setCursor(16 - WRB, 1);
lcd.print(Nbt); // вывод размера файла в байтах
}
}
}
else {
for (byte i = 0; i < 14; i++) { // ищем конец имени файла
if (sfileName[i] == 0x00) {
sfileName[i] = '>'; // если это директория, добавляем в конце символ '>'
sfileName[i + 1] = 0x00;
printtext(sfileName, 1); // вывод имени текущей директории
sfileName[i] = 0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void SwapTpp() // поменять местами значения переменных Tpp и TppSTD
{
Tpp = Tpp + TppSTD; // меняем местами значения Tpp и TppSTD
TppSTD = Tpp - TppSTD;
Tpp = Tpp - TppSTD;
CalcTb();
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
unsigned int CRB_tmp;
do { // Ждём опустошения буфера
delay(Tpp / 64); // задержка на вывод 1 байта (~Tpp*16/1000)
BUFF[lowByte(CWB + 1)] = 0x00; // обнуляем следующий за последним байт -- иногда и он успевает прочитаться...
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_tmp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

byte PlayVKT() // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt - 3); // на позицию -3 байт от конца файла
if (dataFile.read() != 0xFF) return 11; // метка не найдена
Tpp = dataFile.read() * 256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160) | (Tpp > 511)) return 12; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt - 4; // уменьшаем размер данных на метку (4) и предварительно считанные данные (251)

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 250; CWB++) { // первые 251 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt >> 8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

byte RCV = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (RCV == 0) WaitBuffer(); // ожидаем окончания вывода
return RCV; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayAll(byte pnt, byte FType, unsigned int StartAddr) // функция вывода остальных типов файлов
// входные данные: длина имени, тип файла, начальный адрес. Тип файла это:
// 0=CAS и VKT; 1=BAS(C); 2=MON и BAS(B); 3=ASM; 4=DOS
// Возвращаемые RC:
// = 0 -- всё ок
// = 1 -- прерывание по кнопке
// = 2 -- чтение обогнало запись в буфер
{
int i;
byte rcp;
// byte SB; // выводимый байт
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 255; CWB++) { // первые 256 байт
if ((FType != 3) | ((CWB / 64 == 1) | (CWB / 64 == 3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

SwapTpp(); // выставляем стандартную скорость вывода
// Tpp = 376; // стандартная скорость вывода
// Tb = 0;
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
rcp = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (rcp == 0) WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
case 1: // это BAS(C)
for (i = 0; i <= 3; i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i = 0; i <= 3; i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i = 0; i <= 3; i++) ToBUFF(0xE6); // 4x 0xE6
break;
case 4: // это DOS
ToBUFF(0x01);
ToBUFF(0x00); // 0x0100 -- начальный адрес (всегда)
ToBUFF(highByte(Nbt - 1) + 1); // старший байт конечного адреса (нач.адрес+длинна данных)
ToBUFF(0xFF); // младший байт конечного адреса (всегда)
rcp = PlayFile(true); // выводим байты из файла с подсчётом КС
if (rcp > 0) {
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
for (i = lowByte(Nbt - 1); i < 0xFF; i++) ToBUFF(0x00); // дополняем, если надо, нулями
ToBUFF(lowByte(CSF)); // выводим младший байт КС
}

byte SB; // выводимый байт
for (i = 0; i < pnt; i++) { // вывод имени файла без расширения, DOS поддерживаются только 'A'-'Z','0'-'9'
if (sfileName[i] != 0x7E) {// не тильда
SB = sfileName[i]; // берём символ символ
}
else { // тильда -- часто встречабщийся символ, остальные можно переименовать вручную
if (FType != 4) {
SB = '_'; // меняем тильду на подчёркивание
}
else SB = '0'; // для DOS меняем тильду на нолик, иначе при загрузке будет ошибка
}
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву имени
}

// это DOS
if (FType == 4) {
for (i = pnt; i < 8; i++) { // дополняем имя пробелами до 8 байт
ToBUFF(0x20);
CSF += 0x20;
}
// выводим расширение файла
if ( (sfileName[pnt + 1] == 'R') & (sfileName[pnt + 2] == 'O') & (sfileName[pnt + 3] == 'M') ) {
SB = 'C'; // меняем ROM на СОМ
}
else SB = sfileName[pnt + 1];
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер первую букву расширения

for (i = 2; i < 4; i++) {
SB = sfileName[pnt + i];
CSF += SB; // считаем КС
ToBUFF(SB); // заносим в буфер очередную букву расширения
}
ToBUFF(lowByte(CSF)); // выводим младший байт КС (данные + имя файла)
WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return 0; // выходим с кодом 0
}

for (i = 0; i <= 2; i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType == 1) {
for (i = 0; i <= 767; i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i = 0; i <= 2; i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// это MON, BAS(B) или ASM
for (i = 0; i <= 255; i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType == 2) { // это MON или BAS(B)
ToBUFF(lowByte(StartAddr)); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddr + (Nbt - 1) / 256)); // старший байт адреса конца записи
ToBUFF(lowByte(Nbt - 1)); // младший байт адреса конца записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// остался только ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(0xFF); // дополняем FFh в конце
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayFile(boolean CSFp) // Функция вывода битов файла
// CSFp признак необходимости подсчёта контрольной суммы файла
{
byte SB;
unsigned int CWB_tmp;
CSF = 0; // обнуляем контрольную сумму
for (unsigned int i = 0; i < Nbt; i++) { // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (CSFp) CSF += SB;

if ((Nbt - i) % 256 == 0) { // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt - i) >> 8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CWB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB_tmp > CWB) { // проверка -- не обогнало ли воспроизведение запись в буфер?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
return 0;
}

byte PlayROM(int pnt, byte BLs) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt / 256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;
unsigned int CRB_tmp;

if (Nbt % 256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

// Заголовок блока
byte SB[25];
SB[0] = 0x4E;
SB[1] = 0x4F;
SB[2] = 0x44;
SB[3] = 0x49;
SB[4] = 0x53;
SB[5] = 0x43;
SB[6] = 0x30;
SB[7] = 0x30; // NODISK00
SB[22] = 0x52;
SB[23] = 0x4F;
SB[24] = 0x4D; // расширение "ROM"

for (i = 0; i <= 7; i++) { // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if (sfileName[i] != 0x7E) { // не тильда
SB[i + 14] = sfileName[i]; // заносим символ
}
else {
SB[i + 14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i + 14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate) / 10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate) % 10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate) / 10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate) % 10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate) % 100) / 10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate) % 10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i = 0; i <= 3; i++) { // преамбула (4*(00H*25+55H*25))
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt = BLe; BLt >= 1; BLt--) { // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j = 0; j <= 15; j++) ToBUFF(0x00); // 00h*16
for (j = 0; j <= 3; j++) ToBUFF(0x55); // 55h*4
ToBUFF(0xE6); // E6h*1
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4

for (j = 0; j <= 24; j++) { // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(0x00);
ToBUFF(0x00);
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst = 0x80; Nst <= 0x87; Nst++) { // вывод строк (8 шт.)
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j = 0; j <= 31; j++) { // цикл на 32 байта
if (Nbt > 0) { // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CRB_tmp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j = 0; j <= 31; j++) ToBUFF(0x00); // 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb) { // Подпрограмма записи байта в буфер
unsigned int CRB_tmp;
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_tmp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd = PORTD;
if (bBit & 1) { // проверка индекса полубитов на чётность
if (( (Pd >> p) ^ ( BUFF[lowByte(CRB)] >> (bBit >> 1) )) & 1) { // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else { // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else {
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp + Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP] >> 7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP] * CWB + PPeriod * 192) / (CWB + 1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A << 1) + B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP] / 97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}Архив со скетчем и библиотеками: 67242

- - - Добавлено - - -

P.S. Заметил интересную особенность моего Вектора: если загружать данные "на холодную", т.е. после долгого его нахождения в выключеном состоянии, то на скоростях загрузки быстрее 272-280 мкс идут сплошные ошибки, а после "прогрева" хотя бы минут 10-15, скорость воспроизведения можно легко повысить до 184 мкс... Интересно, что может так тормозить в холодном состоянии?

KTSerg
10.12.2018, 10:43
...
P.S. Заметил интересную особенность моего Вектора: если загружать данные "на холодную", т.е. после долгого его нахождения в выключеном состоянии, то на скоростях загрузки быстрее 272-280 мкс идут сплошные ошибки, а после "прогрева" хотя бы минут 10-15, скорость воспроизведения можно легко повысить до 184 мкс... Интересно, что может так тормозить в холодном состоянии?
Точно определить можно только "методом тыка".
Охлаждаешь системку, и локально нагреваешь паяльником или феном. Начинать с компаратора и его обвязки, и далее по цепочке.
У меня напарник, при таком ремонте, в морозилку системку засовывал, вычислял полудохлые (плывущие) ОЗУшки.

svofski
10.12.2018, 11:23
У меня напарник, при таком ремонте, в морозилку системку засовывал, вычислял полудохлые (плывущие) ОЗУшки.
Тут в ветке неподалеку, одна из про ПК-11/16, программатор в морозилке является стандартной частью техпроцесса ;)

Improver
10.12.2018, 12:18
Охлаждаешь системку, и локально нагреваешь паяльником или феном. Начинать с компаратора и его обвязки, и далее по цепочке.Да, наверно попробую так сделать... :)

Improver
07.08.2019, 14:05
Готова новая версия "магнитофона":
Для работы требуются библиотеки TimerOne, SdFat, RTClib, а также стандартные LiquidCrystal и Wire.
#define vers "version 11.6"
/* Оптимизация в IDE 1.8.7 *

Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
MOSI - D11
MISO - D12
CLK - D13
CS - D10

Часы реального времени (RTC DS1307) подключены:
SDA - A4
SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
LCD RS pin - D8
LCD Enable pin - D9
LCD D4 pin - D4
LCD D5 pin - D5
LCD D6 pin - D6
LCD D7 pin - D7
LCD R/W pin - GND
LCD 5V pin - 5V

Кнопки - A0
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
#include <Wire.h>
#include "RTClib.h"

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[14]; // короткое имя текущего файла/директории
int currentFile = 1; // текущая позиция в директории
int maxFile = 0; // всего позиций в директории (файлов и поддиректорий)
boolean isDir = false; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

unsigned int Nbt; // размер файла, байт
unsigned int CSF; // контрольная сумма

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

boolean DT_good = true; // Признак наличия часов

volatile unsigned int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile byte Tb = 16; // Дополнительная задержка сигнала на начало байта в микросекундах
unsigned int TppSTD = 376; // Стандартная длительность задержки сигнала для файлов BAS/MON/ASM...

#define p 3 // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const byte BT_none = 0; // константы -- коды нажатой кнопки
const byte BT_right = 1;
const byte BT_up = 2;
const byte BT_down = 3;
const byte BT_left = 4;
const byte BT_select = 5;

byte MLevel = 0; // текущий пункт меню
const byte M_play = 0; // воспроизведение
const byte M_play_in = 10;
const byte M_dplay = 1; // воспроизведение в формате DOS
const byte M_dplay_in = 11;
const byte M_record = 2; // запись
const byte M_record_in = 12;
const byte M_setup = 3; // настройки
const byte M_setup_in = 13;

void setup() {
//Serial.begin(9600);
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для яркости экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtext("BEKTOP-MF", 0); // вывод названия
printtext(vers, 1); // и версии
delay(2000); // ждем 2 с для солидности :-)

Wire.begin(); // запуск часов
RTC.begin();
if (! RTC.isrunning()) {
printtext("RTC is NOT run!", 1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
DT_good = !(now.month() > 12); // если часы не работают, выставляем признак
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10, SPI_FULL_SPEED)) { // SD-карта готова?
printtext("SD-card failed!", 1);
delay(3000); // ждем 3 с
}
printtext("SD-card is OK.", 1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1 << AIN1D) | (1 << AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
byte RCrom = 0; // для кода возврата из ПП
unsigned int StartAddr = 0x100; // стартовый адрес для MON
char iFName[13] = "input000.vkt"; // имя файла для записи

byte button = getPressedButton(); // какая кнопка нажата?
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > M_play) MLevel--;
else MLevel = M_setup;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < M_setup) MLevel++;
else MLevel = M_play;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
MLevel += 10; // заходим в подменю
switch (MLevel) // выводим надписи меню
{
case M_play_in: // воспроизведение
case M_dplay_in: // воспроизведение (DOS)
printplay(); // вывод надписи "Play file..."
getMaxFile();
break;
case M_record_in: // запись
printtext("Record to file:", 0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
printtext(iFName, 1); // выводим имя файла для записи
break;
case M_setup_in: // настройки
printtext("Setup", 0);
break;
}
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtext("Play file ->", 0);
printtime();
break;
case M_dplay: // воспроизведение (DOS)
printtext("Play to DOS ->", 0);
printtime();
break;
case M_record: // запись
printtext("Record data ->", 0);
printtime();
break;
case M_setup: // настройки
printtext("Settings ->", 0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
case M_dplay_in:
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if (currentFile < 1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if (currentFile > maxFile) {
currentFile = 1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = MLevel - 10; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtext("Playing...", 0);

byte PNT = 0;
for (byte i = 0; i <= 12; i++) { // Переводим все буквы имени файла в заглавные
if ((sfileName[i] >= 0x61) && (sfileName[i] <= 0x7A)) {
sfileName[i] &= 0xDF; // на заглавные буквы
}
if (sfileName[i] == '.') { // ищем точку в имени файла
PNT = i; // запоминаем позицию точки
}
}

if (MLevel == M_dplay_in) { // если вывод в формате DOS
RCrom = PlayAll(4, 0); // вызов ПП для формата DOS
}
else { // другие форматы
switch (sfileName[PNT + 1]) // следующий после точки символ
{
case 'C': // первый символ == 'c'|'C'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(0, 0); // вызов ПП для формата CAS
break;
} // если не CAS, то далее проверяем на COM/C0M
case 'R': // первый символ == 'r'|'R'
if ( ((sfileName[PNT + 2] == 'O') | (sfileName[PNT + 2] == '0')) & (sfileName[PNT + 3] == 'M') ) { // второй символ == 'o'|'O'|'0' и третий == 'm'|'M'
if (sfileName[PNT + 2] != '0') { // проверка на вывод нулевого блока по расширению файла
RCrom = PlayROM(PNT, 0x01);// Передаём позицию точки в качестве параметра, вывод с первого блока
}
else {
RCrom = PlayROM(PNT, 0x00);// Передаём позицию точки в качестве параметра, вывод с нулевого блока
}
}
break;
case 'V': // первый символ == 'v'|'V'
if ( (sfileName[PNT + 2] == 'K') & (sfileName[PNT + 3] == 'T') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(); // вызов ПП для формата VKT
}
break;
case 'B': // первый символ == 'b'|'B'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(1, 0); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'M': // первый символ == 'm'|'M' (или 'b'|'B')
//StartAddr = 0x100;
if ((sfileName[PNT + 2] >= '0') & (sfileName[PNT + 2] <= '9')) { // преобразуем второй символ расширения в HEX
StartAddr = (sfileName[PNT + 2] - '0') * 16;
}
else if ((sfileName[PNT + 2] >= 'A') & (sfileName[PNT + 2] <= 'F')) {
StartAddr = (sfileName[PNT + 2] - 55) * 16;
}
if ((sfileName[PNT + 3] >= '0') & (sfileName[PNT + 3] <= '9')) { // преобразуем третий символ расширения в HEX
StartAddr += sfileName[PNT + 3] - '0';
}
else if ((sfileName[PNT + 3] >= 'A') & (sfileName[PNT + 3] <= 'F')) {
StartAddr += (sfileName[PNT + 3] - 55);
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(2, StartAddr); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'A': // первый символ == 'a'|'A'
if ( (sfileName[PNT + 2] == 'S') & (sfileName[PNT + 3] == 'M') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(3, 0); // вызов ПП для формата ASM
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtext("Done.", 0); // Всё закончилось успешно.
break;
case 1:
printtext("Stopped", 0); // Сообщение об остановке
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtext("File is too big", 0); // большой файл
break;
case 11:
printtext("Unknown format", 0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
case 12:
printtext("Bad speed mark", 0); // метка скорости в VKT не правильная (больше или меньше границы)
break;
default:
printtext("ERROR!", 0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printplay(); // вывод надписи "Play file..."
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtext("Prepare...", 0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtext("Waiting...", 0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1 << ACME); // включить мультиплексор
ADMUX = (1 << MUX0) // включить вход на A1
| (1 << REFS1) | (1 << REFS0); // опорное напряжение АЦП (?)

ACSR = (1 << ACIE) // Разрешить прерывание аналогового компаратора
| (1 << ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0) != HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtext("Recording...", 0); // сигнал зафиксирован, записываем
printtext("Bytes:", 1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
unsigned int CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB % 256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CWB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CWB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128
Pik = false; // сбрасываем флаг "есть сигнал"

Tpp = ((PPeriod_sred[0] + PPeriod_sred[1]) / 384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp % 8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp / 8) * 8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp / 8 + 1) * 8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT_good) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
dataFile.timestamp(T_WRITE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
}
printtext("Done. Speed:", 0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1] > (PPeriod_sred[0] + 3840)) | (PPeriod_sred[0] > (PPeriod_sred[1] + 3840))) { // если средние значения слишьком сильно отличаются
lcd.print("BadSig"); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
printtext("", 1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0] / 192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1] / 192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт

delay(3000); // ждём 3 сек.
}
else if ((Tpp < 160) | (Tpp > 512)) { // если скорость вне допустимых пределов
lcd.print("Error?"); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtext("Canceled.", 0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtext("Error del.file", 0);
}
}
dataFile.close(); // закрываем файл
Tpp = 256; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtext("Error open file", 0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtext("Period(mks):", 1);
switch (button)
{
case BT_up: // вверх
if (Tpp < 400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp > 160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12, 1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
byte getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
//Serial.println(buttonValue);
if (buttonValue < 60) return BT_right;
else if (buttonValue < 180) return BT_up;
else if (buttonValue < 330) return BT_down;
else if (buttonValue < 530) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void printtext(char* text, byte l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0, l);
//byte WRB = lcd.print(text);
for (byte i = lcd.print(text); i < 16; i++) lcd.print(' '); // вывод текста и очистка строки до конца
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT_good) { // если часы работают
char DT[17];
DateTime now = RTC.now(); // получаем текущее время
DT[0] = now.hour() / 10 + '0'; // перевод из целого в символ
DT[1] = now.hour() % 10 + '0'; // часы
DT[2] = ':';
DT[3] = now.minute() / 10 + '0'; // минуты
DT[4] = now.minute() % 10 + '0'; // минуты
DT[5] = ':';
DT[6] = now.second() / 10 + '0'; // секунды
DT[7] = now.second() % 10 + '0'; // секунды
DT[8] = ' ';
DT[9] = now.day() / 10 + '0'; // день
DT[10] = now.day() % 10 + '0'; // день
DT[11] = '/';
DT[12] = now.month() / 10 + '0'; // месяц
DT[13] = now.month() % 10 + '0'; // месяц
DT[14] = ' ';
DT[15] = ' ';
DT[16] = 0x00;
lcd.print(DT); // выводим время и дату
}
else {
byte WRB = lcd.print(millis() / 1000); // выводим количество секунд с момента влючения ардуины вместо времени
for (byte i = WRB; i < 16; i++) lcd.print(' '); // очистка строки после текста
}
}

void printplay() {
if (MLevel != M_dplay_in) { // если вывод в не формате DOS
printtext("Play file:", 0);
}
else {
printtext("Play file (DOS):", 0);
}
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile = 0;
while (dataFile.openNext(dataFile.cwd(), O_READ)) {
dataFile.close();
maxFile++;
}
currentFile = 1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for (int X = 1; X < currentFile; X++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(), O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(), O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize() <= 0xFFFE) { // проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл
if (!isDir) { // это не директория?
printtext(sfileName, 1); // вывод имени текущего файла
if (Nbt < 0xFFFF) { // если размер файла в допустимых пределах
lcd.setCursor(16, 1);
byte WRB;
if (Nbt >= 1000) { // если размер больше 1000 байт
WRB = lcd.print(Nbt / 1024);// подсчёт числа символов
lcd.setCursor(15 - WRB, 1);
lcd.print(Nbt / 1024); // вывод размера файла в кБ
lcd.print('k');
}
else {
WRB = lcd.print(Nbt); // подсчёт числа символов
lcd.setCursor(16 - WRB, 1);
lcd.print(Nbt); // вывод размера файла в байтах
}
}
}
else {
for (byte i = 0; i < 14; i++) { // ищем конец имени файла
if (sfileName[i] == 0x00) {
sfileName[i] = '>'; // если это директория, добавляем в конце символ '>'
sfileName[i + 1] = 0x00;
printtext(sfileName, 1); // вывод имени текущей директории
sfileName[i] = 0x00; // и удаляем его...
break;
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void SwapTpp() // поменять местами значения переменных Tpp и TppSTD
{
Tpp = Tpp + TppSTD; // меняем местами значения Tpp и TppSTD
TppSTD = Tpp - TppSTD;
Tpp = Tpp - TppSTD;
CalcTb();
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
unsigned int CRB_tmp;
do { // Ждём опустошения буфера
delay(Tpp / 64); // задержка на вывод 1 байта (~Tpp*16/1000)
BUFF[lowByte(CWB + 1)] = 0x00; // обнуляем следующий за последним байт -- иногда и он успевает прочитаться...
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_tmp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

byte PlayVKT() // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt - 3); // на позицию -3 байт от конца файла
if (dataFile.read() != 0xFF) return 11; // метка не найдена
Tpp = dataFile.read() * 256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160) | (Tpp > 511)) return 12; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt - 4; // уменьшаем размер данных на метку (4) и предварительно считанные данные (251)

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 250; CWB++) { // первые 251 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt >> 8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

byte RCV = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (RCV == 0) WaitBuffer(); // ожидаем окончания вывода
return RCV; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayAll(byte FType, unsigned int StartAddr) // функция вывода остальных типов файлов
// входные данные: тип файла, начальный адрес. Тип файла это:
// 0=CAS и VKT; 1=BAS(C); 2=MON и BAS(B); 3=ASM; 4=DOS
// Возвращаемые RC:
// = 0 -- всё ок
// = 1 -- прерывание по кнопке
// = 2 -- чтение обогнало запись в буфер
{
int i;
byte rcp;
byte pnta;

char sName[11]; // выводимое имя файла
for (i = 0; i < 9; i++) { // формируем выводимое имя файла, цикл до точки (макс 9 символ)
if (sfileName[i] != '.') {
if (sfileName[i] != 0x7E) { // не тильда
sName[i] = sfileName[i]; // берём символ символ
}
else { // тильда -- часто встречающийся символ, остальные нужно переименовать вручную
if (FType != 4) {
sName[i] = '_'; // меняем тильду на подчёркивание
}
else sName[i] = '0'; // для DOS меняем тильду на нолик, иначе при загрузке будет ошибка, DOS поддерживаются только 'A'-'Z','0'-'9'
}
}
else { // если точка
pnta = i; // запоминаем длинну имени
break; // выход из цикла
}
}

if (FType == 4) { // если DOS, то дополняем пробелы и расширение файла
for (i = pnta; i < 8; i++) { // пробелы...
sName[i] = ' ';
}
// расширение файла...
if ( (sfileName[pnta + 1] == 'R') & (sfileName[pnta + 2] == 'O') & (sfileName[pnta + 3] == 'M') ) {
sName[8] = 'C'; // меняем ROM на СОМ
}
else sName[8] = sfileName[pnta + 1];
// и последние два символа расширения
sName[9] = sfileName[pnta + 2];
sName[10] = sfileName[pnta + 3];
pnta = 11; // выставляем полную длинну имени файла
}
//sName[i] = 0x00; // -- для отладки
//printtext(sName, 1);// -- для отладки

delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 255; CWB++) { // первые 256 байт
if ((FType != 3) | ((CWB / 64 == 1) | (CWB / 64 == 3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

SwapTpp(); // выставляем стандартную скорость вывода
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
rcp = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (rcp == 0) WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
case 1: // это BAS(C)
for (i = 0; i <= 3; i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i = 0; i <= 3; i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i = 0; i <= 3; i++) ToBUFF(0xE6); // 4x 0xE6
break;
case 4: // это DOS
ToBUFF(0x01);
ToBUFF(0x00); // 0x0100 -- начальный адрес (всегда)
ToBUFF(highByte(Nbt - 1) + 1); // старший байт конечного адреса (нач.адрес+длинна данных)
ToBUFF(0xFF); // младший байт конечного адреса (всегда)
rcp = PlayFile(true); // выводим байты из файла с подсчётом КС
if (rcp > 0) {
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
for (i = lowByte(Nbt - 1); i < 0xFF; i++) ToBUFF(0x00); // дополняем, если надо, нулями
ToBUFF(lowByte(CSF)); // выводим младший байт КС
}

for (i = 0; i < pnta; i++) { // вывод имени файла
CSF += sName[i]; // считаем КС
ToBUFF(sName[i]); // заносим в буфер очередную букву имени
}

// это DOS
if (FType == 4) {
ToBUFF(lowByte(CSF)); // выводим младший байт КС (данные + имя файла)
WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return 0; // выходим с кодом 0
}

for (i = 0; i <= 2; i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType == 1) {
for (i = 0; i <= 767; i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i = 0; i <= 2; i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// это MON, BAS(B) или ASM
for (i = 0; i <= 255; i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType == 2) { // это MON или BAS(B)
ToBUFF(lowByte(StartAddr)); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddr + (Nbt - 1) / 256)); // старший байт адреса конца записи
ToBUFF(lowByte(Nbt - 1)); // младший байт адреса конца записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// остался только ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(0xFF); // дополняем FFh в конце
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayFile(boolean CSFp) // Функция вывода битов файла
// CSFp признак необходимости подсчёта контрольной суммы файла
{
byte SB;
unsigned int CWB_tmp;
CSF = 0; // обнуляем контрольную сумму
for (unsigned int i = 0; i < Nbt; i++) { // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (CSFp) CSF += SB;

if ((Nbt - i) % 256 == 0) { // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt - i) >> 8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CWB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB_tmp > CWB) { // проверка -- не обогнало ли воспроизведение запись в буфер?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
SwapTpp(); // восстанавливаем Tpp
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
return 0;
}

byte PlayROM(int pnt, byte BLs) // функция вывода файла ROM
{
delay(1000); // ждем 1 с

if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt / 256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;
unsigned int CRB_tmp;

if (Nbt % 256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

// Заголовок блока
byte SB[25];
SB[0] = 0x4E;
SB[1] = 0x4F;
SB[2] = 0x44;
SB[3] = 0x49;
SB[4] = 0x53;
SB[5] = 0x43;
SB[6] = 0x30;
SB[7] = 0x30; // NODISK00
SB[22] = 0x52;
SB[23] = 0x4F;
SB[24] = 0x4D; // расширение "ROM"

for (i = 0; i <= 7; i++) { // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if (sfileName[i] != 0x7E) { // не тильда
SB[i + 14] = sfileName[i]; // заносим символ
}
else {
SB[i + 14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i + 14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate) / 10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate) % 10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate) / 10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate) % 10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate) % 100) / 10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate) % 10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i = 0; i <= 3; i++) { // преамбула (4*(00H*25+55H*25))
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt = BLe; BLt >= 1; BLt--) { // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j = 0; j <= 15; j++) ToBUFF(0x00); // 00h*16
for (j = 0; j <= 3; j++) ToBUFF(0x55); // 55h*4
ToBUFF(0xE6); // E6h*1
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4

for (j = 0; j <= 24; j++) { // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(0x00);
ToBUFF(0x00);
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst = 0x80; Nst <= 0x87; Nst++) { // вывод строк (8 шт.)
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j = 0; j <= 31; j++) { // цикл на 32 байта
if (Nbt > 0) { // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CRB_tmp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j = 0; j <= 31; j++) ToBUFF(0x00); // 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb) { // Подпрограмма записи байта в буфер
unsigned int CRB_tmp;
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_tmp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd = PORTD;
if (bBit & 1) { // проверка индекса полубитов на чётность
if (( (Pd >> p) ^ ( BUFF[lowByte(CRB)] >> (bBit >> 1) )) & 1) { // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else { // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else {
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp + Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP] >> 7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP] * CWB + PPeriod * 192) / (CWB + 1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A << 1) + B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP] / 97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}

Изменения по отношению к предыдущей:
- заметил иногда проявляющийся глюк при выводе файлов в формате DOS: при коротких именах иногда на выходе получается нечто вроде "ABC.COM .COM" и МДОС ругается на неверное имя, по какой причине это происходит -- не понятно... Для исправления глюка переписал процедуру формирования имени в PlayAll, вроде глюк больше не повторяется.

Архив со скетчем и библиотеками: 69760

KTSerg
02.10.2019, 19:23
Изучая stm32, переношу на него девайс с интерфейсами загрузки программ в Вектор.
Запустил модуль ROM-Player, для загрузки через магнитофонный вход.
Константы для таймера, со старого LPC2146, также подошли. 112 - полупериод, и добавка 60 после каждого байта. Программа 24КБ загрузилась без нареканий за 65 сек.

Improver
22.07.2020, 14:08
Новая версия скетча:
Для работы требуется доустановить в ArduinoIDE библиотеки TimerOne (от Jesse Tane...), SdFat (от Bill Greiman), RTClib (от Adafruit, библиотека TinyWireM не обязательна), скетчем также используются стандартные LiquidCrystal и Wire.
#define vers "version 11.8"
/* Оптимизация в IDE 1.8.7+ *

Вариант на "Data Logger Shield" и "LCD Keypad Shield"
(на первом есть RTC -- используем для показа времени)

Выход - D3
Вход - A1

"Data Logger Shield V1.0":
SD-картридер подключен к выводам ардуино:
MOSI - D11
MISO - D12
CLK - D13
CS - D10

Часы реального времени (RTC DS1307) подключены:
SDA - A4
SDL - A5

"LCD Keypad Shield":
Подключение экрана 1602А:
LCD RS pin - D8
LCD Enable pin - D9
LCD D4 pin - D4
LCD D5 pin - D5
LCD D6 pin - D6
LCD D7 pin - D7
LCD R/W pin - GND
LCD 5V pin - 5V

Кнопки - A0

Дополнительно к стандартным требуются библиотеки:
- RTClib (от Adafruit, библиотека TinyWireM не обязательна)
- SdFat (от Bill Greiman)
- TimerOne (от Jesse Tane...)
*/

// Подгружаемые библиотеки:
#include <LiquidCrystal.h>
#include <SdFat.h>
#include <TimerOne.h>
//#include <Wire.h> // уже включено в <RTClib.h>
#include <RTClib.h>

//инициализация часов
RTC_DS1307 RTC;
// инициализация экрана
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

SdFat sd;
SdFile dataFile;

char sfileName[13]; // короткое имя текущего файла/директории
unsigned int currentFile = 1; // текущая позиция в директории
unsigned int maxFile = 0; // всего позиций в директории (файлов и поддиректорий)
boolean isDir = false; // признак того, что текущая позиция -- это директория
boolean isRoot = true; // признак того, что мы в корне

unsigned int Nbt; // размер файла, байт
unsigned int CSF; // контрольная сумма

volatile byte BUFF[256]; // буфер данных
volatile unsigned int CRB = 0; // индекс чтения из буфера
volatile byte bBit = 15; // индекс читаемого полубита
volatile unsigned int CWB = 0; // индекс записи в буфер
volatile byte A = 0; // записываемый байт
volatile byte B = 0; // записываемый бит

volatile unsigned long PPeriod_sred[2]; // среднее значение границы длинны нечётного полупериода
volatile unsigned long iMicros_old = 0; // предыдущее значение микросекунд
volatile boolean Pik = false; // есть изменение сигнала
volatile byte PP = 1; // =0 -- чётный, =1 -- нечётный полупериод

boolean DT_good = true; // Признак наличия часов

volatile unsigned int Tpp = 256; // Начальная длительность задержки сигнала в микросекундах (один полупериод)
volatile byte Tb = 16; // Дополнительная задержка сигнала на начало байта в микросекундах
unsigned int TppSTD = 376; // Стандартная длительность задержки сигнала для файлов BAS/MON/ASM...

#define p 3 // номер пина, на который будет вывод сигнала
#define InputPIN A1 // номер пина, на котором будет получение сигнала

const byte BT_none = 0; // константы -- коды нажатой кнопки
const byte BT_right = 1;
const byte BT_up = 2;
const byte BT_down = 3;
const byte BT_left = 4;
const byte BT_select = 5;

byte MLevel = 0; // текущий пункт меню
const byte M_play = 0; // воспроизведение
const byte M_play_in = 10;
const byte M_dplay = 1; // воспроизведение в формате DOS
const byte M_dplay_in = 11;
const byte M_record = 2; // запись
const byte M_record_in = 12;
const byte M_setup = 3; // настройки
const byte M_setup_in = 13;

byte PlayROM(int pnt, byte BLs = 0x01); // функция вывода файла ROM, для значения по умолчанию "с первого блока"
byte PlayAll(byte FType, byte StartAddrB = 0); // объявление функции вывода остальных типов файлов, для значения по умолчанию

void setup() {
//Serial.begin(9600);
pinMode(InputPIN, INPUT); // объявляем пин как вход (сигнал)
pinMode(p, OUTPUT); // объявляем пин как выход (сигнал)
digitalWrite(p, LOW); // выход =0
pinMode(10, OUTPUT); // CS для SD-картридера и оно же для подсветки экрана
digitalWrite(10, HIGH); // включаем подсветку экрана
pinMode(A0, INPUT); // объявляем пин с кнопками как вход

lcd.begin(16, 2); // объявляем размер экрана 16 символов и 2 строки

Timer1.initialize(Tpp); // инициализировать timer1, и установить период Tpp мкс.
Timer1.attachInterrupt(SendHalfBit); // прикрепить SendHalfBit(), как обработчик прерывания по переполнению таймера
Timer1.stop(); // остановить таймер

printtextF(F("BEKTOP-MF"), 0); // вывод названия
printtextF(F(vers), 1); // и версии
delay(2000); // ждем 2с для солидности :-)

// Wire.begin(); // уже включено в <RTClib.h>
RTC.begin(); // запуск часов
if (! RTC.isrunning()) {
//printtextF(F("RTC is NOT run!"), 1); // часы стоят
RTC.adjust(DateTime(__DATE__, __TIME__)); // устанавливаем дату/время на момент копмиляции программы
delay(2000); // ждем 2 с
DateTime now = RTC.now(); // проверяем работу часов
DT_good = !(now.month() > 12); // если часы не работают, выставляем признак
}
//RTC.adjust(DateTime(__DATE__, __TIME__)); //-- это если понадобится принудительно обновить время

while (!sd.begin(10, SPI_FULL_SPEED)) { // SD-карта готова?
printtextF(F("SD-card failed!"), 1);
delay(3000); // ждем 3 с
}
//printtextF(F("SD-card is OK."), 1);
sd.chdir(); // устанавливаем корневую директорию SD

DIDR1 = (1 << AIN1D) | (1 << AIN0D); // Выключить цифровые входы контактов Digital 6 и 7
}

void loop() {
byte RCrom = 0; // для кода возврата из ПП
unsigned int StartAddr = 0x100; // стартовый адрес для MON
char iFName[13] = "input000.vkt"; // имя файла для записи

byte button = getPressedButton(); // какая кнопка нажата?
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
switch (button) // проверяем, что было нажато
{
case BT_up: // вверх
if (MLevel < 10) {
if (MLevel > M_play) MLevel--;
else MLevel = M_setup;
}
break;
case BT_down: // вниз
if (MLevel < 10) {
if (MLevel < M_setup) MLevel++;
else MLevel = M_play;
}
break;
case BT_right: // вправо -- вход в меню, запуск действия и т.д.
if (MLevel < 10) {
MLevel += 10; // заходим в подменю
switch (MLevel) // выводим надписи меню
{
case M_play_in: // воспроизведение
case M_dplay_in: // воспроизведение (DOS)
printplay(); // вывод надписи "Play file..."
getMaxFile();
break;
case M_record_in: // запись
printtextF(F("Record to file:"), 0);
sd.chdir(); // устанавливаем корневую директорию SD
while (sd.exists(iFName)) { // ищем первый отсутствующий файл по имени, увеличивая счётчик
if (iFName[7] < '9') { // увеличиваем единицы
iFName[7]++;
}
else {
iFName[7] = '0';
if (iFName[6] < '9') { // увеличиваем десятки
iFName[6]++;
}
else {
iFName[6] = '0'; // увеличиваем сотни
iFName[5]++;
}
}
}
lcd.setCursor(0, 1);
clrstr(lcd.print(iFName)); // выводим имя файла для записи с очисткой строки до конца
break;
case M_setup_in: // настройки
printtextF(F("Setup"), 0);
break;
}
button = BT_none;
}
break;
case BT_left: // влево
break;
case BT_select: // Возврат в корень меню
MLevel = 0;
break;
case BT_none: // ничего не нажато
delay(100);
break;
}

switch (MLevel) // действия в соответствии с текущим пунктом меню
{
case M_play: // воспроизведение
printtextF(F("Play file ->"), 0);
printtime();
break;
case M_dplay: // воспроизведение (DOS)
printtextF(F("Play to DOS ->"), 0);
printtime();
break;
case M_record: // запись
printtextF(F("Record data ->"), 0);
printtime();
break;
case M_setup: // настройки
printtextF(F("Settings ->"), 0);
printtime();
break;
case M_play_in: // зашли в меню вопроизведения
case M_dplay_in:
switch (button)
{
case BT_up: // вверх по файлам
currentFile--;
if (currentFile < 1) {
currentFile = maxFile;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_down: // вниз по файлам
currentFile++;
if (currentFile > maxFile) {
currentFile = 1;
}
seekFile(); // показать имя текущего файла/директории
break;
case BT_left: // в корень или выход
if (isRoot) {
MLevel = MLevel - 10; // из корня выходим в стартовое меню
}
else { // возврат в корневую директорию, ремарка ниже:
//SDFat has no easy way to move up a directory, so returning to root is the easiest way.
//each directory (except the root) must have a file called ROOT (no extension)
sd.chdir(true);
isRoot = true; // помечаем, что мы в корне
getMaxFile();
}
break;
case BT_right: // вход в директорию, или запуск файла на воспроизведение
if (isDir) { //Если это директория, то переход в неё
sd.chdir(sfileName, true);
isRoot = false;
getMaxFile();
}
else { // если не директория -- пробуем воспроизвести файл
if (Nbt != 0xFFFF) { // проверяем размер файла
RCrom = 11; // для вывода сообщения "неверный формат"
printtextF(F("Playing..."), 0);

byte PNT = 0;
for (byte i = 0; i <= 12; i++) { // Переводим все буквы имени файла в заглавные
if ((sfileName[i] >= 0x61) && (sfileName[i] <= 0x7A)) {
sfileName[i] &= 0xDF; // на заглавные буквы
} else if (sfileName[i] == '.') { // ищем точку в имени файла
PNT = i; // запоминаем позицию точки
}
}

if (MLevel == M_dplay_in) { // если вывод в формате DOS
RCrom = PlayAll(4); // вызов ПП для формата DOS
}
else { // другие форматы
StartAddr = chr2hex(sfileName[PNT + 3]);
if (StartAddr > 0x0F) StartAddr++; // если символ определён не верно, то увеличиваем до 0х100
StartAddr += chr2hex(sfileName[PNT + 2]) * 16; // дополняем вторым символом
switch (sfileName[PNT + 1]) // следующий после точки символ
{
case 'C': // первый символ == 'c'|'C'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(0); // вызов ПП для формата CAS
break;
} // если не CAS, то далее проверяем на COM/C0M
case 'R': // первый символ == 'r'|'R'
if ( sfileName[PNT + 3] == 'M' ) { // третий символ == 'm'|'M'
if (sfileName[PNT + 2] == 'O') { // второй символ == 'o'|'O'
StartAddr = 0x01; // вывод с первого блока
}
else if (sfileName[PNT + 2] == '0') { // второй символ == '0'
StartAddr = 0x00; // вывод с нулевого блока
}
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayROM(PNT, lowByte(StartAddr)); // вызов ПП для формата ROM с нужного блока
}
break;
case 'V': // первый символ == 'v'|'V'
if ( (sfileName[PNT + 2] == 'K') & (sfileName[PNT + 3] == 'T') ) { // второй символ == 'k'|'K' и третий == 't'|'T'
RCrom = PlayVKT(); // вызов ПП для формата VKT
}
break;
case 'B': // первый символ == 'b'|'B'
if ( (sfileName[PNT + 2] == 'A') & (sfileName[PNT + 3] == 'S') ) { // второй символ == 'a'|'A' и третий == 's'|'S'
RCrom = PlayAll(1); // вызов ПП для формата BAS(C)
break; // выход из case, если это был BAS-файл
}
case 'M': // первый символ == 'm'|'M' (или 'b'|'B')
if ( (sfileName[PNT + 2] == 'O') & (sfileName[PNT + 3] == 'N') ) { // второй символ == 'o'|'O' и третий == 'n'|'N'
StartAddr = 0x01; // для расширения MON ставим адрес начала 0x0100
}
if (StartAddr < 0x100) { // если стартовый адрес из расширения определён верно
RCrom = PlayAll(2, lowByte(StartAddr)); // вызов ПП для формата MON или Бейсика BLOAD
}
break;
case 'A': // первый символ == 'a'|'A'
if ( (sfileName[PNT + 2] == 'S') & (sfileName[PNT + 3] == 'M') ) { // второй символ == 's'|'S' и третий == 'm'|'M'
RCrom = PlayAll(3); // вызов ПП для формата ASM
}
break;
}
}
}
else {
RCrom = 10; // для вывода сообщения "большой файл"
}

digitalWrite(p, LOW); // выход = 0
switch (RCrom) // Проверяем код возврата
{
case 0:
printtextF(F("Done."), 0); // Всё закончилось успешно.
break;
case 1:
printtextF(F("Stopped"), 0); // Сообщение об остановке
while (getPressedButton() != BT_none) { // Ждём, пока не будет отпущена кнопка
delay(50);
}
break;
case 10:
printtextF(F("File is too big"), 0); // большой файл
break;
case 11:
printtextF(F("Unknown format"), 0); // выбран не ROM/R0M/VKT-файл, либо не найдена метка скорости в VKT
break;
case 12:
printtextF(F("Bad speed mark"), 0); // метка скорости в VKT не правильная (больше или меньше границы)
break;
default:
printtextF(F("ERROR!"), 0); // Сообщение об ошибке
}
delay(1000); // ждем 1 с
printplay(); // вывод надписи "Play file..."
seekFile(); // показать имя текущего файла
}
}
break;
case M_record_in: // зашли в меню записи
if (button == BT_right) { // нажали кнопку вправо?
if (dataFile.open(iFName, FILE_WRITE)) { // открываем файл на запись
printtextF(F("Prepare..."), 0); // готовимся...
dataFile.write(0xFF); // пишем байт для создания файла
dataFile.seekSet(0); // переход на начало файла
CRB = 0; // Сбрасываем индекс
printtextF(F("Waiting..."), 0); // ожидание сигнала

ADCSRA = 0; // выключить АЦП
ADCSRB |= (1 << ACME); // включить мультиплексор
ADMUX = (1 << MUX0) // включить вход на A1
| (1 << REFS1) | (1 << REFS0); // опорное напряжение АЦП (?)

ACSR = (1 << ACIE) // Разрешить прерывание аналогового компаратора
| (1 << ACBG); // внутреннее опорное напряжение
delay(1); // ждём немного
Pik = false; // сбрасываем первое срабатывание
while (!Pik) { // Ждём сигнал
delay(10); // задержка...
if (digitalRead(A0) != HIGH) { // кнопка нажата?
break; // прерывание режима записи при нажатии кнопки
}
}
printtextF(F("Recording..."), 0); // сигнал зафиксирован, записываем
printtextF(F("Bytes:"), 1);
delay(200); // задержка для накопления инфы...
noInterrupts(); // запрет прерываний
unsigned int CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний

//====================================
do { // пока есть данные в буфере
dataFile.write(BUFF[lowByte(CRB)]); // пишем байт из буфера
if (CRB % 256 == 0) { // каждый 256-й байт (1 псевдоблок)
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
}
CRB++;
if (CWB_temp <= CRB) {// если индекс записи меньше или равен индексу чтения
delay(300); // задержка на ввод данных: макс.скорость 160 * 16 полубит * ~117 байт или ~46 байт на минимальной
noInterrupts(); // запрет прерываний
CWB_temp = CWB; // сохраняем CWB во временную переменную
interrupts(); // разрешение прерываний
}
}
while (CWB_temp >= CRB); // если запись на SD обогнала чтение, значит сигнала нет, выходим из цикла
//=====================================
ACSR = 0; // отключаем обработку прерывания компаратора
ADCSRA = 135; // включить АЦП, установить делитель =128
Pik = false; // сбрасываем флаг "есть сигнал"

Tpp = ((PPeriod_sred[0] + PPeriod_sred[1]) / 384); // расчитываем полупериод для последующего вывода... (((S1+S2)/2)/128)*2/3
if ((Tpp % 8) < 4) { // если остаток от деления на 8 меньше 4
Tpp = (Tpp / 8) * 8; // округляем в меньшую сторону
}
else {
Tpp = (Tpp / 8 + 1) * 8; // округляем в большую сторону
}

if (CRB > 25) { // проверка -- если было ложное срабатывание, то ничего не пишем.
dataFile.write(0xFF); // записываем маркер в файл
dataFile.write(highByte(Tpp)); // сохраняем скорость в файле
dataFile.write(lowByte(Tpp));
if (DT_good) { // если часы работают...
DateTime now = RTC.now(); // получаем текущее время и сохраняем в атрибутах файла
dataFile.timestamp(T_CREATE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
dataFile.timestamp(T_WRITE, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
}
printtextF(F("Done. Speed:"), 0);
lcd.setCursor(7, 1); // устанавливаем курсор в позицию 8 в строке 1
lcd.print(CRB); // количество сохранённых байт
lcd.setCursor(13, 0); // устанавливаем курсор в позицию 13 в строке 0
lcd.print(Tpp); // расчётное значение длинны полупериода для вывода
lcd.setCursor(0, 0); // устанавливаем курсор в позицию 0 в строке 0
if ((PPeriod_sred[1] > (PPeriod_sred[0] + 3840)) | (PPeriod_sred[0] > (PPeriod_sred[1] + 3840))) { // если средние значения слишьком сильно отличаются
lcd.print(F("BadSig")); // пишем сообщение о плохом сигнале

// отладочный вывод инфы о полупериодах
/*
printtextF(F(""), 1);
lcd.setCursor(0, 1);
lcd.print(PPeriod_sred[0] / 192);
lcd.setCursor(5, 1);
lcd.print(PPeriod_sred[1] / 192);
lcd.setCursor(10, 1);
lcd.print(CRB); // количество сохранённых байт
*/
delay(3000); // ждём 3 сек.
}
else if ((Tpp < 160) | (Tpp > 512)) { // если скорость вне допустимых пределов
lcd.print(F("Error?")); // пишем сообщение о возможной ошибке
delay(3000); // ждём 3 сек.
}
}
else {
printtextF(F("Canceled."), 0);
if (!dataFile.remove()) { // удаляем недозаписанный файл
delay(1000);
printtextF(F("Error del.file"), 0);
}
}
dataFile.close(); // закрываем файл
Tpp = 256; // устанавливаем полупериод на "стандартное" значение
}
else { // что-то не открылся файл...
printtextF(F("Error open file"), 0);
}

delay(1000);
MLevel = M_record; // переход в корневое меню на пункт записи
}
else {
if (button == BT_left) MLevel = M_record; // нажали кнопку влево? -- переход в корневое меню
}
break;
case M_setup_in: // зашли в меню настроек
printtextF(F("Period(mks):"), 1);
switch (button)
{
case BT_up: // вверх
if (Tpp < 400) Tpp += 8; // увеличиваем скорость
break;
case BT_down: // вниз
if (Tpp > 160) Tpp = Tpp - 8; // уменьшаем скорость
break;
case BT_left: // влево
MLevel = M_setup; // выход в коневое меню на пункт "настройки"
}
lcd.setCursor(12, 1);
lcd.print(Tpp); // пришем текущее значение скорости
break;
}
}

//================================================== ===============
byte chr2hex(byte Ch1) { // символ (байт) в шестнадцатиричное значение
if ((Ch1 >= '0') & (Ch1 <= '9')) { // преобразуем символ в HEX
return (Ch1 - '0');
}
else if ((Ch1 >= 'A') & (Ch1 <= 'F')) {
return (Ch1 - 55);
}
return 0xFF;
}

byte getPressedButton() // функция проверки нажатой кнопки
{
int buttonValue = analogRead(0);
//Serial.println(buttonValue);
if (buttonValue < 60) return BT_right;
else if (buttonValue < 180) return BT_up;
else if (buttonValue < 330) return BT_down;
else if (buttonValue < 530) return BT_left;
else if (buttonValue < 800) return BT_select;
return BT_none;
}

void clrstr(byte l) { // Очистка строки с текущей позиции до конца экрана
for (byte i = l; i < 16; i++) lcd.print(' ');
}

void printtextF(__FlashStringHelper* text, byte l) { // Вывод текста на экран в строке l с очисткой строки
lcd.setCursor(0, l);
clrstr(lcd.print(text)); // вывод текста и очистка строки до конца
}

void printtime() { // вывод времени и даты
lcd.setCursor(0, 1); // устанавливаем курсор в позицию 0 в строке 1
if (DT_good) { // если часы работают
char DT[15]; // = "00:00:00 00/00";
DateTime now = RTC.now(); // получаем и выводим текущее время и дату
//sprintf(DT, "%02d:%02d:02d %-2d/%02d", now.hour(), now.minute(), now.second(), now.day(), now.month());
DT[0] = now.hour() / 10 + '0'; // перевод из целого в символ
DT[1] = now.hour() % 10 + '0'; // часы
DT[2] = ':';
DT[3] = now.minute() / 10 + '0'; // минуты
DT[4] = now.minute() % 10 + '0'; // минуты
DT[5] = ':';
DT[6] = now.second() / 10 + '0'; // секунды
DT[7] = now.second() % 10 + '0'; // секунды
DT[8] = ' ';
DT[9] = now.day() / 10 + '0'; // день
DT[10] = now.day() % 10 + '0'; // день
DT[11] = '/';
DT[12] = now.month() / 10 + '0'; // месяц
DT[13] = now.month() % 10 + '0'; // месяц
DT[14] = 0x00;
clrstr(lcd.print(DT)); // выводим время и дату и очистка строки после текста
}
else {
clrstr(lcd.print(millis() / 1000)); // выводим количество секунд с момента влючения ардуины вместо времени
}
}

void printplay() {
lcd.setCursor(0, 0);
lcd.print(F("Play file"));
if (MLevel == M_dplay_in) { // если вывод в формате DOS
lcd.print(F(" (DOS)"));
}
clrstr(lcd.print(':')); // вывод двоеточия с очисткой строки после текста
}

void getMaxFile() { // считаем файлы и директории в текущей директории и сохраняем в maxFile
dataFile.cwd()->rewind();
maxFile = 0;
while (dataFile.openNext(dataFile.cwd(), O_READ)) {
dataFile.close();
maxFile++;
}
currentFile = 1; // устанавливаем позицию на первый файл
seekFile(); // и переходим туда
}

void seekFile() { // переход на позицию в директории, сохранение имени файла и показ его на экране
dataFile.cwd()->rewind();
for (int X = 1; X < currentFile; X++) { // читаем первые файлы до currentFile
dataFile.openNext(dataFile.cwd(), O_READ);
dataFile.close();
}
dataFile.openNext(dataFile.cwd(), O_READ); // читаем данные текущего файла
dataFile.getSFN(sfileName); // сохраняем короткое имя файла
isDir = dataFile.isDir(); // признак директории
if (dataFile.fileSize() <= 0xFFFE) { // проверка размера файла <=65534 или для VKT без служебной информации будет <=44458
Nbt = dataFile.fileSize(); // размер файла ОК
}
else {
Nbt = 0xFFFF; // слишком большой для загрузки
}
dataFile.close(); // закрываем файл

lcd.setCursor(0, 1);
lcd.print(sfileName);
if (isDir) {
clrstr(lcd.print('>')); // если это директория, добавляем в конце символ '>' и очищаем строку
} else { // это не директория
clrstr(lcd.print(' ')); // очистка строки после текста
if (Nbt < 0xFFFF) { // если размер файла в допустимых пределах
//lcd.setCursor(16, 1);
if (Nbt >= 1000) { // если размер больше 999 байт
//WRB = lcd.print(Nbt / 1024);// подсчёт числа символов
lcd.setCursor(15 - lcd.print(Nbt / 1024), 1);
lcd.print(Nbt / 1024); // вывод размера файла в кБ
lcd.print('k');
}
else {
//WRB = lcd.print(Nbt); // подсчёт числа символов
lcd.setCursor(16 - lcd.print(Nbt), 1);
lcd.print(Nbt); // вывод размера файла в байтах
}
}
}
}

void CalcTb() // Вычисление значения задержки на начало байта Tb
{
if (Tpp <= 176) { // для полупериода меньше или равном 176
Tb = 88;
}
else {
if (Tpp <= 240) { // для полупериода от 184 до 240
Tb = 264 - Tpp;
}
else {
if (Tpp <= 264) { // для полупериода от 248 до 264
Tb = 16;
}
else Tb = 0; // для полупериода больше 264
}
}
}

void SwapTpp() // поменять местами значения переменных Tpp и TppSTD
{
Tpp = Tpp + TppSTD; // меняем местами значения Tpp и TppSTD
TppSTD = Tpp - TppSTD;
Tpp = Tpp - TppSTD;
CalcTb();
}

void WaitBuffer() // ожидание очистки буфера при воспроизведении
{
unsigned int CRB_tmp;
do { // Ждём опустошения буфера
delay(Tpp / 64); // задержка на вывод 1 байта (~Tpp*16/1000)
BUFF[lowByte(CWB + 1)] = 0x00; // обнуляем следующий за последним байт -- иногда и он успевает прочитаться...
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
}
while (CRB_tmp < CWB);
Timer1.stop(); // Останавливаем таймер............
}

byte PlayVKT() // функция вывода файла VKT
{
delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
dataFile.seekSet(Nbt - 3); // на позицию -3 байт от конца файла
if (dataFile.read() != 0xFF) return 11; // метка не найдена
Tpp = dataFile.read() * 256 + dataFile.read(); // считываем Tpp
if ((Tpp < 160) | (Tpp > 511)) return 12; // не правильная метка
dataFile.seekSet(0); // на начало файла
Nbt = Nbt - 4; // уменьшаем размер данных на метку (4) и предварительно считанные данные (251)

CRB = 0; // Сбрасываем индексы.
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 250; CWB++) { // первые 251 байт
BUFF[CWB] = dataFile.read();
}
CWB = 251; // продолжаем с 251-го байта буфера

lcd.setCursor(12, 0); // выводим на экран общее кол-во "псевдоблоков" по 256 байт
lcd.print(Nbt >> 8); // всего данных / 256

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

byte RCV = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (RCV == 0) WaitBuffer(); // ожидаем окончания вывода
return RCV; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayAll(byte FType, byte StartAddrB) // функция вывода остальных типов файлов
// входные данные: тип файла, начальный адрес. Тип файла это:
// 0=CAS и VKT; 1=BAS(C); 2=MON и BAS(B); 3=ASM; 4=DOS
// Возвращаемые RC:
// = 0 -- всё ок
// = 1 -- прерывание по кнопке
// = 2 -- чтение обогнало запись в буфер
{
int i;
byte rcp;
byte pnta;
byte EOFd = 0x1A; // символ для дополнения файлов ДОС

char sName[11]; // выводимое имя файла
for (i = 0; i < 9; i++) { // формируем выводимое имя файла, цикл до точки (макс 9 символ)
if (sfileName[i] != '.') {
if (sfileName[i] != 0x7E) { // не тильда
sName[i] = sfileName[i]; // берём символ имени файла
}
else { // тильда -- часто встречающийся символ, остальные нужно переименовать вручную на SD
if (FType != 4) {
sName[i] = '_'; // меняем тильду на подчёркивание
}
else sName[i] = '0'; // для DOS меняем тильду на нолик, иначе при загрузке будет ошибка, DOS поддерживаются только 'A'-'Z','0'-'9'
}
}
else { // если точка
pnta = i; // запоминаем длинну имени
break; // выход из цикла
}
}

if (FType == 4) { // если DOS, то дополняем пробелы и расширение файла
for (i = pnta; i < 8; i++) { // пробелы...
sName[i] = ' ';
}
// расширение файла...
if ( ((sfileName[pnta + 1] == 'R') | (sfileName[pnta + 1] == 'C')) & (sfileName[pnta + 2] == 'O') & (sfileName[pnta + 3] == 'M') ) {
sName[8] = 'C'; // меняем ROM на СОМ
EOFd = 0;
}
else sName[8] = sfileName[pnta + 1];
// и последние два символа расширения
sName[9] = sfileName[pnta + 2];
sName[10] = sfileName[pnta + 3];
pnta = 11; // выставляем полную длинну имени файла
}

delay(1000); // ждем 1 с
if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
CRB = 0; // Сбрасываем индексы.
bBit = 15;

// Начинаем наполнять буфер
for (CWB = 0; CWB <= 255; CWB++) { // первые 256 байт
if ((FType != 3) | ((CWB / 64 == 1) | (CWB / 64 == 3))) { // если не ASM и это 64-127 или 192-255 байты
BUFF[CWB] = 0;
}
else { // иначе, если ASM и это 0-63 или 128-191 байты
BUFF[CWB] = 0x55;
}
}
CWB = 256; // продолжаем с 256-го байта буфера

SwapTpp(); // выставляем стандартную скорость вывода
Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

delay(10); // немного ждём, чтобы начался вывод первых байт
ToBUFF(0xE6); // синхробайт
switch (FType)
{
case 0: // это CAS
rcp = PlayFile(false); // выводим данные из файла без подсчёта контрольной суммы
if (rcp == 0) WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
case 1: // это BAS(C)
for (i = 0; i <= 3; i++) ToBUFF(0xD3); // 4x 0xD3
break;
case 2: // это MON или BAS(B)
for (i = 0; i <= 3; i++) ToBUFF(0xD2); // 4x 0xD2
break;
case 3: // это ASM
for (i = 0; i <= 3; i++) ToBUFF(0xE6); // 4x 0xE6
break;
case 4: // это DOS
ToBUFF(0x01);
ToBUFF(0x00); // 0x0100 -- начальный адрес (всегда)
ToBUFF(highByte(Nbt - 1) + 1); // старший байт конечного адреса (нач.адрес+длинна данных)
ToBUFF(0xFF); // младший байт конечного адреса (всегда)
rcp = PlayFile(true); // выводим байты из файла с подсчётом КС
if (rcp > 0) {
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
for (i = lowByte(Nbt - 1); i < 0xFF; i++) {
ToBUFF(EOFd); // дополняем, если надо, символами конца файла
CSF += EOFd; // подсчёт КС
}
ToBUFF(lowByte(CSF)); // выводим младший байт КС
}

for (i = 0; i < pnta; i++) { // вывод имени файла
CSF += sName[i]; // считаем КС
ToBUFF(sName[i]); // заносим в буфер очередную букву имени
}

// это DOS
if (FType == 4) {
ToBUFF(lowByte(CSF)); // выводим младший байт КС (данные + имя файла)
WaitBuffer(); // ожидаем окончания вывода
SwapTpp(); // восстанавливаем Tpp
return 0; // выходим с кодом 0
}

for (i = 0; i <= 2; i++) ToBUFF(0x00); // 3x 0x00

// это BAS(C)
if (FType == 1) {
for (i = 0; i <= 767; i++) ToBUFF(0x55); // 768x 0x55
ToBUFF(0xE6); // синхробайт
for (i = 0; i <= 2; i++) ToBUFF(0xD3); // 3x 0xD3
ToBUFF(0x00); // 1x 0x00
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
//for (i = 0; i <= 2; i++) ToBUFF(0x00); // 3x 0x00 -- должно быть в конце файлов BAS
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// это MON, BAS(B) или ASM
for (i = 0; i <= 255; i++) ToBUFF(0x00); // 256x 0x00
ToBUFF(0xE6); // синхробайт

if (FType == 2) { // это MON или BAS(B)
ToBUFF(StartAddrB); // старший байт адреса начала записи
ToBUFF(0x00); // младший байт = 0
ToBUFF(lowByte(StartAddrB + highByte(Nbt - 1))); // старший байт адреса конца записи
ToBUFF(lowByte(Nbt - 1)); // младший байт адреса конца записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(lowByte(CSF)); // младший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}

// остался только ASM
ToBUFF(lowByte(Nbt)); // младший байт длинны записи
ToBUFF(highByte(Nbt)); // старший байт длинны записи
rcp = PlayFile(true); // выводим данные из файла с подсчётом КС
if (rcp == 0) {
ToBUFF(0xFF); // дополняем FFh в конце
ToBUFF(lowByte(CSF)); // младший байт КС
ToBUFF(highByte(CSF)); // старший байт КС
WaitBuffer(); // ожидаем окончания вывода
}
SwapTpp(); // восстанавливаем Tpp
return rcp; // выходим с кодом
}
return 3; // ошибка / нет файла
}

byte PlayFile(boolean CSFp) // Функция вывода битов файла
// CSFp признак необходимости подсчёта контрольной суммы файла
{
byte SB;
unsigned int CWB_tmp;
CSF = 0; // обнуляем контрольную сумму
for (unsigned int i = 0; i < Nbt; i++) { // данные из файла
SB = dataFile.read();
ToBUFF(SB);
if (CSFp) CSF += SB;

if ((Nbt - i) % 256 == 0) { // если остаток кратен 256
lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся "псевдоблоков" по 256 байт
lcd.print((Nbt - i) >> 8); // остаток данных / 256
lcd.print(' ');
}
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
//SwapTpp(); // восстанавливаем Tpp
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CWB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB_tmp > CWB) { // проверка -- не обогнало ли воспроизведение запись в буфер?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
//SwapTpp(); // восстанавливаем Tpp
return 2; // выход из ПП с ошибкой 2.
}
}
dataFile.close(); // закрываем файл
return 0;
}

byte PlayROM(int pnt, byte BLs) // функция вывода файла ROM
// pnt -- позиция точки в имени файла
// BLs -- стартовый блок
{
delay(1000); // ждем 1 с

if (dataFile.open(sfileName, O_READ)) { // открываем файл. Открылся?
byte BLe = Nbt / 256; // всего блоков
byte BLt; // осталось блоков
byte Nst; // номер строки
byte St; // выводимый байт

byte CSz = 0x00; // контрольная сумма заголовка
byte CSs = 0x00; // контрольная сумма строки
byte i;
byte j;
unsigned int CRB_tmp;

if (Nbt % 256 != 0) BLe++; // корректировка количества блоков, если размер файла не кратен 256

// Заголовок блока
byte SB[25];
SB[0] = 0x4E;
SB[1] = 0x4F;
SB[2] = 0x44;
SB[3] = 0x49;
SB[4] = 0x53;
SB[5] = 0x43;
SB[6] = 0x30;
SB[7] = 0x30; // NODISK00
SB[22] = 0x52;
SB[23] = 0x4F;
SB[24] = 0x4D; // расширение "ROM"

for (i = 0; i <= 7; i++) { // заносим в SB имя файла
if (i < pnt) { // имя ещё не закончилось?
if (sfileName[i] != 0x7E) { // не тильда
SB[i + 14] = sfileName[i]; // заносим символ
}
else {
SB[i + 14] = '_'; // меняем тильду на подчёркивание, иначе это будет русская "Ч"
}
}
else SB[i + 14] = 0x20; // дополняем пробелами
}

dir_t d;
dataFile.dirEntry(&d); // Считываем дату файла и сохраняем в заголовке
SB[8] = FAT_DAY(d.lastWriteDate) / 10 + '0'; // перевод из целого в символ -- день
SB[9] = FAT_DAY(d.lastWriteDate) % 10 + '0';
SB[10] = FAT_MONTH(d.lastWriteDate) / 10 + '0'; // месяц
SB[11] = FAT_MONTH(d.lastWriteDate) % 10 + '0';
SB[12] = (FAT_YEAR(d.lastWriteDate) % 100) / 10 + '0'; // последние две цифры года
SB[13] = FAT_YEAR(d.lastWriteDate) % 10 + '0';

CRB = 0; // Сбрасываем индексы.
CWB = 0;
bBit = 15;
CalcTb(); // Вычисляем значение задержки на начало байта Tb

// Начинаем наполнять буфер
for (i = 0; i <= 3; i++) { // преамбула (4*(00H*25+55H*25))
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x00;
CWB++;
}
for (j = 0; j <= 24; j++) {
BUFF[lowByte(CWB)] = 0x55;
CWB++;
}
}

Timer1.setPeriod(Tpp); // Выставляем период таймера
Timer1.start(); // Запускаем таймер............

for (BLt = BLe; BLt >= 1; BLt--) { // Вывод блоков данных в цикле
CSz = BLs;
CSz += BLe;
CSz += BLt;

for (j = 0; j <= 15; j++) ToBUFF(0x00); // 00h*16
for (j = 0; j <= 3; j++) ToBUFF(0x55); // 55h*4
ToBUFF(0xE6); // E6h*1
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4

for (j = 0; j <= 24; j++) { // заголовок блока
CSz += SB[j];
ToBUFF(SB[j]);
}
ToBUFF(0x00);
ToBUFF(0x00);
ToBUFF(BLs); // начальный блок
ToBUFF(BLe); // конечный блок
ToBUFF(BLt); // осталось блоков

lcd.setCursor(12, 0); // выводим на экран кол-во оставшихся блоков
lcd.print(BLt);
lcd.print(' ');

ToBUFF(CSz); // контр.сумма заголовка

for (Nst = 0x80; Nst <= 0x87; Nst++) { // вывод строк (8 шт.)
for (j = 0; j <= 3; j++) ToBUFF(0x00); // 00h*4
ToBUFF(0xE6); // E6h*1
CSs = Nst;
ToBUFF(Nst); // номер строки
CSs += CSz;
ToBUFF(CSz); // контр.сумма заголовка

// начинаем вывод строки данных
for (j = 0; j <= 31; j++) { // цикл на 32 байта
if (Nbt > 0) { // ещё есть данные?
St = dataFile.read(); // читаем очередной байт из файла
Nbt--;
}
else { // нет -- дополняем нулями
St = 0x00;
}
ToBUFF(St); // передаём считанный байт
CSs += St;
if (getPressedButton() != BT_none) { // кнопка нажата?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 1; // выход из ПП с ошибкой 1.
}
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CRB_tmp > CWB) { // проверка -- не обогнало ли чтение запись?
Timer1.stop(); // Останавливаем таймер
dataFile.close(); // закрываем файл
return 2; // выход из ПП с ошибкой 2.
}
}
ToBUFF(CSs); // контр.сумма строки
}
}
dataFile.close(); // закрываем файл

for (j = 0; j <= 31; j++) ToBUFF(0x00); // 00h*32 -- завершение вывода программы (?)
WaitBuffer(); // ожидаем окончания вывода
return 0; // выход из ПП с кодом 0.
}
return 3; // ошибка / нет файла
}

void ToBUFF(byte SBb) { // Подпрограмма записи байта в буфер
unsigned int CRB_tmp;
noInterrupts(); // запрет прерываний
CRB_tmp = CRB; // сохраняем CRB во временную переменную
interrupts(); // разрешение прерываний
if (CWB > (CRB_tmp + 255)) { // Если позиция записи больше, чем позиция чтения + размер буфера - 1
delay(Tpp); // Задержка (Tpp*1000 мкс = Tpp мс = 125 байт)
}
BUFF[lowByte(CWB)] = SBb;
CWB++;
}

void SendHalfBit() { // Подпрограмма вывода полубита по циклу таймера
byte Pd = PORTD;
if (bBit & 1) { // проверка индекса полубитов на чётность
if (( (Pd >> p) ^ ( BUFF[lowByte(CRB)] >> (bBit >> 1) )) & 1) { // Если состояние порта и выводимый бит разные
Pd ^= (1 << p); // инвертируем бит в позиции p
}
}
else { // чётный -- просто инвертируем порт
Pd ^= (1 << p); // инвертируем бит в позиции p(=3)
}
PORTD = Pd; // вывод в порт p
if (bBit > 0) { // правим счётчики полубитов и байтов
bBit--;
if (bBit == 14) Timer1.setPeriod(Tpp); // Выставляем период таймера (биты)
}
else {
bBit = 15;
CRB++;
Timer1.setPeriod(Tpp + Tb); // Выставляем увеличенный период таймера (начало байта)
}
}

ISR(ANALOG_COMP_vect) // ПП обработки прерывания компаратора -- определяет и заносит полученные биты в буфер
{
unsigned long iMicros = micros();
unsigned long PPeriod = iMicros - iMicros_old;
iMicros_old = iMicros;
if (PPeriod < 65000) { // началось...
if (bBit < 16) { // если это не последний полубит
if (PPeriod <= (PPeriod_sred[PP] >> 7)) { // sred/128, если тек. полупериод короткий
if (CWB <= 255) { // расчёт среднего значения, если меньше 256 байт считано
PPeriod_sred[PP] = (PPeriod_sred[PP] * CWB + PPeriod * 192) / (CWB + 1); // "среднее значение" = 1,5*128 от короткого полупериода
}
if (bBit & 1) { // нечётный полубит
A = (A << 1) + B; // заносим бит
} // нечётный -- пропускаем
}
else { // получен длинный полупериод
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
else { // если последний полубит, вводим корректировку на задержку между байтами
// граница будет =([1,5*Tpp*128]*25/19)/128 =~[1,5*Tpp*128]/97 =~1,98*Tpp
if (PPeriod > (PPeriod_sred[PP] / 97)) { // если тек. полупериод длинный
B ^= 1; // инвертируем бит
A = (A << 1) + B; // заносим бит
bBit--; // уменьшаем счётчик полубитов
}
}
// корректировка счётчиков
PP ^= 1; // инвертируем бит признака чётности полупериода
if (bBit > 1) {
bBit--; // счётчик полубитов -1
}
else {
BUFF[lowByte(CWB)] = A; // заносим байт в буфер
BUFF[lowByte(++CWB)] = 0;// счётчик байтов +1 и обнуляем очередной байт в буфере
A = 0; // обнуляем буфер для битов
bBit += 15; // = +16 -1
}
}
else { // был перерыв в сигнале
PPeriod_sred[0] = 98304; // берём заведомо большое среднее значение (192*512)
PPeriod_sred[1] = 98304; // берём заведомо большое среднее значение
CWB = 0; // начинаем запись с начала
bBit = 15;
A = 0;
B = 0;
PP = 1; // = нечётный полупериод
Pik = true; // есть сигнал!
}
}
Добавление вывода в формате FM9 пока нет, но есть немного улучшений:
- Данные в формате загрузчика теперь можно выводить с любого блока, не только с первого или нулевого. Для этого файл должен иметь расширение ".Rxx", где "хх" -- это номер блока в шестнадцатиричном формате (.ROM == .R01). Можно, например, грузить загрузчик FM9 с автозапуском, как тут (https://zx-pk.ru/threads/9532-vektor-06ts-sredstva-razrabotki.html?p=1070566&viewfull=1#post1070566), для этого нужно файлу загрузчика сделать расширение ".RDB".
- Выводимые данные в формате ДОС теперь до размера, кратного 256, дополняются символами конца файла 0x1A (кроме COM-файлов, где оставлено дополнение нулями).
- Заменил библиотеки TimerOne, SdFat и RTClib на те, что есть в "менеджере библиотек" в Arduino IDE, откомпилировал и протестил скетч в версии 1.8.13.
- Немного оптимизировал алгоритмы и улучшил использование памяти на ардуине.

Последняя версия скетча в архиве: 73121

Improver
05.04.2024, 10:14
Исправил небольшой баг, иногда приводящий к сбоям при воспроизведении, при компиляции выяснилось, что используемые библиотеки ушли далеко вперёд и проект даёт ошибки при компиляции, в связи с чем пришлось всё обновить, в результате:

С новой библиотекой SdFat стал возможным возврат на предыдущую директорию, а не только в корень, как ранее. Вернуться в предыдущую директорию можно по кнопке "стрелка влево".
Добавил сообщение "нет файлов" для пустых директорий на карте, убрал из показываемых скрытые файлы и директории на карте для облегчения навигации.
Воспроизведение в формате MDOS убрал из отдельного пункта меню, теперь при нажатии "стрелки вправо" воспроизводится файл в соответствии с его форматом, а при нажатии на кнопку "select" в формате MDOS.

Проект выложен на гитхаб (https://github.com/ImproverX/RW-player), если кому надо, вот архив со скетчем: 80614