CDBlock

From Yabause
Jump to: navigation, search

The CD Block is what the Saturn uses to read data off a CD in the drive. It contains enough on board RAM to store up to 200 raw(2352 byte) sectors and other info such as TOC information, session info, etc.

While the basic idea of what it is is simple, the inner workings of it are rather complicated and very, very powerful. You can for instance setup filters that filter the data being read from a CD to two or more separate partitions that are stored in RAM or sent directly to an add-on board(such as the MPEG card). You can also filter the output of the add-on board directly to the VDP2 and SCSP or access the data via CD block registers. This is especially handy with the MPEG card since you can set it up to decompress data and automatically send the decompressed picture to the VDP2 and the decompressed audio to the SCSP.

Usage

A basic run down as to how a regular sector read is as follows:

  1. Program issues a bunch of commands to set everything up.
  2. Program requests a sector read from specified FAD for so and so many sectors.
  3. CD Block starts sector read, depending on what filters are set(in step 1), it can filter out any sectors that don't match. Once that's done, it sends the sectors to a partition buffer(also specified in step 1) or the MPEG card(if connected and setup for that)
  4. If not sending data to the MPEG card, program then requests the sector data.
  5. Program then retrieves sector data by constantly reading the Data Transfer Register
  6. Once Program is finished, it issues a "End Transfer" command back to the CD block to know it's finished.

CD Block Registers

