Skip to content

Printing from a TI to a modern printer

TI-99/4A So, obviously, the TI-99/4A doesn't support printing to USB printers, nor can it print to network printers.

With a little help, though, it can print to modern printers via a helper device running UNIX/FreeBSD/etc. This leverages the BeagleBone Black-based cassette emulator I described in an earlier web log entry.

Here's how it's done:

Step 1: Decide if you're going to use the TI's parallel port or the serial port. I recommend the parallel port for the sake of speed -- the serial port lacks any sort of handshaking, so you will drop characters if you choose serial.

Step 1a (parallel): obtain a parallel-to-serial converter. They're about fifteen bucks on eBay. Pretty much any unit will do, so long as it has a Centronics port on the input and a DB25/DB9 on the output. Configure it to output as fast as it can (typically 38400, which we'll refer to as ${SPEED} for the rest of this procedure), 8N1.

Step 1a (serial): obtain a straight-through DB25 <-> DB9 cable. DB25 will be male, DB9 will be female.

Step 2: obtain and connect a USB serial adapter to the BeagleBone Black. You will probably need a small USB hub if you're also using it as a cassette deck emulator -- passive hubs are fine. Run "dmesg" on the BBB's console until you see it detect the serial adapter and assign a device name (typically "ttyUSB0"). Note the device name; we'll refer to it as ${DEVICE} for the rest of this procedure.

Step 3: connect the TI to the BBB. If you went with step 1a, verify that you can successfully send characters through your parallel converter -- i.e., print a file while running minicom on ${DEVICE} at ${SPEED}. If you went with step 1b, no further actions are necessary for this step.

Step 4: download my TI utilities from GitHub here ... you care about everything in the printer_listener subdirectory.

Step 5: on the BBB, make an incoming printer spool directory wherever convenient. We will refer to it as ${SPOOL_DIRECTORY} for the rest of this procedure.

Step 6: inside the printer_listener subdirectory is an "epsonps" directory. Enter it, and execute "make; sudo make install". The Epson-to-PostScript converter (and its PostScript preamble) should be installed to /usr/local/bin.

Step 7: on the BBB, as root, ensure that the python2-pyserial package is installed.

Step 8: on the BBB, copy the script from the printer_listener subdirectory to someplace convenient in your path. You can also leave it in-place, but you'll need to explicitly specify its location when started.

Step 8a: on the BBB, look at the sample filter scripts and optionally choose one to use. One will send the printed output to a networked printer as HP PCL, the other will leave a PDF in /tmp. If you choose to use a filter, copy it to someplace convenient in
your path (hereafter referred to as ${PATH_TO_FILTER})

Step 9: on the BBB, execute " -d ${DEVICE} -s ${SPEED} -f ${PATH_TO_FILTER} -n ${SPOOL_DIRECTORY} (with the "-f" and "-n" options being, well, optional). You will see "printer listener listening" after the serial port is initialized.

At this point, anything you print will appear either in the directory that you ran from, or the ${SPOOL_DIRECTORY} if you specified that option. If you specified a filter with "-f", the output will be processed per the directives in the filter file.

That's about it. You'll probably want to wire this into a startup script; I despise systemd, so I'm leaving that as an exercise for the student.


Finished and verified: SMD replacement boards for Horizon HRD+

TI-99/4A As previously mentioned, I've been working on an old Horizon HRD+ RAMdisk card for the TI-99/4A.

Three of the chips (a 74LS138, a 74LS154, and a 74LS259) had other chips stacked and soldered on top of them, with leads bent out and wires connecting to various places on the board. One chip (the 154) is no longer available in .600 DIP form.

For obvious reasons, I decided to replace these chip stacks with plug-in boards. Each board is electrically equivalent to the chip stack that it replaced, with the exception of the 138 replacement board incorporating errata wiring later published by Horizon. All chips are SMD, and are currently available as HCT.

The Eagle schematic and board files are available here.

All three of these boards designs have been tested and are currently in use in my HRD+.

Enjoy. Hopefully this will help someone out there that's trying to get one of these working, but is stymied by the stacked chips and/or can't find a package-exact replacement for the 74LS154.

Two UberGROM images: Logo/Logo-II/Multiplan/TurboForth and Plato/Return to Pirate's Isle

TI-99/4A Thanks to Tursi, Ksarul, and a few others, it's now possible to create bankswitched GROM/ROM cartridges for the TI-99/4A.

Most of the game cartridges were converted long ago to disk-loadable EA/5 images (most needing the 32k RAM expansion). The languages, courseware, and things that wouldn't fit into a RAM expansion have not been converted.

