diff --git a/README.md b/README.md index 16a2588..e98603d 100755 --- a/README.md +++ b/README.md @@ -1,63 +1,34 @@ -apple2pi -======== +# Apple II Pi -What is the Apple II Pi? ------------------------- -Basically, the Apple II Pi is the integration of an Apple II with a Raspberry Pi (http://www.raspberrypi.org) to create a hybrid computer combining the input devices and storage mediums of the Apple with the CPU, GPU (graphical processing unit), USB, and network capabilities of the Broadcom BCM2835 SoC (think smartphone processor). The concept is to create an updated version of the Apple II using some imagination, low-level drivers, off-the-shelf hardware, and a closely coupled communications channel; thus bringing semi-modern hardware and 32 bit *nix software to the Apple II platform. The Apple II is running as a dedicated I/O processor for the Raspberry Pi under ProDOS. The Raspberry Pi is running a version of Debian Linux: Raspbian. Much like the PC Transporter card brought MS-DOS and the Z-80 card brought CP/M, the Apple II Pi brings Linux to the Apple II using the Apple’s input devices and the Raspberry Pi’s video output. As such, knowledge and familiarity with Linux is required to get the most out of its environment. Under Linux, the Apple II Pi can read and write the Apple’s storage devices (floppies/harddrives/CFFA) and also run the GSport Apple IIgs emulator (http://gsport.sourceforge.net). Together, GSport and Apple II Pi provide an immersive environment providing access to most of the Apple II hardware you own plus an accelerated ~20MHz 65816 with up to 8 MB RAM, and all the disk images you can fit on the SD card. +See the [upstream repo](https://github.com/dschmenk/apple2pi) for the original, full info. -Apple II client/server for Raspberry Pi ---------------------------------------- +## Installing -Apple II Pi works by connecting an Apple II to a Raspberry Pi using a RS232 serial connection. In order to get the Raspberry Pi to talk RS232 from it's 3.3V GPIO serial port, you will need to build or buy a converter. They are very cheap on eBay, so I would recommend going that route. Alternatively, you can use a USB serial port converter, but you will need to know its tty device name. To ensure you've hooked the converter up correctly, try loggin into the Raspberry Pi from another modern-ish computer. Raspbian, the default Debian based Linux for the Raspberry Pi, opens up a login (getty) session on the serial port at 115.2K baud. You will probably need a null modem or cross-over cable to login from another computer. Once it all checks out, time to connect your Apple II. All the 3.3V converters and USB serial ports I see have a DB-9 connector and many of the Apple II era connectors are DB-25 so you may need a DB-9 to DB-25 converter. +1. Flash [Apple-II-Pi.uf2](https://github.com/oliverschmidt/apple2pi/releases/latest/download/Apple-II-Pi.uf2) to the Raspberry Pi Pico. -Installing and configuring the Apple II: You will need an Apple //c or Apple ][, //e, IIgs w/ SuperSerial Card. An Apple ][ requires the SHIFT key mod. An Apple II Mouse is recommended for that full-on retro feel, but not required. Download and install the A2PI.PO disk image onto a 5 1/4 floppy. ADTPro would be the recommended tool for that operation although once you have the latest apple2pi version running, you can use the included dskwrite and dskread utilities for writing and reading ProDOS floppies. +2. Install [Raspberry Pi OS](https://www.raspberrypi.org/software/). -Installing and configuring the Raspberry Pi: Download the apple2pi project to your Raspberry Pi. Enter the apple2pi/src directory. Compile the daemon and tools with 'make' and copy the results to /usr/local/bin with 'sudo make install'. To build the FUSE driver needed to mount ProDOS devices under Linux, you will need the libfuse-dev package installed. Get this from apt-get, aptitude, or whichever package manager you like. Build with 'make fusea2pi' and install with 'sudo make fuse-install'. +3. Execute + ``` + sudo apt install git libfuse-dev -y + git clone https://github.com/oliverschmidt/apple2pi.git + cd apple2pi + make + sudo make install + ``` -The following is no longer neessary as the install script carefully makes all the following adjustments automatically. I left this here so you know what the script is doing. +4. Execute `sudo systemctl start a2pi.service` to run the Apple II Pi Daemon right away. +## Using ->You will need to disable the Raspbain serial login by editing /etc/inittab and commenting out the line (probably at the very bottom) to look like:
- -\#T0:23:respawn:/sbin/getty -L ttyAMA0 115200 vt100 - -
-You will also want to disable the console messages that go out to the serial port in the /boot/cmdline.txt file. Remove the "console=" clause and the "kgdboc=" clause from the /boot/cmdline.txt file. Mine looks like:
- -dwc_otg.lpm_enable=0 console=tty1 root=/dev/mmcblk0p2 rootfstype=ext4 elevator=deadline rootwait - +1. Start Apple II Pi either via cold boot or `PR#n`. ->If you are using an HDMI port to display video, skip the overscan settings. This was for my monitor and your values may vary (a lot). I adjust the NTSC output so it fits nicely on my //c monitor, I edited the setting in /boot/config.txt such that: +2. Make sure to check out `a2term` in a Raspberry Pi shell. -> -overscan_left=26
-overscan_right=-8
-overscan_top=-8
-\#overscan_bottom=16
-
+## Building ->To run the a2pid daemon automatically at boot time, edit /etc/rc.local and add: +1. Execute `make` in `/pidrive` (requires [cc65](https://cc65.github.io/)). -> -/usr/local/bin/a2pid --daemon - +2. Build the project in `/pipico` (requires the [Raspberry Pico C/C++ SDK](https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html)). ->right before the line: - -> -exit 0 - - -followed by rebooting the Raspberry Pi. - -NOTE - For USB serial port users and non-Raspberry Pi owners: This isn't actually tied to the Raspberry Pi in any way, except for the default serial port used to connect the Pi to the Apple II. On most up-to-date Linux distributions, you should be able to build all the files. To run the daemon on a specific serial port, just add it as a command line option i.e. a2pid --daemon /dev/ttyUSB0 - -Reboot the Apple II with the newly created floppy in the start-up drive. If everything is configured correctly, you should be able to login to the Raspberry Pi with your Apple II keyboard. If you have an Apple II Mouse, that should control the cursor in X, or in the console if you have gdm installed. - -Using a2pi: The Apple //c and //e keyboards are pretty minimal compared to modern keyboards, and the Apple II Mouse only has one button. In order to provide most of the funcitonality required of modern OSes, the Open-Apple and Closed-Apple keys are used as modifiers to enhance the keyboard and mouse. On the keyboard, Open-Apple acts just like the Alt key. The Closed-Apple key acts like a Fn key, changing the actual key codes. Currently, the Closed-Apple key will modify the number keys 1-0 as funciton keys F1-F10 and the arrow keys as Left-Arrow=Home, Right-Arrow=End, Up-Arrow=PgUp, Down-Arrow=PgDn. For the mouse, when you click the mouse button by itself, that is the left(default)-click. Open-Apple along with the mouse button will return the right-click, and Closed-Apple along with the mouse button will return the middle-click. a2pid can be run directly (not as a daemon) by leaving off the '--daemon' option. Enabling printf's in the code allows one to watch the packets arrive and get processed when run from a network ssh session. - -Theory of operation: Apple II Pi works by running code on the Apple II and the Raspberry Pi, talking to each other with a simple protocol. The Apple II basically appears to the Raspberry Pi as an external peripheral, not unlike a USB keyboard and mouse. The Apple II floppy boots into ProDOS and runs a simple machine language program that scans the keyboard, and mouse if present, sending the events out the serial port to the Raspberry Pi. It is a very simple protocol and the serial port is running at 115.2K baud, so it is fast and low overhead. On the Raspberry Pi, a little daemon runs, waiting for packets on the serial port, converts the Apple II events into Linux compatible events, and inserts them into the input subsystem. This daemon also has a socket interface (port 6551) that can be used to access the Apple II memory and execute arbitrary code. Look at a2lib.c for implementation. - -Enjoy, - -Dave Schmenk +3. Execute `make` in `/src` (requires [libfuse-dev](https://packages.debian.org/en/sid/libfuse-dev)). diff --git a/pipico/CMakeLists.txt b/pipico/CMakeLists.txt new file mode 100644 index 0000000..7bac238 --- /dev/null +++ b/pipico/CMakeLists.txt @@ -0,0 +1,33 @@ +set(PROJECT_NAME Apple-II-Pi) + +cmake_minimum_required(VERSION 3.12) + +include(pico_sdk_import.cmake) +pico_sdk_init() + +project(${PROJECT_NAME} C CXX ASM) +add_executable(${PROJECT_NAME}) +pico_add_extra_outputs(${PROJECT_NAME}) + +pico_enable_stdio_uart(${PROJECT_NAME} 0) +pico_enable_stdio_usb(${PROJECT_NAME} 1) + +pico_generate_pio_header(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}/bus.pio) + +target_sources(${PROJECT_NAME} PRIVATE + main.c + board.c + incbin.S + ) + +target_link_libraries(${PROJECT_NAME} PRIVATE + pico_stdlib + pico_multicore + hardware_pio + ) + +if(${PICO_BOARD} STREQUAL "pico_w") +target_link_libraries(${PROJECT_NAME} PRIVATE + pico_cyw43_arch_none + ) +endif() diff --git a/pipico/LICENSE b/pipico/LICENSE new file mode 100644 index 0000000..8617998 --- /dev/null +++ b/pipico/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Oliver Schmidt (https://a2retro.de/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pipico/board.c b/pipico/board.c new file mode 100644 index 0000000..071575f --- /dev/null +++ b/pipico/board.c @@ -0,0 +1,121 @@ +/* + +MIT License + +Copyright (c) 2022 Oliver Schmidt (https://a2retro.de/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include "bus.pio.h" + +#include "board.h" + +extern const __attribute__((aligned(4))) uint8_t firmware[]; + +volatile bool active; + +static uint32_t command; +static uint32_t control; + +void __time_critical_func(board)(void) { + for (uint gpio = gpio_addr; gpio < gpio_addr + size_addr; gpio++) { + gpio_init(gpio); + gpio_set_pulls(gpio, false, false); // floating + } + + for (uint gpio = gpio_data; gpio < gpio_data + size_data; gpio++) { + pio_gpio_init(pio0, gpio); + gpio_set_pulls(gpio, false, false); // floating + } + + gpio_init(gpio_enbl); + gpio_set_pulls(gpio_enbl, false, false); // floating + + uint offset; + + offset = pio_add_program(pio0, &enbl_program); + enbl_program_init(offset); + + offset = pio_add_program(pio0, &write_program); + write_program_init(offset); + + offset = pio_add_program(pio0, &read_program); + read_program_init(offset); + + active = false; + + command = 0; + control = 0; + + while (true) { + uint32_t enbl = pio_sm_get_blocking(pio0, sm_enbl); + uint32_t addr = enbl & 0x0FFF; + uint32_t io = enbl & 0x0F00; // IOSTRB or IOSEL + uint32_t strb = enbl & 0x0800; // IOSTRB + uint32_t read = enbl & 0x1000; // R/W + + if (read) { + if (!io) { // DEVSEL + switch (addr & 0xF) { + case 0x8: + pio_sm_put(pio0, sm_read, sio_hw->fifo_rd); + break; + case 0x9: + // SIO_FIFO_ST_VLD_BITS _u(0x00000001) + // SIO_FIFO_ST_RDY_BITS _u(0x00000002) + pio_sm_put(pio0, sm_read, (sio_hw->fifo_st & 3) << 3); + break; + case 0xA: + pio_sm_put(pio0, sm_read, command); + break; + case 0xB: + pio_sm_put(pio0, sm_read, control); + break; + } + } else { + if (!strb || active) { + pio_sm_put(pio0, sm_read, firmware[addr]); + } + } + } else { + uint32_t data = pio_sm_get_blocking(pio0, sm_write); + if (!io) { // DEVSEL + switch (addr & 0xF) { + case 0x8: + sio_hw->fifo_wr = data; + break; + case 0xA: + command = data; + break; + case 0xB: + control = data; + break; + } + } + } + + if (io && !strb) { + active = true; + } else if (addr == 0x0FFF) { + active = false; + } + } +} diff --git a/pipico/board.h b/pipico/board.h new file mode 100644 index 0000000..ed3758f --- /dev/null +++ b/pipico/board.h @@ -0,0 +1,34 @@ +/* + +MIT License + +Copyright (c) 2022 Oliver Schmidt (https://a2retro.de/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef _BOARD_H +#define _BOARD_H + +extern volatile bool active; + +void board(void); + +#endif diff --git a/pipico/bus.pio b/pipico/bus.pio new file mode 100644 index 0000000..d708a8e --- /dev/null +++ b/pipico/bus.pio @@ -0,0 +1,205 @@ +/////////////////////////////////////////////////////////////////////////////// + +/* + +MIT License + +Copyright (c) 2022 Oliver Schmidt (https://a2retro.de/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +/////////////////////////////////////////////////////////////////////////////// + +.define public gpio_addr 2 // 12 pins +.define public gpio_rw 14 +.define public gpio_data 15 // 8 pins +.define public gpio_enbl 26 // DEVSEL | IOSEL | IOSTRB +.define public gpio_irq 27 +.define public gpio_res 28 + +.define public size_addr 13 // incl. R/W +.define public size_data 8 + +.define public sm_enbl 0 +.define public sm_read 1 // from Apple II perspective +.define public sm_write 2 // from Apple II perspective + +.define irq_write 4 + +/* + +Implementation of the Apple II peripheral bus protocol: + +- /DEVSEL, /IOSEL and /IOSTRB are supposed to combined to ENBL via an AND gate. + +- On the falling edge of ENBL, the lines A0-A11 and R/W are sampled and pushed + into the 'enable' state machine RX FIFO. + +- In case of an Apple II write cycle, the lines D0-D7 are sampled ~300ns later + and pushed into the 'write' state machine RX FIFO. + +- If a byte is pushed into the 'read' state machine TX FIFO, it is driven out + to the lines D0-D7 until the rising edge of ENBL. + +*/ + +/////////////////////////////////////////////////////////////////////////////// + +.program enbl + +.wrap_target + +idle: + wait 1 gpio gpio_enbl // wait for ENBL to rise + wait 0 gpio gpio_enbl // wait for ENBL to fall + + in pins, size_addr // shift A0-A11 + R/W into ISR + + jmp pin idle // jump to 'idle' if R/W is high + + irq irq_write // set 'write' IRQ + +.wrap + +% c-sdk { +static inline void enbl_program_init(uint offset) { + pio_sm_config c = enbl_program_get_default_config(offset); + + // in_base: gpio_addr + sm_config_set_in_pins(&c, gpio_addr); + + // shift_right: false + // autopush: true + // push_threshold: size_addr + sm_config_set_in_shift(&c, false, true, size_addr); + + // pin: gpio_rw + sm_config_set_jmp_pin(&c, gpio_rw); + + // state_machine: sm_enbl + // initial_pc: offset + pio_sm_init(pio0, sm_enbl, offset, &c); + + // state_machine: sm_enbl + pio_sm_set_enabled(pio0, sm_enbl, true); +} +%} + +/////////////////////////////////////////////////////////////////////////////// + +.program write + +.wrap_target + + wait 1 irq irq_write [31] // wait for 'write' IRQ to be set and clear it + // [31 cycles to allow 6502 to set up D0-D7] + + in pins, size_data // shift D0-D7 into ISR + +.wrap + +% c-sdk { +static inline void write_program_init(uint offset) { + pio_sm_config c = write_program_get_default_config(offset); + + // in_base: gpio_data + sm_config_set_in_pins(&c, gpio_data); + + // shift_right: false + // autopush: true + // push_threshold: size_data + sm_config_set_in_shift(&c, false, true, size_data); + + // state_machine: sm_write + // initial_pc: offset + pio_sm_init(pio0, sm_write, offset, &c); + + // state_machine: sm_write + pio_sm_set_enabled(pio0, sm_write, true); +} +%} + +/////////////////////////////////////////////////////////////////////////////// + +.program read + +/* + +Both set and set-side are limited to 5 pins each. So both set and side-set are +configured to set (the direction of) 4 pins. This approach allows to set the +direction of D0-D7 in one operation. + +Note: The naive approach would have been + mov osr, ~x // move 0xFFFFFFFF to OSR + out pindirs, size_data // enable output of D0-D7 + [...] + mov osr, x // move 0x00000000 to OSR + out pindirs, size_data // disable output of D0-D7 +but this would have required two operations and destroyed OSR. + +*/ + +.side_set (size_data / 2) opt pindirs + +.wrap_target + + pull block // pull data into OSR, block on empty FIFO + + out pins, size_data // shift OSR out to D0-D7 + + set pindirs, 15 side 15 // enable output of D0-D7 + + wait 1 gpio gpio_enbl // wait for ENBL to rise + + set pindirs, 0 side 0 // disable output of D0-D7 + +.wrap + +% c-sdk { +static inline void read_program_init(uint offset) { + pio_sm_config c = read_program_get_default_config(offset); + + // out_base: gpio_data + // out_count: size_data + sm_config_set_out_pins(&c, gpio_data, size_data); + + // shift_right: true + // autopull: false + // pull_threshold: size_data + sm_config_set_out_shift(&c, true, false, size_data); + + // set_base: gpio_data + // set_count: size_data / 2 + sm_config_set_set_pins(&c, gpio_data, size_data / 2); + + // sideset_base: gpio_data + size_data / 2 + sm_config_set_sideset_pins(&c, gpio_data + size_data / 2); + + // state_machine: sm_read + // initial_pc: offset + pio_sm_init(pio0, sm_read, offset, &c); + + // state_machine: sm_read + pio_sm_set_enabled(pio0, sm_read, true); +} +%} + +/////////////////////////////////////////////////////////////////////////////// diff --git a/pipico/incbin.S b/pipico/incbin.S new file mode 100644 index 0000000..50ef96a --- /dev/null +++ b/pipico/incbin.S @@ -0,0 +1,6 @@ +.section .time_critical.firmware +.global firmware +.type firmware, %object +.balign 4 +firmware: +.incbin "../../pidrive/PIROM#062000" diff --git a/pipico/main.c b/pipico/main.c new file mode 100644 index 0000000..e96692f --- /dev/null +++ b/pipico/main.c @@ -0,0 +1,110 @@ +/* + +MIT License + +Copyright (c) 2022 Oliver Schmidt (https://a2retro.de/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#include +#include "pico/printf.h" +#include "pico/stdlib.h" +#include "pico/multicore.h" +#ifdef RASPBERRYPI_PICO_W +#include "pico/cyw43_arch.h" +#endif + +#include "bus.pio.h" +#include "board.h" + +#ifdef TRACE +void uart_printf(uart_inst_t *uart, const char *format, ...) { + static char buffer[0x100]; + + va_list va; + va_start(va, format); + vsnprintf(buffer, sizeof(buffer), format, va); + va_end(va); + + buffer[0xFF] = '\0'; + uart_puts(uart, buffer); +} +#endif + +void res_callback(uint gpio, uint32_t events) { +} + +void main(void) { + multicore_launch_core1(board); + +#ifdef RASPBERRYPI_PICO_W + cyw43_arch_init(); +#elif defined(PICO_DEFAULT_LED_PIN) + gpio_init(PICO_DEFAULT_LED_PIN); + gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT); +#endif + + gpio_init(gpio_irq); + gpio_pull_up(gpio_irq); + + gpio_init(gpio_res); + gpio_set_irq_enabled_with_callback(gpio_res, GPIO_IRQ_EDGE_RISE, true, &res_callback); + + stdio_init_all(); + stdio_set_translate_crlf(&stdio_usb, false); + +#ifdef TRACE + uart_init(uart0, 115200); + uart_set_translate_crlf(uart0, true); + gpio_set_function(0, GPIO_FUNC_UART); + gpio_set_function(1, GPIO_FUNC_UART); +#endif + + while (true) { + if (multicore_fifo_rvalid()) { + uint32_t data = multicore_fifo_pop_blocking(); + putchar(data); +#ifdef TRACE + uart_printf(uart0, "> %02X\n", data); +#endif + } + + if (multicore_fifo_wready()) { + int data = getchar_timeout_us(0); + if (data != PICO_ERROR_TIMEOUT) { + multicore_fifo_push_blocking(data); +#ifdef TRACE + uart_printf(uart0, "< %02X\n", data); +#endif + } + } + +#ifdef RASPBERRYPI_PICO_W + static bool last_active; + if (active != last_active) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, active); + last_active = active; + } +#elif defined(PICO_DEFAULT_LED_PIN) + gpio_put(PICO_DEFAULT_LED_PIN, active); +#endif + } +} diff --git a/pipico/pico_sdk_import.cmake b/pipico/pico_sdk_import.cmake new file mode 100644 index 0000000..65f8a6f --- /dev/null +++ b/pipico/pico_sdk_import.cmake @@ -0,0 +1,73 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG master + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/share/a2pi.service b/share/a2pi.service index e9b02e8..1b3c6a4 100644 --- a/share/a2pi.service +++ b/share/a2pi.service @@ -6,7 +6,7 @@ After= Type=idle Environment="HOME=/root" EnvironmentFile=-/etc/default/a2pi -ExecStart=/sbin/a2pid $A2PID_OPTS +ExecStart=/usr/local/sbin/a2pid /dev/ttyACM* Restart=on-failure RestartSec=60 diff --git a/src/Makefile b/src/Makefile index 36ae301..79f96a5 100755 --- a/src/Makefile +++ b/src/Makefile @@ -1,12 +1,7 @@ DESTDIR=/usr/local SBINDIR=$(DESTDIR)/sbin -ifneq "$(findstring environment,$(origin DESTDIR))" "" BINDIR=$(DESTDIR)/bin SHAREDIR=$(DESTDIR)/share/a2pi -else -BINDIR=$(DESTDIR)/usr/bin -SHAREDIR=$(DESTDIR)/usr/share/a2pi -endif BIN=a2joy a2joymou a2joypad a2mon a2term fusea2pi a2pidcmd dskread dskwrite bload brun nibread dskformat eddread SBIN=a2pid # A2PIDEFS=-DSETSERCLK @@ -24,12 +19,14 @@ clean: install: -mkdir -p $(BINDIR) - cp $(BIN) a2mount $(BINDIR) - cp $(BIN) a2setvd $(BINDIR) + cp $(BIN) a2mount a2setvd $(BINDIR) -mkdir -p $(SBINDIR) cp $(SBIN) $(SBINDIR) -mkdir -p $(SHAREDIR) cp -R ../share/* $(SHAREDIR) + mv $(SHAREDIR)/A2PI-1.7.PO $(SHAREDIR)/A2VD1.PO + mv $(SHAREDIR)/UTILS.PO $(SHAREDIR)/A2VD2.PO + systemctl enable --system $(SHAREDIR)/a2pi.service uninstall: cd $(BINDIR); rm $(BIN) a2mount diff --git a/src/a2pid.c b/src/a2pid.c index a3c07b7..9d7efc5 100755 --- a/src/a2pid.c +++ b/src/a2pid.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ char deftty[] = "/dev/serial0"; /* Default for Raspberry Pi */ * Apple II request entry */ #define MAX_XFER 64 -#define AWAIT_COMPLETE 0x100 +#define AWAIT_COMPLETE 0x100 struct a2request { int idx; int type; @@ -48,10 +49,10 @@ struct a2request { /* * Client info */ -#define CLIENT_OPEN 1 -#define CLIENT_CLOSING 2 -#define CLIENT_COUT 4 -#define MAX_CLIENT 8 +#define CLIENT_OPEN 1 +#define CLIENT_CLOSING 2 +#define CLIENT_COUT 4 +#define MAX_CLIENT 8 struct { int fd; @@ -76,14 +77,14 @@ int vdrvfd[2]; /* * Daemon states. */ -#define RUN 0 -#define STOP 1 -#define RESET 2 +#define RUN 0 +#define STOP 1 +#define RESET 2 volatile int state = RUN, isdaemon = FALSE; void prlog(char *str) { if (!isdaemon) - puts(str); + puts(str); } static void sig_bye(int signo) { @@ -102,12 +103,16 @@ int vdrvopen(char *path) char filename[256]; strcpy(filename, path); strcat(filename, "A2VD1.PO"); - //printf("vdrv: open %s\n", filename); +#ifdef TRACE + printf("vdrv: open %s\n", filename); +#endif if ((vdrvfd[0] = open(filename, O_RDWR, 0)) < 0) vdrvfd[0] = 0; strcpy(filename, path); strcat(filename, "A2VD2.PO"); - //printf("vdrv: open %s\n", filename); +#ifdef TRACE + printf("vdrv: open %s\n", filename); +#endif if ((vdrvfd[1] = open(filename, O_RDWR, 0)) < 0) vdrvfd[1] = 0; return vdrvfd[0] + vdrvfd[1]; @@ -315,7 +320,7 @@ void sendrelxy(int fd, int x, int y) write(fd, &evsync, sizeof(evsync)); } /*****************************************************************\ - * * +* * * Request queue management * * * \*****************************************************************/ @@ -456,6 +461,27 @@ void flushreqs(int a2fd, int clidx, int status, int result) } /*****************************************************************\ * * +* Path matching * +* * +\*****************************************************************/ +int pathmatch(char **path, char *pattern) +{ + glob_t result; + int found; + + if (*path) + { + free(*path); + *path = NULL; + } + found = !glob(pattern, 0, NULL, &result); + if (found) + *path = strdup(*result.gl_pathv); + globfree(&result); + return found; +} +/*****************************************************************\ +* * * Main entrypoint * * * \*****************************************************************/ @@ -468,8 +494,9 @@ void main(int argc, char **argv) int a2fd, kbdfd, moufd, srvfd, maxfd; struct sockaddr_in servaddr; fd_set readset, openset; - char *devtty = deftty; - char *vdrvdir = "/usr/share/a2pi/"; /* default vdrv image directory */ + char *ttypattern = deftty; + char *devtty = NULL; + char *vdrvdir = "/usr/local/share/a2pi/"; /* default vdrv image directory */ /* * Parse arguments @@ -485,16 +512,16 @@ void main(int argc, char **argv) die("a2pid: daemon() failure"); isdaemon = TRUE; /* - * Another argument must be tty device + * Another argument must be tty pattern */ if (argc > 2) - devtty = argv[2]; + ttypattern = argv[2]; } else /* - * Must be tty device + * Must be tty pattern */ - devtty = argv[1]; + ttypattern = argv[1]; } /* * Add signal handlers. @@ -596,6 +623,21 @@ void main(int argc, char **argv) * Get vdrv images. */ vdrvactive = vdrvopen(vdrvdir); + /* + * Open socket. + */ + prlog("a2pid: Open server socket\n"); + bzero(&servaddr, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(6551); + servaddr.sin_addr.s_addr = htonl(INADDR_ANY); + srvfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); + if (srvfd < 0) + die("error: socket create"); + if (bind(srvfd,(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) + die("error: bind socket"); + if (listen(srvfd, MAX_CLIENT - 1) < 0) + die("error: listen socket"); #if defined(SETSERCLK) && defined(__ARMEL__) /* * Initialize ACIA clock for Apple II Pi card @@ -603,13 +645,29 @@ void main(int argc, char **argv) if (devtty == deftty) gpclk(271); /* divisor for ~1.8 MHz => (500/271) MHz */ #endif + /* + * Come back here on serial read error. + */ +openserial: /* * Open serial port. */ + while (!pathmatch(&devtty, ttypattern)) + { + usleep(1000); + if (state == STOP) + die("interrupted"); + } prlog("a2pid: Open serial port\n"); - a2fd = open(devtty, O_RDWR | O_NOCTTY); - if (a2fd < 0) - die("error: serial port open"); +#ifdef TRACE + printf("a2pid: open %s\n", devtty); +#endif + while ((a2fd = open(devtty, O_RDWR | O_NOCTTY)) < 0) + { + usleep(1000); + if (state == STOP) + die("interrupted"); + } tcflush(a2fd, TCIFLUSH); tcgetattr(a2fd, &oldtio); /* save current port settings */ bzero(&newtio, sizeof(newtio)); @@ -644,25 +702,28 @@ void main(int argc, char **argv) state = RESET; } } + /* drain superfluous sync requests from potential fifo */ + usleep(1000); + c = read(a2fd, iopkt, sizeof(iopkt)); + for (i = 0; i < c; i++) + { +#ifdef TRACE + printf("a2pid: drain 0x%02X\n", iopkt[i]); +#endif + /* there's already some other request */ + if (iopkt[i] != 0x80) + { +#ifdef TRACE + printf("a2pid: push back 0x%02X\n", iopkt[i]); +#endif + /* 'simulate terminal input' for a push back */ + ioctl(a2fd, TIOCSTI, &iopkt[i]); + } + } newtio.c_cc[VMIN] = 3; /* blocking read until 3 chars received */ tcsetattr(a2fd, TCSANOW, &newtio); /* - * Open socket. - */ - prlog("a2pid: Open server socket\n"); - bzero(&servaddr, sizeof(servaddr)); - servaddr.sin_family = AF_INET; - servaddr.sin_port = htons(6551); - servaddr.sin_addr.s_addr = htonl(INADDR_ANY); - srvfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); - if (srvfd < 0) - die("error: socket create"); - if (bind(srvfd,(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) - die("error: bind socket"); - if (listen(srvfd, MAX_CLIENT - 1) < 0) - die("error: listen socket"); - /* - * Come basck here on RESET. + * Come back here on RESET. */ reset: state = RUN; @@ -698,7 +759,9 @@ reset: rdycnt--; if (read(a2fd, iopkt, 3) == 3) { - // printf("a2pi: A2 Event [0x%02X] [0x%02X] [0x%02X]\n", iopkt[0], iopkt[1], iopkt[2]); +#ifdef TRACE + printf("a2pi: A2 Event [0x%02X] [0x%02X] [0x%02X]\n", iopkt[0], iopkt[1], iopkt[2]); +#endif switch (iopkt[0]) { case 0x80: /* sync */ @@ -709,23 +772,31 @@ reset: flushreqs(a2fd, 0, -1, -1); break; case 0x82: /* keyboard event */ - // printf("Keyboard Event: 0x%02X:%c\n", iopkt[1], iopkt[2] & 0x7F); +#ifdef TRACE + printf("Keyboard Event: 0x%02X:%c\n", iopkt[1], iopkt[2] & 0x7F); +#endif sendkey(kbdfd, iopkt[1], iopkt[2]); //if (iopkt[2] == 0x9B && iopkt[1] == 0xC0) //state = STOP; break; case 0x84: /* mouse move event */ - //printf("Mouse XY Event: %d,%d\n", (signed char)iopkt[1], (signed char)iopkt[2]); +#ifdef TRACE + printf("Mouse XY Event: %d,%d\n", (signed char)iopkt[1], (signed char)iopkt[2]); +#endif sendrelxy(moufd, (signed char)iopkt[1], (signed char)iopkt[2]); break; case 0x86: /* mouse button event */ - // printf("Mouse Button %s Event 0x%02X\n", iopkt[2] ? "[PRESS]" : "[RELEASE]", iopkt[1]); - sendbttn(moufd, iopkt[1], iopkt[2]); +#ifdef TRACE + printf("Mouse Button %s Event 0x%02X\n", iopkt[2] ? "[PRESS]" : "[RELEASE]", iopkt[1]); +#endif + sendbttn(moufd, iopkt[1], iopkt[2]); break; case 0x90: /* acknowledge read bytes request*/ if (a2reqlist) /* better have an outstanding request */ { - //printf("a2pid: read %d of %d bytes from 0x%04X\n", a2reqlist->xfer, a2reqlist->count, a2reqlist->addr); +#ifdef TRACE + printf("a2pid: read %d of %d bytes from 0x%04X\n", a2reqlist->xfer, a2reqlist->count, a2reqlist->addr); +#endif newtio.c_cc[VMIN] = 1; /* blocking read until 1 char received */ tcsetattr(a2fd, TCSANOW, &newtio); c = a2reqlist->count - a2reqlist->xfer > MAX_XFER @@ -755,7 +826,9 @@ reset: case 0x92: /* acknowledge write bytes */ if (a2reqlist) /* better have an outstanding request */ { - //printf("a2pid: wrote %d of %d bytes to 0x%04X\n", a2reqlist->xfer, a2reqlist->count, a2reqlist->addr); +#ifdef TRACE + printf("a2pid: wrote %d of %d bytes to 0x%04X\n", a2reqlist->xfer, a2reqlist->count, a2reqlist->addr); +#endif newtio.c_cc[VMIN] = 1; /* blocking read until 1 char received */ tcsetattr(a2fd, TCSANOW, &newtio); c = a2reqlist->count - a2reqlist->xfer > MAX_XFER @@ -785,7 +858,9 @@ reset: case 0x94: /* acknowledge call */ if (a2reqlist) /* better have an outstanding request */ { - //printf("a2pid: call address 0x%04X\n", a2reqlist->addr); +#ifdef TRACE + printf("a2pid: call address 0x%04X\n", a2reqlist->addr); +#endif newtio.c_cc[VMIN] = 1; /* blocking read until 1 char received */ tcsetattr(a2fd, TCSANOW, &newtio); if (!writeword(a2fd, a2reqlist->addr, iopkt[0] + 1)) @@ -800,7 +875,9 @@ reset: case 0x96: /* send input char to Apple II */ if (a2reqlist) /* better have an outstanding request */ { - //printf("a2pid: call address 0x%04X\n", a2reqlist->addr); +#ifdef TRACE + printf("a2pid: call address 0x%04X\n", a2reqlist->addr); +#endif newtio.c_cc[VMIN] = 1; /* blocking read until 1 char received */ tcsetattr(a2fd, TCSANOW, &newtio); if (!writeword(a2fd, a2reqlist->addr, 0x97)) @@ -822,7 +899,9 @@ reset: case 0x9F: /* request complete error */ if (a2reqlist) /* better have an outstanding request */ { - //printf("a2pid: complete request 0x%02X:0x%02X\n", (unsigned char)iopkt[0], (unsigned char)iopkt[1]); +#ifdef TRACE + printf("a2pid: complete request 0x%02X:0x%02X\n", (unsigned char)iopkt[0], (unsigned char)iopkt[1]); +#endif if ((a2reqlist->type == 0x90 || a2reqlist->type == 0x92) && (a2reqlist->count > a2reqlist->xfer)) { @@ -831,7 +910,9 @@ reset: } else { - //printf("a2pid: finish request 0x%02X:0x%02X\n", (unsigned char)iopkt[0], (unsigned char)iopkt[1]); +#ifdef TRACE + printf("a2pid: finish request 0x%02X:0x%02X\n", (unsigned char)iopkt[0], (unsigned char)iopkt[1]); +#endif finreq(a2fd, (unsigned char)iopkt[0], (unsigned char)iopkt[1]); } } @@ -840,7 +921,9 @@ reset: break; case 0xA0: /* virtual drive 1 STATUS call */ case 0xA2: /* virtual drive 2 STATUS call */ - //printf("vdrive: STATUS unit:%d\n", (iopkt[0] >> 1) & 0x01); +#ifdef TRACE + printf("vdrive: STATUS unit:%d\n", (iopkt[0] >> 1) & 0x01); +#endif iopkt[3] = iopkt[0] + 1; /* ack */ write(a2fd, &iopkt[3], 1); iopkt[0] = vdrvstat(a2fd, (iopkt[0] >> 1) & 0x01); @@ -849,12 +932,16 @@ reset: { iopkt[0] = a2reqlist->type; write(a2fd, iopkt, 1); - //printf("vdrive: status resend request %04X\n", a2reqlist->type); +#ifdef TRACE + printf("vdrive: status resend request %04X\n", a2reqlist->type); +#endif } break; case 0xA4: /* virtual drive 1 READ call */ case 0xA6: /* virtual drive 2 READ call */ - //printf("vdrive: READ unit:%d block:%d\n", (iopkt[0] >> 1) & 0x01, iopkt[1] | (iopkt[2] << 8)); +#ifdef TRACE + printf("vdrive: READ unit:%d block:%d\n", (iopkt[0] >> 1) & 0x01, iopkt[1] | (iopkt[2] << 8)); +#endif iopkt[3] = iopkt[0] + 1; /* ack */ write(a2fd, &iopkt[3], 1); iopkt[0] = vdrvread(a2fd, (iopkt[0] >> 1) & 0x01, iopkt[1] | (iopkt[2] << 8)); @@ -863,12 +950,16 @@ reset: { iopkt[0] = a2reqlist->type; write(a2fd, iopkt, 1); - //printf("vdrive: read resend request %04X\n", a2reqlist->type); +#ifdef TRACE + printf("vdrive: read resend request %04X\n", a2reqlist->type); +#endif } break; case 0xA8: /* virtual drive 1 WRITE call */ case 0xAA: /* virtual drive 2 WRITE call */ - //printf("vdrive: WRITE unit:%d block:%d\n", (iopkt[0] >> 1) & 0x01, iopkt[1] | (iopkt[2] << 8)); +#ifdef TRACE + printf("vdrive: WRITE unit:%d block:%d\n", (iopkt[0] >> 1) & 0x01, iopkt[1] | (iopkt[2] << 8)); +#endif iopkt[3] = iopkt[0] + 1; /* ack */ write(a2fd, &iopkt[3], 1); newtio.c_cc[VMIN] = 1; /* blocking read until command packet received */ @@ -881,11 +972,15 @@ reset: { iopkt[0] = a2reqlist->type; write(a2fd, iopkt, 1); - //printf("vdrive: write resend request %04X\n", a2reqlist->type); +#ifdef TRACE + printf("vdrive: write resend request %04X\n", a2reqlist->type); +#endif } break; case 0xAC: /* virtual clock TIME call */ - //printf("vclock: TIME\n"); +#ifdef TRACE + printf("vclock: TIME\n"); +#endif iopkt[0] = 0xAD; /* ack */ write(a2fd, iopkt, 1); write(a2fd, prodos_time(), 4); @@ -893,7 +988,9 @@ reset: { iopkt[0] = a2reqlist->type; write(a2fd, iopkt, 1); - //printf("vclock: resend request %04X\n", a2reqlist->type); +#ifdef TRACE + printf("vclock: resend request %04X\n", a2reqlist->type); +#endif } break; default: @@ -905,7 +1002,8 @@ reset: else { prlog("a2pid: error read serial port ????\n"); - state = STOP; + close(a2fd); + goto openserial; } } /* @@ -950,7 +1048,9 @@ reset: rdycnt--; if (read(a2client[i].fd, iopkt, 1) == 1) { - // printf("a2pi: Client Request [0x%02X]\n", iopkt[0]); +#ifdef TRACE + printf("a2pi: Client Request [0x%02X]\n", iopkt[0]); +#endif switch (iopkt[0]) { case 0x90: /* read bytes */ diff --git a/src/dskread.c b/src/dskread.c index 96729a0..fe132d1 100755 --- a/src/dskread.c +++ b/src/dskread.c @@ -1,3 +1,4 @@ +#include #include "a2lib.c" char online[] = {