ihex.tcl

#

Copyright 2017, Erik N. Johnson

FILENAME: ihex.tcl

AUTHOR: erik.johnson@jogle.us

DESCRIPTION: ihex.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.

ihex.tcl contains all the support procs for Intel Hex format, including all subtypes: I8, I16, and I32. Format identifiers supplied by this file are:

  • ihex
  • i8hex
  • i16hex
  • i32hex

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 Intel Hex Format

For details on the Intel Ihex format, see https://en.wikipedia.org/wiki/Intel_HEX

#

ProcessIhexLine

#

ProcessIhexLine is the parser for the readline proc. It takes a line of formatted input, parses out the fields with a regexp, and computes the line checksum. If the checksum is invalid, it errors. If the checksum is valid, 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.

The output list is in the order {type, address, dataList}.

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

proc ::binconvert::ProcessIhexLine {line} {
    set matchStr {:([[:xdigit:]]{2})([[:xdigit:]]{4})([[:xdigit:]]{2})([[:xdigit:]]*)([[:xdigit:]]{2})}
    if {![regexp $matchStr $line -> count addr type data chksum]} {
        set errMsg "binconvert: ProcessIhexLine: Input file syntax error: could not parse $line"
        log::error $errMsg
        error $errMsg
    }
    ;# N.B.: count is in bytes while string length is in digits.
    if {("0x$count" * 2) != [string length $data]} {
        set errMsg "binconvert: ProcessIhexLine: Incorrect data field size! \
                    Expected [expr {$count * 2}], got\
                    [string length $data].\nLine was: $line"
        log::error $errMsg
        error $errMsg
    }
    set csum 0
    foreach {digit1 digit2} [lrange [split $line ""] 1 end] {
        incr csum [expr {"0x$digit1$digit2"}]
    }
    if {($csum & 0xFF) != 0} {
        set errMsg "binconvert: ProcessIhexLine: line checksum fail: computed $csum,\
                    expected 0, line was: $line"
        log::error $errMsg
        error $errMsg
    }
    switch $type {
        00 {
#

Base Data record

            set dataL {}
            foreach {digit1 digit2} [split $data ""] {
                lappend dataL [expr {"0x$digit1$digit2"}]
            }
            return [list "DATA" [expr "0x$addr"] $dataL]
#

Last line of the file is a terminator record

        }
        01 {
            return {"EOF" {} }
#

8086-style "real-mode" segment address: upshifted 4 bits by the processor and added to the base address.

        }
        02 {
            if {$::binconvert::IhexFileType eq "I32"} {
                log::error "binconvert: ProcessIhexLine: File mixes I16 & I32 segment offsets!"
            }
            set ::binconvert::IhexFileType "I16"
#

Upshifting address by 4 bits as we output it

            return [list "SEGADDR" [expr "0x${data}0"] ]
#

8086-style execution address. upper 16 bits are loaded into the CS register; lower 16 bits into the IP register.

        }
        03 {
            if {$::binconvert::IhexFileType eq "I32"} {
                log::error "binconvert: ProcessIhexLine: File mixes I16 & I32 segment offsets!"
            }
            set ::binconvert::IhexFileType "I16"
            return [list "STARTADDR" [expr "0x$data"] "I16" ]
#

386+ style address: segment is upper 16 bits of 32-bit address, line address is lower 16 bits.

        }
        04 {
            if {$::binconvert::IhexFileType eq "I16"} {
                log::error "binconvert: ProcessIhexLine: File mixes I16 & I32 segment offsets!"
            }
            set ::binconvert::IhexFileType "I32"
#

Upshifting address by 16 bits as we output it

            return [list "SEGADDR" [expr "0x${data}0000"] ]
#

386+ style execution address: address is loaded into EIP.

        }
        05 {
            if {$::binconvert::IhexFileType eq "I16"} {
                log::error "binconvert: ProcessIhexLine: File mixes I16 & I32 segment offsets!"
            }
            set ::binconvert::IhexFileType "I32"
            return [list "STARTADDR" [expr "0x$data"] "I32" ]
        }
        default {
            set errMsg "binconvert: ProcessIhexLine: Unrecognized line type! \
                    $type not in 0-5 per spec. \nLine was: $line"
            log::error $errMsg
            error $errMsg
        }
    }
}
#

WriteIhexFile

#

WriteIhexFile will scan the segment list, look for the largest ending address, then call the appropriate output format writer. (I.e., if maxAdd <= 64K, use I8hex; if maxAddr <= 1M, use I16hex, else use I32hex. If maxAdd is >4 GB, error out... and wtf are you doing using this format with that data anyway?!) If it finds a STARTADDR segment statement with, an I16 or I32 format tag, it will immediately declare the format to match the tag.

