Programming delays in Z80 assembler

ZXNet echo conference «code.zx»

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_1 .W ══════════════════ (c) Ivan Roshchin, Moscow Fido: 2:5020/689.53 ZXNet: 500:95/462.53 E-mail: asder_ffc@softhome.net WWW: http://www.ivr.da.ru Programming delays in Z80 assembler ═════════════════════ ══════════════════════ ("Radiomir. Your computer" 3/2002) When writing programs for low-level work with external devices there is often a need to program delay for a certain time. To do this you can either use a timer (if you have one), or insert it into the program fragment whose execution time is equal to the required time delays. I will not consider the first method here, but about I’ll tell you more about the second one. Each command of the Z80 processor takes a certain amount of time to complete. number of cycles. Knowing the required delay in clock cycles, you can write a program fragment with the appropriate time execution. (I’ll immediately note that such a fragment should work when interrupts are disabled.) Formula for calculating the delay time in clock cycles: N=F*t where N is the number of clock cycles, F - processor clock frequency, t - delay time. Since the command execution time in the Z80 processor can be expressed only in an integer number of cycles, the resulting result should be rounded to the nearest whole number.Example: let's say you need a delay of 5 ms, a clock frequency of 3.5 MHz. Then the required number of cycles is: 5*10^-3 s * 3.5*10^6 Hz = 17500. Let's start by programming small delays. Minimum the delay value corresponds to the shortest execution time Z80 commands - 4 cycles. For a 4-cycle delay, the NOP command is ideal - "no operations." Its execution does not depend on the values of the flags and registers, and it does not perform any actions (in other words, has no side effects). Using a chain of NOPs it is easy to get a delay of 8, 12, 16,... cycles (i.e. for a number of the form 4N). A delay of 5 clock cycles can be obtained using the command conditional exit from the subroutine in the case when the condition is not is running. There are eight such teams: RET NC (output at C=0) RET C (output at C=1) RET NZ (output at Z=0) RET Z (output at Z=1) RET P (output at S=0) RET M (output at S=1) RET PO (output at P/V=0) RET PE (output at P/V=1) They have no side effects. By adding NOPs, you can get a delay of 9, 13, 17,... cycles (i.e. by a number of the form 4N+1). Obviously, to use one of the above commands need to know the state of at least one of the fourflags (C, Z, S or P/V) by the time it is executed. If the condition flags are unknown, then a delay of 5 clock cycles cannot be obtained, but at 9, 13, 17,... - you can. To do this, you must first set the flag transfer using the SCF command (its execution time is 4 clock cycles), then use the RET NC command, and then when need to add NOPs. Setting the carry flag will be in in this case a side effect. A delay of 6 cycles can be obtained using one of the following commands: INC BC DEC BC INC DE DEC DE INC HL DEC HL INC SP DEC SP By adding NOPs, you can get a delay of 10, 14, 18,... cycles (i.e. for a number of the form 4N+2). The above commands do not affect the flags, but they do change the contents of the corresponding register pair. If none of four pairs cannot be changed, then you will get a delay of 6 clock cycles it is impossible. But at 10, 14, 18,... cycles - you can, using the command unconditional transition to the next command (JP $+3), on the execution of which takes 10 cycles, and added when necessary NOP commands. The 7-cycle delay can be achieved in various ways. One of them is to use the LD r,(HL) command, where r is one of registers A,B,C,D,E,H,L. This command does not affect flags, but changes the contents of the register involved in the operation. If notone register cannot be changed, but flags can be changed, then The CP (HL) team is suitable. If neither registers nor flags, - you can use the conditional jump command JR (if unfulfilled condition). There are four such teams: JR NC (transition at C=0) JR C (jump at C=1) JR NZ (transition at Z=0) JR Z (transition at Z=1) Naturally, in order for one of them to be used, it is necessary to know the state of flag C or Z at the time of its execution. By adding NOPs, you can get a delay of 11, 15, 19,... cycles (i.e. for a number of the form 4N+3). ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_2 .W ══════════════════ So, based on the above, how can we build section of the program that provides the required delay? Let x - the amount of delay in clock cycles. Divide x by 4, get the quotient a and remainder b. Then b+4 is the length of the first command (4, 5, 6 or 7 clock cycles), and a-1 is the number of added NOPs. Let's look at this with examples. Example 1: x=18. Divide by 4: a=4, b=2. So the length of the first commands will be 2+4=6 clock cycles, and the number of NOPs will be 4-1=3. The corresponding program fragment will look like this: INC HL NOP NOP NOP Example 2: x=29. Divide by 4: a=7, b=1. So the length of the first commands - 5 clock cycles, number of NOPs - 6. As before mentioned, a delay of 5 clock cycles is implemented by a conditional RET, and to use it, you need to know the state of at least one of four flags. Let's assume that there is nothing about the state of the flags known. Then you have to do this: using command sequence SCF: RET NC we get a delay of 9 clock cycles, and there will be one less NOP. As a result we get: SCF RET NC NOP NOP NOP NOP NOP Since, generally speaking, NOPs with such programming there may be quite a lot of delays, so don’t"inflate" the program text and not make mistakes in counting NOPs when typing, it is convenient to use the DS N assembler directive (another recording option is DEFS N). It allocates a piece of memory is N bytes long and (usually) pads it with zeros. Well, 0 is This is precisely the NOP command code. Just make sure that the assembler you use fills the area really zeros. If this is not the case, you will have to specify a null value explicitly: DS N,0. Thus, the examples discussed above can be written like this: INC HL DS 3 and SCF RET NC DS 5 If you are unhappy that a long chain of NOPs takes up there is a lot of memory space, then you can use the following fragment programs: LD B,N DJNZ$ Its execution time is 13N+2 cycles (where N=1..255). So Thus, if you need a delay of x clock cycles, divide x-2 by 13, we get the quotient a and the remainder b. The value a shows how much times the loop needs to be repeated (this is the number loaded into register B), and b - how many clock cycles remain (delay for this time will have to be implemented in the usual way). If b=1, 2 or 3 then, since it is impossible to get a delay for this time, you will have to decrease a by 1 and increase b by 13 (thereby increasing remaining delay up to 14, 15 or 16 clock cycles). Example: x=140. Divide by 13: a=10, b=8. Hence,the number of repetitions of the cycle will be 10; delay for the remaining 8 cycles are provided using two NOP instructions. We get: LD B,10 DJNZ$ DS 2 ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_3 .W ══════════════════ Now let's move on to considering ways to implement more long delays. For delays from 188 to 65535 cycles it is convenient use the following 98 byte procedure: ORG #8000 ;or other address divisible by 256 TAB_ADR DB ADR_0256 DB ADR_1256 .............. DB ADR_31256 ADR_28 NOP ADR_24 NOP ADR_20 NOP ADR_16 NOP ADR_12 NOP ADR_8 NOP ADR_4 NOP ADR_0 NOP ;4 clock cycles RET ADR_29 NOP ADR_25 NOP ADR_21 NOP ADR_17 NOP ADR_13 NOP ADR_9 NOP ADR_5 NOP ADR_1 RET NZ ;5 cycles RET ADR_30 NOP ADR_26 NOP ADR_22 NOP ADR_18 NOP ADR_14 NOP ADR_10 NOP ADR_6 NOP ADR_2 INC HL ;6 clock cycles RET ADR_31 NOP ADR_27 NOP ADR_23 NOP ADR_19 NOP ADR_15 NOP ADR_11 NOP ADR_7 NOP ADR_3 LD A,(HL) ;7 clock cycles RET WAIT LD DE,-156 ADD HL,DE LD A,L AND 31 LD E,A ;To divide HL by 32, just shift HL by 5 bits ;to the right, but there is a shorter and faster way: move HL to ;3 bits to the left, placing the most significant bits of the result in ;accumulator, and then rearrange: A -> H -> L.XOR A ADD HL,HL R.L.A. ADD HL,HL R.L.A. ADD HL,HL R.L.A. LD L,H LD H,A ;Loop with execution time 32 cycles: WAIT_1 DEC HL LD A,H OR L NOP ;for NOP ;delays JP NZ,WAIT_1 EX DE,HL LD H,TAB_ADR/256 LD L,(HL) JP(HL) Let me remind you that the operator "" is the calculation of the remainder of division. In the above procedure it is used to get the low byte of a double-byte value. If in the used your assembler does not support this operator, then perhaps there is a special operator for calculating the low byte, or the assembler itself discards the high byte when the result must be single byte. As a last resort, you can use equality A256=A-((A/256)*256). ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_4 .W ══════════════════ Required delay duration in clock cycles (from 188 to 65535) is specified in the HL register pair before the call procedures. It is not necessary to specifically take into account that execution of commands for loading this number into HL and calling the procedure will also take some time: this is already taken into account in the procedure. If, say, at some point in the program a delay of 500 cycles, just write the following: LD HL,500 CALL WAIT As we can see, the use of this procedure does not require programmer with special knowledge about command execution time. Not you need to think every time what sequence of commands implement one or another delay. We put the required number and that’s it! It couldn't be simpler! This procedure is also very convenient to use if the exact the delay value is not known in advance, and you have to “adjust” it in the process of debugging the program. You can change the delay time by changing only one number in the program code using a debugger. During its operation, the procedure changes the values of register pairs HL and DE, as well as the accumulator and flag register. If this is undesirable, then, depending on the situation, you can either save them on the stack and then restore them (with the PUSH commands, POP), or set for the duration of the procedurean alternative set of registers if there is nothing there desired (by commands EXX, EX AF,AF'). At the same time, from what is indicated in HL delay time will need to subtract command execution time saving and restoring registers (make sure to the indicated delay time has not become less than the minimum - 188 bars!). Let me remind you: the execution time of the commands PUSH HL, PUSH DE, PUSH AF - 11 bars; POP HL, POP DE, POP AF - 10 bars; EXX, EX AF,AF' - 4 bars. Example: let's say we need a delay of 750 clock cycles, and the values of HL and DE should not be corrupted (alternative registers too). So, HL and DE must first be stored on the stack (PUSH HL, PUSH DE), and after calling the procedure - restore (POP DE, POP HL). We calculate the execution time of these commands: 11+11+10+10=42 clock cycles. In the program we write: PUSH HL ;save PUSH DE ;registers LD HL,750-42 CALL WAIT POP DE ;recovery POP HL ;registers Since the WAIT procedure is universal, in case single use, it will obviously take up more space in memory than specifically written for a specific implementation delay program fragment. But if the procedure is called in many places in the program, with different delay times, then this will already be more profitable compared to if in every there was a fragment of the program that implemented the delay.The WAIT procedure could, in principle, be made shorter and do not bind to an address that is a multiple of 256. But when writing it I strived to ensure that the minimum delay achieved with its help was as little as possible, i.e. to increase the area use of the procedure. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_5 .W ══════════════════ Below is the text of another procedure for implementation longer delays (from 469 to 2^32-1 cycles). WAIT_LONG PUSH BC PUSH AF LD BC,-405 ADD HL,BC LD BC,-1 EX DE,HL ADC HL,BC EX DE,HL LD A,L AND 3 ADD A,A ADD A,A ;*4 ADD A,WAIT_CODE256 LD C,A LD A,WAIT_CODE/256 ADC A,0 LD B,A PUSH BC LD A,L RRA RRA CPL AND 15 ADD A,WAIT_NOP256 LD C,A LD A,WAIT_NOP/256 ADC A,0 LD B,A PUSH BC ;Shift DEHL: XOR A ADD HL,HL EX DE,HL ADC HL,HL EX DE,HL R.L.A. ADD HL,HL EX DE,HL ADC HL,HL EX DE,HL R.L.A. LD L,H LD H,E LD E,D LD D,A ;In DE - the number of repetitions of the cycle (each - 64 cycles). LD BC,-1 WAIT_L1 ADD HL,BC INC L ;Checking: L=0? DEC L ;(without changing the C flag). JP Z,WAIT_L2EX DE,HL ADC HL,BC EX DE,HL JR WAIT_L1 WAIT_L2 LD A,H ;L=0, and HDE=0? OR D ;Flag C is reset. OR E NOP ;For NOP ;delays RET C ;in 4+4+5=13 clock cycles. JP NZ,WAIT_L1 RET ;Go to the address previously ;pushed onto the stack. WAIT_NOP DS 15.0 ;15 NOP commands. RET ;Go to the address previously ;pushed onto the stack. WAIT_CODE NOP JP WAIT_EXIT RET C JP WAIT_EXIT INC HL JP WAIT_EXIT LD A,(HL) JP WAIT_EXIT WAIT_EXIT POP AF POP B.C. RET The length of the procedure is 119 bytes. Required duration the delay in clock cycles is indicated as follows: the most significant bits are loaded to the register pair DE, the lower ones to HL. As in the previous one procedure, execution time of loading and calling commands specifically does not need to be taken into account. Example: let's say you need a delay of 100,000 clock cycles. B in hexadecimal it is #186A0. In the program we write: LD HL,#0001 LD DE,#86A0 CALL WAIT_LONG ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.

From Ivan Roshin To All 2 April 2002

Hello, All! ═══════════════════ wait_6 .W ══════════════════ If the program already uses the WAIT procedure for delays less than 469 clock cycles, and the amount of required delay is not is many times higher than 65535, then you can do without the procedure WAIT_LONG (thereby saving in program size), simply placing several calls to the WAIT procedure side by side. Example: you need a delay of 150,000 clock cycles. Divide 150,000 by 65535, we get the quotient 2 and the remainder 18930. In the program we write: LD HL,65535 CALL WAIT LD HL,65535 CALL WAIT LD HL,18930 CALL WAIT Finally, I note that the WAIT and WAIT_LONG procedures are not contain self-modifying areas; therefore it is possible their firmware is in ROM. ════════════════════════ ════════════════════════ Best regards, Ivan Roshchin.