#include "XModem.h" #include "CmdStatus.h" // The original TommyPROM code used XModem CRC protocol but this requires parameters to be // changed for the host communication program. The default is now to use basic XModem // with the 8-bit checksum instead of the 16-bit CRC error checking. This shouldn't // matter because communication errors aren't likely to happen on a 3 foot USB cable like // they did over the long distance dail-up lines that XModem was designed for. Uncomment // the XMODEM_CRC_PROTOCOL line below to restore the original XMODEM CRC support. //#define XMODEM_CRC_PROTOCOL enum { // XMODEM control characters. XMDM_SOH = 0x01, XMDM_EOT = 0x04, XMDM_ACK = 0x06, XMDM_NAK = 0x15, XMDM_CAN = 0x18, XMDM_ESC = 0x1b, XMDM_CRC = 'C', #ifdef XMODEM_CRC_PROTOCOL XMDM_TRANSFER_START = XMDM_CRC #else XMDM_TRANSFER_START = XMDM_NAK #endif }; enum { // Misc constants for XMODEM. PKTLEN = 128 }; enum { RX_OK, RX_ERROR, RX_IGNORE }; uint32_t XModem::ReceiveFile(uint32_t address) { uint8_t buffer[PKTLEN]; int c; uint8_t seq = 1; uint32_t numBytes = 0; bool complete = false; int status; #ifdef XMODEM_CRC_PROTOCOL Serial.println(F("Send the image file using XMODEM CRC")); #else Serial.println(F("Send the image file using XMODEM")); #endif if (!StartReceive()) { cmdStatus.error("Timeout waiting for transfer to start."); return 0; } while (!complete) { if ((c = GetChar()) < 0) { cmdStatus.error("Timeout waiting for start of next packet."); cmdStatus.setValueDec(0, "seq", seq); return 0; } switch (c) { case XMDM_SOH: // Start of a packet status = ReceivePacket(buffer, PKTLEN, seq, address); if (status == RX_OK) { // Good packet - advance the buffer numBytes += PKTLEN; address += PKTLEN; ++seq; } else if (status == RX_ERROR) { return 0; } // else ignore the duplicate packet break; case XMDM_EOT: // End of transfer Serial.write(XMDM_ACK); complete = true; break; case XMDM_CAN: case XMDM_ESC: // Cancel from sender cmdStatus.error("Transfer canceled by sender."); cmdStatus.setValueDec(0, "seq", seq); return 0; break; default: // Fail the transfer on anything else cmdStatus.error("Unexpected character received waiting for next packet."); cmdStatus.setValueDec(0, "char", c); cmdStatus.setValueDec(1, "seq", seq); return 0; break; } } return numBytes; } // This method it not very tolerant of communication errors. If the receiver // does not send a positive ACK for each packet or does not ACK the packet // within one second then the transfer will fail. Unlike in the dial-up // days of old, this is designed to be run on a 3 foot cable betwee two fast // hosts, so communication errors or timeouts are extremely unlikely. bool XModem::SendFile(uint32_t address, uint32_t fileSize) { uint8_t seq = 1; int rxChar = -1; uint32_t bytesSent = 0; #ifdef XMODEM_CRC_PROTOCOL Serial.println(F("Set the terminal to receive XModem CRC")); #else Serial.println(F("Set the terminal to receive XModem")); #endif while (rxChar == -1) { rxChar = GetChar(); } if (rxChar != XMDM_TRANSFER_START) { #ifdef XMODEM_CRC_PROTOCOL cmdStatus.error("Expected XModem CRC start char"); #else cmdStatus.error("Expected XModem NAK char to start"); #endif cmdStatus.setValueDec(0, "char", rxChar); return false; } while (bytesSent < fileSize) { SendPacket(address, seq++); address += PKTLEN; rxChar = GetChar(5000); if (rxChar != XMDM_ACK) { cmdStatus.error("Expected XModem ACK."); cmdStatus.setValueDec(0, "char", rxChar); return false; } bytesSent += PKTLEN; } // Signal end of transfer and process the ACK Serial.write(XMDM_EOT); rxChar = GetChar(5000); if (rxChar != XMDM_ACK) { cmdStatus.error("Expected XModem ACK to EOT, but received:"); cmdStatus.setValueDec(0, "char", rxChar); return false; } return true; } void XModem::Cancel() { // Send a cancel and then eat input until the line is quiet for 3 seconds. Serial.write(XMDM_CAN); while (GetChar(3000) != -1) {} } // Private functions int XModem::GetChar(int msWaitTime) { msWaitTime *= 4; do { if (Serial.available() > 0) { return Serial.read(); } delayMicroseconds(250); } while (msWaitTime--); return -1; } uint16_t XModem::UpdateCrc(uint16_t crc, uint8_t data) { #ifdef XMODEM_CRC_PROTOCOL crc = crc ^ ((uint16_t)data << 8); for (int ix = 0; (ix < 8); ix++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } #else crc = (crc + data) & 0xff; #endif return crc; } bool XModem::StartReceive() { delay(2000); for (int retries = 20; (retries); --retries) { // Send the XMDM_TRANSFER_START until the sender of the file responds with // something. The start character will be sent once a second for a number of // seconds. If nothing is received in that time then return false to indicate // that the transfer did not start. ++promDevice.debugRxStarts; Serial.write(XMDM_TRANSFER_START); for (unsigned ms = 30000; (ms); --ms) { delayMicroseconds(100); if (Serial.available() > 0) { return true; } } } return false; } int XModem::ReceivePacket(uint8_t buffer[], unsigned bufferSize, uint8_t seq, uint32_t destAddr) { int c; uint8_t rxSeq1, rxSeq2; uint16_t calcCrc = 0; uint16_t rxCrc; rxSeq1 = (uint8_t) GetChar(); rxSeq2 = (uint8_t) GetChar(); for (unsigned ix = 0; (ix < bufferSize); ix++) { if ((c = GetChar()) < 0) { // If the read times out then fail this packet. Note that this check isn't // done for the sequence and CRC. If they timeout then the values won't match // so there is not point in the extra code to check for the error. The worst // that will happen is that the transfer will need to wait 3 timeouts before // realizing that something is wrong. cmdStatus.error("Timeout waiting for next packet char."); cmdStatus.setValueDec(0, "seq", seq); Serial.write(XMDM_CAN); return RX_ERROR; } buffer[ix] = (uint8_t) c; calcCrc = UpdateCrc(calcCrc, buffer[ix]); } #ifdef XMODEM_CRC_PROTOCOL rxCrc = ((uint16_t) GetChar()) << 8; rxCrc |= GetChar(); #else rxCrc = GetChar(); #endif // At this point in the code, there should not be anything in the receive buffer // because the sender has just sent a complete packet and is waiting on a response. If // the XModem transfer is not started quickly on the host side, TeraTerm seems to // buffer the NAK character that starts the transfer and use it as an indication that // the first packet should be sent again. That can cause the Arduino's 64 byte buffer // to overflow and gets the transfer out of sync. The normal processing will detect // the first packet and will process it correctly, but the partial packet in the // buffer will cause the processing of the next packet to fail. delay(10); if (Serial.available()) { ++promDevice.debugRxSyncErrors; if (seq == 1) { // If any characters are in the buffer after the first packet, quietly eat // them to get back on a packet boundary and send a NAK to cause a retransmit // of the packet. while (Serial.available() > 0) { delay(1); Serial.read(); ++promDevice.debugExtraChars; } delay(1); Serial.write(XMDM_NAK); return RX_IGNORE; } else { // Sync issues shouldn't happen at any other time so fail the transfer. cmdStatus.error("RX sync error."); cmdStatus.setValueDec(0, "seq", seq); cmdStatus.setValueDec(1, "rxSeq1", rxSeq1); cmdStatus.setValueDec(2, "rxSeq2", rxSeq2); cmdStatus.setValueHex(3, "calcCrc", calcCrc); cmdStatus.setValueHex(4, "rxCrc", rxCrc); Serial.write(XMDM_CAN); return RX_ERROR; } } if ((calcCrc == rxCrc) && (rxSeq1 == seq - 1) && ((rxSeq1 ^ rxSeq2) == 0xff)) { // Resend of previously processed packet. delay(10); ++promDevice.debugRxDuplicates; Serial.write(XMDM_ACK); return RX_IGNORE; } else if ((calcCrc != rxCrc) || (rxSeq1 != seq) || ((rxSeq1 ^ rxSeq2) != 0xff)) { // Fail if the CRC or sequence number is not correct or if the two received // sequence numbers are not the complement of one another. cmdStatus.error("Bad CRC or sequence number."); cmdStatus.setValueDec(0, "seq", seq); cmdStatus.setValueDec(1, "rxSeq1", rxSeq1); cmdStatus.setValueDec(2, "rxSeq2", rxSeq2); cmdStatus.setValueHex(3, "calcCrc", calcCrc); cmdStatus.setValueHex(4, "rxCrc", rxCrc); Serial.write(XMDM_CAN); return RX_ERROR; } else { // The data is good. Process the packet then ACK it to the sender. if (!promDevice.writeData(buffer, bufferSize, destAddr)) { cmdStatus.error("Write failed"); cmdStatus.setValueHex(0, "address", destAddr); return RX_ERROR; } Serial.write(XMDM_ACK); } return RX_OK; } void XModem::SendPacket(uint32_t address, uint8_t seq) { uint16_t crc = 0; Serial.write(XMDM_SOH); Serial.write(seq); Serial.write(~seq); for (int ix = 0; (ix < PKTLEN); ix++) { byte c = promDevice.readData(address++); Serial.write(c); crc = UpdateCrc(crc, c); } #ifdef XMODEM_CRC_PROTOCOL Serial.write(crc >> 8); #endif Serial.write(crc & 0xff); }