unit SRecReader;
//***************************************************************************************
// Project Name: Motorola S-Record Reader
//  Description: Class used to read S-Record files. Supports both S19 and S28 records.
//    File Name: SRecReader.pas
//
//---------------------------------------------------------------------------------------
//                          C O P Y R I G H T
//---------------------------------------------------------------------------------------
//   Copyright (c) 2011 by Feaser    http://www.feaser.com    All rights reserved
//
//   This software has been carefully tested, but is not guaranteed for any particular
// purpose. The author does not offer any warranties and does not guarantee the accuracy,
//   adequacy, or completeness of the software and is not responsible for any errors or
//              omissions or the results obtained from use of the software.
//
//---------------------------------------------------------------------------------------
//                            L I C E N S E
//---------------------------------------------------------------------------------------
// This file is part of OpenBLT. OpenBLT is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// OpenBLT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with OpenBLT.
// If not, see <http://www.gnu.org/licenses/>.
//
// A special exception to the GPL is included to allow you to distribute a combined work
// that includes OpenBLT without being obliged to provide the source code for any
// proprietary components. The exception text is included at the bottom of the license
// file <license.html>.
//
//***************************************************************************************
interface


//***************************************************************************************
// Includes
//***************************************************************************************
uses
  Windows, Messages, SysUtils, Classes, Forms;


//***************************************************************************************
// Type Definitions
//***************************************************************************************
type
  PByte = ^Byte;
  TSRecLineType = (ltInvalid, ltS0, ltS1, ltS2, ltS3, ltS7, ltS8, ltS9);
  TSRecType = (tpS1, tpS2, tpS3, tpMixed);

type
  TSRecData = record
    val  : Byte;
    addr : LongWord;
  end;

type
  PRegion = ^TRegion;
  TRegion = record
    addr : LongWord;
    size : LongWord;
  end;

type
  TSRecReader = class(TObject)
  private
    FFileName   : String;
    FDataSize   : Longword;
    FFirstAddr  : LongWord;
    FLastAddr   : LongWord;
    FFileType   : TSRecType;
    FRegions    : TList;
    FRegionsCnt : Word;
    function    GetLineType(line: String) : TSRecLineType;
    function    GetLineSize(line: String; var size: Word) : Boolean;
    function    GetLineData(line: String; index: Word; var data: TSRecData) : Boolean;
    function    GetLineAddress(line: String; var address: LongWord) : Boolean;
    procedure   UpdateRegions(addr: LongWord; size: Word);
  public
    constructor Create;
    destructor  Destroy; override;
    function    SetFileName(fileName : String) : Boolean;
    function    GetFileName : String;
    function    GetDataSize : Longword;
    function    GetFirstAddress : Longword;
    function    GetLastAddress : Longword;
    function    GetRegionCount : Word;
    procedure   GetRegion(index : Word; var addr : Longword; var length : Longword);
    procedure   GetData(var buffer : array of Byte; addr : Longword; length : Longword);
    function    GetFileType : TSRecType;
    function    BlankCheck(addr : Longword; length : Longword) : Boolean;
  end;


implementation

//***************************************************************************************
// NAME:           Create
// PARAMETER:      none
// RETURN VALUE:   none
// DESCRIPTION:    Class constructore
//
//***************************************************************************************
constructor TSRecReader.Create;
begin
  inherited Create;
  FRegions := TList.Create;
end; //*** end of Create ***


//***************************************************************************************
// NAME:           Destroy
// PARAMETER:      none
// RETURN VALUE:   none
// DESCRIPTION:    Class destructor
//
//***************************************************************************************
destructor TSRecReader.Destroy;
var
  index : Word;
  region: PRegion;
begin
  if FRegions.Count > 0 then
  begin
    for index := 0 to FRegions.Count-1 do
    begin
      region := FRegions.Items[index];
      Dispose(region);
    end;
  end;

  FRegions.Free;
  inherited;
end; //*** end of Destroy ***


//***************************************************************************************
// NAME:           UpdateRegions
// PARAMETER:      addr: start address of data
//                 size: length of data
// RETURN VALUE:   none
// DESCRIPTION:    Processes the data by categorizing it into a region.
//
//***************************************************************************************
procedure TSRecReader.UpdateRegions(addr: LongWord; size: Word);
var
  index : Word;
  added : Boolean;
  region: PRegion;
begin
  added := False;

  if FRegions.Count > 0 then
  begin
    // loop through all existing regions
    for index := 0 to FRegions.Count-1 do
    begin
      // set pointer to this region
      region := FRegions.Items[index];

      // does data fit at the end?
      if (region^.addr+region^.size) = addr then
      begin
        // add at the end of this region
        region^.size := region^.size + size;
        added := True;
      end
      // does data fit at the start?
      else if region^.addr = (addr+size) then
      begin
        // add at the start of this region
        region^.addr := region^.addr - size;
        added := True;
      end;

      // was data added to a region?
      if added then
      begin
        Break; // no need to continue loop
      end;
    end;
  end;

  // data couldn't be added to an existing region?
  if not added then
  begin
    // create a new region and add the data there
    New(region);
    region^.addr := addr;
    region^.size := size;
    FRegions.Add(region);
  end;