The CD block consists of around nine or ten 16-bit registers(I'm honestly not sure what a few of them do, or if they even are registers). The registers start at 0x25800000, are mirrored on both the even and odd words, and mirrored every 0x40 bytes until 0x25801000, then start again at 0x25808000, rinse and repeat up until 0x25891000. The areas in between the mirrored cd block registers look to be reserved for the Netlink/X-band modem and normally return 0xFF if neither cartridge is connected.

Four of the CD block register are command registers(you use them to issue a command to the cd block); Three(although it's possible it's actually two) transfer registers; And two interrupt related registers.

Address Name What's there
DTR Data Transfer Register
0x25890008 HIRQ Interrupt Status Register
0x2589000C HIRQ Mask Interrupt Mask Register
0x25890018 CR1 Command Register 1
0x2589001C CR2 Command Register 2
0x25890020 CR3 Command Register 3
0x25890024 CR4 Command Register 4
0x25890028 MPEGRGB MPEG RGB Data Transfer Register

On reset the command registers are set to as follows:

Register Value
CR1 'C'(low byte)
CR2 'D'(high byte), 'B'(low byte)
CR3 'L'(high byte), 'O'(low byte)
CR4 'C'(high byte),'K'(low byte)

CD Block Commands

There's three kinds of data a command can return: Custom data, CD status data, and MPEG status data. The only commands that return MPEG status data are the commands meant for the MPEG card. In the following table, It'll list the command issuing format, the HIRQ flags that get updated, the command data returned, and eventually the timing as well.

CD-specific Commands


MPEG Specific Commands


Other Commands


Periodic Response

When the CD block is not in the middle of handling a command it regularly issues out what's known as a periodic response. Basically what it does is set the CR registers to the most up to date CD status data and sets the periodic response flag in the status area of CR1. Pretty handy if you want to track the position of the laser during a read or seek. Please note: Periodic responses -will not- occur between the time a command is issued and when the return data is read from the CR registers.

Coding Examples

Considering the difficulty of learning the CD Block just through documentation, here are some examples of CD Block communication to help better explain how it works. Basically each section builds onto the previous section, so please refer to code from the previous section in case you're looking for a specific variable or function.

Issuing a Command

First let's start with issuing a command. We know that CR1-CR4 are used to send a command. So let's start with a simple function:

typedef struct
{
   u16 CR1;
   u16 CR2;
   u16 CR3;
   u16 CR4;
} cdcmd_struct;

#define CDB_REG_CR1         *((volatile u16 *)0x25890018)
#define CDB_REG_CR2         *((volatile u16 *)0x2589001C)
#define CDB_REG_CR3         *((volatile u16 *)0x25890020)
#define CDB_REG_CR4         *((volatile u16 *)0x25890024)

void CDWriteCommand(cdcmd_struct *cdcmd)
{
   CDB_REG_CR1 = cdcmd->CR1;
   CDB_REG_CR2 = cdcmd->CR2;
   CDB_REG_CR3 = cdcmd->CR3;
   CDB_REG_CR4 = cdcmd->CR4;
}

Pretty straight forward. Basically a function that writes to the command registers in sequence. Next we need a function to receive the response:

void CDReadReturnStatus(cdcmd_struct *cdcmdrs)
{
   cdcmdrs->CR1 = CDB_REG_CR1;
   cdcmdrs->CR2 = CDB_REG_CR2;
   cdcmdrs->CR3 = CDB_REG_CR3;
   cdcmdrs->CR4 = CDB_REG_CR4;
}         

Once again straight forward. Now let's bump it up a notch:

#define HIRQ_CMOK     0x0001
#define CDB_REG_HIRQ  *((volatile u16 *)0x25890008)
#define STATUS_WAIT   0x80
#define STATUS_REJECT 0xff
#define ERR_BUSY      -1
#define ERR_OK        0

int CDExecCommand(u16 hirqmask, cdcmd_struct *cdcmd, cdcmd_struct *cdcmdrs)
{
   int old_levelmask;
   u16 hirq_temp;
   u16 cdstatus;
   int i;

   // Mask any interrupts, we don't need to be interrupted
   old_levelmask = InterruptGetLevelMask();[1]
   InterruptSetLevelMask(0xF);

   hirq_temp = CDB_REG_HIRQ;[2]

   // Make sure CMOK flag is set, or we can't continue
   if (!(hirq_temp & HIRQ_CMOK))
      return ERR_BUSY;

   // Clear CMOK and any other user-defined flags
   CDB_REG_HIRQ = ~(hirqmask | HIRQ_CMOK);

   // Alright, time to execute the command
   CDWriteCommand(cdcmd);[3]

   // Let's wait till the command operation is finished
   for (i = 0; i < 0x240000; i++)[4]
   {
      hirq_temp = CDB_REG_HIRQ;
      if (hirq_temp & HIRQ_CMOK)
         break;
   }

   if (!(hirq_temp & HIRQ_CMOK))
      return ERR_BUSY;

   // Read return data
   CDReadReturnStatus(cdcmdrs);[5]

   cdstatus = cdcmdrs->CR1 >> 8;[6]

   // Was command good?
   if (cdstatus == STATUS_REJECT)
      return ERR_BUSY;
   else if (cdstatus & STATUS_WAIT)
      return ERR_BUSY;

   // return interrupts back to normal
   InterruptSetLevelMask(old_levelmask);[7]

   // It's all good
   return ERR_OK;
}
  1. Basically to start off we need to mask all interrupts. Normally you do this by writing over the SH2's status register(SR), of course you want to retain the old value so you save SR. So basically we're using InterruptGetLevelMask() and InterruptSetLevelMask() to do it.
  2. Next step is to read the HIRQ register and verify that CMOK is set. If it isn't, the previous command still hasn't finished executing so an error should be raised. Also CMOK should also be cleared and whatever other HIRQ flags you're waiting for.
  3. Next we send the command using the CDWriteCommand function. Nice and neat.
  4. Then we should wait until the command is finished.
  5. After the command is finished, we need to fetch the response data using the CDReadReturnStatus function.
  6. All commands return the command/cd status in the high byte of CR1, so it needs to be checked after the command is finished to verify if the command was successful. If it returns reject status, it's likely a badly formated command. Wait status is usually returned when the CD Block is in the middle of another operation.
  7. Lastly we restore the old interrupt mask and we're good.

Initializing CD Block

Now that we have a basic function for communicating with the CD Block, let's trying doing something a little more useful. First we need some functions that abort and reset various parts of the CD Block. This first functions stops any file transfers in process.

#define HIRQ_EFLS   0x0200

int CDAbortFile()
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   // Abort File Command
   cdcmd.CR1 = 0x7500;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;
  
   return CDExecCommand(HIRQ_EFLS, &cdcmd, &cdcmdrs);
}

This next function initializes the CD Block. standby argument controls the time before the CD stops moving while idle.

int CDCDBInit(int standby)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   // CD Init Command
   cdcmd.CR1 = 0x0400;
   cdcmd.CR2 = standby;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x040F;

   return CDExecCommand(0, &cdcmd, &cdcmdrs);
}

