{"id":964,"date":"2018-10-03T04:49:43","date_gmt":"2018-10-03T11:49:43","guid":{"rendered":"https:\/\/www.pagetable.com\/?p=964"},"modified":"2018-10-03T04:49:43","modified_gmt":"2018-10-03T11:49:43","slug":"a-minimal-c64-datasette-program-loader","status":"publish","type":"post","link":"https:\/\/www.pagetable.com\/?p=964","title":{"rendered":"A Minimal C64 Datasette Program Loader"},"content":{"rendered":"<p>The Commodore Datasette recording format is heavily optimized for data safety and can compensate for many typical issues of cassette tape, like incorrect speed, inconsistent speed (wow\/flutter), and small as well as longer dropouts. This makes the format more complex and way less efficient than, for example, \u201cTurbo Tape\u201d or all other custom formats used by commercial games. Let\u2019s explore the format by writing a minimal tape loader for the C64, optimized for size, which can decode correct tapes, but does not support error correction.<\/p>\n<h2>Pulses<\/h2>\n<p>All information is encoded in the duration of pulses. There are three types of pulses: short (352 \u00b5s), medium (512 \u00b5s) and long (672 \u00b5s). To measure the lengths of the pulses, we can use one of the timers in the CIA chips: These count cycles of the C64 base clock (roughly microseconds), counting down from a a 16 bit start value. Since the pulse lengths are above 255, this would require us to compare 16 bit measurement results, but since microseconds are too high a resolution for tapes anyway, we can just as well only count units of 8 clock cycles \u2013\u00a0this matches the resolution of the &#8220;TAP&#8221; archival format.<\/p>\n<p>This can be done by having timer A continuously count down from 7 to 0 (at the speed of the system clock) and having timer B count underruns of timer A:<\/p>\n<pre>\n    lda #7          ;divide timer b by 8\n    ldx #0\n    sta $dd04\n    stx $dd05\n    lda #%00010001\n    sta $dd0e       ;start timer a\n    dex ; $ff\n    stx $dd06       ;always start timer b\n    stx $dd07       ;from $ffff\n<\/pre>\n<p>The lengths of the pulses in units of 8 PAL clock cycles are:<\/p>\n<pre>\nlength_short  = $30\nlength_medium = $42\nlength_long   = $56\n<\/pre>\n<p>To differentiate the pulses, let\u2019s take the arithmetic means as thresholds:<\/p>\n<pre>\nthreshold_short_medium = (length_short + length_medium) \/ 2\nthreshold_medium_long = (length_medium + length_long) \/ 2\n<\/pre>\n<p>The subroutine <tt>get_pulse<\/tt> measures a pulse by reading the low byte of the value of timer B (which counts down from $ffff) and restarts the timer. The (negative) length is returned in A. It will also set the C flag if it was a short pulse. If C is clear, it was a medium or a long pulse.<\/p>\n<p><pre>\nget_pulse:\n    lda #$10\np1: bit $dc0d       ;wait for start\n    bne p1          ;of pulse\n\n    ldx #%01011001  ;value to restart timer b\np2: bit $dc0d       ;wait for end\n    beq p2          ;of pulse\n\n    lda $dd06       ;read timer b\n    stx $dd0f       ;restart timer b\n    cmp #$ff-threshold_short_medium\n    rts             ;c=1: short\n<\/pre>\n<h2>Bits and Bytes<\/h2>\n<p>Every bit is encoded using two pulses: short\/medium is 0, and medium\/short is 1:<\/p>\n<pre>\nS\/M 0\nM\/S 1\n<\/pre>\n<p>The byte marker (long\/medium) allows detecting byte boundaries:<\/p>\n<p><pre>\nL\/M byte marker\n<\/pre>\n<p>So a byte is encoded with a byte marker, followed by the 8 bits (LSB first) and an (inverted) parity bit.<\/p>\n<pre>\n L\/M    S\/M  S\/M  S\/M  S\/M  S\/M  S\/M  S\/M  S\/M  M\/S\n marker bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7 parity\n<\/pre>\n<p>There is one more pulse combination, the end of data marker (long\/short), which indicates the end of a file:<\/p>\n<pre>\nL\/S end of data\n<\/pre>\n<p>The code to read a byte first reads pulses until it finds either the byte marker of the end of data marker. In the case of end of data, it returns with the C flag set. If it found a byte marker, it will reads 8 bits, ignoring the length of the first pulse and only deciding whether it was a 0 or a 1 based on the length of the second pulse. The check bit is ignored: When reading the next byte, the code to search for the marker will just skip it.<\/p>\n<p><pre>\nget_byte:\n; wait for byte marker\n    jsr get_pulse\n    cmp #$ff-threshold_medium_long\n    bcs get_byte    ;not long\n    jsr get_pulse\n    bcs b2          ;short = end of data\n; get 8 bits\n    lda #%01111111\nb1: pha\n    jsr get_pulse   ;ignore first\n    jsr get_pulse\n    pla\n    ror             ;shift in bit\n    bcs b1          ;until canary bit\nb2: rts\n<\/pre>\n<p>For code compactness, this loop of 8 iterations stores a canary bit with a value of 0 at position 7 of the result during initialization. With every read bit that is rotated into the result, the canary bit is shifted to the right, and after 8 iterations, it is shifted into the carry flag. This way, we can detect we\u2019re done with all 8 bits without using a counter.<\/p>\n<p><h2>Leader and Countdown<\/h2>\n<p>The encoding described so far allows a cassette tape to store an arbitrary stream of bytes, with a marker to indicate the end, but no way to know where a file starts. Therefore, the data of every file is prefixed by the leader and the countdown.<\/p>\n<p>The leader is a sequence of less than a second to up to ten seconds consisting entirely of short pulses. It allows the reader to adjust the expected pulse lengths to compensate for different motor speeds. In our minimal reader, we can just ignore it.<\/p>\n<p>The countdown is a sequence of the hex bytes $89, $88, $87, \u2026 down to $81. This is the code to detect it:<\/p>\n<pre>\nget_countdown:\nc0: jsr get_byte\nc1: ldy #$89\n    sty tmp_cntdwn  ;start with $89\n    ldy #9\n    bne c2\ncx: jsr get_byte\nc2: cmp tmp_cntdwn\n    bne c4\n    dec tmp_cntdwn\n    dey\n    bne cx\n    rts\nc4: cpy #9           ;first byte wrong?\n    beq c0           ;then read new byte\n    bne c1           ;compare against $89\n<\/pre>\n<p>This code may look more complicated than it needs to be, but the extra complexity comes from the edge case of an incomplete countdown immediately followed by a complete countdown (e.g. $89, $88, $89, $88, $87, \u2026, $81).<\/p>\n<h2>Data<\/h2>\n<p>After the countdown, the subroutine <tt>get_block<\/tt> will read the number of bytes passed in A into memory pointed to by ptr.<\/p>\n<pre>\nget_block:\n    sta count\n    ldy #0\ng1: jsr get_byte\n    bcs g2\n    sta (ptr),y\n    iny\n    dec count\n    bne g1\ng2: rts\n<\/pre>\n<p>If it encounters an end of data marker, the subroutine will return prematurely with the C flag set.<\/p>\n<h2>Header<\/h2>\n<p>The encoding described so far allows storing individual files, but there is no metadata yet. So for every file on tape, there is a 192 byte header which precedes it.<\/p>\n<p>The header encodes the filename, its type, and in case of programs, the start and end addresses.<\/p>\n<pre>\nOffset Length Description\n0      1      File Type ($01 or $03 for PRG)\n1      2      Start Address\n3      2      End Address\n5      16     Filename\n21     171    unused\n<\/pre>\n<p>There are other file types to support SEQ files, which are stored a sequences of individual 192 byte files.<\/p>\n<p>The header itself encoded exactly like a 192 byte file and recorded just before the file data: with a leader and a countdown.<\/p>\n<p>The KERNAL\u2019s save routines will actually store a backup copy of both the header and the full file contents immediately after the original to mitigate tape dropouts, with the countdowns using the sequence $09, $08, $07, \u2026 down to $01. Our code assumes an error-less tape, so we ignore the copies.<\/p>\n<h2>Connecting everything together<\/h2>\n<p>Our minimal loader makes several assumptions: The tape must be error-less and rewound, and the first file must be a program, correctly preceded by a header.<\/p>\n<p><p>The user has to press the PLAY button, so we have to wait until this has happened:<\/p>\n<pre>\n    lda #$10\nm1: bit $01\n    bne m1\n<\/pre>\n<p>Timing is critical when reading from tape, so we have to turn off interrupts and disable the screen to make sure the VIC doesn\u2019t steal cycles:<\/p>\n<pre>\n    sei\n    lda $d011\n    and #$ff-$10    ;disable screen\n    sta $d011\nm2: lda $d012\n    bne m2          ;wait for new screen\n<\/pre>\n<p>Now we turn on the Datasette motor:<\/p>\n<pre>\n    lda $01\n    and #$ff-$20    ;motor on\n    sta $01\n<\/pre>\n<p>Now we can read the header by calling <tt>get_block<\/tt> with a byte count of 192:<\/p>\n<pre>\n    ldx #&lt;buffer\n    ldy #&gt;buffer\n    stx ptr\n    sty ptr + 1\n    jsr get_countdown\n    lda #192\n    jsr get_block\n<\/pre>\n<p>We ignore most fields of the header and only read the start address to know where to load the file:<\/p>\n<pre>\n    ldx buffer + 1\n    ldy buffer + 2\n    stx ptr\n    sty ptr + 1\n<\/pre>\n<p>Now we can load the file:<\/p>\n<pre>\nm3: lda #0\n    jsr get_block\n    inc ptr + 1\n    bcc m3\n<\/pre>\n<p>Once we\u2019re done, we turn the motor off and the screen back on:<\/p>\n<pre>\n    lda $d011\n    ora #$10\n    sta $d011       ;screen on\n    lda 1\n    ora #$20        ;motor off\n    sta 1\n<\/pre>\n<h2>Conclusion<\/h2>\n<p>This minimal tape loader takes up less than 200 bytes. It might seem useless for any other purpose than learning about the Datasette recoding format, since the C64 KERNAL already contains code like this. But it is common to create modified versions of the KERNAL that add features, removing the original tape code to create some space, so this minimal loader could be added to such a modified KERNAL to save on space but still be able to load games from tape.<\/p>\n<p>The complete source is available at <a href=\"https:\/\/github.com\/mist64\/datasette_load\">github.com\/mist64\/datasette_load<\/a>. bugfixes and size optimizations are very much appreciated.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The Commodore Datasette recording format is heavily optimized for data safety and can compensate for many typical issues of cassette tape, like incorrect speed, inconsistent speed (wow\/flutter), and small as well as longer dropouts. This makes the format more complex and way less efficient than, for example, \u201cTurbo Tape\u201d or all other custom formats used &#8230; <a title=\"A Minimal C64 Datasette Program Loader\" class=\"read-more\" href=\"https:\/\/www.pagetable.com\/?p=964\" aria-label=\"Read more about A Minimal C64 Datasette Program Loader\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2,41,8,16,29],"tags":[],"class_list":["post-964","post","type-post","status-publish","format-standard","hentry","category-2","category-c64","category-commodore","category-github","category-tapes"],"_links":{"self":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts\/964","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=964"}],"version-history":[{"count":0,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=\/wp\/v2\/posts\/964\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=964"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=964"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.pagetable.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=964"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}