end; //*** end of UpdateRegions ***


//***************************************************************************************
// NAME:           GetLineType
// PARAMETER:      Line from S-Record
// RETURN VALUE:   line type
// DESCRIPTION:    Determines what type of S-Record line we're dealing with.
//
//***************************************************************************************
function TSRecReader.GetLineType(line: String) : TSRecLineType;
begin
  Result := ltInvalid;

  if Pos('S0', UpperCase(line)) > 0 then Result := ltS0;
  if Pos('S1', UpperCase(line)) > 0 then Result := ltS1;
  if Pos('S2', UpperCase(line)) > 0 then Result := ltS2;
  if Pos('S3', UpperCase(line)) > 0 then Result := ltS3;
  if Pos('S7', UpperCase(line)) > 0 then Result := ltS7;
  if Pos('S8', UpperCase(line)) > 0 then Result := ltS8;
  if Pos('S9', UpperCase(line)) > 0 then Result := ltS9;
end; //*** end of GetLineType ***


//***************************************************************************************
// NAME:           GetLineSize
// PARAMETER:      Line from S-Record
// RETURN VALUE:   Number of data bytes
// DESCRIPTION:    Obtains the number of databytes in the specified S-Record line.
//
//***************************************************************************************
function TSRecReader.GetLineSize(line: String; var size: Word) : Boolean;
var
  unusedBytes : Byte;
begin
  Result := false; // init

  if GetLineType(line) = ltS1 then
    unusedBytes := 3 // 2 for address and 1 for checksum
  else if GetLineType(line) = ltS2 then
    unusedBytes := 4 // 3 for address and 1 for checksum
  else if GetLineType(line) = ltS3 then
    unusedBytes := 5 // 4 for address and 1 for checksum
  else
    Exit; // not a S1 or S2 line

  size := StrToInt('$' + Copy(line, 3, 2)) - unusedBytes;
  Result := true;
end; //*** end of GetLineSize ***


//***************************************************************************************
// NAME:           GetLineData
// PARAMETER:      Line from S-Record
// RETURN VALUE:   data at a specific index (starts at 0)
// DESCRIPTION:    Obtains the data at a specific index in the specified S-Record line.
//
//***************************************************************************************
function TSRecReader.GetLineData(line: String; index: Word; var data: TSRecData) : Boolean;
var
  dataOffset : Byte;
  lineSize   : Word;
  lineAddr   : LongWord;
begin
  Result := false; // init

  if GetLineType(line) = ltS1 then
    dataOffset := 9 // characters untill we reach actual data
  else if GetLineType(line) = ltS2 then
    dataOffset := 11 // characters untill we reach actual data
  else if GetLineType(line) = ltS3 then
    dataOffset := 13 // characters untill we reach actual data
  else
    Exit; // not a S1, S2 or S3 line

  // make sure index parameter is valid
  if not GetLineSize(line, lineSize) then
    Exit
  else
    if index > (lineSize-1) then Exit;

  // obtain the start address of the line
  if not GetLineAddress(line, lineAddr) then
    Exit;

  data.val := StrToInt('$' + Copy(line, dataOffset+(index*2), 2));
  data.addr := lineAddr + index;

  Result := true;
end; //*** end of GetLineData ***


//***************************************************************************************
// NAME:           GetLineAddress
// PARAMETER:      Line from S-Record
// RETURN VALUE:   Start address
// DESCRIPTION:    Obtains the start address as specified in the S-Record line.
//
//***************************************************************************************
function TSRecReader.GetLineAddress(line: String; var address: LongWord) : Boolean;
var
  addrLength : Byte;
begin
  Result := false; // init

  if GetLineType(line) = ltS1 then
    addrLength := 4 // 2 byte address
  else if GetLineType(line) = ltS2 then
    addrLength := 6 // 3 byte address
  else if GetLineType(line) = ltS3 then
    addrLength := 8 // 4 byte address
  else
    Exit; // not a S1, S2 or S3 line

  address := StrToInt('$' + Copy(line, 5, addrLength));
  Result := true;
end; //*** end of GetLineAddress ***


