Thứ Tư, 22 tháng 1, 2014

Vi điều khiển - P3

chương 3
Các lệnh nhảy, vòng lặp và lệnh gọi

Trong một chuỗi lệnh cần thực hiện thường có nhu cần cần chuyển điều khiển
chương trình đến một vị trí khác. Có nhiều lệnh để thực hiện điều này trong 8051, ở
chương này ta sẽ tìm hiểu các lệnh chuyển điều khiển có trong hợp ngữ của 8051
như các lệnh sử dụng cho vòng lặp, các lệnh nhảy có và không có điều khiển, lệnh
gọi và cuối cùng là mô tả về một chương trình con giữ chậm thời gian.
3.1 Vòng lặp và các lệnh nhảy.
3.1.1 Tạo vòng lặp trong 8051.
Qúa trình lặp lại một chuỗi các lệnh với một số lần nhất định được gọi là vòng
lặp. Vòng lặp là một trong những hoạt động được sử dụng rộng rãi nhất mà bất kỳ bộ
vi sử lý nào đều thực hiện. Trong 8051 thì hoạt động vòng lặp được thực hiện bởi
lệnh DJNZ thanh ghi, nhãn. Trong lệnh này thanh ghi được giảm xuống, nếu nó
không bằng không thì nó nhảy đến địa chỉ đích được tham chiếu bởi nhãn. Trước khi
bắt đầu vòng lặp thì thanh ghi được nạp với bộ đếm cho số lần lặp lại. Lưu ý rằng,
trong lệnh này việc giảm thanh ghi và quyết định để nhảy được kết hợp vào trong
một lệnh đơn.
Ví dụ 3.1:
Viết một chương trình để: a) xoá ACC và sau đó b) cộng 3 vào ACC 10 lần.
Lời giải:
!"# $% &' ( )*+ $,,% $ - '
!"# ./% &0' ( 123 45 678 ./ - 0'
9$,:; $<< $% &0' ( ,5=> '? @A* $,,
<B1C ./% $D$E1 ( FG3 H2I JK* 67= LKI ./ - ' M0' HN=O
!"# .P% $ ( ,QR $ @A* RKS=K >KI .P

Trong chương trình trên đây thanh ghi R2 được sử dụng như là bộ đếm. Bộ
đếm lúc đầu được đặt bằng 10. Mỗi lần lặp lại lệnh DJNZ giảm R2 không bằng 0 thì
nó nhảy đến địa chỉ đích gắn với nhãn AGAIN. Hoạt động lặp lại này tiếp tục cho
đến khi R2 trở về không. Sau khi R2 = 0 nó thoát khỏi vòng lặp và thực hiện đứng
ngay dưới nó trong trường hợp này là lệnh MOV R5, A.
Lưu ý rằng trong lệnh DJNZ thì các thanh ghi có thể là bất kỳ thanh ghi nào
trong các thanh ghi R0 - R7. Bộ đếm cũng có thể là một ngăn nhớ trong RAM như ta
sẽ thấy ở chương 5.
Ví dụ 3.2:
Số lần cực đại mà vòng lặp ở ví dụ 3.1 có thể lặp lại là bao nhiêu?
Lời giải:
Vì thanh ghi R2 chứa số đếm và nó là thanh ghi 8 bit nên nó có thể chứa được
giá trị cực đại là FFH hay 155. Do vậy số lần lặp lại cực đại mà vòng lặp ở ví dụ 3.1
có thể thực hiện là 256.
3.2.1 Vòng lặp bền trong một vòng lặp.
Như trình bày ở ví dụ 3.2 số đếm cực đại là 256. Vậy điều gì xảy ra nếu ta
muốn lặp một hành động nhiều hơn 256 lần? Để làm điều đó thì ta sử dụng một vòng
lặp bên trong một vòng lặp được gọi là vòng lặp lồng (Nested Loop). Trong một
vòng lặp lồng ta sử dụng 2 thanh ghi để giữ số đếm. Xét ví dụ 3.3 dưới đây.
Ví dụ 3.3:
Hãy viết một chương trình a) nạp thanh ghi ACC với giá trị 55H và b) bù
ACC 700 lần.
Lời giải:
Vì 700 lớn hơn 256 (là số cực đại mà một thanh ghi vó thể chứa được) nên ta
phải dùng hai thanh ghi để chứa số đếm. Đoạn mã dưới đây trình bày cách sử dụng
hai thanh ghi R2 và R3 để chứa số đếm.

