After a lot of trial and error, I managed to fill in the holes in Nocash's spec and I added support for the RacerMate CompuTrainer exercise bike to the latest build of
Nintaco. If anyone wants to test their finger tapping skills,
here's some information on how to use it.
For emulator authors, here's what I found:
First, the wiki describes a binary counter that triggers an interrupt every
1024 M2 cycles. However, Nocash claims:
Quote:
In practice, 1024 clks per IRQ seems to be too slow (video glitches), 512 clks too fast (1/10 second counter increases non-linear), so, the IRQ rate is probably something around 768 clks.
On this topic, I found the wiki to be accurate. 1024 clks per IRQ does the job.
Regarding IRQ acknowledgement,
the wiki claims:
Quote:
Only one of the 9 above bits is used, but the software has no a priori reason to know which. The firmware thus writes $FF to $F080 and $00 to $F000.
However, Nocash says:
Quote:
[F080h]=FFh disable IRQ and/or reset IRQ-counter or so
[F000h]=00h enable IRQ and/or start IRQ-counter or so
To get it to function, I created a flag that turns the counter on and off. When the game writes to $F080, the flag is cleared, the counter is reset and the interrupt is acknowledged. When the game writes to $F000, the flag is set. When the flag is set, the counter advances once per CPU cycle, eventually triggering an interrupt after 1024 of them.
I found that Nocash's description of the communication protocol is generally correct. Writing to $4016 puts a bit into a temporary register and reading from $4016/$4017 transfers that bit out and simultaneously reads a bit in from one of the bikes. Each transfer packet contains 3 bytes of data. However, each bit to be transferred is converted to 2 bits during transmission. Zero is represented by 10 and one is represented by 01. Consequentially, 3 bytes gets turned into 48 bits. But only 24 bits go out per frame. To make matters worse, there maybe writes/reads between packet halves, adding noise that needs to be skipped.
The main complexity is alignment. The bike probably only detects when the signal drops (a zero bit) or the signal rises (a one bit); hence, the purpose of the bit doubling. The bike also contains a high precision clock. With this alternating scheme, it's possible for 2 of the same bit values to come in a row, but not 3 or more in a row. As a result, it's possible to look a stream of bits and work out the alignment parity. But the bike also needs to work out the transfer-packet-half start points. Per Nocash, the first packet half starts with 0 and the second half starts with 1. In addition, the first half starts with the sequence 110100. The bike uses all this information and the fact that it expects packet halves every 1/60th of a second to complete the alignment.
Luckily, an emulated implementation can cheat a bit. The start of each packet half is triggered by an NMI. The first bit transferred after that determines the packet half. Alternatively, and this what Nintaco does, the start of the packet can be determined by the value written to $4016. $BB or $EE indicates the start of the first half. And $33 or $CC indicates the start of the second half. Well, almost. Sometimes those values will appear within a packet. However, you can get around this by waiting for 24 bits to be transferred before using those indicators again. That scheme works for all the RacerMate Challenge II versions in GoodNES 3.23b. In fact, I suspect that Nocash also used this trick, though his source code is unavailable. But I think he only checked for $BB and $CC. Those are the values that appear in the latest US version. The older US version and the EU version requires the aforementioned additional checks.
Per Nocash's description, the transferred and received packets are offset by 1 bit. That is, the first bit out does not correspond to a bit in. However, the second bit out does. One thing that Nocash fails to mention is that for the second player bike, the receive packet is offset by 2 bits. Meaning, from that bike's point of view, the first and second bits out do not correspond to any bits in.
Regarding the button bits, Nocash could not figure out the purpose of bit 7:
Quote:
7 Unknown/unused? (?)
I found that in the EU version, one button bit must always be set and multiple buttons cannot be pressed simultaneously. I think bit 7 is set when no other buttons are pressed. Without doing that, the EU version does not work. And the US version does not care about the bit 7 value either way.
Nocash could not figure out RPM either either:
Quote:
Unknown if/how/where RPM is transferred.
I discovered that RPM is transferred using index 6. However, the game will only accept it if the highest data bit (data bit 11) is set.
In fact, index 4 and index 5 may do something RPM related as well. There is a screen in the program that shows a torque chart. Torque is computed from RPM, but I never figure out a way for anything to show up on that plot. The plot is supposed to show the difference in torque between your left and right legs. Maybe separate left and right cadence values go over those indices. But nothing showed up while I was experimenting with it. I could just be using that screen incorrectly; I'm not sure.
Each transfer packet contains one of the grade, wind speed, player weight, pulse target, etc. values. The device responds to this data by adjusting the resistance of the bike. The bike does not physically tilt based on the grade. Nintaco uses a simple physics model to simulate the bike; basically, you have to tap faster when going up hill or when there is a lot of wind.
The
wiki also mentions that the game has 64K of battery-backed CHR RAM. That appears to be the case. The game records a history of it's user. I don't recall any other game with so much CHR RAM and certainly none that had nonvolatile CHR RAM.
The Nintaco source is available
here for anyone else crazy enough to implement this. Simply search the code for "racermate".