/************************************************************************ * Copyright (c) 2009-2011, Microchip Technology Inc. * * Microchip licenses this software to you solely for use with Microchip * products. The software is owned by Microchip and its licensors, and * is protected under applicable copyright laws. All rights reserved. * * SOFTWARE IS PROVIDED "AS IS." MICROCHIP EXPRESSLY DISCLAIMS ANY * WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING BUT * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. IN NO EVENT SHALL * MICROCHIP BE LIABLE FOR ANY INCIDENTAL, SPECIAL, INDIRECT OR * CONSEQUENTIAL DAMAGES, LOST PROFITS OR LOST DATA, HARM TO YOUR * EQUIPMENT, COST OF PROCUREMENT OF SUBSTITUTE GOODS, TECHNOLOGY * OR SERVICES, ANY CLAIMS BY THIRD PARTIES (INCLUDING BUT NOT LIMITED * TO ANY DEFENSE THEREOF), ANY CLAIMS FOR INDEMNITY OR CONTRIBUTION, * OR OTHER SIMILAR COSTS. * * To the fullest extent allowed by law, Microchip and its licensors * liability shall not exceed the amount of fees, if any, that you * have paid directly to Microchip to use this software. * * MICROCHIP PROVIDES THIS SOFTWARE CONDITIONALLY UPON YOUR ACCEPTANCE * OF THESE TERMS. * * Author Date Ver Comment ************************************************************************* * E. Schlunder 2009/04/14 0.01 Initial code ported from VB app. * T. Lawrence 2011/01/14 2.9 Initial implementation of USB version of this * bootloader application. * F. Schlunder 2011/07/06 2.9a Small update to support importing of hex files * with "non-monotonic" line address ordering. * F. Schlunder 2015/07/28 2.13 Added ability to override default VID/PID by specifing * new values when launching the .exe. * Example Usage: * HIDBootloader.exe 0x04D8 0x1234 * Where "0x04D8" is the target VID (of the bootloader firmware) * Where "0x1234" is the target PID (of the bootloader firmware) * Also added ability to specify optional third parameter (a .hex file) * to be programmed immediately (ex: for non-GUI/fully automated programming * operation, where the bootloader immediately programs the device and then exits). * Example Usage: * HIDBootloader.exe 0x04D8 0x1234 TheNewApplicationImage.hex ************************************************************************/ #include #include #include #include #include #include #include #include #include #include "MainWindow.h" #include "ui_MainWindow.h" #include "Settings.h" #include "../version.h" // Device Vendor and Product IDs. These values should match the values used in the USB device firmware (in the device descriptor). // These are the default values that this PC application will serach for when trying to find/connect to the device. If this application // is called from the command line with extra parameters (specifying new VID and PID values), then those values will be used instead // of these default values. #define DEFAULT_VID 0x04D8 #define DEFAULT_PID 0x003C //Surely the micro doesn't have a programmable memory region greater than 268 Megabytes... //Value used for error checking device reponse values. #define MAXIMUM_PROGRAMMABLE_MEMORY_SEGMENT_SIZE 0x0FFFFFFF bool deviceFirmwareIsAtLeast101 = false; Comm::ExtendedQueryInfo extendedBootInfo; volatile bool consoleInvokedEraseProgramVerifyComplete = false; volatile bool consoleEraseProgramVerifiedOkay = false; uint16_t DeviceVID; uint16_t DevicePID; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindowClass) { hexOpen = false; timer = new QTimer(); ui->setupUi(this); setWindowTitle(QCoreApplication::applicationName() + QString(" v") + VERSION); // by default hide the advanced controls // (qtimer with 0 delay is a dumb way to make sure this is called after the window finishes updating geometry) QTimer::singleShot(0, this, [this]() { on_checkBoxAdvancedMode_stateChanged(0); }); QSettings settings; settings.beginGroup("WriteOptions"); writeFlash = settings.value("writeFlash", true).toBool(); writeEeprom = settings.value("writeEeprom", false).toBool(); settings.endGroup(); comm = new Comm(); deviceData = new DeviceData(); hexData = new DeviceData(); device = new Device(deviceData); qRegisterMetaType("Comm::ErrorCode"); connect(ui->btnFlashFirmware, &QPushButton::clicked, this, [=](){ future = QtConcurrent::run([this](){ Comm::ErrorCode subResult; //Update the progress bar so the user knows things are happening. emit SetProgressBar(3); //First erase the entire device. emit IoWithDeviceStateChanged("(1/3) Erasing device..."); subResult = EraseDevice(); if (subResult != Comm::Success) { emit IoWithDeviceStateChanged("Erasing device failed!"); QMessageBox::critical(this, "Device Flashing Failed", "An error ocurred while erasing the device. Please disconnect " "and reconnect the device and try again."); return subResult; } //then write the new firmware to the device emit IoWithDeviceStateChanged("(2/3) Writing device..."); subResult = WriteDevice(); if (subResult != Comm::Success) { emit IoWithDeviceStateChanged("Writing device failed!"); QMessageBox::critical(this, "Device Flashing Failed", "An error ocurred while writing the device. Please disconnect " "and reconnect the device and try again."); return subResult; } //finally verify the device's memory and write the signature word emit IoWithDeviceStateChanged("(3/3) Verifying device..."); subResult = VerifyDevice(); emit SetProgressBar(100); if (subResult != Comm::Success) { emit IoWithDeviceStateChanged("Verifying device failed!"); QMessageBox::critical(this, "Device Flashing Failed", "An error ocurred while verifying the device. Please disconnect " "and reconnect the device and try again."); return subResult; } emit AppendString("Firmware was sucessfully flashed"); emit IoWithDeviceStateChanged("Ready."); emit UserMustResetPrompt(); return Comm::Success; }); ui->plainTextEdit->clear(); ui->plainTextEdit->appendPlainText("Starting Erase/Program/Verify Sequence."); ui->plainTextEdit->appendPlainText("Do not unplug device or disconnect power until the operation is fully complete."); ui->plainTextEdit->appendPlainText(" "); }); connect(timer, SIGNAL(timeout()), this, SLOT(Connection())); connect(this, SIGNAL(IoWithDeviceCompleted(QString,Comm::ErrorCode,double)), this, SLOT(IoWithDeviceComplete(QString,Comm::ErrorCode,double))); connect(this, SIGNAL(IoWithDeviceStarted(QString)), this, SLOT(IoWithDeviceStart(QString))); connect(this, SIGNAL(AppendString(QString)), this, SLOT(AppendStringToTextbox(QString))); connect(this, SIGNAL(SetProgressBar(int)), this, SLOT(UpdateProgressBar(int))); connect(comm, SIGNAL(SetProgressBar(int)), this, SLOT(UpdateProgressBar(int))); connect(this, &MainWindow::IoWithDeviceStateChanged, this, &MainWindow::IoWithDeviceStateChange); connect(this, &MainWindow::UserMustResetPrompt, this, &MainWindow::PromptUserToReset); //Assume initally that the USB device that we are trying to find is using the default VID/PID. DeviceVID = DEFAULT_VID; DevicePID = DEFAULT_PID; //Check if the caller specified alternate VID and PID values by specifying them at the command line when they called this application //ex: HIDBootloader.exe 0x04D8 0x1234 //Where the first number string is the VID and the second number provided is the PID, of the USB device bootloader //firmware (the "application" or non-bootloader firmware VID and PID don't matter and aren't relevant to this program, //only the bootloader VID/PID). //If the caller provided VID/PID values, use those values instead of the DEFAULT_VID and DEFAULT_PID when searching/connecting //to the USB device. //Fetch an array of strings of the input parmaters that the caller used when invoking the application. QStringList args = QCoreApplication::arguments(); if(args.size() >= 2) { //Check if the user passed in a VID string when invoking the program. QString firstParameter = args.value(1); if((firstParameter.size() == 4) || (firstParameter.size() == 6)) //could be 4 characters long if no leading "0x" was provided, or 6 chars if the full thing was provided, ex: "0x04D8" { bool status; //Convert the hex ASCII string into a proper ulong type variable. ulong HexVID = firstParameter.toULong(&status, 16); if(status == true) { //Make sure the ulong converted from the text string was a 16-bit hex value. if(HexVID <= 0x0000FFFF) { //Looks okay. Use the value as the real value to search for when looking for the USB device. DeviceVID = HexVID; } } } //Check if the user passed in a PID string when invoking the program. if(args.size() >= 3) { QString secondParameter = args.value(2); if((secondParameter.size() == 4) || (secondParameter.size() == 6)) { bool status; ulong HexPID = secondParameter.toULong(&status, 16); if(status == true) { //Make sure the ulong converted from the text string was a 16-bit hex value. if(HexPID <= 0x0000FFFF) { //Looks okay. Use the value as the real value to search for when looking for the USB device. DevicePID = HexPID; } } } } } //Make initial check to see if the USB device is attached comm->PollUSB(DeviceVID, DevicePID); if(comm->isConnected()) { qWarning("Attempting to open device..."); comm->open(DeviceVID, DevicePID); ui->plainTextEdit->setPlainText("Device Attached."); ui->plainTextEdit->appendPlainText("Connecting..."); GetQuery(); } else { ui->plainTextEdit->appendPlainText("Device not detected. Verify device is attached and in firmware update mode."); hexOpen = false; setBootloadEnabled(false); emit SetProgressBar(0); } timer->start(1000); //Check for future USB connection status changes every 1000 milliseconds. //Check if the user ran the .exe with extensions pointing staight to the hex file of interest to be programmed. //If the device is connected, and the extra info was provided, jump straight to erase/program/verify operation //and then exit the program. This is useful if you want to use the bootloader in a more production or automated //environment where you don't necessarily want to have to click a button on the GUI. if(args.size() >= 4) { //Copy the .hex file name/path string from the string array into a separate string. QString argument = args.value(3); if(argument.endsWith(".hex", Qt::CaseInsensitive)) { //Make sure the device was already connected in bootloader mode and ready. if(comm->isConnected()) { //Load up the .hex file from the hard drive LoadFile(argument); printf("Found device and hex file. Programming device..."); //Start the thread that does the erase/program/verify sequence. future = QtConcurrent::run(this, &MainWindow::WriteDevice); //Wait for the thread to finish up. while((future.isFinished() == false) && (consoleInvokedEraseProgramVerifyComplete == false)) { QThread::msleep(5); } //Check the status and exit the application. printf("\r\nFinished erase/program/verify sequence."); if(consoleEraseProgramVerifiedOkay == true) { //Successfully erase/program/verified via console invocation. printf(" Operation successful."); //Now command the USB device to reset itself. This would normally switch it out //of USB HID bootloader mode, into application run mode (although the exact //behavior will depend upon how the bootloader firmware is implemented and what is uses //as an entry method into the bootloader mode). comm->Reset(); //Exit the application now and return '0', which indicates success to the caller. exit(0); } else { //Failure during verify. Return something other than '0' to let the caller //know something unexpected happened. printf(" Operation FAILED!"); exit(-366); } } else { //The device isn't in bootloader mode or otherwise couldn't be connected with. //In this scenario, we should promptly exit with failure status, so the automated caller //knows something is wrong promptly (rather than launching the full GUI and waiting indefinitely //for a human to do something). printf("\r\nDevice was not found. Exiting application..."); exit(-367); } } } } MainWindow::~MainWindow() { QSettings settings; settings.beginGroup("WriteOptions"); settings.setValue("writeFlash", writeFlash); settings.setValue("writeEeprom", writeEeprom); settings.endGroup(); comm->close(); setBootloadEnabled(false); delete timer; delete ui; delete comm; delete deviceData; delete hexData; delete device; } void MainWindow::Connection(void) { bool currStatus = comm->isConnected(); comm->PollUSB(DeviceVID, DevicePID); if(currStatus != comm->isConnected()) { if(comm->isConnected()) { qWarning("Attempting to open device..."); comm->open(DeviceVID, DevicePID); ui->plainTextEdit->setPlainText("Device Attached."); ui->plainTextEdit->appendPlainText("Connecting..."); GetQuery(); } else { qWarning("Closing device."); comm->close(); ui->plainTextEdit->setPlainText("Device Detached."); ui->lblDeviceConnected->setText("Not Connected"); ui->lblDeviceConnected->setStyleSheet("QLabel { background-color : red; }"); ui->lblDeviceStatusMsg->setText("Device not detected. Please connect the dancepad interface's USB cable to this computer."); hexOpen = false; setBootloadEnabled(false); emit SetProgressBar(0); } } } void MainWindow::setBootloadEnabled(bool enable) { ui->groupFlashFirmware->setEnabled(enable && hexOpen); ui->groupFirmwareFile->setEnabled(enable); ui->btnResetDevice->setEnabled(enable); ui->progressBar->reset(); if (!enable) { ui->lblFlashStatus->setText("Not ready."); ui->lblDeviceBootloaderVer->setText(""); ui->lblDeviceAppFWVer->setText(""); } } void MainWindow::setBootloadBusy(bool busy) { if(busy) { QApplication::setOverrideCursor(Qt::BusyCursor); timer->stop(); } else { QApplication::restoreOverrideCursor(); timer->start(1000); } ui->btnFlashFirmware->setEnabled(!busy && hexOpen); ui->groupFirmwareFile->setEnabled(!busy); ui->btnResetDevice->setEnabled(!busy); } void MainWindow::IoWithDeviceStart(QString msg) { ui->plainTextEdit->appendPlainText(msg); setBootloadBusy(true); } //Useful for adding lines of text to the main window from other threads. void MainWindow::AppendStringToTextbox(QString msg) { ui->plainTextEdit->appendPlainText(msg); } void MainWindow::UpdateProgressBar(int newValue) { ui->progressBar->setValue(newValue); } void MainWindow::IoWithDeviceComplete(QString msg, Comm::ErrorCode result, double time) { QTextStream ss(&msg); switch(result) { case Comm::Success: ss << " Complete (" << time << "s)\n"; setBootloadBusy(false); break; case Comm::NotConnected: ss << " Failed. Device not connected.\n"; setBootloadBusy(false); break; case Comm::Fail: ss << " Failed.\n"; setBootloadBusy(false); break; case Comm::IncorrectCommand: ss << " Failed. Unable to communicate with device.\n"; setBootloadBusy(false); break; case Comm::Timeout: ss << " Timed out waiting for device (" << time << "s)\n"; setBootloadBusy(false); break; default: break; } ui->plainTextEdit->appendPlainText(msg); if (result != Comm::Success) { ui->lblFlashStatus->setText(*ss.string()); QMessageBox::critical(this, "Error", *ss.string()); } } void MainWindow::IoWithDeviceStateChange(QString msg) { ui->lblFlashStatus->setText(msg); } void MainWindow::PromptUserToReset() { QMessageBox::StandardButton resButton; resButton = QMessageBox::information(this, "Firmware was sucessfully flashed", "Reset device now?" "(Note: Device must be reset or physically disconnected and " "reconnected before it can be used.)", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); //reset device if user chose to if (resButton == QMessageBox::Yes) on_btnResetDevice_clicked(); } //Routine that verifies the contents of the non-voltaile memory regions in the device, after an erase/programming cycle. //This function requests the memory contents of the device, then compares it against the parsed .hex file data to make sure //The locations that got programmed properly match. Comm::ErrorCode MainWindow::VerifyDevice() { Comm::ErrorCode result; DeviceData::MemoryRange deviceRange, hexRange; QTime elapsed; unsigned int i, j; bool failureDetected = false; unsigned char flashData[MAX_ERASE_BLOCK_SIZE]; unsigned char hexEraseBlockData[MAX_ERASE_BLOCK_SIZE]; uint32_t startOfEraseBlock; uint32_t errorAddress = 0; uint16_t expectedResult = 0; uint16_t actualResult = 0; //Initialize an erase block sized buffer with 0xFF. //Used later for post SIGN_FLASH verify operation. memset(&hexEraseBlockData[0], 0xFF, MAX_ERASE_BLOCK_SIZE); emit IoWithDeviceStarted("Verifying Device..."); elapsed.start(); foreach(deviceRange, deviceData->ranges) { if(deviceRange.type == PROGRAM_MEMORY) { result = comm->GetData(deviceRange.start, device->bytesPerPacket, device->bytesPerAddressFLASH, device->bytesPerWordFLASH, deviceRange.end, deviceRange.pDataBuffer); if(result != Comm::Success) { failureDetected = true; qWarning("Error reading device."); } //Search through all of the programmable memory regions from the parsed .hex file data. //For each of the programmable memory regions found, if the region also overlaps a region //that was included in the device programmed area (which just got read back with GetData()), //then verify both the parsed hex contents and read back data match. foreach(hexRange, hexData->ranges) { if(deviceRange.start == hexRange.start) { //For this entire programmable memory address range, check to see if the data read from the device exactly //matches what was in the hex file. for(i = deviceRange.start; i < deviceRange.end; i++) { //For each byte of each device address (1 on PIC18, 2 on PIC24, since flash memory is 16-bit WORD array) for(j = 0; j < device->bytesPerAddressFLASH; j++) { //Check if the device response data matches the data we parsed from the original input .hex file. if(deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j] != hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j]) { //A mismatch was detected. //Check if this is a PIC24 device and we are looking at the "phantom byte" //(upper byte [j = 1] of odd address [i%2 == 1] 16-bit flash words). If the hex data doesn't match //the device (which should be = 0x00 for these locations), this isn't a real verify //failure, since value is a don't care anyway. This could occur if the hex file imported //doesn't contain all locations, and we "filled" the region with pure 0xFFFFFFFF, instead of 0x00FFFFFF //when parsing the hex file. if((device->family == Device::PIC24) && ((i % 2) == 1) && (j == 1)) { //Not a real verify failure, phantom byte is unimplemented and is a don't care. } else { //If the data wasn't a match, and this wasn't a PIC24 phantom byte, then if we get //here this means we found a true verify failure. failureDetected = true; if(device->family == Device::PIC24) { qWarning("Device: 0x%x Hex: 0x%x", *(uint16_t*)&deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j], *(uint16_t*)&hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j]); } else { qWarning("Device: 0x%x Hex: 0x%x", deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j], hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j]); } qWarning("Failed verify at address 0x%x", i); emit IoWithDeviceCompleted("Verify", Comm::Fail, ((double)elapsed.elapsed()) / 1000); return Comm::Fail; } }//if(deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j] != hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j]) }//for(j = 0; j < device->bytesPerAddressFLASH; j++) }//for(i = deviceRange.start; i < deviceRange.end; i++) }//if(deviceRange.start == hexRange.start) }//foreach(hexRange, hexData->ranges) //emit IoWithDeviceCompleted("Verify", Comm::Success, ((double)elapsed.elapsed()) / 1000); }//if(writeFlash && (deviceRange.type == PROGRAM_MEMORY)) else if(writeEeprom && (deviceRange.type == EEPROM_MEMORY)) { result = comm->GetData(deviceRange.start, device->bytesPerPacket, device->bytesPerAddressEEPROM, device->bytesPerWordEEPROM, deviceRange.end, deviceRange.pDataBuffer); if(result != Comm::Success) { failureDetected = true; qWarning("Error reading device."); //emit IoWithDeviceCompleted("Verifying Device's EEPROM Memory", result, ((double)elapsed.elapsed()) / 1000); } //Search through all of the programmable memory regions from the parsed .hex file data. //For each of the programmable memory regions found, if the region also overlaps a region //that was included in the device programmed area (which just got read back with GetData()), //then verify both the parsed hex contents and read back data match. foreach(hexRange, hexData->ranges) { if(deviceRange.start == hexRange.start) { //For this entire programmable memory address range, check to see if the data read from the device exactly //matches what was in the hex file. for(i = deviceRange.start; i < deviceRange.end; i++) { //For each byte of each device address (only 1 for EEPROM byte arrays, presumably 2 for EEPROM WORD arrays) for(j = 0; j < device->bytesPerAddressEEPROM; j++) { //Check if the device response data matches the data we parsed from the original input .hex file. if(deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressEEPROM)+j] != hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressEEPROM)+j]) { //A mismatch was detected. failureDetected = true; qWarning("Device: 0x%x Hex: 0x%x", deviceRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j], hexRange.pDataBuffer[((i - deviceRange.start) * device->bytesPerAddressFLASH)+j]); qWarning("Failed verify at address 0x%x", i); emit IoWithDeviceCompleted("Verify EEPROM Memory", Comm::Fail, ((double)elapsed.elapsed()) / 1000); return Comm::Fail; } } } } }//foreach(hexRange, hexData->ranges) //emit IoWithDeviceCompleted("Verifying", Comm::Success, ((double)elapsed.elapsed()) / 1000); }//else if(writeEeprom && (deviceRange.type == EEPROM_MEMORY)) else { continue; } }//foreach(deviceRange, deviceData->ranges) if(failureDetected == false) { //Successfully verified all regions without error. //If this is a v1.01 or later device, we now need to issue the SIGN_FLASH //command, and then re-verify the first erase page worth of flash memory //(but with the exclusion of the signature WORD address from the verify, //since the bootloader firmware will have changed it to the new/magic //value (probably 0x600D, or "good" in leet speak). if(deviceFirmwareIsAtLeast101 == true) { comm->SignFlash(); qDebug("Expected Signature Address: 0x%x", extendedBootInfo.PIC18.signatureAddress); qDebug("Expected Signature Value: 0x%x", extendedBootInfo.PIC18.signatureValue); //Now re-verify the first erase page of flash memory. if(device->family == Device::PIC18) { startOfEraseBlock = extendedBootInfo.PIC18.signatureAddress - (extendedBootInfo.PIC18.signatureAddress % extendedBootInfo.PIC18.erasePageSize); result = comm->GetData(startOfEraseBlock, device->bytesPerPacket, device->bytesPerAddressFLASH, device->bytesPerWordFLASH, (startOfEraseBlock + extendedBootInfo.PIC18.erasePageSize), &flashData[0]); if(result != Comm::Success) { failureDetected = true; qWarning("Error reading, post signing, flash data block."); } //Search through all of the programmable memory regions from the parsed .hex file data. //For each of the programmable memory regions found, if the region also overlaps a region //that is part of the erase block, copy out bytes into the hexEraseBlockData[] buffer, //for re-verification. foreach(hexRange, hexData->ranges) { //Check if any portion of the range is within the erase block of interest in the device. if((hexRange.start <= startOfEraseBlock) && (hexRange.end > startOfEraseBlock)) { unsigned int rangeSize = hexRange.end - hexRange.start; unsigned int address = hexRange.start; unsigned int k = 0; //Check every byte in the hex file range, to see if it is inside the erase block of interest for(i = 0; i < rangeSize; i++) { //Check if the current byte we are looking at is inside the erase block of interst if(((address+i) >= startOfEraseBlock) && ((address+i) < (startOfEraseBlock + extendedBootInfo.PIC18.erasePageSize))) { //The byte is in the erase block of interst. Copy it out into a new buffer. hexEraseBlockData[k] = *(hexRange.pDataBuffer + i); //Check if this is a signature byte. If so, replace the value in the buffer //with the post-signing expected signature value, since this is now the expected //value from the device, rather than the value from the hex file... if((address+i) == extendedBootInfo.PIC18.signatureAddress) { hexEraseBlockData[k] = (unsigned char)extendedBootInfo.PIC18.signatureValue; //Write LSB of signature into buffer } if((address+i) == (extendedBootInfo.PIC18.signatureAddress + 1)) { hexEraseBlockData[k] = (unsigned char)(extendedBootInfo.PIC18.signatureValue >> 8); //Write MSB into buffer } k++; } if((k >= extendedBootInfo.PIC18.erasePageSize) || (k >= sizeof(hexEraseBlockData))) break; }//for(i = 0; i < rangeSize; i++) } }//foreach(hexRange, hexData->ranges) //We now have both the hex data and the post signing flash erase block data //in two RAM buffers. Compare them to each other to perform post-signing //verify. for(i = 0; i < extendedBootInfo.PIC18.erasePageSize; i++) { if(flashData[i] != hexEraseBlockData[i]) { failureDetected = true; qWarning("Post signing verify failure."); EraseDevice(); //Send an erase command, to forcibly //remove the signature (which might be valid), since //there was a verify error and we can't trust the application //firmware image integrity. This ensures the device jumps //back into bootloader mode always. errorAddress = startOfEraseBlock + i; expectedResult = hexEraseBlockData[i] + ((uint32_t)hexEraseBlockData[i+1] << 8); //expectedResult = hexEraseBlockData[i]; actualResult = flashData[i] + ((uint32_t)flashData[i+1] << 8); //actualResult = flashData[i]; break; } }//for(i = 0; i < extendedBootInfo.PIC18.erasePageSize; i++) }//if(device->family == Device::PIC18) }//if(deviceFirmwareIsAtLeast101 == true) }//if(failureDetected == false) if(failureDetected == true) { qDebug("Verify failed at address: 0x%x", errorAddress); qDebug("Expected result: 0x%x", expectedResult); qDebug("Actual result: 0x%x", actualResult); emit AppendString("Operation aborted due to error encountered during verify operation."); emit AppendString("Please try the erase/program/verify sequence again."); emit AppendString("If repeated failures are encountered, this may indicate the flash"); emit AppendString("memory has worn out, that the device has been damaged, or that"); emit AppendString("there is some other unidentified problem."); emit IoWithDeviceCompleted("Verify", Comm::Fail, ((double)elapsed.elapsed()) / 1000); //Set flags only used when the program was invoked from the console with extra parameters for auto-program and close behavior. consoleEraseProgramVerifiedOkay = false; consoleInvokedEraseProgramVerifyComplete = true; } else { emit IoWithDeviceCompleted("Verify", Comm::Success, ((double)elapsed.elapsed()) / 1000); emit AppendString("Device verified successfully."); //Set flags only used when the program was invoked from the console with extra parameters for auto-program and close behavior. consoleEraseProgramVerifiedOkay = true; consoleInvokedEraseProgramVerifyComplete = true; } return failureDetected ? Comm::Fail : Comm::Success; }//void MainWindow::VerifyDevice() //This thread programs previously parsed .hex file data into the device's programmable memory regions. Comm::ErrorCode MainWindow::WriteDevice(void) { QTime elapsed; Comm::ErrorCode result; DeviceData::MemoryRange hexRange; //Now begin re-programming each section based on the info we obtained when //we parsed the user's .hex file. // NOTE: The bootloader allows writing config bits. This functionality remains in // the bootloader firmware but has been removed from this utility because the end user // will never need to use it. // NOTE 2: The original version of this utility provided a settings dialog to select which // sections of flash memory should be written. This has been removed and this utility will // instead always write only program flash. The ability to write EEPROM has been retained, // however it is unused. emit IoWithDeviceStarted("Writing Device..."); elapsed.start(); foreach(hexRange, hexData->ranges) { if(hexRange.type == PROGRAM_MEMORY) { result = comm->Program(hexRange.start, device->bytesPerPacket, device->bytesPerAddressFLASH, device->bytesPerWordFLASH, device->family, hexRange.end, hexRange.pDataBuffer); } else if(writeEeprom && (hexRange.type == EEPROM_MEMORY)) { result = comm->Program(hexRange.start, device->bytesPerPacket, device->bytesPerAddressEEPROM, device->bytesPerWordEEPROM, device->family, hexRange.end, hexRange.pDataBuffer); } else { continue; } if(result != Comm::Success) { qWarning("Programming failed"); return result; } } emit IoWithDeviceCompleted("Write", result, ((double)elapsed.elapsed()) / 1000); return Comm::Success; } void MainWindow::on_checkBoxAdvancedMode_stateChanged(int state) { int heightShrinkSize = ui->groupBoxAdvanced->height() + ui->groupBoxAdvanced->parentWidget()->layout()->spacing(); QSize cWidgetFinalSize(ui->centralWidget->width(), ui->centralWidget->height() - heightShrinkSize); QSize windowFinalSize(width(), height() - heightShrinkSize); ui->groupBoxAdvanced->setVisible(state != 0); ui->centralWidget->updateGeometry(); // handle adjustment of window height when advanced controls are hidden if (state == 0) { QTimer::singleShot(0, ui->centralWidget, [this,cWidgetFinalSize,windowFinalSize]() { ui->centralWidget->resize(cWidgetFinalSize); resize(windowFinalSize); }); } } Comm::ErrorCode MainWindow::EraseDevice(void) { QTime elapsed; Comm::ErrorCode result; Comm::BootInfo bootInfo; emit IoWithDeviceStarted("Erasing Device... (no status update until complete, may take several seconds)"); elapsed.start(); result = comm->Erase(); if(result != Comm::Success) { emit IoWithDeviceCompleted("Erase", result, ((double)elapsed.elapsed()) / 1000); return result; } result = comm->ReadBootloaderInfo(&bootInfo); emit IoWithDeviceCompleted("Erase", result, ((double)elapsed.elapsed()) / 1000); return result; } //Executes when the user clicks the open hex file button on the main form. void MainWindow::on_btnFwBrowse_clicked() { QString msg, newFileName; QTextStream stream(&msg); //Create an open file dialog box, so the user can select a .hex file. newFileName = QFileDialog::getOpenFileName(this, "Open Hex File", fileName, "Hex Files (*.hex *.ehx)"); if(newFileName.isEmpty()) { return; } ui->txtFirmwareFile->setText(newFileName); LoadFile(newFileName); } void MainWindow::LoadFile(QString newFileName) { QString msg; QTextStream stream(&msg); QFileInfo nfi(newFileName); QApplication::setOverrideCursor(Qt::BusyCursor); HexImporter import; HexImporter::ErrorCode result; int i; hexData->ranges.clear(); //Print some debug info to the debug window. qDebug("Total Programmable Regions Reported by Device: %d", deviceData->ranges.count()); //First duplicate the deviceData programmable region list and //allocate some RAM buffers to hold the hex data that we are about to import. for(i = 0; i < deviceData->ranges.count(); i++) { //Allocate some RAM for the hex file data we are about to import. //Initialize all bytes of the buffer to 0xFF, the default unprogrammed memory value, //which is also the "assumed" value, if a value is missing inside the .hex file, but //is still included in a programmable memory region. DeviceData::MemoryRange HexBuffer = deviceData->ranges[i]; //Allocate a new RAM buffer to hold the .hex file contents associated with the programmable region in the microcontroller. HexBuffer.pDataBuffer = new unsigned char[deviceData->ranges[i].dataBufferLength]; //Initialize the assumed .hex contents buffer to 0xFF, or the values read from the microcontroller initally. //Any contents found in the .hex will subsequently overwrite these values with the new values from the .hex file. memset(HexBuffer.pDataBuffer, 0xFF, HexBuffer.dataBufferLength); if(deviceData->ranges[i].type == CONFIG_MEMORY) { //Initialize our local RAM buffer (intended for storing .hex file read in data) with //values that we obtained by reading from the device. //When the .hex file is opened/parsed, the values that overlap with what we read from //the device, but also existin the .hex file, will get overwritten //with the values from the hex file. However, values that are not in the .hex file, will //remain the same as what we read from the device initially, prior to an erase/program/verify //operation. This is done, since it is possible to have a .hex file that is incomplete, and //only includes some config bit values, but not others (which may still be in the silicon, or could be an unimplemented location). //In these cases, in these special locations, during the erase/program/verify, we want to program into the device //the same values that already exist in the silicon, at these extra locations. if(deviceData->ranges[i].pDataBuffer != 0) { unsigned int copySize; copySize = deviceData->ranges[i].dataBufferLength; if(HexBuffer.dataBufferLength < copySize) { copySize = HexBuffer.dataBufferLength; } memcpy(HexBuffer.pDataBuffer, deviceData->ranges[i].pDataBuffer, copySize); } //comm->GetData(bootInfo.memoryRegions[i].address, bootInfo.bytesPerPacket, device->bytesPerAddressConfig, device->bytesPerWordConfig, bootInfo.memoryRegions[i].address + (range.dataBufferLength / device->bytesPerAddressConfig), &range.pDataBuffer[0]); } hexData->ranges.append(HexBuffer); //Print info regarding the programmable memory region to the debug window. qDebug("Device Programmable Region: [0x%X - 0x%X]", HexBuffer.start, HexBuffer.end); } //Import the hex file data into the hexData->ranges[].pDataBuffer buffers. result = import.ImportHexFile(newFileName, hexData, device); //Based on the result of the hex file import operation, decide how to proceed. switch(result) { case HexImporter::Success: break; case HexImporter::CouldNotOpenFile: QApplication::restoreOverrideCursor(); stream << "Error: Could not open file " << nfi.fileName() << "\n"; ui->plainTextEdit->appendPlainText(msg); return; case HexImporter::NoneInRange: QApplication::restoreOverrideCursor(); stream << "No address within range in file: " << nfi.fileName() << ". Verify the correct firmware image was specified and is designed for your device.\n"; ui->plainTextEdit->appendPlainText(msg); return; case HexImporter::ErrorInHexFile: QApplication::restoreOverrideCursor(); stream << "Error in hex file. Please make sure the firmware image supplied was designed for the device to be programmed. \n"; ui->plainTextEdit->appendPlainText(msg); return; case HexImporter::InsufficientMemory: QApplication::restoreOverrideCursor(); stream << "Memory allocation failed. Please close other applications to free up system RAM and try again. \n"; ui->plainTextEdit->appendPlainText(msg); return; default: QApplication::restoreOverrideCursor(); stream << "Failed to import: " << result << "\n"; ui->plainTextEdit->appendPlainText(msg); return; } fileName = newFileName; watchFileName = newFileName; stream.setIntegerBase(10); msg.clear(); QFileInfo fi(fileName); QString name = fi.fileName(); stream << "Opened: " << name << "\n"; ui->plainTextEdit->appendPlainText(msg); ui->lblFlashStatus->setText("Ready."); hexOpen = true; setBootloadEnabled(true); QApplication::restoreOverrideCursor(); return; } void MainWindow::openRecentFile(void) { QAction *action = qobject_cast(sender()); if (action) { LoadFile(action->data().toString()); } } void MainWindow::GetQuery() { QTime totalTime; Comm::BootInfo bootInfo; DeviceData::MemoryRange range; QString connectMsg; QTextStream ss(&connectMsg); qDebug("Executing GetQuery() command."); totalTime.start(); if(!comm->isConnected()) { qWarning("Query not sent, device not connected"); return; } //Send the Query command to the device over USB, and check the result status. switch(comm->ReadBootloaderInfo(&bootInfo)) { case Comm::Fail: case Comm::IncorrectCommand: ui->plainTextEdit->appendPlainText("Unable to communicate with device\n"); return; case Comm::Timeout: ss << "Operation timed out"; break; case Comm::Success: wasBootloaderMode = true; ss << "Device Ready"; ui->lblDeviceConnected->setText("Connected"); ui->lblDeviceConnected->setStyleSheet("QLabel { background-color : green; }"); ui->lblDeviceStatusMsg->setText(""); break; default: return; } ss << " (" << (double)totalTime.elapsed() / 1000 << "s)\n"; ui->plainTextEdit->appendPlainText(connectMsg); deviceData->ranges.clear(); //Now start parsing the bootInfo packet to learn more about the device. The bootInfo packet contains //contains the query response data from the USB device. We will save these values into globabl variables //so other parts of the application can use the info when deciding how to do things. device->family = (Device::Families) bootInfo.deviceFamily; device->bytesPerPacket = bootInfo.bytesPerPacket; //Set some processor family specific global variables that will be used elsewhere (ex: during program/verify operations). switch(device->family) { case Device::PIC18: device->bytesPerWordFLASH = 2; device->bytesPerAddressFLASH = 1; break; default: device->bytesPerWordFLASH = 2; device->bytesPerAddressFLASH = 1; break; } //Initialize the deviceData buffers and length variables, with the regions that the firmware claims are //reprogrammable. We will need this information later, to decide what part(s) of the .hex file we //should look at/try to program into the device. Data sections in the .hex file that are not included //in these regions should be ignored. for(int i = 0; i < MAX_DATA_REGIONS; i++) { if(bootInfo.memoryRegions[i].type == END_OF_TYPES_LIST) { //Before we quit, check the special versionFlag byte, //to see if the bootloader firmware is at least version 1.01. //If it is, then it will support the extended query command. //If the device is based on v1.00 bootloader firmware, it will have //loaded the versionFlag location with 0x00, which was a pad byte. if(bootInfo.versionFlag == BOOTLOADER_V1_01_OR_NEWER_FLAG) { deviceFirmwareIsAtLeast101 = true; qDebug("Device bootloader firmware is v1.01 or newer and supports Extended Query."); //Now fetch the extended query information packet from the USB firmware. comm->ReadExtendedQueryInfo(&extendedBootInfo); qDebug("Device bootloader firmware version is: %d", extendedBootInfo.PIC18.bootloaderVersion); } else { deviceFirmwareIsAtLeast101 = false; } break; } //Error check: Check the firmware's reported size to make sure it is sensible. This ensures //we don't try to allocate ourselves a massive amount of RAM (capable of crashing this PC app) //if the firmware claimed an improper value. if(bootInfo.memoryRegions[i].size > MAXIMUM_PROGRAMMABLE_MEMORY_SEGMENT_SIZE) { bootInfo.memoryRegions[i].size = MAXIMUM_PROGRAMMABLE_MEMORY_SEGMENT_SIZE; } //Parse the bootInfo response packet and allocate ourselves some RAM to hold the eventual data to program. if(bootInfo.memoryRegions[i].type == PROGRAM_MEMORY) { range.type = PROGRAM_MEMORY; range.dataBufferLength = bootInfo.memoryRegions[i].size * device->bytesPerAddressFLASH; range.pDataBuffer = new unsigned char[range.dataBufferLength]; memset(&range.pDataBuffer[0], 0xFF, range.dataBufferLength); } else if(bootInfo.memoryRegions[i].type == EEPROM_MEMORY) { range.type = EEPROM_MEMORY; range.dataBufferLength = bootInfo.memoryRegions[i].size * device->bytesPerAddressEEPROM; range.pDataBuffer = new unsigned char[range.dataBufferLength]; memset(&range.pDataBuffer[0], 0xFF, range.dataBufferLength); } else if(bootInfo.memoryRegions[i].type == CONFIG_MEMORY) { range.type = CONFIG_MEMORY; range.dataBufferLength = bootInfo.memoryRegions[i].size * device->bytesPerAddressConfig; range.pDataBuffer = new unsigned char[range.dataBufferLength]; memset(&range.pDataBuffer[0], 0xFF, range.dataBufferLength); //Read in the current config bit values from the device, and initialize our local RAM //buffer to contain the same values. When the .hex file is opened/parsed, the values //that overlap with what we read from the device and what is in the .hex file will get overwritten //with the values from the hex file. However, values that are not in the .hex file, will //remain the same as what we read from the device initially, prior to an erase/program/verify //operation. This is done, since it is possible to have a .hex file that is incomplete, and //only include some config bit values, but not others. In these cases, we want to program into //the device the same values for these extra locations (not in the .hex, but present in the silicon), //as were present in the device, prior to doing the erase/program/verify operation. comm->GetData(bootInfo.memoryRegions[i].address, bootInfo.bytesPerPacket, device->bytesPerAddressConfig, device->bytesPerWordConfig, bootInfo.memoryRegions[i].address + (range.dataBufferLength / device->bytesPerAddressConfig), &range.pDataBuffer[0]); } //Notes regarding range.start and range.end: The range.start is defined as the starting address inside //the USB device that will get programmed. For example, if the bootloader occupies 0x000-0xFFF flash //memory addresses (ex: on a PIC18), then the starting bootloader programmable address would typically //be = 0x1000 (ex: range.start = 0x1000). //The range.end is defined as the last address that actually gets programmed, plus one, in this programmable //region. For example, for a 64kB PIC18 microcontroller, the last implemented flash memory address //is 0xFFFF. If the last 1024 bytes are reserved by the bootloader (since that last page contains the config //bits for instance), then the bootloader firmware may only allow the last address to be programmed to //be = 0xFBFF. In this scenario, the range.end value would be = 0xFBFF + 1 = 0xFC00. //When this application uses the range.end value, it should be aware that the actual address limit of //range.end does not actually get programmed into the device, but the address just below it does. //In this example, the programmed region would end up being 0x1000-0xFBFF (even though range.end = 0xFC00). //The proper code to program this would basically be something like this: //for(i = range.start; i < range.end; i++) //{ // //Insert code here that progams one device address. Note: for PIC18 this will be one byte for flash memory. // //For PIC24 this is actually 2 bytes, since the flash memory is addressed as a 16-bit word array. //} //In the above example, the for() loop exits just before the actual range.end value itself is programmed. range.start = bootInfo.memoryRegions[i].address; range.end = bootInfo.memoryRegions[i].address + bootInfo.memoryRegions[i].size; //Add the new structure+buffer to the list deviceData->ranges.append(range); } ui->lblDeviceBootloaderVer->setText(QString("%1").arg(extendedBootInfo.PIC18.bootloaderVersion, 4, 16, QChar('0'))); QString appVerMsg = (extendedBootInfo.PIC18.applicationVersion > 0) ? QString("%1").arg(extendedBootInfo.PIC18.applicationVersion, 4, 16, QChar('0')) : "No FW on device"; ui->lblDeviceAppFWVer->setText(appVerMsg); // if the user already loaded a hex file, attempt to load it again for the newly connected device if (!ui->txtFirmwareFile->text().isEmpty()) LoadFile(ui->txtFirmwareFile->text()); setBootloadEnabled(true); } void MainWindow::on_btnResetDevice_clicked() { if(!comm->isConnected()) { failed = -1; qWarning("Reset not sent, device not connected"); return; } ui->plainTextEdit->appendPlainText("Resetting..."); comm->Reset(); }