!"# $% &PPT ( 123 $ - PPT
!"# .?% &0' ( 123 .? - 0' UV 678 @W=> HG3 =>*AI
1X)Y; !"# ./% &Z' ( 123 ./ - Z' UV 678 @W=> HG3 R[*=>
$D$E1; \ ,]F $ ( 9^ RKS=K >KI $
<B1C ./% $D$E1 ( FG3 H2I Z' HN= M@W=> HG3 R[*=>O
<B1C .?% 1X)Y

Trong chương trình này thanh ghi R2 được dùng để chứa số đếm vòng lặp
trong. Trong lệnh DJNZ R2, AGAIN thì mỗi khi R2 = 0 nó đi thẳng xuống và lệnh
JNZ R3, NEXT được thực hiện. Lệnh này ép CPU nạp R2 với số đếm 70 và vòng
lặp trong khi bắt đầu lại quá trình này tiếp tục cho đến khi R3 trở về không và vòng
lặp ngoài kết thúc.
3.1.3 Các lệnh nhảy có điều kiện.
Các lệnh nhảy có điều kiện đối với 8051 được tổng hợp trong bảng 3.1. Các
chi tiết về mỗi lệnh được cho trong phụ lục AppendixA. Trong bảng 3.1 lưu ý rằng
một số lệnh như JZ (nhảy nếu A = 0) và JC (nhảy nếu có nhớ) chỉ nhảy nếu một điều
kiện nhất định được thoả mãn. Kế tiếp ta xét một số lệnh nhảy có điều kiện với các
Ví dụ minh hoạ sau.
a- Lệnh JZ (nhảy nếu A = 0). Trong lệnh này nội dung của thanh ghi A được kiểm
tra. Nếu nó bằng không thì nó nhảy đến địa chỉ đích. Ví dụ xét đoạn mã sau:

!"# $% .' ( 123 >I+ R[_ J`S .' @A* $
BC "#X. ( 1Kab 67= "#X. =7c $ - '
!"# $% .0 ( 123 >I+ R[_ J`S .0 @A* $
BC "#X. ( 1Kab 67= "#X. =7c $ - '
"#X. ddd

Trong chương trình này nếu R0 hoặc R1 có giá trị bằng 0 thì nó nhảy đến địa
chỉ có nhãn OVER. Lưu ý rằng lệnh JZ chỉ có thể được sử dụng đối với thanh ghi A.
Nó chỉ có thể kiểm tra xem thanh ghi A có bằng không không và nó không áp dụng
cho bất kỳ thanh ghi nào khác. Quan trọng hơn là ta không phải thực hiện một lệnh
số học nào như đếm giảm để sử dụng lệnh JNZ như ở ví dụ 3.4 dưới đây.
Ví dụ 3.4:
Viết một chương trình để xác định xem R5 có chứa giá trị 0 không? Nếu nạp
thì nó cho giá trị 55H.
Lời giải:

!"# $% .P ( eS* =5I fc=> .P @A* $
B1C 1X)Y ( 1Kab 67= 1X)Y =7c $ LKg=> 4h=> '
!"# .P% &PPT (
1X)Y; ddd

b- Lệnh JNC (nhảy nếu không có nhớ, cờ CY = 0).
Trong lệnh này thì bit cờ nhớ trong thanh ghi cờ PSW được dùng để thực hiện
quyết định nhảy. Khi thực hiện lệnh JNC nhãn thì bộ xử lý kiểm tra cờ nhớ xem nó
có được bật không (CY = 1). Nếu nó không bật thì CPU bắt đầu nạp và thực hiện các
lệnh từ địa chỉ của nhãn. Nếu cờ CY = 1 thì nó sẽ không nhảy và thực hiện lệnh kế
tiếp dưới JNC.
Cần phải lưu ý rằng cũng có lệnh JC nhãn. Trong lệnh JC thì nếu CY = 1 nó
nhảy đến địa chỉ đích là nhãn. Ta sẽ xét các ví dụ về các lệnh này trong các ứng
dụng ở các chương sau.
Ngoài ra còn có lệnh JB (nhảy nếu bit có mức cao) và JNB (nhảy nếu bit có
mức thấp). Các lệnh này được trình bày ở chương 4 và 8 khi nói về thao tác bit.
Bảng 3.1: Các lệnh nhảy có điều kiện.

