Home | History | Annotate | Line # | Download | only in haudfw
      1 /*	$NetBSD: haudfw.c,v 1.1 2026/06/11 08:16:44 rumble Exp $	*/
      2 
      3 /*
      4  * Copyright (c) 2025 Stephen M. Rumble <rumble (at) ephemeral.org>
      5  *
      6  * Permission to use, copy, modify, and distribute this software for any
      7  * purpose with or without fee is hereby granted, provided that the above
      8  * copyright notice and this permission notice appear in all copies.
      9  *
     10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     17  */
     18 
     19 /*
     20  * This program facilitates loading the SGI Indigo's "Hollywood Audio" Motorola
     21  * 56K DSP firmware by translating a loadable text file into a binary blob.
     22  *
     23  * The input is a .lod text file and the output is a 128KiB binary that the
     24  * sgimips/haud(4) driver can copy directly into the DSP's SRAM at byte offset
     25  * zero.
     26  *
     27  * The DSP's memory space is provided by three 8-bit 32KiB SRAMs, which are
     28  * combined to provide 32768 24-bit DSP words. On the CPU side, this memory is
     29  * mapped onto 32768 contiguous 32-bit words via the HPC chip.
     30  *
     31  * The DSP's X, Y, and P address spaces map on to different subsets of the SRAM
     32  * memory space. The .lod file specifies where words are loaded into each DSP
     33  * address space. These addresses need to be translated into offsets into the
     34  * raw SRAM mapped into the CPU's address space in order to load code and data
     35  * in the correct locations.
     36  *
     37  * Mappings between the DSP addresses and CPU/SRAM addresses were determined by
     38  * using a test .lod file and IRIX's /usr/etc/audio/dsploader. Dumps of the SRAM
     39  * showed where the utility wrote test various patterns defined in the test
     40  * file's fabricated _DATA entries. The results are as follows below and appear
     41  * to indicate that the DSP is configured in mode 2 ("normal expanded mode"),
     42  * with 0xe000 as the post-reset start address. The ranges are inclusive.
     43  *
     44  *             Section      DSP Word Addr        CPU Word Addr
     45  *                P        0xd000 - 0xefff      0x1000 - 0x2fff
     46  *                X        0xb000 - 0xbfff      0x7000 - 0x7fff
     47  *                X        0xc000 - 0xcfff      0x0000 - 0x0fff
     48  *                Y        0xb000 - 0xefff      0x3000 - 0x6fff
     49  *
     50  * Note that the above CPU addresses are 32-bit word offsets from the HPC's
     51  * SRAM base address. So multiply by 4 to get the CPU byte address offset.
     52  */
     53 
     54 #include <err.h>
     55 #include <errno.h>
     56 #include <limits.h>
     57 #include <stdarg.h>
     58 #include <stdbool.h>
     59 #include <stdio.h>
     60 #include <stdlib.h>
     61 #include <string.h>
     62 #include <unistd.h>
     63 #include <arpa/inet.h>
     64 
     65 // Dump a C array representing the data to load into SRAM to stdout.
     66 bool c_output = false;
     67 
     68 // Larger values > 0 increase debugging output to stderr.
     69 int verbosity = 0;
     70 
     71 const char whitespace[] = " \t\r\n";
     72 
     73 static void
     74 debug_printf(int level, const char *format, ...)
     75 {
     76 	if (verbosity >= level) {
     77 		va_list ap;
     78 		va_start(ap, format);
     79 		vfprintf(stderr, format, ap);
     80 		va_end(ap);
     81 	}
     82 }
     83 
     84 static void
     85 check(bool constraint, const char *format, ...)
     86 {
     87 	if (!constraint) {
     88 		va_list ap;
     89 		va_start(ap, format);
     90 		fprintf(stderr, "error: ");
     91 		verrx(1, format, ap);
     92 		va_end(ap);
     93 	}
     94 }
     95 
     96 __attribute__((noreturn))
     97 static void
     98 fail(const char *format, ...)
     99 {
    100 	va_list ap;
    101 	va_start(ap, format);
    102 	check(false, format, ap);
    103 	// Never hit. exit() squelches warning.
    104 	va_end(ap);
    105 	exit(1);
    106 }
    107 
    108 static void
    109 usage(void)
    110 {
    111 	fprintf(stderr, "usage: %s [-cv] input.lod output.bin\n",
    112 	    getprogname());
    113 	exit(1);
    114 }
    115 
    116 static int
    117 dsp_to_cpu_address(char section, int dsp_addr)
    118 {
    119 	switch (section) {
    120 	case 'P':
    121 		check(0xd000 <= dsp_addr && dsp_addr <= 0xefff,
    122 		      "P section dsp_addr 0x%04x not in [0xd000,0xefff]",
    123 		      dsp_addr);
    124 		return dsp_addr - 0xc000;
    125 	case 'X':
    126 		if (0xb000 <= dsp_addr && dsp_addr <= 0xbfff) {
    127 			return dsp_addr - 0x4000;
    128 		} else if (0xc000 <= dsp_addr && dsp_addr <= 0xcfff) {
    129 			return dsp_addr - 0xc000;
    130 		} else {
    131 			fail("X section dsp_addr 0x%04x not in either "
    132 			    "[0xb000,0xbfff] or [0xc000,0xcfff]", dsp_addr);
    133 		}
    134 	case 'Y':
    135 		check(0xb000 <= dsp_addr && dsp_addr <= 0xefff,
    136 		      "Y section dsp_addr 0x%04x not in [0xb000,0xefff)",
    137 		      dsp_addr);
    138 		return dsp_addr - 0x8000;
    139 	default:
    140 		fail("unknown section type '%c'", section);
    141 	}
    142 }
    143 
    144 static void
    145 parse_data_line(const char *line, char *section, int *dsp_data_offset,
    146     int* section_data_count) {
    147 	char *line_copy = strdup(line);
    148 	char *field = strtok(line_copy, whitespace);
    149 	for (int i = 0; field != NULL; i++, field = strtok(NULL, whitespace)) {
    150 		switch (i) {
    151 		case 0:
    152 			check(strcmp(field, "_DATA") == 0,
    153 			    "unexpected _DATA header: \"%s\"", field);
    154 			break;
    155 		case 1:
    156 			*section = field[0];
    157 			check(strlen(field) == 1 && strspn(field, "XYPL") == 1,
    158 			    "unexpected section type: \"%s\"", field);
    159 			break;
    160 		case 2:
    161 			*dsp_data_offset = strtol(field, NULL, 16);
    162 			break;
    163 		default:
    164 			fail("too many fields in _DATA line: \"%s\", "
    165 			   "expected 3", line);
    166 		}
    167 	}
    168 	*section_data_count = 0;
    169 	free(line_copy);
    170 }
    171 
    172 int
    173 main(int argc, char **argv)
    174 {
    175 	int ch;
    176 	while ((ch = getopt(argc, argv, "cv")) != -1) {
    177 		switch (ch) {
    178 		case 'c':
    179 			c_output = true;
    180 			break;
    181 		case 'v':
    182 			verbosity++;
    183 			break;
    184 		case '?':
    185 		default:
    186 			usage();
    187 		}
    188 	}
    189 	argv += optind;
    190 	argc -= optind;
    191 	if (argc != 2) {
    192 		usage();
    193 	}
    194 
    195 	check(strcmp(argv[0], argv[1]) != 0,
    196 	    "input and output files must be different");
    197 
    198 	const int firmware_words = 32768;
    199 	u_int32_t *firmware = malloc(firmware_words * sizeof(u_int32_t));
    200 	check(firmware != NULL, "unable to malloc buffer for firmware: %s",
    201 	    strerror(errno));
    202 	memset(firmware, 0, firmware_words * sizeof(u_int32_t));
    203 
    204 	FILE *fp = fopen(argv[0], "r");
    205 	check(fp != NULL, "unable to open input file %s: %s",
    206 	    argv[0], strerror(errno));
    207 
    208 	int dsp_data_offset = 0;
    209 	int section_data_count = 0;
    210 	char section = '\0';
    211 	char line[100];
    212 	for (int lineno = 1; fgets(line, sizeof(line), fp) != NULL; lineno++) {
    213 		if (strspn(line, whitespace) == strlen(line)) {
    214 			// skip blank lines
    215 		} else if (strncmp(line, "_START", 6) == 0) {
    216 			debug_printf(1, "_START on line %d\n", lineno);
    217 		} else if (strncmp(line, "_END", 4) == 0) {
    218 			if (section != '\0') {
    219 				debug_printf(1, "%c section had %d words\n",
    220 				    section, section_data_count);
    221 			}
    222 			debug_printf(1, "_END on line %d\n", lineno);
    223 			break;
    224 		} else if (strncmp(line, "_DATA", 5) == 0) {
    225 			if (section != '\0') {
    226 				debug_printf(1, "%c section had %d words\n",
    227 				    section, section_data_count);
    228 			}
    229 			debug_printf(1, "_DATA on line %d\n", lineno);
    230 			parse_data_line(line, &section, &dsp_data_offset,
    231 					&section_data_count);
    232 			debug_printf(1, "%c data section starting at dsp "
    233 			    "offset 0x%04x\n", section, dsp_data_offset);
    234 		} else {
    235 			// We must be in a _DATA section
    236 			check(section != '\0', "parse error on line %d: not in "
    237 			    "a _DATA section", lineno);
    238 
    239 			char *word_str = strtok(line, whitespace);
    240 			while (word_str != NULL) {
    241 				u_int32_t word = strtol(word_str, NULL, 16);
    242 
    243 				int cpu_offset = dsp_to_cpu_address(
    244 				    section != 'L' ? section : (
    245 				    section_data_count % 2 ?  'Y' : 'X'),
    246 				    dsp_data_offset);
    247 				if (section_data_count == 0 || section == 'L') {
    248 					debug_printf(1, "  cpu offset 0x%04x "
    249 					    " (KSEG1 address 0x%08x)\n",
    250 					    cpu_offset,
    251 					    0xbfbe0000 + cpu_offset * 4);
    252 				}
    253 
    254 				check(cpu_offset >= 0 &&
    255 				    cpu_offset < firmware_words,
    256 				    "cpu_offset %d is out of bounds for "
    257 				    "section %c, on line %d", cpu_offset,
    258 				    section, lineno);
    259 
    260 				firmware[cpu_offset] = word;
    261 
    262 				section_data_count++;
    263 				dsp_data_offset++;
    264 
    265 				word_str = strtok(NULL, whitespace);
    266 			}
    267 		}
    268 	}
    269 	fclose(fp);
    270 
    271 	if (c_output) {
    272 		printf("// Firmware in big-endian format:\n");
    273 		printf("u_int32_t firmware[%d] = {\n", firmware_words);
    274 		u_int32_t zeroes[16];
    275 		memset(zeroes, 0, sizeof(zeroes));
    276 		for (int i = 0; i < firmware_words; i += 4) {
    277 			if (i > 0) {
    278 				printf(",\n");
    279 			}
    280 			if (i < firmware_words - 16 &&
    281 			    memcmp(&firmware[i], zeroes, sizeof(zeroes)) == 0) {
    282 				// Make empty ranges more compact.
    283 				printf("\t/* 0x%04x */  0, 0, 0, 0, 0, 0, 0, "
    284 				    "0, 0, 0, 0, 0, 0, 0, 0, 0", i);
    285 				i += (16 - 4);
    286 			} else {
    287 				printf("\t/* 0x%04x */  0x%08x, 0x%08x, "
    288 				    "0x%08x, 0x%08x", i,
    289 				    firmware[i+0], firmware[i+1],
    290 				    firmware[i+2], firmware[i+3]);
    291 			}
    292 		}
    293 		printf("\n};\n");
    294 	}
    295 
    296 	// Output needs to be big-endian, so swap if we're not on a BE machine.
    297 	for (int i = 0; i < firmware_words; i++) {
    298 		firmware[i] = htonl(firmware[i]);
    299 	}
    300 
    301 	FILE *fpout = fopen(argv[1], "w");
    302 	check(fpout != NULL, "unable to open output file %s: %s",
    303 	    argv[1], strerror(errno));
    304 	check(fwrite(firmware, sizeof(u_int32_t), firmware_words, fpout) ==
    305 	    firmware_words, "failed to write entire output to file %s: %s",
    306 	    argv[1], strerror(errno));
    307 	fclose(fpout);
    308 
    309 	return 0;
    310 }
    311