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;
}
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)
{};
}
/**************************************************************************************************
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.
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).
ReplyDeleteThanks for the hint! It is indeed a typo.
ReplyDeleteThanks for sharing - the progress you guys make is really interesting to follow.
ReplyDeleteOut of curiosity, this mini-project doesn't happen to be part of plan that involves shipping every M65 with a 1351-compatible mouse?
Hello,
DeleteYou'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.
Hi Paul!
DeleteMuch 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?
Hello,
DeleteWe 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.