/* ***************************************************************************
FreePicProg V1.10 (12Sep2004)
Copyright (c) 2004 Steven Simon 

This software is open source and is released under the MIT license. For more
information on open source licensing go to http://www.opensource.org

Copyright (c) 2004 Steven Simon

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*************************************************************************** */

// ###########################################################################
// Revision history
// Version 1.00 12 January 2004
// First release
//
// Version 1.10 12 September 2004
// Support for 18f1320

// ###########################################################################
//
// Known bugs/limitations
//
// ###########################################################################
//
// Intermittent failure while programming on WindowsXP.
//
// The WindowsXP parallel port device driver may periodically write to the parallel
// port. If the programmer is active during this period the programming
// will fail.
//
// Solution: Write a Windows parallel port device driver that allows direct
// access to the parallel port bits.
//
// Possible workaround 1: The following registry entry will stop Windows XP
// checking for new printer connections:
// [HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\Parport\Parameters]
// "DisableWarmPoll"=dword:00000001
//
// Possible workaround 2: Disable the parallel port on the motherboard and install
// an old non plug and play parallel port ISA card. Disable the Windows
// parallel port device driver.
// (I have not tested this and it will only work on older PCs with ISA expansion
// slots)
//
// ###########################################################################
//
// Comments and suggestions on possible new features
//
// ###########################################################################
//
//////////////////////////////////////////////////////////////////////////////
// Porting to Unix
//
// The only classes that need major changes are: parallel_io and high_res_delay.
//
// Some of the challenges you may face:
//
// - Most Unix systems including Linux do not have support for high resolution timers.
//   This will make porting the high_res_delay class difficult.
//
// - Accessing the parallel port from user level programs may not be trivial. You will
//   have to use a parallel port device driver. Your target Unix platform may not
//   have a suitable parallel port device driver.
//
//
//////////////////////////////////////////////////////////////////////////////
// Creating a graphical user interface
//
// I have designed this application to easily accomodate a GUI. No major changes to
// the code should be necessary.
//
// These are some of the tasks you will need to do when creating a GUI:
//
// - Replace the main() function with a GUI event handler.
//
// - Write a GUI equivalent of progress_ostream. Your new class should derive from
//   the 'progress' virtual base class.
//
// - A useful feature would be to display the eeprom of a PIC chip in a spreadsheet like
//   table. Create a class that implements the chip_memory_sink interface and stores the
//   PIC eeprom in an array. Pass this class to the engine::read() method.
//
// - You may want to split the source code into multiple files. I have put everything
//   into one file for simplicity. As the source code grows, it may not be feasible
//   to keep everything in one file anymore.
//
//////////////////////////////////////////////////////////////////////////////
// Adding support for more PIC programmers
//
// You will need to create a new driver class for the PIC programmer.
// The only requirement for this driver class is that it must implement all the
// methods in the 'programmer' virtual base class.
//
// For parallel port type programmers you may want to derive from the
// 'programmer_parallel' class.
//
// For serial or USB PIC programmers driven by an onboard microcontroller,
// implement the methods of the 'programmer' virtual base classs on the PIC programmer's
// microcontroller. Devise a remote procedure call mechanism to communicate
// with the PIC programmer's microcontroller.
//
//
//////////////////////////////////////////////////////////////////////////////
// Adding support for more PIC chips
//
// For new 16f and 18f series PIC chips you can derive from the 'engine_16f' and
// 'engine18f' classes.
//
// For other devices you may need to start from scratch and implement the
// 'engine' virtual base class.


// ###########################################################################
//
// A brief description of the main classes
//
// ###########################################################################
//
//////////////////////////////////////////////////////////////////////////////
// class:
//    engine
//
// purpose:
//    implements the programming algorithm for PIC chips
//
// derived classes:
//    engine_16f, engine_18f, engine_12f675, engine_16f628a, engine_16f87x, engine_18fxx2
//
// methods:
//    void erase_chip(progress &)
//       erases the entire memory of the chip
//
//    void erase_program_memory(progress &)
//       erases the program memory of the chip
//       for some devices this will also erases the configuration word
//
//    void erase_data_memory(progress &)
//       erases the data eeprom
//
//    void blank_check(progress &)
//       checks if device is blaank
//
//    void read(chip_memory_sink &, progress &)
//       reads the memory of the chip, uses the chip_memory_sink to store the
//       address and value of the memory locations
//
//    void verify(chip_memory_source &, progress &)
//       verify the memory of the chip against the data read from the chip_memory_source
//
//    void program(chip_memory_source &, progress &)
//       programs the chip with the data sourced from the chip_memory_source
//
//    void print_config(ostream &)
//       prints the config memory to the specified stream
//
//    void print_device_id(ostream &)
//       prints the device id to the specified stream
//
//    void create_test_data(string path, string device_name)
//       creates test files in the directory specified by path
//
//////////////////////////////////////////////////////////////////////////////
// class:
//    programmer
//
// purpose:
//    controls the PIC programmer hardware
//
// derived classes:
//    programmer_parallel, programmer_p16pro40
//
// methods:
//    bool is_connected()
//       returns true if the PIC programmer connected to the computer
//
//    void enter_prog_verify_mode(int pin_count, vpp_vdd_order)
//       switches the chip into program/verify mode
//       the order in which the mclr/vpp and vdd pins are activated depends
//       on the value of vpp_vdd_order
//    
//    void reset_device()
//       exits the program/verify mode
//
//    void send_bits(word value, int bit_count, int clock_time, int start_delay, int last_clock_hold_time)
//       sends a value to the chip
//          value:         bits to send
//          bit_count:     number of bits to send (max 16)
//          clock_time:    clock_period/2 in 100ns units
//          start_delay:   delay before sending bits in 100ns units
//                         (default is 0: no delay)
//          last_clock_hold_time: clock is held high for this period when sending the last bit,
//                         specified in 100ns units
//                         (default is 0: send last bit as the other bits)
//
//    word receive_bits(int bit_count, int clock_time, int start_delay)
//       reads and returns a value from the chip
//          bit_count:     number of bits to read
//          clock_time:    clock_period/2 in 100ns units
//          start_delay:   delay before clocking in bits in 100ns units
//                         (default is 0: no delay)
//
//    void delay(int unit_100ns)
//       waits for the specified amount of time
//      
//
//////////////////////////////////////////////////////////////////////////////
// class:
//    chip_memory_source
//
// purpose:
//    used to source data for programming and verifying the chip
//
// derived classes:
//    inhex_file_reader
//
// methods:
//    bool read_next_byte(dword *address, byte *value)
//       reads the next byte, returns false when there are no more bytes
//
//    bool read_next_word(dword *address, word *value)
//       reads the next word, returns false when there are no more words
//
//
//////////////////////////////////////////////////////////////////////////////
// class:
//    chip_memory_sink
//
// purpose:
//    used to save data read from the chip
//
// derived classes:
//    inhex_file_writer
//
// methods:
//    void write_byte(dword address, byte value)
//       saves the specified byte
//
//    void write_word(dword address, word value)
//       saves the specified word
//
//
//////////////////////////////////////////////////////////////////////////////
// class: parallel_io
//
// purpose:
//    provides operating system dependent paralel i/o functions
//
//
//////////////////////////////////////////////////////////////////////////////
// class: high_res_delay
//
// purpose:
//    provides operating system dependent delay functions
//
//
//////////////////////////////////////////////////////////////////////////////
// class: features_chip
//
// purpose:
//    this is just a data structure, it helps to create and initialize the 'engine' classes
//    stores the following information about a PIC device:
//       - device name
//       - pointer to a function that creates an engine class for this device
//       - program eeprom and data eeprom size
//       - pin count
//
//
//////////////////////////////////////////////////////////////////////////////
// class: progress
//
// purpose:
//    used to provide feedback to the user while executing methods in the
//    engine class
//
// derived classes:
//    progress_ostream
//
//
//////////////////////////////////////////////////////////////////////////////
// class: cex
//
// pupose:
//    static helper class used to create and throw exceptions
//
//
//////////////////////////////////////////////////////////////////////////////
// class: io_bit
//
// purpose:
//    helper class for setting or clearing individul bits on an i/o port
//
//
//////////////////////////////////////////////////////////////////////////////


// ###########################################################################
//
// TODO list for the next version
//
// ###########################################################################
//
// - write a parallel port device driver for windows xp
// - config file:
//    - printer port to use (LPT1, LPT2)
//    - default device to use when one is not specified on the command line
//


// disable warning: "identifier was truncated to '255' characters in the debug information"
//
#pragma warning (disable : 4786)

// disable warning:  C4355: 'this' : used in base member initializer list
//
#pragma warning (disable : 4355)

#include <stdlib.h>
#include <assert.h>

#include <iostream>
#include <fstream>
#include <strstream>
#include <string>
#include <iomanip>
#include <algorithm>
#include <vector>

#include <windows.h>
#include <conio.h>

using namespace std;

typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;


// ***************************************************************************
class cex
{
public:
   cex(string message) { m_error_message = message; };
   string get_error_message() { return m_error_message; };

   ///////////////////////////////////////////////////////////////////////////
   static void throw_error(const char *error_fmt, ...)
   {  
      char sbuf[256];

      va_list argptr;
      va_start(argptr, error_fmt);       

      _vsnprintf(sbuf, 256-1, error_fmt, argptr);
     
      throw cex(string(sbuf));
   };

private:
   string m_error_message;
};

// ***************************************************************************
typedef void (__stdcall *port_write_func_ptr)(ULONG, UCHAR); 
typedef UCHAR (__stdcall *port_read_func_ptr)(ULONG);

class parallel_io
{
private:
   ///////////////////////////////////////////////////////////////////////////
   static port_write_func_ptr m_write_to_port;
   static port_read_func_ptr m_read_from_port;
  
   ///////////////////////////////////////////////////////////////////////////  
   static int m_printer_port;
   static int m_prev_printer_data_value;
   static bool m_done_first_data_write;

public:
   ///////////////////////////////////////////////////////////////////////////
   static void init(int printer_port)
   {
      m_printer_port = printer_port;

      DWORD dwVersion = GetVersion();

      // If OS is WinNT then use dlportio.dll for port i/o
      //
      if (dwVersion < 0x80000000)
      {
         // load dlportio.dll
         //
         HINSTANCE hinstLib = LoadLibrary("dlportio");

         if (hinstLib != NULL) 
         { 
            // by default m_write_to_port and m_read_from_port point to the Win9x direct i/o functions
            //
            m_write_to_port = (port_write_func_ptr) GetProcAddress(hinstLib, "DlPortWritePortUchar"); 
            m_read_from_port = (port_read_func_ptr) GetProcAddress(hinstLib, "DlPortReadPortUchar"); 

            if (m_write_to_port == 0 || m_read_from_port == 0)
            {
               cex::throw_error("Internal error: Cannot find required functions in dlportio.dll");
            }
         }
         else
         {
            cex::throw_error("Error: Cannot find DLPortIO.dll: DLPortIO is required for Windows NT\n"
                             "DLPortIO is avalilable from Scientific Software Tools at www.sstnet.com");
         }
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   static void write_to_port(int port_offset, unsigned char value)
   {
      // compare the value of the parallel port to the previous data value written
      // if they are not equal, most probably the OS or another application has
      // tried to access the parallel port
      //
      // directly writing to the parallel port is unreliable at best
      // all parallel i/o should be done using a custom device driver !!
      //
      if (m_done_first_data_write && port_offset == 0)
      {
         byte b = m_read_from_port(m_printer_port + port_offset);

         if (b != m_prev_printer_data_value)
         {
            char sbuf[1024];
            sbuf[1023] = 0;

            _snprintf(sbuf, 1023,"Error while writing to the parallel port:\n\n"
                                 "It has been detected that Windows or another\n"
                                 "application has accessed the parallel port.\n\n"
                                 "Last value written to the parallel port: 0x%02x\n"
                                 "Value read from the parallel port: 0x%02x\n\n"
                                 "This application will now terminate.",
                                 (unsigned) m_prev_printer_data_value, (unsigned) b);

            MessageBox(0, sbuf, 0, MB_ICONERROR | MB_SYSTEMMODAL);

            exit(1);
         }
      }

      m_write_to_port(m_printer_port + port_offset, value);

      if (port_offset == 0)
      {
         // always remember the previous value written
         //
         m_prev_printer_data_value = value;

         m_done_first_data_write = true;
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   static unsigned char read_from_port(int port_offset)
   {
      if (port_offset == 0 && m_done_first_data_write)
      {
         // returned cached value
         //
         return m_prev_printer_data_value;
      }
      else
      {
         return m_read_from_port(m_printer_port + port_offset);
      }
   };

private:
   ///////////////////////////////////////////////////////////////////////////
   static void __stdcall port_write_direct(ULONG Port, UCHAR Value)
   {
      _outp((unsigned short) Port, (int) Value);
   }

   ///////////////////////////////////////////////////////////////////////////
   static UCHAR __stdcall port_read_direct(ULONG Port)
   {
      return (UCHAR) _inp((unsigned short) Port);
   }
};

// declare and init static member variables
//
int parallel_io::m_printer_port = 0x378;

port_write_func_ptr parallel_io::m_write_to_port = parallel_io::port_write_direct;
port_read_func_ptr parallel_io::m_read_from_port = parallel_io::port_read_direct;

int parallel_io::m_prev_printer_data_value = 0;
bool parallel_io::m_done_first_data_write = false;

// ***************************************************************************
class high_res_delay
{
private:
	static LARGE_INTEGER counter_freq;

public:
   ///////////////////////////////////////////////////////////////////////////
   static void init()
   {
      // this process must run on one processor only
      //
      if (SetThreadAffinityMask(GetCurrentThread(), 1) == 0)
      {
         cex::throw_error("Internal error: SetThreadAffinityMask() failed.");
      }

      // read the timer frequency
      //
      if (QueryPerformanceFrequency(&counter_freq) == 0)
      {
         cex::throw_error("Error: System does not support a high resolution timer.\n"
                          "A Pentium II class CPU is recommended for this application.");
      }

      // printf("counter freq = %I64d\n\n", counter_freq.QuadPart);
   }

   ///////////////////////////////////////////////////////////////////////////
   static void delay(int unit_100ns)
   {
      // init() must be called before calling this function
      //
      assert(counter_freq.QuadPart != 0);

		LARGE_INTEGER count;
		LARGE_INTEGER start_count;

      // calculate number of counter ticks to wait
      //
      __int64 delay_count = (counter_freq.QuadPart * unit_100ns) / (__int64) 10000000;

      // make sure delay count is not zero
      //
      if (delay_count < 1) delay_count = 1;

      // first wait for one "tick" to elapse
      //
      QueryPerformanceCounter(&start_count);
		do
		{
			QueryPerformanceCounter(&count);

		} while (count.QuadPart == start_count.QuadPart);

      // and wait..
      //
      QueryPerformanceCounter(&start_count);          
      do
		{
			QueryPerformanceCounter(&count);

		} while (count.QuadPart - start_count.QuadPart < delay_count);
   };
};

// declare static member variables
//
LARGE_INTEGER high_res_delay::counter_freq;

// ***************************************************************************
class progress
{
public:
   virtual void at_address(word address) = 0;
   virtual void at_address(dword address) = 0;
   virtual void print_message(const char *message_fmt, ...) = 0;
};

// ***************************************************************************
class progress_ostream : public progress
{
private:
   ostream &m_os;
   string m_prefix;

public:
   ///////////////////////////////////////////////////////////////////////////
   progress_ostream(ostream &os, const char *prefix)
      : m_os(os)
   {
      m_prefix = string(prefix);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void at_address(word address)
   {
      long ios_flags = m_os.flags(ios::hex | ios::internal);

      m_os << "\r" << m_prefix << ": 0x" << setfill('0') << setw(4) << address << "         ";

      m_os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void at_address(dword address)
   {
      long ios_flags = m_os.flags(ios::hex | ios::internal);

      m_os << "\r" << m_prefix << ": 0x" << setfill('0') << setw(8) << address << "         ";

      m_os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_message(const char *message_fmt, ...)
   {
      char sbuf[1024];

      va_list argptr;
      va_start(argptr, message_fmt);       

      _vsnprintf(sbuf, 1024-1, message_fmt, argptr);

      m_os << sbuf << endl;
   };

};

// ***************************************************************************
class chip_memory_source
{
public:
   virtual bool read_next_word(dword *address, word *value) = 0;
   virtual bool read_next_byte(dword *address, byte *value) = 0;
};

// ***************************************************************************
class chip_memory_sink
{
public:
   virtual void write_word(dword address, word value) = 0;
   virtual void write_byte(dword address, word value) = 0;
};

// ***************************************************************************
class inhex_file_reader : public chip_memory_source
{
private:
   word m_current_address;
   word m_address_offset;
   byte m_record_count;

   istream &m_is;

public:
   ///////////////////////////////////////////////////////////////////////////
   inhex_file_reader(istream &is) 
      : m_current_address(0), m_address_offset(0), m_record_count(0), m_is(is)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool read_next_byte(dword *address, byte *value)
   {
      assert(address != 0 && value != 0);

      word inhex_address;

      if (get_next_value(&inhex_address, value))
      {
         *address = m_address_offset;

         *address = (*address << 16) | (dword) inhex_address;

         return true;
      }

      return false;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool read_next_word(dword *address, word *value)
   {
      assert(address != 0 && value != 0);

      byte lsb, msb;
      word address_lsb, address_msb;

      if (! get_next_value(&address_lsb, &lsb)) return false;
      if (! get_next_value(&address_msb, &msb)) return false;

      if (address_lsb % 2 != 0 || address_msb != address_lsb + 1)
      {
         cex::throw_error("Error: A two byte word was expected in the input file at address 0x%08x",
               ((unsigned) m_address_offset << 16) | (unsigned) address_lsb);
      }

      *address = m_address_offset;
      *address = ((*address << 16) | address_lsb) / 2;

      *value = msb;
      *value = (*value << 8) | lsb;

      return true;
   };

private:
   ///////////////////////////////////////////////////////////////////////////
   bool get_next_value(word *address, byte *value)
   {
      byte record_type = 0;

      do
      {
         m_is >> ws;

         // is this a new line ?
         //
         if (m_is.peek() == ':')
         {
            assert(m_record_count == 0);

            // skip :
            m_is.ignore();

            // get record count
            m_record_count = get_byte();

            // get address
            word hex_address = get_word();
   
            // get record type
            record_type = get_byte();
         
            switch(record_type)
            {
             // data record
            case 0:               
               m_current_address = hex_address;
               break;

            // end of file record
            case 1:                
               return false;

            // extended linear address record
            case 4:                
               m_address_offset = get_word();

               m_record_count = m_record_count - 2;

               // discard checksum
               (void) get_byte();
               break;

            default:
               cex::throw_error("Error: Unexpected record type found in the input file: %d", (int) record_type);
            }
         }
         else
         {
            m_current_address++;
         }
      }
      while (record_type == 4);

      *value = get_byte();
      *address = m_current_address;

      if (--m_record_count == 0)
      {
         // discard checksum
         (void) get_byte();
      }

      if (! m_is.good())
      {
         cex::throw_error("Error: Unexpected end of file found in the input file.");
      }

      return true;
   };
      
   ///////////////////////////////////////////////////////////////////////////
   byte get_byte()
   {
      char hex8[3];
   
      hex8[0] = m_is.get();
      hex8[1] = m_is.get();
      hex8[2] = 0;

      return (byte) strtoul(hex8, NULL, 16);
   };

   ///////////////////////////////////////////////////////////////////////////
   word get_word()
   {
      char hex16[5];
   
      hex16[0] = m_is.get();
      hex16[1] = m_is.get();
      hex16[2] = m_is.get();
      hex16[3] = m_is.get();
      hex16[4] = 0;

      return (word) strtoul(hex16, NULL, 16);
   };
};

// ***************************************************************************
class inhex_file_writer : public chip_memory_sink
{
private:
   word m_record_address;
   word m_record_address_msb_word;
   word m_address_offset;
   byte m_record_count;
   byte m_record_data[16];

   ostream &m_os;

public:
   ///////////////////////////////////////////////////////////////////////////
   inhex_file_writer(ostream &os)
      : m_record_address(0), m_record_address_msb_word(0), m_record_count(0), m_os(os)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   ~inhex_file_writer()
   {
      if (m_record_count > 0)
      {
         put_data_record();
         put_end_record();
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void write_byte(dword address, word value)
   {
      // split address into msb and lsb words
      //
      word address_msb_word = (address >> 16) & 0xffff;
      word address_lsb_word = address & 0xffff;

      // if msb word has changed
      //
      if (address_msb_word != m_record_address_msb_word)
      {
         put_data_record();

         m_record_count = 0;

         m_record_address_msb_word = address_msb_word;

         m_record_address = address_lsb_word;

         put_extended_linear_address_record();
      }

      // start new line if record count is 16 or the given address is not the next address
      //
      else if (m_record_count == 16 || address_lsb_word != m_record_address + m_record_count)
      {
         put_data_record();

         m_record_count = 0;

         m_record_address = address_lsb_word;
      }

      m_record_data[m_record_count++] = value;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void write_word(dword address, word value)
   {
      assert(address < 0x80000000);

      write_byte(address * 2, (value & 0x00FF));
      write_byte(address * 2 + 1, (value >> 8));
   };

private:
   ///////////////////////////////////////////////////////////////////////////
   void put_data_record()
   {
      if (m_record_count > 0)
      {
         byte checksum = 0;

         // start code
         m_os << ':';
      
         // record count
         checksum += put_hex_byte(m_record_count);

         // address
         checksum += put_hex_byte((byte) (m_record_address >> 8));
         checksum += put_hex_byte((byte) (m_record_address & 0x00FF));

         // record type
         checksum += put_hex_byte(0);

         // data
         for (int i = 0; i < m_record_count; i++)
         {
            checksum += put_hex_byte(m_record_data[i]);
         }

         // checksum
         (void) put_hex_byte( (~checksum) + 1);

         // and a line feed
         m_os << endl;

         // check state of output stream
         if (! m_os.good()) cex::throw_error("Error: Write to output file failed");
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   void put_extended_linear_address_record()
   {
      byte checksum = 0;

      // start code
      m_os << ':';
   
      // record count
      checksum += put_hex_byte(2);

      // address field always zero
      checksum += put_hex_byte(0);
      checksum += put_hex_byte(0);

      // record type is 4
      checksum += put_hex_byte(4);

      // msb of long address
      checksum += put_hex_byte((byte) (m_record_address_msb_word >> 8));
      checksum += put_hex_byte((byte) (m_record_address_msb_word & 0x00FF));

      // checksum
      (void) put_hex_byte( (~checksum) + 1);

      // and a line feed
      m_os << endl;

      // check state of output stream
      if (! m_os.good()) cex::throw_error("Error: Write to output file failed");
   };
   
   ///////////////////////////////////////////////////////////////////////////
   void put_end_record()
   {
      m_os << ":00000001FF" << endl;

      // check state of output stream
      if (! m_os.good()) cex::throw_error("Error: Write to output file failed");
   };

   ///////////////////////////////////////////////////////////////////////////
   byte put_hex_byte(byte value)
   {
      byte ms_nibble = value >> 4;
      byte ls_nibble = value & 0x0F;

      m_os << (char) (ms_nibble <= 9 ? ms_nibble + 0x30 : ms_nibble + 0x37);
      m_os << (char) (ls_nibble <= 9 ? ls_nibble + 0x30 : ls_nibble + 0x37);

      return value;
   };
};

// ***************************************************************************
class programmer
{
public:
   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_connected() = 0;

   ///////////////////////////////////////////////////////////////////////////
   enum vpp_vdd_order {vdd_first, vpp_first};

   virtual void enter_prog_verify_mode(int pin_count, vpp_vdd_order order) = 0;
   virtual void reset_device() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual void send_bits(word value, int bit_count, int clock_time, int start_delay = 0, int last_clock_hold_time = 0) = 0;
   virtual word receive_bits(int bit_count, int clock_time, int start_delay = 0) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual void delay(int unit_100ns) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual ~programmer() {};
};

// ***************************************************************************
class programmer_parallel : public programmer
{
protected:
   ///////////////////////////////////////////////////////////////////////////
   virtual ~programmer_parallel()
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void clock_set(int val) = 0;
   virtual void data_set(int val) = 0;
   virtual int  data_read() = 0;

   virtual void data_direction_read() = 0;
   virtual void data_direction_write() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_min_programmer_clock_time() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual void send_bits(word value, int bit_count, int clock_time, int start_delay = 0, int last_clock_hold_time = 0)
   {
      // the programmer may need longer clock periods
      //
      clock_time = __max(get_min_programmer_clock_time(), clock_time);
      last_clock_hold_time = __max(get_min_programmer_clock_time(), last_clock_hold_time);

      // hold data and clock steady before sending bits
      //
      if (start_delay != 0) delay(start_delay);  
      
      for (int i = 0; i < bit_count; i++)   // send lsb first
      {
         clock_set(1);

         data_set(value & 1);
   
         if (last_clock_hold_time != 0 && i == bit_count - 1)
         {
            delay(last_clock_hold_time);
         }
         else
         {
            delay(clock_time);
         }

         clock_set(0);

         delay(clock_time);

         value = value >> 1;
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word receive_bits(int bit_count, int clock_time, int start_delay = 0)
   {
      // the programmer may need longer clock periods
      //
      clock_time = __max(get_min_programmer_clock_time(), clock_time);

      word val = 0;
      unsigned bit_pos = 1;

      if (start_delay != 0) delay(start_delay);

      data_direction_read();

      for (int i = 0; i < bit_count; i++)   // receive lsb first
      {
         clock_set(1);

         delay(clock_time);

         if (data_read() != 0)
         {
            val = val | bit_pos;
         }
         
         bit_pos = bit_pos << 1;

         clock_set(0);

         // data line is input again on the falling edge of the last bit
         //
         if (i == bit_count - 1) data_direction_write();
         
         delay(clock_time);
      }

      return val;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void delay(int unit_100ns)
   {
      high_res_delay::delay(unit_100ns);
   };
};

// ***************************************************************************
class io_bit
{
private:
   int m_port_offset;
   int m_active_state;
   byte m_bit_mask;

public:
   ///////////////////////////////////////////////////////////////////////////
   io_bit(int port_offset, byte bit_mask, int active_state)
      : m_port_offset(port_offset), m_bit_mask(bit_mask), m_active_state(active_state)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   // returns 1 if port bit is 1 and active state is non zero
   //
   int read()
   {
      if (parallel_io::read_from_port(m_port_offset) & m_bit_mask)
      {
         if (m_active_state != 0) return 1; else return 0;
      }
      else
      {
         if (m_active_state != 0) return 0; else return 1;
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   // sets port bit to 1 if active state is non zero and value is non zero
   //
   void set(int value)
   {
      byte outval = parallel_io::read_from_port(m_port_offset);

      if ((m_active_state != 0 && value == 0) || (m_active_state == 0 && value != 0))
      {
         parallel_io::write_to_port(m_port_offset, outval & (~m_bit_mask));
      }
      else
      {
         parallel_io::write_to_port(m_port_offset, outval | m_bit_mask);
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void delay(int unit_100ns)
   {
      high_res_delay::delay(unit_100ns + (100 * 10));
   };
};

// ***************************************************************************
class programmer_p16pro40 : public programmer_parallel
{
private:
   // 10ms delay before and after vdd and vpp transitions
   // this should be plenty for any pic/programmer combination
   //
   enum { v_delay = (10000 * 10) };

   io_bit m_vdd;
   io_bit m_vpp18;
   io_bit m_vpp40;
   io_bit m_clock;
   io_bit m_data_in;
   io_bit m_data_out;

   io_bit *m_vpp;

public:
   static programmer *create()
   {
      return new programmer_p16pro40();
   };

   ///////////////////////////////////////////////////////////////////////////
   programmer_p16pro40()
      : programmer_parallel(),
        m_vdd(0, (1<<2), 1),       // D2 active high
        m_vpp18(0, (1<<3), 1),     // D3 active high
        m_vpp40(0, (1<<4), 1),     // D4 active high
        m_clock(0, (1<<1), 0),     // D1 active low
        m_data_in(1, (1<<6), 0),   // ACK active low
        m_data_out(0, (1<<0), 0)   // D0 active low
   {
      m_vdd.set(0);
      m_vpp18.set(0);
      m_vpp40.set(0);
      m_clock.set(0);
      m_data_out.set(0);

      m_vpp = 0;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual ~programmer_p16pro40()
   {
      m_vdd.set(0);
      m_vpp18.set(0);
      m_vpp40.set(0);
      m_clock.set(0);
      m_data_out.set(0);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void vpp_set(int val)
   {
      if (val != 0)
      {
         assert(m_vpp != 0);

         m_vpp->set(1);
      }
      else
      {
         m_vpp18.set(0);
         m_vpp40.set(0);
      }
   };      

   ///////////////////////////////////////////////////////////////////////////
   virtual void vdd_set(int val)      { m_vdd.set(val); };
   virtual void clock_set(int val)    { m_clock.set(val); };
   virtual void data_set(int val)     { m_data_out.set(val); };

   ///////////////////////////////////////////////////////////////////////////
   virtual int data_read()            { return m_data_in.read(); };

   ///////////////////////////////////////////////////////////////////////////
   virtual void data_direction_read() {  m_data_out.set(1); };
   virtual void data_direction_write() {};

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_min_programmer_clock_time() { return 5 * 10; };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode(int pin_count, vpp_vdd_order order)
   {
      if (pin_count == 40 || pin_count == 28) m_vpp = &m_vpp40; else m_vpp = &m_vpp18;

      reset_device();

      if (order == vdd_first) vdd_set(1); else vpp_set(1);
   
      delay(v_delay);

      if (order == vdd_first) vpp_set(1); else vdd_set(1);

      delay(v_delay);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      data_direction_read();

      vpp_set(0);
      clock_set(0);
      data_set(0);
      vdd_set(0);
      
      delay(v_delay);      
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_connected()
   {
      vdd_set(1);
      data_set(0);
      delay(v_delay);
      if (data_read() == 0)
      {
         data_set(1);
         delay(v_delay);
         if (data_read() != 0)
         {
            data_set(0);
            vdd_set(0);
            return true;
         }
      }
      data_set(0);
      vdd_set(0);
      return false;
   };
};

// ***************************************************************************
class programmer_lvp : public programmer_parallel
{
private:
   // 10ms delay before and after pgm and mclr
   //
   enum { v_delay = (10000 * 10) };

   io_bit m_mclr;
   io_bit m_pgm;
   io_bit m_clock;
   io_bit m_data_out;
   io_bit m_data_in;
   io_bit m_data_in_pullup1;
   io_bit m_data_in_pullup2;

public:
   static programmer *create()
   {
      return new programmer_lvp();
   };

   ///////////////////////////////////////////////////////////////////////////
   programmer_lvp()
      : programmer_parallel(),
        m_pgm              (2, (1<<3), 0),   // PGM active low
        m_mclr             (2, (1<<2), 1),   // MCLR active high
        m_clock            (2, (1<<1), 0),   // CLOCK active low
        m_data_out         (2, (1<<0), 0),   // DATA OUT active low
        m_data_in          (1, (1<<7), 0),   // DATA IN active low
        m_data_in_pullup1  (0, (1<<0), 1),   // DATA IN pullup, active high
        m_data_in_pullup2  (0, (1<<1), 1)    // DATA IN pullup, active high
   {
      m_mclr.set(1);
      m_pgm.set(0);
      m_clock.set(1);
      m_data_out.set(1);
      m_data_in_pullup1.set(1);
      m_data_in_pullup2.set(1);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual ~programmer_lvp()
   {
      m_pgm.set(0);
      m_clock.set(1);
      m_data_out.set(1);
      m_data_in_pullup1.set(1);
      m_data_in_pullup2.set(1);

      delay(v_delay);
      m_mclr.set(1);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode(int pin_count, vpp_vdd_order order)
   {
      m_mclr.set(0);
      m_pgm.set(0);
      m_clock.set(0);
      m_data_out.set(0);
      m_data_in_pullup1.set(1);
      m_data_in_pullup2.set(1);

      delay(v_delay);
      m_pgm.set(1);

      delay(v_delay);
      m_mclr.set(1);

      delay(v_delay);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_mclr.set(0);
      delay(v_delay);

      m_pgm.set(0);
      delay(v_delay);

      m_clock.set(1);
      m_data_out.set(1);
      m_data_in_pullup1.set(1);
      m_data_in_pullup2.set(1);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void clock_set(int val)    { m_clock.set(val); };
   virtual void data_set(int val)     { m_data_out.set(val); };

   ///////////////////////////////////////////////////////////////////////////
   virtual int data_read()            { return m_data_in.read(); };

   ///////////////////////////////////////////////////////////////////////////
   virtual void data_direction_read()  { m_data_out.set(0); };
   virtual void data_direction_write() {};

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_min_programmer_clock_time() { return 50 * 10; };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_connected()
   {
      m_data_in_pullup1.set(1);
      m_data_in_pullup2.set(1);

      data_set(0);
      delay(v_delay);
      if (data_read() == 0)
      {
         data_set(1);
         delay(v_delay);
         if (data_read() != 0)
         {
            m_data_in_pullup1.set(0);
            m_data_in_pullup2.set(0);
            data_set(0);
            return true;
         }
      }
      m_data_in_pullup1.set(0);
      m_data_in_pullup2.set(0);
      data_set(0);
      return false;
   };
};

// ***************************************************************************
class programmer_lvp_fast : public programmer_lvp
{
public:
   static programmer *create()
   {
      return new programmer_lvp_fast();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_min_programmer_clock_time() { return 10 * 10; };
};

// ***************************************************************************
class engine
{
public:
   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr) = 0;
   virtual void erase_data_memory(progress &pr) = 0;
   virtual void erase_chip(progress &pr) = 0;
   virtual void program(chip_memory_source &reader, progress &pr) = 0;
   virtual void verify(chip_memory_source &reader, progress &pr) = 0;
   virtual void read(chip_memory_sink &writer, progress &pr) = 0;
   virtual void blank_check(progress &pr) = 0;
   virtual void print_config(ostream &os) = 0;
   virtual void print_device_id(ostream &os) = 0;
   virtual void create_test_data(string path, string device_name) = 0;
};
// ***************************************************************************
class engine_18f : public engine
{
protected:
   programmer &m_programmer;

   ///////////////////////////////////////////////////////////////////////////
   engine_18f(programmer &p)
      : m_programmer(p),
        m_blank_check(this)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode() = 0;
   virtual void reset_device() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual dword get_program_eeprom_size() = 0;
   virtual word get_data_eeprom_size() = 0;
   virtual word get_code_block_count() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_code_protected() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual vector<string> get_config_bit_desc() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_code_protect_bits(dword address) = 0;
   virtual byte get_low_voltage_program_bits(dword address) = 0;
   virtual byte get_verify_mask(dword address) = 0;
   virtual byte get_blank_value(dword address) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_clock_time()                             { return 10 * 10; };
   virtual int get_program_erase_time()                     { return 5000 * 10; };
   virtual int get_minimum_programming_time()               { return 1000 * 10; };
   virtual int get_delay_after_programming_time()           { return 5 * 10; };

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_cmd_shift_out_tablat_register()          { return 0x02; };
   virtual int get_cmd_table_read()                         { return 0x08; };
   virtual int get_cmd_table_read_post_increment()          { return 0x09; };
   virtual int get_cmd_table_write()                        { return 0x0c; };
   virtual int get_cmd_table_write_post_increment_by_2()    { return 0x0d; };
   virtual int get_cmd_table_write_start_programming()      { return 0x0f; };

   ///////////////////////////////////////////////////////////////////////////
   static inline byte get_upper_byte(dword value) { return ((value >> 16) & 0x3f); };
   static inline byte get_high_byte(dword value)  { return ((value >> 8) & 0xff); };
   static inline byte get_low_byte(dword value)   { return (value & 0xff); };

   ///////////////////////////////////////////////////////////////////////////
   virtual void send_command(int command, byte value_msb, byte value_lsb)
   {
      m_programmer.send_bits(command, 4, get_clock_time());

      m_programmer.send_bits(value_lsb, 8, get_clock_time());

      m_programmer.send_bits(value_msb, 8, get_clock_time());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte send_command_and_receive_byte(int command)
   {
      m_programmer.send_bits(command, 4, get_clock_time());

      m_programmer.send_bits(0x00, 8, get_clock_time());

      return m_programmer.receive_bits(8, get_clock_time());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void delay(int unit100ns)
   {
      m_programmer.delay(unit100ns);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_set_table_pointer(dword address)
   {
      // set table pointer
      //
      send_command(0, 0x0e, get_upper_byte(address));   // movlw addr[21:16]
      send_command(0, 0x6e, 0xf8);                      // movwf tblptru
      send_command(0, 0x0e, get_high_byte(address));    // movlw addr[15:8]
      send_command(0, 0x6e, 0xf7);                      // movwf tblptrh
      send_command(0, 0x0e, get_low_byte(address));     // movlw addr[7:0]
      send_command(0, 0x6e, 0xf6);                      // movwf tblptrl
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_direct_access_to_config_memory()
   {
      send_command(0, 0x8e, 0xa6);                      // bsf eecon1, eepgd
      send_command(0, 0x8c, 0xa6);                      // bsf eecon1, cfgs
      send_command(0, 0x86, 0xa6);                      // bsf eecon1, wren
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_direct_access_to_code_memory()
   {
      send_command(0, 0x8e, 0xa6);                      // bsf eecon1, eepgd
      send_command(0, 0x9c, 0xa6);                      // bcf eecon1, cfgs
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_direct_access_to_data_eeprom()
   {
      send_command(0, 0x9e, 0xa6);                      // bcf eecon1, eepgd
      send_command(0, 0x9c, 0xa6);                      // bcf eecon1, cfgs
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte exec_read_data_eeprom_byte(dword address)
   {
      exec_direct_access_to_data_eeprom();

      // set the data address pointer
      send_command(0, 0x0e, get_low_byte(address));   // movlw addr_lo
      send_command(0, 0x6e, 0xa9);                    // movwf eeadr
      send_command(0, 0x0e, get_high_byte(address));  // movlw addr_high
      send_command(0, 0x6e, 0xaa);                    // movwf eeadr

      // initiate memory read
      send_command(0, 0x80, 0xa6);                    // bsf eecon1, rd

      // and get the data
      send_command(0, 0x50, 0xa8);                    // movf eedata w, 0
      send_command(0, 0x6e, 0xf5);                    // movwf tablat

      return send_command_and_receive_byte(get_cmd_shift_out_tablat_register());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual vector<byte> exec_read_memory_bytes(dword address, int byte_count)
   {
      vector<byte> byte_vector;

      exec_set_table_pointer(address);
      
      for (int i = 0; i < byte_count; i++)
      {
         byte b = send_command_and_receive_byte(get_cmd_table_read_post_increment());

         byte_vector.push_back(b);
      }

      return byte_vector;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_bulk_erase(byte erase_option)
   {
      exec_set_table_pointer(0x3c0004);
      
      send_command(get_cmd_table_write(), 0x00, erase_option);

      send_command(0, 0, 0);

      m_programmer.send_bits(0, 4, get_clock_time());

      delay(get_program_erase_time() + get_delay_after_programming_time());

      m_programmer.send_bits(0, 16, get_clock_time());
   };

   ///////////////////////////////////////////////////////////////////////////
   enum program_flag {start_programming, do_not_start};

   virtual void exec_load_write_buffer(dword address, vector<byte> &byte_vector, program_flag f = do_not_start)
   {
      assert(byte_vector.size() == 8);

      // set address
      //
      exec_set_table_pointer(address);

      // and write all 8 bytes
      //
      int i = 0;

      while(i < 8)
      {
         byte byte1 = byte_vector[i++];
         byte byte2 = byte_vector[i++];

         if (f != start_programming || i < 7)
         {
            send_command(get_cmd_table_write_post_increment_by_2(), byte2, byte1);
         }
         else
         {
            send_command(get_cmd_table_write_start_programming(), byte2, byte1);

            // send nop command, hold sclk high after command nibble
            //
            m_programmer.send_bits(0, 4, get_clock_time(), 0, get_minimum_programming_time());

            // sclk low time after programming (high voltage discharge time)
            //
            delay(get_delay_after_programming_time());

            // send nop data
            //
            m_programmer.send_bits(0, 16, get_clock_time());
         }
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_program_code_memory(dword address, vector<byte> &byte_vector)
   {
      exec_direct_access_to_code_memory();

      exec_load_write_buffer(address, byte_vector, start_programming);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_program_config_byte(dword address, byte value)
   {
      exec_direct_access_to_config_memory();

      // move pc outside of code protection area
      //
      send_command(get_cmd_table_write(), 0xef, 0x00);   // goto 0x100000
      send_command(get_cmd_table_write(), 0xf8, 0x00);

      exec_set_table_pointer(address);
      
      send_command(get_cmd_table_write_start_programming(), value, value);

      // send nop command, hold sclk high after command nibble
      //
      m_programmer.send_bits(0, 4, get_clock_time(), 0, get_minimum_programming_time());

      // sclk low time after programming (high voltage discharge time)
      //
      delay(get_delay_after_programming_time());

      // send nop data
      //
      m_programmer.send_bits(0, 16, get_clock_time());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_program_data_eeprom_byte(dword eeprom_address, byte value)
   {
      byte eecon1;

      exec_direct_access_to_data_eeprom();

      // set the data address pointer
      send_command(0, 0x0e, get_low_byte(eeprom_address));  // movlw addr_lo
      send_command(0, 0x6e, 0xa9);                          // movwf eeadr
      send_command(0, 0x0e, get_high_byte(eeprom_address)); // movlw addr_high
      send_command(0, 0x6e, 0xaa);                          // movwf eeadr

      // load data to be written
      send_command(0, 0x0e, value);                         // movlw <value>
      send_command(0, 0x6e, 0xa8);                          // movwf eedata

      // enable memory writes
      send_command(0, 0x84, 0xa6);                          // bsf eecon1, wren

      // write 0x55 and 0xaa to eecon2
      send_command(0, 0x0e, 0x55);                          // movlw 0x55
      send_command(0, 0x6e, 0xa7);                          // movwf eecon2
      send_command(0, 0x0e, 0xaa);                          // movlw 0xaa
      send_command(0, 0x6e, 0xa7);                          // movwf eecon2

      // initiate write
      send_command(0, 0x82, 0xa6);                          // bsf eecon1, wr

      // poll wr bit, repeat until clear
      do
      {
         send_command(0, 0x50, 0xa6);                       // movwf eecon1, w, 0
         send_command(0, 0x6e, 0xf5);                       // movwf tablat
         
         eecon1 = send_command_and_receive_byte(get_cmd_shift_out_tablat_register());
      }
      while (!(eecon1 & 0x02));

      delay(get_delay_after_programming_time());

      // disable writes
      send_command(0, 0x94, 0xa6);                          // bcf eecon1, wren
   };

   ///////////////////////////////////////////////////////////////////////////
   class blank_check18f : public chip_memory_sink
   {
   public:
      engine_18f *m_parent;

      blank_check18f(engine_18f *parent) : m_parent(parent) {};

      virtual void write_byte(dword address, word value)
      {
         byte blank_value = m_parent->get_blank_value(address);

         if (value != blank_value)
         {
            cex::throw_error("Error: Device is not blank at address: %08x, device: %02x, expected: %02x",
               (unsigned) address, (unsigned) value, (unsigned) blank_value );
         }
      };

      virtual void write_word(dword address, word value) { assert(0); };

   } m_blank_check;

   friend class blank_check18f;

public:
   ///////////////////////////////////////////////////////////////////////////
   virtual void blank_check(progress &pr)
   {
      read(m_blank_check, pr);
   }

   ///////////////////////////////////////////////////////////////////////////
   virtual void read(chip_memory_sink &writer, progress &pr)
   {
      dword address;

      // read program memory and configuration
      //
      enter_prog_verify_mode();

      exec_set_table_pointer(0);
      
      for (address = 0; address <= 0x30000d; address++)
      {
         // goto id locations
         //
         if (address == get_program_eeprom_size())
         {
            address = 0x200000;

            exec_set_table_pointer(address);
         }

         // goto config address
         //
         if (address == 0x200008)
         {            
            address = 0x300000;

            exec_set_table_pointer(address);
         }

         byte b = send_command_and_receive_byte(get_cmd_table_read_post_increment());

         writer.write_byte(address, b);

         // cout << "address: " << address << ", byte: " << (int) b << endl;

         pr.at_address(address);
      }
      
      // read eeprom data
      //
      for (address = 0; address < get_data_eeprom_size(); address++)
      {
         byte b = exec_read_data_eeprom_byte(address);

         writer.write_byte(address + 0xf00000, b);

         pr.at_address(address + 0xf00000);
      }

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void verify(chip_memory_source &reader, progress &pr)
   {
      byte pic_byte;
      byte inhex_byte;
      dword pic_address = 0;
      dword inhex_address;

      if (is_code_protected())
      {
         pr.print_message("Read/write protection is enabled on this device. Verify may fail");
      }

      enter_prog_verify_mode();

      exec_set_table_pointer(pic_address);

      while (reader.read_next_byte(&inhex_address, &inhex_byte))
      {
         pr.at_address(inhex_address);

         // program memory ?
         //
         if (inhex_address < 0xf00000)
         {
            if (inhex_address != pic_address)
            {
               pic_address = inhex_address;

               exec_set_table_pointer(pic_address);
            }

            pic_byte = send_command_and_receive_byte(get_cmd_table_read_post_increment());

            pic_address++;
         }

         // eeprom data memory
         //
         else
         {
            pic_byte = exec_read_data_eeprom_byte(inhex_address - 0xf00000);
         }

         byte dont_care_bits = get_verify_mask(inhex_address);

         if ((inhex_byte & dont_care_bits) != (pic_byte & dont_care_bits))
         {
            cex::throw_error("Error: Verify failed at address: 0x%06x, hex file: 0x%02x, device: 0x%02x",
               (unsigned) inhex_address, (unsigned) inhex_byte, (unsigned) pic_byte );
         }
      }

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr)
   {
      if (is_code_protected())
      {
         cex::throw_error("Error: Cannot erase program memory because code protection is enabled.\n");
      }

      enter_prog_verify_mode();
      
      // erase boot block
      //
      pr.print_message("Erasing boot block...");

      exec_bulk_erase(0x83);

      // erase all panels
      //
      byte erase_option = 0x88;

      for (int i = 0; i < get_code_block_count(); i++)
      {
         pr.print_message("Erasing block %d...", i + 1);

         exec_bulk_erase(erase_option + i);
      }

      reset_device();
   };
      
   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_data_memory(progress &pr)
   {
      if (is_code_protected())
      {
         cex::throw_error("Error: Cannot erase data memory because code protection is enabled.\n");
      }

      pr.print_message("Erasing data memory...");

      enter_prog_verify_mode();
      
      exec_bulk_erase(0x81);

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_chip(progress &pr)
   {
      pr.print_message("Erasing entire chip...");

      enter_prog_verify_mode();

      exec_bulk_erase(0x80);
     
      reset_device();
   }

   ///////////////////////////////////////////////////////////////////////////
   virtual void program(chip_memory_source &reader, progress &pr)
   {
      byte inhex_byte;
      dword inhex_address;
      dword prev_inhex_address = 0;

      if (is_code_protected())
      {
         cex::throw_error("Error: Code protection is enabled on this device. Erase chip before programming.");
      }

      vector<byte> byte_buffer;
      for (int i = 0; i < 8; i++) byte_buffer.push_back(0xff);

      enter_prog_verify_mode();

      while(reader.read_next_byte(&inhex_address, &inhex_byte)) 
      {
         pr.at_address(inhex_address);

         // address must increment
         //
         if (inhex_address < prev_inhex_address)
         {
            cex::throw_error("Error: Address must always increment in input file: 0x%06x\n", inhex_address); 
         }

         // address must be valid
         //
         if (!( (inhex_address < get_program_eeprom_size()) ||
                (inhex_address >= 0x200000 && inhex_address <= 0x200007) ||
                (inhex_address >= 0x300000 && inhex_address <= 0x30000d) ||
                (inhex_address >= 0xf00000 && inhex_address < 0xf00000 + get_data_eeprom_size())))
         {
            cex::throw_error("Error: Address in input file is out of range: 0x%08x\n", inhex_address); 
         }

         // write out program memory buffer if necessary
         //
         if (inhex_address >= 0x300000 || (inhex_address & (~0x07)) != (prev_inhex_address & (~0x07)))
         {
            for (int i = 0; i < 8; i++)
            {
               if (byte_buffer[i] != 0xff)
               {
                  exec_program_code_memory(prev_inhex_address & (~0x07), byte_buffer);

                  // and verify
                  //
                  if (byte_buffer != exec_read_memory_bytes(prev_inhex_address & (~0x07), 8))
                  {
                     cex::throw_error("Error: Verify failed near address 0x%06x", (unsigned) prev_inhex_address & (~0x07));
                  }

                  // reset buffer bytes to 0xff
                  //
                  for (int j = 0; j < 8; j++) byte_buffer[j] = 0xff;

                  break;
               }
            }
         }

         // program memory or id location ?
         //
         if (inhex_address < 0x300000)
         {
            byte_buffer[inhex_address & 0x07] = inhex_byte;
         }
         
         // config byte ?
         //
         else if (inhex_address >= 0x300000 && inhex_address <= 0x30000d)
         {
            exec_program_config_byte(inhex_address, inhex_byte);

            // and verify
            //
            byte dont_care_bits = get_verify_mask(inhex_address);

            vector<byte> config_bytes = exec_read_memory_bytes(inhex_address, 1);

            if ((config_bytes[0] & dont_care_bits) != (inhex_byte & dont_care_bits))
            {
               cex::throw_error("Error: Verify failed near configuration address 0x%06x written: 0x%02x read: 0x%02x",
                  (unsigned) inhex_address, (unsigned) inhex_byte, (unsigned) config_bytes[0]);
            }
         }

         // data eeprom ?
         //
         else if (inhex_address >= 0xf00000)
         {
            exec_program_data_eeprom_byte(inhex_address - 0xf00000, inhex_byte);

            // and verify
            //
            if (exec_read_data_eeprom_byte(inhex_address - 0xf00000) != inhex_byte)
            {
               cex::throw_error("Error: Verify failed near eeprom address 0x%06x", (unsigned) inhex_address - 0xf00000);
            }
         }

         else assert(0);

         prev_inhex_address = inhex_address;
      }

      // write out any remaining program memory bytes
      //
      for (int j = 0; j < 8; j++)
      {
         if (byte_buffer[j] != 0xff)
         {
            exec_program_code_memory(prev_inhex_address & (~0x07), byte_buffer);

            // and verify
            //
            if (byte_buffer != exec_read_memory_bytes(prev_inhex_address & (~0x07), 8))
            {
               cex::throw_error("Error: Verify failed near address 0x%06x", (unsigned) prev_inhex_address & (~0x07));
            }

            break;
         }
      }
 
      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_config(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      // read config bytes
      //
      enter_prog_verify_mode();

      vector<byte> config_byte_vector = exec_read_memory_bytes(0x300000, 14);

      reset_device();

      // get the bit descriptions
      //
      vector<string> config_description_vector = get_config_bit_desc();

      assert(config_description_vector.size() == 14); 

      // and print...
      //
      for (int i = 0; i < config_byte_vector.size() && i < config_description_vector.size(); i++)
      {
         os << "0x" << setfill('0') << setw(6) << 0x300000 + i << " ";

         os << config_description_vector[i] << endl;

         os << "        ";

         for (byte bit_pos = (byte) (1 << 7); bit_pos != 0; bit_pos = bit_pos >> 1)
         {
            os << " " << setfill(' ') << setw(6) << (config_byte_vector[i] & bit_pos ? 1 : 0);
         }

         os << endl;
      }

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void create_test_data(string path, string device_name)
   {
      int i;

      // subset of program eeprom
      //
      ofstream ofp(string(path + device_name + string("_program_eeprom.hex")).c_str());
      inhex_file_writer fwp(ofp);

      for (i = 3; i < get_program_eeprom_size() / 11; i++)
      {
         fwp.write_byte(i, (byte)(rand() & 0xff) );
      }

      for (i = get_program_eeprom_size() / 7; i < get_program_eeprom_size() / 3; i++)
      {
         fwp.write_byte(i, (byte)(rand() & 0xff) );
      }

      // subset of data eeprom
      //
      ofstream ofd(string(path + device_name + string("_data_eeprom.hex")).c_str());
      inhex_file_writer fwd(ofd);

      for (i = 1; i < get_data_eeprom_size() / 11; i++)
      {
         fwd.write_byte(0xf00000 + i, (byte)(rand() & 0xff) );
      }

      for (i = get_data_eeprom_size() / 7; i < get_data_eeprom_size() / 3; i++)
      {
         fwd.write_byte(0xf00000 + i, (byte)(rand() & 0xff) );
      }

      // id locations
      //
      ofstream ofi(string(path + device_name + string("_idloc.hex")).c_str());
      inhex_file_writer fwi(ofi);

      for (i = 0; i < 8; i++)
      {
         fwi.write_byte(0x200000 + i, (byte)(rand() & 0xff) );
      }

      // config code protect off
      //
      ofstream off(string(path + device_name + string("_config_cp_off.hex")).c_str());
      inhex_file_writer fwf(off);

      for (i = 0; i < 14; i++)
      {
         if (i == 0x0b)
            fwf.write_byte(0x300000 + i, 0xe0); // leave write protection bits alone
         else
            fwf.write_byte(0x300000 + i, (byte)(rand() & 0xff) |
                  get_code_protect_bits(0x300000 + i) | get_low_voltage_program_bits(0x300000 + i));
      }

      // config code protect on
      //
      ofstream ofn(string(path + device_name + string("_config_cp_on.hex")).c_str());
      inhex_file_writer fwn(ofn);

      for (i = 0; i < 14; i++)
      {
         if (i == 0x0b)
            fwn.write_byte(0x300000 + i, 0xe0);
         else
            fwn.write_byte(0x300000 + i, (byte)(rand() & 0xff) &
                  ~get_code_protect_bits(0x300000 + i) | get_low_voltage_program_bits(0x300000 + i));
      }
    
      // entire device, code protect off
      //
      ofstream ofa(string(path + device_name + string("_all_eeprom.hex")).c_str());
      inhex_file_writer fwa(ofa);

      for (i = 0; i < get_program_eeprom_size(); i++)
      {
         fwa.write_byte(i, (byte)(rand() & 0xff) );
      }

      for (i = 0; i < 8; i++)
      {
         fwa.write_byte(0x200000 + i, (byte)(rand() & 0xff) );
      }

      for (i = 0; i < 14; i++)
      {
         if (i == 0x0b)
            fwa.write_byte(0x300000 + i, 0xe0);
         else
            fwa.write_byte(0x300000 + i, (byte)(rand() & 0xff) |
                  get_code_protect_bits(0x300000 + i) | get_low_voltage_program_bits(0x300000 + i));
      }

      for (i = 0; i < get_data_eeprom_size(); i++)
      {
         fwa.write_byte(0xf00000 + i, (byte)(rand() & 0xff) );
      }
   };
};

// ***************************************************************************
class engine_18fxx2 : public engine_18f
{
private:
   ///////////////////////////////////////////////////////////////////////////
   dword m_program_eeprom_size;
   word m_data_eeprom_size;
   int m_pin_count;

public:
   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_18fxx2(p, prog_size, eeprom_size, pins);
   };

protected:
   ///////////////////////////////////////////////////////////////////////////
   engine_18fxx2(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_18f(p)
   {
      m_program_eeprom_size = (word) program_eeprom_size;
      m_data_eeprom_size  = (word) data_eeprom_size;
      m_pin_count = pin_count;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual dword get_program_eeprom_size()         { return m_program_eeprom_size; };
   virtual word get_data_eeprom_size()             { return m_data_eeprom_size;    };
   virtual word get_code_block_count()             { return 4; };

   ///////////////////////////////////////////////////////////////////////////
   virtual void exec_program_code_memory(dword address, vector<byte> &byte_vector)
   {
      exec_direct_access_to_config_memory();

      // write 00h to 0x3c0006 to enable single-panel writes
      //
      exec_set_table_pointer(0x3c0006);

      send_command(get_cmd_table_write(), 0x00, 0x00);

      // and program 
      //
      exec_direct_access_to_code_memory();

      exec_load_write_buffer(address, byte_vector, start_programming);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(m_pin_count, programmer::vdd_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      enter_prog_verify_mode();

      vector<byte> device_id_vector = exec_read_memory_bytes(0x3ffffe, 2);
      
      reset_device();

      os << "Device ID1 at 0x3ffffe: " << "0x" << setfill('0') << setw(2) << (int) device_id_vector[0] << endl;
      os << "Device ID2 at 0x3fffff: " << "0x" << setfill('0') << setw(2) << (int) device_id_vector[1] << endl;

      string device;
      
      unsigned revision = device_id_vector[0] & 0x1f;

      if (device_id_vector[1] == 0x04 && (device_id_vector[0] & 0xe0) == 0)
      {
         device = "18f252";
      }
      else if (device_id_vector[1] == 0x04 && (device_id_vector[0] & 0xe0) == 0x20)
      {
         device = "18f452";
      }
      else
      {
         cex::throw_error("Error: Device id is not valid for this chip");
      }

      os << "Device: " << device << ", Revision: 0x" << setfill('0') << setw(2) << revision << endl; 

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual vector<string> get_config_bit_desc()
   {
      vector<string> v;

      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------  OSCEN ------ ------  FOSC2  FOSC1  FOSC0 ");
      v.push_back("------ ------ ------ ------  BORV1  BORV0  BOREN PWRTEN");
      v.push_back("------ ------ ------ ------ WDTPS2 WDTPS1 WDTPS0  WDTEN ");
      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------ ------ ------ ------ CCP2MX");
      v.push_back(" DEBUG ------ ------ ------ ------    LVP ------ STVREN");
      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------    CP3    CP2    CP1    CP0");
      v.push_back("   CPD    CPB ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------   WRT3   WRT2   WRT1   WRT0");
      v.push_back("  WRTD   WRTB   WRTC ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------  EBTR3  EBTR2  EBTR1  EBTR0");
      v.push_back("------  EBTRB ------ ------ ------ ------ ------ ------");

      return v;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_code_protect_bits(dword address)
   {
      switch (address)
      {
      case 0x300008: return (1<<3) | (1<<2) | (1<<1) | (1<<0);
      case 0x300009: return (1<<7) | (1<<6);
      default:       return 0;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_low_voltage_program_bits(dword address)
   {
      switch (address)
      {
      case 0x300006: return (1<<2);
      default:       return 0;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_verify_mask(dword address)
   {
      switch (address)
      {
      case 0x300000: return 0x00;
      case 0x300001: return 0x27;
      case 0x300002: return 0x0f;
      case 0x300003: return 0x0f;
      case 0x300004: return 0x00;
      case 0x300005: return 0x01;
      case 0x300006: return 0x85;
      case 0x300007: return 0x00;
      case 0x300008: return 0x0f;
      case 0x300009: return 0xc0;
      case 0x30000a: return 0x0f;
      case 0x30000b: return 0xe0;
      case 0x30000c: return 0x0f;
      case 0x30000d: return 0x40;
      default:       return 0xff;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_blank_value(dword address)
   {
      // same as verify mask
      //
      return get_verify_mask(address);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_code_protected()
   {
      enter_prog_verify_mode();

      vector<byte> config_byte_vector = exec_read_memory_bytes(0x300000, 14);

      reset_device();

      for (int i = 0; i < config_byte_vector.size(); i++)
      {
         if (~config_byte_vector[i] & get_code_protect_bits(0x300000 + i))
         {
            return true;
         }
      }

      return false;
   };
};

// ***************************************************************************
class engine_18f1x20 : public engine_18f
{
private:
   ///////////////////////////////////////////////////////////////////////////
   dword m_program_eeprom_size;
   word m_data_eeprom_size;
   int m_pin_count;

public:
   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_18f1x20(p, prog_size, eeprom_size, pins);
   };

protected:
   ///////////////////////////////////////////////////////////////////////////
   engine_18f1x20(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_18f(p)
   {
      m_program_eeprom_size = (word) program_eeprom_size;
      m_data_eeprom_size  = (word) data_eeprom_size;
      m_pin_count = pin_count;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual dword get_program_eeprom_size()         { return m_program_eeprom_size; };
   virtual word get_data_eeprom_size()             { return m_data_eeprom_size;    };
   virtual word get_code_block_count()             { return 2; };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(m_pin_count, programmer::vdd_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      enter_prog_verify_mode();

      vector<byte> device_id_vector = exec_read_memory_bytes(0x3ffffe, 2);
      
      reset_device();

      os << "Device ID1 at 0x3ffffe: " << "0x" << setfill('0') << setw(2) << (int) device_id_vector[0] << endl;
      os << "Device ID2 at 0x3fffff: " << "0x" << setfill('0') << setw(2) << (int) device_id_vector[1] << endl;

      string device;
      
      unsigned revision = device_id_vector[0] & 0x1f;

      if (device_id_vector[1] == 0x07 && (device_id_vector[0] & 0xe0) == 0xe0)
      {
         device = "18f1220";
      }
      else if (device_id_vector[1] == 0x07 && (device_id_vector[0] & 0xe0) == 0xc0)
      {
         device = "18f1320";
      }
      else
      {
         cex::throw_error("Error: Device id is not valid for this chip");
      }

      os << "Device: " << device << ", Revision: 0x" << setfill('0') << setw(2) << revision << endl; 

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual vector<string> get_config_bit_desc()
   {
      vector<string> v;

      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back("  IESO  FCMEM ------ ------  FOSC3  FOSC2  FOSC1  FOSC0");
      v.push_back("------ ------ ------ ------  BORV1  BORV0  BOREN PWRTEN");
      v.push_back("------ ------ ------  WDPS3  WDPS2  WDPS1  WDPS0  WDTEN");
      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back(" MCLRE ------ ------ ------ ------ ------ ------ ------");
      v.push_back(" DEBUG ------ ------ ------ ------    LVP ------ STVREN");
      v.push_back("------ ------ ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------ ------ ------    CP1    CP0");
      v.push_back("   CPD    CPB ------ ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------ ------ ------   WRT1   WRT0");
      v.push_back("  WRTD   WRTB   WRTC ------ ------ ------ ------ ------");
      v.push_back("------ ------ ------ ------ ------ ------ EBTR1  EBTR0");
      v.push_back("------  EBTRB ------ ------ ------ ------ ------ ------");

      return v;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_code_protect_bits(dword address)
   {
      switch (address)
      {
      case 0x300008: return (1<<1) | (1<<0);
      case 0x300009: return (1<<7) | (1<<6);
      default:       return 0;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_low_voltage_program_bits(dword address)
   {
      switch (address)
      {
      case 0x300006: return (1<<2);
      default:       return 0;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_verify_mask(dword address)
   {
      switch (address)
      {
      case 0x300000: return 0x00;
      case 0x300001: return 0xcf;
      case 0x300002: return 0x0f;
      case 0x300003: return 0x1f;
      case 0x300004: return 0x00;
      case 0x300005: return 0x80;
      case 0x300006: return 0x85;
      case 0x300007: return 0x00;
      case 0x300008: return 0x03;
      case 0x300009: return 0xc0;
      case 0x30000a: return 0x03;
      case 0x30000b: return 0xe0;
      case 0x30000c: return 0x03;
      case 0x30000d: return 0x40;
      default:       return 0xff;   
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual byte get_blank_value(dword address)
   {
      // same as verify mask
      //
      return get_verify_mask(address);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_code_protected()
   {
      enter_prog_verify_mode();

      vector<byte> config_byte_vector = exec_read_memory_bytes(0x300000, 14);

      reset_device();

      for (int i = 0; i < config_byte_vector.size(); i++)
      {
         if (~config_byte_vector[i] & get_code_protect_bits(0x300000 + i))
         {
            return true;
         }
      }

      return false;
   };
};
   
// ***************************************************************************
class engine_16f : public engine
{
protected:
   programmer &m_programmer;

   word m_program_eeprom_size;
   word m_data_eeprom_size;
   int m_pin_count;

   ///////////////////////////////////////////////////////////////////////////
   engine_16f(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : m_programmer(p),
        m_blank_check(this)
   {
      m_program_eeprom_size = (word) program_eeprom_size;
      m_data_eeprom_size  = (word) data_eeprom_size;
      m_pin_count = pin_count;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode() = 0;
   virtual void reset_device() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual const char *get_config_bit_desc() = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_code_protect_bits() = 0;
   virtual word get_low_voltage_program_bits() = 0;
   virtual word get_verify_mask(word address) = 0;
   virtual word get_blank_value(word address) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os) = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_cmd_begin_prog_cycle()           = 0;
   virtual int get_cmd_load_configuration()         { return 0x00; };
   virtual int get_cmd_read_data_from_prog_memory() { return 0x04; };
   virtual int get_cmd_increment_address()          { return 0x06; };
   virtual int get_cmd_read_data_from_data_memory() { return 0x05; };
   virtual int get_cmd_load_data_for_prog_memory()  { return 0x02; };
   virtual int get_cmd_load_data_for_data_memory()  { return 0x03; };

   ///////////////////////////////////////////////////////////////////////////
   virtual int get_clock_time()     { return 5*10; };       // clock is held high or low for this period
   virtual int get_command_pause()  { return 10*10; }       // pause between commands and data

   virtual int get_program_data_cycle_time()  = 0;
   virtual int get_program_prog_cycle_time()  = 0;

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_program_eeprom_size()          { return m_program_eeprom_size; };
   virtual word get_data_eeprom_size()             { return m_data_eeprom_size;    };
   virtual int get_pin_count()                     { return m_pin_count;    };

   ///////////////////////////////////////////////////////////////////////////
   virtual void send_command(int command)
   {
      m_programmer.send_bits(command, 6, get_clock_time(), get_command_pause());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void send_command(int command, word value)
   {
      m_programmer.send_bits(command, 6, get_clock_time(), get_command_pause());

      m_programmer.send_bits((value << 1), 16, get_clock_time(), get_command_pause());
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word receive_word()
   {
      word w = m_programmer.receive_bits(16, get_clock_time(), get_command_pause()); 
      
      return (w >> 1) & 0x3FFF;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void delay(int unit100ns)
   {
      m_programmer.delay(unit100ns);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual bool is_code_protected()
   {
      // read config word to determine if code protection is enabled
      //
      word config_word = read_config(0x2007);

      if ((~read_config(0x2007)) & get_code_protect_bits()) return true; else return false;
   };

   ///////////////////////////////////////////////////////////////////////////
   class blank_check16f : public chip_memory_sink
   {
   public:
      engine_16f *m_parent;

      blank_check16f(engine_16f *parent) : m_parent(parent) {};

      virtual void write_word(dword address, word value)
      {
         word blank_value = m_parent->get_blank_value(address);

         if (value != blank_value)
         {
            cex::throw_error("Error: Device is not blank at address: %04x, device: %04x, expected: %04x",
               (unsigned) address, (unsigned) value, (int) blank_value );
         }
      };

      virtual void write_byte(dword address, word value)  { assert(0); };

   } m_blank_check;
 
   friend class blank_check16f;

public:
   ///////////////////////////////////////////////////////////////////////////
   virtual void blank_check(progress &pr)
   {
      read(m_blank_check, pr);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void read(chip_memory_sink &writer, progress &pr)
   {
      word address;

      // read program memory and configuration
      //
      enter_prog_verify_mode();

      for (address = 0; address <= 0x2007; address++)
      {
         // enter the config memory, by default program counter wraps around to address 0
         //
         if (address == 0x2000)
         {
            send_command(get_cmd_load_configuration(), 0);
         }

         if ((address < get_program_eeprom_size()) ||
             (address >= 0x2000 && address <= 0x2003) ||
             (address == 0x2007) )
         {
            pr.at_address(address);

            // read the next program word and write it to the output file
            //
            send_command(get_cmd_read_data_from_prog_memory());
            word w = receive_word();

            writer.write_word(address, w);
         }

         // and goto next address (no need to increment pic address where there is no rom)
         //
         if (address < get_program_eeprom_size() || address >= 0x2000)
         {
            send_command(get_cmd_increment_address());
         }
      }

      reset_device();

      // read data memory
      //
      enter_prog_verify_mode();

      for (address = 0x2100; address < 0x2100 + get_data_eeprom_size(); address++)
      {
         pr.at_address(address);

         // read the next program word and write it to the output file
         //
         send_command(get_cmd_read_data_from_data_memory());
         word w = receive_word() & 0xff;

         writer.write_word(address, w);

         send_command(get_cmd_increment_address());
      }

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void verify(chip_memory_source &reader, progress &pr)
   {    
      word pic_word;
      word pic_address = 0;

      word inhex_word;
      word inhex_address;
      word prev_inhex_address = 0;

      dword inhex_dw_address;

      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         pr.print_message("Read/write protection is enabled on this device. Verify may fail");
      }

      enter_prog_verify_mode();

      while (reader.read_next_word(&inhex_dw_address, &inhex_word))
      {
         if (inhex_dw_address > 0xffff)
         {
            cex::throw_error("Error: Address is out of range in input file: %08x", (unsigned) inhex_dw_address);
         }

         inhex_address = (word) inhex_dw_address;

         pr.at_address(inhex_address);

         if (inhex_address >= 0x2000 && inhex_address <= 0x2007 && prev_inhex_address < 0x2000)
         {
            send_command(get_cmd_load_configuration(), 0);
            
            pic_address = 0x2000;
         }
         else if (inhex_address >= 0x2100 && prev_inhex_address < 0x2100)
         {
            reset_device();

            enter_prog_verify_mode();

            pic_address = 0x2100;
         }

         while (pic_address < inhex_address)
         {
            pic_address++;

            send_command(get_cmd_increment_address());
         }

         if (pic_address < 0x2100)
         {
            send_command(get_cmd_read_data_from_prog_memory());

            pic_word = receive_word();
         }
         else
         {
            send_command(get_cmd_read_data_from_data_memory());

            pic_word = receive_word() & 0xff;
         }

         word dont_care_mask = get_verify_mask(inhex_address);

         if ((inhex_word & dont_care_mask) != (pic_word & dont_care_mask))
         {
            cex::throw_error("Error: Verify failed at address: %04x, hex file: %04x, device: %04x",
               (int) pic_address, (int) inhex_word, (int) pic_word );
         }

         prev_inhex_address = inhex_address;
      }      
      
      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void program(chip_memory_source &reader, progress &pr)
   {
      word inhex_word;
      word inhex_address;
      word prev_inhex_address = 0;

      word pic_word;
      word pic_address = 0;

      dword inhex_dw_address;

      enter_prog_verify_mode();

      while(reader.read_next_word(&inhex_dw_address, &inhex_word))
      {
         if (inhex_dw_address > 0xffff)
         {
            cex::throw_error("Error: Address in input file is out of range: %08x", (unsigned) inhex_dw_address);
         }

         inhex_address = (word) inhex_dw_address;

         if (inhex_address < pic_address)
         {
            cex::throw_error("Error: Address in input file must always increment. Address: %04x", (int) inhex_address);
         }

         pr.at_address(inhex_address);

         // program address counter wraps around to 0 after 0x1fff !!
         //
         if (inhex_address >= 0x2000 && inhex_address <= 0x2007 && prev_inhex_address < 0x2000)
         {
            // goto address 0x2000
            //
            send_command(get_cmd_load_configuration(), 0);

            pic_address = 0x2000;
         }
         else if (inhex_address >= 0x2100 && prev_inhex_address < 0x2100)
         {
            // goto back to address 0 to program the data eeprom
            //
            reset_device();

            enter_prog_verify_mode();

            pic_address = 0x2100;
         }

         // line up pic address with address from hex file
         //
         while (inhex_address > pic_address)
         {
            send_command(get_cmd_increment_address());

            pic_address++;
         }

         // and program...
         //
         if (pic_address <= 0x2007)
         {
            send_command(get_cmd_load_data_for_prog_memory(), inhex_word);

            send_command(get_cmd_begin_prog_cycle());

            delay(get_program_prog_cycle_time());

            send_command(get_cmd_read_data_from_prog_memory());

            pic_word = receive_word();
         }
         else if (pic_address >= 0x2100 && pic_address < 0x2100 + get_data_eeprom_size())
         {
            send_command(get_cmd_load_data_for_data_memory(), inhex_word & 0xff);

            send_command(get_cmd_begin_prog_cycle());

            delay(get_program_data_cycle_time());

            send_command(get_cmd_read_data_from_data_memory());

            pic_word = receive_word() & 0xff;
         }
         else
         {
            cex::throw_error("Error: Address in input file is out of range: 0x%04x", pic_address);
         }

         word dont_care_mask = get_verify_mask(inhex_address);

         if ((inhex_word & dont_care_mask) != (pic_word & dont_care_mask))
         {
            cex::throw_error("Error: Verify failed at address: 0x%04x, written: 0x%04x, read: 0x%04x",
                 (unsigned) pic_address, (unsigned) inhex_word, (unsigned) pic_word );
         }

         prev_inhex_address = inhex_address;
      }

      if (pic_address == 0)
      {
         cex::throw_error("Error: Input file contains no valid data.");
      }

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word read_config(word address)
   {
      assert(address >= 0x2000 && address <= 0x2007);

      enter_prog_verify_mode();

      // load config, address points to 0x2000
	   //
      send_command(get_cmd_load_configuration(), 0);

      // goto address 0x2007
      //
      for (int i = 0; i < address - 0x2000; i++)
      {
         send_command(get_cmd_increment_address());
      }
         
      // read the config word
      //
      send_command(get_cmd_read_data_from_prog_memory());
      word config = receive_word();

      reset_device();

      return config;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_config(ostream &os)
   {
      int bit_pos;
      string bit_desc;

      long ios_flags = os.flags(ios::hex | ios::internal);

      word config = read_config(0x2007);

      // print config bit descriptions on first line
      //
      os << "       ";

      istrstream istr1(get_config_bit_desc());

      for (bit_pos = (1 << 13); bit_pos != 0; bit_pos = bit_pos >> 1)
      {
         if (istr1 >> bit_desc)
         {
            os << " " << bit_desc;
         }
      }
      os << endl;

      // print config bits on second line
      //
      os << "0x" << setfill('0') << setw(4) << config << ":";

      istrstream istr2(get_config_bit_desc());

      for (bit_pos = (1 << 13); bit_pos != 0; bit_pos = bit_pos >> 1)
      {
         if (istr2 >> bit_desc)
         {
            os << " " << setfill(' ') << setw(bit_desc.length()) << (config & bit_pos ? 1 : 0);
         }
      }
      os << endl;

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void create_test_data(string path, string device_name)
   {
      int i;

      // subset of program eeprom
      //
      ofstream ofp(string(path + device_name + string("_program_eeprom.hex")).c_str());
      inhex_file_writer fwp(ofp);

      for (i = 3; i < get_program_eeprom_size() / 11; i++)
      {
         fwp.write_word(i, (word)(rand() & 0x3fff) );
      }

      for (i = get_program_eeprom_size() / 7; i < get_program_eeprom_size() / 3; i++)
      {
         fwp.write_word(i, (word)(rand() & 0x3fff) );
      }

      // subset of data eeprom
      //
      ofstream ofd(string(path + device_name + string("_data_eeprom.hex")).c_str());
      inhex_file_writer fwd(ofd);

      for (i = 1; i < get_data_eeprom_size() / 11; i++)
      {
         fwd.write_word(0x2100 + i, (byte)(rand() & 0xff) );
      }

      for (i = get_data_eeprom_size() / 7; i < get_data_eeprom_size() / 3; i++)
      {
         fwd.write_word(0x2100 + i, (byte)(rand() & 0xff) );
      }

      // id locations
      //
      ofstream ofi(string(path + device_name + string("_idloc.hex")).c_str());
      inhex_file_writer fwi(ofi);

      for (i = 0; i < 4; i++)
      {
         fwi.write_word(0x2000 + i, (word)(rand() & 0x3fff) );
      }

      // config code protect off
      //
      ofstream off(string(path + device_name + string("_config_cp_off.hex")).c_str());
      inhex_file_writer fwf(off);

      fwf.write_word(0x2007, ((word)(rand() & 0x3fff)) | get_code_protect_bits() | get_low_voltage_program_bits());

      // config code protect on
      //
      ofstream ofn(string(path + device_name + string("_config_cp_on.hex")).c_str());
      inhex_file_writer fwn(ofn);

      fwn.write_word(0x2007, ((word)(rand() & 0x3fff)) & ~get_code_protect_bits()  | get_low_voltage_program_bits());
    
      // entire device, code protect off
      //
      ofstream ofa(string(path + device_name + string("_all_eeprom.hex")).c_str());
      inhex_file_writer fwa(ofa);

      for (i = 0; i < get_program_eeprom_size(); i++)
      {
         fwa.write_word(i, (word)(rand() & 0x3fff) );
      }

      for (i = 0; i < 4; i++)
      {
         fwa.write_word(0x2000 + i, (word)(rand() & 0x3fff) );
      }

      fwa.write_word(0x2007, ((word)(rand() & 0x3fff)) | get_code_protect_bits()  | get_low_voltage_program_bits());

      for (i = 0; i < get_data_eeprom_size(); i++)
      {
         fwa.write_word(0x2100 + i, (word)(rand() & 0xff) );
      }
   };
};

// ***************************************************************************
class engine_16f87x : public engine_16f
{
protected:
   virtual int get_cmd_begin_prog_cycle()          { return 0x18; };
   virtual int get_cmd_begin_erase_prog_cycle()    { return 0x08; };
   virtual int get_cmd_bulk_erase_setup1()         { return 0x01; };
   virtual int get_cmd_bulk_erase_setup2()         { return 0x07; };

   virtual int get_program_data_cycle_time()       { return 4000 * 10; }; 
   virtual int get_program_prog_cycle_time()       { return 4000 * 10; };
   virtual int get_bulk_erase_cycle_time()         { return 8000 * 10; };

   ///////////////////////////////////////////////////////////////////////////
   virtual const char *get_config_bit_desc()
   {
      return "CP1 CP0 RESV --- WRT CPD LVP BODEN CP1 CP0 PWRTE WDTE F0SC1 F0SC0";
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_code_protect_bits()
   {
      return (1<<13) | (1<<12) | (1<<8) | (1<<5) | (1<<4);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_low_voltage_program_bits()
   {
      return (1<<7);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_verify_mask(word address)
   {
      // config bit 10 is unimplemented
      //
      if (address == 0x2007) return ~(1 << 10); else return 0xffff;   
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_blank_value(word address)
   {
      if (address >= 0x2100) return 0xff; else return 0x3fff;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(get_pin_count(), programmer::vdd_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

public:
   ///////////////////////////////////////////////////////////////////////////
   engine_16f87x(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_16f(p, program_eeprom_size, data_eeprom_size, pin_count)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_16f87x(p, prog_size, eeprom_size, pins);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      word device_id = read_config(0x2006);

      os << "Device id: 0x" << setfill('0') << setw(4) << (unsigned) device_id << endl;

      if (device_id & 0x3800 != (1<<11))
      {
         cex::throw_error("Error: This is not a valid device id for this chip");
      }

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase program memory because code protection is enabled.");
      }
      
      enter_prog_verify_mode();

      // erase program memory only, config memory and data memory will be kept intact
      //
      pr.print_message("Erasing program memory...");

      // 1. Execute load data for program memory with all 1s
      //
      send_command(get_cmd_load_data_for_prog_memory(), 0x3FFF);

      // 2. execute bulk erase setup1 command
      // 3. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      // 4. execute begin erase prog command
      // 5. wait
      //
      send_command(get_cmd_begin_erase_prog_cycle());
      delay(get_bulk_erase_cycle_time());

      // 6. execute bulk erase setup1 command
      // 7. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_data_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase data memory because code protection is enabled.");
      }

      pr.print_message("Erasing data memory...");

      enter_prog_verify_mode();

      // 1. Execute load data for data memory with all 1s
      //
      send_command(get_cmd_load_data_for_data_memory(), 0x3FFF);

      // 2. execute bulk erase setup1 command
      // 3. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      // 4. execute begin erase prog command
      // 5. wait
      //
      send_command(get_cmd_begin_erase_prog_cycle());
      delay(get_bulk_erase_cycle_time());

      // 6. execute bulk erase setup1 command
      // 7. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_chip(progress &pr)
   {
      pr.print_message("Erasing chip...");

      // erase configuration word (this may or may not erase the program and the data memory)      
      //
      enter_prog_verify_mode();

      // 1. Execute load config command with all 1s
      //
      send_command(get_cmd_load_configuration(), 0x3FFF);

      // 1.1 goto address 0x2007
      //
      for (int i = 0; i < 7; i++)
      {
         send_command(get_cmd_increment_address());
      }

      // 2. execute bulk erase setup1 command
      // 3. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      // 4. execute begin erase prog command
      // 5. wait
      //
      send_command(get_cmd_begin_erase_prog_cycle());
      delay(get_bulk_erase_cycle_time());

      // 6. execute bulk erase setup1 command
      // 7. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      reset_device();

      // make sure the program memory and the data memory is erased
      //
      ostrstream osnull;
      progress_ostream pnull(osnull, "");

      erase_program_memory(pnull);
      erase_data_memory(pnull);
   };
};

// ***************************************************************************
class engine_12f675 : public engine_16f
{
protected:
   virtual int get_cmd_begin_prog_cycle()          { return 0x08; };
   virtual int get_cmd_bulk_erase_program_memory() { return 0x09; };
   virtual int get_cmd_bulk_erase_data_memory()    { return 0x0b; };

   virtual int get_program_data_cycle_time()       { return 6000 * 10; }; 
   virtual int get_program_prog_cycle_time()       { return 2500 * 10; };
   virtual int get_bulk_erase_cycle_time()         { return 8000 * 10; };

   virtual word get_program_eeprom_size()          { return m_program_eeprom_size; };
   virtual word get_data_eeprom_size()             { return m_data_eeprom_size;    };

   ///////////////////////////////////////////////////////////////////////////
   virtual const char *get_config_bit_desc()
   {
      return "BG1 BG0 --- --- --- CPD CP BODEN MCLRE PWRTE WDTE FOSC2 F0SC1 F0SC0";
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_verify_mask(word address)
   {
      if (address == 0x2007) return ~((1 << 11) | (1 << 10) | (1 << 9)); else return 0xffff;   
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_code_protect_bits()
   {
      return (1<<8) | (1<<7);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_low_voltage_program_bits()
   {
      return 0;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_blank_value(word address)
   {
      if (address == 0x2007)
      {
         return 0x3fff & ~((1<<11) | (1<<10) | (1<<9));
      }
      else if (address >= 0x2100)
      {
         return 0xff;
      }
      else
      {
         return 0x3fff;
      }
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(get_pin_count(), programmer::vpp_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

public:
   ///////////////////////////////////////////////////////////////////////////
   engine_12f675(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_16f(p, program_eeprom_size, data_eeprom_size, pin_count)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_12f675(p, prog_size, eeprom_size, pins);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      os << "This chip has no factory programmed device id";
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_chip(progress &pr)
   {
      pr.print_message("Erasing chip...");

      erase_program_memory(pr);
      erase_data_memory(pr);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr)
   {
      pr.print_message("Erasing program memory and configuration data");
      
      enter_prog_verify_mode();

      // print OSCCAL at 0x3ff
      //
      for (word address = 0; address < 0x3ff; address++)
      {
         send_command(get_cmd_increment_address());
      }

      send_command(get_cmd_read_data_from_prog_memory());
      word osccal = receive_word();

      pr.print_message("Warning: OSCCAL at 0x3ff and Bandgap calibration bits will be erased");

      pr.print_message("Previous OSCCAL at 0x3ff: 0x%04x", (unsigned) osccal);

      // print the config word
      //
      send_command(get_cmd_load_configuration(), 0);

      for (int i = 0; i < 7; i++)
      {
         send_command(get_cmd_increment_address());
      }
         
      send_command(get_cmd_read_data_from_prog_memory());
      word config = receive_word();

      pr.print_message("Previous config word: 0x%04x", (unsigned) config);

      // execute bulk erase
      //
      send_command(get_cmd_bulk_erase_program_memory());
      delay(get_bulk_erase_cycle_time());

      reset_device();
   }

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_data_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase data memory because code protection is enabled.");
      }
      
      pr.print_message("Erasing data memory...");

      enter_prog_verify_mode();

      send_command(get_cmd_bulk_erase_data_memory());
      delay(get_bulk_erase_cycle_time());

      reset_device();
   };
};

// ***************************************************************************
// this class hasn't been tested because I do not have a 16f628a chip
//
class engine_16f628a : public engine_16f
{
protected:
   virtual int get_cmd_begin_prog_cycle()          { return 0x08; };
   virtual int get_cmd_bulk_erase_program_memory() { return 0x09; };
   virtual int get_cmd_bulk_erase_data_memory()    { return 0x0b; };

   virtual int get_program_data_cycle_time()       { return 6000 * 10; }; 
   virtual int get_program_prog_cycle_time()       { return 2500 * 10; };
   virtual int get_bulk_erase_cycle_time()         { return 6000 * 10; };

   ///////////////////////////////////////////////////////////////////////////
   virtual const char *get_config_bit_desc()
   {
      return "CP --- --- --- --- CPD LVP BOREN MCLRE FOSC2 PWRTE WDTE FOSC1 FOSC0";
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_code_protect_bits()
   {
      return (1<<13) | (1<<8);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_low_voltage_program_bits()
   {
      return (1<<7);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_verify_mask(word address)
   {
      // config bits 9-12 are unimplemented
      //
      if (address == 0x2007)
      {
         return ~((1 << 12) | (1 << 11) | (1 << 10) | (1 << 9));
      }

      return 0x3fff;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_blank_value(word address)
   {
      if (address >= 0x2100) return 0xff; else return 0x3fff;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(get_pin_count(), programmer::vpp_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

public:
   ///////////////////////////////////////////////////////////////////////////
   engine_16f628a(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_16f(p, program_eeprom_size, data_eeprom_size, pin_count)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_16f628a(p, prog_size, eeprom_size, pins);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      word device_id = read_config(0x2006);

      os << "Device id: 0x" << setfill('0') << setw(4) << (unsigned) device_id << endl;

      if ((device_id & 0x3FE0) != 0x10a0 &&   // 16f627a
          (device_id & 0x3FE0) != 0x1060 &&   // 16f628a
          (device_id & 0x3FE0) != 0x1100)     // 16f648a
      {
         cex::throw_error("Error: This is not a valid device id for this chip");
      }

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_chip(progress &pr)
   {
      pr.print_message("Erasing chip...");

      ostrstream osnull;
      progress_ostream pnull(osnull, "");

      erase_program_memory(pnull);
      erase_data_memory(pnull);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr)
   {
      pr.print_message("Erasing program memory and configuration data");
      
      enter_prog_verify_mode();

      send_command(get_cmd_load_configuration(), 0);

      send_command(get_cmd_bulk_erase_program_memory());

      delay(get_bulk_erase_cycle_time());

      reset_device();
   }

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_data_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase data memory because code protection is enabled.");
      }

      pr.print_message("Erasing data memory...");

      enter_prog_verify_mode();

      send_command(get_cmd_bulk_erase_data_memory());
      delay(get_bulk_erase_cycle_time());

      reset_device();
   };
};

// ***************************************************************************
class engine_16f628 : public engine_16f
{
protected:
   virtual int get_cmd_begin_prog_cycle()          { return 0x18; };
   virtual int get_cmd_begin_erase_prog_cycle()    { return 0x08; };
   virtual int get_cmd_bulk_erase_setup1()         { return 0x01; };
   virtual int get_cmd_bulk_erase_setup2()         { return 0x07; };
   virtual int get_cmd_bulk_erase_program_memory() { return 0x09; };
   virtual int get_cmd_bulk_erase_data_memory()    { return 0x0b; };

   virtual int get_program_data_cycle_time()       { return 8000 * 10; }; 
   virtual int get_program_prog_cycle_time()       { return 8000 * 10; };
   virtual int get_bulk_erase_cycle_time()         { return 5000 * 10; };

public:
   ///////////////////////////////////////////////////////////////////////////
   engine_16f628(programmer &p, int program_eeprom_size, int data_eeprom_size, int pin_count)
      : engine_16f(p, program_eeprom_size, data_eeprom_size, pin_count)
   {
   };

   ///////////////////////////////////////////////////////////////////////////
   static engine *create(programmer &p, int prog_size, int eeprom_size, int pins)
   {
      return new engine_16f628(p, prog_size, eeprom_size, pins);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual const char *get_config_bit_desc()
   {
      return "CP1 CP0 CP1 CP0 --- CPD LVP BOREN MCLRE FOSC2 PWRTE WDTE FOSC1 FOSC0";
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_code_protect_bits()
   {
      return (1<<13) | (1<<12) | (1<<11) | (1<<10) | (1<<8);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_low_voltage_program_bits()
   {
      return (1<<7);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_verify_mask(word address)
   {
      // config bit 9 is not implemented
      //
      if (address == 0x2007)
      {
         return ~(1 << 9);
      }

      return 0x3fff;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual word get_blank_value(word address)
   {
      if (address >= 0x2100) return 0xff; else return 0x3fff;
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void enter_prog_verify_mode()
   {
      m_programmer.enter_prog_verify_mode(get_pin_count(), programmer::vpp_first);
   };
   
   ///////////////////////////////////////////////////////////////////////////
   virtual void reset_device()
   {
      m_programmer.reset_device();
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void print_device_id(ostream &os)
   {
      long ios_flags = os.flags(ios::hex | ios::internal);

      word device_id = read_config(0x2006);

      os << "Device id: 0x" << setfill('0') << setw(4) << (unsigned) device_id << endl;

      if ((device_id & 0x3FE0) != 0x7a0 &&   // 16f627
          (device_id & 0x3FE0) != 0x7c0)     // 16f628
      {
         cex::throw_error("Error: This is not a valid device id for this chip");
      }

      os.flags(ios_flags);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_chip(progress &pr)
   {
      pr.print_message("Erasing chip...");

      // clear code protection first; this will not erase the program and data eeprom
      // if code protection is not enabled

      enter_prog_verify_mode();

      // 1. Execute load config command with all 0s
      //
      send_command(get_cmd_load_configuration(), 0x3fff);

      // 2. goto address 0x2007
      //
      for (int i = 0; i < 7; i++)
      {
         send_command(get_cmd_increment_address());
      }

      // 3. execute bulk erase setup1 command
      // 4. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      // 5. execute begin erase prog command
      // 6. wait tera + tprog
      //
      send_command(get_cmd_begin_erase_prog_cycle());
      delay(get_bulk_erase_cycle_time() + get_program_prog_cycle_time() + 100000 * 10);

      // 7. execute bulk erase setup1 command
      // 8. execute bulk erase setup2 command
      //
      send_command(get_cmd_bulk_erase_setup1());
      send_command(get_cmd_bulk_erase_setup2());

      reset_device();

      // erase the program and data memory - do not print any messages
      //
      ostrstream osnull;
      progress_ostream pnull(osnull, "");

      erase_program_memory(pnull);
      erase_data_memory(pnull);
   };

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_program_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase program memory because code protection is enabled.");
      }

      pr.print_message("Erasing program memory...");

      enter_prog_verify_mode();
      
      send_command(get_cmd_load_data_for_prog_memory(), 0x3FFF);

      send_command(get_cmd_bulk_erase_program_memory());

      send_command(get_cmd_begin_prog_cycle());

      delay(get_bulk_erase_cycle_time());

      reset_device();
   }

   ///////////////////////////////////////////////////////////////////////////
   virtual void erase_data_memory(progress &pr)
   {
      if ((~read_config(0x2007)) & get_code_protect_bits())
      {
         cex::throw_error("Error: Cannot erase data memory because code protection is enabled.");
      }

      pr.print_message("Erasing data memory...");

      enter_prog_verify_mode();
      
      send_command(get_cmd_load_data_for_data_memory(), 0x3FFF);

      send_command(get_cmd_bulk_erase_data_memory());

      send_command(get_cmd_begin_prog_cycle());

      delay(get_bulk_erase_cycle_time());

      reset_device();
   };
};


// ###########################################################################

// pointer to a function that creates pic programmer engines
// every pic programmer engine should have a static member called create
//
typedef engine* (*create_engine_func) (programmer &p, int prog_size, int eeprom_size, int pins);

// list of programmer engines 
//
class features_chip
{
public:
   const char *device_name;
   create_engine_func create_func;
   int prog_size;
   int eeprom_size;
   int pin_count;
};

features_chip chip_inventory[] =
{
   { "16f877",  &engine_16f87x::create,  8192,  256,  40 },
   { "16f876",  &engine_16f87x::create,  8192,  256,  28 },
   { "12f629",  &engine_12f675::create,  1024,  128,   8 },
   { "12f675",  &engine_12f675::create,  1024,  128,   8 },
   { "16f628",  &engine_16f628::create,  2048,  128,  18 },
   { "18f452",  &engine_18fxx2::create, 32768,  256,  40 },
   { "18f252",  &engine_18fxx2::create, 32768,  256,  28 },
   { "18f1320", &engine_18f1x20::create, 8192,  256,  18 },
   { 0, 0, 0, 0, 0 }
};

// list of programmer hardware
//
typedef programmer* (*create_programmer_func) ();

class features_programmer
{
public:
   const char *programmer_name;
   create_programmer_func create_func;
};

features_programmer programmer_inventory[] =
{
   { "lvp",      &programmer_lvp::create },
   { "lvp_fast", &programmer_lvp_fast::create },
   { "p16pro40", &programmer_p16pro40::create },
   { 0, 0 }
};


//////////////////////////////////////////////////////////////////////////////
string get_about_text()
{
   return string(
      "FreePicProg V1.10 (12Sep2004)\n"
      "Copyright (c) 2004 Steven Simon\n\n"

      "This software is open source and is released under the MIT license. For more "
      "information on open source licensing go to http://www.opensource.org\n\n"

      "Copyright (c) 2004 Steven Simon\n\n"

      "Permission is hereby granted, free of charge, to any person obtaining a copy "
      "of this software and associated documentation files (the \"Software\"), to deal "
      "in the Software without restriction, including without limitation the rights "
      "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell "
      "copies of the Software, and to permit persons to whom the Software is "
      "furnished to do so, subject to the following conditions:\n\n"

      "The above copyright notice and this permission notice shall be included in "
      "all copies or substantial portions of the Software.\n\n"

      "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR "
      "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, "
      "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE "
      "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER "
      "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, "
      "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN "
      "THE SOFTWARE.\n"
      );
}

// ***************************************************************************
int main(int argc, char* argv[])
{
   int i;
   programmer *prog = 0;
   engine *eng = 0;
   int return_val = 0;

   string device_name, programmer_name, option, file_name;

   try
   {
      // get command line arguments
      //
      assert(argc >= 1);

      if (argc < 2 || argc > 5) cex::throw_error("Error: Bad command line arguments.");

      if (argc > 1) programmer_name = string(argv[1]);

      if (argc > 2) device_name = string(argv[2]);

      if (argc > 3) option = string(argv[3]);

      if (argc > 4) file_name = string(argv[4]);

      // print license
      //
      if (programmer_name == "-l" && device_name == "")
      {
         cout << get_about_text();

         exit(0);
      }
    
      // convert device, programmer and option to lower case
      //
      transform(option.begin(), option.end(), option.begin(), tolower);
      transform(device_name.begin(), device_name.end(), device_name.begin(), tolower);
      transform(programmer_name.begin(), programmer_name.end(), programmer_name.begin(), tolower);

      // init delay class
      //
      high_res_delay::init();

      // init printer io class
      //
      parallel_io::init(0x378);

      if (programmer_name == "" || device_name == "") cex::throw_error("Error: Bad command line arguments.");
     
      // create the programmer object
      //
      for (i = 0; programmer_inventory[i].programmer_name != 0; i++)
      {
         if (string(programmer_inventory[i].programmer_name) == programmer_name)
         {
            prog = programmer_inventory[i].create_func();
            break;
         }
      }

      if (prog == 0) cex::throw_error("Error: Programmer %s is not supported", programmer_name.c_str());
     
      // -t ? test if programmer is connected
      //
      if (device_name == "-t" && option == "")
      {
         cout << endl << "Checking if programmer is connected...";

         if (prog->is_connected())
         {
            cout << "OK" << endl;

            delete prog;
            exit(0);
         }
         else
         {
            cout << "Not found" << endl;

            delete prog;
            exit(1);
         }
      }
      
      // create test files for all supported devices
      //
      else if (device_name == "-test_data" && option == "")
      {
         for (i = 0; chip_inventory[i].device_name != 0; i++)
         {
            engine *test_eng = chip_inventory[i].create_func(*prog,
                                           chip_inventory[i].prog_size,
                                           chip_inventory[i].eeprom_size,
                                           chip_inventory[i].pin_count);

            test_eng->create_test_data("", chip_inventory[i].device_name);

            delete test_eng;
         }

         delete prog;

         exit(0);
      }

      // find the device name and create the programming engine
      //
      for (i = 0; chip_inventory[i].device_name != 0; i++)
      {
         if (string(chip_inventory[i].device_name) == device_name)
         {
            eng = chip_inventory[i].create_func(*prog,
                                           chip_inventory[i].prog_size,
                                           chip_inventory[i].eeprom_size,
                                           chip_inventory[i].pin_count);
            break;
         }
      }

      if (eng == 0) cex::throw_error("Error: Chip %s is not supported", device_name.c_str());

      // print config
      //
      if (option == "-c")
      {
         eng->print_config(cout);
      }

      // print device id
      //
      else if (option == "-i")
      {
         eng->print_device_id(cout);
      }

      // erase chip
      //
      else if (option == "-e")
      {
         progress_ostream ps(cout, "Erasing");

         eng->erase_chip(ps);
      }   

      // erase program memory
      //
      else if (option == "-ep")
      {
         progress_ostream ps(cout, "Erasing");

         eng->erase_program_memory(ps);
      }   

      // erase data memory
      //
      else if (option == "-ed")
      {
         progress_ostream ps(cout, "Erasing");

         eng->erase_data_memory(ps);
      }   

      // blank check
      //
      else if (option == "-b")
      {
         progress_ostream ps(cout, "Blank check");

         eng->blank_check(ps);
      }   

      // read pic and save to hex file
      //
      else if (option == "-r")
      {
         if (file_name == "") cex::throw_error("Error: Bad command line arguments. File name expected.");

         ofstream ofs(file_name.c_str());

         if (ofs.fail()) cex::throw_error("Error: Cannot open output file %s", file_name.c_str());

         inhex_file_writer writer(ofs);

         progress_ostream ps(cout, "Reading");

         eng->read(writer, ps);
      }   

      // program pic
      //
      else if (option == "-p")
      {
         if (file_name == "") cex::throw_error("Error: Bad command line arguments. File name expected.");

         ifstream ifs(file_name.c_str());

         if (ifs.fail()) cex::throw_error("Error: Cannot open input file %s", file_name.c_str());

         inhex_file_reader reader(ifs);

         progress_ostream ps(cout, "Programming and verifying");

         eng->program(reader, ps);
      }   

      // verify pic
      //
      else if (option == "-v")
      {
         if (file_name == "") cex::throw_error("Error: Bad command line arguments. File name expected");

         ifstream ifs(file_name.c_str());

         if (ifs.fail()) cex::throw_error("Error: Cannot open input file %s", file_name.c_str());

         inhex_file_reader reader(ifs);

         progress_ostream ps(cout, "Verifying");

         eng->verify(reader, ps);
      }   

      // this is not a valid option
      //
      else
      {
         cex::throw_error("Error: Bad command line arguments. Invalid option: %s", option.c_str());
      }
   }
   catch (cex e)
   {
      cerr << endl << e.get_error_message() << endl << endl;

      if (e.get_error_message().find("Bad command") != string::npos)
      {
         cerr << "FreePicProg V1.10 (12Sep2004), Copyright (c) 2004 Steven Simon" << endl << endl;

         cerr << "Usage:" << endl;
         cerr << argv[0] << " -l                                 -- print license" << endl;
         cerr << argv[0] << " programmer -t                      -- hardware test" << endl;
         cerr << argv[0] << " programmer device -c               -- print config word" << endl;
         cerr << argv[0] << " programmer device -i               -- print device id" << endl;
         cerr << argv[0] << " programmer device -e               -- erase entire chip" << endl;
         cerr << argv[0] << " programmer device -ep              -- erase program memory" << endl;
         cerr << argv[0] << " programmer device -ed              -- erase data memory" << endl;
         cerr << argv[0] << " programmer device -b               -- blank check" << endl;
         cerr << argv[0] << " programmer device -r file_name.hex -- read" << endl;
         cerr << argv[0] << " programmer device -p file_name.hex -- program" << endl;
         cerr << argv[0] << " programmer device -v file_name.hex -- verify" << endl;
         cerr << "Supported programmers: lvp, lvp_fast (use with short cables), p16pro40" << endl;
         cerr << "Supported devices: 12f675, 12f629, 16f628, 16f876, 16f877," << endl;
         cerr << "                   18f252, 18f452, 18f1320" << endl;
         cerr << endl;
      }

      if (prog) prog->reset_device();

      return_val = 1;
   }
   
   if (eng) delete eng;
   if (prog) delete prog;

   cout << endl;

   return return_val;
};

1