git.y1.nz

gbdk-2020

GameBoy Development Kit
download: https://git.y1.nz/archives/gbdk.tar.gz
README | Files | Log | Refs | LICENSE

gbdk-support/romusage/src/ihx_file.c

      1 // This is free and unencumbered software released into the public domain.
      2 // For more information, please refer to <https://unlicense.org>
      3 // bbbbbr 2020
      4 
      5 #include <stdio.h>
      6 #include <string.h>
      7 #include <stdlib.h>
      8 #include <unistd.h>
      9 #include <stdbool.h>
     10 #include <stdint.h>
     11 
     12 #include "common.h"
     13 #include "logging.h"
     14 #include "banks.h"
     15 #include "ihx_file.h"
     16 
     17 // Example data to parse from a .ihx file
     18 // No area names
     19 // : 01 0020 00 E9 F6
     20 // S BB AAAA RR DD CC
     21 //
     22 // S:    Start (":") (1 char)
     23 // BB:   ByteCount   (2 chars, one byte)
     24 // AAAA: Address     (4 chars, two bytes)
     25 // RR:   Record Type (2 chars, one byte. 00=data, 01=EOF, Others)
     26 // DD:   Data        (<ByteCount>  bytes)
     27 // CC:   Checksum    (2 chars, one byte. Sum record bytes, 2's complement of LSByte)
     28 /*
     29 :01002000E9F6
     30 :05002800220D20FCC9BF
     31 :070030001A22130D20FAC98A
     32 .
     33 .
     34 .
     35 :00000001FF (EOF indicator)
     36 */
     37 
     38 /* TODO/WARNING/BUG: 100% full banks
     39 It may not be possible to easily tell the difference between a perfectly filled
     40 bank (100% and no more) and an adjacent partially filled bank that starts at
     41 zero - versus - the first bank overflowed into the second that is empty.
     42 
     43 Currently the 100% bank will get merged into the next one and present as overflow
     44 */
     45 
     46 
     47 #define IS_NOT_BANK_START(addr)  (!((addr & 0x00003FFFU) == 0x00000000U))
     48 #define IS_NOT_BANK_END(addr)    (!((addr & 0x00003FFFU) == 0x00003FFFU))
     49 #define BANK_NUM(addr)  ((addr & 0xFFFFC000U) >> 14)
     50 #define ADDR_UNSET      0xFFFFFFFEU
     51 
     52 #define MAX_STR_LEN     4096
     53 #define IHX_DATA_LEN_MAX 255
     54 #define IHX_REC_LEN_MIN  (1 + 2 + 4 + 2 + 0 + 2) // Start(1), ByteCount(2), Addr(4), Rec(2), Data(0..255x2), Checksum(2)
     55 
     56 // IHX record types
     57 #define IHX_REC_DATA     0x00U
     58 #define IHX_REC_EOF      0x01U
     59 #define IHX_REC_EXTSEG   0x02U
     60 #define IHX_REC_STARTSEG 0x03U
     61 #define IHX_REC_EXTLIN   0x04U
     62 #define IHX_REC_STARTLIN 0x05U
     63 
     64 typedef struct ihx_record {
     65     uint16_t length;
     66     uint32_t byte_count;
     67     uint32_t address;
     68     uint32_t address_end;
     69     uint32_t type;
     70     uint32_t checksum; // Would prefer this be a uint8_t, but mingw sscanf("%2hhx") has a buffer overflow that corrupts adjacent data
     71 } ihx_record;
     72 
     73 uint32_t g_address_upper;
     74 
     75 // Converts sequentally stored ihx bank style (ROM0=0x0000, ROM1=0x4000, ROM2=0x8000)
     76 // into map/noi banked style (ROM0=0x0000, ROM1=0x04000, ROM2=0x14000)
     77 uint32_t ihx_bank_2_mapnoi_bank(uint32_t addr) {
     78 
     79     // If above 0x4000, upshift any address bits beyond 0x3FFF
     80     // into upper 16 bits and force 0x4000 as base address
     81     if (addr >= 0x4000)
     82         addr = (addr & 0x3FFF) | ((addr & 0xFFFFC000) << 2) + 0x4000;
     83     return addr;
     84 }
     85 
     86 
     87 // Return false if any character isn't a valid hex digit
     88 int check_hex(char * c) {
     89     while (*c != '\0') {
     90         if ((*c >= '0') && (*c <= '9'))
     91             c++;
     92         if ((*c >= 'A') && (*c <= 'F'))
     93             c++;
     94         if ((*c >= 'a') && (*c <= 'f'))
     95             c++;
     96         else
     97             return false;
     98     }
     99 
    100     return true;
    101 }
    102 
    103 
    104 // Parse and validate an IHX record
    105 int ihx_parse_and_validate_record(char * p_str, ihx_record * p_rec) {
    106 
    107         int calc_length = 0;
    108         int c;
    109         uint32_t ctemp, checksum_calc = 0; // Avoid mingw sscanf("%2hhx") buffer overflow with uint8_t
    110 
    111         // Remove trailing CR and LF
    112         p_rec->length = strlen(p_str);
    113         for (c = 0;c < p_rec->length;c++) {
    114             if (p_str[c] == '\n' || p_str[c] == '\r') {
    115                 p_str[c] = '\0';   // Replace char with string terminator
    116                 p_rec->length = c; // Shrink length to truncated size
    117                 break;             // Exit loop after finding first CR or LF
    118             }
    119         }
    120 
    121         // Only parse lines that start with ':' character (Start token for IHX record)
    122         if (p_str[0] != ':') {
    123             log_warning("Warning: IHX: Invalid start of line token for line: %s \n", p_str);
    124             return false;
    125         }
    126 
    127        // Require minimum length
    128         if (p_rec->length < IHX_REC_LEN_MIN) {
    129             log_warning("Warning: IHX: Invalid line, too few characters: %s. Is %d, needs at least %d \n", p_str, p_rec->length, IHX_REC_LEN_MIN);
    130             return false;
    131         }
    132 
    133         // Only hex characters are allowed after start token
    134         p_str++; // Advance past Start code
    135         if (check_hex(p_str)) {
    136             log_warning("Warning: IHX: Invalid line, non-hex characters present: %s\n", p_str);
    137             return false;
    138         }
    139 
    140         // Read record header: byte count, start address, type
    141         sscanf(p_str, "%2x%4x%2x", &p_rec->byte_count, &p_rec->address, &p_rec->type);
    142         p_str += (2 + 4 + 2);
    143 
    144         // Require expected data byte count to fit within record length (at 2 chars per hex byte)
    145         calc_length = IHX_REC_LEN_MIN + (p_rec->byte_count * 2);
    146         if (p_rec->length != calc_length) {
    147             log_warning("Warning: IHX: byte count doesn't match length available in record! Record length = %d, Calc length = %d, bytecount = %d \n", p_rec->length, calc_length, p_rec->byte_count);
    148             return false;
    149         }
    150 
    151         // Is this an extended linear address record? Read in offset address if so
    152         if (p_rec->type == IHX_REC_EXTLIN) {
    153             sscanf(p_str, "%4x", &g_address_upper);
    154             g_address_upper <<= 16; // Shift into upper 16 bits of address space
    155         }
    156         else if (p_rec->type == IHX_REC_DATA) {
    157 
    158             // Don't process records with zero bytes of length
    159             if (p_rec->byte_count == 0) {
    160                 log_warning("Warning: IHX: Zero length record starting at %x\n", p_rec->address);
    161                 return false;
    162             }
    163 
    164             // Apply extended linear address (upper 16 bits of address space)
    165             // Calculate end address
    166             p_rec->address |= g_address_upper;
    167             p_rec->address_end = p_rec->address + p_rec->byte_count - 1;
    168         }
    169 
    170         // Read data segment and calculate checsum of data + headers
    171         checksum_calc = p_rec->byte_count + (p_rec->address & 0xFF) + ((p_rec->address >> 8) & 0xFF) + p_rec->type;
    172         for (c = 0;c < p_rec->byte_count;c++) {
    173             sscanf(p_str, "%2x", &ctemp);
    174             p_str += 2;
    175             checksum_calc += ctemp;
    176         }
    177 
    178         // Final calculated checeksum is 2's complement of LSByte
    179         checksum_calc = (((checksum_calc & 0xFF) ^ 0xFF) + 1) & 0xFF;
    180 
    181         // Read checksum from data
    182         sscanf(p_str, "%2x", &p_rec->checksum);
    183         p_str += 2;
    184 
    185         if (p_rec->checksum != checksum_calc) {
    186             log_warning("Warning: IHX: record checksum %x didn't match calculated checksum %x\n", p_rec->checksum, checksum_calc);
    187             return false;
    188         }
    189 
    190         // For records that start in banks above the unbanked region (0x000 - 0x3FFF)
    191         // Warn if they cross the boundary between different banks
    192         if ((p_rec->address >= 0x00004000U) &&
    193             ((p_rec->address & 0xFFFFC000U) != (p_rec->address_end & 0xFFFFC000U))) {
    194             log_warning("Warning: IHX: Write from one bank spans into the next. Bank overflow? %x -> %x (bank %d -> %d)\n",
    195                    p_rec->address, p_rec->address_end, BANK_NUM(p_rec->address), BANK_NUM(p_rec->address_end));
    196         }
    197 
    198     return true;
    199 }
    200 
    201 void area_convert_and_add(area_item area) {
    202 
    203     area_item t_area;
    204 
    205     // Convert ihx banked addresses to map/noi style
    206     // End should be calculated relative to start
    207 
    208     // Records which cross from (< 0x4000) into (>= 0x4000)
    209     // could produce ROM_0 entries that should be in ROM_1.
    210     // These are not warned about earlier since it could be
    211     // legit behavior for a 32K unbanked ROM.
    212     //
    213     // Split them on the bank boundary to avoid that.
    214     if ((area.start < 0x00004000U) && (area.end >= 0x00004000U))
    215     {
    216         // Copy area and drop part in the lower unbanked bank area
    217         t_area = area;
    218         t_area.start = 0x00004000U;
    219         area_convert_and_add(t_area);
    220         // Truncate original area at unbanked ROM boundary
    221         area.end = 0x00003FFFU;
    222     }
    223 
    224     area.end   = (area.end - area.start) + ihx_bank_2_mapnoi_bank(area.start);
    225     area.start = ihx_bank_2_mapnoi_bank(area.start);
    226     banks_check(area);
    227 }
    228 
    229 
    230 int ihx_file_process_areas(char * filename_in) {
    231 
    232     char cols;
    233     char strline_in[MAX_STR_LEN] = "";
    234     FILE * ihx_file = fopen(filename_in, "r");
    235     area_item area;
    236     ihx_record ihx_rec;
    237 
    238     set_option_input_source(OPT_INPUT_SRC_IHX);
    239 
    240     // IHX record areas don't have distinct names, so turn
    241     // off duplicate suppression to avoid any being dropped.
    242     // They are also never allowed to overlap
    243     set_option_suppress_duplicates(false);
    244     set_option_all_areas_exclusive(true);
    245 
    246     // Initialize global upper address modifier
    247     g_address_upper = 0x0000;
    248 
    249     // Initialize area record
    250     snprintf(area.name, sizeof(area.name), "ihx record");
    251     area.exclusive = option_all_areas_exclusive; // Default is false
    252     area.start = ADDR_UNSET;
    253     area.end   = ADDR_UNSET;
    254 
    255     if (ihx_file) {
    256 
    257         // Read one line at a time into \0 terminated string
    258         while (fgets(strline_in, sizeof(strline_in), ihx_file) != NULL) {
    259 
    260             // Parse record, skip if fails validation
    261             if (!ihx_parse_and_validate_record(strline_in, &ihx_rec))
    262                 continue;
    263 
    264             // Process the pending record and exit if last record (EOF)
    265             // Also ignore non-default data records (don't seem to occur for gbz80)
    266             if (ihx_rec.type == IHX_REC_EOF) {
    267                 area_convert_and_add(area);
    268                 continue;
    269             } else if (ihx_rec.type == IHX_REC_EXTLIN) {
    270                 // printf("Extended linear address changed to %08x %s\n\n\n", g_address_upper, strline_in);
    271                 continue;
    272             } else if (ihx_rec.type != IHX_REC_DATA) {
    273                 log_warning("Warning: IHX: dropped record %s of type %d\n", strline_in, ihx_rec.type);
    274                 continue;
    275             }
    276 
    277             // Records are left pending (non-processed) until they don't merge
    278             // with the current incoming record *or* the final (EOF) record is found.
    279 
    280             // Try to merge with (pending) previous record if it's address-adjacent,
    281             // except when the new record starts or ends on a bank boundary
    282             // (this reduces count from 1000's since most are only 32 bytes long)
    283             if ((ihx_rec.address == area.end + 1) && IS_NOT_BANK_START(ihx_rec.address)) {
    284                 area.end = ihx_rec.address_end;  // append to previous area
    285             } else if ((ihx_rec.address_end == area.start + 1) && IS_NOT_BANK_END(ihx_rec.address_end)) {
    286                 area.start = ihx_rec.address;    // pre-pend to previous area
    287             } else {
    288                 // New record was *not* adjacent to last,
    289                 // so process the last/pending record
    290                 if (area.start != ADDR_UNSET) {
    291                     area_convert_and_add(area);
    292                 }
    293                 // Now queue current record as pending for next loop
    294                 area.start = ihx_rec.address;
    295                 area.end   = ihx_rec.address + ihx_rec.byte_count - 1;
    296             }
    297 
    298         } // end: while still lines to process
    299 
    300         fclose(ihx_file);
    301 
    302     } // end: if valid file
    303     else {
    304         log_error("Error: Failed to open input file %s\n", filename_in);
    305         return false;
    306     }
    307 
    308     return true;
    309 }
    310 

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.