srec.tcl

#

Copyright 2017, Erik N. Johnson

FILENAME: srec.tcl

AUTHOR: erik.johnson@jogle.us

DESCRIPTION: srec.tcl is part of the binconvert package. binconvert is a package that reads & writes EEPROM memory files in multiple formats. It converts the data to & from a Tcl representation as a list of data segments, which is available for processing.

srec.tcl contains all the support procs for Motorola SREC format, including all subtypes: S19, S28, and S37. Format identifiers supplied by this file are:

  • srec
  • s19
  • s28
  • s37

This package documentation is auto-generated with Pycco: https://pycco-docs.github.io/pycco/

Use "pycco filename" to re-generate HTML documentation in ./docs .

#

Procedures for Motorola SREC Format

For details on the Motorola SREC format, see: https://en.wikipedia.org/wiki/SREC_(file_format)

#

ProcessSrecLine

#

ProcessSrecLine takes a line of formatted input and parses out the fields with a regexp, and computes and checks the line checksum. If the checksum is valid, it checks the byte count against the number of actual digits in the line. If those match, it returns a list of up to three fields: the symbolic type of the line per the spec, the local address, and/or the list of data bytes, either of which may not exist depending on the type.

If the parsing or checksum fail, error out - there's no real recovery from a truly corrupted file.

proc ::binconvert::ProcessSrecLine {line} {
    set matchStr {S([[:digit:]])([[:xdigit:]]{2})([[:xdigit:]]*)([[:xdigit:]]{2})}
    if {![regexp $matchStr $line -> type count payload chksum]} {
#

Some SREC implementations put in comments, though it's not in the strict format. If this is a comment, treat it as a NoOp.

        if {([string index $line 0] eq ";") || ($line eq "")} {
            log::debug "Found COMMENT"
            return {NOP {} }
        }
#

Not a comment. Bad input data - error out of it.

        set errMsg "binconvert: ProcessSrecLine: Input file syntax error: \
                    could not parse $line (line $::binconvert::SrecLineCount)"
        log::error $errMsg
        error $errMsg
    }
#

N.B.: count is in bytes while string length is in digits.

    if {("0x$count" * 2) != ([string length $payload] + 2)} {
        set errMsg "binconvert: ProcessSrecLine: Incorrect record length! \
                    Expected [expr {$count * 2}], got\
                    [expr {[string length $payload] + 2}].\nLine was: $line"
        log::error $errMsg
        error $errMsg
    }
    set csum 0
    foreach {digit1 digit2} [lrange [split $line ""] 2 end] {
        incr csum [expr {"0x$digit1$digit2"}]
    }
    if {($csum & 0xFF) != 0xFF} {
        set errMsg "binconvert: ProcessSrecLine: line checksum fail. Computed
                    [expr {$csum & 0xFF}], expected 0xFF, line was: $line"
        log::error $errMsg
        error $errMsg
    }
    switch $type {
        0 {
#

Header

            set dataL {}
            if {[string range $payload 0 3] ne "0000"} {
                set errMsg "binconvert: ProcessSrecLine: invalid address\
                            in header. Spec requires '0000', got \
                            [string range $payload 0 3]. Line was: $line"
                log::error $errMsg
                error $errMsg
            }
            set ::binconvert::SrecLineCount 0
            foreach {digit1 digit2} [split [string range $payload 4 end] ""] {
                lappend dataL [expr {"0x$digit1$digit2"}]
            }
            return [list "HEADER" $dataL]
        }
        1 {
#

16-bit address

            set dataL {}
            set addr [expr {[format "0x%s" [string range $payload 0 3]]}]
            foreach {digit1 digit2} [split [string range $payload 4 end] ""] {
                lappend dataL [expr {"0x$digit1$digit2"}]
            }
            incr ::binconvert::SrecLineCount
            return [list "DATA" $addr $dataL]
        }
        2 {
#

24-bit address

            set dataL {}
            set addr [expr {[format "0x%s" [string range $payload 0 5]]}]
            foreach {digit1 digit2} [split [string range $payload 6 end] ""] {
                lappend dataL [expr {"0x$digit1$digit2"}]
            }
            incr ::binconvert::SrecLineCount
            return [list "DATA" $addr $dataL]
        }
        3 {
#

32-bit address

            set dataL {}
            set addr [expr {[format "0x%s" [string range $payload 0 7]]}]
            foreach {digit1 digit2} [split [string range $payload 8 end] ""] {
                lappend dataL [expr {"0x$digit1$digit2"}]
            }
            incr ::binconvert::SrecLineCount
            return [list "DATA" $addr $dataL]
        }
        5 {
#

16-bit line count

            if {$::binconvert::SrecLineCount != [expr {"0x$payload"}]} {
                set errMsg "binconvert: ProcessSrecLine: Line count does\
                            not match!  [expr {"0x$payload"}] lines expected,\
                            $::binconvert::SrecLineCount lines seen."
                log::error $errMsg
                error $errMsg
            }
            return [list "NOP" {}]
        }
        6 {
#

24-bit line count

            if {$::binconvert::SrecLineCount != [expr {"0x$payload"}]} {
                set errMsg "binconvert: ProcessSrecLine: Line count does\
                            not match!  [expr {"0x$payload"}] lines expected,\
                            $::binconvert::SrecLineCount lines seen."
                log::error $errMsg
                error $errMsg
            }
            return [list "NOP" {}]
        }
        7 {
#

32-bit start address

            if {[expr {"0x$payload"}] == 0} {
                return { "EOF" {} }
            }
            return [list "STARTADDR" [expr {"0x$payload"} ] ]
        }
        8 {
#

24-bit start address

            if {[expr {"0x$payload"}] == 0} {
                return { "EOF" {} }
            }
            return [list "STARTADDR" [expr {"0x$payload"} ] ]
        }
        9 {
#

16-bit start address

            if {[expr {"0x$payload"}] == 0} {
                return { "EOF" {} }
            }
            return [list "STARTADDR" [expr {"0x$payload"} ] ]
        }
        default {
            set errMsg "binconvert: ProcessSrecLine: Invalid record type! \
                    $type not in 0-3 or 5-9 per spec.\nLine was: $line"
            log::error $errMsg
            error $errMsg
        }
    }
}
#

