Some Experiences of Using Keil C to Write Delay Program for 51 Single-Chip Microcontroller

When applying a single-chip microcomputer, it is often encountered that a short time delay is required. The required delay time is very short, generally tens to hundreds of microseconds (us). Sometimes high precision is also required. For example, when using a microcontroller to drive the DS18B20, the allowable error range is within a dozen us, otherwise it is easy to make mistakes. In this case, using a timer is often a bit of a fuss. In extreme cases, the timer has even been used for other purposes. Then we need to think of another way.

In the past, this problem was relatively easy to solve when MCU programs were written in assembly language. For example, 51 with a 12MHz crystal oscillator is used, and the delay is planned to be 20us. As long as the following code is used, it can meet the general needs:

mov r0, #09h

loop: djnz r0, loop

51 The instruction cycle of the single-chip microcomputer is 1/12 of the frequency of the crystal oscillator, that is, a cycle of 1us. mov r0, #09h takes 2 extreme cycles, and djnz also takes 2 extreme cycles. Then the number stored in r0 is (20-2)/2. In this way, it is very convenient to realize the time delay below 256us. If it takes longer, two levels of nesting can be used. And the accuracy can reach 2us, which is generally enough.

Now, the more widely used is undoubtedly Keil’s C compiler. Compared with assembly, C certainly has many advantages, such as easy maintenance of programs, easy to understand, and suitable for large projects. But the disadvantage (I think this is the only disadvantage of C) is that there is no guarantee of real-time performance, and the instruction cycle of code execution cannot be predicted. Therefore, in occasions with high real-time requirements, the combined application of assembly and C is also required. But does such a delay program also need to be implemented in assembly? To find this answer, I did an experiment.

When implementing a delay program in C language, the first thing that comes to mind is the loop statement commonly used in C. The following code is what I often see online:

void delay2 (unsigned char i)

{

for(; i != 0; i–);

}

How accurate is this code? In order to directly measure the effect of this code, I found out the assembly code generated by Keil C based on this code:

; FUNCTION _delay2 (BEGIN)

; SOURCE LINE # 18

;——- Variable “i” assigned to Register “R7” ——-

; SOURCE LINE # 19

; SOURCE LINE # 20

0000? C0007:

0000 EF MOV A, R7

0001 6003 JZ? C0010

0003 1F DEC R7

0004 80FA SJMP? C0007

; SOURCE LINE # 21

0006? C0010:

0006 22 RET

; FUNCTION _delay2 (END)

I really don’t know if I don’t see it~~~ Just look at how inaccurate this delay program is. Just looking at the main four statements, it takes 6 machine cycles. That is to say, its accuracy is at most 6us, not including one lcall and one ret. If we make a table of the root delay length of the i value assigned when calling the function, it is:

i delay time/us

0 6

1 12

2 18

Because the call to the function requires 2 clock cycles of lcall, the delay time is 2 more than the execution time of the code from the function. By the way, some friends wrote this code:

void delay2 (unsigned char i)

{

unsigned char a;

for(a = i; a != 0; a–);

}

One might think this would generate longer assembly code, but it turns out:

; FUNCTION _delay2 (BEGIN)

; SOURCE LINE # 18

;——- Variable “i” assigned to Register “R7” ——-

; SOURCE LINE # 19

; SOURCE LINE # 21

;——- Variable “a” assigned to Register “R7” ——-

0000? C0007:

0000 EF MOV A, R7

0001 6003 JZ? C0010

0003 1F DEC R7

0004 80FA SJMP? C0007

; SOURCE LINE # 22

0006? C0010:

0006 22 RET

; FUNCTION _delay2 (END)

The generated code is the same. But this is really not a good habit. Because there is really no need to introduce redundant variables here. Let’s move on to the topic. Some friends even use this code in order to properly prolong the delay:

void delay2 (unsigned long i)

{

for(; i != 0; i–);

}

What does the assembly code produced by this code look like? In fact, don’t think about it to know how terrifying it is $#^%&%$.. … . Let’s take a look:

; FUNCTION _delay2 (BEGIN)

; SOURCE LINE # 18

0000 8F00 R MOV i+03H, R7

