open-simh/simh

AltairZ80: TMXR/THROTTLE Hell

deltecent opened this issue · 15 comments

@markpizz, I am in TMXR/THROTTLE Hell again.

SET THROTTLE 250K

After it runs a bit and calibrates, my service routines work great. The SIMH queue is running with some screwy timing causing the simulator to run like a pig.

Altair 8800 (Z80) simulator Open SIMH V4.1-0 Current        git commit id: 2b6884fc+uncommitted-changes

sim> show q
Altair 8800 (Z80) event queue status, time = 162565441, executing 1,900 instructions/sec
  JAIRS1 at 1 (526 usecs)
  JAIRS0 at 1 (526 usecs)
  SIMH at 4559 (2.399474 seconds) <----- ?????
asynchronous pending event queue
  Empty
asynch latency: 4000 nanoseconds
asynch instruction latency: 1 instructions
sim> show throttle
Throttle:                      250 kiloinstructions
Throttling by sleeping for:    1 ms every 668 instructions
sim> show clocks
Minimum Host Sleep Time:        1 ms (1000Hz)
Host Clock Resolution:          1 ms
Execution Rate:                 1,900 instructions/sec
Throttle:                      250 kiloinstructions
Throttling by sleeping for:    1 ms every 668 instructions
Calibrated Timer:               Internal Timer
Catchup Ticks:                  Disabled
Pre-Calibration Estimated Rate: 45,871,559
Calibration:                    Always
Asynchronous Clocks:            Available

Altair 8800 (Z80) clock device is Internal Calibrated Timer(INT-TIMER)
Calibrated Timer 8:
  Running at:                100 Hz
  Tick Size:                 
  Ticks in current second:   2
  Seconds Running:           84 (1:24 minutes)
  Calibration Opportunities: 84
  Calibs Skip Gap Too Big:   1
  Instruction Time:          162565400
  Current Insts Per Tick:    19
  Initializations:           1
  Ticks:                     8,301
  Initialize Base Time:      19:38:53.048
  Wall Clock Time Now:       19:44:59.741
sim> show mux
Multiplexer device: JAIRS1, ModemControl=enabled, Output Unit: JAIRS1,
    Input Polling Unit: JAIRS1,
    attached to Connect=/dev/cu.usbserial-AL009KFH;19200-8N1, connected, sessions=0, Speed=19200 bps
Connected to serial port /dev/cu.usbserial-AL009KFH
 Connected 00:07:22
 Modem Bits: DTR RTS DCD CTS DSR 
  input (on) queued/total = 0/36098
  output (on) queued/total = 0/1610
  speed = 19200 bps
  bytes in buffer = 256

If I don't attach a serial port and TMXR isn't called, throttle works great:

sim> show q
Altair 8800 (Z80) event queue status, time = 344111613, executing 196,600 instructions/sec
  JAIRS1 at 0
  JAIRS0 at 19 (97 usecs)
  SIMH at 3387 (17.228 msecs) <----- Looks right
asynchronous pending event queue
  Empty
asynch latency: 4000 nanoseconds
asynch instruction latency: 1 instructions
sim> show throttle
Throttle:                      250 kiloinstructions
Throttling by sleeping for:    1 ms every 250 instructions
sim> show clocks
Minimum Host Sleep Time:        1 ms (1000Hz)
Host Clock Resolution:          1 ms
Execution Rate:                 196,600 instructions/sec
Throttle:                      250 kiloinstructions
Throttling by sleeping for:    1 ms every 250 instructions
Calibrated Timer:               Internal Timer
Catchup Ticks:                  Disabled
Pre-Calibration Estimated Rate: 47,619,047
Calibration:                    Always
Asynchronous Clocks:            Available

