0
\$\begingroup\$

So as the title says, I need exact delays, not too long, ideally in the range of 0 - 350 CPU clocks, but if anything would work in narrower range the absolute minimum range is 20 - 127 CPU clocks. So these are under or just above single micro-second delays (50MHz CPU clock), relatively short several clocks to several tens of clocks. The problem with polling a timer, is that the precision results in a step of 7 clocks at max, depending on the implementation, for example:

  1. while(!TF0) {} While, not, and bit operator, all together take 7 clocks. So if I call anything in between the 15-21 clocks, it will result in the flat delay of 21 clocks ...
  2. Using interrupt on timer and CPU stop mode - Gives good results for over 50 clocks, probably depends on current CPU condition, so sometimes goes way beyond 50 clocks, into 100 clocks range due to the interrupt and wake-up latency, but anything below again flat 50 (or 100) CPU clocks.
  3. Using switch-case, with for example 30 entries for 30 delays with 1 clock increment, having different number of NOPes as a delay, results in compiler optimization that makes it unpredictable in terms of timing and majorly too long again, over 100 clocks. This renders the approach unusable.
  4. I am planning to try table of pointer to functions with different number of NOPes. But before I try I already see two problems to that approach: a. it will require a lot of memory and I have 1k left; b. the latency of a void function(void) in and out is around 18 clocks so it is very very tight to meet the absolute minimum of 20 clocks I need ...

How to approach this type of problem? Any ideas will be more than welcome?

By the way, I run it on C8051F38x microcontroller from Silicon Labs, using C51 and Keil to code and compile if that matters.

The code that emerged as a partial solution, seems like it is follows the same timing while loop in C does, and the "djnz" instruction takes 5-6 cycles of CPU, instead of the datasheet stated 2/4.

ACC_save=   ACC;
ACC     =   counter102;
P0b3    =   1;  //             Start the Pulse
#pragma ASM     //             Precice DELAY using assembler
clr C           //  ; 1       Clear Carry
rrc A           //  ; 1       C = 1 if odd
jnc even        //  ; 2 or 4  extra 2 cycles if branch taken (spoils cache)
nop             //  ; 1
nop             //  ; 1
clr   C         //  ; 1
even:
subb  A,#4      //  ; 1
mov   R7,A      //  ; 1
loop:
djnz  R7, loop  //  ; supposed to be 2, but practically takes 5 to 6 cycles!
#pragma ENDASM`
P0b3    =   0;  //             Stop the Pulse

EDIT

Thank you very much every one for great input, I couldn't imagine that the flow of ideas could be so positive and most important productive. So my deep appreciations to all who contributed, and will contribute in the future. So after your valuable input guys, and great ideas I came up with something that works for me, to some extent. The code is below:

void    delay(unsigned char delay_time) {
switch (delay_time)
{case     8:    goto     Q08;
case      9:    goto     Q09;
case     10:    goto     Q10;
case     11:    goto     Q11;
case     12:    goto     Q12;
case     13:    goto     Q13;
case     14:    goto     Q14;
case     15:    goto     Q15;
case     16:    goto     Q16;
case     17:    goto     Q17;
case     18:    goto     Q18;
case     19:    goto     Q19;
case     20:    goto     Q20;
default      :  goto     Q00;   }

Q19:    PORT_ACTIVE(1); //  2clk
Q17:    PORT_ACTIVE(1); //  2clk
Q15:    PORT_ACTIVE(1); //  2clk
Q13:    PORT_ACTIVE(1); //  2clk
Q11:    PORT_ACTIVE(1); //  2clk
Q09:    PORT_ACTIVE(1); //  2clk
    _nop_();            //  1clk
goto EXIT1;             //  Skip the Even delay part

Q20:    PORT_ACTIVE(1); //  2clk
Q18:    PORT_ACTIVE(1); //  2clk
Q16:    PORT_ACTIVE(1); //  2clk
Q14:    PORT_ACTIVE(1); //  2clk
Q12:    PORT_ACTIVE(1); //  2clk
Q10:    PORT_ACTIVE(1); //  2clk
Q08:    PORT_ACTIVE(1); //  2clk
Q00:                    //  0clk
EXIT1:
return;                 //  Exit from the function takes 7 clocks
}   //  END of function delay

// Continued execution after the delay function
PORT_ACTIVE(0);     //  2clk

So PORT_ACTIVE(x) is a #define function that activates the pulsing port. Since I have all the time I need before I commence the pulse, I was able to squeeze in most of the overhead related with decisions before the actual activation of the port. Then, the return instruction is pretty much takes always the same amount of time so I am now able to generate a pulse with minimum of 8 clk cycles wide, and up to 20 cycles. I am now extending it up to 100 clocks, at the expense of the storage memory available of course. And so this solution is in fact thanks to the idea from JimmyB to drop the pulse activation into the function and not before it, and of course thanks to the great ideas by TCROSLEY, of how to manage the odd and even delays, its' just that switching to assembly is not really friendly to the debugging experience, and the code does much more than simpkle delays, and so I prefer to stay in C.

