mac-rom-simm-programmer/simm_programmer.c

717 lines
22 KiB
C

/*
* simm_programmer.c
*
* Created on: Dec 9, 2011
* Author: Doug
*
* Copyright (C) 2011-2023 Doug Brown
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "simm_programmer.h"
#include "hal/usbcdc.h"
#include "drivers/parallel_flash.h"
#include "tests/simm_electrical_test.h"
#include "programmer_protocol.h"
#include "led.h"
#include "hardware.h"
#include <stdbool.h>
#include <string.h>
/// Maximum size of an individual chip on a SIMM we read
#define MAX_CHIP_SIZE (2UL * 1024UL * 1024UL)
/// Number of bytes we read/write at once
#define READ_WRITE_CHUNK_SIZE_BYTES 1024UL
/// Make sure the chunk size is a multiple of 4 bytes, since there are 4 chips
#if ((READ_WRITE_CHUNK_SIZE_BYTES % 4) != 0)
#error Read/write chunk size should be a multiple of 4 bytes
#endif
/// The maximum number of erase groups we deal with
#define MAX_ERASE_SECTOR_GROUPS 10
/// Version info to respond with
#define VERSION_MAJOR 1
#define VERSION_MINOR 5
#define VERSION_REVISION 1
/// The number of erase sector groups we know about currently.
/// If it's zero, we don't know, so fall back to defaults.
static uint8_t numEraseSectorGroups = 0;
/// The erase sector groups that we will pass to the programmer
static ParallelFlashEraseSectorGroup eraseSectorGroups[MAX_ERASE_SECTOR_GROUPS];
/// Internal state so we know how to interpret the next-received byte
typedef enum ProgrammerCommandState
{
WaitingForCommand = 0, //!< No active commands
ReadingChipsReadLength, //!< Reading the length for reading data from the SIMM
ReadingChips, //!< Reading data from the SIMM
WritingChips, //!< Writing data to the SIMM
ErasePortionReadingPosLength,//!< Reading the length of SIMM data to erase
ReadingChipsReadStartPos, //!< Reading the start position for reading data from the SIMM
WritingChipsReadingStartPos, //!< Reading the start position for writing data to the SIMM
ReadingChipsMask, //!< Reading the bitmask of which chips should be programmed
ReadingSectorLayout, //!< Reading the erase sector layout
} ProgrammerCommandState;
static ProgrammerCommandState curCommandState = WaitingForCommand;
// State info for reading/writing
static uint16_t curReadIndex;
static uint32_t readLength;
static uint8_t readLengthByteIndex;
static int16_t writePosInChunk = -1;
static uint16_t curWriteIndex = 0;
static bool verifyDuringWrite = false;
static uint32_t erasePosition;
static uint32_t eraseLength;
static uint8_t chipsMask = ALL_CHIPS;
/// Buffers we use to store incoming/outgoing data.
static union
{
uint32_t words[READ_WRITE_CHUNK_SIZE_BYTES / PARALLEL_FLASH_NUM_CHIPS];
uint8_t bytes[READ_WRITE_CHUNK_SIZE_BYTES];
} writeChunks, readChunks;
// Private functions
static void SIMMProgrammer_HandleWaitingForCommandByte(uint8_t byte);
static void SIMMProgrammer_HandleReadingChipsByte(uint8_t byte);
static void SIMMProgrammer_HandleReadingChipsReadLengthByte(uint8_t byte);
static void SIMMProgrammer_SendReadDataChunk(void);
static void SIMMProgrammer_HandleWritingChipsByte(uint8_t byte);
static void SIMMProgrammer_ElectricalTest_Fail_Handler(uint8_t index1, uint8_t index2);
static void SIMMProgrammer_HandleErasePortionReadPosLengthByte(uint8_t byte);
static void SIMMProgrammer_HandleReadingChipsReadStartPosByte(uint8_t byte);
static void SIMMProgrammer_HandleWritingChipsReadingStartPosByte(uint8_t byte);
static void SIMMProgrammer_HandleReadingChipsMaskByte(uint8_t byte);
static void SIMMProgrammer_HandleReadingSectorLayoutByte(uint8_t byte);
/** Initializes the SIMM programmer and prepares it for USB communication.
*
*/
void SIMMProgrammer_Init(void)
{
USBCDC_Init();
}
/** Allows the SIMM programmer to do its thing. Main loop handler.
*
* Call this function during every main loop iteration.
*/
void SIMMProgrammer_Check(void)
{
// Read as many bytes as we can and process them
int16_t result;
while ((result = USBCDC_ReadByte()) >= 0)
{
uint8_t recvByte = (uint8_t)result;
// Hand it off to the correct handler function based on the current state
switch (curCommandState)
{
case WaitingForCommand:
SIMMProgrammer_HandleWaitingForCommandByte(recvByte);
break;
case ReadingChipsReadLength:
SIMMProgrammer_HandleReadingChipsReadLengthByte(recvByte);
break;
case ReadingChips:
SIMMProgrammer_HandleReadingChipsByte(recvByte);
break;
case WritingChips:
SIMMProgrammer_HandleWritingChipsByte(recvByte);
break;
case ErasePortionReadingPosLength:
SIMMProgrammer_HandleErasePortionReadPosLengthByte(recvByte);
break;
case ReadingChipsReadStartPos:
SIMMProgrammer_HandleReadingChipsReadStartPosByte(recvByte);
break;
case WritingChipsReadingStartPos:
SIMMProgrammer_HandleWritingChipsReadingStartPosByte(recvByte);
break;
case ReadingChipsMask:
SIMMProgrammer_HandleReadingChipsMaskByte(recvByte);
break;
case ReadingSectorLayout:
SIMMProgrammer_HandleReadingSectorLayoutByte(recvByte);
break;
}
}
// And do any periodic USB CDC tasks
USBCDC_Check();
}
/** Handles a received byte when we are waiting for a command
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleWaitingForCommandByte(uint8_t byte)
{
switch (byte)
{
// Asked to enter waiting mode -- we're already there, so say OK.
case EnterWaitingMode:
USBCDC_SendByte(CommandReplyOK);
curCommandState = WaitingForCommand;
break;
// Asked to do the electrical test. Reply OK, and then do the test,
// sending whatever replies necessary
case DoElectricalTest:
USBCDC_SendByte(CommandReplyOK);
// Flush out the initial "OK" reply immediately in this case so the
// caller gets immediate feedback that the test has started
USBCDC_Flush();
SIMMElectricalTest_Run(SIMMProgrammer_ElectricalTest_Fail_Handler);
USBCDC_SendByte(ProgrammerElectricalTestDone);
curCommandState = WaitingForCommand;
break;
// Asked to identify the chips in the SIMM. Identify them and send reply.
case IdentifyChips:
{
struct ParallelFlashChipID chips[PARALLEL_FLASH_NUM_CHIPS];
USBCDC_SendByte(CommandReplyOK);
ParallelFlash_IdentifyChips(chips);
for (int i = 0; i < PARALLEL_FLASH_NUM_CHIPS; i++)
{
USBCDC_SendByte(chips[i].manufacturer);
USBCDC_SendByte(chips[i].device);
}
USBCDC_SendByte(ProgrammerIdentifyDone);
break;
}
// Asked to read a single byte from each SIMM. Change the state and reply.
case ReadByte:
USBCDC_SendByte(CommandReplyInvalid); // not implemented yet
break;
// Asked to read all four chips. Set the state, reply with the first chunk.
// This will read from the BEGINNING of the SIMM every time. Use
// ReadChipsAt to specify a start position
case ReadChips:
curCommandState = ReadingChipsReadLength;
curReadIndex = 0;
readLengthByteIndex = 0;
readLength = 0;
USBCDC_SendByte(CommandReplyOK);
break;
case ReadChipsAt:
curCommandState = ReadingChipsReadStartPos;
curReadIndex = 0;
readLengthByteIndex = 0;
readLength = 0;
USBCDC_SendByte(CommandReplyOK);
break;
// Erase the chips and reply OK. (TODO: Sometimes erase might fail)
case EraseChips:
ParallelFlash_EraseChips(chipsMask);
USBCDC_SendByte(CommandReplyOK);
break;
// Begin writing the chips. Change the state, reply, wait for chunk of data
case WriteChips:
curCommandState = WritingChips;
curWriteIndex = 0;
writePosInChunk = -1;
USBCDC_SendByte(CommandReplyOK);
break;
case WriteChipsAt:
curCommandState = WritingChipsReadingStartPos;
curWriteIndex = 0;
readLengthByteIndex = 0;
writePosInChunk = -1;
USBCDC_SendByte(CommandReplyOK);
break;
// Asked for the current bootloader state. We are in the program right now,
// so reply accordingly.
case GetBootloaderState:
USBCDC_SendByte(CommandReplyOK);
USBCDC_SendByte(BootloaderStateInProgrammer);
break;
// Enter the bootloader. Wait a bit, then jump to the bootloader location.
case EnterBootloader:
USBCDC_SendByte(CommandReplyOK);
// Force this to be sent immediately so the programmer software knows.
USBCDC_Flush();
// Now enter the bootloader
Board_EnterBootloader();
break;
// Enter the programmer. We're already there, so reply OK.
case EnterProgrammer:
// Already in the programmer
USBCDC_SendByte(CommandReplyOK);
break;
// Set the SIMM type to the older, smaller chip size (2MB and below)
case SetSIMMTypePLCC32_2MB:
ParallelFlash_SetChipType(ParallelFlash_SST39SF040_x4);
USBCDC_SendByte(CommandReplyOK);
break;
case SetSIMMTypeLarger:
ParallelFlash_SetChipType(ParallelFlash_M29F160FB5AN6E2_x4);
USBCDC_SendByte(CommandReplyOK);
break;
case SetVerifyWhileWriting:
verifyDuringWrite = true;
USBCDC_SendByte(CommandReplyOK);
break;
case SetNoVerifyWhileWriting:
verifyDuringWrite = false;
USBCDC_SendByte(CommandReplyOK);
break;
case ErasePortion:
readLengthByteIndex = 0;
eraseLength = 0;
erasePosition = 0;
curCommandState = ErasePortionReadingPosLength;
USBCDC_SendByte(CommandReplyOK);
break;
case SetChipsMask:
curCommandState = ReadingChipsMask;
USBCDC_SendByte(CommandReplyOK);
break;
case SetSectorLayout:
curCommandState = ReadingSectorLayout;
USBCDC_SendByte(CommandReplyOK);
break;
case GetFirmwareVersion:
USBCDC_SendByte(CommandReplyOK);
USBCDC_SendByte(VERSION_MAJOR);
USBCDC_SendByte(VERSION_MINOR);
USBCDC_SendByte(VERSION_REVISION);
USBCDC_SendByte(0);
USBCDC_SendByte(ProgrammerGetFWVersionDone);
break;
// We don't know what this command is, so reply that it was invalid.
default:
USBCDC_SendByte(CommandReplyInvalid);
break;
}
}
/** Handles a received byte when we are reading from chips
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleReadingChipsByte(uint8_t byte)
{
// The byte should be a reply from the computer. It should be either:
// 1) ComputerReadOK -- meaning it got the chunk we just sent
// or
// 2) ComputerReadCancel -- meaning the user canceled the read
switch (byte)
{
case ComputerReadOK:
// If they have confirmed the final data chunk, let them know
// that they have finished, and enter command state.
if (curReadIndex >= readLength)
{
LED_Off();
USBCDC_SendByte(ProgrammerReadFinished);
curCommandState = WaitingForCommand;
}
else // There's more data left to read, so read it and send it to them!
{
LED_Toggle();
USBCDC_SendByte(ProgrammerReadMoreData);
SIMMProgrammer_SendReadDataChunk();
}
break;
case ComputerReadCancel:
// If they've canceled, let them know we got their request and go back
// to "waiting for command" state
USBCDC_SendByte(ProgrammerReadConfirmCancel);
curCommandState = WaitingForCommand;
break;
}
}
/** Handles a received byte when we are reading the length of data requested to be read
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleReadingChipsReadLengthByte(uint8_t byte)
{
// There will be four bytes, so count up until we know the length. If they
// have sent all four bytes, send the first read chunk.
readLength |= (((uint32_t)byte) << (8*readLengthByteIndex));
if (++readLengthByteIndex >= 4)
{
// Ensure it's within limits and a multiple of 1024
if ((curReadIndex + readLength > PARALLEL_FLASH_NUM_CHIPS * MAX_CHIP_SIZE) ||
(readLength % READ_WRITE_CHUNK_SIZE_BYTES) ||
(curReadIndex % READ_WRITE_CHUNK_SIZE_BYTES) ||
(readLength == 0))// Ensure it's within limits and a multiple of 1024
{
USBCDC_SendByte(ProgrammerReadError);
curCommandState = WaitingForCommand;
}
else
{
// Convert the length/pos into the number of chunks we need to send
readLength /= READ_WRITE_CHUNK_SIZE_BYTES;
curReadIndex /= READ_WRITE_CHUNK_SIZE_BYTES;
curCommandState = ReadingChips;
USBCDC_SendByte(ProgrammerReadOK);
SIMMProgrammer_SendReadDataChunk();
}
}
}
/** Reads a chunk of data from the SIMM and sends it over the USB CDC serial port.
*
*/
static void SIMMProgrammer_SendReadDataChunk(void)
{
// Read the next chunk of data, send it over USB, and make sure
// we sent it correctly.
ParallelFlash_Read(curReadIndex * (READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS),
readChunks.words, READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS);
bool retVal = USBCDC_SendData(readChunks.bytes, READ_WRITE_CHUNK_SIZE_BYTES);
// If for some reason there was an error, mark it as such. Otherwise,
// increment our pointer so we know the next chunk of data to send.
if (!retVal)
{
//curCommandState = ReadingChipsUnableSendError; // TODO: not implemented
curCommandState = WaitingForCommand;
}
else
{
curReadIndex++;
}
}
/** Handles a received byte when we are in the "writing chips" state
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleWritingChipsByte(uint8_t byte)
{
// This means we have just started the entire process or just finished
// a chunk, so see what the computer has decided for us to do.
if (writePosInChunk == -1)
{
switch (byte)
{
// The computer asked to write more data to the SIMM.
case ComputerWriteMore:
writePosInChunk = 0;
// Make sure we don't write past the capacity of the chips.
if (curWriteIndex < MAX_CHIP_SIZE / (READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS))
{
USBCDC_SendByte(ProgrammerWriteOK);
}
else
{
LED_Off();
USBCDC_SendByte(ProgrammerWriteError);
curCommandState = WaitingForCommand;
}
break;
// The computer said that it's done writing.
case ComputerWriteFinish:
LED_Off();
USBCDC_SendByte(ProgrammerWriteOK);
curCommandState = WaitingForCommand;
break;
// The computer asked to cancel.
case ComputerWriteCancel:
LED_Off();
USBCDC_SendByte(ProgrammerWriteConfirmCancel);
curCommandState = WaitingForCommand;
break;
}
}
else // Interpret the incoming byte as data to write to the SIMM.
{
// Save the byte. Then, block until we receive the rest of the data.
writeChunks.bytes[writePosInChunk++] = byte;
while (writePosInChunk < READ_WRITE_CHUNK_SIZE_BYTES)
{
writeChunks.bytes[writePosInChunk++] = USBCDC_ReadByteBlocking();
}
// We filled up the chunk, write it out and confirm it, then wait
// for the next command from the computer!
if (chipsMask == ALL_CHIPS)
{
ParallelFlash_WriteAllChips(curWriteIndex * (READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS),
writeChunks.words, READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS);
}
else
{
ParallelFlash_WriteSomeChips(curWriteIndex * (READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS),
writeChunks.words, READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS, chipsMask);
}
// Verify if we were asked to.
uint8_t badVerifyChipsMask = 0;
if (verifyDuringWrite)
{
// Read back a chunk
ParallelFlash_Read(curWriteIndex * (READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS),
readChunks.words, READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS);
// Compare the readback to what we attempted to flash.
// Look at each chip
for (uint8_t chip = 0; chip < PARALLEL_FLASH_NUM_CHIPS; chip++)
{
uint16_t bytePos = chip;
uint8_t thisChipMask = 1 << chip;
// Loop over all bytes that are on this chip
for (uint16_t i = 0; i < READ_WRITE_CHUNK_SIZE_BYTES/PARALLEL_FLASH_NUM_CHIPS; i++)
{
if (writeChunks.bytes[bytePos] != readChunks.bytes[bytePos])
{
badVerifyChipsMask |= thisChipMask;
}
bytePos += PARALLEL_FLASH_NUM_CHIPS;
}
}
// Filter out chips we didn't care about
badVerifyChipsMask &= chipsMask;
}
// Bail if verification failed
if (badVerifyChipsMask != 0)
{
// Verification failed. The mask we calculated is actually
// backwards. We need to reverse it when we transmit the IC
// status back to the programmer software. This is kind of silly
// but it's too late to update the protocol.
uint8_t actualBadMask = 0;
for (uint8_t i = 0; i < PARALLEL_FLASH_NUM_CHIPS; i++)
{
if (badVerifyChipsMask & (1 << i))
{
actualBadMask |= 0x80;
}
actualBadMask >>= 1;
}
// Uh oh -- verification failure.
LED_Off();
// Send the fail bit along with a mask of failed chips.
USBCDC_SendByte(ProgrammerWriteVerificationError | badVerifyChipsMask);
curCommandState = WaitingForCommand;
}
else
{
USBCDC_SendByte(ProgrammerWriteOK);
curWriteIndex++;
writePosInChunk = -1;
LED_Toggle();
}
}
}
/** Handler called during an electrical test when a short is detected
*
* @param index1 The index of the first shorted pin
* @param index2 The index of the second shorted pin
*
* The two pins at index1 and index2 have been detected as shorted together.
* The numbering is internal to the SIMM electrical test, and the programmer
* software knows how to interpret it.
*/
static void SIMMProgrammer_ElectricalTest_Fail_Handler(uint8_t index1, uint8_t index2)
{
USBCDC_SendByte(ProgrammerElectricalTestFail);
USBCDC_SendByte(index1);
USBCDC_SendByte(index2);
}
/** Handles a received byte when we are determining what part of the chip to erase
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleErasePortionReadPosLengthByte(uint8_t byte)
{
// Read in the position and length to erase
if (readLengthByteIndex < 4)
{
erasePosition |= (((uint32_t)byte) << (8*readLengthByteIndex));
}
else
{
eraseLength |= (((uint32_t)byte) << (8*(readLengthByteIndex - 4)));
}
if (++readLengthByteIndex >= 8)
{
bool eraseSuccess = false;
// Ensure the position and length are a multiple of 4 so that the division by 4
// won't confuse anything.
if (((erasePosition % 4) == 0) &&
((eraseLength % 4) == 0))
{
uint32_t boundary = eraseLength + erasePosition;
// Ensure they are within the limits of our addressable length too.
// We can't address more than 8 MB of data at a time.
if (boundary <= (8 * 1024UL * 1024UL))
{
// OK! We're erasing certain sectors of a SIMM.
USBCDC_SendByte(ProgrammerErasePortionOK);
// Send the response immediately, it could take a while.
USBCDC_Flush();
if (ParallelFlash_EraseSectors(erasePosition/PARALLEL_FLASH_NUM_CHIPS,
eraseLength/PARALLEL_FLASH_NUM_CHIPS, chipsMask,
numEraseSectorGroups, eraseSectorGroups))
{
eraseSuccess = true;
}
}
}
if (eraseSuccess)
{
// Not on a sector boundary for erase position and/or length
USBCDC_SendByte(ProgrammerErasePortionFinished);
curCommandState = WaitingForCommand;
}
else
{
// Not on a sector boundary for erase position and/or length
USBCDC_SendByte(ProgrammerErasePortionError);
curCommandState = WaitingForCommand;
}
}
}
/** Handles a received byte when we are determining where to start reading from the SIMM
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleReadingChipsReadStartPosByte(uint8_t byte)
{
// There will be four bytes, so count up until we know the position. If they
// have sent all four bytes, then start reading the length
curReadIndex |= (((uint32_t)byte) << (8*readLengthByteIndex));
if (++readLengthByteIndex >= 4)
{
readLengthByteIndex = 0;
curCommandState = ReadingChipsReadLength;
}
}
/** Handles a received byte when we are determining where to start writing to the SIMM
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleWritingChipsReadingStartPosByte(uint8_t byte)
{
// There will be four bytes, so count up until we know the position. If they
// have sent all four bytes, then confirm the write and begin
curWriteIndex |= (((uint32_t)byte) << (8*readLengthByteIndex));
if (++readLengthByteIndex >= 4)
{
// Got it...now, is it valid? If so, allow the write to begin
if ((curWriteIndex % READ_WRITE_CHUNK_SIZE_BYTES) ||
(curWriteIndex >= PARALLEL_FLASH_NUM_CHIPS * MAX_CHIP_SIZE))
{
USBCDC_SendByte(ProgrammerWriteError);
curCommandState = WaitingForCommand;
}
else
{
// Convert write size into an index appropriate for rest of code
curWriteIndex /= READ_WRITE_CHUNK_SIZE_BYTES;
USBCDC_SendByte(ProgrammerWriteOK);
curCommandState = WritingChips;
}
}
}
/** Handles a received byte when we are determining the mask of which chips to write to
*
* @param byte The received byte
*/
static void SIMMProgrammer_HandleReadingChipsMaskByte(uint8_t byte)
{
// Single byte follows containing mask of chips we're programming
if (byte <= 0x0F)
{
// Mask has to be less than or equal to 0x0F because there are only
// four valid mask bits.
chipsMask = byte;
USBCDC_SendByte(CommandReplyOK);
}
else
{
USBCDC_SendByte(CommandReplyError);
}
// Done either way; now we're waiting for a command to arrive
curCommandState = WaitingForCommand;
}
/** Handles a received byte when we are reading in the sector layout
*
* @param byte The received byte, which is the first sector layout byte
*/
static void SIMMProgrammer_HandleReadingSectorLayoutByte(uint8_t byte)
{
numEraseSectorGroups = 0;
uint32_t sectorCount = byte;
uint32_t sectorSize = 0;
int byteIndex = 1;
while (1)
{
// Read in the sector size
for (int i = byteIndex; i < 4; i++)
{
uint32_t nextByte = (uint32_t)USBCDC_ReadByteBlocking();
sectorCount |= nextByte << (i * 8);
}
// From now on, we loop over 4 bytes, not 3
byteIndex = 0;
// If we read in a count of 0, we're done!
if (sectorCount == 0)
{
break;
}
// We have a nonzero count, so read in the size now
for (int i = 0; i < 4; i++)
{
uint32_t nextByte = (uint32_t)USBCDC_ReadByteBlocking();
sectorSize |= nextByte << (i * 8);
}
// If we have room to store it in the array, do it
if (numEraseSectorGroups < MAX_ERASE_SECTOR_GROUPS)
{
eraseSectorGroups[numEraseSectorGroups].count = sectorCount;
eraseSectorGroups[numEraseSectorGroups].size = sectorSize;
numEraseSectorGroups++;
}
// Now read in the next chunk of data
sectorCount = 0;
sectorSize = 0;
}
// We got the list. Done!
USBCDC_SendByte(CommandReplyOK);
curCommandState = WaitingForCommand;
}