This function stops a memory transfer from CD block to SH2 memory space.

#define HIRQ_DRDY   0x0002

int CDEndTransfer()
{
   int ret;
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   cdcmd.CR1 = 0x0600;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;

   ret = CDExecCommand(0, &cdcmd, &cdcmdrs);

   CDB_REG_HIRQ = (~HIRQ_DRDY) | HIRQ_CMOK;

   return ret;
}

These functions resets the selectors. These will be explained later.

#define HIRQ_ESEL   0x0040

static int CDResetSelector(int resetflags, int selnum)
{
   int ret;
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   // Reset Selector Command
   cdcmd.CR1 = 0x4800 | ((u8)resetflags);
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = (selnum << 8);
   cdcmd.CR4 = 0x0000;

   if ((ret = CDExecCommand(HIRQ_EFLS, &cdcmd, &cdcmdrs)) != 0)
      return ret;

   // wait for function to finish
   while (!(CDB_REG_HIRQ & HIRQ_ESEL)) {}

   return ERR_OK;
}
int CDResetSelectorAll()
{
   return CDResetSelector(0xFC, 0);
}

Now let's put everything together. Basically we're putting the CD Block in a known state. Technically we don't necessarily have to call all this, but you're better off doing so.

int CDInit()
{
   int ret;

   // Abort any file transfers that may be currently going
   if ((ret = CDAbortFile()) != 0)
      return ret;

   // Init CD Block
   if ((ret = CDCDBInit(0)) != 0)
      return ret;

   // End any previous cd buffer data transfers
   if ((ret = CDEndTransfer()) != 0)
      return ret;

   // Reset all buffer partitions, partition output connectors, all filter
   // conditions, all filter input connectors, etc.
   if ((ret = CDResetSelectorAll()) != 0)
      return ret;

   return ERR_OK;
}

So now the CD Block is ready to be used!

Reading a CD sector

Now comes the fun part! Let's try to read a sector off the disc. It helps if you understand a bit about the ISO9660 specification. Consult wikipedia or google for more information on that. The first function we need is a function that sets the sector size.

static int cdsectorsize = SECT_2048;
int sectorsizetbl[4] = { 2048, 2336, 2340, 2352 };[1]
int CDSetSectorSize(int size)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   cdsectorsize = size;

   cdcmd.CR1 = 0x6000 | (size & 0xFF);
   cdcmd.CR2 = size << 8;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;

   return CDExecCommand(HIRQ_ESEL, &cdcmd, &cdcmdrs);
}

We also need a function that resets a designated selector, which will be explained later.

int CDResetSelectorOne(int selnum)
{
   return CDResetSelector(0, selnum);
}

Next we need a function for connecting the CD transfer to the specified filter.

int CDConnectCDToFilter(int filternum)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   cdcmd.CR1 = 0x3000;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = filternum << 8;
   cdcmd.CR4 = 0x0000;

   return CDExecCommand(HIRQ_ESEL, &cdcmd, &cdcmdrs);
}

And of course a function to start the actual sector reading.

int CDPlayFAD(int playmode, int startfad[2], int numsectors)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;
   int ret;

   // Clear flags
   CDB_REG_HIRQ = ~(HIRQ_PEND|HIRQ_CSCT) | HIRQ_CMOK;

   cdcmd.CR1 = 0x1080 | (startfad >> 16);
   cdcmd.CR2 = startfad;
   cdcmd.CR3 = (playmode << 8) | 0x80 | (numsectors >> 16);
   cdcmd.CR4 = numsectors;

   ret = CDExecCommand(0, &cdcmd, &cdcmdrs);

   return ret;
}

We need to know when the data is ready to be transferred for our use. Here's the function for that.

int CDIsDataReady(int selnum)
{
   int ret;
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   cdcmd.CR1 = 0x5100;
   cdcmd.CR2 = 0;
   cdcmd.CR3 = (selnum << 8);
   cdcmd.CR4 = 0;

   if ((ret = CDExecCommand(0, &cdcmd, &cdcmdrs)) != 0)
      return ERR_OK;

   // Return the number of sectors ready
   return cdcmdrs.CR4;
}