Altair 8800 (Z80) clock device is Internal Calibrated Timer(INT-TIMER)
Calibrated Timer 8:
  Running at:                100 Hz
  Tick Size:                 
  Ticks in current second:   46
  Seconds Running:           11 (11 seconds)
  Calibration Opportunities: 11
  Instruction Time:          344020600
  Current Insts Per Tick:    1,966
  Initializations:           1
  Ticks:                     1,045
  Initialize Base Time:      19:52:04.740
  Wall Clock Time Now:       19:52:58.861

As far as I know, nobody is calling sleep on their own.

It seems to be related to tmxr_poll_tx, which I believe I was told was necessary but I don't know what it does. If I take that function out of my service routing, throttling seems to work ok.

Any suggestions?

And we really need to come up with a way to get THROTTLE working out of the gate. I tried THROTTLE x/y, but that doesn't work quite right. It does throttle the CPU appropriately but the service dispatch times are all slow so the attached serial ports don't work right (input and output is very slow). At least, that's what it appears to be.

Right now I load a small program, let it run, escape out, load the real program, then run it. I'm working with a boot loader that requires timing. It's a pain, but it works.

When you throttle to a more or less historically accurate execution rate, what do you see for serial port rate? Does it also slow down to something like the historic rate -- which can be surprisingly slow compared to what we're used to -- or does it become much slower than that? Is 19.2 kbaud a realistic rate for that machine?

That depends on which problem I'm trying to figure out. Without tmxr_poll_tx, which I guess I don't really need, SET THROTTLE 250K works fine (other than having to wait for a bunch of instruction executions before it works).

After the throttle calibrates, I can see that it is sleeping 1ms for every 350 instructions.

If I try SET THROTTLE 350/1, it starts sleeping 1ms for every 350 instructions, but it doesn't seem to adjust the service routine dispatch time, so instead of the service routines being called every x microseconds, it's called every x milliseconds, essentially outputting data at about 8-10 characters a second.

Patric,

It seems that you're working with a new device. If you put that code in your personal repo, I'll likely have some useful feedback.

  • Mark

You may find it here:

https://github.com/deltecent/open-simh/tree/jair

Thanks for taking a look at it. I figure all of my devices that use the same sort of service routines are wrong.

I added some code utilizing sim_os_msec() to display how many ms it takes to call the service routine 1000 times.

This is with SET THROTTLE 250K

msec=50
msec=50
msec=49
msec=48
msec=50
msec=49
msec=50
msec=49
msec=51
msec=49
msec=49
msec=49
msec=48
msec=50
msec=49
msec=50
msec=50
msec=49
msec=48
msec=49
msec=51
msec=49

Simulation stopped, PC: 0F138 (LDA 0FD82h)

Simulation stopped C0Z1M0E1I1 A=00 B=00E4 D=EEBC H=FD00 S=FCFC P=F138 LDA 0FD82h
sim> show throttle
Throttle:                      250 kiloinstructions
Throttling by sleeping for:    1 ms every 251 instructions
sim> 

This is with SET THROTTLE 251/1

msec=11996
msec=11999
msec=12029

Simulation stopped, PC: 0F13B (ORA A)

Simulation stopped C0Z1M0E1I1 A=00 B=00E4 D=EEBC H=FD00 S=FCFC P=F13B ORA A
sim> show throttle
Throttle:                      251/1
Throttling by sleeping for:    1 ms every 251 instructions
sim>

As you can see, it takes about 50ms for the service routine to be called 1000 times with SET THROTTLE 250K vs 12000ms with SET THROTTLE 251/1.

The instructions are executing at the right speed. The service routine dispatch time is what is not right.

I don't know what I'm doing wrong to make that happen.

Looking at your code in s100_jair.c, it seems you've got 3 simh DEVICEs: (JAIRP_SNAME, JAIRS0_SNAME, JAIRS1_NAME), each of these separate devices have independent UNITs (1 per DEVICE), but all of these share exactly the same unit service routine. I'm not really sure how all of this might actually be work, but I think you need some info about how TMXR expects things to work:

For a given MUX (with one or more lines), there are 3 things related to data that need to happen:
a) Detecting newly arriving connections (done by calling tmxr_poll_conn()).
b) Detecting data arriving from one or more ports.
c) Transmitting data out a port.

Item a, can be done with frequency of 100's to 1000's of milliseconds (i.e. no rush).
Item b should be done at an appropriate rate that corresponds to the line speed (or baud rate) of the port that has data
Item c should be done when data is available to be transmitted and any prior data has completed transmission.

The simh UNIT that does each of these functions could be just by one UNIT or completely separate for each of the respective activities and ports on the MUX.

If it is all done by one UNIT, then when that UNIT's service routine runs it might busily call each of the respective TMXR routines tmxr_poll_conn, tmxr_poll_rx or tmxr_poll_tx, with possibly nothing actually happening in each of those routines except consumption of any number of system calls. It is your choice how to do this. However, you should reflect what choices you made by conveyng those choices to TMXR by calling tmxr_set_line_unit() and tmxr_set_line_output_unit(). These routines should be called in the DEVICE reset routine. Additionally, since it seems that you've got logic which messes with line speeds, then you should be calling tmxr_set_port_speed_control() in your reset routine.

Separate from this, the UNIT service routine talking to a MUX should always be calling sim_activate_xx rescheduling it's next callback just BEFORE returning, and NOT at the beginning of the service routine. This is so that whatever happened during the current run through the service routine is considered internally within TMXR to actually schedule the next activation at the best time and only using the sim_activate provided interval as a fallback when nothing tmxr special is needed (i.e. the specified interval should be relatively large so that arriving connections can be detected). There is ABSOLUTELY NO need for your code to mess with trying to plan when the callbacks should be scheduled.

Additionally, if a callback is happening when a received character has previously arrived, but it hasn't yet been consumed by instructions running within the simulator, then tmxr_poll_rx() shouldn't be called. If the simulator is ready for more input, then tmxr_poll_rx() should be called and tmxr_getc_ln() to see if anything new has arrived.

On the transmit side, if there is data to go out, then tmxr_putc_ln() should be called AND only when SCPE_LOST or SCPE_OK is returned, should the code consider the data sent. Otherwise, you probably got SCPE_STALL as a return and the data to be transmitted should be used again on a future call to the service routine.

You could separate the transmit and receive activities to be done by separate simh UNITs. Such additional UNITs could merely be part of the UNIT array in the DEVICE, but have flags which simply were UNIT_DIS with separate service routines which was specific to the purpose (transmit or receive) of that unit. The transmit unit could be scheduled by a sim_activate call in the code path which is invoked when simulator instructions send data outbound, and when transmit activity for that line was determined to complete in the transmit service routine, the service routine would schedule an interrupt or indicate complete status in a CPU visible register and NOT reschedule itself..

The above comments have nothing to do with throttling.

