mac-rom-simm-programmer/simm_programmer.c
Doug Brown 4221c18682 Bump version to 1.5.1
This new version number doesn't add anything to the AVR version of the
firmware. However, it signifies the release of the ARM version.
2023-09-10 05:02:44 -07:00

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;
}