proc ::binconvert::WriteIhexFile {segmentList outchan} {
    set bits 8
    foreach {addr dataL} $segmentList {
        set len [llength $dataL]
        ;# Have to test alpha cases first as they break all the math
        if {$addr eq "HEADER"} {
            continue
        } elseif {$addr eq "STARTADDR"} {
            if {[llength $dataL] == 1} {
                continue
            }
            if {[lindex $dataL 1] eq "I16"} {
                set bits 16
                break
            } elseif {[lindex $dataL 1] eq "I32"} {
                set bits 32
                break
            }
        } elseif {($addr + $len - 1) > 0xFFFFFFFF} {
            set errMsg "Address > 4GB too large for Intel Hex format. \n \
                        What were you thinking?"
            error $errMsg
        } elseif {(($addr + $len - 1) > 0xFFFFF) && ($bits < 32)} {
            set bits 32
            break
        } elseif {(($addr + $len - 1) > 0xFFFF) && ($bits < 16)} {
            set bits 16
        }
    }
    set formatter "WriteI${bits}File"
    log::info "Formatting output file as I${bits}hex file..."
    tailcall $formatter $segmentList $outchan
}
#

WriteI8File

#

I8hex format uses no segments; addresses are verbatim since they can by definition fit in the 16-bit field of the line. We expect to only need DATA and EOF lines.

Start by checking the input segment list to make sure we can use the format.

proc ::binconvert::WriteI8File {segmentList outchan} {
    foreach {addr dataL} $segmentList {
        set len [llength $dataL]
        if {($addr eq "HEADER") || ($addr eq "STARTADDR")} {
            ;# I8 format only uses type 0 & 1 records, so it ignores STARTADDR
            continue
        } elseif {($addr + $len - 1) > 0xFFFF} {
            set errMsg "binconvert: WriteI8File: Input data exceeds address space for I8hex format."
            error $errMsg
        }
        lappend outSegL $addr $dataL
    }
#

For each segment, write the data starting at the start address

    foreach {addr dataL} $outSegL {
        writeIhexSegment $outchan $addr $dataL
    }
#

Finish by writing the EOF marker

    puts $outchan ":00000001FF"
}
#

WriteI16File

#

I16hex format uses 8086-style "real mode" segments, where 16-bit segment address is shifted up 4 and added to all other addresses in the segment.

Start by checking the input segment list to make sure we can use the format. We will also need to examine the segment data and split any segments that (a) are longer than 64K or (b) cross a 64K boundary (not strictly necessary in-spec, but a good idea). Actually, if a segment is too large it's guaranteed to cross a boundary, so we only need the one check.

proc ::binconvert::WriteI16File {segmentList outchan} {
    foreach {addr dataL} $segmentList {
        set len [llength $dataL]
        ;# Have to test alpha cases first as they break all the math
        if {$addr eq "STARTADDR"} {
            lappend outSegL $addr $dataL
        } elseif {$addr eq "HEADER"} {
            continue
        } elseif {($addr + $len - 1) > 0xFFFFF} {
            set errMsg "binconvert: WriteI16File: Input data exceeds address space for I16hex format."
            error $errMsg
        } elseif {($addr & 0xFFFF0000) != (($addr + $len - 1) & 0xFFFF0000)} {
            log::debug "segment crosses boundary: addr = $addr; end = [expr \
                                                        {$addr + $len - 1}]"
            lappend outSegL {*}[splitSegment $addr $dataL]
        } else {
            lappend outSegL $addr $dataL
        }
    }
#

For each segment, write the data starting at the start address after subtracting off the segment address offset.

    set lastSeg -1
    foreach {addr dataL} $outSegL {
        ;# Handle CSIP first because alpha breaks math
        if {$addr eq "STARTADDR"} {
            set outS [format ":04000003%08X" [lindex $dataL 0]]
            append outS [makeIhexLineChecksum $outS]
            puts $outchan $outS
            continue
        }
        set segAddr [expr {$addr & 0xF0000}]
        set startAddr [expr {$addr - $segAddr}]
#

If we crossed a segment boundary, update & output a new segAddr.

        if {$segAddr != $lastSeg} {
            set outS [format ":02000002%04X" [expr {$segAddr >> 4}] ]
            append outS [makeIhexLineChecksum $outS]
            puts $outchan $outS
            set lastSeg $segAddr
        }
        writeIhexSegment $outchan $startAddr $dataL
    }
#

All segments processed. Write EOF marker.

    puts $outchan ":00000001FF"
}
#

WriteI32File

#

I32hex format uses extended linear addressing, where the 16-bit segment address forms the upper 16 bits of the 32 bit address, and is added to the 16-bit line address to form the full 32-bit byte address. I32hex segments must break on all 64K boundaries, and only on 64K boundaries.

