SDLEP-READER, remplace tous les lecteurs de cassette (MO, TO et autres)

SDLEP-READER est un montage électronique à base d'Arduino. Il permet de remplacer les magnétophones TO et MO en lecture, en utilisant des fichiers "images de cassettes" stockés dans une carte microSD. La carte peut contenir jusqu'à 128 fichiers, sélectionnables par 7 interrupteurs à deux positions. Ce système fonctionne pour toutes les cassettes, protégées ou non, avec tous les ordinateurs de la gamme Thomson 8 bits. Il permet uniquement la lecture et pas l'enregistrement. Des essais ont été aussi réalisés avec succès avec d'autres ordinateurs utilisant des lecteurs de cassettes à sortie audio analogique ou à sortie TTL, ce qui fait de SDLEP-READER un outil universel de remplacement des lecteurs de cassettes des ordinateurs de collection.

Auteur : Daniel Coulom - Site internet : Emulateur DCMOTO - Dernière mise à jour le 17 juin 2017

previous Retour à la page Bricolage

SDLEP-READER a été utilisé avec succès pour charger des programmes avec les ordinateurs suivants :
- Thomson MO5, MO6, TO7/70, TO8
- Philips VG5000, VG8000, VG8010, VG8020
- Matra MC10, Alice, Alice 32, Alice 90
- Exelvision EXL100
- Amstrad 464, 6128
- Sinclair Spectrum 48K, 128K, +2A, +3
- Oric 1, Oric Atmos
- Vic20, C64, C128
- Acorn Electron
- Videopac C7420

D'autres tests sont en cours et seront annoncés ici prochainement.


sdlep-reader

Le problème des cassettes de programmes


Les lecteurs de cassettes n'ont jamais été des périphériques très fiables, et même avec du matériel neuf les erreurs d'entrées/sorties étaient fréquentes. Aujourd'hui les mécaniques sont usées, les courroies sont déformées, les supports magnétiques ont vieilli et il est encore plus difficile de lire une cassette sans erreur. Les premières générations d'ordinateurs Thomson utilisaient presque exclusivement ce support. La quantité de programmes et de jeux sur cassette est impressionnante et un jour viendra où elles seront toutes illisibles.

Le remplacement d'un magnétophone classique par un système électronique, sans pièce mécanique en mouvement et sans support magnétique, apporte une fiabilité absolue et permet de charger toute la logithèque Thomson sur cassette avec une grande facilité. C'est un périphérique simple, peu coûteux, peu encombrant et très pratique. En particulier la copie d'un fichier sur la carte SD à partir d'un PC est d'une facilité déconcertante par rapport à la création d'une cassette audio avec un magnétophone connecté à la carte son.

Malheureusement le temps de chargement reste identique à celui d'une cassette, et ce système ne permet pas de rivaliser avec la simulation de disquette sur carte SD. Néanmoins il garde un grand intérêt pour les programmes sur cassette qui n'existent pas en version disquette.

Description du système SDLEP-READER


sdlep-reader

Le coeur du système est un Arduino Pro-Mini, à base de processeur Atmega 328 ou 168. On lui adjoint un module permettant d'accéder à une carte mémoire microSD en mode SPI. Un bloc de 7 interrupteurs permet de sélectionner un fichier de la carte. Ce fichier est lu et interprété par l'Arduino, qui génère en sortie vers l'ordinateur Thomson les mêmes signaux qu'un lecteur de cassette Thomson. Le signal MOTOR, envoyé par l'ordinateur vers l'Arduino, permet de commencer la transmission ou de l'arrêter.

Une diode électro-luminescente donne des indications sur le fonctionnement du système. Elle clignote au rythme des données pendant la transmission, puis par brève impulsion toutes les deux secondes quand la fin de fichier est atteinte (fin de cassette). Au démarrage du système, si le fichier sélectionné par les interrupteurs n'est pas trouvé sur la carte, cette diode clignote très rapidement pour signaler l'anomalie.

L'alimentation du montage peut se faire de multiples façons :
- par la broche 5 du connecteur du crayon optique, pour les ordinateurs qui en sont équipés.
- par la sortie 12V des TO7 et TO7/70.
- par une alimentation 5V ou 7V à 12V.
- par une pile de 9V.

Le programme de l'Arduino

Le programme de l'Arduino (sketch ou croquis, selon la dénomination usuelle) utilise ses propres fonctions pour accéder le plus rapidement possible à la carte SD. Elles ont été en partie inspirées par la bibliothèque SimpleSDAudio.
De plus, l'accès à la carte est effectué par une commande de lecture directe des octets, pour éviter tout délai indésirable.

Le reste du programme est d'une très grande simplicité et les commentaires expliquent les quelques subtilités.

/**************************************************\
*                  S D - L E P                     * 
*           (c) 2017 - Daniel Coulom               *  
*           http://dcmoto.free.fr/                 *
*           http://forum.system-cfg.com/           *
*--------------------------------------------------*
* Ce code est distribue gratuitement dans l'espoir *
* qu'il sera utile, mais sans aucune  garantie  et *
* sans  engager  la  responsabilité  de  l'auteur. *
* Vous  pouvez  l' utiliser,  le  modifier  et  le *
* diffuser librement, en conservant cette  licence *
* et les références de l'auteur dans   toutes  les *
* copies. L'exploitation commerciale est interdite.*
\**************************************************/