//***************************************************************************************
// NAME:           SetFileName
// PARAMETER:      Filename with full path
// RETURN VALUE:   True when the S-Record was valid and could be loaded, otherwise False.
// DESCRIPTION:    Function verifies that the file is actually a S-Record. If so, then
//                 the most class properties are set based on the info in the S-Record.
//
//***************************************************************************************
function TSRecReader.SetFileName(fileName : String) : Boolean;
var
  SRecFile   : TextFile;
  Line       : string;
  ValidSRec  : Boolean;
  LineSize   : Word;
  LineAddr   : LongWord;
  S1RecFound : Boolean;
  S2RecFound : Boolean;
  S3RecFound : Boolean;
begin
  // first reset all the internal properties
  FFileName  := '';
  FDataSize  := 0;
  FFirstAddr := $ffffffff;
  FLastAddr  := 0;
  FFileType := tpS1;
  FRegionsCnt := 0;
  FRegions.Clear;

  // init locals
  S1RecFound := false;
  S2RecFound := false;
  S3RecFound := false;
  Result := false;
  ValidSRec := false;

  // 1. Verify if file exists
  if not FileExists(fileName) then Exit;

  // 2. Verify if file contains S-Records
  AssignFile(SRecFile, fileName); // get file handle
  Reset(SRecFile); // go to start of file
  while not Eof(SRecFile) do
  begin
    ReadLn(SRecFile, Line); // read line from file
    if (GetLineType(Line) = ltS1) or (GetLineType(Line) = ltS2)  or
       (GetLineType(Line) = ltS3) then
    begin
      ValidSRec := true;
      Break; // valid S-Record
    end;
  end;
  CloseFile(SRecFile); // release file
  if not ValidSRec then Exit;

  // 3. Calculate datasize, first address and last address
  AssignFile(SRecFile, fileName); // get file handle
  Reset(SRecFile); // go to start of file
  while not Eof(SRecFile) do
  begin
    ReadLn(SRecFile, Line); // read line from file
    LineSize := 0; // init
    if GetLineSize(Line, LineSize) then
      FDataSize := FDataSize + LineSize; // add to previous value

    if GetLineAddress(Line, LineAddr) then
    begin
      if LineAddr < FFirstAddr then
        FFirstAddr := LineAddr;

      if LineAddr > FLastAddr then
        FLastAddr := LineAddr + LineSize -1;
    end;

    // check line type
    if GetLineType(line) = ltS1 then
      S1RecFound := true;
    if GetLineType(line) = ltS2 then
      S2RecFound := true;
    if GetLineType(line) = ltS3 then
      S3RecFound := true;

  end;
  CloseFile(SRecFile); // release file

  // set file type
  if (S1RecFound) and (not S2RecFound) and (not S3RecFound) then
    FFileType := tpS1
  else if (S2RecFound) and (not S1RecFound) and (not S3RecFound) then
    FFileType := tpS2
  else if (not S2RecFound) and (not S1RecFound) and (S3RecFound) then
    FFileType := tpS3
  else
    FFileType := tpMixed;

  // 4. Determine regions
  AssignFile(SRecFile, fileName); // get file handle
  Reset(SRecFile); // go to start of file
  while not Eof(SRecFile) do
  begin
    ReadLn(SRecFile, Line); // read line from file
    LineSize := 0; // init

    if GetLineAddress(Line, LineAddr) then
    begin
      if GetLineSize(Line, LineSize) then
      begin
        UpdateRegions(LineAddr, LineSize);
      end;
    end;
  end;
  CloseFile(SRecFile); // release file

  // set region count
  FRegionsCnt := FRegions.Count;

  // 5. Verify properties and if ok, set the FFilename  property
  if (FDataSize <> 0) and (FFirstAddr <> $ffffffff) and (FLastAddr <> 0) then
  begin
    FFileName  := fileName; // set the filename property
    Result := true;
  end;
end; //*** end of SetFileName ***


//***************************************************************************************
// NAME:           GetFileName
// PARAMETER:      none
// RETURN VALUE:   Filename with full path or '' when invalid.
// DESCRIPTION:    S-Record filename that is configured to be read be this class.
//
//***************************************************************************************
function TSRecReader.GetFileName : String;
begin
  Result := FFileName;
end; //*** end of GetFileName ***


//***************************************************************************************
// NAME:           GetDataSize
// PARAMETER:      none
// RETURN VALUE:   Size of data.
// DESCRIPTION:    Obtains the number of databytes in the S-Record. 0xFF values are
//                 not included.
//
//***************************************************************************************
function TSRecReader.GetDataSize : Longword;
begin
  Result := FDataSize;
end; //*** end of GetDataSize ***


//***************************************************************************************
// NAME:           GetFirstAddress
// PARAMETER:      none
// RETURN VALUE:   First address in S-Record.
// DESCRIPTION:    Obtains the first memory address with data in the S-Record.
//
//***************************************************************************************
function TSRecReader.GetFirstAddress : Longword;
begin
  Result := FFirstAddr;
end; //*** end of GetFirstAddress ***