As I've mentioned before, the THROTTLE code is a mystery. But, I think the issue is a piece of magic code here:

            /* Run through all timers and adjust the calibration for each */
            /* one that is running to reflect the throttle rate */
            for (tmr=0; tmr<=SIM_NTIMERS; tmr++) {
                rtc = &rtcs[tmr];
                if (rtc->hz) {                                      /* running? */
                    rtc->currd = (int32)(sim_throt_cps / rtc->hz);/* use throttle calibration */
                    rtc->ticks = rtc->hz - 1;                     /* force clock calibration on next tick */
                    rtc->rtime = sim_throt_ms_start - 1000 + 1000/rtc->hz;/* adjust calibration parameters to reflect throttled rate */
                    rtc->gtime = sim_throt_inst_start - sim_throt_cps + sim_throt_cps/rtc->hz;
                    rtc->nxintv = 1000;
                    rtc->based = rtc->currd;
                    if (rtc->clock_unit)
                        sim_activate_abs (rtc->clock_unit, rtc->currd);/* reschedule next tick */
                    }

This timer calibration doesn't happen if SET THROTTLE x/y is used. On a whim, I put that code in sim_set_throt, and it seems to have fixed the problem:

% git diff sim_timer.c
diff --git a/sim_timer.c b/sim_timer.c
index 4028a687..c384f439 100644
--- a/sim_timer.c
+++ b/sim_timer.c
@@ -1699,6 +1699,8 @@ t_stat sim_set_throt (int32 arg, CONST char *cptr)
 CONST char *tptr;
 char c;
 t_value val, val2 = 0;
+int32 tmr;
+RTC *rtc = NULL;
 
 if (arg == 0) {
     if ((cptr != NULL) && (*cptr != 0))
@@ -1751,6 +1753,21 @@ else {
             }
         sim_throt_state = SIM_THROT_STATE_THROTTLE;         /* force state */
         sim_throt_wait = sim_throt_val;
+            /* PAL Run through all timers and adjust the calibration for each */
+            /* one that is running to reflect the throttle rate */
+            for (tmr=0; tmr<=SIM_NTIMERS; tmr++) {
+                rtc = &rtcs[tmr];
+                if (rtc->hz) {                                      /* running? */
+                    rtc->currd = (int32)(sim_throt_cps / rtc->hz);/* use throttle calibration */
+                    rtc->ticks = rtc->hz - 1;                     /* force clock calibration on next tick */
+                    rtc->rtime = sim_os_msec ();
+                    rtc->gtime = sim_throt_inst_start - sim_throt_cps + sim_throt_cps/rtc->hz;
+                    rtc->nxintv = 1000;
+                    rtc->based = rtc->currd;
+                    if (rtc->clock_unit)
+                        sim_activate_abs (rtc->clock_unit, rtc->currd);/* reschedule next tick */
+                    }
+                }
         }
     }
 if (sim_throt_type == SIM_THROT_SPC)    /* Set initial value while correct one is determined */

Looking at your code in s100_jair.c, it seems you've got 3 simh DEVICEs: (JAIRP_SNAME, JAIRS0_SNAME, JAIRS1_NAME), each of these separate devices have independent UNITs (1 per DEVICE), but all of these share exactly the same unit service routine. I'm not really sure how all of this might actually be work, but I think you need some info about how TMXR expects things to work:

For a given MUX (with one or more lines), there are 3 things related to data that need to happen: a) Detecting newly arriving connections (done by calling tmxr_poll_conn()). b) Detecting data arriving from one or more ports. c) Transmitting data out a port.

Item a, can be done with frequency of 100's to 1000's of milliseconds (i.e. no rush). Item b should be done at an appropriate rate that corresponds to the line speed (or baud rate) of the port that has data Item c should be done when data is available to be transmitted and any prior data has completed transmission.

The simh UNIT that does each of these functions could be just by one UNIT or completely separate for each of the respective activities and ports on the MUX.

If it is all done by one UNIT, then when that UNIT's service routine runs it might busily call each of the respective TMXR routines tmxr_poll_conn, tmxr_poll_rx or tmxr_poll_tx, with possibly nothing actually happening in each of those routines except consumption of any number of system calls. It is your choice how to do this. However, you should reflect what choices you made by conveyng those choices to TMXR by calling tmxr_set_line_unit() and tmxr_set_line_output_unit(). These routines should be called in the DEVICE reset routine. Additionally, since it seems that you've got logic which messes with line speeds, then you should be calling tmxr_set_port_speed_control() in your reset routine.

The tmxr_set_line_unit() and tmxr_set_line_output_unit() comments state, "Only devices which poll on a unit different from the unit provided at MUX attach time need call this function." So in my case, this function wouldn't need to be called because each unit schedules its own service routine?