0002 8E00 R MOV i+02H, R6

0004 8D00 R MOV i+01H, R5

0006 8C00 R MOV i, R4

; SOURCE LINE # 19

; SOURCE LINE # 20

0008? C0007:

0008 E4 CLR A

0009 FF MOV R7, A

000A FE MOV R6, A

000B FD MOV R5, A

000C FC MOV R4, A

000D AB00 R MOV R3, i+03H

000F AA00 R MOV R2, i+02H

0011 A900 R MOV R1, i+01H

0013 A800 R MOV R0, i

0015 C3 CLR C

0016 120000 E LCALL? C? ULCMP

0019 601A JZ? C0010

001B E500 R MOV A, i+03H

001D 24FF ADD A, #0FFH

001F F500 R MOV i+03H, A

0021 E500 R MOV A, i+02H

0023 34FF ADDC A, #0FFH

0025 F500 R MOV i+02H, A

0027 E500 R MOV A, i+01H

0029 34FF ADDC A, #0FFH

002B F500 R MOV i+01H, A

002D E500 R MOV A, i

002F 34FF ADDC A, #0FFH

0031 F500 R MOV i, A

0033 80D3 SJMP? C0007

; SOURCE LINE # 21

0035? C0010:

0035 22 RET

; FUNCTION _delay2 (END)

Hehe, this can indeed be delayed for a long time~~~ but there is no precision at all.

So, is it possible to achieve accurate delay with C? I changed the code a little bit:

void delay1 (unsigned char i)

{

while(i–);

}

Because according to experience, the more concise C code often leads to the more concise machine code. So what is the result? Take a look at the assembly code it generates and you’ll know. In hope, I pressed the “Build target” button, and the blow was huge:

; FUNCTION _delay1 (BEGIN)

; SOURCE LINE # 13

;——- Variable “i” assigned to Register “R7” ——-

; SOURCE LINE # 14

0000? C0004:

; SOURCE LINE # 15

0000 AE07 MOV R6, AR7

0002 1F DEC R7

0003 EE MOV A, R6

0004 70FA JNZ? C0004

; SOURCE LINE # 16

0006? C0006:

0006 22 RET

; FUNCTION _delay1 (END)

Although the generated code is not quite the same as using the for statement, I can say without a doubt that the two methods are equally efficient. It seems to end there, because I really can’t think of any room for simplification of the source program. It seems that I am about to come to this conclusion: “If you need us-level delay accuracy, use assembly language when you need it. "But is it really so? I am still not reconciled. Because I don’t believe that the famous Keil C compiler can’t even use djnz? ? ? Because in fact, only one sentence loop is needed in the program body: djnz r7, loop. When I was almost desperate (often people can burst out in this situation, oh hehe~~~), I changed it casually:

void delay1 (unsigned char i)

{

while(–i);

}

Compile absentmindedly, look at the source code:

; FUNCTION _delay1 (BEGIN)

; SOURCE LINE # 13

;——- Variable “i” assigned to Register “R7” ——-

; SOURCE LINE # 14

0000? C0004:

; SOURCE LINE # 15

0000 DFFE DJNZ R7, ? C0004

; SOURCE LINE # 16

0002? C0006:

0002 22 RET

; FUNCTION _delay1 (END)

Day ~ ~ a miracle appeared. … .. I think this program should already be able to meet the needs of the general case. If you make a table:

i delay time/us

1 5

2 7

3 9

When calculating the delay time, the 2 clock cycles taken by the lcall statement to call the function have been taken into account.

Finally, the result is clear. As long as it is used reasonably, C can still achieve unexpected results. Many friends complain that the efficiency of C is much worse than that of assembly. In fact, if you have a deeper understanding of the compilation principle of Keil C, you can use the appropriate syntax to optimize the generated C code. Even if this seems unlikely, there are some simple principles to follow: 1. Try to use unsigned data structures. 2. Try to use char type, it is not enough to use int, then long. 3. If possible, do not use floating point. 4. Use terse code, because as a rule of thumb, terse C code tends to produce terse object code (though not in all cases).

The Links:   PM20CEE060 NL6448BC18-06F IGBT-SOURCE