WriteSrecFile

#

WriteSrecFile will scan the segment list & look for the largest ending address to determine the appropriate output format . (I.e., if maxAdd <= 64K, use S37; if maxAddr <= 16M, use S28, else use S19. If maxAdd is >4 GB, error out... and wtf are you doing using this format with that data anyway?!) If user specifies a smaller subformat, it will log an error message and use the correct (larger) format; if the user specified subformat is larger it will use that.

It starts by scanning the segment list. If it finds a HEADER or STARTADDR segment, it will save them for later use at the appropriate time. Data segments get added to a working list. Max address is used to determine which output format is selected.

proc ::binconvert::WriteSrecFile {segmentList outchan args} {
    set filetype S19
    set headerData {0}
    set startAddr 0
    foreach {addr dataL} $segmentList {
        set len [llength $dataL]
        ;# Have to test alpha cases first as they break all the math
        if {$addr eq "HEADER"} {
            set headerData $dataL
            continue
        } elseif {$addr eq "STARTADDR"} {
            set startAddr [lindex $dataL 0]
            continue
        } elseif {($addr + $len - 1) > 0xFFFFFFFF} {
            set errMsg "binConvert: WriteSrecFile: Address > 4GB too large for\
                        Motorola SREC format.\nWhat were you thinking?"
            error $errMsg
        } elseif {(($addr + $len - 1) > 0xFFFFF) && ($bits < 32)} {
            set filetype S37
        } elseif {(($addr + $len - 1) > 0xFFFF) && ($bits < 16)} {
            set filetype S28
        }
        lappend outSegL $addr $dataL
    }
#

If the user selected a format, override the self-detected filetype, if and only if the data will fit in that format.

    if {($args ne {})} {
        set subformat [lindex $args 0]
        if {($subformat < $filetype)} {
            log::error "binconvert: writeSrecFile: User specified output format\
                    cannot hold addresses found in data. Using format $filetype."
        } elseif {$subformat ni {S19 S28 S37}} {
            log::error "binconvert: writeSrecFile: User specified subformat\
                    $subformat not known.  Valid subformats are S19, S28, &\
                    S37.  Using format $filetype."
        } else {
            set filetype $subformat
        }
    }
    log::info "Formatting output file as $filetype file..."
#

Our pre-scan is done. Let's start writing the output file.

#

Write out the header. If we didn't find any HEADER segments, headerData was initialized to a safe empty string. Byte Count is length of (address (2) + Checksum (1) + header data (?))

    set len [expr {[llength $headerData] + 3}]
    set outS [format "S0%02X0000" $len]
    foreach byte $headerData {
        append outS [format "%02X" $byte]
    }
    append outS [makeSrecLineChecksum $outS]
    puts $outchan $outS
#

For each data segment, write the data starting at the start address and continuing with data lines until we run out of data.

    set lineCount 0
    foreach {addr dataL} $outSegL {
        incr lineCount [WriteSrecSegment $outchan $filetype $addr $dataL]
    }
#

Finish by writing the Line Count and the Start Address. Record type for line count depends on how many bytes needed to store number.

    if {$lineCount < 0xFFFF} {
        set outS [format "S503%04X" $lineCount]
    } else {
        set outS [format "S604%06X" $lineCount]
    }
    append outS [makeSrecLineChecksum $outS]
    puts $outchan $outS
#

The type of terminator record to use depends on the file type.

    switch $filetype {
        S19 { set fmtS "S903%04X" }
        S28 { set fmtS "S804%06X" }
        S37 { set fmtS "S705%08X" }
    }
    set outS [format $fmtS $startAddr]
    append outS [makeSrecLineChecksum $outS]
    puts $outchan $outS
}
#

