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.