diff --git a/Altair8800/altair8800_sys.c b/Altair8800/altair8800_sys.c
index ad314c8a..20ce33a8 100644
--- a/Altair8800/altair8800_sys.c
+++ b/Altair8800/altair8800_sys.c
@@ -74,6 +74,8 @@ DEVICE *sim_devices[] = {
&m2sio0_dev,
&m2sio1_dev,
&acr_dev,
+ &daz_dev,
+ &pmmi_dev,
&sio_dev,
&sbc200_dev,
&tarbell_dev,
diff --git a/Altair8800/altair8800_sys.h b/Altair8800/altair8800_sys.h
index b5a1a225..0adf69dd 100644
--- a/Altair8800/altair8800_sys.h
+++ b/Altair8800/altair8800_sys.h
@@ -48,6 +48,8 @@ extern DEVICE mdsk_dev;
extern DEVICE m2sio0_dev;
extern DEVICE m2sio1_dev;
extern DEVICE acr_dev;
+extern DEVICE daz_dev;
+extern DEVICE pmmi_dev;
extern DEVICE sio_dev;
extern DEVICE sbc200_dev;
extern DEVICE tarbell_dev;
diff --git a/Altair8800/cromemco_dazzler.c b/Altair8800/cromemco_dazzler.c
new file mode 100644
index 00000000..9a395c44
--- /dev/null
+++ b/Altair8800/cromemco_dazzler.c
@@ -0,0 +1,615 @@
+/* cromemco_dazzler.c: Cromemco DAZZLER and JS-1 Joystick
+
+ Copyright (c) 2026 Patrick A. Linstruth
+
+ 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
+ PETER SCHORN 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.
+
+ Except as contained in this notice, the name of Patrick Linstruth shall not
+ be used in advertising or otherwise to promote the sale, use or other dealings
+ in this Software without prior written authorization from Patrick Linstruth.
+
+ History:
+ 18-Jan-2026 Initial version
+
+ ==================================================================
+
+ This device simulates the Cromemco Dazzler and D+7A with JS-1 Joystick
+ Console.
+
+*/
+
+#include "sim_defs.h"
+#include "sim_video.h"
+#include "altair8800_defs.h"
+#include "s100_bus.h"
+#include "s100_cpu.h"
+#include "cromemco_dazzler.h"
+
+/*
+** Public VID_DISPLAY for other devices that may want
+** to access the video display directly, such as joystick
+** events.
+*/
+VID_DISPLAY *daz_vptr = NULL;
+
+static t_bool daz_0e = 0x00;
+static t_bool daz_0f = 0x80;
+static uint32 daz_addr = 0x0000;
+static t_bool daz_frame = 0x3f;
+static uint8 daz_resolution = 32;
+static uint16 daz_pages = 1;
+static uint16 daz_window_width = 640;
+static uint16 daz_window_height = 640;
+static uint16 daz_screen_width = 32;
+static uint16 daz_screen_height = 32;
+static uint16 daz_screen_pixels = 32 * 32;
+static uint8 daz_color = 0;
+static uint32 daz_surface[DAZ_PIXELS];
+static uint32 daz_cpalette[16];
+static uint32 daz_gpalette[16];
+
+#define DAZ_SHOW_VIDEO(b) (b & DAZ_ON) ? "ON" : "OFF"
+#define DAZ_SHOW_RES(b) (b & DAZ_RESX4) ? "X4" : "NORMAL"
+#define DAZ_SHOW_MEMSIZE(b) (b & DAZ_2K) ? "2K" : "512"
+#define DAZ_SHOW_COLOR(b) (b & DAZ_COLOR) ? "COLOR" : "B/W"
+
+extern t_stat exdep_cmd(int32 flag, CONST char *cptr);
+
+static const char *daz_description(DEVICE *dptr);
+static t_stat daz_svc(UNIT *uptr);
+static t_stat daz_reset(DEVICE *dptr);
+static t_stat daz_boot(int32 unitno, DEVICE *dptr);
+static void daz_set_0f(uint8 val);
+static t_stat daz_set_video(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
+static t_stat daz_show_video(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
+static t_stat daz_set_resolution(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
+static t_stat daz_show_resolution(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
+static t_stat daz_set_memsize(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
+static t_stat daz_show_memsize(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
+static t_stat daz_set_color(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
+static t_stat daz_show_color(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
+static int32 daz_io(const int32 port, const int32 io, const int32 data);
+static t_stat daz_open_video(void);
+static t_stat daz_close_video(void);
+static void daz_resize_video(void);
+static void daz_refresh(void);
+static void daz_render_normal(void);
+static void daz_render_x4(void);
+static int32 daz_quad_surfacex(int q);
+static int32 daz_quad_surfacey(int q);
+static t_stat daz_show_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr);
+
+/* Debug flags */
+#define VERBOSE_MSG (1 << 0)
+
+/*
+ DAZZLER data structures
+
+ daz_dev DAZ device descriptor
+ daz_unit DAZ unit descriptor
+ daz_reg DAZ register list
+*/
+
+static RES daz_res = { DAZ_IO_BASE, DAZ_IO_SIZE, 0 ,0, NULL };
+
+static UNIT daz_unit = {
+ UDATA (&daz_svc, 0, 0), 33000 /* 30 fps */
+};
+
+static REG daz_reg[] = {
+ { NULL }
+};
+
+static DEBTAB daz_debug[] = {
+ { "VERBOSE", VERBOSE_MSG, "Verbose messages" },
+ { "JOYSTICK", SIM_VID_DBG_JOYSTICK, "Joystick messages" },
+ { "VIDEO", SIM_VID_DBG_VIDEO, "Video messages" },
+ { 0 }
+};
+
+static MTAB daz_mod[] = {
+ { MTAB_XTD|MTAB_VDV, 0, "VIDEO", "VIDEO",
+ &daz_set_video, &daz_show_video, NULL, "DAZZLER Video [ ON | OFF ]" },
+ { MTAB_XTD|MTAB_VDV, 0, "IOBASE", "IOBASE",
+ &set_iobase, &show_iobase, NULL, "DAZZLER Base I/O Address" },
+ { MTAB_XTD|MTAB_VDV, 0, "MEMSIZE", "MEMSIZE",
+ &daz_set_memsize, &daz_show_memsize, NULL, "DAZZLER Memory Size [ 512 | 2K ]" },
+ { MTAB_XTD|MTAB_VDV, 0, "RESOLUTION", "RESOLUTION",
+ &daz_set_resolution, &daz_show_resolution, NULL, "DAZZLER Resolution [ NORMAL | HIGH ]" },
+ { MTAB_XTD|MTAB_VDV, 0, "COLOR", "COLOR",
+ &daz_set_color, &daz_show_color, NULL, "DAZZLER Color [ BW | COLOR ]" },
+ { 0 }
+};
+
+DEVICE daz_dev = {
+ "DAZZLER", &daz_unit, daz_reg, daz_mod,
+ 1, ADDRRADIX, ADDRWIDTH, 1, DATARADIX, DATAWIDTH,
+ NULL, NULL, &daz_reset,
+ &daz_boot, NULL, NULL,
+ &daz_res, DEV_DEBUG | DEV_DIS | DEV_DISABLE, 0,
+ daz_debug, NULL, NULL, &daz_show_help, NULL, NULL,
+ &daz_description
+};
+
+/*
+ DAZZLER routines
+
+ daz_description
+ daz_svc
+ daz_reset
+*/
+static const char *daz_description (DEVICE *dptr)
+{
+ return "Cromemco Dazzler";
+}
+
+static t_stat daz_svc(UNIT *uptr)
+{
+ daz_refresh();
+
+ sim_activate_after_abs(uptr, uptr->wait);
+
+ return SCPE_OK;
+}
+
+static t_stat daz_reset(DEVICE *dptr)
+{
+ t_stat r = SCPE_OK;
+
+ if (dptr->flags & DEV_DIS) {
+ s100_bus_remio(daz_res.io_base, daz_res.io_size, &daz_io);
+
+ sim_cancel(&daz_unit);
+
+ if (daz_vptr != NULL) {
+ return daz_close_video();
+ }
+ } else {
+ s100_bus_addio(daz_res.io_base, daz_res.io_size, &daz_io, "DAZZLER");
+
+ if (daz_vptr == NULL) {
+ daz_open_video();
+ } else {
+ sim_activate_after_abs(&daz_unit, daz_unit.wait);
+ }
+ }
+
+ return r;
+}
+
+static t_stat daz_boot(int32 unitno, DEVICE *dptr)
+{
+ ChipType chiptype = cpu_get_chiptype();
+
+ if (chiptype == CHIP_TYPE_8080) {
+ exdep_cmd(EX_D, "-m 100 MVI A,01H");
+ exdep_cmd(EX_D, "-m 102 ORI 80H");
+ exdep_cmd(EX_D, "-m 104 OUT 0EH");
+ exdep_cmd(EX_D, "-m 106 MVI A,10H");
+ exdep_cmd(EX_D, "-m 108 OUT 0FH");
+ exdep_cmd(EX_D, "-m 10A LXI H,200H");
+ exdep_cmd(EX_D, "-m 10D MVI C,32");
+ exdep_cmd(EX_D, "-m 10F MVI B,16");
+ exdep_cmd(EX_D, "-m 111 XRA A");
+ exdep_cmd(EX_D, "-m 112 MOV M,A");
+ exdep_cmd(EX_D, "-m 113 ADI 11H");
+ exdep_cmd(EX_D, "-m 115 INX H");
+ exdep_cmd(EX_D, "-m 116 DCR B");
+ exdep_cmd(EX_D, "-m 117 JNZ 112H");
+ exdep_cmd(EX_D, "-m 11A DCR C");
+ exdep_cmd(EX_D, "-m 11B JNZ 10FH");
+ exdep_cmd(EX_D, "-m 11E JMP 11EH");
+ } else if (chiptype == CHIP_TYPE_Z80) {
+ exdep_cmd(EX_D, "-m 100 LD A,01H");
+ exdep_cmd(EX_D, "-m 102 OR 80H");
+ exdep_cmd(EX_D, "-m 104 OUT (0EH),A");
+ exdep_cmd(EX_D, "-m 106 LD A,10H");
+ exdep_cmd(EX_D, "-m 108 OUT (0FH),A");
+ exdep_cmd(EX_D, "-m 10A LD HL,200H");
+ exdep_cmd(EX_D, "-m 10D LD C,32");
+ exdep_cmd(EX_D, "-m 10F LD B,16");
+ exdep_cmd(EX_D, "-m 111 XOR A");
+ exdep_cmd(EX_D, "-m 112 LD (HL),A");
+ exdep_cmd(EX_D, "-m 113 ADD A,11H");
+ exdep_cmd(EX_D, "-m 115 INC HL");
+ exdep_cmd(EX_D, "-m 116 DEC B");
+ exdep_cmd(EX_D, "-m 117 JP NZ,112H");
+ exdep_cmd(EX_D, "-m 11A DEC C");
+ exdep_cmd(EX_D, "-m 11B JP NZ,10FH");
+ exdep_cmd(EX_D, "-m 11E JP 11EH");
+ }
+
+ *((int32 *) sim_PC->loc) = 0x0100;
+
+ return SCPE_OK;
+}
+
+static int32 daz_io(const int32 port, const int32 io, const int32 data)
+{
+ int32 p = port - daz_res.io_base;
+
+ if (io == 0) { /* IN */
+ switch (p) {
+ case 0x00: /* 0E */
+ daz_frame = 0x7f;
+ if ((sim_os_msec() % 30) > 25) {
+ daz_frame &= ~DAZ_EOF;
+ } else {
+ daz_frame |= (sim_os_msec() & 1) ? 0x00 : DAZ_EVEN;
+ }
+
+ return daz_frame;
+
+ case 0x01: /* 0F */
+ sim_debug(VERBOSE_MSG, &daz_dev, "Unspecified IN 0x%02X\n", port);
+ break;
+ }
+ } else { /* OUT */
+ switch (p) {
+ case 0x00: /* 0E */
+ daz_0e = data;
+ daz_addr = (data & 0x7f) << 9;
+
+ sim_debug(VERBOSE_MSG, &daz_dev, "New video address 0x%04X Video is %s\n", daz_addr, daz_0e & DAZ_ON ? "ON" : "OFF");
+ break;
+
+ case 0x01: /* 0F */
+ daz_set_0f(data);
+ break;
+ }
+ }
+
+ return 0xff;
+}
+
+static t_stat daz_open_video(void)
+{
+ t_stat r = SCPE_OK;
+ int i;
+
+ if (daz_vptr == NULL) {
+ sim_debug(VERBOSE_MSG, &daz_dev, "Opening new video window w:%d h:%d\n", daz_window_width, daz_window_height);
+
+ r = vid_open_window(&daz_vptr, &daz_dev, "Display", daz_window_width, daz_window_height, SIM_VID_IGNORE_VBAR | SIM_VID_RESIZABLE); /* video buffer size */
+
+ if (r != SCPE_OK) {
+ sim_printf("Could not open video window r=%X\n", r);
+ return r;
+ }
+
+ daz_resize_video();
+
+ daz_cpalette[0] = vid_map_rgb_window(daz_vptr, 0x00, 0x00, 0x00);
+ daz_cpalette[1] = vid_map_rgb_window(daz_vptr, 0x80, 0x00, 0x00);
+ daz_cpalette[2] = vid_map_rgb_window(daz_vptr, 0x00, 0x80, 0x00);
+ daz_cpalette[3] = vid_map_rgb_window(daz_vptr, 0x80, 0x80, 0x00);
+ daz_cpalette[4] = vid_map_rgb_window(daz_vptr, 0x00, 0x00, 0x80);
+ daz_cpalette[5] = vid_map_rgb_window(daz_vptr, 0x80, 0x00, 0x80);
+ daz_cpalette[6] = vid_map_rgb_window(daz_vptr, 0x00, 0x80, 0x80);
+ daz_cpalette[7] = vid_map_rgb_window(daz_vptr, 0x80, 0x80, 0x80);
+ daz_cpalette[8] = vid_map_rgb_window(daz_vptr, 0x00, 0x00, 0x00);
+ daz_cpalette[9] = vid_map_rgb_window(daz_vptr, 0xff, 0x00, 0x00);
+ daz_cpalette[10] = vid_map_rgb_window(daz_vptr, 0x00, 0xff, 0x00);
+ daz_cpalette[11] = vid_map_rgb_window(daz_vptr, 0xff, 0xff, 0x00);
+ daz_cpalette[12] = vid_map_rgb_window(daz_vptr, 0x00, 0x00, 0xff);
+ daz_cpalette[13] = vid_map_rgb_window(daz_vptr, 0xff, 0x00, 0xff);
+ daz_cpalette[14] = vid_map_rgb_window(daz_vptr, 0x00, 0xff, 0xff);
+ daz_cpalette[15] = vid_map_rgb_window(daz_vptr, 0xff, 0xff, 0xff);
+ daz_gpalette[0] = vid_map_rgb_window(daz_vptr, 0x00, 0x00, 0x00);
+ daz_gpalette[1] = vid_map_rgb_window(daz_vptr, 0x10, 0x10, 0x10);
+ daz_gpalette[2] = vid_map_rgb_window(daz_vptr, 0x20, 0x20, 0x20);
+ daz_gpalette[3] = vid_map_rgb_window(daz_vptr, 0x30, 0x30, 0x30);
+ daz_gpalette[4] = vid_map_rgb_window(daz_vptr, 0x40, 0x40, 0x40);
+ daz_gpalette[5] = vid_map_rgb_window(daz_vptr, 0x50, 0x50, 0x50);
+ daz_gpalette[6] = vid_map_rgb_window(daz_vptr, 0x60, 0x60, 0x60);
+ daz_gpalette[7] = vid_map_rgb_window(daz_vptr, 0x70, 0x70, 0x70);
+ daz_gpalette[8] = vid_map_rgb_window(daz_vptr, 0x80, 0x80, 0x80);
+ daz_gpalette[9] = vid_map_rgb_window(daz_vptr, 0x90, 0x90, 0x90);
+ daz_gpalette[10] = vid_map_rgb_window(daz_vptr, 0xa0, 0xa0, 0xa0);
+ daz_gpalette[11] = vid_map_rgb_window(daz_vptr, 0xb0, 0xb0, 0xb0);
+ daz_gpalette[12] = vid_map_rgb_window(daz_vptr, 0xc0, 0xc0, 0xc0);
+ daz_gpalette[13] = vid_map_rgb_window(daz_vptr, 0xd0, 0xd0, 0xd0);
+ daz_gpalette[14] = vid_map_rgb_window(daz_vptr, 0xe0, 0xe0, 0xe0);
+ daz_gpalette[15] = vid_map_rgb_window(daz_vptr, 0xff, 0xff, 0xff);
+
+ for (i = 0; i < daz_screen_pixels; i++) {
+ daz_surface[i] = 0;
+ }
+ }
+
+ sim_activate_after_abs(&daz_unit, daz_unit.wait);
+
+ return r;
+}
+
+static t_stat daz_close_video(void)
+{
+ t_stat r;
+
+ sim_debug(VERBOSE_MSG, &daz_dev, "Closing video window\n");
+
+ if ((r = vid_close_window(daz_vptr)) == SCPE_OK) {
+ sim_cancel(&daz_unit);
+
+ daz_vptr = NULL;
+ }
+
+ return r;
+}
+
+static void daz_resize_video(void)
+{
+ if (daz_vptr != NULL) {
+ vid_render_set_logical_size(daz_vptr, daz_screen_width, daz_screen_height);
+ if (!sim_is_running) {
+ daz_refresh();
+ }
+ }
+}
+
+/*
+ * Draw and refresh the screen in the video window
+ */
+static void daz_refresh(void) {
+ if (daz_vptr != NULL) {
+ if (daz_0f & DAZ_RESX4) {
+ daz_render_x4();
+ } else {
+ daz_render_normal();
+ }
+ vid_draw_window(daz_vptr, 0, 0, daz_screen_width, daz_screen_height, daz_surface);
+ vid_refresh_window(daz_vptr);
+ }
+}
+
+static void daz_render_normal(void)
+{
+ int q, x, y;
+ int32 maddr = daz_addr;
+ int32 saddr = 0;
+
+ for (q = 0; q < daz_pages; q++) {
+ for (y = daz_quad_surfacey(q); y < daz_quad_surfacey(q) + 32; y++) {
+ for (x = daz_quad_surfacex(q); x < daz_quad_surfacex(q) + 32; x+= 2) {
+ saddr = (y * daz_resolution) + x;
+ if (!(daz_0e & DAZ_ON)) {
+ daz_surface[saddr] = 0x00;
+ daz_surface[saddr+1] = 0x00;
+ } else if (daz_0f & DAZ_COLOR) {
+ daz_surface[saddr] = daz_cpalette[s100_bus_memr(maddr) & 0x0f];
+ daz_surface[saddr+1] = daz_cpalette[(s100_bus_memr(maddr) & 0xf0) >> 4];
+ } else {
+ daz_surface[saddr] = daz_gpalette[s100_bus_memr(maddr) & 0x0f];
+ daz_surface[saddr+1] = daz_gpalette[(s100_bus_memr(maddr) & 0xf0) >> 4];
+ }
+ maddr++;
+ }
+ }
+ }
+}
+
+static void daz_render_x4(void)
+{
+ int b, q, x, y;
+ int32 maddr = daz_addr;
+ int32 saddr = 0;
+ int32 soffset[] = {0, 1, daz_resolution, daz_resolution + 1, 2, 3, daz_resolution + 2, daz_resolution + 3};
+ uint32 color;
+
+ if (daz_0f & DAZ_COLOR) {
+ color = daz_cpalette[daz_color];
+ } else {
+ color = daz_gpalette[daz_color];
+ }
+
+ for (q = 0; q < daz_pages; q++) {
+ for (y = daz_quad_surfacey(q); y < daz_quad_surfacey(q) + 64; y+=2) {
+ for (x = daz_quad_surfacex(q); x < daz_quad_surfacex(q) + 64; x += 4) {
+ saddr = (y * daz_resolution) + x;
+ for (b = 0; b < 8; b++) {
+ if (daz_0e & DAZ_ON) {
+ daz_surface[saddr + soffset[b]] = (s100_bus_memr(maddr) & (1 << b)) ? color : 0;
+ } else {
+ daz_surface[saddr + soffset[b]] = 0x00;
+ }
+ }
+ maddr++;
+ }
+ }
+ }
+}
+
+static int32 daz_quad_surfacex(int q)
+{
+ if (q == 1 || q == 3) {
+ return daz_resolution / ((daz_0f & DAZ_RESX4) ? 2 : 2);
+ }
+
+ return 0;
+}
+
+static int32 daz_quad_surfacey(int q)
+{
+ if (q == 2 || q == 3) {
+ return daz_resolution / ((daz_0f & DAZ_RESX4) ? 2 : 2);
+ }
+
+ return 0;
+}
+
+static void daz_set_0f(uint8 val) {
+ uint8 old = daz_0f;
+
+ /* Update daz_0f register */
+ daz_0f = val;
+ daz_color = daz_0f & 0x0f;
+
+ /* Did resolution change? */
+ if ((daz_0f & (DAZ_RESX4 | DAZ_2K)) != (old & (DAZ_RESX4 | DAZ_2K))) {
+ daz_resolution = 32;
+ daz_pages = 1;
+ if (daz_0f & DAZ_RESX4) {
+ daz_resolution *= 2;
+ }
+ if (daz_0f & DAZ_2K) {
+ daz_pages = 4;
+ daz_resolution *= 2;
+ }
+
+ sim_debug(VERBOSE_MSG, &daz_dev, "Setting resolution to %02X %dx%d (%d pages) %s %s\n",
+ daz_0f, daz_resolution, daz_resolution, daz_pages, DAZ_SHOW_RES(daz_0f), DAZ_SHOW_MEMSIZE(daz_0f));
+
+ daz_screen_width = daz_resolution;
+ daz_screen_height = daz_resolution;
+ daz_screen_pixels = daz_screen_width * daz_screen_height;
+
+ daz_resize_video();
+ }
+
+ if (!sim_is_running) {
+ daz_refresh();
+ }
+}
+
+static t_stat daz_set_video(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
+{
+ if (!cptr) return SCPE_IERR;
+ if (!strlen(cptr)) return SCPE_ARG;
+
+ /* this assumes that the parameter has already been upcased */
+ if (!strncmp(cptr, "OFF", strlen(cptr))) {
+ daz_0e &= ~DAZ_ON;
+ } else if (!strncmp(cptr, "ON", strlen(cptr))) {
+ daz_0e |= DAZ_ON;
+ } else {
+ return SCPE_ARG;
+ }
+
+ if (!sim_is_running) {
+ daz_refresh();
+ }
+
+ return SCPE_OK;
+}
+
+static t_stat daz_show_video(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
+ if (!st) return SCPE_IERR;
+
+ fprintf(st, "VIDEO=%s", DAZ_SHOW_VIDEO(daz_0e));
+
+ return SCPE_OK;
+}
+
+static t_stat daz_set_resolution(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
+{
+ uint8 old = daz_0f;
+
+ if (!cptr) return SCPE_IERR;
+ if (!strlen(cptr)) return SCPE_ARG;
+
+ /* this assumes that the parameter has already been upcased */
+ if (!strncmp(cptr, "NORMAL", strlen(cptr))) {
+ old &= ~DAZ_RESX4;
+ } else if (!strncmp(cptr, "HIGH", strlen(cptr))) {
+ old |= DAZ_RESX4;
+ } else {
+ return SCPE_ARG;
+ }
+
+ daz_set_0f(old);
+
+ return SCPE_OK;
+}
+
+static t_stat daz_show_resolution(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
+ if (!st) return SCPE_IERR;
+
+ fprintf(st, "RES=%s", DAZ_SHOW_RES(daz_0f));
+
+ return SCPE_OK;
+}
+
+static t_stat daz_set_memsize(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
+{
+ uint8 old = daz_0f;
+
+ if (!cptr) return SCPE_IERR;
+ if (!strlen(cptr)) return SCPE_ARG;
+
+ /* this assumes that the parameter has already been upcased */
+ if (!strncmp(cptr, "512", strlen(cptr))) {
+ old &= ~DAZ_2K;
+ } else if (!strncmp(cptr, "2K", strlen(cptr))) {
+ old |= DAZ_2K;
+ } else {
+ return SCPE_ARG;
+ }
+
+ daz_set_0f(old);
+
+ return SCPE_OK;
+}
+
+static t_stat daz_show_memsize(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
+ if (!st) return SCPE_IERR;
+
+ fprintf(st, "MEMSIZE=%s @ %04X", DAZ_SHOW_MEMSIZE(daz_0f), daz_addr);
+
+ return SCPE_OK;
+}
+
+static t_stat daz_set_color(UNIT *uptr, int32 val, CONST char *cptr, void *desc)
+{
+ uint8 old = daz_0f;
+
+ if (!cptr) return SCPE_IERR;
+ if (!strlen(cptr)) return SCPE_ARG;
+
+ /* this assumes that the parameter has already been upcased */
+ if (!strncmp(cptr, "BW", strlen(cptr))) {
+ old &= ~DAZ_COLOR;
+ } else if (!strncmp(cptr, "COLOR", strlen(cptr))) {
+ old |= DAZ_COLOR;
+ } else {
+ return SCPE_ARG;
+ }
+
+ daz_set_0f(old);
+
+ return SCPE_OK;
+}
+
+static t_stat daz_show_color(FILE *st, UNIT *uptr, int32 val, CONST void *desc) {
+ if (!st) return SCPE_IERR;
+
+ fprintf(st, "%s", DAZ_SHOW_COLOR(daz_0f));
+
+ return SCPE_OK;
+}
+
+static t_stat daz_show_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
+{
+ fprintf (st, "\nCROMEMCO DAZZLER (%s)\n", dptr->name);
+
+ fprint_set_help (st, dptr);
+ fprint_show_help (st, dptr);
+ fprint_reg_help (st, dptr);
+
+ return SCPE_OK;
+}
+
diff --git a/Altair8800/cromemco_dazzler.h b/Altair8800/cromemco_dazzler.h
new file mode 100644
index 00000000..3de4ff54
--- /dev/null
+++ b/Altair8800/cromemco_dazzler.h
@@ -0,0 +1,56 @@
+/* cromemco_dazzler.h
+
+ Copyright (c) 2026 Patrick A. Linstruth
+
+ 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
+ PETER SCHORN 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.
+
+ Except as contained in this notice, the name of Patrick Linstruth shall not
+ be used in advertising or otherwise to promote the sale, use or other dealings
+ in this Software without prior written authorization from Patrick Linstruth.
+
+ History:
+ 01/18/26 Initial version
+
+*/
+
+#ifndef _CROMEMCO_DAZZLER_H
+#define _CROMEMCO_DAZZLER_H
+
+
+#define DAZ_PIXELS (128 * 128) /* total number of pixels */
+
+#define DAZ_IO_BASE 0x0e
+#define DAZ_IO_SIZE 2
+#define DAZ_MEM_SIZE 2048
+#define DAZ_MEM_MASK (2048 - 1)
+
+#define DAZ_ON 0x80 /* On/Off */
+#define DAZ_RESX4 0x40 /* Resolution X 4 */
+#define DAZ_2K 0x20 /* Picture in 2K bytes of memory */
+#define DAZ_COLOR 0x10 /* Picture in 2K bytes of memory */
+#define DAZ_HIGH 0x08 /* High intensity color */
+#define DAZ_BLUE 0x04 /* Blue */
+#define DAZ_GREEN 0x02 /* Green */
+#define DAZ_RED 0x01 /* Red */
+#define DAZ_EOF 0x40 /* End of Frame */
+#define DAZ_EVEN 0x80 /* Even Line */
+
+extern VID_DISPLAY *daz_vptr;
+
+#endif
+
diff --git a/Altair8800/pmmi_mm103.c b/Altair8800/pmmi_mm103.c
new file mode 100644
index 00000000..eb1fcc3f
--- /dev/null
+++ b/Altair8800/pmmi_mm103.c
@@ -0,0 +1,705 @@
+/* pmmi_mm103: PMMI MM-103 MODEM
+
+ Copyright (c) 2026 Patrick A. Linstruth
+
+ 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
+ PETER SCHORN 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.
+
+ Except as contained in this notice, the name of Patrick Linstruth shall not
+ be used in advertising or otherwise to promote the sale, use or other dealings
+ in this Software without prior written authorization from Patrick Linstruth.
+
+ History:
+ 18-Jan-2026 Initial version
+
+ ==================================================================
+
+ This device emulates a PMMI Communications MM-103 Modem & Communications
+ adapter.
+
+ The MM-103 uses 4 input and 4 output addresses. This driver defaults to
+ E0-E3 hex.
+
+ The MM-103 uses the Motorola MC6860L digital modem chip. This device does
+ not have the ability to emulate the modulation and demodulation functions
+ or the ability to connect to a phone line. All modem features, such as
+ switch hook, dial tone detection, and dialing, are emulated in such a way
+ that most software written for the MM-103 should function in some useful
+ fashion.
+
+ To provide any useful functionality, this device needs to be attached to
+ a socket or serial port. Enter "HELP PMMI" at the "simh>" prompt for
+ additional information.
+*/
+
+#include "sim_defs.h"
+#include "sim_tmxr.h"
+#include "altair8800_defs.h"
+#include "s100_bus.h"
+#include "pmmi_mm103.h"
+
+#define DEVICE_DESC "PMMI MM-103 MODEM"
+#define DEVICE_NAME "PMMI"
+
+static int32 poc = TRUE; /* Power On Clear */
+
+/* Debug flags */
+#define STATUS_MSG (1 << 0)
+#define ERROR_MSG (1 << 1)
+#define VERBOSE_MSG (1 << 2)
+
+typedef struct {
+ int32 conn; /* Connected Status */
+ int32 baud; /* Baud rate */
+ int32 dtr; /* DTR Status */
+ int32 txp; /* Transmit Pending */
+ int32 stb; /* Status Buffer */
+ int32 ireg0; /* In Register 0 */
+ int32 ireg1; /* In Register 1 */
+ int32 ireg2; /* In Register 2 */
+ int32 ireg3; /* In Register 3 */
+ int32 oreg0; /* Out Register 0 */
+ int32 oreg1; /* Out Register 1 */
+ int32 oreg2; /* Out Register 2 */
+ int32 oreg3; /* Out Register 3 */
+ int32 intmsk; /* Interrupt Mask */
+ uint32 ptimer; /* Next Pulse Timer */
+ uint32 dtimer; /* Next DT Timer */
+ uint32 flags; /* Original Flags */
+} PMMI_CTX;
+
+extern t_stat set_iobase(UNIT *uptr, int32 val, CONST char *cptr, void *desc);
+extern t_stat show_iobase(FILE *st, UNIT *uptr, int32 val, CONST void *desc);
+extern uint32 sim_map_resource(uint32 baseaddr, uint32 size, uint32 resource_type,
+ int32 (*routine)(const int32, const int32, const int32), const char* name, uint8 unmap);
+
+
+static const char* pmmi_description(DEVICE *dptr);
+static t_stat pmmi_svc(UNIT *uptr);
+static t_stat pmmi_reset(DEVICE *dptr);
+static t_stat pmmi_attach(UNIT *uptr, CONST char *cptr);
+static t_stat pmmi_detach(UNIT *uptr);
+static t_stat pmmi_set_baud(UNIT *uptr, int32 value, const char *cptr, void *desc);
+static t_stat pmmi_show_baud(FILE *st, UNIT *uptr, int32 value, const void *desc);
+static t_stat pmmi_config_line(UNIT *uptr);
+static int32 pmmi_io(int32 addr, int32 io, int32 data);
+static int32 pmmi_reg0(int32 io, int32 data);
+static int32 pmmi_reg1(int32 io, int32 data);
+static int32 pmmi_reg2(int32 io, int32 data);
+static int32 pmmi_reg3(int32 io, int32 data);
+static t_stat pmmi_set_console(UNIT *uptr, int32 value, const char *cptr, void *desc);
+static t_stat pmmi_show_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr);
+
+/* Debug Flags */
+static DEBTAB pmmi_dt[] = {
+ { "STATUS", STATUS_MSG, "Status messages" },
+ { "ERROR", ERROR_MSG, "Error messages" },
+ { "VERBOSE", VERBOSE_MSG, "Verbose messages" },
+ { NULL, 0 }
+};
+
+/* Terminal multiplexer library descriptors */
+
+static TMLN pmmi_tmln[] = { /* line descriptors */
+ { 0 }
+};
+
+
+static TMXR pmmi_tmxr = { /* multiplexer descriptor */
+ 1, /* number of terminal lines */
+ 0, /* listening port (reserved) */
+ 0, /* master socket (reserved) */
+ pmmi_tmln, /* line descriptor array */
+ NULL, /* line connection order */
+ NULL /* multiplexer device (derived internally) */
+};
+
+static RES pmmi_res = { PMMI_IOBASE, PMMI_IOSIZE, 0, 0, &pmmi_tmxr };
+
+static MTAB pmmi_mod[] = {
+ { MTAB_XTD|MTAB_VDV, 0, "IOBASE", "IOBASE",
+ &set_iobase, &show_iobase, NULL, "Sets MITS 2SIO base I/O address" },
+ { UNIT_PMMI_RTS, UNIT_PMMI_RTS, "RTS", "RTS", NULL, NULL, NULL,
+ "RTS follows DTR (default)" },
+ { UNIT_PMMI_RTS, 0, "NORTS", "NORTS", NULL, NULL, NULL,
+ "RTS does not follow DTR" },
+ { MTAB_XTD|MTAB_VDV|MTAB_VALR, 0, "BAUD", "BAUD", &pmmi_set_baud, &pmmi_show_baud,
+ NULL, "Set baud rate (default=300)" },
+
+ { MTAB_XTD | MTAB_VUN, UNIT_PMMI_CONSOLE, NULL, "CONSOLE", &pmmi_set_console, NULL, NULL, "Set as CONSOLE" },
+ { MTAB_XTD | MTAB_VUN, 0, NULL, "NOCONSOLE", &pmmi_set_console, NULL, NULL, "Remove as CONSOLE" },
+
+ { 0 }
+};
+
+static PMMI_CTX pmmi_ctx = { 0, PMMI_BAUD, 1 };
+
+static UNIT pmmi_unit[] = {
+ { UDATA (&pmmi_svc, UNIT_ATTABLE | UNIT_DISABLE | UNIT_PMMI_RTS, 0), PMMI_WAIT },
+};
+
+static REG pmmi_reg[] = {
+ { HRDATAD (IREG0, pmmi_ctx.ireg0, 8, "PMMI input register 0"), },
+ { HRDATAD (IREG1, pmmi_ctx.ireg1, 8, "PMMI input register 1"), },
+ { HRDATAD (IREG2, pmmi_ctx.ireg2, 8, "PMMI input register 2"), },
+ { HRDATAD (IREG3, pmmi_ctx.ireg3, 8, "PMMI input register 3"), },
+ { HRDATAD (OREG0, pmmi_ctx.oreg0, 8, "PMMI output register 0"), },
+ { HRDATAD (OREG1, pmmi_ctx.oreg1, 8, "PMMI output register 1"), },
+ { HRDATAD (OREG2, pmmi_ctx.oreg2, 8, "PMMI output register 2"), },
+ { HRDATAD (OREG3, pmmi_ctx.oreg3, 8, "PMMI output register 3"), },
+ { HRDATAD (TXP, pmmi_ctx.txp, 8, "PMMI tx data pending"), },
+ { FLDATAD (CON, pmmi_ctx.conn, 0, "PMMI connection status"), },
+ { DRDATAD (BAUD, pmmi_ctx.baud, 8, "PMMI calculated baud rate"), },
+ { HRDATAD (INTMSK, pmmi_ctx.intmsk, 8, "PMMI interrupt mask"), },
+ { FLDATAD (TBMT, pmmi_ctx.ireg0, 0, "PMMI TBMT status"), },
+ { FLDATAD (DAV, pmmi_ctx.ireg0, 1, "PMMI DAV status"), },
+ { FLDATAD (OR, pmmi_ctx.ireg0, 4, "PMMI OVRN status"), },
+ { FLDATAD (DT, pmmi_ctx.ireg2, 0, "PMMI dial tone status (active low)"), },
+ { FLDATAD (RNG, pmmi_ctx.ireg2, 1, "PMMI ringing status (active low)"), },
+ { FLDATAD (CTS, pmmi_ctx.ireg2, 2, "PMMI CTS status (active low)"), },
+ { FLDATAD (AP, pmmi_ctx.ireg2, 0, "PMMI answer phone status (active low)"), },
+ { FLDATAD (PULSE, pmmi_ctx.ireg2, 7, "PMMI timer pulse"), },
+ { DRDATAD (TIMER, pmmi_ctx.ptimer, 32, "PMMI timer pulse ms"), },
+ { DRDATAD (WAIT, pmmi_unit[0].wait, 32, "PMMI wait cycles"), },
+ { NULL }
+};
+
+DEVICE pmmi_dev = {
+ DEVICE_NAME, /* name */
+ pmmi_unit, /* unit */
+ pmmi_reg, /* registers */
+ pmmi_mod, /* modifiers */
+ 1, /* # units */
+ ADDRRADIX, /* address radix */
+ ADDRWIDTH, /* address width */
+ 1, /* address increment */
+ DATARADIX, /* data radix */
+ DATAWIDTH, /* data width */
+ NULL, /* examine routine */
+ NULL, /* deposit routine */
+ &pmmi_reset, /* reset routine */
+ NULL, /* boot routine */
+ &pmmi_attach, /* attach routine */
+ &pmmi_detach, /* detach routine */
+ &pmmi_res, /* context */
+ (DEV_DISABLE | DEV_DIS | DEV_DEBUG | DEV_MUX), /* flags */
+ 0, /* debug control */
+ pmmi_dt, /* debug flags */
+ NULL, /* mem size routine */
+ NULL, /* logical name */
+ &pmmi_show_help, /* help */
+ NULL, /* attach help */
+ NULL, /* context for help */
+ &pmmi_description /* description */
+};
+
+static const char* pmmi_description(DEVICE *dptr)
+{
+ return DEVICE_DESC;
+}
+
+static t_stat pmmi_reset(DEVICE *dptr)
+{
+ if (dptr->flags & DEV_DIS) { /* Disable Device */
+ s100_bus_remio(pmmi_res.io_base, pmmi_res.io_size, &pmmi_io);
+
+ poc = TRUE;
+ }
+ else {
+ if (poc) {
+ s100_bus_addio(pmmi_res.io_base, pmmi_res.io_size, &pmmi_io, DEVICE_NAME);
+
+ poc = FALSE;
+ }
+ }
+
+ /* Set DEVICE for this UNIT */
+ dptr->units[0].dptr = dptr;
+
+ /* Enable TMXR modem control passthrough */
+ tmxr_set_modem_control_passthru(pmmi_res.tmxr);
+
+ /* Reset status registers */
+ pmmi_ctx.ireg0 = 0;
+ pmmi_ctx.ireg1 = 0;
+ pmmi_ctx.ireg2 = PMMI_RNG | PMMI_CTS | PMMI_DT | PMMI_AP;
+ pmmi_ctx.ireg3 = 0;
+ pmmi_ctx.oreg0 = 0;
+ pmmi_ctx.oreg1 = 0;
+ pmmi_ctx.oreg2 = 0;
+ pmmi_ctx.oreg3 = 0;
+ pmmi_ctx.txp = 0;
+ pmmi_ctx.intmsk = 0;
+ pmmi_ctx.ptimer = sim_os_msec() + 40;
+ pmmi_ctx.dtimer = 0;
+
+ if (!(dptr->flags & DEV_DIS)) {
+ sim_activate(&dptr->units[0], dptr->units[0].wait);
+ } else {
+ sim_cancel(&dptr->units[0]);
+ }
+
+ sim_debug(STATUS_MSG, dptr, "reset adapter.\n");
+
+ return SCPE_OK;
+}
+
+static t_stat pmmi_svc(UNIT *uptr)
+{
+ int32 c,s,ireg2;
+ t_stat r = SCPE_OK;
+ uint32 ms;
+
+ /* Check for new incoming connection */
+ if (uptr->flags & UNIT_ATT) {
+ if (tmxr_poll_conn(pmmi_res.tmxr) >= 0) { /* poll connection */
+
+ /* Clear DTR and RTS if serial port */
+ if (pmmi_res.tmxr->ldsc->serport) {
+ s = TMXR_MDM_DTR | ((pmmi_dev.units[0].flags & UNIT_PMMI_RTS) ? TMXR_MDM_RTS : 0);
+ tmxr_set_get_modem_bits(pmmi_res.tmxr->ldsc, 0, s, NULL);
+ }
+
+ pmmi_res.tmxr->ldsc->rcve = 1; /* Enable receiver */
+ pmmi_ctx.conn = 1; /* set connected */
+
+ sim_debug(STATUS_MSG, uptr->dptr, "new connection.\n");
+ }
+ }
+
+ /* Update incoming modem status bits */
+ if (uptr->flags & UNIT_ATT) {
+ tmxr_set_get_modem_bits(pmmi_res.tmxr->ldsc, 0, 0, &s);
+
+ ireg2 = pmmi_ctx.ireg2;
+ pmmi_ctx.ireg2 &= ~PMMI_CTS;
+ pmmi_ctx.ireg2 |= (s & TMXR_MDM_CTS) ? 0 : PMMI_CTS; /* Active Low */
+
+ /* CTS status changed */
+ if ((ireg2 ^ pmmi_ctx.ireg2) & PMMI_CTS) {
+ if (pmmi_ctx.ireg2 & PMMI_CTS) { /* If no CTS, set AP bit */
+ pmmi_ctx.ireg2 |= PMMI_AP; /* Answer Phone Bit (active low) */
+ }
+ sim_debug(STATUS_MSG, uptr->dptr, "CTS state changed to %s.\n", (pmmi_ctx.ireg2 & PMMI_CTS) ? "LOW" : "HIGH");
+ }
+
+ pmmi_ctx.ireg2 &= ~PMMI_RNG;
+ pmmi_ctx.ireg2 |= (s & TMXR_MDM_RNG) ? 0 : PMMI_RNG; /* Active Low */
+
+ /* RNG status changed */
+ if ((ireg2 ^ pmmi_ctx.ireg2) & PMMI_RNG) {
+ /* Answer Phone Bit on RI */
+ if (!(pmmi_ctx.ireg2 & PMMI_RNG)) {
+ pmmi_ctx.ireg2 &= ~PMMI_AP; /* Answer Phone Bit (active low) */
+ }
+
+ sim_debug(STATUS_MSG, uptr->dptr, "RNG state changed to %s.\n", (pmmi_ctx.ireg2 & PMMI_RNG) ? "LOW" : "HIGH");
+ }
+
+ /* Enable receiver if CTS is active low */
+ pmmi_res.tmxr->ldsc->rcve = !(pmmi_ctx.ireg2 & PMMI_CTS);
+
+ /* If socket, connection status follows CTS */
+ if (!pmmi_res.tmxr->ldsc->serport) {
+ pmmi_ctx.conn = !(pmmi_ctx.ireg2 & PMMI_CTS);
+ }
+ }
+
+ /* TX data */
+ if (pmmi_ctx.txp) {
+ if (uptr->flags & UNIT_ATT) {
+ /*
+ ** If CTS active low, send byte
+ ** otherwise, toss character
+ */
+ if (!(pmmi_ctx.ireg2 & PMMI_CTS)) {
+ r = tmxr_putc_ln(pmmi_res.tmxr->ldsc, pmmi_ctx.oreg1);
+ }
+ pmmi_ctx.txp = 0; /* Reset TX Pending */
+ } else {
+ r = sim_putchar(pmmi_ctx.oreg1);
+ pmmi_ctx.txp = 0; /* Reset TX Pending */
+ }
+
+ if (r == SCPE_LOST) {
+ pmmi_ctx.conn = 0; /* Connection was lost */
+ sim_debug(STATUS_MSG, uptr->dptr, "lost connection.\n");
+ }
+ }
+
+ /* Update TBMT if not set and no character pending */
+ if (!pmmi_ctx.txp && !(pmmi_ctx.ireg0 & PMMI_TBMT)) {
+ if (uptr->flags & UNIT_ATT) {
+ tmxr_poll_tx(pmmi_res.tmxr);
+ pmmi_ctx.ireg0 |= (tmxr_txdone_ln(pmmi_res.tmxr->ldsc) && pmmi_ctx.conn) ? (PMMI_TBMT | PMMI_TEOC) : 0;
+ } else {
+ pmmi_ctx.ireg0 |= (PMMI_TBMT | PMMI_TEOC);
+ }
+ }
+
+ /* Check for Data if RX buffer empty */
+ if (!(pmmi_ctx.ireg0 & PMMI_DAV)) {
+ if (uptr->flags & UNIT_ATT) {
+ tmxr_poll_rx(pmmi_res.tmxr);
+
+ c = tmxr_getc_ln(pmmi_res.tmxr->ldsc);
+ } else {
+ c = s100_bus_poll_kbd(uptr);
+ }
+
+ if (c & (TMXR_VALID | SCPE_KFLAG)) {
+ pmmi_ctx.ireg1 = c & 0xff;
+ pmmi_ctx.ireg0 |= PMMI_DAV;
+ pmmi_ctx.ireg0 &= ~(PMMI_FE | PMMI_OR | PMMI_RPE);
+ }
+ }
+
+ /* Timer Pulses */
+ ms = sim_os_msec();
+
+ if (ms > pmmi_ctx.ptimer) {
+ if (pmmi_ctx.oreg2) {
+ if (pmmi_ctx.ireg2 & PMMI_TMR) {
+ pmmi_ctx.ireg2 &= ~PMMI_TMR;
+ pmmi_ctx.ptimer = sim_os_msec() + 600 / (PMMI_CLOCK / pmmi_ctx.oreg2); /* 60% off */
+ } else {
+ pmmi_ctx.ireg2 |= PMMI_TMR;
+ pmmi_ctx.ptimer = sim_os_msec() + 400 / (PMMI_CLOCK / pmmi_ctx.oreg2); /* 40% on */
+ }
+ } else {
+ pmmi_ctx.ptimer = sim_os_msec() + 100; /* default to 100ms if timer rate is 0 */
+ }
+ }
+
+ /* Emulate dial tone */
+ if ((ms > pmmi_ctx.dtimer) && (pmmi_ctx.oreg0 & PMMI_SH) && (pmmi_ctx.ireg2 & PMMI_DT)) {
+ pmmi_ctx.ireg2 &= ~PMMI_DT;
+ sim_debug(STATUS_MSG, uptr->dptr, "dial tone active.\n");
+ }
+
+ /* Don't let TMXR clobber our wait time */
+ uptr->wait = PMMI_WAIT;
+
+ sim_activate_abs(uptr, uptr->wait);
+
+ return SCPE_OK;
+}
+
+
+/* Attach routine */
+static t_stat pmmi_attach(UNIT *uptr, CONST char *cptr)
+{
+ t_stat r;
+
+ sim_debug(VERBOSE_MSG, uptr->dptr, "attach (%s).\n", cptr);
+
+ if ((r = tmxr_attach(pmmi_res.tmxr, uptr, cptr)) == SCPE_OK) {
+
+ pmmi_ctx.flags = uptr->flags; /* Save Flags */
+
+ if (!pmmi_res.tmxr->ldsc->serport) {
+ uptr->flags |= UNIT_PMMI_RTS; /* Force following DTR on sockets */
+ }
+
+ pmmi_res.tmxr->ldsc->rcve = 1;
+
+ sim_activate(uptr, uptr->wait);
+
+ sim_debug(VERBOSE_MSG, uptr->dptr, "activated service.\n");
+ }
+
+ return r;
+}
+
+/* Detach routine */
+static t_stat pmmi_detach(UNIT *uptr)
+{
+ sim_debug(VERBOSE_MSG, uptr->dptr, "detach.\n");
+
+ if (uptr->flags & UNIT_ATT) {
+ uptr->flags = pmmi_ctx.flags; /* Restore Flags */
+
+ sim_cancel(uptr);
+
+ return (tmxr_detach(pmmi_res.tmxr, uptr));
+ }
+
+ return SCPE_UNATT;
+}
+
+static t_stat pmmi_set_baud(UNIT *uptr, int32 value, const char *cptr, void *desc)
+{
+ int32 baud;
+ t_stat r = SCPE_ARG;
+
+ if (!(uptr->flags & UNIT_ATT)) {
+ return SCPE_UNATT;
+ }
+
+ if (cptr != NULL) {
+ if (sscanf(cptr, "%d", &baud)) {
+ if (baud >= 61 && baud <=600) {
+ pmmi_ctx.baud = baud;
+ r = pmmi_config_line(uptr);
+ }
+ }
+ }
+
+ return r;
+}
+
+static t_stat pmmi_show_baud(FILE *st, UNIT *uptr, int32 value, const void *desc)
+{
+ if (uptr->flags & UNIT_ATT) {
+ fprintf(st, "Baud rate: %d", pmmi_ctx.baud);
+ }
+
+ return SCPE_OK;
+}
+
+static t_stat pmmi_config_line(UNIT *uptr)
+{
+ char config[20];
+ char b,p,s;
+ t_stat r = SCPE_IERR;
+
+ switch (pmmi_ctx.oreg0 & PMMI_BMSK) {
+ case PMMI_5BIT:
+ b = '5';
+ break;
+
+ case PMMI_6BIT:
+ b = '6';
+ break;
+
+ case PMMI_7BIT:
+ b = '7';
+ break;
+
+ case PMMI_8BIT:
+ default:
+ b = '8';
+ break;
+ }
+
+ switch (pmmi_ctx.oreg0 & PMMI_PMSK) {
+ case PMMI_OPAR:
+ p = 'O';
+ break;
+
+ case PMMI_EPAR:
+ p = 'E';
+ break;
+
+ case PMMI_NPAR:
+ default:
+ p = 'N';
+ break;
+ }
+
+ switch (pmmi_ctx.oreg0 & PMMI_SMSK) {
+ case PMMI_2SB:
+ s = '2';
+ break;
+
+ case PMMI_1SB:
+ default:
+ s = '1';
+ break;
+ }
+
+ sprintf(config, "%d-%c%c%c", pmmi_ctx.baud, b,p,s);
+
+ sim_debug(STATUS_MSG, uptr->dptr, "setting port configuration to '%s'.\n", config);
+
+ r = tmxr_set_config_line(pmmi_res.tmxr->ldsc, config);
+
+ return r;
+}
+
+static int32 pmmi_io(int32 addr, int32 io, int32 data)
+{
+ int32 r = 0;
+
+ addr &= 0xff;
+ data &= 0xff;
+
+ if (io == S100_IO_WRITE) {
+ sim_debug(VERBOSE_MSG, &pmmi_dev, "OUT %02X,%02X\n", addr , data);
+ } else {
+ sim_debug(VERBOSE_MSG, &pmmi_dev, "IN %02X\n", addr);
+ }
+
+ switch (addr & 0x03) {
+ case PMMI_REG0:
+ r = pmmi_reg0(io, data);
+ break;
+
+ case PMMI_REG1:
+ r = pmmi_reg1(io, data);
+ break;
+
+ case PMMI_REG2:
+ r = pmmi_reg2(io, data);
+ break;
+
+ case PMMI_REG3:
+ r = pmmi_reg3(io, data);
+ break;
+ }
+
+ return(r);
+}
+
+static int32 pmmi_reg0(int32 io, int32 data)
+{
+ int32 r;
+
+ if (io == S100_IO_READ) {
+ r = pmmi_ctx.ireg0;
+ } else { pmmi_ctx.oreg0 = data; /* Set UART configuration */
+ pmmi_config_line(&pmmi_dev.units[0]);
+
+ if (data & PMMI_SH) { /* If off-hook, clear dial tone bit (active low) */
+ pmmi_ctx.dtimer = sim_os_msec() + 500; /* Dial tone in 500ms */
+ if (pmmi_ctx.oreg0 & PMMI_SH) {
+ pmmi_ctx.ireg2 &= ~PMMI_AP; /* Answer Phone Bit (active low) */
+ }
+ } else if (!(pmmi_ctx.ireg2 & PMMI_DT)) {
+ pmmi_ctx.dtimer = 0;
+ pmmi_ctx.ireg2 |= PMMI_DT;
+ sim_debug(STATUS_MSG, &pmmi_dev, "dial tone inactive.\n");
+ }
+
+ if (data & PMMI_RI) { /* Go off-hook in answer mode */
+ pmmi_ctx.ireg2 &= ~PMMI_AP; /* Answer Phone Bit (active low) */
+ }
+
+ r = 0x00;
+ }
+
+ return(r);
+}
+
+static int32 pmmi_reg1(int32 io, int32 data)
+{
+ int32 r;
+
+ if (io == S100_IO_READ) {
+ r = pmmi_ctx.ireg1;
+ pmmi_ctx.ireg0 &= ~(PMMI_DAV | PMMI_FE | PMMI_OR | PMMI_RPE);
+ } else {
+ pmmi_ctx.oreg1 = data;
+ pmmi_ctx.ireg0 &= ~(PMMI_TBMT | PMMI_TEOC);
+ pmmi_ctx.txp = 1;
+
+ r = 0x00;
+ }
+
+ return(r);
+}
+
+static int32 pmmi_reg2(int32 io, int32 data)
+{
+ int32 r;
+
+ if (io == S100_IO_READ) {
+ r = pmmi_ctx.ireg2;
+ } else {
+ pmmi_ctx.oreg2 = data;
+
+ /*
+ ** The actual baud rate is determined by the following:
+ ** Rate = 250,000/(Reg X 16) where Reg = the binary
+ ** value loaded into the rate generator.
+ */
+ if (data) {
+ pmmi_ctx.baud = 250000/(data * 16);
+
+ pmmi_config_line(&pmmi_dev.units[0]);
+ }
+
+ r = 0x00;
+ }
+
+ return(r);
+}
+
+static int32 pmmi_reg3(int32 io, int32 data)
+{
+ int32 s;
+
+ if (io == S100_IO_READ) {
+ pmmi_ctx.intmsk = pmmi_ctx.oreg2; /* Load int mask from rate generator */
+ } else {
+ pmmi_ctx.oreg3 = data;
+ /* Set/Clear DTR */
+ s = TMXR_MDM_DTR | ((pmmi_dev.units[0].flags & UNIT_PMMI_RTS) ? TMXR_MDM_RTS : 0);
+ if (data & PMMI_DTR) {
+ sim_debug(STATUS_MSG, &pmmi_dev, "setting DTR HIGH.\n");
+ tmxr_set_get_modem_bits(pmmi_res.tmxr->ldsc, s, 0, NULL);
+ if (pmmi_ctx.oreg0 & PMMI_SH) {
+ pmmi_ctx.ireg2 &= ~PMMI_AP; /* Answer Phone Bit (active low) */
+ }
+ } else {
+ sim_debug(STATUS_MSG, &pmmi_dev, "setting DTR LOW.\n");
+ tmxr_set_get_modem_bits(pmmi_res.tmxr->ldsc, 0, s, NULL);
+ pmmi_ctx.ireg2 |= PMMI_AP;
+ }
+ }
+ return 0x00;
+}
+
+static t_stat pmmi_set_console(UNIT *uptr, int32 value, const char *cptr, void *desc)
+{
+ if (value == UNIT_PMMI_CONSOLE) {
+ s100_bus_console(uptr);
+ }
+ else {
+ s100_bus_noconsole(uptr);
+ }
+
+ return SCPE_OK;
+}
+
+static t_stat pmmi_show_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, const char *cptr)
+{
+ fprintf (st, "\nPMMI MM-103 (%s)\n", dptr->name);
+
+ fprint_set_help (st, dptr);
+ fprint_show_help (st, dptr);
+ fprint_reg_help (st, dptr);
+ fprintf(st, "\n\n");
+ tmxr_attach_help(st, dptr, uptr, flag, cptr);
+
+ fprintf(st, "----- NOTES -----\n\n");
+ fprintf(st, "Only one device may poll the host keyboard for CONSOLE input.\n");
+ fprintf(st, "Use SET %s CONSOLE to select this UNIT as the CONSOLE device.\n", sim_dname(dptr));
+ fprintf(st, "\nUse SHOW BUS CONSOLE to display the current CONSOLE device.\n\n");
+
+ fprintf(st, "This device may be attached to a serial port on the host computer\n");
+ fprintf(st, "with the ATTACH command:\n\n");
+ fprintf(st, " sim> ATTACH %s CONNECT=/dev/tty.usbserial-AB0NW409\n\n", sim_dname(dptr));
+
+ fprintf(st, "This device may also be attached to a TCP/IP port on the host computer\n");
+ fprintf(st, "with the ATTACH command. The following will listen for a connection\n");
+ fprintf(st, "on port 8800:\n\n");
+ fprintf(st, " sim> ATTACH %s 8800\n", sim_dname(dptr));
+
+ return SCPE_OK;
+}
+
diff --git a/Altair8800/pmmi_mm103.h b/Altair8800/pmmi_mm103.h
new file mode 100644
index 00000000..fc141202
--- /dev/null
+++ b/Altair8800/pmmi_mm103.h
@@ -0,0 +1,93 @@
+/* pmmi_mm103.h
+
+ Copyright (c) 2026 Patrick A. Linstruth
+
+ 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
+ PETER SCHORN 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.
+
+ Except as contained in this notice, the name of Patrick Linstruth shall not
+ be used in advertising or otherwise to promote the sale, use or other dealings
+ in this Software without prior written authorization from Patrick Linstruth.
+
+ History:
+ 01/18/26 Initial version
+
+*/
+
+#ifndef _PMMI_MM103_H
+#define _PMMI_MM103_H
+
+
+#define UNIT_V_PMMI_VERBOSE (UNIT_V_UF + 0) /* Verbose messages */
+#define UNIT_PMMI_VERBOSE (1 << UNIT_V_PMMI_VERBOSE)
+#define UNIT_V_PMMI_CONSOLE (UNIT_V_UF + 1) /* Use this device for console */
+#define UNIT_PMMI_CONSOLE (1 << UNIT_V_PMMI_CONSOLE)
+#define UNIT_V_PMMI_RTS (UNIT_V_UF + 2) /* RTS follows DTR */
+#define UNIT_PMMI_RTS (1 << UNIT_V_PMMI_RTS)
+
+#define PMMI_WAIT 500 /* Service Wait Interval */
+
+#define PMMI_IOBASE 0xC0
+#define PMMI_IOSIZE 4
+
+#define PMMI_REG0 0 /* Relative Address 0 */
+#define PMMI_REG1 1 /* Relative Address 1 */
+#define PMMI_REG2 2 /* Relative Address 2 */
+#define PMMI_REG3 3 /* Relative Address 3 */
+
+#define PMMI_TBMT 0x01 /* Transmit Data Register Empty */
+#define PMMI_DAV 0x02 /* Receive Data Register Full */
+#define PMMI_TEOC 0x04 /* Transmit Serializer Empty */
+#define PMMI_RPE 0x08 /* Parity Error */
+#define PMMI_OR 0x10 /* Overrun */
+#define PMMI_FE 0x20 /* Framing Error */
+
+#define PMMI_DT 0x01 /* Dial Tone */
+#define PMMI_RNG 0x02 /* Ringing */
+#define PMMI_CTS 0x04 /* Clear to Send */
+#define PMMI_RXBRK 0x08 /* RX Break */
+#define PMMI_AP 0x10 /* Answer Phone */
+#define PMMI_FO 0x20 /* Digital Carrier Signal */
+#define PMMI_MODE 0x40 /* Mode */
+#define PMMI_TMR 0x80 /* Timer Pulses */
+
+#define PMMI_ST 0x10 /* Self Test */
+#define PMMI_DTR 0x40 /* DTR */
+
+#define PMMI_SH 0x01 /* Switch Hook */
+#define PMMI_RI 0x02 /* Ring Indicator */
+#define PMMI_5BIT 0x00 /* 5 Data Bits */
+#define PMMI_6BIT 0x04 /* 6 Data Bits */
+#define PMMI_7BIT 0x08 /* 7 Data Bits */
+#define PMMI_8BIT 0x0C /* 8 Data Bits */
+#define PMMI_BMSK 0x0C /* Data Bits Bit Mask */
+
+#define PMMI_OPAR 0x00 /* Odd Parity */
+#define PMMI_NPAR 0x10 /* No Parity */
+#define PMMI_EPAR 0x20 /* Odd Parity */
+#define PMMI_PMSK 0x30 /* Parity Bit Mask */
+
+#define PMMI_1SB 0x00 /* 1 Stop Bit */
+#define PMMI_15SB 0x40 /* 1.5 Stop Bits */
+#define PMMI_2SB 0x40 /* 2 Stop Bits */
+#define PMMI_SMSK 0x40 /* Stop Bits Bit Mask */
+
+#define PMMI_CLOCK 2500 /* Rate Generator / 100 */
+#define PMMI_BAUD 300 /* Default baud rate */
+
+#endif
+
diff --git a/Altair8800/s100_bus.c b/Altair8800/s100_bus.c
index 9d40ad67..826f69d4 100644
--- a/Altair8800/s100_bus.c
+++ b/Altair8800/s100_bus.c
@@ -45,8 +45,6 @@ static t_stat bus_hexsave_command (int32 flag, const char *cptr);
static t_stat hexload (const char *filename, t_addr bias);
static t_stat hexsave (FILE *outFile, t_addr start, t_addr end);
-static ChipType chiptype = CHIP_TYPE_Z80;
-
static MDEV mdev_table[MAXPAGE]; /* Active memory table */
static MDEV mdev_dflt; /* Default memory table */
@@ -413,18 +411,6 @@ void s100_bus_memw(t_addr addr, int32 data)
mdev_table[page].routine(addr, S100_IO_WRITE, data);
}
-ChipType s100_bus_set_chiptype(ChipType new)
-{
- chiptype = new;
-
- return chiptype;
-}
-
-ChipType s100_bus_get_chiptype(void)
-{
- return chiptype;
-}
-
uint32 s100_bus_set_addr(uint32 new)
{
bus_addr = new;
diff --git a/Altair8800/s100_cpu.c b/Altair8800/s100_cpu.c
index 7cffa6e8..8443383a 100644
--- a/Altair8800/s100_cpu.c
+++ b/Altair8800/s100_cpu.c
@@ -159,6 +159,11 @@ void cpu_set_chiptype(ChipType new_type)
cpu_reset(&cpu_dev);
}
+ChipType cpu_get_chiptype()
+{
+ return cpu_type;
+}
+
char * cpu_get_chipname(ChipType type)
{
return cpu_chipname[type];
diff --git a/Altair8800/s100_cpu.h b/Altair8800/s100_cpu.h
index bbc5b60b..183e41e8 100644
--- a/Altair8800/s100_cpu.h
+++ b/Altair8800/s100_cpu.h
@@ -56,6 +56,7 @@ typedef struct {
} CPU;
extern void cpu_set_chiptype(ChipType type);
+extern ChipType cpu_get_chiptype(void);
extern char * cpu_get_chipname(ChipType type);
#endif
diff --git a/Altair8800/s100_ram.c b/Altair8800/s100_ram.c
index 31026d64..bb699fbd 100644
--- a/Altair8800/s100_ram.c
+++ b/Altair8800/s100_ram.c
@@ -41,6 +41,7 @@ static t_stat ram_default_dis (UNIT *uptr, int32 value, const char *cptr,
static t_stat ram_set_memsize (int32 value);
static t_stat ram_clear_command (UNIT *uptr, int32 value, const char *cptr, void *desc);
static t_stat ram_enable_command (UNIT *uptr, int32 value, const char *cptr, void *desc);
+static t_stat ram_prot_command (UNIT *uptr, int32 value, const char *cptr, void *desc);
static t_stat ram_randomize_command (UNIT *uptr, int32 value, const char *cptr, void *desc);
static t_stat ram_size_command (UNIT *uptr, int32 value, const char *cptr, void *desc);
static void ram_clear (void);
@@ -54,7 +55,7 @@ static uint32 GetBYTE(register uint32 Addr);
static int32 poc = TRUE; /* Power On Clear */
static int32 M[MAXBANKSIZE]; /* RAM */
-static int32 P[MAXBANKSIZE >> LOG2PAGESIZE]; /* Active pages */
+static int32 P[MAXBANKSIZE >> LOG2PAGESIZE]; /* Protected pages */
static int32 memsize = MAXBANKSIZE;
static const char* ram_description(DEVICE *dptr) {
@@ -86,6 +87,10 @@ static MTAB ram_mod[] = {
NULL, NULL, "Enable RAM page(s)" },
{ MTAB_XTD | MTAB_VDV | MTAB_VALR, 0, NULL, "REMRAM={PAGE | START-END | ALL}", &ram_enable_command,
NULL, NULL, "Disable RAM page(s)" },
+ { MTAB_XTD | MTAB_VDV | MTAB_VALR, TRUE, NULL, "PROT={PAGE | START-END | ALL}", &ram_prot_command,
+ NULL, NULL, "Protect RAM page(s)" },
+ { MTAB_XTD | MTAB_VDV | MTAB_VALR, FALSE, NULL, "UNPROT={PAGE | START-END | ALL}", &ram_prot_command,
+ NULL, NULL, "Unprotect RAM page(s)" },
{ MTAB_VDV, 0, NULL, "CLEAR", &ram_clear_command,
NULL, NULL, "Sets RAM to 0x00" },
{ MTAB_VDV, 0, NULL, "RANDOM", &ram_randomize_command,
@@ -93,8 +98,12 @@ static MTAB ram_mod[] = {
{ 0 }
};
+/* Debug Flags */
+#define VERBOSE_MSG (1 << 0)
+
/* Debug Flags */
static DEBTAB ram_dt[] = {
+ { "VERBOSE", VERBOSE_MSG, "Verbose messages" },
{ NULL, 0 }
};
@@ -116,7 +125,7 @@ DEVICE ram_dev = {
NULL, /* attach routine */
NULL, /* detach routine */
NULL, /* context */
- (DEV_DISABLE), /* flags */
+ (DEV_DISABLE | DEV_DEBUG), /* flags */
0, /* debug control */
ram_dt, /* debug flags */
NULL, /* mem size routine */
@@ -129,6 +138,8 @@ DEVICE ram_dev = {
static t_stat ram_reset(DEVICE *dptr)
{
+ t_addr addr;
+
if (dptr->flags & DEV_DIS) { /* Disable Device */
s100_bus_remmem(0x0000, MAXBANKSIZE, &ram_memio);
ram_default_dis(NULL, 0, NULL, NULL);
@@ -143,6 +154,14 @@ static t_stat ram_reset(DEVICE *dptr)
ram_default_ena(NULL, 0, NULL, NULL);
}
+ for (addr = 0; addr < MAXADDR; addr++) {
+ M[addr] = 0x00;
+
+ if ((addr & 0xff) == 0x00) {
+ P[addr >> LOG2PAGESIZE] = FALSE;
+ }
+ }
+
poc = FALSE;
}
}
@@ -175,12 +194,17 @@ static int32 ram_memio(const int32 addr, const int32 rw, const int32 data)
return 0x0ff;
}
-static void PutBYTE(register uint32 Addr, const register uint32 Value)
+static void PutBYTE(uint32 Addr, const register uint32 Value)
{
- M[Addr & ADDRMASK] = Value & DATAMASK;
+ if (!P[(Addr & ADDRMASK) >> LOG2PAGESIZE]) {
+ M[Addr & ADDRMASK] = Value & DATAMASK;
+ }
+ else {
+ sim_debug(VERBOSE_MSG, &ram_dev, "RAM: Page %02X is protected\n", Addr >> LOG2PAGESIZE);
+ }
}
-static uint32 GetBYTE(register uint32 Addr)
+static uint32 GetBYTE(uint32 Addr)
{
return M[Addr & ADDRMASK] & DATAMASK; /* RAM */
}
@@ -200,9 +224,8 @@ static t_stat ram_default_dis(UNIT *uptr, int32 value, const char *cptr, void *d
}
/* set memory to 'size' kilo byte */
-static t_stat ram_set_memsize(int32 size) {
- int32 page;
-
+static t_stat ram_set_memsize(int32 size)
+{
size <<= KBLOG2;
if (size < KB) {
@@ -218,11 +241,6 @@ static t_stat ram_set_memsize(int32 size) {
s100_bus_remmem(0x0000, MAXBANKSIZE, &ram_memio); /* Remove all pages */
s100_bus_addmem(0x0000, memsize, &ram_memio, "RAM"); /* Add memsize pages */
- /* Keep track of active pages for SHOW */
- for (page = 0; page < (MAXBANKSIZE >> LOG2PAGESIZE); page++) {
- P[page] = (page << LOG2PAGESIZE) <= memsize;
- }
-
ram_unit.capac = memsize;
return SCPE_OK;
@@ -246,7 +264,8 @@ static void ram_randomize()
}
}
-static t_stat ram_size_command(UNIT *uptr, int32 value, const char *cptr, void *desc) {
+static t_stat ram_size_command(UNIT *uptr, int32 value, const char *cptr, void *desc)
+{
int32 size, result;
if (cptr == NULL) {
@@ -263,12 +282,13 @@ static t_stat ram_size_command(UNIT *uptr, int32 value, const char *cptr, void *
return SCPE_ARG | SCPE_NOMESSAGE;
}
-static t_stat ram_enable_command(UNIT *uptr, int32 value, const char *cptr, void *desc) {
+static t_stat ram_enable_command(UNIT *uptr, int32 value, const char *cptr, void *desc)
+{
int32 size;
t_addr start, end;
if (cptr == NULL) {
- sim_printf("Memory page(s) must be provided as SET RAM ENABLE=E0-EF\n");
+ sim_printf("Memory page(s) must be provided as SET RAM %s=E0-EF\n", value ? "ADDRAM" : "REMRAM");
return SCPE_ARG | SCPE_NOMESSAGE;
}
@@ -298,6 +318,35 @@ static t_stat ram_enable_command(UNIT *uptr, int32 value, const char *cptr, void
return SCPE_OK;
}
+static t_stat ram_prot_command(UNIT *uptr, int32 value, const char *cptr, void *desc)
+{
+ t_addr start, end;
+ uint16 page;
+
+ if (cptr == NULL) {
+ sim_printf("Memory page(s) must be provided as SET RAM %s=E0-EF\n", value ? "PROT" : "UNPROT");
+ return SCPE_ARG | SCPE_NOMESSAGE;
+ }
+
+ if (get_range(NULL, cptr, &start, &end, 16, PAGEMASK, 0) == NULL) {
+ return SCPE_ARG;
+ }
+
+ if (start > MAXPAGE) {
+ start = start >> LOG2PAGESIZE;
+ }
+ if (end > MAXPAGE) {
+ end = end >> LOG2PAGESIZE;
+ }
+
+ /* Protect or Unprotect Pages */
+ for (page = start; page <= end; page++) {
+ P[page] = value;
+ }
+
+ return SCPE_OK;
+}
+
static t_stat ram_clear_command(UNIT *uptr, int32 value, const char *cptr, void *desc)
{
ram_clear();
diff --git a/Altair8800/s100_rom.c b/Altair8800/s100_rom.c
index dfbd4b61..5c71ac19 100644
--- a/Altair8800/s100_rom.c
+++ b/Altair8800/s100_rom.c
@@ -55,11 +55,12 @@ static t_stat rom_show_help(FILE *st, DEVICE *dptr, UNIT *uptr, int32 flag, cons
static int32 M[MAXBANKSIZE];
static ROM rom_table[] = {
- { UNIT_ROM_ALTMON, rom_altmon, ROM_ALTMON_BASEADDR, ROM_ALTMON_SIZE, ROM_ALTMON_NAME, ROM_ALTMON_DESC },
- { UNIT_ROM_DBL, rom_mits_dbl, ROM_MITS_DBL_BASEADDR, ROM_MITS_DBL_SIZE, ROM_MITS_DBL_NAME, ROM_MITS_DBL_DESC },
- { UNIT_ROM_HDSK, rom_mits_hdsk, ROM_MITS_HDSK_BASEADDR, ROM_MITS_HDSK_SIZE, ROM_MITS_HDSK_NAME, ROM_MITS_HDSK_DESC },
- { UNIT_ROM_TURMON, rom_mits_turmon, ROM_MITS_TURMON_BASEADDR, ROM_MITS_TURMON_SIZE, ROM_MITS_TURMON_NAME, ROM_MITS_TURMON_DESC },
- { UNIT_ROM_AZ80DBL, rom_az80_dbl, ROM_AZ80_DBL_BASEADDR, ROM_AZ80_DBL_SIZE, ROM_AZ80_DBL_NAME, ROM_AZ80_DBL_DESC },
+ { UNIT_ROM_ALTMON, rom_altmon, ROM_ALTMON_BASEADDR, ROM_ALTMON_SIZE, ROM_ALTMON_NAME, ROM_ALTMON_DESC },
+ { UNIT_ROM_DBL, rom_mits_dbl, ROM_MITS_DBL_BASEADDR, ROM_MITS_DBL_SIZE, ROM_MITS_DBL_NAME, ROM_MITS_DBL_DESC },
+ { UNIT_ROM_HDSK, rom_mits_hdsk, ROM_MITS_HDSK_BASEADDR, ROM_MITS_HDSK_SIZE, ROM_MITS_HDSK_NAME, ROM_MITS_HDSK_DESC },
+ { UNIT_ROM_TURMON, rom_mits_turmon, ROM_MITS_TURMON_BASEADDR, ROM_MITS_TURMON_SIZE, ROM_MITS_TURMON_NAME, ROM_MITS_TURMON_DESC },
+ { UNIT_ROM_CDBL, rom_me_cdbl, ROM_ME_CDBL_BASEADDR, ROM_ME_CDBL_SIZE, ROM_ME_CDBL_NAME, ROM_ME_CDBL_DESC },
+ { UNIT_ROM_AZ80DBL, rom_az80_dbl, ROM_AZ80_DBL_BASEADDR, ROM_AZ80_DBL_SIZE, ROM_AZ80_DBL_NAME, ROM_AZ80_DBL_DESC },
{ 0, NULL, 0x0000, 0, "", "" }
};
@@ -108,6 +109,11 @@ static MTAB rom_mod[] = {
{ UNIT_ROM_TURMON, 0, "NO" ROM_MITS_TURMON_NAME, "NO" ROM_MITS_TURMON_NAME, &rom_dis_turmon, NULL,
NULL, "Disable " ROM_MITS_TURMON_DESC },
+ { UNIT_ROM_CDBL, UNIT_ROM_CDBL, ROM_ME_CDBL_NAME, ROM_ME_CDBL_NAME, &rom_ena, NULL,
+ NULL, "Enable " ROM_ME_CDBL_DESC },
+ { UNIT_ROM_CDBL, 0, "NO" ROM_ME_CDBL_NAME, "NO" ROM_ME_CDBL_NAME, &rom_dis_turmon, NULL,
+ NULL, "Disable " ROM_ME_CDBL_DESC },
+
{ MTAB_XTD | MTAB_VDV | MTAB_NMO, 0, "LIST", NULL, NULL, &rom_show_list, NULL, "Show available ROMs" },
{ 0 }
diff --git a/Altair8800/s100_rom.h b/Altair8800/s100_rom.h
index 6a7981a2..4352288d 100644
--- a/Altair8800/s100_rom.h
+++ b/Altair8800/s100_rom.h
@@ -43,7 +43,9 @@
#define UNIT_ROM_ALTMON (1 << UNIT_ROM_V_ALTMON )
#define UNIT_ROM_V_TURMON (UNIT_V_UF+4) /* Enable/Disable Turnkey Monitor */
#define UNIT_ROM_TURMON (1 << UNIT_ROM_V_TURMON )
-#define UNIT_ROM_V_AZ80DBL (UNIT_V_UF+5) /* Enable/Disable AltairZ80 Disk Boot Loader */
+#define UNIT_ROM_V_CDBL (UNIT_V_UF+5) /* Enable/Disable Combined Disk Boot Loader */
+#define UNIT_ROM_CDBL (1 << UNIT_ROM_V_CDBL)
+#define UNIT_ROM_V_AZ80DBL (UNIT_V_UF+6) /* Enable/Disable AltairZ80 Disk Boot Loader */
#define UNIT_ROM_AZ80DBL (1 << UNIT_ROM_V_AZ80DBL)
typedef struct {
diff --git a/Altair8800/s100_roms.h b/Altair8800/s100_roms.h
index 0474ee20..ae57520f 100644
--- a/Altair8800/s100_roms.h
+++ b/Altair8800/s100_roms.h
@@ -330,6 +330,53 @@ static int32 rom_altmon[ROM_ALTMON_SIZE] = {
0x9c, 0x23, 0xc0, 0x13, 0xc9, 0xff, 0xff, 0xff
};
+/*
+ * Martin Eberhard Combined Disk Boot Loader
+ * https://deramp.com/downloads/altair/software/roms/custom_roms/M%20Eberhard%20Improved%20ROMs/CDBL.ASM
+ */
+#define ROM_ME_CDBL_BASEADDR 0xff00
+#define ROM_ME_CDBL_SIZE 0x0100
+#define ROM_ME_CDBL_NAME "CDBL"
+#define ROM_ME_CDBL_DESC "Combined Disk Boot Loader"
+
+static int32 rom_me_cdbl[ROM_ME_CDBL_SIZE] = {
+ 0xf3, 0x11, 0xf5, 0xff, 0x21, 0xe1, 0x4c, 0x1b,
+ 0x2b, 0x1a, 0x77, 0x7d, 0xb7, 0xc2, 0x07, 0xff,
+ 0x01, 0x82, 0x06, 0xe9, 0xd3, 0x22, 0x2f, 0xd3,
+ 0x23, 0x3e, 0x2c, 0xd3, 0x22, 0xaf, 0xd3, 0x08,
+ 0xdb, 0x08, 0xe6, 0x08, 0xc2, 0x09, 0x4c, 0x3e,
+ 0x04, 0xd3, 0x09, 0x3e, 0x01, 0xd3, 0x09, 0x0b,
+ 0x78, 0xb1, 0xc2, 0x1b, 0x4c, 0x0c, 0xdb, 0x08,
+ 0x0f, 0x0f, 0xda, 0x22, 0x4c, 0xe6, 0x10, 0x3e,
+ 0x02, 0xc2, 0x19, 0x4c, 0xdb, 0x09, 0xe6, 0x3f,
+ 0xfe, 0x1e, 0xc2, 0x30, 0x4c, 0xdb, 0x09, 0x0f,
+ 0xd2, 0x39, 0x4c, 0xdb, 0x09, 0x0f, 0xda, 0x3f,
+ 0x4c, 0xe6, 0x1f, 0xc6, 0x10, 0x4f, 0x65, 0x3e,
+ 0x03, 0xd3, 0x10, 0x3e, 0x11, 0xd3, 0x10, 0x3e,
+ 0x10, 0x31, 0x7b, 0x4d, 0xf5, 0xdb, 0x09, 0xe6,
+ 0x3f, 0x0f, 0xb8, 0xc2, 0x59, 0x4c, 0x11, 0x7b,
+ 0x4d, 0x7c, 0xaa, 0xe6, 0xfe, 0x3e, 0x4f, 0xca,
+ 0xc5, 0x4c, 0xe5, 0xc5, 0x01, 0x80, 0x00, 0xdb,
+ 0x08, 0x07, 0xda, 0x73, 0x4c, 0xdb, 0x0a, 0x12,
+ 0x1c, 0xc2, 0x73, 0x4c, 0x1e, 0x7e, 0x1a, 0x77,
+ 0xbe, 0xc2, 0xc3, 0x4c, 0x80, 0x47, 0x13, 0x23,
+ 0x0d, 0xc2, 0x82, 0x4c, 0xeb, 0x4e, 0x0c, 0x23,
+ 0xae, 0xb1, 0xc1, 0xc2, 0xb6, 0x4c, 0x2a, 0x7c,
+ 0x4d, 0xeb, 0x7d, 0x93, 0x7c, 0x9a, 0xd2, 0xc7,
+ 0x4c, 0x11, 0x53, 0x4c, 0xd5, 0x04, 0x04, 0x78,
+ 0xb9, 0xd8, 0x06, 0x01, 0xc8, 0x78, 0xd3, 0x09,
+ 0x05, 0xc9, 0x3e, 0x04, 0xd3, 0x09, 0xe1, 0xf1,
+ 0x3d, 0xc2, 0x55, 0x4c, 0x3e, 0x43, 0x11, 0x3e,
+ 0x4d, 0x47, 0x37, 0x3e, 0x80, 0xd3, 0x08, 0xd2,
+ 0x00, 0x00, 0xfb, 0x22, 0x01, 0x00, 0x78, 0x32,
+ 0x00, 0x00, 0xd3, 0x01, 0xd3, 0x11, 0xd3, 0x05,
+ 0xd3, 0x23, 0xc3, 0xd6, 0x4c, 0xff, 0xff, 0xff,
+ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
+};
+
+/*
+ * AltairZ80 Disk Boot Loader
+ */
#define ROM_AZ80_DBL_BASEADDR 0xff00
#define ROM_AZ80_DBL_SIZE 0x0100
#define ROM_AZ80_DBL_NAME "AZ80DBL"
diff --git a/Visual Studio Projects/Altair8800.vcproj b/Visual Studio Projects/Altair8800.vcproj
index 26da4748..4711b744 100644
--- a/Visual Studio Projects/Altair8800.vcproj
+++ b/Visual Studio Projects/Altair8800.vcproj
@@ -203,6 +203,10 @@
RelativePath="..\Altair8800\altair8800_sys.c"
>
+
+
@@ -215,6 +219,10 @@
RelativePath="..\Altair8800\mits_dsk.c"
>
+
+
@@ -336,6 +344,10 @@
RelativePath="..\Altair8800\altair8800_sys.h"
>
+
+
@@ -348,6 +360,10 @@
RelativePath="..\Altair8800\mits_dsk.h"
>
+
+
diff --git a/makefile b/makefile
index 282a834e..99237f9c 100644
--- a/makefile
+++ b/makefile
@@ -2242,9 +2242,11 @@ ALTAIR8800 = \
${ALTAIR8800D}/s100_ram.c \
${ALTAIR8800D}/s100_rom.c \
${ALTAIR8800D}/s100_z80.c \
+ ${ALTAIR8800D}/cromemco_dazzler.c \
${ALTAIR8800D}/mits_2sio.c \
${ALTAIR8800D}/mits_acr.c \
${ALTAIR8800D}/mits_dsk.c \
+ ${ALTAIR8800D}/pmmi_mm103.c \
${ALTAIR8800D}/sds_sbc200.c \
${ALTAIR8800D}/sds_vfii.c \
${ALTAIR8800D}/tarbell_fdc.c \