Separate from this, the UNIT service routine talking to a MUX should always be calling sim_activate_xx rescheduling it's next callback just BEFORE returning, and NOT at the beginning of the service routine. This is so that whatever happened during the current run through the service routine is considered internally within TMXR to actually schedule the next activation at the best time and only using the sim_activate provided interval as a fallback when nothing tmxr special is needed (i.e. the specified interval should be relatively large so that arriving connections can be detected). There is ABSOLUTELY NO need for your code to mess with trying to plan when the callbacks should be scheduled.

This was done while trying to figure out why THROTTLE x/y isn't working right. I typically have the sim_activate_xx at the end of the service routine before returning.

Additionally, if a callback is happening when a received character has previously arrived, but it hasn't yet been consumed by instructions running within the simulator, then tmxr_poll_rx() shouldn't be called. If the simulator is ready for more input, then tmxr_poll_rx() should be called and tmxr_getc_ln() to see if anything new has arrived.

I should only be calling tmxr_poll_rx if the UART indicates its receive buffer is empty and is awaiting another byte.

On the transmit side, if there is data to go out, then tmxr_putc_ln() should be called AND only when SCPE_LOST or SCPE_OK is returned, should the code consider the data sent. Otherwise, you probably got SCPE_STALL as a return and the data to be transmitted should be used again on a future call to the service routine.

Pretty sure that's how my service routine works on the transmit side.

You could separate the transmit and receive activities to be done by separate simh UNITs. Such additional UNITs could merely be part of the UNIT array in the DEVICE, but have flags which simply were UNIT_DIS with separate service routines which was specific to the purpose (transmit or receive) of that unit. The transmit unit could be scheduled by a sim_activate call in the code path which is invoked when simulator instructions send data outbound, and when transmit activity for that line was determined to complete in the transmit service routine, the service routine would schedule an interrupt or indicate complete status in a CPU visible register and NOT reschedule itself..

Hmmm... so if JAIRS0 and 2 additional units with UNIT_DIS, I would have JAIRS00, JAIRS01 and JAIRS02, and each could have a separate service routine for receive, transmit, and status, and the serial port would still attach to JAIRS0? And in this case, I would use tmxr_set_line_unit and tmxr_set_line_output_unit to tell TMXR which unit handles which function?

I had always assumed that a unit with UNIT_DIS was disabled and couldn't be used.

The above comments have nothing to do with throttling.

I thought one of the issues I was having with TMXR and throttling was if I set a baud rate, TMXR tried to simulate the baud rate and I thought that might be messing up the service dispatch time. If I attach a serial port at 9600 bps, I don't need TMXR to simulate 9600 bps since the serial port will do that all by itself.

What is the purpose of tmxr_poll_tx()?

sim> show mux
Multiplexer device: JAIRS1, ModemControl=enabled, Output Unit: JAIRS1,
    Input Polling Unit: JAIRS1,
    attached to Connect=/dev/cu.usbserial-AL009KFH;38400-8N1, connected, sessions=0, Speed=38400 bps
Connected to serial port /dev/cu.usbserial-AL009KFH
 Connected 00:00:05
 Modem Bits: DTR RTS DCD CTS DSR 
  input (on) queued/total = 1/807
  output (off) queued/total = 0/10
  speed = 38400 bps
  bytes in buffer = 10

This is another issue I run into. Why would TMXR disable output when a serial port is attached? Is it possible to tell TMXR to just send and receive data, don't mess with data rate, flow control, modem status, etc.? All I want to do is send/receive data and control status lines from the sim.

Looking at your code in s100_jair.c, it seems you've got 3 simh DEVICEs: (JAIRP_SNAME, JAIRS0_SNAME, JAIRS1_NAME), each of these separate devices have independent UNITs (1 per DEVICE), but all of these share exactly the same unit service routine. I'm not really sure how all of this might actually be work, but I think you need some info about how TMXR expects things to work:

For a given MUX (with one or more lines), there are 3 things related to data that need to happen: a) Detecting newly arriving connections (done by calling tmxr_poll_conn()). b) Detecting data arriving from one or more ports. c) Transmitting data out a port.