//***************************************************************************************
// NAME:           GetLastAddress
// PARAMETER:      none
// RETURN VALUE:   Last address in S-Record.
// DESCRIPTION:    Obtains the last memory address with data in the S-Record.
//
//***************************************************************************************
function TSRecReader.GetLastAddress : Longword;
begin
  Result := FLastAddr;
end; //*** end of GetLastAddress ***


//***************************************************************************************
// NAME:           GetRegionCount
// PARAMETER:      none
// RETURN VALUE:   Number of address regions
// DESCRIPTION:    Obtains the number of address regions found in the S-record.
//
//***************************************************************************************
function TSRecReader.GetRegionCount : Word;
begin
  Result := FRegionsCnt;
end;  //*** end of GetRegionCount ***


//***************************************************************************************
// NAME:           GetRegion
// PARAMETER:      index: region index to retrieve info from
//                 addr: destination for region's start address
//                 length: destination for region's length
// RETURN VALUE:   none
// DESCRIPTION:    Obtains the address region info.
//
//***************************************************************************************
procedure TSRecReader.GetRegion(index : Word; var addr : Longword; var length : Longword);
var
  region: PRegion;
begin
  // initialize return values
  addr := 0;
  length := 0;

  if FRegions.Count > 0 then
  begin
    if index < FRegions.Count then
    begin
      // set pointer to this region
      region := FRegions.Items[index];

      // obtain region info
      addr := region^.addr;
      length := region^.size;
    end;
  end;
end; //*** end of GetRegion ***


//***************************************************************************************
// NAME:           GetData
// PARAMETER:      buffer is a pointer to a byte buffer where the data will be written
//                 to from memory address addr to addr+length.
// RETURN VALUE:   none
// DESCRIPTION:    Obtains the data in the S-Record for a memory block that starts at
//                 addr and goes until addr+length and stores the data in buffer. empty
//                 data will be filled with 0xFF.
//
//***************************************************************************************
procedure TSRecReader.GetData(var buffer : array of Byte; addr : Longword; length : Longword);
var
  data        : TSRecData;
  line        : string;
  lineAddr    : LongWord;
  lineSize    : Word;
  SRecFile    : TextFile;
  cnt         : Word;
  ok2Continue : boolean;
begin
  // check parameters
  if length = 0 then Exit;

  // first init entire buffer to 0xff
  for cnt := 0 to length-1  do
  begin
    buffer[cnt] := $ff;
  end;

  // go through each line to see if it has data for us
  AssignFile(SRecFile, FFileName); // get file handle
  Reset(SRecFile); // go to start of file
  while not Eof(SRecFile) do
  begin
    // this can take a while so process messages to not stall the parent app
    Application.ProcessMessages;
    
    ReadLn(SRecFile, line); // read line from file
    ok2Continue := true; // init

    // obtain line properties
    if not GetLineAddress(line, lineAddr) then ok2Continue := false;
    if not GetLineSize(line, lineSize) then ok2Continue := false;

    if ok2Continue then
    begin
      // process all data on the line
      for cnt := 0 to lineSize-1 do
      begin
        // read data info
        if not GetLineData(line, cnt, data) then ok2Continue := false;
        if ok2Continue then
        begin
          // is this one for us?
          if (data.addr >= addr) and (data.addr <= (addr+length-1)) then
          begin
            // store it in the memory buffer
            buffer[data.addr-addr] := data.val;
          end;
        end;
      end;
    end;

  end;
  CloseFile(SRecFile); // release file
end; //*** end of GetData ***


//***************************************************************************************
// NAME:           GetFileType
// PARAMETER:      none
// RETURN VALUE:   S-Record file type
// DESCRIPTION:    Determines is the file contains just S1 data lines, just S2 data
//                 lines. or a combination of these two.
//
//***************************************************************************************
function TSRecReader.GetFileType : TSRecType;
begin
  Result := FFileType;
end; //*** end of GetFileType ***


//***************************************************************************************
// NAME:           BlankCheck
// PARAMETER:      checks from addr to addr+length-1.
// RETURN VALUE:   true if all bytes are 0xff, false otherwise
// DESCRIPTION:    Checks if a memory range in the S-Record file is empty (0xff) or not.
//
//***************************************************************************************
function TSRecReader.BlankCheck(addr : Longword; length : Longword) : Boolean;
var
  buffer : array of Byte;
  cnt    : LongWord;
begin
  Result := true;
  SetLength(buffer, length); // init size of the dynamic array
  GetData(buffer, addr, length); // fill it with the data contents from the S-Record

  for cnt := 0 to length-1 do
  begin
    if buffer[cnt] <> $ff then
    begin
      Result := false; // memory range is not blank
      Break; // no need to continue loop
    end;
  end;
end; //*** end of BlankCheck ***

end.
//******************************** end of SRecReader.pas ********************************