Here's the function that actually transfers the data from the CD Block to an SH2 address.

int CDTransferDataBytes(u32 numbytes, u32 *buffer)
{
   u32 i;
   int ret;
   int numsectors=numbytes / sectorsizetbl[cdsectorsize];

   if (numbytes % sectorsizetbl[cdsectorsize])[3]
      numsectors++;

   // Setup a transfer from cd buffer to wram, then delete data
   // from cd buffer
   if ((ret = CDGetThenDeleteSectorData(0, 0, numsectors)) != 0)[4]
      return ret;

   // wait a bit
   for (i = 0; i < 20000; i++) {}

   // Do transfer
   for (i = 0; i < (numbytes >> 2); i++)
   {
      buffer[0] = CDB_REG_DATATRNS; // this can also be done in word units as well[5]
      buffer++;
   }

   // Get the remainder
   if (numbytes % 4)
   {
      u32 data;
     u8 *datapointer=&data;
 
      data = CDB_REG_DATATRNS;

      for (i = 0; i < (numbytes % 4); i++)
         ((u8 *)buffer)[i] = datapointer[i];
   }

   if ((ret = CDEndTransfer()) != 0)[6]
      return ret;

   return ERR_OK;
}

Now let's put everything together. One thing that should be mentioned is that at this point we can only read the first 16 sectors of the disc. We'll address that issue in the next section.

int CDReadSector(void *buffer, unsigned long FAD, int sectorsize, unsigned long numbytes)
{
   int ret;
   int done=0;
   // Figure out how many sectors we actually have to read
   int numsectors=numbytes / sectorsizetbl[cdsectorsize];

   if (numbytes % sectorsizetbl[cdsectorsize] != 0)
      numsectors++;

   if ((ret = CDSetSectorSize(sectorsize)) != 0)[7]
      return ret;

   // Clear partition 0
   if ((ret = CDResetSelectorOne(0)) != 0)[8]
      return ret;

   // Connect CD device to filter 0
   if ((ret = CDConnectCDToFilter(0)) != 0)[9]
      return ret;

   // Start reading sectors
   if ((ret = CDPlayFAD(0, FAD, numsectors)) != 0)[10]
      return ret;

   while (!done)
   {
      unsigned long sectorstoread=0;
      unsigned long bytestoread;

      // Wait until there's data ready
      while ((sectorstoread = CDIsDataReady(0)) == 0) {} [11]

      if ((sectorstoread * sectorsizetbl[cdsectorsize]) > numbytes)
         bytestoread = numbytes;
      else
         bytestoread = sectorstoread * sectorsizetbl[cdsectorsize];

      // Setup a transfer from cd buffer to wram, then delete data
      // from cd buffer
      if ((ret = CDTransferDataBytes(bytestoread, buffer)) != 0)
         return ret;

      numbytes -= bytestoread;
      buffer += bytestoread;

      if (numbytes == 0)
         done = 1;
   }

   return ERR_OK;
}


  1. We have a choice of 2048, 2336, 2340, 2352 sized sectors. Generally you'll be using 2048, however if you're playing around with XA audio or MPEG videos, etc. you'll need to use one of the other sector sizes.
  2. Please note that the CD Block uses FAD(Frame ADdress) as opposed to LBA(Logical Block Address). To calculate FAD from LBA use the following equation: FAD = LBA + 150
  3. Keep in mind all sectors are multiples of 2048, 2336, etc. So we need round up to the nearest number of sectors
  4. This call tells the CD Block to take the stored sectors and start transferring the data to the SH2 memory space. After the data is fetched it's purged from CD Block memory.
  5. We transfer the data by reading the data transfer register. Each read collects the next set of data.
  6. Once we're done, we tell the CD Block to finish. Since we're using "Get then Delete" mode, it purges the sector data from the CD Block at this point.
  7. We first need to set the sector size before we do anything.
  8. Always clear the partition use before using it, in this case it's partition 0
  9. The CD device needs to connect into a filter before you can collect CD sectors. A filter allows you to sort sectors based on FAD, data type, etc. Pretty handy for MPEG video for instance. In this case we're using the default filter.
  10. This forces the CD Block to start reading sectors from the disc and storing it in internal memory.
  11. Here we're checking to see if the CD Block has stored at least one sector, if so, we start transferring to SH2 memory space.