Item a, can be done with frequency of 100's to 1000's of milliseconds (i.e. no rush). Item b should be done at an appropriate rate that corresponds to the line speed (or baud rate) of the port that has data Item c should be done when data is available to be transmitted and any prior data has completed transmission.

The simh UNIT that does each of these functions could be just by one UNIT or completely separate for each of the respective activities and ports on the MUX.

If it is all done by one UNIT, then when that UNIT's service routine runs it might busily call each of the respective TMXR routines tmxr_poll_conn, tmxr_poll_rx or tmxr_poll_tx, with possibly nothing actually happening in each of those routines except consumption of any number of system calls. It is your choice how to do this. However, you should reflect what choices you made by conveyng those choices to TMXR by calling tmxr_set_line_unit() and tmxr_set_line_output_unit(). These routines should be called in the DEVICE reset routine. Additionally, since it seems that you've got logic which messes with line speeds, then you should be calling tmxr_set_port_speed_control() in your reset routine.

The tmxr_set_line_unit() and tmxr_set_line_output_unit() comments state, "Only devices which poll on a unit different from the unit provided at MUX attach time need call this function." So in my case, this function wouldn't need to be called because each unit schedules its own service routine?

True, but there are efficiencies and the separation of the transmit & recieve logic may make things clearer.

Separate from this, the UNIT service routine talking to a MUX should always be calling sim_activate_xx rescheduling it's next callback just BEFORE returning, and NOT at the beginning of the service routine. This is so that whatever happened during the current run through the service routine is considered internally within TMXR to actually schedule the next activation at the best time and only using the sim_activate provided interval as a fallback when nothing tmxr special is needed (i.e. the specified interval should be relatively large so that arriving connections can be detected). There is ABSOLUTELY NO need for your code to mess with trying to plan when the callbacks should be scheduled.

This was done while trying to figure out why THROTTLE x/y isn't working right. I typically have the sim_activate_xx at the end of the service routine before returning.

Actually, the code I saw had the reschedule happening at the beginning of the service routine...

Additionally, if a callback is happening when a received character has previously arrived, but it hasn't yet been consumed by instructions running within the simulator, then tmxr_poll_rx() shouldn't be called. If the simulator is ready for more input, then tmxr_poll_rx() should be called and tmxr_getc_ln() to see if anything new has arrived.

I should only be calling tmxr_poll_rx if the UART indicates its receive buffer is empty and is awaiting another byte.

On the transmit side, if there is data to go out, then tmxr_putc_ln() should be called AND only when SCPE_LOST or SCPE_OK is returned, should the code consider the data sent. Otherwise, you probably got SCPE_STALL as a return and the data to be transmitted should be used again on a future call to the service routine.

Pretty sure that's how my service routine works on the transmit side.

Look closer. I don't think so, anytime (port->txp == TRUE) is true, you end up port->txp = FALSE, which should only be done IFF you got SCPE_LOST or SCPE_OK. You process the interrupt activity IFF an interrupt for the most recent output character had not been set.

You should call tmxr_poll_tx() after any time you got SCPE_OK. In the more complex case where a MUX device has many lines/ports tmxr_poll_tx() should be called if a character was successfully output (SCPE_OK) on any line.

You could separate the transmit and receive activities to be done by separate simh UNITs. Such additional UNITs could merely be part of the UNIT array in the DEVICE, but have flags which simply were UNIT_DIS with separate service routines which was specific to the purpose (transmit or receive) of that unit. The transmit unit could be scheduled by a sim_activate call in the code path which is invoked when simulator instructions send data outbound, and when transmit activity for that line was determined to complete in the transmit service routine, the service routine would schedule an interrupt or indicate complete status in a CPU visible register and NOT reschedule itself..

