The safe version reads three times in a row using the fast version, then compares the last two. If equal, that result is returned, otherwise the first read is returned. It doesn't need to check it in that case because the DMC can't clash with more than one of the reads, due to them being so closely spaced. The routine is timed so that the same number of clocks is used in each case.
The tests run the DMC at maximum rate and repeatedly read from the controller, printing an X when the DMC clashed with that read. A clash causes the fast version to give an erroneous result (left screenshot), but doesn't affect the safe version (right screenshot). Since the safe version compares two of the three reads it does, as expected it has twice the number of clashes (3.0% versus 1.3% for fast).
I might point out, though it is not related to this discussion, that it is "proper" to read both bit 0 and bit 1 from each $4016/4017 access, combining the two to produce the final controller data. This is for the benefit of Famicom users who are using controllers that plug in to the expansion port instead of the normal ports. Every commercial game I've looked at acknowledges inputs on both bit 0 and bit 1 of $4016/4017.
EDIT: Figured out how to OR both bits and only add 2 clocks per iteration! Just change the LSR A to AND #$03, CMP #$01. This general technique can OR any number of bits from A into carry. Just mask off the bits, then CMP #1 (to have the opposite carry - set if any of the masked bits are zero - CMP #mask instead).
Code: Select all
loop: lda $4016 ; 4 bits 0 and 1 contain relevant data and #$03 ; 2 cmp #$01 ; 2 carry = bit 0 OR bit 1 ror <temp ; 5 bcc loop ; 3
Code: Select all
lda $4016 lsr rol <$00 lsr rol <$01
I also finally got around to doing the analysis of the case where the DMC corrupts the first read, and the controller input changes during the the second two reads. In this case, the corrupted first read will be returned, rather than one of the two correct reads from the controller.
There are 8 opportunities for DMC corruption of the first read, and the window for the controller change during the second two is 162 clocks. That means that the DMC corruption at maximum rate has an 8 in 29780 = 1 in ~3722 chance in a given frame, and the controller change a 162 in 29780 = 1 in ~184 chance in a given frame.
Assuming the controller were changing at a random time each frame, and the DMC were running, that makes the chance of both occurring in a given frame 1 in ~684848. So there would be an average of one of these errors every 3.2 hours at this worst-case setup. If the controller input were changing on average 10 times per frame, that would put one error every 19 hours.
I guess I'll try writing and analyzing a version that reads until two consecutive reads give the same value. The main problem is that this takes a varying amount of time, and would hang if someone fed the controller a very rapid turbo signal.