Skip to content

Commit

Permalink
Support for mouse scroll wheel and extra buttons (#15)
Browse files Browse the repository at this point in the history
* Use Stefan's compact version of OneButton.

* Enable Perixx keyboard; set numlock at power on

* Remove activity LED debug code.

* Move mouse and keyboard initialization state machine to START_RESET after reset.

* Reset mouse PS2 state machine when powering off.

* Pull x16community/main

* Add firmware versioning

* Update to origin main

* Fix for hotplug problem

* Intellimouse support

* Make mouse device ID = BAT_FAIL if init fails

* Comment out COMMUNITYX16_PINS to support proto4

* Starting writing a README file

* Added content to the README file

* Updated README file

* Updated README file

* Updated README

* Bump version number

---------

Co-authored-by: jburks <[email protected]>
  • Loading branch information
stefan-b-jakobsson and jburks authored Sep 3, 2023
1 parent d806d43 commit ca6d172
Show file tree
Hide file tree
Showing 6 changed files with 531 additions and 280 deletions.
152 changes: 152 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# About

This is the firmware for the Commander X16 System Management Controller (SMC).

The SMC is responsible for

- Power control
- The physical buttons on the board
- The activity LED
- Interfacing a PS/2 keyboard
- Interfacing a PS/2 mouse

# I2C API

## Table of commands
| Command # | Master read/write | Data | Description |
| ----------|-------------------|-------------------|-------------------------------|
| 0x01 | Master write | 0x00 | Power off |
| 0x02 | Master write | 0x00 | Reset |
| 0x03 | Master write | 0x00 | NMI    |
| 0x05 | Master write | 1 byte | Set activity LED level |
| 0x07 | Master read | 1 byte | Get keyboard keycode |
| 0x08 | Master write | 1 byte | Echo |
| 0x09 | Master write | 1 byte | Debug output |
| 0x18 | Master read | 1 byte | Get keyboard command status |
| 0x19 | Master write | 1 byte | Send keyboard command |
| 0x1a | Master write | 2 bytes | Send keyboard command |
| 0x20 | Master write | 1 byte | Set requested mouse device ID |
| 0x21 | Master read | 1, 3 or 4 bytes | Get mouse movement |
| 0x22 | Master read | 1 byte | Get mouse device ID |
| 0x30 | Master read | 1 byte | Firmware version major |
| 0x31 | Master read | 1 byte | Firmware version minor |
| 0x32 | Master read | 1 byte | Firmware version patch |
| 0x8f | Master write | 0x31 | Start bootloader |

## Power, Reset and Non-Maskable Interrupt (0x01, 0x02, 0x03)

The commands 0x01 (Power Off), 0x02 (Reset) and 0x03 (NMI) have the same function as the board's physical push buttons.

Each function requires the user to send one byte with the value 0x00. Example that powers off the system.

```
I2CPOKE $42,$01,$00
```

## Set Activity LED level (0x05)

The Activity LED is turned off if you write the value 0x00 and turned on if you write the value 0xff to this offset.

Writing other values may cause instability issues, and is not recommended.

Example that turns on the Activty LED:

```
I2CPOKE $42,$05,$FF
```

## Get keyboard keycode (0x07)

The SMC translates PS/2 scan codes sent by the keyboard into IBM key codes. A list of key
codes is available in the Kernal source.

Key codes are one byte. Bit 7 indicates if the key was pressed (0) or released (1).

This command gets one keycode from the buffer. If the buffer is empty, it returns 0.

## Get keyboard command status (0x18)

This offset returns the status of the last host to keyboard command.

The possible return values are:

- 0x00 = idle, no command has been sent
- 0x01 = pending, the last command is being processed
- 0xfa = the last command was successful
- 0xfe = the last command failed

## Send keyboard command (0x19 and 0x1a)

These offsets send host to keyboard commands.

Offset 0x19 sends a command that expects just the command number, and no data. Example that disables the keyboard:

```
I2CPOKE $42,$19,$f5
```

Offset 0x1a sends a command that expects a command number and one data byte. This can't be done with the I2CPOKE command.

## Set requested mouse device ID (0x20)

By default the SMC tries to initialize the mouse with support for a scroll wheel and two extra buttons, in total five buttons (device ID 4).

If the connected mouse does not support device ID 4, the SMC then tries to initialize the mouse with support for a scroll wheel but no extra buttons (decive ID 3).

If that fails as well, the SMC falls back to a standard mouse with three buttons and no scroll wheel (device ID 0).

This command lets you set what device ID you want to use. The valid options are 0, 3 or 4, as described above.

Example that forces the SMC to initialize the mouse as a standard mouse with no scroll wheel support:

```
I2CPOKE $42,$20,$00
```

## Get mouse movement (0x21)

This command returns the mouse movement packet.

If no movement packet is available, it returns a 0 (1 byte).

If the mouse is initialized as device ID 0 (standard three button mouse, no scroll wheel), it returns a three byte packet.

And if the mouse is initialized as device ID 3 (scroll wheel) or 4 (scroll wheel+extra buttons), it returns a four byte packet.

## Get mouse device ID (0x22)

This returns the current mouse device ID. The possible values are:

- 0x00 = a standard PS/2 mouse with three buttons, no scroll wheel
- 0x03 = a mouse with three buttons and a scroll wheel
- 0x04 = a mouse with two extra buttons, a total of five buttons, and a scroll wheel
- 0xfc = no mouse connected, or initialization of the mouse failed

The returned device ID may differ from the requested device ID. For instance,
if you requested mouse device ID 4, but the extra buttons are not available on the
connected mouse, you will get ID 3 (if there is a scroll wheel).

Example that returns the current mouse device ID:

```
PRINT I2CPEEK($42,$22)
```


## Firmware version (0x30, 0x31 and 0x32)

The offsets 0x30, 0x31 and 0x32 return the current firmware version (major-minor-patch).

## Start bootloader (0x8f)

This command initiates the sequence that starts the bootloader.

The bootloader, if present, is stored at 0x1E00 in the SMC flash memory. It is
used to update the firmware from the Commander X16 without an external programmer.
The new firmware is transmitted over I2C to the bootloader.

Calling this function starts a timer (20 s). Within that time the user must
press the physical Power and Reset buttons on the board for the bootloader
to actually start. This is a safety measure, so that you don't
start the bootloader by mistake. Doing so will leave the SMC inoperable if the
update process is not carried through.
128 changes: 88 additions & 40 deletions mouse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

extern bool SYSTEM_POWERED;
extern PS2KeyboardPort<PS2_KBD_CLK, PS2_KBD_DAT, 16> Keyboard;
extern PS2Port<PS2_MSE_CLK, PS2_MSE_DAT, 8> Mouse;
extern PS2Port<PS2_MSE_CLK, PS2_MSE_DAT, 16> Mouse;

MOUSE_INIT_STATE_T mouse_init_state = OFF;
uint8_t mouse_id = BAT_FAIL;
uint8_t mouse_id_req = 0x04;
bool mouse_wheel_request_made = false;

// Mouse initialization state machine
void MouseTick()
Expand Down Expand Up @@ -43,6 +46,8 @@ void MouseTick()
{
case OFF:
if (SYSTEM_POWERED != 0) {
mouse_id = BAT_FAIL;
mouse_wheel_request_made = false;
next_state = POWERUP_BAT_WAIT;

// If we don't see the mouse respond, jump to sending it a reset.
Expand All @@ -68,11 +73,16 @@ void MouseTick()
case POWERUP_ID_WAIT:
if (Mouse.available()) {
uint8_t b = Mouse.next();
if (b == MOUSE_ID)
{
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE);
if (b == MOUSE_ID || b == INTELLIMOUSE_ID1 || b == INTELLIMOUSE_ID2) {
mouse_id = b;
if (mouse_id_req == 0x00) {
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE,40);
next_state = SAMPLERATE_ACK_WAIT;
}
else {
next_state = ENABLE_INTELLIMOUSE;
}
MOUSE_REARM_WATCHDOG();
next_state = SAMPLERATECMD_ACK_WAIT;
}
// Watchdog will eventually send us to START_RESET if we don't get MOUSE_ID
}
Expand All @@ -83,6 +93,8 @@ void MouseTick()
break;

case START_RESET:
mouse_id = BAT_FAIL;
mouse_wheel_request_made = false;
Mouse.flush();
Mouse.sendPS2Command(mouse_command::RESET);
MOUSE_WATCHDOG(FAILED);
Expand All @@ -93,63 +105,98 @@ void MouseTick()
if (mstatus == mouse_command::ACK) {
Mouse.next();
MOUSE_REARM_WATCHDOG();
next_state = RESET_BAT_WAIT;
next_state = POWERUP_BAT_WAIT;
} else {
next_state = FAILED; // Assume an error of some sort.
}
break;

case RESET_BAT_WAIT: // RECEIVE BAT_OK
// expect self test OK
if (Mouse.available()) {
uint8_t b = Mouse.next();
if ( b != mouse_command::BAT_OK ) {
next_state = FAILED;
} else {
MOUSE_REARM_WATCHDOG();
next_state = RESET_ID_WAIT;
}

case SAMPLERATE_ACK_WAIT:
Mouse.flush();
if (mstatus != mouse_command::ACK) {
next_state = PRE_RESET; // ?? Try resetting again, I guess.
}
else {
Mouse.sendPS2Command(mouse_command::ENABLE);
MOUSE_REARM_WATCHDOG();
next_state = ENABLE_ACK_WAIT;
}
break;

case RESET_ID_WAIT: // RECEIVE MOUSE_ID, SEND SET_SAMPLE_RATE
// expect mouse ID byte (0x00)
if (Mouse.available()) {
uint8_t b = Mouse.next();
if ( b != mouse_command::MOUSE_ID ) {
next_state = FAILED;
} else {
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE);
MOUSE_REARM_WATCHDOG();
next_state = SAMPLERATECMD_ACK_WAIT;
case ENABLE_INTELLIMOUSE:
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE, 200);
MOUSE_REARM_WATCHDOG();
next_state = INTELLIMOUSE_WAIT_1;
break;

case INTELLIMOUSE_WAIT_1:
Mouse.flush();
if (mstatus != mouse_command::ACK) {
next_state = PRE_RESET; // ?? Try resetting again, I guess.
}
else {
if (mouse_wheel_request_made) {
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE, 200);
}
else {
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE, 100);
}
MOUSE_REARM_WATCHDOG();
next_state = INTELLIMOUSE_WAIT_2;
}
break;

case SAMPLERATECMD_ACK_WAIT: // RECEIVE ACK, SEND 20 updates/sec
Mouse.next();
if (mstatus != mouse_command::ACK)
case INTELLIMOUSE_WAIT_2:
Mouse.flush();
if (mstatus != mouse_command::ACK) {
next_state = PRE_RESET; // ?? Try resetting again, I guess.
}
else {
Mouse.sendPS2Command(60);
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE, 80);
MOUSE_REARM_WATCHDOG();
next_state = SAMPLERATE_ACK_WAIT;
next_state = INTELLIMOUSE_WAIT_3;
}
break;

case SAMPLERATE_ACK_WAIT: // RECEIVE ACK, SEND ENABLE
Mouse.next();
if (mstatus != mouse_command::ACK)
next_state = PRE_RESET;
case INTELLIMOUSE_WAIT_3:
Mouse.flush();
if (mstatus != mouse_command::ACK) {
next_state = PRE_RESET; // ?? Try resetting again, I guess.
}
else {
Mouse.sendPS2Command(mouse_command::ENABLE);
Mouse.sendPS2Command(mouse_command::READ_DEVICE_TYPE);
MOUSE_REARM_WATCHDOG();
next_state = ENABLE_ACK_WAIT;
next_state = READ_DEVICE_ID_WAIT;
}
break;

case READ_DEVICE_ID_WAIT:
if (Mouse.available()) {
uint8_t b = Mouse.next();
if (b == mouse_command::ACK) {
break;
}
else if ( b == mouse_command::MOUSE_ID || b == mouse_command::INTELLIMOUSE_ID1 || b == mouse_command::INTELLIMOUSE_ID2) {
mouse_id = b;
if (mouse_wheel_request_made || mouse_id_req == 0x03) {
Mouse.sendPS2Command(mouse_command::SET_SAMPLE_RATE, 40);
MOUSE_REARM_WATCHDOG();
next_state = SAMPLERATE_ACK_WAIT;
}
else {
mouse_wheel_request_made = true;
MOUSE_REARM_WATCHDOG();
next_state = ENABLE_INTELLIMOUSE;
}
}
else {
next_state = FAILED;
}
}
break;