Hmmm... so if JAIRS0 and 2 additional units with UNIT_DIS, I would have JAIRS00, JAIRS01 and JAIRS02, and each could have a separate service routine for receive, transmit, and status, and the serial port would still attach to JAIRS0? And in this case, I would use tmxr_set_line_unit and tmxr_set_line_output_unit to tell TMXR which unit handles which function?

Yes.

I had always assumed that a unit with UNIT_DIS was disabled and couldn't be used.

A unit with UNIT_DIS is not accessible from commands at the sim> prompt, unless it also has UNIT_DISABLE which then means that sim> prompt will allow the unit to be enabled which will clear UNIT_DIS. Note that I recommended UNIT_DIS and explicitly didn't say UNIT_DISABLE.

The above comments have nothing to do with throttling.

I thought one of the issues I was having with TMXR and throttling was if I set a baud rate, TMXR tried to simulate the baud rate and I thought that might be messing up the service dispatch time. If I attach a serial port at 9600 bps, I don't need TMXR to simulate 9600 bps since the serial port will do that all by itself.

Not true. The port speed controls the precise timing of when tmxr_getc_ln() will successfully return input, and when tmxr_putc_ln() will accept output. simh can't actually know when output to physical ports succeeds. That detail is buried in physical device driver info deep in the host OS kernel. TMXR sets the port speed to the specified rate and then outputs precisely at that rate.

sim> show mux
Multiplexer device: JAIRS1, ModemControl=enabled, Output Unit: JAIRS1,
Input Polling Unit: JAIRS1,
attached to Connect=/dev/cu.usbserial-AL009KFH;38400-8N1, connected, sessions=0, Speed=38400 bps
Connected to serial port /dev/cu.usbserial-AL009KFH
Connected 00:00:05
Modem Bits: DTR RTS DCD CTS DSR
input (on) queued/total = 1/807
output (off) queued/total = 0/10
speed = 38400 bps
bytes in buffer = 10

This is another issue I run into. Why would TMXR disable output when a serial port is attached? Is it possible to tell TMXR to just send and receive data, don't mess with data rate, flow control, modem status, etc.? All I want to do is send/receive data and control status lines from the sim.

The "off" output indicator says that the xmte (Transmit Enable) status for the line is off.

Legacy TMXR DEVICEs directly examined the xmte variable for the line status and DIDN'T look at the status returned from tmxr_putc_ln().

For example:

/* Terminal output service */

t_stat dco_svc (UNIT *uptr)
{
int32 c;
int32 ln = uptr - dco_unit;                             /* line # */

if (dcx_ldsc[ln].conn) {                                /* connected? */
    if (dcx_ldsc[ln].xmte) {                            /* tx enabled? */
        TMLN *lp = &dcx_ldsc[ln];                       /* get line */
        c = sim_tt_outcvt (dco_buf[ln], TT_GET_MODE (dco_unit[ln].flags));
        if (c >= 0)                                     /* output char */
            tmxr_putc_ln (lp, c);
        tmxr_poll_tx (&dcx_desc);                       /* poll xmt */
        }
    else {
        tmxr_poll_tx (&dcx_desc);                       /* poll xmt */
        sim_activate (uptr, dco_unit[ln].wait);         /* wait */
        return SCPE_OK;
        }
    }
dco_csr[ln] |= CSR_DONE;                                /* set done */
if (dco_csr[ln] & CSR_IE)                               /* ie set? */
    dco_set_int (ln);                                   /* req int */
return SCPE_OK;
}

Having DEVICE code look into the TMXR data structures (rather than leveraging the APIs) is not preferable.

The xmte being on suggests that a tmxr_putc_ln() will have some place to put an output character. There might indeed be a place to put the character which will be precisely determined when tmxr_putc_ln() any way since that will review the line state and adjust the state as needed.

Everything seems to work fine, as long as I don't use SET THROTTLE x/y. As soon as I use that command, nothing seems to work right in my service routine because everything is significantly delayed.