/***************************************************
*                Version 2017.04.16                *
****************************************************
Historique
2017.04.16 optimisations des acces a la carte SD
2017.03.13 integration des acces a la carte SD
2017.01.17 bit data a 1 pendant l'arret moteur
2016.12.30 unite du delai definie en parametre
2016.12.29 noms de fichiers .lep sur 7 caracteres
2016.12.19 quelques optimisations mineures
2016.12.18 clignotement LED si fichier non trouve
2016.12.18 nom de fichier choisi par switches
2016.12.17 choix de broches different pour les I/O
2016.12.17 delai en dixiemes de ms dans sd.lep
2016.12.16 ajout LED d'activite
2016.12.16 reste de count/127 ecrit avant les zeros
2016.12.14 premiere version du programme

Emulation d'un lecteur de cassette Thomson TO ou MO
Lecture des donnees sur carte SD dans un fichier au
format .lep (image de cassette Thomson).

Connexions vers module Catalex pour carte micro SD
 GND --> GND
 D13 --> SCK
 D12 --> MISO
 D11 --> MOSI
 VCC --> VCC (5V)
 D10 --> CS 

Connexions vers DIN magnetophone
GND --> DIN 2
 D0 --> DIN 1  (MOTOR)
 D1 --> DIN 4  (READ DATA) 
   
Connexion vers DIN crayon optique
VCC --> DIN 5  (+5V)
   
Connexion LED d'activite
 D2 --> LED d'activite + resistance 1.5K
   
Connexion des interrupteurs de selection du fichier
 D3 --> interrupteur 1
 D4 --> interrupteur 2
 D5 --> interrupteur 3
 D6 --> interrupteur 4
 D7 --> interrupteur 5
 D8 --> interrupteur 6
 D9 --> interrupteur 7
 
*****************************************************************
SD card management is adapted from SimpleSDAudio library.
Visit SimpleSDAudio website for more information:
http://www.hackerspace-ffm.de/wiki/index.php?title=SimpleSDAudio
*****************************************************************
*/

// SD card constants
#define SD_CARD_TYPE_SD1        1 /** Standard capacity V1 SD card */
#define SD_CARD_TYPE_SD2        2 /** Standard capacity V2 SD card */
#define SD_CARD_TYPE_SDHC       3 /** High Capacity SD card */
#define SD_PARTTYPE_UNKNOWN     0
#define SD_PARTTYPE_SUPERFLOPPY 1
#define SD_PARTTYPE_FAT16       2
#define SD_PARTTYPE_FAT32       3
#define SD_INIT_TIMEOUT      2000 /** temps maxi pour l'initialisation */
#define SD_READ_TIMEOUT       300 /** temps maxi pour le debut de la lecture d'un bloc */
#define SD_COMMAND_TIMEOUT    300 /** temps maxi pour repondre a une commande */
#define SD_READY_STATE       0x00 /** status for card in the ready state */
#define SD_IDLE_STATE        0x01 /** status for card in the idle state */
#define SD_ILLEGAL_COMMAND   0x04 /** status bit for illegal command */
#define SD_DATA_START_BLOCK  0xFE /** start data token for read or write single block*/

// SD card commands
#define SD_CMD0   0x00  /** GO_IDLE_STATE - init card in spi mode if CS low */
#define SD_CMD8   0x08  /** SEND_IF_COND - verify SD Memory Card interface operating condition.*/
#define SD_CMD9   0x09  /** SEND_CSD - read the Card Specific Data (CSD register), response R1 */
#define SD_CMD10  0x0A  /** SEND_CID - read the card identification information (CID register), response R1 */
#define SD_CMD12  0x0C  /** STOP_TRANSMISSION - end multiple block read sequence, response R1b */
#define SD_CMD13  0x0D  /** SEND_STATUS - read the card status register, response R2 */
#define SD_CMD16  0x10  /** SET_BLOCKLEN arg0[31:0]: block length, response R1 */
#define SD_CMD17  0x11  /** READ_SINGLE_BLOCK - read a single data block from the card, response R1 */
#define SD_CMD18  0x12  /** READ_MULTIPLE_BLOCK - read a multiple data blocks from the card, response R1 */
#define SD_CMD55  0x37  /** APP_CMD - escape for application specific command */
#define SD_CMD58  0x3A  /** READ_OCR - read the OCR register of a card */
#define SD_CMD59  0x3B  /** CRC_ON_OFF - Turns CRC option on or off, response R1 */
#define SD_ACMD41 0x29  /** SD_SEND_OP_COMD - Sends host capacity support information and activates the card's initialization process */

// SD card error codes
#define SD_ERROR_CMD0           0x01 /** timeout error for command CMD0 (initialize card in SPI mode), signal problem */
#define SD_ERROR_CMD8           0x02 /** CMD8 was not accepted - not a valid SD card */
#define SD_ERROR_ACMD41         0x03 /** ACMD41 initialization process timeout */
#define SD_ERROR_CMD58          0x04 /** card returned an error response for CMD58 (read OCR) */
#define SD_ERROR_CMD16          0x05 /** card returned an error response for CMD16 (set block len) */
#define SD_ERROR_VOLTMATCH      0x06 /** card operation voltage range doesn't match (2.7V - 3.6V) */
#define SD_ERROR_READ_TIMEOUT   0x07 /** timeout while waiting for start of read data */
#define SD_ERROR_READ           0x08 /** card returned error token when tried to read data */
#define SD_ERROR_CMD17          0x09 /** card returned an error response for CMD17 (read single block) */
#define SD_ERROR_CMD9           0x0e /** card returned an error response for CMD9  (read CSD) */
#define SD_ERROR_CMD10          0x0f /** card returned an error response for CMD10 (read CID) */
#define SD_ERROR_CMD18          0x10 /** card returned an error response for CMD18 (read multi block) */
#define SD_ERROR_INVAL_SECT0    0x30 /** No valid MBR/FAT-BS signature found in sector 0 */
#define SD_ERROR_INVAL_BS       0x31 /** Malformed FAT boot sector */
#define SD_ERROR_FAT12          0x32 /** FAT12 is not supported */
#define SD_ERROR_FAT_NOT_INIT   0x33 /** FAT not initialized properly */
#define SD_ERROR_DIR_EOC        0x34 /** End of cluster reached (not a real error, just information) */
#define SD_ERROR_FILE_NOT_FOUND 0x35 /** File not found after reaching end of directory */
#define SD_ERROR_EOF            0x38 /** End of file reached */

// SD card variables
typedef struct {
  uint8_t     Attributes;
  uint32_t    Size;           // in bytes
  uint32_t    FirstCluster;   // First cluster
  uint32_t    ActSector;      // 0 to (SD_FAT.SecPerClus - 1)
  uint32_t    ActBytePos;     // 0 to Size
} SD_File_t;

SD_File_t LepFileInfo;  