Start by checking the input segment list to make sure we can use the format. We will also need to examine the segment data and split any segments that (a) are longer than 64K or (b) cross a 64K boundary. Actually, if a segment is too large it's guaranteed to cross a boundary, so we only need the one check.

proc ::binconvert::WriteI32File {segmentList outchan} {
    foreach {addr dataL} $segmentList {
        set len [llength $dataL]
        ;# Have to test alpha cases first as they break all the math
        if {$addr eq "STARTADDR"} {
            lappend outSegL $addr $dataL
        } elseif {$addr eq "HEADER"} {
            continue
        } elseif {($addr + $len - 1) > 0xFFFFFFFF} {
            set errMsg "binconvert: WriteI32File: Input data exceeds address space for I32hex format."
            error $errMsg
        } elseif {($addr & 0xFFFF0000) != (($addr + $len - 1) & 0xFFFF0000)} {
            log::debug "segment crosses boundary: addr = $addr; end = [expr \
                                                        {$addr + $len - 1}]"
            lappend outSegL {*}[splitSegment $addr $dataL]
        } else {
            lappend outSegL $addr $dataL
        }
    }
#

For each segment, write the data starting at the start address after subtracting off the segment address offset.

    set lastSeg -1
    foreach {addr dataL} $outSegL {
        ;# Handle START first because alpha breaks math
        if {$addr eq "STARTADDR"} {
            set outS [format ":04000005%08X" [lindex $dataL 0]]
            append outS [makeIhexLineChecksum $outS]
            puts $outchan $outS
            continue
        }
        set segAddr [expr {$addr & 0xFFFF0000}]
        set startAddr [expr {$addr - $segAddr}]
#

If we crossed a segment boundary, update & output a new segAddr.

        if {$segAddr != $lastSeg} {
            set outS [format ":02000004%04X" [expr {$segAddr >> 16}] ]
            append outS [makeIhexLineChecksum $outS]
            puts $outchan $outS
            set lastSeg $segAddr
        }
        writeIhexSegment $outchan $startAddr $dataL
    }
#

All segments processed. Write EOF marker.

    puts $outchan ":00000001FF"
}
#

splitSegment

#

This helper proc is used by WriteI16File and WriteI32File. It takes as input the address & data list of a segment that's too long or crosses a 64K boundary, and breaks it into multiple segments across all 64K boundaries.

It returns a new list of segments, formatted as a segment list per above.

proc ::binconvert::splitSegment {addr dataL} {
    log::debug "Splitting segment at $addr, length [llength $dataL]"
    set startAddr $addr
    while {[llength $dataL] > 0} {
        set boundary [expr {($addr & 0xFFFF0000) + 0x10000}]
        set len [expr {$boundary - $addr}]
        set outData [lrange $dataL 0 $len-1]
        lappend resultL $addr $outData
        log::debug "addr=$addr, data len=[llength $outData]"
        set dataL [lrange $dataL $len end]
        set addr $boundary
    }
    return $resultL
}
#

writeIhexSegment

#

This helper proc is common to Ihex file subformats. It takes a segment (starting address & data, with segment offset already subtracted) as input along with the file handle (not name), 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.

proc ::binconvert::writeIhexSegment {outchan addr dataL} {
    while {[llength $dataL] > 0} {
        if {[llength $dataL] > 16} {
            set lineData [lrange $dataL 0 15]
            puts $outchan [makeIhexDataLine $addr $lineData]
            set dataL [lrange $dataL 16 end]
            incr addr 16
        } else {
            set lineData $dataL
            puts $outchan [makeIhexDataLine $addr $lineData]
            set dataL [list]
            incr addr [llength $lineData]
        }
    }
}
#

makeIhexDataLine

#

This helper proc is only used by writeIhexSegment, 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::makeIhexDataLine {addr dataL} {
    set len [llength $dataL]
    set outS [format ":%02X%04X00" $len $addr]
    foreach byte $dataL {
        append outS [format "%02X" $byte]
    }
    append outS [makeIhexLineChecksum $outS]
    return $outS
}
#

makeIhexLineChecksum

#

This helper proc is common to all Ihex subformats. It takes the current line as a string of hex characters (starting with a colon, which is discarded), groups them appropriately into 2-character bytes, and sums them. It then computes a checksum value that will make the complete line sum to 0 and returns that as a string. It does not append it to the line; it leaves that to the caller.

proc ::binconvert::makeIhexLineChecksum {line} {
    set csum 0
    foreach {digit1 digit2} [lrange [split $line ""] 1 end] {
        incr csum [expr {"0x$digit1$digit2"}]
    }
    set csum [expr {(256 - $csum) % 256}]
    return [format "%02X" $csum]
}