There are definitely improvements I can make based on what you've written above, but I guess I should get back to original issue with THROTTLE.

I am attempting to load CP/M over a serial port using Mike Douglas' FDC+ serial server on a JAIR. Since I don't have a JAIR, I emulated it. Much of this software assumes a 2MHz 8080. If it use SET THROTTLE 250K and start the sim with a simple loop until THROTTLE initializes, I can boot CP/M and things are happy.

I am trying to eliminate having to run a loop, wait for throttle to figure things out, stop and restart the sim with the boot loader.

This is about as simple a test I can think of. I do not know why the amount of time between service calls changes.

static t_stat null_reset(DEVICE *dptr)
{
    sim_printf("%s: reset\n", DEV_NAME);

    return SCPE_OK;
}

static t_stat null_svc(UNIT *uptr)
{
    static unsigned int ticks = 0;
    static unsigned int msec = 0;

    if (msec) {
        sim_printf("%s: %dms\n", (++ticks & 1) ? "TICK" : "TOCK", sim_os_msec() - msec);
    }

    msec = sim_os_msec();

    sim_activate_abs(uptr, 100 * 100000);

    return SCPE_OK;
}

static t_stat null_attach(UNIT *uptr, CONST char *cptr)
{
    DEVICE *dptr;
    NULL_INFO *info;
    t_stat r;

    dptr = find_dev_from_unit(uptr);
    info = (NULL_INFO *) dptr->ctxt;

    if ((r = tmxr_attach(info->tmxr, uptr, cptr)) == SCPE_OK) {
        sim_printf("%s: attached '%s' to interface.\n", dptr->name, cptr);
    } else {
        sim_printf("%s: attach '%s' failed %d.\n", dptr->name, cptr, r);
    }

    r = sim_activate_abs(uptr, 100 * 100000);

    return r;
}

SET NULL ENA
SET SIO NOSLEEP
SET THROTTLE 250K

ATTACH NULL CONNECT=/dev/cu.usbserial-AL009KFH

DEP 00 C3
DEP 01 00
DEP 02 00

G 0  
Altair 8800 (Z80) simulator Open SIMH V4.1-0 Current        git commit id: 7539b112+uncommitted-changes
NULL: reset
NULL: reset
NULL: reset
NULL: reset
NULL: attached 'CONNECT=/dev/cu.usbserial-AL009KFH' to interface.
TICK: 67ms
TOCK: 67ms
TICK: 66ms
TOCK: 66ms
TICK: 66ms
TOCK: 67ms
TICK: 66ms
TOCK: 66ms
TICK: 66ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 66ms
TOCK: 67ms
TICK: 66ms
TOCK: 66ms
TICK: 67ms
TOCK: 65ms
TICK: 67ms
TOCK: 66ms
TICK: 66ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 66ms
TOCK: 66ms
TICK: 67ms
TOCK: 66ms
TICK: 66ms
TOCK: 66ms
TICK: 66ms
TOCK: 67ms
TICK: 35284ms
TOCK: 40978ms

SET THROTTLE 250/1

Altair 8800 (Z80) simulator Open SIMH V4.1-0 Current        git commit id: 7539b112+uncommitted-changes
NULL: reset
NULL: reset
NULL: reset
NULL: reset
NULL: attached 'CONNECT=/dev/cu.usbserial-AL009KFH' to interface.
TICK: 51118ms
TOCK: 50634ms
TICK: 50424ms

@deltecent: Just for S&G's, try adding AIO_CHECK_EVENT in your sim_instr loop (vax does this before interpreting the current instruction.) I don't see it referenced directly in your code and may be the missing piece. vax_cpu.c code extract at line 658:

    AIO_CHECK_EVENT;                                    /* queue async events */
    if (sim_interval <= 0) {                            /* chk clock queue */
        temp = sim_process_event ();
        if (temp)
            ABORT (temp);
        SET_IRQL;                                       /* update interrupts */
        }