The aforementioned guys built a GROM simulator around an AVR, added a bank-switching 512k EEPROM for the ROM side of the cartridge, and released it as the UberGROM cartridge board.

Therefore, I went ahead and created two multicarts. The first one contains Logo, Logo II, and Microsoft Multiplan. The second contains Plato and Return to Pirate's Isle. They can be found here.

Update: the first image now also contains TurboForth v1.2.2.

To convert these to cartridges, first obtain two UberGROM cartridge boards. The link above goes to ArcadeShopper's web site, but you can also purchase them directly from Ksarul on AtariAge.

The AVR must be prepared as per Tursi's instructions here. The instructions expect you to have a cheap eBay TL866 EPROM burner, but it's also possible to program with avrdude via SPI. Contact me for details if you decide to go this route.

Next, extract the files from the archives you downloaded from the links above. If you're building both cartridges, extract into separate directories.

Next, burn the "eprom.bin" that corresponds with which of the two multicarts you want to create onto the EEPROM that you should have received with your UberGROM and install it onto the board.

Next, copy "cart1.tifiles" or "cart2.tifiles" onto a TI-readable media. I use and recommend the HxC Floppy Emulator, which uses disk image files on a SD card to emulate a Shugart-compatible floppy drive. Note: these files are (obviously) in TIFILES format; do the needful when copying them onto the target media.

Next, copy "gromcfg" from Tursi's website onto the same TI-readable media. Insert that media into the floppy drive. If HxC, be sure to mount it.

Next, insert the prepared UberGROM cartridge into the TI. Hold down the space bar and turn it on. After the rainbow screen, you should see a menu that includes "Run Program". Choose it.

The file you run will be "DSKx.GROMCFG", where "x" is the number of the drive that contains the prepared media. After several seconds, you will see the GROMCFG browser.