Lệnh Hoạt động
BC 1Kab =7c $ - '
B1C 1Kab =7c $ i '
<B1C DIa8 @A =Kab =7c $ - '
,B1X $% 4bRj 1Kab =7c $ i 4bRj
,B1X [j% & fSRS 1Kab =7c 9bRj i fSRS
B, 1Kab =7c ,k - 0
B1, 1Kab =7c ,k - '
B9 1Kab =7c 4IR - 0
B19 1Kab =7c 4IR - '
B9, 1Kab =7c 4IR - 0 @A l*+ =m

Ví dụ 3.5:
Hãy tìm tổng của các giá trị 79H, F5H và E2H. Đặt vào trong các thanh ghi
R0 (byte thấp) và R5 (byte cao).
Lời giải:

!"# $% &' ( )*+ RKS=K >KI $ - '
!"# .P% $ ( )*+ .P
$<< $ &ZnT ( ,5=> ZnT @A* $ M$ - ' o ZnT - ZnTO
B1, 1p0 ( 17c LKg=> Jm =Kq J5=> L7 RI73
E1, .P ( 17c ,k - 0% Rr=> .P

1p0; $<< $% &'sPT ( ,5=> sPT @A* $ M$ - ZnT o sPT - tXTO @A ,k - 0
B1, 1p/ ( 1Kab =7c ,k - '
E1, .P ( 17c ,k - 0 Rr=> .P M.P - 0O
1p/; $<< $% &'X/T ( ,5=> X/T @A* $ M$ - DX o X/ - P'O @A ,k - 0
B1, "#X. ( 1Kab =7c ,k - '
E1, .P ( 17c ,k - 0 Rr=> .P
"#X.; !"# .'% $ ( 9ub >Iv .' - P'T @A .P - '/

c- Tất cả các lệnh nhảy có điều kiện đều là những phép nhảy ngắn.
Cần phải lưu ý rằng tất cả các lệnh nhảy có điều kiện đều là các phép nhảy
ngắn, có nghĩa là địa chỉ của đích đều phải nằm trong khoảng -127 đến +127 byte
của nội dung bộ đếm chương trình PC.
3.1.4 Các lệnh nhảy không điều kiện.
Lệnh nhảy không điều kiện là một phép nhảy trong đó điều khiển được truyền
không điều kiện đến địa chỉ đích. Trong 8051 có hai lệnh nhảy không điều kiện đó
là: LJMP - nhảy xa và SJMP - nhảy gần.
a- Nhảy xa LJMP:
Nhảy xa LJMP là một lệnh 3 byte trong đó byte đầu tiên là mã lệnh còn hai
byte còn lại là địa chỉ 16 bit của đích. Địa chỉ đích 02 byte có phép một phép nhảy
đến bất kỳ vị trí nhớ nào trong khoảng 0000 - FFFFH.
Hãy nhớ rằng, mặc dù bộ đếm chương trình trong 8051 là 16 bit, do vậy cho
không gian địa chỉ là 64k byte, nhưng bộ nhớ chương trình ROM trên chíp lớn như
vậy. 8051 đầu tiên chỉ có 4k byte ROM trên chíp cho không gian chương trình, do
vậy mỗi byte đều rất quý giá. Vì lý do đó mà có cả lệnh nhảy gần SJMP chỉ có 2
byte so với lệnh nhảy xa LZ0MP dài 3 byte. Điều này có thể tiết kiệm được một số
byte bộ nhớ trong rất nhiều ứng dụng mà không gian bộ nhớ có hạn hẹp.
b- Lệnh nhảy gồm SJMP.
Trong 2 byte này thì byte đầu tiên là mã lệnh và byte thứ hai là chỉ tương đối
của địa chỉ đích. Đích chỉ tương đối trong phạm vi 00 - FFH được chia thành các
lệnh nhảy tới và nhảy lùi: Nghĩa là -128 đến +127 byte của bộ nhớ tương đối so với
địa chỉ hiện thời của bộ đếm chương trình. Nếu là lệnh nhảy tới thì địa chỉ đích có
thể nằm trong khoảng 127 byte từ giá trị hiện thời của bộ đếm chương trình. Nếu địa
chỉ đích ở phía sau thì nó có thể nằm trong khoảng -128 byte từ giá trị hiện hành của
PC.
3.1.5 Tính toán địa chỉ lệnh nhảy gần.
Ngoài lệnh nhảy gần SJMP thì tất cả mọi lệnh nhảy có điều kiện như JNC, JZ
và DJNZ đều là các lệnh nhảy gần bởi một thực tế là chúng đều lệnh 2 byte. Trong
những lệnh này thì byte thứ nhất đều là mã lệnh, còn byte thứ hai là địa chỉ tương
đối. Địa chỉ đích là tương đối so với giá trị của bộ đếm chương trình. Để tính toán
địa chỉ đích byte thứ hai được cộng vào thanh ghi PC của lệnh đứng ngay sau lệnh
nhảy. Để hiểu điều này hãy xét ví dụ 3.6 dưới đây.
Ví dụ 3.6:
Sử dụng tệp tin liệt kê dưới đây hãy kiểm tra việc tín toán địa chỉ nhảy về
trước.

'0 '''' ".D ''''
'/ '''' Zw'' !"# .'% &'
'? '''/ ZxPP !"# $% &PPT
'x '''x t''? BC 1X)Y
'P '''t 'w 1E, .'
't '''Z 'x $D$E1; E1, $
'Z '''w 'x E1, $
'w '''n /xZZ 1X)Y; $<< $% &ZZK
'n '''9 P''P B1, "#X.
0' '''< Xx ,F. $
00 '''X sw !"# .'% $
0/ '''s sn !"# .0% $
0? ''0' s$ !"# ./% $
0x ''00 s9 !"# .?% $
0P ''0/ /9 "#X.; $<< $% .?
0t ''0? P's/ B1, $D$E1
0Z ''0P w'sX TX.X; eB!] eTX.X
0w ''0Z X1<

Lời giải:
Trước hết lưu ý rằng các lệnh JZ và JNC đều là lệnh nhảy về trước. Địa chỉ
đích đối với lệnh nhảy về trước được tính toán bằng cách cộng giá trị PC của lệnh đi
ngay sau đó vào byte thứ hai của lệnh nhảy gần được gọi là địa chỉ tương đối. ở
dòng 04 lệnh JZ NEXT có mã lệnh 60 và toán hạng 03 tại địa chỉ 0004 và 0005. ở
đây 03 là địa chỉ tương đối, tương đối so với địa chỉ của lệnh kế tiếp là: INC R0 và
đó là 0006. Bằng việc cộng 0006 vào 3 thì địa chỉ đích của nhãn NEXT là 0009 được
tạo ra. Bằng cách tương tự như vậy đối với dòng 9 thì lệnh JNC OVER có mã lệnh
và toán hạng là 50 và 05 trong đó 50 là mã lệnh và 05 là địa chỉ tương đối. Do vậy,
05 được cộng vào OD là địa chỉ của lệnh CLA A đứng ngay sau lệnh JNC
OVER và cho giá trị 12H chính là địa chỉ của nhãn OVER.
Ví dụ 3.7:
Hãy kiểm tra tính toán địa chỉ của các lệnh nhảy lùi trong ví dụ 3.6.
Lời giải:
Trong danh sách liệt kê chương trình đó thì lệnh JNC AGAIN có mã lệnh là
50 và địa chỉ tương đối là F2H. Khi địa chỉ tương đối của F2H được cộng vào 15H là
địa chỉ của lệnh đứng dưới lệnh nhảy ta có 15H + F2H = 07 (và phần nhớ được bỏ
đi). Để ý rằng 07 là địa chỉ nhãn AGAIN. Và hãy cũng xét lệnh SJMP HERE có
mã lệnh 80 và địa chỉ tương đối FE giá trị PC của lệnh kế tiếp là 0017H được cộng
vào địa chỉ tương đối FEH ta nhận được 0015H chính là địa chỉ nhãn HERE (17H +
FEH = 15H) phần nhớ được bỏ đi). Lưu ý rằng FEH là -2 và 17h + (-2) = 15H. Về
phép cộng số âm sẽ được bàn ở chương 6.
3.1.6 Tính toán địa chỉ đích nhảy lùi.
Trong khi ở trường hợp nhảy tới thì giá trị thay thế là một số dương trong
khoảng từ 0 đến 127 (00 đến 7F ở dạng Hex) thì đối với lệnh nhảy lùi giá trị thay thế
là một số âm nằm trong khoảng từ 0 đến -128 như được giải thích ở ví dụ 3.7.
Cần phải nhấn mạnh rằng, bất luận SJMP nhảy tới hay nhảy lùi thì đối với
một lệnh nhảy bất kỳ địa chỉ của địa chỉ đích không bao giờ có thể lớn hơn 0 -128
đến +127 byte so với địa chỉ gắn liền với lệnh đứng ngay sau lệnh SJMP. Nếu có một
sự nỗ lực nào vi phạm luật này thì hợp ngữ sẽ tạo ra một lỗi báo rằng lệnh nhảy
ngoài phạm vi.
3.2 Các lệnh gọi CALL.
Một lệnh chuyển điều khiển khác là lệnh CALL được dùng để gọi một
chương trình con. Các chương trình con thường được sử dụng để thực thi các công
việc cần phải được thực hiện thường xuyên. Điều này làm cho chương trình trở nên
có cấu trúc hơn ngoài việc tiết kiệm được thêm không gian bộ nhớ. Trong 8051 có 2
lệnh để gọi đó là: Gọi xa CALL và gọi tuyệt đối ACALL mà quyết định sử dụng
lệnh nào đó phụ thuộc vào địa chỉ đích.
3.2.1 Lệnh gọi xa LCALL.
Trong lệnh 3 byte này thì byte đầu tiên là mã lệnh, còn hai byte sau được
dùng cho địa chỉ của chương trình con đích. Do vậy LCALL có thể được dùng để gọi
các chương trình con ở bất kỳ vị trí nào trong phạm vi 64k byte, không gian địa chỉ
của 8051. Để đảm bảo rằng sau khi thực hiện một chương trình được gọi để 8051
biết được chỗ quay trở về thì nó tự động cất vào ngăn xếp địa chỉ của lệnh đứng ngay
sau lệnh gọi LCALL. Khi một chương trình con được gọi, điều khiển được chuyển
đến chương trình con đó và bộ xử lý cất bộ đếm chương trình PC vào ngăn xếp và bắt
đầu nạp lệnh vào vị trí mới. Sau khi kết thúc thực hiện chương trình con thì lệnh trở
về RET chuyển điều khiển về cho nguồn gọi. Mỗi chương trình con cần lệnh RET
như là lệnh cuối cùng (xem ví dụ 3.8).
Các điểm sau đây cần phải được lưu ý từ ví dụ 3.8.
1. Lưu ý đến chương trình con DELAY khi thực hiện lệnh LCALL DELAY
đầu tiên thì địa chỉ của lệnh ngay kế nó là MOV A, #0AAH được đẩy vào
ngăn xếp và 8051 bắt đầu thực hiện các lệnh ở địa chỉ 300H.
2. Trong chương trình con DELAY, lúc đầu bộ đếm R5 được đặt về giá trị 255
(R5 = FFH). Do vậy, vòng lặp được lặp lại 256 lần. Khi R5 trở về 0 điều
khiển rơi xuống lệnh quay trở về RET mà nó kéo địa chỉ từ ngăn xếp vào bộ
đếm chương trình và tiếp tục thực hiện lệnh sau lệnh gọi CALL.
Ví dụ 3.8:
Hãy viết một chương trình để chốt tất cả các bit của cổng P1 bằng cách gửi
đến nó giá trị 55H và AAH liên tục. Hãy đặt một độ trễ thời gian giữa mỗi lần xuất
dữ liệu tới cổng P1. Chương trình này sẽ được sử dụng để kiểm tra các cổng của
8051 trong chương tiếp theo.
Lời giải:
".D ''''
9$,:; !"# $% &PPT ( 123 $ @qI >I+ R[_ PPT
!"# ]0% $ ( DyI PPT 67= Jz=> ]0
F,$FF <XF$k ( Y2* R[{ RKvI >IS=
!"# $% &'$$T ( 123 $ @qI >I+ R[_ $$T
!"# ]0% $ ( DyI $$T 67= Jz=> ]0
F,$FF <XF$k ( DI| JK}8
eB!] 9$,: ( FG3 H2I @g R}=
( pppppppppppppppppp p ~ub HA JKơ=> R[ì=K J*= R2* 65 R[{ RKvI >IS=
".D ?''T ( ~GR JKơ=> R[ì=K J*= R[{ RKvI >IS= ở 6_S JKỉ ?''T
<XF$k; !"# .P% &''T ( 123 45 678 .P - /PPT MKSb ssTO
$D$E1; <B1C .P% $D$E1 ( YI73 RụJ JK* 67= LKI .P @ề LKg=>
.XY ( Y[a 6Iềc LKIể= @ề =>cồ= >ọI MLKI .P - 'O
X1< ( :êR RKúJ Rệ3 RI= J`S Kợ3 =>|

Lượng thời gian trễ trong ví dụ 8.3 phục thuộc vào tần số của 8051. Cách tính
chính xác thời gian sẽ được giải thích ở chương 4. Tuy nhiên ta có thể tăng thời gian
độ trễ bằng cách sử dụng vòng lặp lồng như chỉ ra dưới đây.

<XF$k; ( #W=> HG3 Hồ=> >I| JK}8
!"# .x% &/PP ( 123 .x - /PP MssT f2=> KjlO
1X)Y; !"# .P% &/PP ( 123 .P - /PP MssT f2=> KjlO
$D$E1; <B1C .P% $D$E1 ( FG3 H2I JK* 67= LKI .Y - '
<B1C .x% 1X)Y ( DIa8 .x
(YI73 RụJ =23 .P JK* 67= LKI .x - '
.XY ( Y[ở @ề MLKI .x - 'O

3.2.2 Lệnh gọi CALL và vai trò của ngăn xếp.
Ngăn xếp và con trỏ ngăn xếp ta sẽ nghiên cứu ở chương cuối. Để hiểu được
tầm quan trọng của ngăn xếp trong các bộ vi điều khiển bây giờ khảo sát nội dung
của ngăn xếp và con trỏ ngăn xếp đối với ví dụ 8.3. Điều này được trình bày ở ví dụ
3.9 dưới đây.
Ví dụ 3.9:
Hãy phân tích nội dung của ngăn xếp sau khi thực hiện lệnh LCALL đầu tiên
dưới đây.

001 0000 OR6
002 0000 7455 BACK: M O V A, #55H ; Nạp A với giá trị 55H
003 0002 F590 MOV P1, A ; Gửi 55H tới cổng P1
004 0004 120300 LCALL D E LAY ; Tạo trễ thời gian
005 0007 74AA MOV A, #0AAH ; Nạp A với giá trị AAH
006 0009 F590 MOV P1, A ; Gửi AAH tới cổng P1
007 000B 120300 LCALL D E LAY ; Tạo trễ thời gian
008 000E 80F0 SJMP BACK ; Tiếp tục thực hiện
009 0010
010 0010 ; Đây là chương trình con giữ chậm
011 0300 MOV 300H
012 0300 DELAY:
013 0300 7DFF MOV R5, #FFH ; Nạp R5 = 255
014 0302 DDFE AGAIN:DJNZ R5, AGAIN ; Dừng ở đây
015 0304 22 RET ; Trở về nguồn gọi
016 0305 END ; Kết thúc nạp tin hợp ngữ

Lời giải:
Khi lệnh LCALL đầu tiên được thực hiện thì địa chỉ của lệnh MOV A,
#0AAH được cất vào ngăn xếp. Lưu ý rằng byte thấp vào trước và byte cao vào sau.
Lệnh cuối cùng của chương trình con được gọi phải là lệnh trở về RET để chuyển
CPU kéo (POP) các byte trên đỉnh của ngăn xếp vào bộ đếm chương trình PC và tiếp
tục thực hiện lệnh tại địa chỉ 07. Sơ đồ bên chỉ ra khung của ngăn xếp sau lần gọi
LCALL đầu tiên.

0A
09 00
08 07
SP = 09

3.2.3 Sử dụng lệnh PUSH và POP trong các chương trình con.
Khi gọi một chương trình con thì ngăn xếp phải bám được vị trí mà CPU cần
trở về. Sau khi kết thúc chương trình con vì lý do này chúng ta phải cẩn thận mỗi khi
thao tác với các nội dung của ngăn xếp. Nguyên tắc là số lần đẩy vào (PUSH) và kéo
ra (POP) luôn phải phù hợp trong bất kỳ chương trình con được gọi vào. Hay nói
cách khác đối với mỗi lệnh PUSH thì phải có một lệnh POP. Xem ví dụ 3.10.
3.2.4 Gọi các chương trình con.
Trong lập trình hợp ngữ thường có một chương trình chính và rất nhiều
chương trình con mà chúng được gọi từ chương trình chính. Điều này cho phép ta tạo
mới chương trình con trong một mô-đun riêng biệt. Mỗi mô-đun có thể được kiểm
tra tách biệt và sau đó được kết hợp với nhau cùng với chương trình chính. Quan
trọng hơn là trong một chương trình lớn thì các mô-đun có thể được phân cho các lập
trình viên khác nhau nhằm rút ngắn thời gian phát triển.
Ví dụ 3.10:
Phân tích ngăn xếp đối với lệnh LCALL đầu tiên trong đoạn mã.

'0 '''' ".D '
'/ '''' ZxPP 9$,:; !"# $% &PPT ( 123 $ @qI >I+ R[_ PPT
'? '''/ sPn' !"# ]0% $ ( DyI PPT [S Jz=> ]0
'x '''x Z,nn !"# .x% &nnT
'P '''t Z<tZ !"# .P% &tZT
't '''w 0/'?'' F,$FF <XF$k ( Y2* >I| JK}8 RKvI >IS=
'Z '''9 Zx$$ !"# $% &'$$T ( 123 $ @qI $$T
'w '''< sPn' !"# ]0% $ ( DyI $$T [S Jz=> ]0
'n '''s 0/'?'' F,$FF <XF$k
0' ''0/ w'X, eB!] 9$,: ( YI73 RụJ RKựJ KIệ=
00 ''0x ( dddddddddddd ~ub HA JKơ=> R[ì=K J*= <XF$k
0/ '?'' ".D ?''T
0? '?'' ,''x <XF$k ]UeT x ( ~ẩb .x @A* =>r= l73
0x '?'/ ,''P ]UeT P ( ~ẩb .P @A* =>r= l73
0P '?'x Z,ss !"# .x% ''sT ( D+= .x - ssT
0t '?'t Z<ss 1X)Y; !"# .P% &''sT ( D+= .P - /PP
0Z '?'w <<sX $D$E1; <B1C .P% $D$E1
0w '?'$ <,s$ <B1C .x% 1X)Y
0n '?', <''P ]"] P ( :é* 6ỉ=K =>r= l73 @A* .P
/' '?'X <''x ]"] x ( :é* 6ỉ=K =>r= l73 @A* .x
/0 '?0' // .XY ( Y[ở @ề =>cồ= >ọI
// '?00 X1< ( :7R RKúJ Rệ3 RI= Kợ3 =>|

Lời giải:
Trước hết lưu ý rằng đối với các lệnh PUSH và POP ta phải xác định địa chỉ
trực tiếp của thanh ghi được đẩy vào, kéo ra từ ngăn xếp. Dưới đây là sơ đồ khung
của ngăn xếp.

Sau lệnh LCALL thứ nhất Sau lệnh PUSH 4 Sau lệnh POSH 5
0B 0B 0B 67 R5
0A 0A 99 R4 0A 09 R4
09 00 PCH 09 00 PCH 09 00 PCL
08 0B PCL 0B 0B PCL 08 0B PCL

Cần phải nhấn mạnh rằng trong việc sử dụng LCALL thì địa chỉ đích của các
chương trình con có thể ở đâu đó trong phạm vi 64k byte không gian bộ nhớ của
8051. Điều này không áp dụng cho tất cả mọi lệnh gọi CALL chẳng hạn như đối với
ACALL dưới đây:
























Hình 3.1: Chương trình chính hợp ngữ của 8051 có gọi các chương trình con.
3.2.5 Lệnh gọi tuyệt đối ACALL (Absolute call).
Lệnh ACALL là lệnh 2 byte khác với lệnh LCALL dài 3 byte. Do ACALL chỉ
có 2 byte nên địa chỉ đích của chương trình con phải nằm trong khoảng 2k byte địa
chỉ vì chỉ có 11bit của 2 byte được sử dụng cho địa chỉ. Không có sự khác biệt nào
giữa ACALL và LCALL trong khái niệm cất bộ đếm chương trình vào ngăn xếp hay
trong chức năng của lệnh trở về RET. Sự khác nhau duy nhất là địa chỉ đích của lệnh
LCALL có thể nằm bất cứ đâu trong phạm vi 64k byte không gian địa chỉ của 8051,
còn trong khi đó địa chỉ của lệnh ACALL phải nằm trong khoảng 2 byte. Trong
nhiều biến thế của 8051 do các hãng cung cấp thì ROM trên chíp chỉ có 1k byte
Trong những trường hợp như vậy thì việc sử dụng ACALL thay cho LCALL có thể
tiết kiệm được một số byte bộ nhớ của không gian ROM chương trình.
Ví dụ 3.11:
Một nhà phát triển sử dụng chíp vi điều khiển Atmel AT89C1051 cho một sản
phẩm. Chíp này chỉ có 1k byte ROM Flash trên chíp. Hỏi trong khi lệnh LCALL và
ACALL thì lệnh nào hữu ích nhất trong lập trình cho chíp này.
Lời giải:
Lệnh ACALL là hữu ích hơn vì nó là lệnh 2 byte. Nó tiết kiệm một byte mỗi
lần gọi được sử dụng.
Tất nhiên, việc sử dụng các lệnh gọn nhẹ, chúng ta có thể lập trình hiệu quả
bằng cách có một hiểu biết chi tiết về tất cả các lệnh được hỗ trợ bởi bộ vi xử lý đã
cho và sử dụng chúng một cách khôn ngoan. Xét ví dụ 3.12 dưới đây.
Ví dụ 3.12:
Hãy viết lại chương trình ở ví dụ 3.8 một cách hiệu quả mà bạn có thể:
Lời giải:
( !$E1 3[*>[S8 JSHHI=> Uc4[*cRI=jU
".D '
!$E1; F,$FF eU9.p0
F,$FF eU9.p/
F,$FF eU9.p?

TX.X; eB!] !$E1
(ppppppppppppppppp j=f *f !$E1
(
eU9.p0H ddd
ddd
.XY
( ppppppppppppppppp j=f *f Uc4[*cRI=jH 0
( eU9.p0H ddd
ddd
.XY
( ppppppppppppppppp j=f *f Uc4[*cRI=jH /
( eU9.p0H ddd
ddd
.XY
( ppppppppppppppppp j=f *f Uc4[*cRI=jH ?
X1< ( j=f *f RKj SU8 fIHj


".D '
!"# $% &PPT ( 123 $@qI >I+ R[_ PPT
9$,:; !"# ]0% $ ( )cấR >I+ R[_ R[*=> $ [S Jz=> ]0
$,$FF <XF$k ( DI| JK}8
,]F $ ( 9^ RKA=K >KI $
eB!] 9$,: ( YI73 RụJ RKựJ KIệ= @g K2=
( pppppppp ~ub HA JKơ=> R[ì=K J*= >I| JK}8 <XF$k
<XF$k;
!"# .P% &'ssT ( 123 .P - /PP MKSb ssTO HA8 JK* 45 678
$D$E1; <B1C .P% $D$E1 ( <ừ=> ở 6ub JK* 67= LKI .P - '
.XY ( Y[ở @ề
X1< ( :7R RKúJ

3.3 Tạo và tính toán thời gian giữ chậm.
3.3.1 Chu kỳ máy:
Đối với CPU để thực hiện một lệnh thì mất một chu kỳ đồng hồ này được coi
như các chu kỳ máy. Phụ lục AppendixA.2 cung cấp danh sách liệt kê các lệnh 8051
và các chu kỳ máy của chúng. Để tính toán một độ trễ thời gian, ta sử dụng danh
sách liệt kê này. Trong họ 8051 thì độ dài của chu kỳ máy phụ thuộc vào tần số của
bộ dao động thạch anh được nối vào hệ thống 8051. Bộ dao động thạch anh cùng với
mạch điện trên chip cung cấp xung đồng hồ cho CPU của 8051 (xem chương 4). Tần
số của tinh thể thạch anh được nối tới họ 8051 dao động trong khoảng 4MHz đến 30
MHz phụ thuộc vào tốc độ chíp và nhà sản xuất. Thường xuyên nhất là bộ dao động
thạch anh tần số 10.0592MHz được sử dụng để làm cho hệ 8051 tương thích với
cổng nối tiếp của PC IBM (xem chương 10). Trong 8051, một chu kỳ máy kéo dài 12
chu kỳ dao động. Do vậy, để tính toán chu kỳ máy ta lấy 1/12 của tần số tinh thể
thạch anh, sau đó lấy giá trị nghịch đảo như chỉ ra trong ví dụ 3.13.
Ví dụ 3.13:
Đoạn mã dưới đây trình bày tần số thạch anh cho 3 hệ thống dựa trên 8051
khác nhau. Hãy tìm chu kỳ máy của mỗi trường hợp: a) 11.0592MHz b) 16MHz và
c) 20MHz.
Lời giải:
a) 11.0592/12 = 921.6kHz; Chu kỳ máy là 1/921.6kHz = 1.085ms (micro giây)
b) 16MHz/12 = 1.333MHz; Chu kỳ máy MC = 1/1.333MHz = 0.75ms
c) 20MHz/12 = 1.66MHz ị MC = 1/1.66MHz = 0.60ms
Ví dụ 3.14:
Đối với một hệ thống 8051 có 11.0592MHz hãy tìm thời gian cần thiết để
thực hiện các lệnh sau đây.

SO !"# .?% &PP 4O <X, .? JO <B1C ./ 6íJK
fO FB!] jO eB!] fO 1"] >O !UF $9

Lời giải:
Chu kỳ máy cho hệ thống 8051 có tần số đồng hồ là 11.0592MHz Là 1.085ms
như đã tính ở ví dụ 3.13. Bảng A-1 trong phụ lục Appendix A trình bày số chu kỳ
máy đối với các lệnh trên. Vậy ta có:



Lệnh Chu kỳ máy Thời gian thực hiện
MSO !"# .?% &PP 0
0 0d'wP mU - 0d'wP mU
M4O <X, .? 0
0 0d'wP mU - 0d'wP mU
MJO <B1C ./% RS[>jR /
/ 0d'wP mU - /d0Z mU
MfO FB!] /
/ 0d'wP mU - /d0Z mU
MjO eB!] /
/ 0d'wP mU - /d0Z mU
MfO 1"] 0
0 0d'wP mU - 0d'wP mU
M>O !UF $9 x
x 0d'wP mU - xd?x mU

3.3.2 Tính toán độ trễ.
Như đã trình bày ở trên đây, một chương trình con giữ chậm gồm có hai
phần: (1) thiết lập bộ đếm và (2) một vòng lặp. Hầu hết thời gian giữ chậm được thực
hiện bởi thân vòng lặp như trình bày ở ví dụ 3.15.
Ví dụ 3.15:
Hãy tìm kích thước của thời gian giữ chậm trong chương trình sau, nếu tần số
giao động thach anh là 11.0592MHz.

!"# $% &PPT
$D$E1; !"# ]0% $
$,$FF <XF$k
,]F $
eB!] $D$E1
( pppppppp YI8j fjHSb
<XF$k; !"# .?% &/''
TX.X ; <B1C .?% TX.X
.XY

Lời giải:
Từ bảng A-1 của phụ lục Appendix A ta có các chu kỳ máy sao cho các lệnh
của chương trình con giữ chậm là:

<XF$k; !"# .?% &/'' 0
TX.X ; <B1C .?% TX.X /
.XY 0

Do vậy tổng thời gian giữ chậm là [(200 2) + 1 + 1] 1.085 = 436.17ms.
Thông thường ta tính thời gian giữ chậm dựa trên các lệnh bên trong vòng lặp
và bỏ qua các chu kỳ đồng hồ liên quan với các lệnh ở ngoài vòng lặp.
Trong ví dụ 3.15 giá trị lớn nhất mà R3 có thể chứa là 255, do vậy một cách
tăng độ trễ là sử dụng lệnh UOP (không làm gì) trong vòng lặp để tiêu tốn thời gian
một cách đơn giản. Điều này được chỉ ra trong ví dụ 3.16 dưới đây.
Ví dụ 3.16:
Hãy tìm độ trễ thời gian cho chương trình con sau. Giả thiết tần số dao động
thạch anh là 11.0592MHz.



Xem chi tiết: Vi điều khiển - P3


Không có nhận xét nào:

Đăng nhận xét