4

I make a homebrew (for hobby) game console on Z80A, and I need help.
Now I'm stuck - I need to be able to count milliseconds and microseconds in the code. For example, how it is done in Arduino :

millis() // function - returns the number of milliseconds since the start of the program
micros() // function - the same, but returns microseconds.

I tried to solve the problem myself, here are the ways:

Way 1: Use two different functions in which the code runs exactly 1 millisecond and 1 microsecond respectively. After this function code, the values of the variables increase: globalMilliseconds and globalMicroseconds, respectively. But how often to call it and where? For example in this code:

void gameCycle(){
    UpdateInput();
    UpdateLogic();
    Draw();
}

while(true){
    gameCycle();
}

Way 2: Use external timer circuit. There are two options.

Way 2.1 to pin a non-masked interrupt, attach a crystal with a frequency of 10KHz (not sure about this value, but with another value idea is same), and in the non-masked interrupt handler, increase the value of the variable globalMicroseconds. That is, each crystal tick - means that one microsecond has passed.

Way 2.2 use a programable timer chip, as is done in many examples of homemade Z80. For example, Z80CTC or Intel 8255 / 8254. But it can be done in different ways. At the moment I see it this way: the timer is connected to an interrupt of Z80, when the timer is triggered, I increase the value of the variable globalMicroseconds, and restart the timer. The timer works on its own crystal.

Wouldn’t there be a delay in the I / O code? Because interrupt timer should be processed at any point of any code, including I / O?

3
  • 3
    1 microsecond is hard to handle on a Z80A. with a 10MHz clock you will have a executions speed of only a few instructions per micro second (10 clock cycles per microsecond, and every instruction take at least 3 or 4 cycles)
    – UncleBod
    Jun 11, 2019 at 14:04
  • 3
    On the Z80 I played with back then there was a video refresh interrupt 50 times a second which allowed me to implement a clock. Perhaps that is feasible for you too? Jun 11, 2019 at 22:21
  • Update: one of the options works. Option with an external counter, but instead of the counter chip - a small circuit (frequency generator, counter, trigger). Circuit Example
    – Alex
    Jun 12, 2019 at 19:54

5 Answers 5

4

Now I'm stuck - I need to be able to count milliseconds and microseconds in the code.

Forget about microseconds. Single instructions on a Z80 are already in the range of 1 µs or even longer, depending on the clock speed used.

For fast CPUs it's fine to use some 'standard' value; for classic CPUs it's more appropriate to use a unit that fits your game design. Like frames. After all, no reaction can be counted and shown to the user faster than a frame takes. That would make something like 1/50th or 1/60th of a second. For practical game use even 1/10th is still good.

If there are no frames, create them. Like with your interrupt approach.

For example, how it is done in Arduino :

millis() // function - returns the number of milliseconds since the start of the program
micros() // function - the same, but returns microseconds.

To do exactly the same you'd need an external counter (CTC), running continuously and in parallel with the Z80. After all, the Z80 is, much like the Arduino's AVR, a single CPU: not able to count and work at the same time.

I tried to solve the problem myself, here are the ways:

Way 1: Use two different functions in which the code runs exactly 1 millisecond and 1 microsecond respectively. After this function code, the values of the variables increase: globalMilliseconds and globalMicroseconds, respectively. But how often to call it and where?

Won't work. After all, it would just count the time the CPU is within these functions, so no time for anything else.

Way 2: Use external timer circuit. There are two options.

Way 2.1 to pin a non-masked interrupt, attach a crystal with a frequency of 10KHz (not sure about this value, but with another value idea is same),

A bit high, see before. Rather use your frame rate - or 1/100th - as base. Put your main program on hold and have the interrupt release it from the halt for one game cycle. Maybe increment the timer value every such start, giving you all the precision you need.

If there is no frame source, use the CPU clock to do so.

Way 2.2 use a programmable timer chip, as is done in many examples of home-made Z80. For example, Z80CTC or Intel 8255 / 8254. But it can be done in different ways. At the moment I see it this way: the timer is connected to an interrupt of Z80, when the timer is triggered, I increase the value of the variable globalMicroseconds, and restart the timer. The timer works on its own crystal.

Works fine, except it will have to be milliseconds, as servicing an interrupt on the Z80 will take considerably more than a microsecond, and thus using more then 100% of CPU time - no time left for the game and the clock will run behind. :))

Wouldn’t there be a delay in the I / O code? Because interrupt timer should be processed at any point of any code, including I / O?

Sure, but it's only a small part. Of course, this depends on the rest of your system. If there are timing-critical sections the approach is problematic.

So best might be Way 2.3:

If the system has a frame counter, use it as an interrupt source for synchronisation. Otherwise use a CTC in reload mode to synchronize the game look to a useful time slice - anywhere between 1/10th and 1/100th of a second. Slower will make negative effects, faster brings no gain (can't be noticed by player).

Base line: Make your program fit the system, not force the system to your program.

(Alternative Motto: Just make it as good as needed, not more)

6

Way 2.1 to pin a non-masked interrupt, attach a crystal with a frequency of 10KHz (not sure about this value, but with another value idea is same), and in the non-masked interrupt handler, increase the value of the variable globalMicroseconds. That is, each crystal tick - means that one microsecond has passed.

Way 2.2 use a programable timer chip, as is done in many examples of homemade Z80. For example, Z80CTC or Intel 8255 / 8254. But it can be done in different ways. At the moment I see it this way: the timer is connected to an interrupt of Z80, when the timer is triggered, I increase the value of the variable globalMicroseconds, and restart the timer. The timer works on its own crystal.

This is not a complete answer, but I'd like to mention what you've described is not "cheating", it's the standard way to count time. If you want to count time in the most generic and flexible manner, using an external timer, like a Z80-CTC is the way to go. And ultimately, this is exactly how millisecond-counting is implemented in Arduino internally as well!

One thing to keep in mind is the fact that an Atmega chip is a microcontroller, in other words, a microprocessor core with a bunch of builtin peripherals, while the Z80 is only a microprocessor. Please take a look of the block diagram of an Atmega328P.

Atmega328P block diagram

As, you can see, an Atmega328P microcontroller is pretty much a complete computer on a chip: there's a AVR microprocessor with its own flash and SRAM, but there are also 8-bit and 16-bit timers attached to the system bus, not too different from a Z80 with an external timer chip.

In the Arduino development environment, the function millis() is controlled by interrupts generated by 8-bit timer 0. The complete implementation is available in hardware/cores/arduino/wiring.c.

Here's an overview of Arduino's implementation. I hope it helps.

First, the global variables are defined as counters,

volatile unsigned long timer0_overflow_count = 0;
volatile unsigned long timer0_millis = 0;
static unsigned char timer0_fract = 0;

The init() function is called to enable the Timer0 interrupt. This occurs before Arduino executes the setup() function.

void init()
{
    // set timer 0 prescale factor to 64
#if defined(__AVR_ATmega8__)
    sbi(TCCR0, CS01);
    sbi(TCCR0, CS00);
#else
    sbi(TCCR0B, CS01);
    sbi(TCCR0B, CS00);
#endif

    // enable timer 0 overflow interrupt
#if defined(__AVR_ATmega8__)
    sbi(TIMSK, TOIE0);
#else
    sbi(TIMSK0, TOIE0);
#endif
}

The interrupts from Timer0 is handled by function TIMER0_OVF_vect, which updates the global variables corresponding to timing.

SIGNAL(TIMER0_OVF_vect)
{
    // copy these to local variables so they can be stored in registers
    // (volatile variables must be read from memory on every access)
    unsigned long m = timer0_millis;
    unsigned char f = timer0_fract;

    m += MILLIS_INC;
    f += FRACT_INC;
    if (f >= FRACT_MAX) {
        f -= FRACT_MAX;
        m += 1;
    }

    timer0_fract = f;
    timer0_millis = m;
    timer0_overflow_count++;
}

When millis() is called, the function simply copies and returns the global counter. Before reading the global counter, millis() also temporally disables the interrupts to prevent the global counter from incrementing while reading them.

unsigned long millis()
{
    unsigned long m;
    uint8_t oldSREG = SREG;

    // disable interrupts while we read timer0_millis or we might get an
    // inconsistent value (e.g. in the middle of a write to timer0_millis)
    cli();
    m = timer0_millis;
    SREG = oldSREG;

    return m;
}

Furthermore, the delayMicroseconds() function in Arduino is implemented through cycle-counting, but the millisecond-delay function is simply a busy-loop.

void delay(unsigned long ms)
{
    unsigned long start = millis();
    
    while (millis() - start <= ms)
        ;
}

In principle, if you add an external timer to your Z80 computer, you can implement an interrupt handler and provide an assembly routine to return the current value of your counter just like how Arduino does it. Although it's more difficult if you want low overhead and high resolution.

5

You have correctly surmised the two approaches to measuring time, not just on the Z80 but in computers in general.

  1. With a processor like the Z80 where every instruction takes a known number of cycles, you can simply count cycles to keep track of time. All branches, subroutines and loops in your code must be accounted for in the cycle count. It's possible, but such programming is also infamous for its complexity.

  2. Use an external device with a clock. This can be a device with a register which increments with time, or a device which interrupts the processor at a regular interval, or both (like most programmable timers). If you have a timer on the system, certainly use it.

2

your best bet would be to hook up binary counter directly to your CPU clock and use it as divider that creates interrupt ...

For example if you got 4.0MHz and want ~1ms resolution then divide the clock by 2^12 so use 11th bit of counter (counting from zero) ... that will divide the clock by 4096 resulting in roughly 1ms interrupt ...

Then just write ISR that increments your global time value and or call time related code...

There are also decadic counters out there if you need better match to 1ms or just select proper crystal vs divider. You can also adjust the difference by SW adding or removing the increment once in a while so average rate would still match 1ms...

AVRs and even x86 from Pentium have this counter integrated directly on chip and its value is available as register. However it does not create interrupt so you need polling if you want to use it but because of it (no interrupts) the resolution is better (but of coarse you only measure more accurately and can not schedule tasks so fast ...).

So if you really want ~1us resolution you need at least 1MHz source clock and access the counter output as I/O or memory address with a decoder. Again CPU clock is ideal for this.

0

Other answers deal with millisecond resolution. If you really, really need microseconds, you can use the R register, which is increased every M1 cycle, that is roughly one opcode fetch - depending on RAM wait states, it can be somewhat reliable (though it overflows on 7 bits, so you have to fit your measurement within this interval or compensate accordingly if you know how many instruction were executed).

1
  • 6
    M1 cycles don't occur on a regular schedule; instructions take a varying number of cycles to complete, so you can't get a reliable measurement using R. Jun 11, 2019 at 16:51

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .