Implement Hardware I/O Simulator (OUT/INP/WAIT/MOTOR, port emulation)

New portio.c module following the virmem.c dispatch-by-address pattern,
emulating 8253 PIT channel 2 (speaker frequency), PPI port B (speaker
on/off), CGA mode/color select registers, game port (joystick stub),
COM1 serial (transmitter-ready stub), and floating bus default (0xFF).

OUT/WAIT/MOTOR statements and INP()/STICK()/STRIG() functions now fully
functional. Continuous tone generation via PulseAudio pthread worker for
programs that drive the speaker through OUT &H43/&H42/&H61.
This commit is contained in:
Eremey Valetov
2026-03-28 18:27:19 -04:00
parent 0d9f29aa7a
commit e100290dd3
12 changed files with 407 additions and 3 deletions
+2 -1
View File
@@ -38,6 +38,7 @@ set(SOURCES
src/print_using.c
src/graphics.c
src/virmem.c
src/portio.c
src/sound.c
src/tui.c
platform/hal_posix.c
@@ -49,5 +50,5 @@ target_link_libraries(gwbasic m)
if(PULSEAUDIO_FOUND)
target_compile_definitions(gwbasic PRIVATE HAVE_PULSEAUDIO)
target_include_directories(gwbasic PRIVATE ${PULSEAUDIO_INCLUDE_DIRS})
target_link_libraries(gwbasic ${PULSEAUDIO_LIBRARIES})
target_link_libraries(gwbasic ${PULSEAUDIO_LIBRARIES} pthread)
endif()
+15
View File
@@ -0,0 +1,15 @@
#ifndef PORTIO_H
#define PORTIO_H
#include <stdint.h>
/* Read a byte from an I/O port */
uint8_t portio_inp(uint16_t port);
/* Write a byte to an I/O port */
void portio_out(uint16_t port, uint8_t value);
/* Reset all port state (called on NEW, CLEAR, RUN) */
void portio_reset(void);
#endif
+2
View File
@@ -8,5 +8,7 @@ void snd_beep(void);
void snd_tone(int freq_hz, int duration_ticks);
void snd_tone_sync(int freq_hz, int duration_ticks);
void snd_play(const char *mml);
void snd_start_continuous(int freq_hz);
void snd_stop_continuous(void);
#endif
+20 -2
View File
@@ -2,6 +2,7 @@
#include "tui.h"
#include "graphics.h"
#include "virmem.h"
#include "portio.h"
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
@@ -700,9 +701,26 @@ static gw_value_t eval_function(uint8_t prefix, uint8_t func_tok)
return v;
}
case FUNC_INP:
case FUNC_INP: {
gw_expect('(');
uint16_t port = gw_eval_uint16();
gw_expect_rparen();
v.type = VT_INT;
v.ival = portio_inp(port);
return v;
}
case FUNC_STICK: {
gw_expect('(');
int arg = gw_eval_int();
gw_expect_rparen();
if (arg < 0 || arg > 3) gw_error(ERR_FC);
v.type = VT_INT;
v.ival = 128; /* center position */
return v;
}
case FUNC_PEN:
case FUNC_STICK:
case FUNC_STRIG:
gw_expect('(');
gw_eval();
+47
View File
@@ -2,6 +2,7 @@
#include "tui.h"
#include "graphics.h"
#include "virmem.h"
#include "portio.h"
#include "sound.h"
#include <ctype.h>
#include <string.h>
@@ -1375,6 +1376,7 @@ void gw_exec_stmt(void)
if (tok == TOK_NEW) {
gw_chrget();
gfx_shutdown();
portio_reset();
gw_free_program();
gw_vars_clear();
gw_arrays_clear();
@@ -1399,6 +1401,7 @@ void gw_exec_stmt(void)
/* CLEAR */
if (tok == TOK_CLEAR) {
gw_chrget();
portio_reset();
gw_vars_clear();
gw_arrays_clear();
gw_file_close_all();
@@ -1452,6 +1455,7 @@ void gw_exec_stmt(void)
if (!start) return;
portio_reset();
gw_vars_clear();
gw_arrays_clear();
memset(gw.fn_defs, 0, sizeof(gw.fn_defs));
@@ -2494,6 +2498,49 @@ void gw_exec_stmt(void)
return;
}
/* OUT port, value */
if (tok == TOK_OUT) {
gw_chrget();
uint16_t port = gw_eval_uint16();
gw_skip_spaces();
gw_expect(',');
int val = gw_eval_int();
if (val < 0 || val > 255) gw_error(ERR_FC);
portio_out(port, (uint8_t)val);
return;
}
/* WAIT port, mask [, xor_mask] */
if (tok == TOK_WAIT) {
gw_chrget();
uint16_t port = gw_eval_uint16();
gw_skip_spaces();
gw_expect(',');
int mask = gw_eval_int();
if (mask < 0 || mask > 255) gw_error(ERR_FC);
int xor_mask = 0;
gw_skip_spaces();
if (gw_chrgot() == ',') {
gw_chrget();
xor_mask = gw_eval_int();
if (xor_mask < 0 || xor_mask > 255) gw_error(ERR_FC);
}
while (((portio_inp(port) ^ xor_mask) & mask) == 0) {
if (tui.active)
tui_check_break();
}
return;
}
/* MOTOR [n] — silently ignored */
if (tok == TOK_MOTOR) {
gw_chrget();
gw_skip_spaces();
if (gw_chrgot() && gw_chrgot() != ':' && gw_chrgot() != 0)
gw_eval(); /* consume optional argument */
return;
}
/* WIDTH */
if (tok == TOK_WIDTH) {
gw_chrget();
+2
View File
@@ -1,6 +1,7 @@
#include "gwbasic.h"
#include "tui.h"
#include "sound.h"
#include "portio.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -169,6 +170,7 @@ int main(int argc, char **argv)
gw_hal->init();
gw_init();
snd_init();
portio_reset();
int interactive = isatty(fileno(stdin));
+212
View File
@@ -0,0 +1,212 @@
#include "portio.h"
#include "sound.h"
#include "graphics.h"
#include <string.h>
/*
* Hardware I/O port emulation.
*
* Dispatch-by-port pattern (cf. virmem.c dispatch-by-address).
* Default: reads return 0xFF (floating bus), writes silently discarded.
*/
/* --- 8253 PIT Channel 2 (ports 0x42-0x43) --- */
#define PIT_CLOCK 1193180 /* 8253 input clock frequency */
static uint8_t pit_ctrl; /* last control word written to 0x43 */
static uint16_t pit_divisor; /* frequency divisor (lobyte | hibyte<<8) */
static int pit_latch; /* 0 = expecting lobyte, 1 = expecting hibyte */
static uint8_t pit_inp(uint16_t port)
{
if (port == 0x42)
return (uint8_t)(pit_divisor & 0xFF);
return 0xFF;
}
static void pit_out(uint16_t port, uint8_t value)
{
if (port == 0x43) {
pit_ctrl = value;
pit_latch = 0;
return;
}
/* port 0x42: channel 2 data */
if (pit_latch == 0) {
pit_divisor = (pit_divisor & 0xFF00) | value;
pit_latch = 1;
} else {
pit_divisor = (pit_divisor & 0x00FF) | ((uint16_t)value << 8);
pit_latch = 0;
}
}
/* --- PPI Port B (port 0x61) --- */
static uint8_t ppi_portb;
static void ppi_update_speaker(void)
{
if ((ppi_portb & 0x03) == 0x03 && pit_divisor > 0)
snd_start_continuous(PIT_CLOCK / pit_divisor);
else
snd_stop_continuous();
}
static uint8_t ppi_inp(void)
{
return ppi_portb;
}
static void ppi_out(uint8_t value)
{
uint8_t old = ppi_portb;
ppi_portb = value;
if ((old & 0x03) != (value & 0x03))
ppi_update_speaker();
}
/* --- CGA Mode Control Register (port 0x3D8) --- */
static uint8_t cga_mode;
static void cga_mode_out(uint8_t value)
{
uint8_t old_bits = cga_mode & 0x12; /* bits 1 and 4 */
cga_mode = value;
uint8_t new_bits = value & 0x12;
if (old_bits == new_bits)
return;
if (new_bits & 0x10)
gfx_init(2); /* 640x200 */
else if (new_bits & 0x02)
gfx_init(1); /* 320x200 */
else
gfx_shutdown(); /* text mode */
}
/* --- CGA Color Select Register (port 0x3D9) --- */
static uint8_t cga_color;
static void cga_color_out(uint8_t value)
{
cga_color = value;
/* Background color (bits 0-3) → palette attribute 0 */
gfx_palette_set(0, value & 0x0F);
/* Palette select (bit 5): 0 = green/red/yellow, 1 = cyan/magenta/white */
if (value & 0x20) {
gfx_palette_set(1, 3); /* cyan */
gfx_palette_set(2, 5); /* magenta */
gfx_palette_set(3, 7); /* white */
} else {
gfx_palette_set(1, 2); /* green */
gfx_palette_set(2, 4); /* red */
gfx_palette_set(3, 6); /* yellow/brown */
}
}
/* --- Game Port (port 0x201) --- */
#define GAME_PORT_NO_JOYSTICK 0xF0 /* bits 4-7 set = no buttons */
/* --- COM1 Serial (ports 0x3F8-0x3FE) --- */
static uint8_t com1_regs[7]; /* 0x3F8..0x3FE */
static uint8_t com1_inp(uint16_t port)
{
int reg = port - 0x3F8;
if (reg == 5) /* LSR: transmitter ready */
return 0x60;
return com1_regs[reg];
}
static void com1_out(uint16_t port, uint8_t value)
{
int reg = port - 0x3F8;
if (reg >= 0 && reg < 7)
com1_regs[reg] = value;
}
/* --- Public API --- */
uint8_t portio_inp(uint16_t port)
{
/* PIT channel 2 */
if (port == 0x42 || port == 0x43)
return pit_inp(port);
/* PPI port B */
if (port == 0x61)
return ppi_inp();
/* CGA registers */
if (port == 0x3D8)
return cga_mode;
if (port == 0x3D9)
return cga_color;
/* Game port */
if (port == 0x201)
return GAME_PORT_NO_JOYSTICK;
/* COM1 */
if (port >= 0x3F8 && port <= 0x3FE)
return com1_inp(port);
/* Floating bus */
return 0xFF;
}
void portio_out(uint16_t port, uint8_t value)
{
/* PIT channel 2 */
if (port == 0x42 || port == 0x43) {
pit_out(port, value);
return;
}
/* PPI port B */
if (port == 0x61) {
ppi_out(value);
return;
}
/* CGA registers */
if (port == 0x3D8) {
cga_mode_out(value);
return;
}
if (port == 0x3D9) {
cga_color_out(value);
return;
}
/* COM1 */
if (port >= 0x3F8 && port <= 0x3FE) {
com1_out(port, value);
return;
}
/* Default: silently discard */
}
void portio_reset(void)
{
snd_stop_continuous();
pit_ctrl = 0;
pit_divisor = 0;
pit_latch = 0;
ppi_portb = 0;
cga_mode = 0;
cga_color = 0;
memset(com1_regs, 0, sizeof(com1_regs));
}
+63
View File
@@ -3,10 +3,12 @@
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#ifdef HAVE_PULSEAUDIO
#include <pulse/simple.h>
#include <pulse/error.h>
#include <pthread.h>
#endif
#define SAMPLE_RATE 44100
@@ -55,6 +57,7 @@ void snd_init(void)
void snd_shutdown(void)
{
snd_stop_continuous();
#ifdef HAVE_PULSEAUDIO
if (pa_conn) {
pa_simple_drain(pa_conn, NULL);
@@ -309,3 +312,63 @@ void snd_play(const char *mml)
pa_drain();
#endif
}
/* --- Continuous tone for PPI speaker emulation --- */
#ifdef HAVE_PULSEAUDIO
static volatile bool cont_running;
static pthread_t cont_thread;
static int cont_freq;
static bool cont_active;
static void *cont_worker(void *arg)
{
(void)arg;
int16_t buf[4096];
double phase = 0.0;
double phase_inc = 2.0 * M_PI * cont_freq / SAMPLE_RATE;
while (cont_running) {
for (int i = 0; i < 4096; i++) {
buf[i] = (int16_t)(sin(phase) * 24000.0);
phase += phase_inc;
if (phase >= 2.0 * M_PI)
phase -= 2.0 * M_PI;
}
pa_simple_write(pa_conn, buf, sizeof(buf), NULL);
}
return NULL;
}
#endif
void snd_start_continuous(int freq_hz)
{
#ifdef HAVE_PULSEAUDIO
snd_stop_continuous();
if (freq_hz < 37)
return;
pa_open();
if (!pa_conn)
return;
cont_freq = freq_hz;
cont_running = true;
cont_active = true;
pthread_create(&cont_thread, NULL, cont_worker, NULL);
#else
(void)freq_hz;
#endif
}
void snd_stop_continuous(void)
{
#ifdef HAVE_PULSEAUDIO
if (!cont_active)
return;
cont_running = false;
pthread_join(cont_thread, NULL);
cont_active = false;
#endif
}
+9
View File
@@ -0,0 +1,9 @@
INP(0)= 255
INP(&H201)= 240
COM1 LSR= 96
PPI= 0
CGA MODE= 0
MOTOR OK
STICK(0)= 128
STRIG(0)= 0
DONE
+1
View File
@@ -0,0 +1 @@
SPEAKER OK
+25
View File
@@ -0,0 +1,25 @@
10 REM Test OUT/INP/WAIT/MOTOR port I/O
20 REM --- INP from unhandled port returns 0xFF (floating bus)
30 PRINT "INP(0)="; INP(0)
40 REM --- INP from game port returns 0xF0
50 PRINT "INP(&H201)="; INP(&H201)
60 REM --- OUT to unhandled port does not crash
70 OUT 0, 0
80 REM --- COM1 LSR reports transmitter ready
90 PRINT "COM1 LSR="; INP(&H3FD)
100 REM --- PPI port B read/write
110 OUT &H61, 0
120 PRINT "PPI="; INP(&H61)
130 REM --- CGA mode register
140 OUT &H3D8, 0
150 PRINT "CGA MODE="; INP(&H3D8)
160 REM --- MOTOR is silently accepted
170 MOTOR
180 MOTOR 0
190 MOTOR 1
200 PRINT "MOTOR OK"
210 REM --- STICK and STRIG return defaults
220 PRINT "STICK(0)="; STICK(0)
230 PRINT "STRIG(0)="; STRIG(0)
240 PRINT "DONE"
250 SYSTEM
+9
View File
@@ -0,0 +1,9 @@
10 REM Test speaker via OUT (PIT + PPI)
20 OUT &H43, &HB6
30 OUT &H42, &HD3
40 OUT &H42, &H04
50 OUT &H61, INP(&H61) OR 3
60 REM Speaker on briefly
70 OUT &H61, INP(&H61) AND &HFC
80 PRINT "SPEAKER OK"
90 SYSTEM