Hit control-L. Say yes, you really want to do this, then feed it the name of the cartridge file ("DSKx.CART1" or "DSKx.CART2".

Now go get a beer, this takes about five minutes.

When it's finished, powercycle the TI. After the rainbow screen, you'll see an option for "Multicart". Choose that, and you'll see the selection menu for the multicart that you just created.


HxC: support for 80-track DSSD .hfe -> .dsk conversions

TI-99/4A There's a bug in the HxC floppy image conversion program that prevents conversion from .hfe back to .dsk for 80-track DSSD disk images. In my opinion, the geometry selection logic is flawed -- it decides what the disk image format is based on disk size, and there's a size overlap between 80-track DSSD FM-encoded images and 40-track DSDD MFM-encoded images.

I posted a rough patch last night in another thread, have worked out a cleaner solution this morning, and have opened a ticket with the upstream developer on GitHub.

The geometry should be parsed from sector 0 of the .hfe (trying FM first as it's most common, then falling through to MFM) but that involves more code churn than is necessary to fix this particular use case and I sort of used up my internal energy budget for the week yesterday.

Edit: scratch that, I went ahead and refactored the code, so it parses the geometry as described above. I've replaced the patch below with the new code because ...

With luck the patch will be accepted and integrated into a near-future release of the software. In the meantime, the patch to libhxcfe is at the end of this post.

Edit: the patch was accepted and will be in the next release of the software. The latest, all-singing-all-dancing version, has not been accepted yet.

(There are many changes that need to be made to the HxCFE suite of interdependent libraries just to get it to build. Makes me wonder what sort of environment the developer uses for releases ... internal svn, maybe?)

diff --git a/sources/loaders/ti99v9t9_loader/ti99v9t9_writer.c b/sources/loaders/ti99v9t9_loader/ti99v9t9_writer.c index ba0d0e4..8359f8f 100644 --- a/sources/loaders/ti99v9t9_loader/ti99v9t9_writer.c +++ b/sources/loaders/ti99v9t9_loader/ti99v9t9_writer.c @@ -48,13 +48,11 @@ int TI99V9T9_libWrite_DiskFile(HXCFE_IMGLDR* imgldr_ctx,HXCFE_FLOPPY <strong> floppy,ch   int32_t nbsector,imagesize;     int32_t numberofsector,numberofside,numberoftrack; - int32_t bitrate; - int32_t density; - int32_t interleave; + int32_t density = ISOIBM_FM_ENCODING;;   int file_offset; - int32_t sectorsize; + int32_t sectorsize = 256;   unsigned char </strong> diskimage; - int error; + int error = 0;   HXCFE_SECTORACCESS* ss;   HXCFE_SECTCFG* sc;   @@ -64,183 +62,116 @@ int TI99V9T9_libWrite_DiskFile(HXCFE_IMGLDR* imgldr_ctx,HXCFE_FLOPPY * floppy,ch     imgldr_ctx->hxcfe->hxc_printf(MSG_INFO_1,"Disk size : %d Bytes %d Sectors",imagesize,nbsector);   - numberofsector=9; - numberofside=1; - numberoftrack=40; - bitrate=250000; - density=ISOIBM_FM_ENCODING; - interleave=4; - sectorsize = 256; - - switch(imagesize) + ss = hxcfe_initSectorAccess(imgldr_ctx->hxcfe, floppy); + if (ss)   { - case 1*40*9*256: - numberofside=1; - numberoftrack=40; - numberofsector=9; - bitrate=250000; - density=ISOIBM_FM_ENCODING; - interleave=4; - break; - - case 2*40*9*256: - // 180kbytes: either DSSD or 18-sector-per-track SSDD. - // We assume DSSD since DSSD is more common and is supported by - // the original TI SD disk controller. - numberofside=2; - numberoftrack=40; - numberofsector=9; - bitrate=250000; - density=ISOIBM_FM_ENCODING; - interleave=4; - break; - - case 1*40*16*256: - // 160kbytes: 16-sector-per-track SSDD (standard format for TI - // DD disk controller prototype, and the TI hexbus disk - // controller?) <strong>/ - numberofside=1; - numberoftrack=40; - numberofsector=16; - bitrate=250000; - density=ISOIBM_MFM_ENCODING; - interleave=9; - break; - - case 2*40*16*256: - // 320kbytes: 16-sector-per-track DSDD (standard format for TI - // DD disk controller prototype, and TI hexbus disk - // controller?) - numberofside=2; - numberoftrack=40; - numberofsector=16; - bitrate=250000; - density=ISOIBM_MFM_ENCODING; - interleave=9; - break; - - case 2*40*18*256: - //  360kbytes: 18-sector-per-track DSDD (standard format for most - // third-party DD disk controllers, but reportedly not supported by - // the original TI DD disk controller prototype) - numberofside=2; - numberoftrack=40; - numberofsector=18; - bitrate=250000; - density=ISOIBM_MFM_ENCODING; - interleave=5; - break; - - case 2*80*18*256: - // 720kbytes: 18-sector-per-track 80-track DSDD (Myarc only) - numberofside=2; - numberoftrack=80; - numberofsector=18; - bitrate=250000; - density=ISOIBM_MFM_ENCODING; - interleave=5; - break; - - case 2*80*36*256: - // 1.44Mbytes: DSHD (Myarc only) - numberofside=2; - numberoftrack=80; - numberofsector=36; - bitrate=500000; - density=ISOIBM_MFM_ENCODING; - interleave=11; - break; - - default: - imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Bad image size!.."); - return 0; - break; - } + sc = hxcfe_searchSector(ss, 0, 0, 0, density); + if (!sc) + { + density = ISOIBM_MFM_ENCODING; + sc = hxcfe_searchSector(ss, 0, 0, 0, density); + if (!sc) + { + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR, "This disk is neither FM nor MFM.  Exiting."); + return HXCFE_FILECORRUPTED; + } + }   - error = 0; - imagesize = numberofsector </strong> numberoftrack <strong> numberofside </strong> sectorsize; - diskimage = malloc(imagesize) ; - if(diskimage) - { - memset( diskimage ,0xF6 , numberofsector <strong> numberoftrack </strong> numberofside <strong> sectorsize); + // sc->input_data should contain the disk geometry + + numberofside = sc->input_data[0x12]; + numberofsector = sc->input_data[0x0c]; + numberoftrack = sc->input_data[0x11]; + + if ( (numberofside < 1) && (numberofside > 2)) + { + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR, "Image claims it has %i sides, which is clearly wrong.  Exiting.", numberofside); + return HXCFE_FILECORRUPTED; + } +  + if ( (numberoftrack != 40) && (numberoftrack != 80)) + { + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR, "Image claims each side has %i tracks, which is clearly wrong.  Exiting.", numberoftrack); + return HXCFE_FILECORRUPTED; + } +  + if ( (numberofsector != 9) && (numberofsector != 16) && (numberofsector != 18) && (numberofsector != 36)) + { + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR, "Image claims each track has %i sectors, which is clearly wrong.  Exiting.", numberofsector); + return HXCFE_FILECORRUPTED; + }   - ss = hxcfe_initSectorAccess(imgldr_ctx->hxcfe,floppy); - if(ss) + if ( (numberofsector </strong> numberoftrack <strong> numberofside) != nbsector )   { - for(i=0;i<numberofside;i++) + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR, "Disk geometry is %i sides, %i tracks per side, %i sectors per track, but does not match disk length of %i sectors.  Exiting.", numberofside, numberoftrack, numberofsector, nbsector); + return HXCFE_FILECORRUPTED; + } +  + imgldr_ctx->hxcfe->hxc_printf(MSG_INFO_1, "Disk geometry is %i sides, %i tracks per side, %i sectors per track.", numberofside, numberoftrack, numberofsector); + + imagesize = numberofsector </strong> numberoftrack <strong> numberofside </strong> sectorsize; + diskimage = malloc(imagesize); + if (!diskimage) + return HXCFE_INTERNALERROR; + memset(diskimage, 0xF6, imagesize); + + for(i=0;i<numberofside;i++) + { + for(j=0;j<numberoftrack;j++)   { - for(j=0;j<numberoftrack;j++) - { - hxcfe_imgCallProgressCallback(imgldr_ctx, j + (i*numberoftrack),numberofside*numberoftrack); + hxcfe_imgCallProgressCallback(imgldr_ctx, j + (i*numberoftrack),numberofside*numberoftrack);   - for(k=0;k<numberofsector;k++) + for(k=0;k<numberofsector;k++) + { + sc = hxcfe_searchSector(ss,j,i,k,density); + if(sc)   { - sc = hxcfe_searchSector(ss,j,i,k,density); - if(sc) + if(sc->use_alternate_data_crc) + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Warning : Bad Data CRC : T:%d H:%d S:%d Size :%dB",j,i,k,sc->sectorsize); + + if(sc->sectorsize == sectorsize)   { - if(sc->use_alternate_data_crc) - imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Warning : Bad Data CRC : T:%d H:%d S:%d Size :%dB",j,i,k,sc->sectorsize); - - if(sc->sectorsize == sectorsize) - { - if(i==0) - { - file_offset=(j*numberofsector)*sectorsize + ( k * sectorsize ); - } - else - { - file_offset=(  numberoftrack      <strong>numberofsector*sectorsize) + + if(i==0) + file_offset=(j*numberofsector)*sectorsize + ( k </strong> sectorsize ); + else + file_offset=(  numberoftrack      <strong>numberofsector*sectorsize) +   (((numberoftrack-1)-j)*numberofsector*sectorsize) +   ( k </strong> sectorsize ); - } - memcpy(&diskimage[file_offset], sc->input_data, sectorsize); - } - else - { - error++; - imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Bad Sector Size : T:%d H:%d S:%d Size :%dB, Should be %dB",j,i,k,sc->sectorsize,sectorsize); - } - - hxcfe_freeSectorConfig(ss,sc); - } - else - { - imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Sector not found : T:%d H:%d S:%d",j,i,k); + memcpy(&diskimage[file_offset], sc->input_data, sectorsize); + } else { + error++; + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Bad Sector Size : T:%d H:%d S:%d Size :%dB, Should be %dB",j,i,k,sc->sectorsize,sectorsize);   } - } - } - }   - if(!error) - { - ti99v9t9file=hxc_fopen(filename,"wb"); - if(ti99v9t9file) - { - fwrite(diskimage,imagesize,1,ti99v9t9file); - hxc_fclose(ti99v9t9file); - } - else - { - free(diskimage); - hxcfe_deinitSectorAccess(ss); - return HXCFE_ACCESSERROR; + hxcfe_freeSectorConfig(ss,sc); + } else { + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"Sector not found : T:%d H:%d S:%d",j,i,k); + }   }   }   }   - free(diskimage); - hxcfe_deinitSectorAccess(ss); -   if(!error) - return HXCFE_NOERROR; - else   { - imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"This disk have some errors !"); - return HXCFE_FILECORRUPTED; + ti99v9t9file=hxc_fopen(filename,"wb"); + if(ti99v9t9file) + { + fwrite(diskimage,imagesize,1,ti99v9t9file); + hxc_fclose(ti99v9t9file); + } else { + free(diskimage); + hxcfe_deinitSectorAccess(ss); + return HXCFE_ACCESSERROR; + }   }   } - else - { - return HXCFE_INTERNALERROR; - } + + free(diskimage); + hxcfe_deinitSectorAccess(ss); + + if(!error) + return HXCFE_NOERROR; + + imgldr_ctx->hxcfe->hxc_printf(MSG_ERROR,"This disk have some errors !"); + return HXCFE_FILECORRUPTED;  }