typedef struct  {
  uint8_t     PartType;           // Use this to test whether it is FAT16 or FAT32 or not initialized
  // Stuff from FAT boot sector
  uint8_t     SecPerClus;
  uint16_t    RsvdSecCnt;
  uint8_t     NumFATs;
  uint16_t    RootEntryCount;
  uint32_t    TotalSec;
  uint32_t    SecPerFAT;
  uint32_t    RootClus;
  // For cluster calculations
  uint8_t     ClusterSizeShift;
  uint32_t    ClusterCount;
  // Start addresses (all in blocks / sector addresses)
  uint32_t    BootSectorStart;    // Address of boot sector from FAT
  uint32_t    FatStart;           // First file allocation table starts here
  uint32_t    RootDirStart;       // Root directory starts here
  uint32_t    DataStart;          // Cluster 0 starts here
  uint32_t    ClusterEndMarker;   // if Cluster >= this then end of file reached.
} SD_FAT_t;

SD_FAT_t SD_FAT;

uint8_t  SD_type;        // type de la carte SD
uint8_t  SD_CSPin;       // numero de la broche CS de la carte SD
uint8_t  SD_buffer[513]; // SD buffer must hold 512 bytes + 1

#define UNITE 50  //unite du delai en microsecondes


/**************************************************************************\
* Lecture du fichier .lep
\**************************************************************************/
void setup()
{
 int i;                      // compteur de boucle
 int delai;                  // delai en microsecondes 
 bool niveau;                // niveau du signal en sortie
 bool activite;              // niveau diode d'activite
 char octet;                 // zone de stockage d'un octet
 uint32_t count = 0;         // compteur d'octets
 char filename[13];          // nom du fichier .lep
 
 //Initialisations
 pinMode(0, INPUT_PULLUP);   // configurer pin0(RX) en entree (commande moteur)
 pinMode(1, OUTPUT);         // configurer pin1(TX) en sortie (donnees lues)
 pinMode(2, OUTPUT);         // configurer pin2 en sortie (led activite)
 pinMode(3, INPUT_PULLUP);   // interrupteur 1
 pinMode(4, INPUT_PULLUP);   // interrupteur 2
 pinMode(5, INPUT_PULLUP);   // interrupteur 3
 pinMode(6, INPUT_PULLUP);   // interrupteur 4
 pinMode(7, INPUT_PULLUP);   // interrupteur 5
 pinMode(8, INPUT_PULLUP);   // interrupteur 6
 pinMode(9, INPUT_PULLUP);   // interrupteur 7
 SD_CSPin = 10;              // definir CS de la carte SD en D10
 SD_Init_FileSystem();       // initialiser la carte SD
 SD_SpiSetHighSpeed();       // frequence d'horloge maxi 

 //Determination du nom du fichier (extension .lep)
 //en fonction de la position des 7 interrupteurs (128 possibilites)
 //Le nom du fichier est compose de sept chiffres 0 ou 1
 //decrivant la position des interrupteurs 1 à 7
 //Exemple : interrupteurs 2, 4, 6 sur ON --> fichier 0101010.lep 
 //Exemple : interrupteurs 5 sur ON --> fichier 0000100.lep
 //Exemple : interrupteurs 1,2,3,5,7 sur ON --> fichier 1110101.lep 
 strcpy(filename, "0000000.lep");
 if(digitalRead(3) == LOW) filename[0] = '1';
 if(digitalRead(4) == LOW) filename[1] = '1';
 if(digitalRead(5) == LOW) filename[2] = '1';
 if(digitalRead(6) == LOW) filename[3] = '1';
 if(digitalRead(7) == LOW) filename[4] = '1';
 if(digitalRead(8) == LOW) filename[5] = '1';
 if(digitalRead(9) == LOW) filename[6] = '1';
 
 // Search for file SD_FILE in Rootdir (=cluster 0),
 // search shortname files only (0x00,0x18)
 i = SD_SearchFile((uint8_t *)filename, 0UL, 0x00, 0x18, &LepFileInfo);
 //Si le fichier est trouve alors i est nul et on passe le while sans rien faire.
 //Si le fichier n'est pas trouve alors i n'est pas nul et on boucle indefiniment.
 //Remarque : La fonction delai necessite d'avoir les interruptions actives.
 //           On peut les desactiver apres, mais pas avant.
 while(i)                  //clignotement rapide de la diode d'activite
 {                         //pour signaler un fichier non trouve
  digitalWrite(2, HIGH);   // allumer led activite 
  delay(5);                // temporisation
  digitalWrite(2, LOW);    // eteindre led activite 
  delay(50);               // temporisation
 }
 
 // send CMD18 (multiblock read)
 noInterrupts();                          // desactiver les interruptions
 uint32_t offset = LepFileInfo.ActSector; // offset = secteur
 if (SD_type != SD_CARD_TYPE_SDHC)        // si carte non SDHC
     offset <<= 9;                        // offset = octet
 SD_CardCommand(18, offset);              // lance CMD18
 niveau = HIGH;                           // signal a 1 pour detection lep
 digitalWrite(1, niveau);                 // initialisation sortie
 activite = LOW;                          // signal a 0 pour led d'activite
 digitalWrite(2, activite);               // eteindre led activite

 while(count < LepFileInfo.Size)    // tant qu'il reste des octets
 {
  // attente octet 0xfe de debut de bloc
  while(SD_SpiReadByte() != 0xfe); 

  //traitement d'un bloc de 512 octets lus sur la carte SD 
  for(i = 0; i < 512; i++)
  {
   if(digitalRead(0) == HIGH)      // si le moteur est arrete,
   {                               // pour que le LEP soit detecte,
    digitalWrite(1, HIGH);         // mettre le signal en sortie a 1
    digitalWrite(2, LOW);          // eteindre la led d'activite
    while(digitalRead(0) == HIGH); // attendre le MOTOR ON
   }
   count++;                        // nombre d'octets lus 
   octet = SD_SpiReadByte();       // lecture d'un octet
   delai = abs(octet);             // initialisation delai
   if(octet == 0) delai = 127;     // absence de signal (127 unites)
   delai *= UNITE;                 // conversion en microsecondes
   if(octet > 0) niveau = HIGH;    // creneau positif
   if(octet < 0) niveau = LOW;     // creneau negatif
   digitalWrite(1, niveau);        // signal en sortie
   //if(i == 511) delai -= 49;       // compensation temps chgt bloc 
   delayMicroseconds(delai);       // temporisation
  }

  //lecture des deux octets de CRC
  SD_SpiReadByte();                // lecture octet CRC1
  SD_SpiReadByte();                // lecture octet CRC2
  
  //clignotement diode d'activite
  activite = (activite == LOW) ? HIGH : LOW;
  digitalWrite(2, activite);        
 }
 digitalWrite(2, 0);               // eteindre led activite 
 interrupts();                     //activer les interruptions
}

/**************************************************************************\
* Flash toutes les deux secondes apres la fin de fichier
\**************************************************************************/
void loop(void) {
//la lecture est arrivee en fin de fichier
//clignotement lent de la diode d'activite
digitalWrite(2, HIGH);   // allumer led activite 
delay(100);              // temporisation
digitalWrite(2, LOW);    // eteindre led activite 
delay(1900);             // temporisation
}

/**************************************************************************\
* Set CS High 
* Sends also one dummy byte to ensure MISO goes high impedance
\**************************************************************************/
void SD_SetCSHigh()
{
 digitalWrite(SD_CSPin, HIGH);
 SD_SpiSendByte(0xff);
}

/**************************************************************************\
* Sends a raw byte to the SPI - \param[in] b The byte to sent.
\**************************************************************************/
void SD_SpiSendByte(uint8_t b)
{
 SPDR = b;
 while(!(SPSR & (1 << SPIF))); /* wait for byte to be shifted out */
 SPSR &= ~(1 << SPIF);
}

/**************************************************************************\
* Send a command to the memory card which responses with a R1 response 
* (and possibly others).
* \param[in] command The command to send.
* \param[in] arg The argument for command.
* \returns The command answer.
\**************************************************************************/
uint8_t SD_CardCommand(uint8_t cmd, uint32_t arg) 
{
 uint8_t response;
 uint8_t crc;
 uint16_t t0;
      
 // select card
 digitalWrite(SD_CSPin, LOW);

 // wait up to timeout if busy
 t0 = ((uint16_t)millis());
 while (SD_SpiReadByte() != 0xFF)
  if ((((uint16_t)millis()) - t0) >= SD_COMMAND_TIMEOUT) break;

 // send command
 SD_SpiSendByte(cmd | 0x40);

 // send argument
 SD_SpiSendByte((arg >> 24) & 0xff);
 SD_SpiSendByte((arg >> 16) & 0xff);
 SD_SpiSendByte((arg >>  8) & 0xff);
 SD_SpiSendByte((arg >>  0) & 0xff);
  
 // send CRC, only required for commands 0 and 8
 crc = 0xFF;
 if (cmd == SD_CMD0) crc = 0x95;  // correct crc for CMD0 with arg 0
 if (cmd == SD_CMD8) crc = 0x87;  // correct crc for CMD8 with arg 0X1AA
 SD_SpiSendByte(crc);

 // skip stuff byte for stop read
 if (cmd == SD_CMD12) SD_SpiReadByte();

 // wait for response
 for(uint8_t i = 0; i < 100; ++i)
 {
  response = SD_SpiReadByte();
  if(response != 0xff) break;
 }
 return response;
}

/**************************************************************************\
* Send an application specific command which responses with a R1 response 
* (and possibly others).
* \param[in] command The command to send.
* \param[in] arg The argument for command.
* \returns The command answer.
\**************************************************************************/
uint8_t SD_CardACommand(uint8_t cmd, uint32_t arg)
{
 SD_CardCommand(SD_CMD55, 0);
 return SD_CardCommand(cmd, arg);
}

/**************************************************************************\
 * Initialize the SD memory card.
 * Power up the card, set SPI mode.
 * Detects the card version (V1, V2, SDHC), sets sector length to 512.
 * \return Zero if successfull, errorcode otherwise
\**************************************************************************/
uint8_t SD_Init()
{
 uint8_t status;
 uint16_t t0 = ((uint16_t)millis());
 uint32_t arg;

 /* Setup ports */
 pinMode(SD_CSPin, OUTPUT);
 digitalWrite(SD_CSPin, HIGH);
 pinMode(MISO, INPUT);
 pinMode(SCK, OUTPUT);
 pinMode(MOSI, OUTPUT);
 pinMode(SS, OUTPUT);
  
 digitalWrite(SCK, LOW);
 digitalWrite(MOSI, LOW);
 digitalWrite(SS, HIGH);

 /*
 * SPI configuration: 
 * - enable uC for SPI master
 * - typical no interrupts are used for SPI
 * - data order: MSB is transmitted first
 * - clock polarity: CLK is low when idle
 * - clock phase: 1-0 > Sample, 0-1 > Setup
 * - clock frequency: less than 400kHz 
 *   (will be switched to higher value after initialization)
 */
 /* initialize SPI with lowest frequency; max. 400kHz during identification mode of card */
 SPCR = (0 << SPIE) | /* SPI Interrupt Enable */
        (1 << SPE)  | /* SPI Enable */
        (0 << DORD) | /* Data Order: MSB first */
        (1 << MSTR) | /* Master mode */
        (0 << CPOL) | /* Clock Polarity: SCK low when idle */
        (0 << CPHA) | /* Clock Phase: sample on rising SCK edge */
        (1 << SPR1) | /* Clock Frequency: f_OSC / 128 */
        (1 << SPR0);
 SPSR &= ~(1 << SPI2X); /* No doubled clock frequency */
  
 // must supply min of 74 clock cycles with CS high.
 SD_SetCSHigh();
 for (uint8_t i = 0; i < 10; i++) SD_SpiSendByte(0xFF);

 // command to go idle in SPI mode
 while ((SD_CardCommand(SD_CMD0, 0)) != SD_IDLE_STATE)
  if ((((uint16_t)millis()) - t0) > SD_INIT_TIMEOUT) {SD_SetCSHigh(); return(SD_ERROR_CMD0);}
  
 // check SD version ( 2.7V - 3.6V + test pattern )
 SD_type = 0;
 if ((SD_CardCommand(SD_CMD8, 0x1AA) & SD_ILLEGAL_COMMAND)) SD_type = SD_CARD_TYPE_SD1;
    // Not done here: Test if SD or MMC card here using CMD55 + CMD1
 else
 {
  // only need last byte of r7 response
  SD_SpiReadByte();
  SD_SpiReadByte();
  status = SD_SpiReadByte();
  if ((status & 0x01) == 0) // card operation voltage range doesn't match
  {SD_SetCSHigh(); return(SD_ERROR_VOLTMATCH);}
  if (SD_SpiReadByte() != 0xAA) {SD_SetCSHigh(); return(SD_ERROR_CMD8);}
  SD_type = SD_CARD_TYPE_SD2;
 }
  
 // Turn CRC option off
 SD_CardCommand(SD_CMD59, 0);
  
 // initialize card and send host supports SDHC if SD2
 arg = (SD_type == SD_CARD_TYPE_SD2) ? 0X40000000 : 0;
 while ((SD_CardACommand(SD_ACMD41, arg)) != SD_READY_STATE) // check for timeout
  if ((((uint16_t)millis()) - t0) > SD_INIT_TIMEOUT) {SD_SetCSHigh(); return(SD_ERROR_ACMD41);}
  
 // if SD2 read OCR register to check for SDHC card
 if (SD_type == SD_CARD_TYPE_SD2)
 {
  if (SD_CardCommand(SD_CMD58, 0)) {SD_SetCSHigh(); return(SD_ERROR_CMD58);}
  // other implementation test only against 0x40 for SDHC detection...
  if ((SD_SpiReadByte() & 0xC0) == 0xC0) SD_type = SD_CARD_TYPE_SDHC;
  // discard rest of ocr - contains allowed voltage range
  SD_SpiReadByte();
  SD_SpiReadByte();
  SD_SpiReadByte();
 }

 // set block size to 512 bytes
 if(SD_CardCommand(SD_CMD16, 512)) {SD_SetCSHigh(); return(SD_ERROR_CMD16);}
 SD_SetCSHigh();
 SD_SpiSetHighSpeed();
 return 0;
}