case ENABLE_ACK_WAIT: // Receive ACK
Mouse.next();
Mouse.flush();
if (mstatus != mouse_command::ACK) {
next_state = PRE_RESET;
} else {
Expand All @@ -169,6 +216,7 @@ void MouseTick()

default:
// This is where the FAILED state will end up
mouse_id = BAT_FAIL;
break;
}
mouse_init_state = next_state;
Expand Down
12 changes: 11 additions & 1 deletion mouse.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ enum mouse_command : uint8_t
BAT_OK = 0xAA, // Basic Acceptance Test passed
BAT_FAIL = 0xFC, // Basic Acceptance Test failed
MOUSE_ID = 0x00, // Mouse identifier
INTELLIMOUSE_ID1 = 0x03, // Intellimouse scroll wheel identifier
INTELLIMOUSE_ID2 = 0x04, // Intellimouse scroll wheel and extra button identifier
SET_SAMPLE_RATE = 0xF3, // Mouse sample rate command
READ_DEVICE_TYPE = 0xF2, // Request device type
SET_RESOLUTION = 0xE8,
Expand All @@ -26,7 +28,13 @@ typedef enum MOUSE_INIT_STATE : uint8_t {
RESET_BAT_WAIT,
RESET_ID_WAIT,

SAMPLERATECMD_ACK_WAIT,
ENABLE_INTELLIMOUSE,
INTELLIMOUSE_WAIT_1,
INTELLIMOUSE_WAIT_2,
INTELLIMOUSE_WAIT_3,

READ_DEVICE_ID_WAIT,

SAMPLERATE_ACK_WAIT,

ENABLE_ACK_WAIT,
Expand All @@ -44,6 +52,8 @@ typedef enum MOUSE_INIT_STATE : uint8_t {
} MOUSE_INIT_STATE_T;

extern MOUSE_INIT_STATE_T mouse_init_state;
extern uint8_t mouse_id;
extern uint8_t mouse_id_req;
extern uint8_t kbd_init_state;

#define MOUSE_REARM_WATCHDOG() \
Expand Down
Loading

0 comments on commit ca6d172

Please sign in to comment.