WriteSrecSegment

#

This helper proc is common to Srec file subformats. It takes a segment (starting address & data, with segment offset already subtracted) as input along with the file handle (not name) and subtype, and writes to the file handle a sequence of correctly formatted lines, 16 data bytes per line, until all the segment data has been written. If the segment data length is not a multiple of 16 bytes, the last line will be the short line. It keeps track of the number of lines written and returns it.

proc ::binconvert::WriteSrecSegment {outchan filetype addr dataL} {
    set count 0
    while {[llength $dataL] > 0} {
        if {[llength $dataL] > 16} {
            set lineData [lrange $dataL 0 15]
            puts $outchan [makeSrecDataLine $filetype $addr $lineData]
            set dataL [lrange $dataL 16 end]
            incr addr 16
        } else {
            set lineData $dataL
            puts $outchan [makeSrecDataLine $filetype $addr $lineData]
            set dataL [list]
            incr addr [llength $lineData]
        }
        incr count
    }
    return $count
}
#

makeSrecDataLine

#

This helper proc is only used by writeSrecSegment, ensuring that data line formatting is only done in one place in the code. It returns the line as a string; it does not attempt to output it.

proc ::binconvert::makeSrecDataLine {filetype addr dataL} {
    set len [llength $dataL]
    switch $filetype {
        S19 { set outS [format "S1%02X%04X" [expr {$len + 3}] $addr]}
        S28 { set outS [format "S2%02X%06X" [expr {$len + 4}] $addr]}
        S37 { set outS [format "S3%02X%08X" [expr {$len + 5}] $addr]}
    }
    foreach byte $dataL {
        append outS [format "%02X" $byte]
    }
    append outS [makeSrecLineChecksum $outS]
    return $outS
}
#

makeSrecLineChecksum

#

This helper proc is common to all Srec subformats. It takes the current line as a string of hex characters (starting with the type field, which is discarded), groups them appropriately into 2-character bytes, and sums them. It then computes a checksum value that is the 1's complement of the least significant byte of the result, and returns that as a string. It does not append it to the line; it leaves that to the caller.

proc ::binconvert::makeSrecLineChecksum {line} {
    set csum 0
    foreach {digit1 digit2} [lrange [split $line ""] 2 end] {
        incr csum [expr {"0x$digit1$digit2"}]
    }
    set csum [expr {~$csum & 0xFF}]
    return [format "%02X" $csum]
}