Skip to content

Commit

Permalink
Implement firmware command handling and second-core HTTP transfers
Browse files Browse the repository at this point in the history
 - Avoid allocating strings in the heap (server and URL path names)
 - Allow HTTP transfers to be interrupted
 - Add stop command to streamer ROM
 - Add underflow handling to streamer ROM
 - Fix constness of internal interfaces
 - Add missing overview comments in headers

Not done yet:
 - Streaming command handling
 - Server metadata format
  • Loading branch information
joeyparrish committed Jul 2, 2024
1 parent a4ef2d9 commit 8f20f8d
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 34 deletions.
4 changes: 4 additions & 0 deletions firmware/error.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
//
// See MIT License in LICENSE.txt

// Firmware that runs on the microcontroller inside the cartridge.
// The microcontroller accepts commands from the player in the Sega ROM, and
// can stream video from the Internet to the cartridge's shared banks of SRAM.

// Error reporting.

#ifndef _KINETOSCOPE_ERROR_H
Expand Down
185 changes: 174 additions & 11 deletions firmware/firmware.ino
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,52 @@
#include "registers.h"
#include "speed-tests.h"
#include "sram.h"
#include "string-util.h"

#define DEBUG
#define RUN_TESTS

// NOTE: This must be a plain HTTP server. HTTPS is too expensive for this
// application and microcontroller. Even though we could do it, it would hurt
// our slim throughput margins too much.
#define VIDEO_SERVER "storage.googleapis.com"
#define VIDEO_SERVER_PORT 80
// TODO: Finalize the metadata format and upload this.
#define VIDEO_LIST_PATH "/sega-kinetoscope/canned-videos/list.txt"

#define MAX_SERVER 256
#define MAX_PATH 256
#define MAX_FETCH_SIZE (1024 * 1024)

// Allocate a second 8kB stack for the second core.
// https://github.com/earlephilhower/arduino-pico/blob/master/docs/multicore.rst
bool core1_separate_stack = true;

// An actual MAC address assigned to me with my ethernet board.
// Don't put two of these devices on the same network, y'all.
uint8_t MAC_ADDR[] = { 0x98, 0x76, 0xB6, 0x12, 0xD4, 0x9E };
static const uint8_t MAC_ADDR[] = { 0x98, 0x76, 0xB6, 0x12, 0xD4, 0x9E };

static void freeze() {
while (true) { delay(1000); }
while (true) { delay(1000 /* ms */); }
}

void setup() {
Serial.begin(115200);
while (!Serial) { delay(10); } // Wait for serial port to connect
// The second core waits on this variable before beginning its loop.
static bool hardware_ready = false;

// Delay startup so we can have the serial monitor attached.
delay(1000);
Serial.println("Kinetoscope boot!\n");
// The second core uses these to receive commands from the first core.
static volatile bool second_core_idle = true;
static volatile bool second_core_interrupt = false;
static char fetch_server[MAX_SERVER];
static volatile uint16_t fetch_port = 0;
static char fetch_path[MAX_PATH];
static volatile int fetch_start_byte = 0;
static volatile int fetch_size = 0;

static void init_all_hardware() {
sram_init();
registers_init();

// Turn on LED as a primitive visual status.
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);

Expand Down Expand Up @@ -73,12 +100,148 @@ void setup() {
}
}

void loop() {
#if 0
static void second_core_fetch(const char* server, uint16_t port,
const char* path, int start_byte = 0,
int size = MAX_FETCH_SIZE) {
copy_string(fetch_server, server, MAX_SERVER);
fetch_port = port;
copy_string(fetch_path, path, MAX_PATH);
fetch_start_byte = start_byte;
fetch_size = size;
second_core_idle = false;
}

static void await_second_core_fetch() {
while (!second_core_idle) { delay(1 /* ms */); }
}

static void process_command(uint8_t command, uint8_t arg) {
switch (command) {
case KINETOSCOPE_CMD_ECHO:
// Write the argument to SRAM so the ROM software knows we are listening.
sram_start_bank(0);
sram_write(&arg, sizeof(arg));
sram_flush();
break;

case KINETOSCOPE_CMD_LIST_VIDEOS:
if (!second_core_idle) {
report_error("Command conflict! Busy!");
break;
}

// Pull video list into SRAM.
Serial.println("Fetching video list...");
second_core_fetch(VIDEO_SERVER, VIDEO_SERVER_PORT, VIDEO_LIST_PATH);
await_second_core_fetch();
Serial.println("Done.");
break;

case KINETOSCOPE_CMD_START_VIDEO:
if (!second_core_idle) {
report_error("Command conflict! Busy!");
break;
}

// TODO: Start streaming. Fill both SRAM banks before returning.
break;

case KINETOSCOPE_CMD_STOP_VIDEO:
if (!second_core_idle) {
// Stop streaming. Interrupt any download in progress.
second_core_interrupt = true;
// Wait for recognition of the interrupt.
while (!second_core_idle || second_core_interrupt) {
delay(1 /* ms */);
}
}
break;

case KINETOSCOPE_CMD_FLIP_REGION:
if (!second_core_idle) {
report_error("Buffer underflow! Internet too slow?");
break;
}

// TODO: Start filling the next SRAM bank.
break;

case KINETOSCOPE_CMD_GET_ERROR:
// Write a buffered error message to SRAM so the ROM software can read it.
write_error_to_sram();
break;
}

clear_cmd();
}

// Setup and loop for the first core. This core will initialize all hardware
// and then process incoming commands from the Sega.

void setup() {
Serial.begin(115200);

#ifdef DEBUG
while (!Serial) { delay(10 /* ms */); } // Wait for serial port to connect

// Delay startup so we can have the serial monitor attached.
delay(1000 /* ms */);
Serial.println("Kinetoscope boot!\n");
#endif

init_all_hardware();

#ifdef RUN_TESTS
if (!is_error_flagged()) {
run_tests();
}
#endif

freeze();
// Allow the second core to start its loop.
hardware_ready = true;
}

void loop() {
if (!is_cmd_set()) {
return;
}

uint8_t command = read_register(KINETOSCOPE_REG_CMD);
uint8_t arg = read_register(KINETOSCOPE_REG_ARG);
process_command(command, arg);
}

// Setup and loop for the second core. These methods may be specific to the
// RP2040 core, so they may need to be ported if anyone wants to use a
// different microcontroller.

static bool http_transfer_callback(const uint8_t* buffer, int bytes) {
// Check for interrupt.
if (second_core_interrupt) {
return false;
}

sram_write(buffer, bytes);
return true;
}

void setup1() {
// Wait for the first core to finish initializing the hardware.
while (!hardware_ready) { delay(1 /* ms */); }
}

void loop1() {
if (second_core_idle) {
return;
}

// Begin requested transfer. The callback will check for interrupts via
// second_core_interrupt. The http library will report an error to the Sega
// if it fails.
http_fetch(fetch_server, fetch_port, fetch_path, fetch_start_byte,
fetch_size, http_transfer_callback);

// Clear state.
second_core_interrupt = false;
second_core_idle = true;
}
31 changes: 18 additions & 13 deletions firmware/http.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

#include "error.h"
#include "http.h"
#include "string-util.h"

#define DEFAULT_PORT 80

#define MAX_READ 8192
#define MAX_SERVER 256

#define CONTENT_LENGTH_HEADER "Content-Length: "
#define CONTENT_LENGTH_HEADER_LENGTH 16
Expand All @@ -44,7 +46,7 @@ struct HeaderData {
};

static Client* client = NULL;
static char* current_server = NULL;
static char current_server[MAX_SERVER];
static int current_port = 0;
static char range_value[128];
static char request_buffer[1024];
Expand All @@ -53,14 +55,11 @@ static uint8_t read_buffer[MAX_READ];

void http_init(Client* network_client) {
client = network_client;
current_server[0] = '\0';
}

// We will use persistent connections as much as possible to speed up requests.
static inline bool need_new_connection(const char* server, int port) {
if (!current_server) {
return true;
}

if (strcmp(server, current_server)) {
return true;
}
Expand All @@ -78,11 +77,7 @@ static inline bool need_new_connection(const char* server, int port) {

static void close_connection() {
client->stop();

if (current_server) {
free(current_server);
current_server = NULL;
}
current_server[0] = '\0';
}

static inline void connect_if_needed(const char* server, int port) {
Expand All @@ -103,7 +98,7 @@ static inline void connect_if_needed(const char* server, int port) {

client->connect(server, port);

current_server = strdup(server);
copy_string(current_server, server, MAX_SERVER);
current_port = port;
}

Expand Down Expand Up @@ -276,7 +271,12 @@ int http_fetch(const char* server, uint16_t port, const char* path,

// Copy the body bytes found in the header buffer.
if (header_data.body_start_length) {
callback(header_data.body_start, header_data.body_start_length);
bool ok = callback(header_data.body_start, header_data.body_start_length);
if (!ok) {
Serial.println("Transfer interrupted.");
close_connection();
return -1;
}
bytes_left -= header_data.body_start_length;
}

Expand All @@ -298,7 +298,12 @@ int http_fetch(const char* server, uint16_t port, const char* path,
continue;
}

callback(read_buffer, bytes_read);
bool ok = callback(read_buffer, bytes_read);
if (!ok) {
Serial.println("Transfer interrupted.");
close_connection();
return -1;
}
bytes_left -= bytes_read;
}

Expand Down
2 changes: 1 addition & 1 deletion firmware/http.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

#include <Client.h>

typedef void (*http_buffer_callback)(const uint8_t* buffer, int bytes);
typedef bool (*http_buffer_callback)(const uint8_t* buffer, int bytes);

void http_init(Client* network_client);

Expand Down
5 changes: 3 additions & 2 deletions firmware/internet-wired.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

EthernetClient client;

Client* internet_init_wired(uint8_t* mac) {
Client* internet_init_wired(const uint8_t* mac) {
SPI.begin();

// Default SPI pins for RP2040:
Expand All @@ -28,7 +28,8 @@ Client* internet_init_wired(uint8_t* mac) {
// MISO == GP16
// SCK == 18

int ok = Ethernet.begin(mac);
// It's really stupid that this library doesn't take a const input.
int ok = Ethernet.begin(const_cast<uint8_t*>(mac));

if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println("Ethernet shield was not found.");
Expand Down
2 changes: 1 addition & 1 deletion firmware/internet.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@
// Password can be blank or null if there is no authentication required.
Client* internet_init_wifi(const char* ssid, const char* password);

Client* internet_init_wired(uint8_t* mac);
Client* internet_init_wired(const uint8_t* mac);

#endif // _KINETOSCOPE_INTERNET_H
1 change: 1 addition & 0 deletions firmware/registers.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#define KINETOSCOPE_CMD_START_VIDEO 0x02 // Begins streaming to SRAM
#define KINETOSCOPE_CMD_STOP_VIDEO 0x03 // Stops streaming
#define KINETOSCOPE_CMD_FLIP_REGION 0x04 // Switch SRAM banks for streaming
#define KINETOSCOPE_CMD_GET_ERROR 0x05 // Load error information into SRAM

void registers_init();

Expand Down
9 changes: 7 additions & 2 deletions firmware/speed-tests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// A safe buffer size for these tests.
#define BUFFER_SIZE 100 * 1024

static long test_sram_speed(uint8_t* buffer, int bytes) {
static long test_sram_speed(const uint8_t* buffer, int bytes) {
// 100kB: ~83ms
// 1MB: ~830ms
// 3s video+audio: ~731ms
Expand Down Expand Up @@ -66,6 +66,11 @@ static long test_register_read_speed() {
return end - start;
}

static bool sram_write_callback(const uint8_t* buffer, int bytes) {
sram_write(buffer, bytes);
return true; // HTTP transfer can continue.
}

static long test_download_speed(int first_byte, int total_size) {
// 2.5Mbps minimum required
// ~2.7Mbps with initial HTTP connection overhead
Expand All @@ -74,7 +79,7 @@ static long test_download_speed(int first_byte, int total_size) {
sram_start_bank(0);
int bytes_read = http_fetch(SERVER, /* default port */ 0, PATH,
first_byte, total_size,
sram_write);
sram_write_callback);
sram_flush();
long end = millis();
return end - start;
Expand Down
4 changes: 4 additions & 0 deletions firmware/speed-tests.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
//
// See MIT License in LICENSE.txt

// Firmware that runs on the microcontroller inside the cartridge.
// The microcontroller accepts commands from the player in the Sega ROM, and
// can stream video from the Internet to the cartridge's shared banks of SRAM.

// Microcontroller function speed tests.

#ifndef _KINETOSCOPE_SPEED_TESTS_H
Expand Down
Loading

0 comments on commit 8f20f8d

Please sign in to comment.