Authenticating and unlocking CD

So now we can read a disc, however there's still one issue. We can only read the first 16 sectors. As part of their copy-protection, Sega set things up so that only the first 16 sectors can be read unless the disc is approved by both the CD Block and the bios. So now we have to start the process to authenticate the disc.

If a disc is detected to be an audio disc or a non-Saturn game disc(such as a video cd), the CD Block instantly approves and unlocks the rest of the sectors to be read. However if a Saturn game disc is detected at this point it starts the ring check. If it fails, the CD Block remains locked. Otherwise it's authenticated and unlocked.

So let's see the code.

We need a function to read the status of the CD Block. You should also be calling this from time to time while reading data off the disc just to make sure the disc tray cover hasn't been opened.

typedef struct
{
   unsigned char status;
   unsigned char flag;
   unsigned char repeatcnt;
   unsigned char ctrladdr;
   unsigned char track;
   unsigned char index;
   unsigned long FAD;
} cdstat_struct;

int CDGetStat(cdstat_struct *cdstatus)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;
   int ret;

   cdcmd.CR1 = 0x0000;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;

   if ((ret = CDExecCommand(0, &cdcmd, &cdcmdrs)) != 0)
      return ret;

   cdstatus->status = cdcmdrs.CR1 >> 8;
   cdstatus->flag = (cdcmdrs.CR1 >> 4) & 0xF;
   cdstatus->repeatcnt = cdcmdrs.CR1 & 0xF;
   cdstatus->ctrladdr = cdcmdrs.CR2 >> 8;
   cdstatus->track = cdcmdrs.CR2 & 0xFF;
   cdstatus->index = cdcmdrs.CR3 >> 8;
   cdstatus->FAD = ((cdcmdrs.CR3 & 0xFF) << 16) | cdcmdrs.CR4;

   return ERR_OK;
}

This function checks to see if the CD Block is in a locked state. It returns whether the disc has been authenticated and why it hasn't been.

int IsCDAuth(unsigned short *disctypeauth)
{
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;

   cdcmd.CR1 = 0xE100;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;

   // If command fails, assume it's not authenticated
   if (CDExecCommand(0, &cdcmd, &cdcmdrs) != ERR_OK)
      return FALSE;

   if (disctypeauth)
      *disctypeauth = cdcmdrs.CR2;

   // Disc type Authenticated:
   // 0x00: No CD/Not Authenticated
   // 0x01: Audio CD
   // 0x02: Regular Data CD(not Saturn disc)
   // 0x03: Copied/Pirated Saturn Disc
   // 0x04: Original Saturn Disc
   if (cdcmdrs.CR2 != 0x04 && cdcmdrs.CR2 != 0x02)
      return FALSE;

   return TRUE;
}

This function starts the authentication checks and waits until it's finished.

#define HIRQ_DCHG   0x0020

int CDAuth()
{
   int ret;
   cdcmd_struct cdcmd;
   cdcmd_struct cdcmdrs;
   unsigned short auth;
   cdstat_struct cdstatus;
   int i;

   // Clear hirq flags
   CDB_REG_HIRQ = ~(HIRQ_DCHG | HIRQ_EFLS);

   // Authenticate disc
   cdcmd.CR1 = 0xE000;
   cdcmd.CR2 = 0x0000;
   cdcmd.CR3 = 0x0000;
   cdcmd.CR4 = 0x0000;

   if ((ret = CDExecCommand(HIRQ_EFLS, &cdcmd, &cdcmdrs)) != 0)
      return ret;

   // wait till operation is finished
   while (!(CDB_REG_HIRQ & HIRQ_EFLS)) {}

   // Wait until drive has finished seeking
   for (;;)
   {
      // wait a bit
      for (i = 0; i < 100000; i++) { }

      if (CDGetStat(&cdstatus) != 0) continue;

      if (cdstatus.status == STATUS_PAUSE) break;
      else if (cdstatus.status == STATUS_FATAL) return ERR_UNKNOWN;
   }

   // Was Authentication successful?
   if (!IsCDAuth(&auth))
      return ERR_AUTH;

   return ERR_OK;
}

Putting it all together

(finish me)