Thursday, November 23, 2017

1351 Mouse Progress: Accuracy better than the original

Here is the latest update from Daniël's work on making a 1351-compatible mouse using an Arduino, which was achieved a few weeks ago, but I have been tied up (not literally) in remote tropical jungle (literally) for my work.

Wiring the Arduino to the C64

Time to start connecting the Arduino PWM outputs to the C64. The PWM outputs of the Arduino cannot be directly connected to the C64, some components in between are necessary. At this point, plugging stuff directly into the Arduino became impractical, so I spent quite a few hourrs searching where I had left my breadboard. Luckily I found it, and work could continue.
First, we need some resistance to control the speed that the capacitors in the C64 charge, and to limit the current output the output pins. In order to determine the right restistance experimentally I used two potmeters of 0-1000 Ω. This is much lower than the 1351 uses, but my experimentation did show that in order to be able to transmit low values, it is necessary to be able to charge the SID's capacitors fast, and I don't see any objection against fast charging, as long as the currents remain reasonable.
While experimenting I was also remebered that the PWM outputs have the properly they pull down the line when they are low: In other words, they pull the charge out of the C64's capacitors and they pull strongly. This is undesired, as this prevents detection of the SID sync pulses, and therfore we need a diode to prevent this.
In my box of electronic components I have two types of diodes: 1N4007 and BAT43 (a Schottky diode). If we need to able to charge fast, a low voltage drop might come handy, so I went for the BAT43. If you look at the voltage drop, the BAT43 is one of the best diodes that you can buy at good prices as a hobbyist.
After some experimentation I had my design ready, and it looks as follows:



I've recreated it in Fritzing, so you can look at it in more detail. Breadboard view:





Schematic view:



With the circuitry in place, it should be possible to send values from the Arduino to the C64. I typed the following program on the Commodore 64:

10 POKE $DC0C,1
20 PRINT PEEK($D419)
30 GOTO 20

The first line disables the CIA timer interrupt on the C64. This is necessary because the CIA line that controls the analog multiplexer that connects either control port 1 or control port 2 to the SID, shared its control lines with the keyboard matrix select lines. The C64 timer interrupt modifies this line in order to scan the keyboard, but as a result also switches between control port 1 or control port 2 POTX lines. Because this happens in the middle of SID read cycles, the C64 interrupt causes noise to read on the C64.

Because disabling the timer interrupt makes the C64 keyboard inoperable, you cannot interrupt above program once started other than with a reset button. Because of this it is highly recommended to use a cartridge that can recover a BASIC program after reset. I am using Final Cartridge III. A KCS Power Cartridge will do the trick as well. To interrupt, just reset and type “OLD” for the FC3, or “UNNEW”for the KCS. Both cartridges extend BASIC with $ for hexadecimal, which is why I am conveniently using hexadecimal numbers in my program.

Yes, it did work! However, the results did need calibration and they were still noisy. I still had to do some programming to get it correct.

Reducing noise and calibrating the thing

The initial results were way too noisy in order to be usefull. It did take some analysis to see where the nosie came from. I found 3 sources. First, there was noise on the 5V line when the Arduino was powered from USB. Powering it from the C64 did show much more stable results. Some people might be tempted to think that this is because of the linear power supply of the C64, but no, those original power supplies should not be used anymore. For my experiments here I use a modified power supply with switching regulators. My laptop could be powered by battery and still the USB 5V power supply was much more noisy than the C64 5V power. While not a problem for the final product, I did need to connect the USB for uploading sketches. It turns out that the 5V noise causes some inaccuracy on when the comparator did exactly trigger. Through experimentation I found out that modifying my voltage divider from 100k/15k to 100k/33k resistors did reduce this jitter a lot, evading the problem.
Another noise source was found in the Arduino's timer interrupt. By default the Arduino's timer0 generates interrupts in order to support library functions such as delay(). However, if a comparator interrupt is triggered while the timer interrupt is being handled, there before the comparator interrupt handler gets executed. To eleminate this noise I did disable timer0. This renders all timing functions inside the Arduino runtime library unusable. All timing needs to be done with our own timer: The SID synchronisation pulse.
A third source of noise was found in timer1 itself: Because the Arduino 16MHz clock is divided by 8 in order to get to 2 MHz, after setting the timer1 counter to 0, there can be 1-8 Arduino cycles before it is increase to 1. Luckily the timer prescaler can be reset, so besides setting the timer counter to 0, we also need to reset the prescaler. i.e. our IRQ handler should look like this:


ISR(ANALOG_COMP_vect) {
GTCCR = _BV(PSRSYNC);
TCNT1 = 0;
}

After these adjustments, the values read on the C64 were quite stable, only they werent correct, we need to calbrate stuff. Now because the PAL C64 runs at 985 KHz and the NTSC C64 runs at 1023 KHz, this calibration is PAL/NTSC dependent. Therfore I did add a small check to the IRQ handler:

commodore_is_pal = (TCNT1 < 512);

It's quite simple: The PAL C64 runs slower than 1MHz, so the timer will have overflown when the comparator interrupt occurs, the value in the timer counter should be low when the interrupt occurs. The NTSC C64 runs faster than 1 MHz, so the timer counter will not yet have overflown when the interrupt occurs, the counter should contain a high value. So a simple check low or high can distinguish between PAL and NTSC.

Through experimentation I found that if I set my potmeters to 340 Ω and do the following adjustments in software:

void set_potx(u8 potx) {
/* Our timer runs at 2000KHz while a PAL C64 runs at 985 KHz.
This means that we need approximately 2 timer cycles for 1 C64 cycle, so multiply
by 2 */
u16 d;
d = 0x1f2 + 2 * potx;
/* However, because the difference is not exactly 2, this means our pulses would be slightly too short.
Compensate. */
if (commodore_is_pal) {
if (potx>=24) d++;
if (potx>=48) d++;
if (potx>=87) d++;
if (potx>=121) d++;
if (potx>=156) d++;
if (potx>=187) d++;
if (potx>=223) d++;
if (potx>=251) d++;
} /* TODO NTSC */
OCR1A=d;

}

... I was able to get perfect results! This means that I am able to transmit values in the full range of 0 to 255 to the C64, with no noise at all. The real 1351 can only transmit values in the range of 64-191 and the lowest bit is always noise, so this is a notable achievement. It is also not possible to get the full range of values with analog paddles, Commodore's paddles get you results from about 20 to 235.

Here are some screenshots with a value of 2 and a value of 64:





...with a real 1351 you will see the noise in the least significant bit on the screen.

The oscilloscope view for both situations (green measured on POT line, yellow on pulse line before any components):




Full Arduino program


The following program counts POTX and POTY from 0 to 255 in a loop. Arduino pin2 can be used to pause the process: Put a wire between pin 2 and ground and the counting stops. You can watch the program do its work by reading registers $D419 and $D41A on the Commodore 64.

/**************************************************************************************************

Commodore 1351 mouse simulation code for Arduino

Written by Daniël Mantione

**************************************************************************************************/
volatile unsigned long sid_measurement_cycles=0;

u8 posx=0;
u8 posy=0;

bool commodore_is_pal = true;

ISR(ANALOG_COMP_vect) {
/* The SID has started its discharge cycle. We now need to synchronize the PWM, but first we do a
PAL/NTSC check.
This is highly time critical code: Any modifications here before the timer is set to 0 will
require adjustment of the PWM offset in set_potx/set_poty. */
/* This is untested on NTSC! A PAL system runs at less than 1MHz and therefore 512 SID cycles
will take longer than our 1024 timer cycles. Therfore we expect a low timer counter value when
the interrupt occurs. An NTSC system runs at higher than 1MHz and therefore 512 SID cycles
will take shorter than our 1024 timer cycles. Therefore we expect a high timer value when the
interrupt occurs. */
commodore_is_pal = (TCNT1 < 512);
/* In order to synchronize the PWM with the SID we will reset the clock prescaler of the
microcontroller and reset timer 1 by writing 0 to its counter: */
GTCCR = _BV(PSRSYNC);
TCNT1 = 0;
/* Timer has been reset, so end of time critical code. */
sid_measurement_cycles++;
}

void setup_comparator() {
ACSR =
(0<<ACD) | // Analog Comparator: Enabled
(0<<ACBG) | // Analog Comparator Bandgap Select: AIN0 is applied to the positive input
(0<<ACO) | // Analog Comparator Output: Off
(1<<ACI) | // Analog Comparator Interrupt Flag: Clear Pending Interrupt
(1<<ACIE) | // Analog Comparator Interrupt: Enabled
(0<<ACIC) | // Analog Comparator Input Capture: Disabled
(1<<ACIS1) | (0<ACIS0); // Analog Comparator Interrupt Mode: Comparator Interrupt on Falling
// Output Edge
pinMode(6, INPUT); //Avoid interfering with comparator
pinMode(7, INPUT); //Avoid interfering with comparator
}

void setup_timer1() {
/* For generating the pulses for POTX/POTY we will use timer 1 of the Atmega328p. This is a 16-
bit timer, which allows for high precision.*/
TIMSK1 = 0;
GTCCR |= _BV(PSRSYNC);
/* We set the clock source to none, so the timer does not run while we adjust it. The WGM12 bit
is to already select the right timer mode, otherwise OCR1A/ORC1B cannot be set correctly (read
on). */
TCCR1B = _BV(WGM12);

/* Activate PWM on Arduino pin 9 and 10. The PWM pin is high when the counter is higher than
MATCH. Select Fast PWM mode 7. */
TCCR1A = _BV(COM1A1) | _BV(COM1A0) | _BV(COM1B0) | _BV(COM1B1) | _BV(WGM10) | _BV(WGM11);
TCNT1 = 0x000;

/* WGM12=1 to select Fast PWM mode 7. CS11 selects a clock source of clkio / 8, which results in
2MHz. A timer clock of 2MHz combined with a range of 0-1023 is acceptable for our purposes. By
selecting a clock the timer starts counting */
TCCR1B = _BV(WGM12) | _BV(CS11);
DDRB |= _BV(PORTB1) | _BV(PORTB2);
}

void setup() {
setup_comparator();
setup_timer1();
/* Arduino timer 0 is used by default to support timing functions like delay(). Its interrupt
handler may delay the analog comparator interrupt and thus cause noise. Therefore switch it
off. */
TCCR0B=0;
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW);
set_potx(0);
set_poty(0);
Serial.begin(9600);
Serial.println("Start");
}

void set_potx(u8 potx) {
/* Our timer runs at 2000KHz while a PAL C64 runs at 985 KHz.
This means that we need approximately 2 timer cycles for 1 C64 cycle, so multiply
by 2 */
u16 d;
d = 0x1f2 + 2 * potx;
/* However, because the difference is not exactly 2, this means our pulses would be slightly too short.
Compensate. */
if (commodore_is_pal) {
if (potx>=24) d++;
if (potx>=48) d++;
if (potx>=87) d++;
if (potx>=121) d++;
if (potx>=156) d++;
if (potx>=187) d++;
if (potx>=223) d++;
if (potx>=251) d++;
} /* TODO NTSC */
OCR1A=d;
}

void set_poty(u8 poty) {
/* Our timer runs at 2000KHz while a PAL C64 runs at 985 KHz.
This means that we need approximately 2 timer cycles for 1 C64 cycle, so multiply
by 2 */
u16 d;
d = 0x1f2 + 2 * poty;
/* However, because the difference is not exactly 2, this means our pulses would be slightly too short.
Compensate. */
if (commodore_is_pal) {
if (poty>=24) d++;
if (poty>=48) d++;
if (poty>=87) d++;
if (poty>=121) d++;
if (poty>=156) d++;
if (poty>=187) d++;
if (poty>=223) d++;
if (poty>=251) d++;
} /* TODO NTSC */
OCR1B=d;
}

void loop() {
int s;
if (digitalRead(2)) {
posx ++;
set_potx(posx);
posy ++;
set_poty(posy);
}
Serial.println(posx);
s=sid_measurement_cycles>>8;
/* Because timer 0 is stopped, we cannot use the normal delay() routines, so we have to
delay a different way. */
while (s==sid_measurement_cycles>>8)
{};
}

The input voltage


There is one final thing to worry about. The program above gives the exact right result when the Arduino is powered from USB. When powered from the C64, the values are off by 1. It turns out that the cause is the voltage: My voltage meter measures 5.11V on the Arduino when powered from USB, just 4.91V when powered from C64.

Therefore, in order to generate really 100% perfect results, it is necessary to adjust for this. I think this can be done by burning a little bit of the voltage with a zener diode like this:



The Arduino outputs voltages of approximately 5V. The zener diode burns any voltage higher than 4.7V away into heat. The BAT43 has an official voltage drop of max. 0.33V, but my measurements show about 0.2V drop. This means that any supply voltage higher than about 4.9V is burned away, probably good enough for our purposes. We should be able to charge the SID capacitors fast enough with 4.7V and knowing it is exactly 4.7 should eliminate the uncertainty about the exact input voltage.

The next standard zener value below 4.7V is 4.3V. Here I have more doubts that we are able to charge fast and high enough, but if so, this allows even more tolerance in the input voltage and more choice in diodes.

I think will be a good idea to design the Megastick PCB with room for both a zener diode and potmeter: It is always possible to remove the zener later and replace the potmeter with a fixed resistor, but at least initially it will be very useful to do some manual adjustments in order to get exactly the right value. 0-500 Ω is probably better for accuracy purposes than the 0-1000 Ω that I used.

I was thinking about using a MOSFET as an alternative, so you can avoid the diode and its voltage drop:



... but this probably doesn't work: While the POT line is charged, the voltage between gate and drain drops, causing the the MOSFET to turn itself off, likely too early.


6 comments:

  1. Small hint: The term "0<ACIS0" in the ACR configuration looks as if "0<<ACIS0" was supposed to be there instead. In this specific case, it works though (ACIS0 is 0, and 0<0 is 0).

    ReplyDelete
  2. Thanks for the hint! It is indeed a typo.

    ReplyDelete
  3. Thanks for sharing - the progress you guys make is really interesting to follow.

    Out of curiosity, this mini-project doesn't happen to be part of plan that involves shipping every M65 with a 1351-compatible mouse?

    ReplyDelete
    Replies
    1. Hello,

      You're welcome :)

      At this stage, we don't yet have our own 1351-compatible mouse, so it would be premature to suggest such a bundling. Also, we are keen to keep the price of the M65 as low as possible for everyone. However, that doesn't preclude us releasing a 1351-compatible mouse. In particular, we are exploring creating a solid-state joystick with analog sensors that can be used as a 1351-compatible mouse, with proportional movement, to avoid the need to switch input devices as often.

      Paul.

      Delete
    2. Hi Paul!

      Much appreciated! ^=^

      Great to hear - I _love_ progressive thinking, perhaps even more so when it comes to the typical retro scene. Adding that extra flair to the Commodore range of computers usually adds to the experience (at least if implemented right!).

      That being said, I love the idea of having a joystick to act as a 1351 stand-in! Like you say, there are situations where your desk can be filled with all sorts of accessories, and this is certainly one way of getting rid of one extra device.

      However, in other situations I feel that there may actually be a good idea to use an actual mouse. Granted, I don't play Ultima 5 every day, but when I do I'd choose a 1351 over any joystick - including the Tac-2! - any day. Moreover, I would never dream of using GEOS with a joystick.

      That being said, do you guys reason in a similar way? Does developing a solid-state joystick with analog senors rule out official mouse support?

      Delete
    3. Hello,

      We fully intend that real 1351 mice will work with the M65 -- this is part of why we wanted to reverse engineer the 1351, so we know what it needs to work.

      As for our solid-state joystick, it will support proportional motion when emulating a mouse, so it will likely work quite well for GEOS and some other cases where a normal digital joystick is horrible. The only way to find out, is to make it.

      Paul.

      Delete