One more note, is that as soon as I finished celebrating a working solution, I hit the next problem.

SECOND PROBLEM

I need to execute a second pulse back to back to the first one with independent width. So no overhead for the second pulse, otherwise it will end up with varying width. It pretty much puts me back into the spot I was before, since the second pulse is again limited to the 6 cycles bottleneck of the while loop, unless there is a way to put the branching overhead for the second pulse before the first pulse ... Any ideas on that?

\$\endgroup\$
9
  • 1
    \$\begingroup\$ Will these delay times change dynamically during run time (i.e. variable parameter to the delay function), or can they be calculated at compile time (constant parameter to the delay function)? How many different calls to the delay function do you envision? \$\endgroup\$
    – tcrosley
    Commented May 8, 2016 at 17:34
  • \$\begingroup\$ Take a step back and describe the problem you're actually solving. If you need delays that are that precise, then a subroutine is probably NOT the right way to go about it. If you need to precisely control the times between two external events, you need to account for ALL of the code between the actual instructions associated with those events, including the call and return from your delay routine along with any other logic in the calling function. \$\endgroup\$
    – Dave Tweed
    Commented May 8, 2016 at 20:42
  • \$\begingroup\$ --- So to follow up on the problem a bit. \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 5:02
  • \$\begingroup\$ --The code comes to execute a precise pulse of certain width. There is a tune up procedure before the actual execution, where the MCU adjusts the width of the pulse based on some response of the system. So in general it goes like this: MCU receives pulse parameters START: Calculation of delays + inits PORT = 1 PRECISE DELAY PORT = 0 Result analysis Back to START if the results are not satisfactory, or continue to another spot in program execution … So the delay is dynamic. The number of calls to delay could be just 1 or end up to be thousands before the algorithm satisfies all the parameters. \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 5:18
  • \$\begingroup\$ To make such a dynamic delay, you have to make a delay subroutine with delay as minimum as possible and then call it as many times as the program requires. Here you have to consider the calling overhead delay, loop delay etc. I would say that you look out programs to generate delays in assembly and then write the code as per your requirement. \$\endgroup\$
    – Jasser
    Commented May 9, 2016 at 6:25

3 Answers 3

2
\$\begingroup\$

As others have mentioned, this is best done in assembly. Here is my original attempt at coding this, when I thought the jump instructions took either 2 or 4 cycles (see Edit below for the revised version).

void delay_sub(unsigned char i)
{
// convert 20, 21, 22 etc to count in R7 of 1, 2, 3 (extra cycle added if i is odd)
                    ; cycles
    rrc A           ; 1            c = 1 if odd
    jnc even        ; 2 or 4       extra 2 cycles if branch taken (spoils cache)
    nop             ; 1            delete if using lcall's instead of acall's
    nop             ; 1            same
    clc             ; 1            in either case carry is clear prior to subb
even:
    subb A,#9       ; 1

    mov R7,A        ; 1            R7 now = (i / 2) - 9
    //while (i--);
loop:
    djnz R7, loop   ; 2     loop address should be in cache, so no extra cycles needed
    ret             ; 6
}

timing calculation (assuming acall's)
if i even:
    5+7+R7*2+6 = minimum of 20 22 24 ... => R7 = 1, 2, 3 ...
if i odd:
    5+8+R7*2+6 = minimum of 21 23 25 ... => R7 = 1, 2, 3 ...

It assumes a call is made like ACALL(nn), where nn is a constant or a variable in a byte variable, so that the parameter can be passed using a one cycle MOV A,#n instruction for example. The minimum timing you can do is 20 clocks, as you asked for.

mov  A,#n        ; 1   
acall delay_sub  ; 4

There is no check that the parameter is greater than or equal to 20, any values less than 20 will give incorrect timing.

The mov instruction and acall will take 5 cycles. First off, the count (i) is divided by two to account for the DJNZ instruction taking two cycles. Then the count is adjusted to add a cycle if i is odd. Finally a fixed value is subtracted so the value in the register to be decremented (R7) is in the range 1, 2, 3 ... R7 is then decremented in a tight loop (two cycles per count). There is a fixed cycle count of 6 for the return.

If you have to use a LCALL instead of a ACALL, the minimum timing you can do will be 21 clocks instead of 20, and you will need to delete the two nop's after the jnc instruction. You have to use either all ACALL's or LCALL's, you can't mix them.

I would avoid using C to call the function unless you can guarantee the compiler doesn't add extra overhead. Also, I'm using R7 as a scratch register; your compiler manual will tell you what registers can be used inside an assembler function without having to save them (if any).

This also doesn't account for disabling and re-enabling interrupts, if necessary, to guarantee the timing routine will not be interrupted.

The behavior of the jump instructions are based on the datasheet for the C8051F38x as I understand it (in terms of when the instruction cache is spoiled or not). This may be different for other versions of the 8051.

Finally, I haven't shown the syntax for jumping into in-line assembly and back out again. The subroutine could also be put into a separate file and assembled.

Edit

Since I wrote the original code, the OP has informed me that the number of clock cycles for a jump in his 8051 is 5 or 6, not the 2 or 4 stated in the datasheet I read. So I have re-written the routine to take this into account. Unfortunately, this bumps the minimum cycle count that can be timed to 32 instead of 20. So if counts between 20 and 31 are absolutely needed to be handled, some special purpose code will need to be written specific to that case (see below).

void delay_sub(unsigned char i)
{
// minimum value of i is 32 
                    ; cycles
    clr C           ; 1
    subb A,#32      ; 1   adjust for overhead of call and this routine
                    ; a branch could be added here in case the result is negative
    mov B,#6        ; 1
    div AB          ; 4            quotient in A, remainder in B
    mov DPTR,#adjustcycles   ; 1
    mov R7,B        ; 1
    mov B,A         ; 1   save quotient in B as temp
    mov A,#6        ; 1
    clr C           ; 1
    subb A,R7       ; 1   A now has 5 - B (remainder)
    mov R7,#0       ; 1
    jmp @A+DPTR     ; 6   jump into table to add clocks based on remainder

adjustcycles:       ; execute additional cycles based on remainder
    inc R7          ; 1   for remainder of 5
    inc R7          ; 1   for remainder of 4 
    inc R7          ; 1   for remainder of 3 
    inc R7          ; 1   for remainder of 2
    inc R7          ; 1   for remainder of 1 
    nop             ; 1   for remainder of 0

    mov A,B         ; 1   now has i / 6, have already adjusted for remainder
loop:
    djnz loop       ; 6
    ret             ; 6

timing in clock cycles is: 5 (call) + 21 (fixed overhead) + 6*(i/6) + (i%6) + 6 (ret)

if i = 0, 5 + 21 + 6 = 32 therefore that is the minimum count

Instead of dividing the parameter i by 2 as in the previous example, I now have to divide it by 6 because I am assuming the DJNZ instruction takes 6 cycles. So we need to loop i / 6 times, and also add 0 to 5 cycles for the remainder (i % 6).

The remainder of my comments above pretty well apply to this example. I am leaving the original code, in case anyone actually has a 8051 with a two-cycle DJNZ instruction.

For counts of 20-31, you could create a subroutine with just one nop, that takes 12 cycles including the call and return:

void delay12(void)
{
    nop
}

For 20-23 counts, you would call it once plus adding 8 to 11 nops after the call (or a dummy jump to the next instruction which would eat up 6 cycles plus 2 to 5 nops -- so delaying 20 cycles would cost just four instructions plus the subroutine which is assumed to be used more than once.). For 24-31 counts, you would call delay12 twice, and add 0to 5 nops and/or a jump instruction as needed.

So to delay 20 cycles:

    acall delayl12
    jump next
next:
    nop
    nop
\$\endgroup\$
9
  • \$\begingroup\$ Thank you for the detailed answer. I took your code, and with slight modifications was able to run it inline using ASM pragmas in between the port commands. I was unable to execute it as a function, the linker went nuts about inability to link all the stuff. Two major problems with the inline execution. 1. Once switched to assembly, debugging is in assembly, simply impossible for my 15k code to be debugged. 2. Any jump instructions take 6 !!!!! yes 6 cycles instead of promised 2. I double checked the datasheet, it reads 2/4 cycles, but no it takes 6 !!! ??? How is that??? \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 10:20
  • \$\begingroup\$ Even the DJNZ in the tight loop takes six cycles? That's clearly not what the datasheet says. Are you actually running it in real hardware or a simulator? The only way to really test this is on real hardware. \$\endgroup\$
    – tcrosley
    Commented May 9, 2016 at 14:14
  • \$\begingroup\$ Yes, the DJNZ in the next loop takes ones 5 ones 6 cycles, it drives me nuts. Here is the code I use: 'ACC_save = ACC; ACC = counter102; P0b3 = 1; #pragma ASM // ; cycles clr C // ; 1 Clear Carry rrc A // ; 1 C = 1 if odd jnc even // ; 2 or 4 extra 2 cycles if branch taken (spoils cache) nop // ; 1 nop // ; 1 clr C // ; 1 subb A,#4 // ; 1 mov R7,A // ; 1 loop: djnz R7, loop // ; 2 #pragma ENDASM' \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 14:44
  • \$\begingroup\$ @cezar You don't need the clc before the rrc. The rrc will write over it. You have subb A,#4, should be #9. You didn't say whether you are testing with a simulator. I suspect you are. I don't trust them. You really need to have this as a function. Try putting the asm code in a separate file and use an assembler as I suggested in my answer. Then put 100 calls to the function in a row, and put that into a loop that executes 100000 times. (The reason for the 100 calls is to reduce the overhead to ~ 1%) Run this on real hardware. Should take very close to 4 seconds at 50 MHz. \$\endgroup\$
    – tcrosley
    Commented May 9, 2016 at 15:12
  • \$\begingroup\$ Hey Tcrosley, thanks for your quick response. I test on real hardware with a decent oscilloscope (tektronix MSO 5104 - 10GS/s) by my side, so what I see in the debugger I have a timing confirmation on oscilloscope with down to 1ns precision. With no CLR C, the RRC doesn't overwrite the carry for some reason, and I get huge numbers in A. I did recalculation for the timing due to the code being inline and not as a function, and changed "subb A,#4", so no there is no RET 6 cycels, and passing the variable to function ... \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 15:13
1
\$\begingroup\$

You could (I think, I don't remember the 8051 architecture that well) do a calculated jump into a 'sea' of nops. Perhaps combine it with a loop to reduce the required number of nops (or there may be a more sophisticated way to do it..)

I did that once in the earlier, but related, MCS-48 architecture to deal with variable latency or something similar.

You should work in assembly for single cycle precision. Keil supports a couple methods of using assembly in combination with C, and probably the simpler inline assembly would work for you.

There will be some overhead whatever you do, so the best you can do will be something like n+1 to n+255 cycles delay. n <= 20 cycles should be doable.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your input, I will look into putting some assembly code for time critical part. I was trying to avoid that so far, but looks like no go with C here. \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 5:19
0
\$\begingroup\$

Yes as Spehro pointed out you should work in assembly and you can get precision of upto one machine cycle using assembly but not a clock cycle.

Two ways using assembly language

  1. Use nop commands and loops in such a way that the commands and nops execution time, is your required time delay. This way you need to know how many machine cycles does each command takes in your code.

  2. Use timers.

The compilers will modify your C code so as to opmimize the code which would surely change your delay so we have to go towards assembly for precision.

As I have studied assembly language of 8051 a year ago. It doesnt have a lot of instruction set and if you study you would surely learn a lot about microcontroller and their basic architecture. Though you dont have to learn assembly of any microcontroller but knowing how assembly language works for atleast one microcontroller, would surely be helpful.

\$\endgroup\$
9
  • \$\begingroup\$ Thank you for your comment. I have actually tried both in C, so I will probably try it in assembly. I pretty much see the way to implement it using NOPes, but not so sure how to go about it with timers in assembly, it is still requires a conditional decision, which takes plenty of cycles on it's own. \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 5:32
  • \$\begingroup\$ circuitstoday.com/delay-using-8051-timer This has a pretty looking code to generate a delay of 1ms (you can change the values inside the timer registers in your code to generate delays as per your requirement). Though the author has not considered the delay of commands in delay subroutine in assembly but you can also consider that to be 100% precise. \$\endgroup\$
    – Jasser
    Commented May 9, 2016 at 6:16
  • \$\begingroup\$ Hey Jasser, thank you for the followup. I tried the timer case, which is much more simple in terms of assembly code addition, but unfortunately for some reason I am getting a some strange results, the "JNB TF2H,$" instruction I use for timer overflow polling, takes 6-7 cycles to execute, while it is supposed to be 3/5 according to the data sheet. Can't find the reason for it to be so long. May be it is connected to the location of the code in the external memory? Any ideas? \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 11:21
  • \$\begingroup\$ The only thing which matters is the instructions and the time taken for it to execute and not where the code resides(The insructions delays take into account of the external residence of code if any). I would recommend you to try on hardware to check the amount of delays you are getting. I have not encountered such thing as of now on keil but maybe...it have something to do with the software... Try to check the delays on a oscilloscope! \$\endgroup\$
    – Jasser
    Commented May 9, 2016 at 13:26
  • \$\begingroup\$ Hey Jasser, Thanks for a prompt response. So I sit next to the micro-controller and an oscilloscope, and what I see in clocks on the debugger, is what I get on the scope. Can't figure out how is that possible. It is nearly twice the amount of time silicon labs state on their datasheet. I am certainly doing something wrong here, and can't figure out what. \$\endgroup\$
    – Cezar
    Commented May 9, 2016 at 14:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.