/**************************************************************************\
* Initialize the file system .
* Does the lower level initialization and * tries to find the boot sector 
* of the first FAT16 or FAT32 partition and parse it.
* Workbuf must hold at least 512 bytes.
* Workbuf will be used later also for following functions:
* - SD_SearchFile
* - SD_Dir
* \return Zero if successful, error code otherwise
\**************************************************************************/
uint8_t SD_Init_FileSystem()
{
 uint8_t  retval;
 uint8_t  PartType;
 uint16_t temp16;
 uint32_t temp32;
    
 SD_FAT.PartType = SD_PARTTYPE_UNKNOWN;
    
 // Try init SD-Card
 retval = SD_Init();
 if(retval) return(retval);
    
 // ==== MBR (partition table) access here =====
    
 // Read sector 0 
 retval = SD_ReadBlock(0);
 if(retval) return(retval);

 // Test for signature (valid not only for MBR, but FAT Boot Sector as well!)
 if((SD_buffer[0x1fe] != 0x55) || (SD_buffer[0x1ff] != 0xaa)) return(SD_ERROR_INVAL_SECT0);
    
 // Store most important MBR values for first partition
 PartType = SD_buffer[0x1be + 0x04];
 SD_FAT.BootSectorStart =  (uint32_t)SD_buffer[0x1be + 0x08] 
                        | ((uint32_t)SD_buffer[0x1be + 0x09] << 8UL)
                        | ((uint32_t)SD_buffer[0x1be + 0x0a] << 16UL)
                        | ((uint32_t)SD_buffer[0x1be + 0x0b] << 24UL);
    
    // Check MBR values for plausibility
    if(  ((SD_buffer[0x1be] & 0x7f) == 0)
      && ((PartType == 0x04) || (PartType == 0x06) || (PartType == 0x0B) 
           || (PartType == 0x0C) || (PartType == 0x0E)) )  
    {
        // MBR seems to contain valid FAT16/FAT32 partition entry
        SD_FAT.PartType = ((PartType == 0x0B) || (PartType == 0x0C)) ? SD_PARTTYPE_FAT32 : SD_PARTTYPE_FAT16;
    }
    else
    {
        // MBR seems to contain not an valid entry, so try for super-floppy now
        SD_FAT.BootSectorStart = 0UL;
        SD_FAT.PartType = SD_PARTTYPE_SUPERFLOPPY;
    }
    
    // ====== FAT access here ======
    
    // Read Boot-Sector and test for signature
    retval = SD_ReadBlock(SD_FAT.BootSectorStart);
    if(retval) return(retval);  

    // Test for signature (valid not only for MBR, but FAT Boot Sector as well!)
    if((SD_buffer[0x1fe] != 0x55) || (SD_buffer[0x1ff] != 0xaa)) return(SD_ERROR_INVAL_BS);
    
    // Plausibility checks for FAT
    if((SD_buffer[0x0b] != 0x00) || (SD_buffer[0x0c] != 0x02) || (SD_buffer[0x15] != 0xf8)) return(SD_ERROR_INVAL_BS);

    // Read fields that are same for FAT16 and FAT32
    SD_FAT.SecPerClus = SD_buffer[0x0d];
    SD_FAT.RsvdSecCnt = (uint16_t)SD_buffer[0x0e] | ((uint16_t)SD_buffer[0x0f]<<8U);
    if((SD_FAT.SecPerClus == 0) || (SD_FAT.RsvdSecCnt == 0)) return(SD_ERROR_INVAL_BS);
    SD_FAT.NumFATs = SD_buffer[0x10];
    SD_FAT.RootEntryCount = (uint16_t)SD_buffer[0x11] | ((uint16_t)SD_buffer[0x12]<<8U);
    
    temp16 = (uint16_t)SD_buffer[0x13] | ((uint16_t)SD_buffer[0x14]<<8U);
    temp32 = (uint32_t)SD_buffer[0x20] | ((uint32_t)SD_buffer[0x21]<<8U)
             | ((uint32_t)SD_buffer[0x22]<<16U) | ((uint32_t)SD_buffer[0x23]<<24U);
    SD_FAT.TotalSec  = temp16 ? temp16 : temp32;
    
    temp16 = (uint16_t)SD_buffer[0x16] | ((uint16_t)SD_buffer[0x17]<<8U);
    temp32 = (uint32_t)SD_buffer[0x24] | ((uint32_t)SD_buffer[0x25]<<8U)
             | ((uint32_t)SD_buffer[0x26]<<16U) | ((uint32_t)SD_buffer[0x27]<<24U);
    SD_FAT.SecPerFAT  = temp16 ? temp16 : temp32;
    
    // Calculate start sectors
    SD_FAT.FatStart = SD_FAT.BootSectorStart + (uint32_t)SD_FAT.RsvdSecCnt;
    SD_FAT.RootDirStart = SD_FAT.FatStart + SD_FAT.NumFATs * (uint32_t)SD_FAT.SecPerFAT;
    
    // Data area starts at cluster #2
    SD_FAT.DataStart = SD_FAT.RootDirStart+ ((32 * (uint32_t)SD_FAT.RootEntryCount + 511)/512) - (2 * SD_FAT.SecPerClus);
    
    // determine shift that is same as multiply by SD_FAT.SecPerClus
    SD_FAT.ClusterSizeShift = 0;
    while (SD_FAT.SecPerClus != (1 << SD_FAT.ClusterSizeShift)) {
      // error if not power of 2
      if (SD_FAT.ClusterSizeShift++ > 7) return(SD_ERROR_INVAL_BS);
    }  
    
     // Calculate number and shifting of clusters
    // total data blocks
    SD_FAT.ClusterCount = SD_FAT.TotalSec - (SD_FAT.DataStart - SD_FAT.BootSectorStart);
    // divide by cluster size to get cluster count
    SD_FAT.ClusterCount >>= SD_FAT.ClusterSizeShift;  
    
    // determine if FAT16 or FAT32 (only by cluster count as done by M$)
    if (SD_FAT.ClusterCount < 4085) {
        // this would be FAT12, which is not supported
        SD_FAT.PartType = SD_PARTTYPE_UNKNOWN;
        return(SD_ERROR_FAT12);
    } else if (SD_FAT.ClusterCount < 65525) {
        SD_FAT.PartType = SD_PARTTYPE_FAT16;
        SD_FAT.ClusterEndMarker = 0xfff8UL;
    } else {
        temp32 = (uint32_t)SD_buffer[0x2c] | ((uint32_t)SD_buffer[0x2d]<<8U)
                 | ((uint32_t)SD_buffer[0x2e]<<16U) | ((uint32_t)SD_buffer[0x2f]<<24U);
        SD_FAT.RootDirStart = SD_Cluster2Sector(temp32);
        SD_FAT.PartType = SD_PARTTYPE_FAT32;
        SD_FAT.ClusterEndMarker = 0xffffff8UL;
    }
      
    return 0;
}

/**************************************************************************\
* Set SPI for full operation speed (up to 25 MHz).
* Will be called after first part of card 
* initialization was successful.
\**************************************************************************/
void SD_SpiSetHighSpeed(void)
{
 SPCR &= ~((1 << SPR1) | (1 << SPR0)); /* Clock Frequency: f_OSC / 4 */
 SPSR |= (1 << SPI2X);         /* Doubled Clock Frequency: f_OSC / 2 */
}

/**************************************************************************\
* Receives a raw byte from the SPI.
* \returns The byte which should be read.
\**************************************************************************/
uint8_t SD_SpiReadByte()
{
 SPDR = 0xff; /* send dummy data for receiving some */
 while(!(SPSR & (1 << SPIF)));
 SPSR &= ~(1 << SPIF);
 return SPDR;
}

/**************************************************************************\
* Read a 512 byte block from an SD card.
* \param[in] blockNumber Logical block to be read.
* \param[out] dst Pointer to the location that will receive the data.
* \return 0 is returned for success, error code otherwise
\**************************************************************************/
uint8_t SD_ReadBlock(uint32_t blockNumber) 
{
  uint8_t status;
  uint16_t t0;

 // use address if not SDHC card
 if (SD_type != SD_CARD_TYPE_SDHC) blockNumber <<= 9;
 if (SD_CardCommand(SD_CMD17, blockNumber)) {SD_SetCSHigh(); return(SD_ERROR_CMD17);}

 // wait for start block token
 t0 = ((uint16_t)millis());
 while ((status = SD_SpiReadByte()) == 0xFF)
  if ((((uint16_t)millis()) - t0) > SD_READ_TIMEOUT) {SD_SetCSHigh(); return(SD_ERROR_READ_TIMEOUT);}
 if (status != SD_DATA_START_BLOCK) {SD_SetCSHigh(); return(SD_ERROR_READ);}
  
 // transfer data
 SPDR = 0xFF;
 for (uint16_t i = 0; i < 512; i++)
 {
  while (!(SPSR & (1 << SPIF)));
  SD_buffer[i] = SPDR;
  SPDR = 0xFF;
 }
 while (!(SPSR & (1 << SPIF)));
 SD_buffer[512] = SPDR;

 // discard CRC
 SD_SpiReadByte();
 SD_SpiReadByte();
 SD_SetCSHigh();

 return 0;
}

/**************************************************************************\
* Returns the first sector of a given cluster
\**************************************************************************/
uint32_t SD_Cluster2Sector(uint32_t cluster)
{
 return((cluster << SD_FAT.ClusterSizeShift) + SD_FAT.DataStart);
}

/**************************************************************************\
* Search a file in the directory.
* Filename must be 8.3 format, terminated by \0 (can not access ".." now...)
* Works only over one cluster of directory information. 
* If SD_ERROR_DIR_EOC is returned call function again with next cluster number. 
* Set cluster to 0 to access root directory.
* Deleted files and long name entries are not shown generally.
* Only files are printed that has their attributes set/unset regarding maskSet/maskUnset.
* Examples for maskSet, maskUnset:
*  Ouput everything:           0x00, 0x00
*  Shortname files only:       0x00, 0x18
*  Shortname files and dirs:   0x00, 0x08
*  Shortname dirs:             0x10, 0x08
*  Volume name:                0x08, 0x10
* Mask bits: B7 = 0, B6 = 0, B5 = archive, B4 = directory, 
*            B3 = volume name, B2 = system, B1 = hidden, B0 = read only
* If file is found, fileinfo gets filled with file attributes, 
* file size in bytes and first cluster.
*  return Zero if successfully, error code otherwise
\**************************************************************************/
uint8_t SD_SearchFile(uint8_t *filename, const uint32_t cluster, const uint8_t maskSet, const uint8_t maskUnset, SD_File_t *fileinfo)
{
 uint16_t maxsect = SD_FAT.SecPerClus;
 uint32_t startsect = SD_Cluster2Sector(cluster);
 char     fnentry[12];

 if((SD_FAT.PartType != SD_PARTTYPE_FAT16) && (SD_FAT.PartType != SD_PARTTYPE_FAT32)) return(SD_ERROR_FAT_NOT_INIT);
 if(cluster == 0)
 {
  startsect = SD_FAT.RootDirStart; // Set root dir sector
  if(SD_FAT.PartType == SD_PARTTYPE_FAT16) maxsect = (uint16_t)((32 * (uint32_t)SD_FAT.RootEntryCount + 511)/512);
 }
    
 // convert filename to space-filled uppercase format
 for(uint8_t i = 0; i < 11; i++) fnentry[i] = ' ';
 for(uint8_t i = 0; i < 9; i++)
 {
  uint8_t c = *filename++;
  if((c < 0x20) || (c == '.')) break;
  if((c>='a') && (c<='z')) c -= 0x20;  // to upper case
  fnentry[i] = c;
 }
 for(uint8_t i = 8; i < 11; i++)
 {
  uint8_t c = *filename++;
  if(c < 0x20) break;
  if((c>='a') && (c<='z')) c -= 0x20;  // to upper case
  fnentry[i] = c;
 }
 fnentry[11] = 0;
 //Serial.println(fnentry);
    
 // go through sectors
 for(uint16_t i = 0; i= 11)
    {
     // found it
     fileinfo->Attributes = attrib;
     fileinfo->Size = (uint32_t)SD_buffer[j+0x1c] | (((uint32_t)SD_buffer[j+0x1d])<<8) 
                  | (((uint32_t)SD_buffer[j+0x1e])<<16) | (((uint32_t)SD_buffer[j+0x1f])<<24);
     if(SD_FAT.PartType == SD_PARTTYPE_FAT16)
      {fileinfo->FirstCluster = (uint32_t)SD_buffer[j+0x1a] | (((uint32_t)SD_buffer[j+0x1b])<<8);}
     else
      {fileinfo->FirstCluster = (uint32_t)SD_buffer[j+0x1a] | (((uint32_t)SD_buffer[j+0x1b])<<8) 
                            | (((uint32_t)SD_buffer[j+0x14])<<16) | (((uint32_t)SD_buffer[j+0x15])<<24);} 
                    
     // Initialize some things
     fileinfo->ActSector = SD_Cluster2Sector(fileinfo->FirstCluster);
     fileinfo->ActBytePos = 0;
     return(0);
    }
   }
  }
 }
 if(SD_FAT.PartType == SD_PARTTYPE_FAT16) return(SD_ERROR_FILE_NOT_FOUND);
 return(SD_ERROR_DIR_EOC);
}

La structure des fichiers images de cassettes

Les images de cassette au format .k7 utilisées par les émulateurs ne peuvent pas convenir pour la simulation du magnétophone, car ils ne contiennent pas les informations nécessaires pour reconstituer le signal. En particulier ils ne connaissent pas les délais entre blocs, ni les fréquences utilisées, ni les systèmes de protection mis en place par les éditeurs de logiciels.

Les fichiers .wav reproduisent le signal audio analogique enregistré sur la bande, mais pas le signal numérique généré par le magnétophone. Celui-ci est très voisin du signal audio dans le cas du LEP MO, par contre il est totalement différent dans le cas du LEP TO. Un fichier .wav 44,1 kHz 8 bits monophonique est très gros pour le peu d'informations contenues (49 octets pour 1 bit d'information dans le cas du TO), mais en plus il ne représente pas le signal envoyé par le magnétophone à l'ordinateur. Il ne convient donc pas.

Un nouveau format, donnant les informations requises sous une forme condensée, a été créé. C'est le format .lep dont les spécifications sont énoncées ci-dessous :

===============================================================================
Format de cassette Thomson .lep
===============================================================================

Un fichier .lep est au depart une image de cassette Thomson, TO ou MO
Il permet de reproduire le signal en sortie des LEP TO et MO.
Par extension il peut aussi être utilisé pour tous les autres formats
d'enregistrement de données numériques sur des cassettes audio.

--------------
Format TO
--------------
Le signal enregistré sur la cassette est transformé en une suite de bits selon
le système d'encodage TO : 5 périodes à 4500 Hz donnent un bit 0, 7 périodes à
6300 Hz donnent un bit 1. Chaque octet utile commence par un bit de start à 0
et se termine par deux bits de stop à 1. Les séquences de synchronisation et
les passages non enregistrés sont remplacés par des bits à 1.
Un fichier .wav 44,1 kHz 8 bits mono est créé. Il est semblable à la sortie du
magnétophone TO. Il contient 49 échantillons par bit, avec un niveau haut pour
les bits 1 et un niveau bas pour les bits 0.

----------------------------
Format MO et autres formats
----------------------------
Le fichier .wav à 44,1 kHz 8 bits mono de la cassette est mis en forme pour
contenir des créneaux rectangulaires d'amplitude constante.

--------------
Fichier .lep
--------------
Chaque octet du fichier .lep contient la durée jusqu'au prochain changement de
valeur du signal. Cette durée N est exprimée dans l'unité donnée en paramètre
(50 µs), avec le signe + pour les créneaux positifs et le signe - pour les
créneaux négatifs.
Si le nombre N est supérieur à 127, il y a un octet égal au reste de N/127
(ou 1 si le reste est nul) affecté du signe + ou - selon le sens du créneau,
suivi de N/127 octets à zéro.



Des recueils de jeux au format .lep ont été créés pour de nombreux ordinateurs des années 1980 : EXL100, Alice, Thomson TO et MO, VG5000, VIC20, C64, Acorn Electron, Oric 1 et Atmos... et d'autres suivront. Ils sont disponibles à forum.system-cfg.com/viewtopic.php?f=6&t=8064.
Il est également possible de transformer un fichier .wav en .lep grâce à l'utilitaire DCLEP. Pour le téléchargez allez à la page Emulateur.

Réalisation du module SDLEP-READER

Le module SDLEP-READER est réalisé sur une plaquette de circuit à bande de 5x3 cm. L'Arduino est soudé sur la plaquette, mais son connecteur est accessible pour une éventuelle reprogrammation in-situ. Le module pour la carte SD se place en haut à droite sur un connecteur femelle. Le connecteur de l'Arduino, en haut à gauche, reçoit le câble de liaison avec l'ordinateur. Le connecteur au milieu à droite est utilisé pour l'alimentation de 7V à 12V, dans le cas ou le module n'est pas alimenté en 5V par le câble de liaison.

sdlep-reader

La construction est très simple et ne pose aucun problème particulier. Tous les composants sont standard et se trouvent très facilement sur internet, pour un prix dérisoire, sans aucun frais de port, auprès des vendeurs de matériel électronique de Shenzen.


sdlep-readersdlep-reader

Commencer par couper la plaquette de circuit à bande aux bonnes dimensions, pour qu'elle entre dans le boîtier. Couper les pistes selon le schéma, souder les straps, le connecteur de la carte SD, le connecteur +12V, les composants discrets : diode, résistance et LED.


sdlep-readersdlep-reader

Souder ensuite l'Arduino et les interrupteurs. Dans le boîtier découper une entaille dans le fond et dans le couvercle pour laisser passer le connecteur de l'Arduino, une autre entaille dans le fond uniquement pour laisser passer le connecteur de la carte SD, un trou de 3 mm pour la LED. On peut aussi découper une entaille rectangulaire dans le couvercle pour accéder aux interrupteurs, mais ce n'est pas indispensable car il est facile d'ouvrir le boîtier pour changer leur position. N'oubliez pas de coller l'étiquette SDLEP pour donner la touche finale.


sdlep-etiquette

Confection des câbles

La dernière étape est la réalisation des câbles de liaison avec les ordinateurs. Les schémas ont été finalisés ci-dessous pour toute la gamme Thomson et pour le VG5000. Pour chaque autre modèle il y a une ou plusieurs solutions, utilisez le fil de discussion du forum system.cfg pour avoir plus de précisions.


sdlep-thomson

sdlep-vg5000

Utilisation

Le module SDLEP-READER est relié d'une part au connecteur magnétophone de l'ordinateur par un cordon spécial, d'autre part à une source d'alimentation. Pour les ordinateurs possédant un connecteur de crayon optique externe, c'est le +5V disponible à la broche 5 de ce connecteur. Pour le TO7 et le TO7/70, c'est la prise d'alimentation 12V externe située à proximité de l'interrupteur secteur. Pour d'autres ordinateurs on peut utiliser une alimentation 5V (sur le connecteur de gauche), ou une alimentation de 7V à 12V (sur le connecteur de droite), ou une pile 9V (sur le connecteur de droite). Attention, une seule alimentation doit être utilisée. Il y a un gros risque si le module est alimenté simultanément par les deux connecteurs.

Les 7 interrupteurs numéroté de 1 à 7 permettent de choisir le fichier de la carte SD à utiliser.

Determination du nom du fichier (extension .lep)
en fonction de la position des 7 interrupteurs (128 possibilites)

Le nom du fichier est compose de sept chiffres 0 ou 1
decrivant la position des interrupteurs 1 à 7
Exemple : interrupteurs 2, 4, 6 sur ON --> fichier 0101010.lep 
Exemple : interrupteurs 5 sur ON --> fichier 0000100.lep
Exemple : interrupteurs 1,2,3,5,7 sur ON --> fichier 1110101.lep 

Le fichier sélectionné par les interrupteurs doit être présent sur la carte. S'il n'est pas trouvé, la LED d'activité clignote rapidement dès la mise sous tension. Sinon elle reste éteinte jusqu'au chargement d'un programme.

Pendant le chargement, elle clignote au rythme des transferts de données. Si l'ordinateur cherche à lire après la fin du fichier, la LED émet un flash toutes les deux secondes pour signaler l'erreur "lecture après la fin de la cassette".

A tout moment, l'appui sur le bouton d'initialisation de l'Arduino rembobine la cassette. Pour changer de cassette il faut modifier la position des interrupteurs puis réinitialiser l'Arduino.

Galerie de photos

A gauche le premier prototype (sans les interrupteurs), à droite le premier essai d'intégration dans un boîtier.

sdlep-reader

Le montage définitif, avec un câble de liaison pour MO5, TO8 ou TO9.

sdlep-reader

Complément d'information

La source principale d'informations sur le projet SDLEP-READER est system-cfg
Vous pouvez y trouver tous les échanges entre l'auteur et les utilisateurs, et participer à la discussion avec vos questions et vos suggestions.

Chacun peut se procurer directement les composants nécessaires à la construction du système SDLEP-READER.
Il est aussi possible, en exprimant la demande dans le forum system-cfg, de s'associer à une commande groupée de composants.
La deuxième solution est plus simple et plus rapide, mais un peu plus chère à cause des frais (paypal, emballage, affranchissement).
Elle ne nécessite pas le logiciel et le matériel pour charger le programme dans l'Arduino.
Ce lien permet d'accéder directement au fil de discussion : SDLEP-READER - Forum system-cfg

ATTENTION :
Il s'agit des composants bruts, sans préparation particulière. Il reste à usiner le boîtier et la plaque de circuit à bande. Seul l'Arduino a été préparé : le connecteur est soudé, le programme est chargé et testé, prêt à fonctionner. Notez bien qu'il n'est pas prévu de fournir des modules SDLEP-READER prêts à l'emploi : si vous n'avez pas l'expérience nécessaire faites vous aider par un électronicien compétent.


Quatre lots sont proposés :

Lot 1: module SDLEP-READER

lot1

Lot 2: câbles pour tous les ordinateurs Thomson sauf MO6 et PC128 (il manque sur la photo le connecteur 12V pour TO7 et TO7/70)

lot2

Lot 3: câbles pour VG5000 et autres ordinateurs avec connecteur jack (il manque sur la photo le connecteur pour la télécommande)

lot3

Lot 4: câbles pour MO6 et PC128

lot4

IMPORTANT :
Les trois lots de câbles sont des exemples. Il y a d'autres configurations possibles pour tous les cas particuliers. Avant de commander expliquez bien avec quels ordinateurs vous souhaitez utiliser SDLEP-READER et nous trouverons ensemble la combinaison de câbles la mieux adaptée.


Le bouton ci-dessous permet la participation aux frais. Notez qu'il n'est pas nécessaire d'avoir un compte paypal, le paiement peut se faire par carte bancaire avec une procédure sécurisée. Dans le formulaire de don, bien préciser les lots demandés. Vous pouvez aussi donner une adresse d'expédition si elle est différente de votre adresse paypal.

previous Retour à la page Bricolage