You are on page 1of 24

CHƯƠNG 9: LẬP TRÌNH C MCU8051

9.1: Giới thiệu:


Chương này giới thiệu lập trình C cho MCU8051.Vấn đề chọn lập trình bằng hợp ngữ(Assembly) hay
C tùy thuộc vào người thiết kế: dung lượng mã lệnh,quản lý thanh ghi,bộ nhớ,thời gian thực thi,tốc độ,dễ lập
trình…. Những vấn đề này là những điểm tiêu biểu để so sánh thuận lợi và bất lợi giữa lập trình hợp ngữ và
C.Thông qua một số ví dụ cụ thể trong chương này ta sẽ có cái nhìn cụ thể hơn!
Có 2 lưu ý khi tham khảo chương này:
 Xem như đã biết về ngôn ngữ C căn bản
 Các ví dụ biên dịch trong môi trường Keil C51
9.2: Thuận lợi và bất lợi của ngôn ngữ C 8051:
 Cung cấp các tiện ích của ngôn ngữ cấp cao có cấu trúc như C,dễ dàng viết các lệnh tính toán,
chương trình con,hàm số,dãy số,các vòng lặp…
 Giảm bớt chi tiết phần cứng người lập trình cần phải hiểu như khi lập trình hợp ngữ
 Dễ viết nhất là đối với các chương trình lớn và phức tạp
 Dễ đọc và kiểm tra mã nguồn
 Bất lợi chung là phải khai báo rườm rà đối ngôn ngữ bậc cao và có cấu trúc
 Dung lượng mã lệnh lớn hơn so với hợp ngữ nhiều lần
 Người lập trình không điều khiển hay tác động trực tiếp đến phần cứng như :tính thời gian thực
thi,quản lý các thanh ghi,bộ nhớ,địa chỉ bộ nhớ,ngoại vi…
Để so sánh giữa C 8051 và hợp ngữ ta xem ví dụ 9.1.
Ví dụ 9.1: Viết một chương trình tạo chuỗi xung vuông đối xứng tần số 1Khz xuất ra P1.0 sử dụng Timer0.
Giải:
 Chương trình assembly:
ORG 0
MOV TMOD,#01H
LOOP: MOV TH0,#0FEH
MOV TL0,#0CH
SETB TR0
JNB TF0,$
CLR TR0
CLR TF0
CPL P1.0
JMP LOOP
END
 Chương trình C 8051:
#include <reg51.h>
sbit portbit = P1^0;
main()
{
TMOD = 1;
while(1)
{
TH0 = 0XFE;
TL0 = 0X0C;
TR0=1;
while (TF0 != 1);
TR0=0;
TF0=0;
portbit=!(P1^0);

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 1


}
}

 Chương trình viết bằng C đọc dễ hiểu hơn chương trình viết bằng hợp ngữ.Tuy nhiên khi biên dịch
sang mã máy(code) chương trình C dài 42 byte trong khi chương trình hợp ngữ dài 22 byte chỉ bằng
½ chương trình C!Ngoài ra chương trình C phải có một số khai báo có liên quan ở hai dòng đầu tiên.
9.3: Biên dịch C 8051:
Trình biên dịch (compiler)C 8051 sang mã máy phức tạp hơn nhiều so với trình biên dịch hợp ngữ
(assembler) vì hợp ngữ rất sát mã máy.
Có nhiều trình biên dịch C 8051,như trên đã giới thiệu ta sử dụng trình biên dịch Keil µvision rất
thuận tiện vì có bao gồm trình biên dịch chéo C51(cross compiler) chuyển sang hợp ngữ 8051.Ta có thể
quan sát chương trình hợp ngữ tương ứng trong cửa sổ Disassembly phía trên cửa sổ chứa chương trình
C khi chạy Debug chương trình C.

9.4: Kiểu dữ liệu(data types):


Các kiểu dữ liệu trong C 8051 cũng giống như trong C thông dụng.Ngoài ra C 8051 còn 4 kiểu dữ
liệu riêng như : bit,sbit,sfr,sfr16 dùng để khai báo cho các bit và SFR trong MCU8051.
Bảng 9-1 trình bày các kiểu dữ liệu và tầm giá trị tương ứng cho C 8051:
Bảng 9-1: Các kiểu dữ liệu trong C 8051:

Data type Số bit Số byte Tầm giá trị


bit 1 0÷1
signed char 8 1 -128 ÷ +127
unsigned char 8 1 0 ÷ 255
enum 16 2 -32768 ÷ +32767
signed short 16 2 -32768 ÷ +32767
unsigned short 16 2 0 ÷ 65535
signed int 16 2 -32768 ÷ +32767
unsigned int 16 2 0 ÷ 65535
signed long 32 4 -2,147,483,148 ÷ +2,147,483,147
unsigned long 32 4 0 ÷ 4,294,967,295
float 32 4 ±1.175494E-38 ÷ ± 3.402823E+38
sbit 1 0÷1
sfr 8 1 0 ÷ 255
sfr16 16 2 0 ÷ 65535
 bit khai báo cho các biến có giá trị bit chứa trong các ô nhớ RAM nội định vị bit(địa chỉ bit từ
00H đến 7FH thuộc ô nhớ địa chỉ byte 20H đến 2FH)
 sbit khai báo cho các biến có giá trị bit chứa trong các SFR
 sfr khai báo các tên gọi SFR độ dài 8 bit trong MCU8051
 sfr16 khai báo tên gọi SFR 16 bit trong MCU8051,chỉ có DPTR
Ví dụ 9.2: Xem các khai báo sau:
bit flag = 0 ; /* biến flag là bit, gán flag=0,trình biên dịch cất flag vào ô nhớ bit 00H÷7FH*/
sbit P = 0xD0 ; /* biến P địa chỉ bit D0H thuộc SFR PSW*/
sbit P= 0xD0^0; /* biến P địa chỉ bit 0 thuộc SFR địa chỉ D0H là PSW,giống như trên*/
sfr PSW = 0xD0 ; /* SFR tên PSW địa chỉ D0H*/
sbit P= PSW^0 ; /* biến P =PSW.0*/
sfr IE=0xA8 ; /* SFR tên IE địa chỉ A8H*/
sfr16 DPTR = 0x82 ; /* SFR DPTR 16 bit địa chỉ 82H(byte thấp) và 83H(byte cao)*/
 Trong thực tế,các ký hiệu SFR và các bit định vị bit được của các SFR đều đã được các trình
biên dịch C 8051 khai báo sẵn trong các file header <REG51.H> cho MCU8051 và
Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 2
<REG52.H> cho MCU8052.Ta chỉ cần dùng chỉ dẫn #include file header này là có thể sử
dụng các ký hiệu SFR và các bit định vị bit của các SFR.Lưu ý là các file header trên không
khai báo bank thanh ghi R0-R7(do sử dụng R0-R7 mặc định trong trình biên dịch)
Xem lại ví dụ 9.1,dòng đầu tiên #include <reg51.h> nên sau đó có thể sử dụng các ký hiệu
P1^0,TH0,TL0,TR0,TF0 trình biên dịch hiểu mà không báo lỗi!

9.5: Kiểu và mô thức bộ nhớ(Memory types and models):


MCU8051 có các kiểu bộ nhớ sau: 128 byte data RAM nội,64KB data ngoài,4KB code ROM nội,
64KB code ngoài.Riêng MCU8052 có thêm 128 byte data RAM nội truy xuất gián tiếp.Bảng 9-2 mô tả các
kiểu bộ nhớ sử dụng trong C 8051.
Bảng 9-2: Các kiểu bộ nhớ sử dụng trong C 8051:

Kiểu bộ nhớ Mô tả(kích thước)


code Bộ nhớ code (64KB)
data Bộ nhớ data trong định vị trực tiếp(128 byte)
idata Bộ nhớ data trong định vị gián tiếp(256 byte)
bdata Bộ nhớ data trong định vị bit (16 byte)
xdata Bộ nhớ data ngoài (64KB)
pdata Bộ nhớ data ngoài phân trang (256 byte)
Ví dụ 9.3: Xem các khai báo sau:
char code msg[ ] = “ This is a message”;
/* khai báo chuỗi ký tự chứa trong bộ nhớ code*/
signed int data num1;
/*khai báo biến num1 là số nguyên có dấu dài 2 byte chứa trong bộ nhớ data trong(00H-7FH)*/
bit bdata numbit;
/* khai báo biến numbit là bit chứa trong bộ nhớ data trong định vị bit(byte 20H-2FH)*/
unsigned int xdata num2 ;
/* khai báo biến num2 là số nguyên không dấu dài 2 byte chứa trong bộ nhớ data ngoài 64KB*/
unsigned char pdata num3;
/* khai báo biến num3 là ký tự không dấu dài 1 byte chứa trong bộ nhớ data ngoài phân trang*/
/*( 256 byte đầu tiên của bộ nhớ ngoài)*/
unsigned long idata num4;
/* khai báo biến num4 là số nguyên không dấu 4 byte chứa trong bộ nhớ data định vị gián tiếp*/
Trường hợp khai báo biến không có định nghĩa cụ thể loại bộ nhớ,hoặc muốn các biến đều cùng một
loại bộ nhớ,ta sử dụng khai báo mô thức bộ nhớ(memory models) như bảng 9-3.
Bảng 9-3: Mô thức bộ nhớ trong C 8051:
Mô thức bộ nhớ Mô tả
Small Các biến mặc định chứa trong bộ nhớ data trong(data)
Compact Các biến mặc định chứa trong 256 byte đầu tiên bộ nhớ data
ngoài(pdata)
Large Các biến mặc định chứa trong bộ nhớ data ngoài(xdata)
Khai báo mô thức bộ nhớ như bảng 9-3 sử dụng chỉ dẫn #pragma.
Nếu không có khai báo loại hay mô thức bộ nhớ, trình biên dịch mặc định loại bộ nhớ data trong
(data) hay mô thức Small để thời gian thực hiện lệnh nhanh hơn.Nhưng ta cần lưu ý là dung lượng bộ nhớ
data trong của MCU8051 chỉ 128 byte!
 Trường hợp khai báo các biến nằm trong bộ nhớ data trong dài 128 byte(địa chỉ từ 00H-
7FH),trình biên dịch C51 phân vùng dự trữ như sau:
 Vùng địa chỉ từ 00H-07H bank thanh ghi R0-R7 phục vụ biên dịch vào ra số liệu
 Vùng địa chỉ từ 08H trở lên chứa các biến
Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 3
 Vùng địa chỉ trên các biến chứa các dãy,chuỗi
 Vùng địa chỉ trên cùng là vùng stack
Ví dụ 9.4 minh họa cách phân vùng địa chỉ bộ nhớ trong khi biên dịch C51.Mặc dù lập trình
C 8051 không trực tiếp cho can thiệp vào địa chỉ bộ nhớ,nhưng nếu hiểu cách phân vùng địa chỉ bộ nhớ
thông qua biên dịch C51 và chạy debug xem lệnh disassembly tương ứng,ta vẫn có thể can thiệp vào phần
cứng thông qua lập trình C8051!
Ví dụ 9.4: Viết chương trình C51 cộng hai số nhị phân 16 bit kết quả trả về số nhị phân 16 bit và cờ báo tràn.
Giải:
#include <reg.51>
unsigned int x,y,z ;
bit bdata flagsum ;
main ( )
{
x=0xfffa ;
y=0xfffc ;
z=x + y ;
flagsum = CY ;
}
Sau khi biên dịch và chạy debug,quan sát các dòng lệnh tương ứng với các dòng lệnh assembly,mở
các cửa sổ thanh ghi và bộ nhớ RAM nội,ta thấy như sau:
 Đầu tiên khi khởi động C51 gọi chương trình con xóa toàn bộ bộ nhớ RAM nội từ 00H-7FH
 Tiếp theo là lệnh nạp SP giá trị 20H,giá trị này tùy thuộc vào khai báo các biến ở đầu chương trình.
Giải thích như sau:
 C51 chừa vùng bank 0 từ 00H-07H cho các thanh ghi R0-R7
 Dòng lệnh 2 khai báo các biến x,y,z là 3 số nguyên không dấu mặc định chứa trong bộ nhớ
data nên C51 gán địa chỉ x=08H-09H,địa chỉ y=0AH-0BH,địa chỉ z=0CH-0DH,địa chỉ thấp chứa byte
cao data.
 Dòng lệnh 3 khai báo biến flagsum là bit trong vùng bộ nhớ data định vị bit,C51 gán địa chỉ
flagsum=00H(địa chỉ bit 0 ô nhớ 20H).
 Do đó SP được nạp giá trị 20H
 Dòng lệnh x=0xfffa, C51 nạp (08H)=ff,(09H)=fa
 Dòng lệnh y=0xfffc,C51 nạp (0AH)=ff,(0BH)=fc
 Dòng lệnh z=x+y,C51 tính phép cộng và trả kết quả về (0CH)=ff,(0DH)=f6
 Dòng lệnh flagsum=CY,C51 gán giá trị cờ carry vào bit flagsum(<reg51.h> khai báo cờ carry bằng
ký hiệu CY).Ta sử dụng lệnh gán này vì biết chắc rằng C=1 khi kết quả phép cộng bị tràn!
 Ta cũng thấy rất rõ việc cộng 2 số nhị phân 16 bit viết bằng C 8051 rất đơn giản!
 Ta cũng thấy C51 không cho biết địa chỉ các toán hạng ,kết quả và cờ!Việc này sẽ gây khó cho người
lập trình trong trường hợp cần biết địa chỉ cụ thể!Tuy nhiên ta có thể giải quyết được vấn đề này bằng
khai báo con trỏ(pointer)trong phần sau.

9.6: Dãy(Arrays):
Thông thường một nhóm các biến dùng để lưu trữ data cùng một kiểu nên được gom chung thành một
dãy sẽ dễ truy xuất hơn.Chẳng hạn để xuất ra một thông điệp hay bảng tra mã 7 đoạn Anode chung như
trong ví dụ 4.7.Trong C 8051,ta sử dụng khai báo dãy array sẽ thuận tiện hơn khi truy xuất.
Ta xem hai ví dụ 9.5 và 9.6.
Ví dụ 9.5: Viết một đoạn chương trình xuất thông điệp “This is a message” ra port1.
Giải:
 Chương trình hợp ngữ
;Thông điệp có tổng cộng 17 ký tự kể cả khoảng trống
TAB_CHR: DB “This is a message”

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 4


MOV R7,#17
MOV DPTR,#TAB_CHR
LOOP: CLR A
MOVC A,@A+DPTR
MOV P1,A
INC DPTR
DJNZ R7,LOOP
 Chương trình C51:
#include <reg51.h>
unsigned char code tab_chr[] = "This is a message" ;
unsigned char i,x;
main()
{ i=0;
while (i<=16)
{
P1=tab_chr[i];
i++;
}
}
 Trong chương trình C51,ta khai báo array mã ASCII dãy ký tự thông điệp chứa trong bộ nhớ
code,mỗi ký tự dài một byte(do dãy ký tự nằm trong dấu “ “ nên không cần bao ngoài bằng dấu { })
 Chỉ số thứ tự dãy luôn bắt đầu bằng 0(chỉ vị trí ký tự “T”) kết thúc là 16(chỉ vị trí ký tự”e”),
nên ta khởi động biến chỉ số dãy i=0 và cho lập vòng while với điều kiện i<=16.
 Trong vòng lặp while ta gán P1= ký tự chỉ số i và tăng i thêm 1.
Ví dụ 9.6: Lập lại ví dụ 4.7,viết bằng C51.
Giải:
#include <reg52.h>
sbit SW = P2^0; // đặt SW=P2.0
char code seven_seg[16]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71}; // khai báo array mã 7 đoạn
char x,count; // khai báo biến x,count số nguyên 1 byte chứa trong RAM nội
void main()
{
P1=0 ; // tắt LED
while(1) // lặp vòng vô hạn
{
for(count=50;count>0;count--)
{
if(SW==1){count=50;}; // nhận dạng SW nhấn=0 ,đọc 50 lần
}
x=P3 ; // đọc data từ P3
x=x&0x0F ; // che nibble thấp
x=seven_seg[x] ; // lấy mã 7 đoạn theo chỉ số x từ array
P1=x ; // xuất ra P1
}
}
 Dòng 3 khai báo array bảng mã 7 đoạn gồm 16 ký tự chứa trong bộ nhớ code
 Dòng 6 khởi động chương trình ta thêm khai báo void báo module main không có kết quả
trả về để trình biên dịch không đưa ra cảnh báo “ missing return value”
 Chương trình lặp vòng vô hạn bằng lệnh while(1): điều kiện luôn thỏa mãn

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 5


 Nhận dạng SW nhấn=0 bằng lệnh if và vòng lặp for 50 lần đọc trạng thái phím để chống
rung phím ,sau 50 lần đọc nếu SW=0 sẽ thoát khỏi vòng lặp.
 Đọc data từ P3 gán vào biến x,che nibble thấp,x chính là chỉ số vị trí mã 7 đoạn trong
array,sau đó đọc mã 7 đoạn ứng với chỉ số x và xuất ra P1.
 Có thể ghép hai lệnh cuối bằng lệnh P1=seven_seg[x]; .Ở đây ta muốn gán vào x trước để
dễ theo dõi trong quá trình chạy debug.
 Dòng giải thích đặt sau dấu //

9.7: Cấu trúc(Structure):


Trong một số trường hợp các biến có kiểu dữ liệu khác nhau nhưng có liên quan đến nhau nên cần
được phân nhóm chung với nhau.Chẳng hạn tên,tuổi ngày sinh của một người có các kiểu dữ liệu khác nhau
nhưng đều là đặc tả người đó.Hoặc một hàm số y=f(x) với y kiểu data long,x kiểu data char cùng được khai
báo trong một array hai chiều thể hiện từng cặp giá trị(xi,yi) tương ứng.
Ta dùng khai báo structure cho các kiểu dữ liệu như trên.
Nhắc lại cú pháp structure:
struct [tên structure]
{ kiểu data biến 1;
kiểu data biến 2;
….
} [ danh sách biến kiểu structure];
Ta xem ví dụ khai báo biến structure cho một người như sau:
struct person
{ char name[20];
int age ;
long DOB ;
}
struct person An = {“Vo Van An”,20,12081999};
Để truy xuất biến riêng biệt trong kiểu structure ta chỉ cần gọi tên biến structure dấu(.) theo sau là tên
biến cần truy xuất như: An.name(Vo Van An),An.age(20),An.DOB(12081999)
Ví dụ 9.7: Để tính y=10x,ta lập các giá trị(xi,yi) với x=0,1,2,3,4.Viết một đoạn chương trình C51 nhập data
từ P1(từ 0-4),trả về giá trị x=P1 và y=10x.
Giải:
#include<reg51.h>
struct F // khai báo kiểu struct tên là F gồm
{
char x; // biến x kiểu char 1 byte
long y; // biến y kiểu long 4 byte
};
struct F GTR[]={0,1,1,10,2,100,3,1000,4,10000}; // biến GTR [x,y]là array 2 chiều kiểu struct
char a,i ;
long b ;
void main()
{
i=P1 ; // gán i=P1,giả sử P1=3→i=3
a=GTR[i].x ; // gán a= giá trị x trong array GTR vị trí i→a=3
b=GTR[i].y ; // gán b= giá trị y trong array GTR vị trí i→b=1000=03E8H
}

9.8: Con trỏ(Pointers):


Trong lập trình hợp ngữ,khi sử dụng định vị gián tiếp,ta thường dùng các thanh ghi R0,R1(8 bit)

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 6


DPTR(16bit) chứa địa chỉ ô nhớ data.Các thanh ghi này gọi là các con trỏ địa chỉ.
 Lưu ý:
 Khi truy xuất bộ nhớ RAM ngoài hay ngoại vi định vị gián tiếp địa chỉ 16 bit,ta dùng các
lệnh: MOVX A,@DPTR(đọc) hay MOVX @DPTR,A(ghi) với (DPTR)=địa chỉ truy xuất
 Khi đọc bộ nhớ code ta dùng lệnh: MOVC A,@A+DPTR với (DPTR)=địa chỉ bộ nhớ code
Tương tự ,trong C 8051 cũng cho phép truy xuất gián tiếp qua khai báo con trỏ,nội dung con trỏ
chính là địa chỉ của biến cần truy xuất data.Như vậy con trỏ cũng là một loại biến như các biến khác.
 Cú pháp khai báo con trỏ:
data_type * tên con trỏ ;
 data_type là kiểu dữ liệu của biến mà con trỏ chỉ đến
 Dấu * chỉ đây là biến con trỏ
 tên con trỏ chỉ biến con trỏ
Ví dụ ta khai báo:
int * Ptr ; // biến con trỏ tên Ptr chỉ đến kiểu dữ liệu int
int num ; // biến num có kiểu dữ liệu int
Ptr = &num ; // nội dung Ptr chính là địa chỉ của biến num
Toán tử & chỉ địa chỉ của biến theo sau nó.
Các khai báo trên có thể gom lại thành 2 dòng:
int num ;
int * Ptr = &num
Ví dụ 9.8 minh họa cách sử dụng con trỏ và giải thích C51 biên dịch trên bộ nhớ như thế nào.
Ví dụ 9.8:
#include<reg51.h>
char num ; // khai báo biến num kiểu char 1 byte
char * Ptr = &num ; // khai báo con trỏ Ptr = địa chỉ biến num kiểu char 1 byte
long a,x ; // khai báo biến a,x dài 4 byte để đọc địa chỉ(C51 qui định địa chỉ chung dài 3 byte)
void main()
{
num=P1 ; // đọc P1 gán vào biến num→giả sử (P1)=7EH
a= &num; // gán vào a địa chỉ num
P3= * Ptr ; // xuất nội dung biến trỏ bởi Ptr ra P3→(P3)=((Ptr))=(num)=7EH
x=&Ptr ; // gán vào x địa chỉ con trỏ Ptr
}
 Biên dịch và chạy debug ví dụ 9.8,mở IO port P1,P3 và bộ nhớ RAM nội.Quan sát bộ nhớ RAM nội
 C51 dự trữ địa chỉ 00H-07H cho R0-R7,gán địa chỉ 08H,09H,0AH cho con trỏ Ptr,gán địa chỉ 0BH
cho biến num,từ 0CH đến 0FH gán cho biến a,từ 10H đến 13H gán cho biến x,(SP)=13H
 Địa chỉ biến num=0BH nên (Ptr)=(08H-0AH)=00000BH
 Lệnh a=&num gán địa chỉ num vào a → (a)=0000000BH
 Lệnh P3=*Ptr đọc nội dung biến có địa chỉ trỏ bởi Ptr và xuất ra P3→(P3)=((0BH))=7EH
 Lệnh x=&Ptr đọc địa chỉ con trỏ Ptr gán vào x→C51 gán (x)=00000008H với 08H là địa chỉ đầu
trong 3 byte địa chỉ của con trỏ Ptr.
9.8.1: Kiểu bộ nhớ của con trỏ:
Trong ví dụ 9.8 ta không khai báo kiểu bộ nhớ con trỏ,C51 mặc định con trỏ được cất trong bộ nhớ
data trong(RAM nội)(3 byte từ 08H đến 0AH trong ví dụ 9.8).Nếu ta muốn cất con trỏ trong bộ nhớ
code,như trong ví dụ 9.8 ta sẽ khai báo:
char * code Ptr =&num ;
Dòng trên khai báo con trỏ Ptr chứa trong bộ nhớ code,trỏ vào data kiểu char cất trong biến num
(đương nhiên biến num phải khai báo kiểu char trước đó)
Khai báo con trỏ như trên gọi là khai báo kiểu bộ nhớ con trỏ và là kiểu bộ nhớ code.
 Lưu ý: Ta có 5 kiểu bộ nhớ con trỏ là data,bdata,xdata,code,idata

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 7


9.8.2: Con trỏ có kiểu(typed pointer):
Ta xem dòng khai báo sau:
int data * xdata Ptr = &num ;
Dòng trên khai báo con trỏ Ptr chứa trong bộ nhớ data ngoài,trỏ vào dữ liệu kiểu int cất trong
biến num và biến num này chứa trong bộ nhớ data trong.
Kiểu bộ nhớ data đặt trước dấu * định nghĩa kiểu bộ nhớ cho dữ liệu.Kiểu bộ nhớ xdata đặt sau dấu
* định nghĩa kiểu bộ nhớ cho con trỏ.
Khai báo con trỏ có định nghĩa rõ ràng kiểu bộ nhớ cho dữ liệu như trên gọi là con trỏ có kiểu(typed
pointer).Con trỏ có kiểu quyết định đến việc truy xuất bộ nhớ chứa dữ liệu,nơi mà con trỏ chỉ đến.Kích
thước của con trỏ có kiểu tùy thuộc vào kiểu bộ nhớ chứa dữ liệu và có thể dài 1 đến 2 byte.
Ta xem ví dụ 9.9 viết lại ví dụ 9.8 với khai báo con trỏ có kiểu.
Ví dụ 9.9: Viết lại ví dụ 9.8 với khai báo con trỏ có kiểu.
#include<reg51.h>
char num ;
char data * data Ptr = &num ; // khai báo biến num kiểu data,con trỏ kiểu data,cả hai đều
// chứa trong RAM nội
char a,x ; // khai báo biến đọc địa chỉ num và con trỏ Ptr chỉ 1 byte
void main()
{
num=P1 ;
a= &num;
P3= * Ptr ;
x=&Ptr ;
}
 Khi biên dịch chương trình C51 sẽ có 2 cảnh báo tại dòng 9 và 11 là “pointer truncation”,do C51 quy
định địa chỉ chung dài 3 byte,trong khi ta chỉ khai báo a và x chỉ dài 1 byte.Điều này không sao vì ta
biết chắc chắn địa chỉ RAM nội chỉ dài 1 byte
 Khi chạy debug như ví dụ 9.8 C51 gán địa chỉ Ptr=08H,địa chỉ num=09H,địa chỉ a=0AH và địa chỉ
x=0BH.Kết quả (a)=09H và (x)=08H.
 Lưu ý: Để truy xuất bộ nhớ data ngoài(RAM ngoài)hay ngoại vi định vị gián tiếp,bắt buộc phải
khai báo kiểu bộ nhớ cho data là xdata,kiểu bộ nhớ cho con trỏ nên sử dụng kiểu data để có thể
ghi địa chỉ cần truy xuất vào con trỏ!
Ví dụ 9.10: Viết một đoạn chương trình C51 nhập data từ P1 và xuất ra ô nhớ RAM ngoài địa chỉ 1000H.
Giải:
#include <reg51.h>
char num ;
char xdata * data Ptr ; // Ptr là con trỏ chứa trong RAM nội trỏ vào bộ nhớ RAM ngòai
void main()
{
num=P1 ; // đọc từ P1 gán vào biến num
Ptr=0x1000 ; // gán địa chỉ RAM ngoài 1000H cho con trỏ
*Ptr = num ; // xuất data ra ô nhớ RAM ngoài địa chỉ 1000H
}
 Với cách khai báo con trỏ Ptr có kiểu như trên dòng lệnh *Ptr = num tương đương như dòng lệnh hợp
ngữ MOVX @DPTR,A .
Ví dụ 9.11: Lập lại ví dụ 4.9 ,viết một chương trình C51 đọc data từ DIP_SW địa chỉ 4000H,tách thành 2
nibble,chuyển từng nibble sang mã 7 đoạn và xuất ra LED0 địa chỉ 2000H mã 7 đoạn nibble
thấp,xuất ra LED1 địa chỉ 3000H mã 7 đoạn nibble cao.
Giải:

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 8


#include <reg51.h>
const LED0 = 0X2000 ; // địa chỉ ngoại vi LED0 định vị gián tiếp
const LED1 = 0X3000 ; // địa chỉ ngoại vi LED1 định vị gián tiếp
const DIP_SW = 0X4000 ; // địa chỉ DIP_SW định vị gián tiếp
char num,tam ;
char xdata * data ptr ; // khai báo ptr là con trỏ chứa trong RAM nội trỏ địa chỉ data ngoài
char seven_tab[]={0XC0,0XF9,0XA4,0XB0,0X99,0X92,0X82,0XF8,0X80,
0X90,0X88,0X83,0XC6,0XA1,0X86,0X8E} ; // array mã 7 đoạn AC
void main()
{
num = 0xff ; // mã tối đèn
ptr = LED0 ; // ptr trỏ địa chỉ LED0
*ptr = num ; // tối LED0
ptr = LED1 ; // ptr trỏ địa chỉ LED1
*ptr = num ; // tối LED1
while (1) // lập vòng vô hạn
{
ptr = DIP_SW ; //ptr trỏ địa chỉ DIP_SW
num = *ptr ; //đọc data từ DIP_SW gán vào num
tam = num ; // cất num vao tam
num = num & 0x0f ; // che nibble thấp
num = seven_tab[num] ; // lấy mã 7 đoạn theo chỉ số num
ptr = LED0 ; // ptr trỏ địa chỉ LED0
*ptr = num ; // xuất mã 7 đoạn nibble thấp ra LED0
num = tam >> 4 ; // chuyển nibble cao sang nibble thấp(4 bit cao =0 )
num = num & 0x0f ; // che nibble thấp(thực ra không cần lệnh này)
num = seven_tab[num] ; lấy mã 7 đoạn theo chỉ số num
ptr = LED1 ; // ptr trỏ địa chỉ LED1
*ptr = num ; // xuất mã 7 đoạn nibble cao ra LED1
}
}
 Từ ví dụ 9.11 ta thấy chương trình C51 phải viết khá dài tương đương như chương trình hợp ngữ ở ví
dụ 4.9.Từ đó ta có nhận xét với các chương trình giao tiếp ngoại vi cần điều khiển phần cứng ,C51
không thể hiện sự mềm dẻo và rõ ràng hơn hợp ngữ!

9.8.3: Con trỏ không có kiểu(untyped pointer):


Nếu ta không định nghĩa rõ kiểu bộ nhớ cho dữ liệu khi khai báo con trỏ,ta có con trỏ không có kiểu.
Con trỏ không có kiểu có điểm thuận tiện là có thể trỏ đến data chứa trong bất kỳ loại bộ nhớ nào.Thông
thường con trỏ không có kiểu dài 3 byte và tất nhiên dài hơn con trỏ có kiểu(chỉ 1 hoặc 2 byte).Thực thi
chương trình với con trỏ không có kiểu lâu hơn vì không xác định được kiểu bộ nhớ cho data cho đến
thời điểm chương trình chạy đến.Byte đầu tiên của con trỏ không có kiểu chỉ loại bộ nhớ truy xuất data
như bảng 9.5.Byte 2 và 3 lần lượt là địa chỉ byte cao và byte thấp con trỏ chỉ đến.
Bảng 9.5: Giá trị trong byte 1 của con trỏ không có kiểu chỉ kiểu bộ nhớ chứa data:

Giá trị Kiểu bộ nhớ


00H data/idata/bdata
01H xdata
FEH pdata
FFH code

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 9


 Lưu ý: Khi sử dụng con trỏ không có kiểu, trình biên dịch tự động chọn loại bộ nhớ chứa data trỏ
đến,thường mặc định là bộ nhớ data trong(RAM nội).Tuy nhiên để dễ dàng quản lý bộ nhớ và
điều khiển phần cứng,ta nên khai báo con trỏ có kiểu sẽ thuận lợi hơn!
Ví dụ 9.12 minh họa việc sử dụng con trỏ trong chuyển vùng bộ nhớ mảng data.
Ví dụ 9.12: Viết một chương trình C51 chuyển 50 byte data cất trong RAM nội địa chỉ đầu 30H sang RAM
ngoài địa chỉ đầu 1000H.
Giải:
#include<reg51.h>
char i;
char * data y1 ; // con trỏ y1 không có kiểu cất trong RAM nội ,trỏ data 1 byte
char xdata * data y2 ; // khai báo con trỏ y2 có kiểu cất trong RAM nội, trỏ vào data bộ nhớ
// ngoài 1 byte
const MAX=50 ; // chiều dài mảng data
const X_ADDR = 0x1000 ; // địa chỉ đầu RAM ngoài
void main ()
{
y1=0x30 ; // y1 trỏ địa chỉ đầu RAM nội
for (i=0 ; i < MAX ; i++) // tạo mảng data trong RAM nội để chạy chương trình
{
*y1 = i ;
y1++ ;
}
// Phần chính của ví dụ bắt đầu từ đây:
y1= 0x30 ; // y1 trỏ địa chỉ đầu RAM nội
y2 = X_ADDR ; // y2 trỏ địa chỉ đầu RAM ngoài
for (i =0 ; i <MAX ; i++)
{
*y2 = *y1 ; // chuyển data từ RAM nội ra RAM ngoài
y1++ ; // tăng con trỏ RAM nội( tăng 1 vì khai báo char 1 byte)
y2++ ; // tăng con trỏ RAM ngoài(tăng 1 vì khai báo char 1 byte)
}
}

9.9: Hàm(Function):
Trong lập trình hợp ngữ,ta có các lệnh gọi chương trình con(subroutine).Trong C51 cũng có các lệnh
gọi chương trình con tương tự như vậy nhưng ta gọi là hàm(Function).
Cú pháp khai báo hàm đầy đủ trong C51 như sau:
return_type function_name ( arguments) [memory] [reentrant] [interrupt] [using]
 return_type là kiểu data của giá trị trả về(output)
 function_name là tên hàm,ta sẽ gọi tên hàm khi muốnthực hiện hàm
 arguments là danh sách số lượng và kiểu data của các biến vào(inputs)
 memory xác định mô thức bộ nhớ(small,compact,large) của các biến vào(arguments)
 reentrant xác định hàm có đệ quy(recursive) hay không
 interrupt xác định hàm có phải thật sự là trình phục vụ ngắt(ISR)
 using xác định bank thanh ghi sử dụng cho hàm
 Các lưu ý quan trọng:
 Mỗi hàm chỉ trả về một giá trị (một output)
 Kết thúc hàm bằng phát biểu return (output) để trình biên dịch trả về giá trị output.Nếu không
có giá trị trả về không cần phát biểu này.
 Khi gọi thực hiện hàm chỉ cần viết tên hàm(function_name) và gán các giá trị cho các input

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 10


 Nếu đã khai báo và định nghĩa hàm(các phát biểu thực hiện hàm) ở đầu chương trình trước khi
thực hiện chương trình chính,trong chương trình chính gọi hàm bình thường.
 Nếu thực hiện gọi hàm trước khi định nghĩa hàm(định nghĩa hàm được đặt ở cuối chương trình),
ta phải khai báo hàm ở đầu chương trình cùng với các khai báo biến.
 Một hàm có thể không cần giá trị trả về hay các biến ngõ vào,ta chỉ cần khai báo:
void function_name (void) ;
Ví dụ 9.13 minh họa cách khai báo,định nghĩa và gọi hàm.
Ví dụ 9.13: Viết một chương trình cộng hai số nhị phân 16 bit,kết quả trả về số nhị phân 32 bit.
Giải:
#include<reg51.h>
#define uint unsigned int // định nghĩa uint là số 2 byte không dấu
#define ulong unsigned long // định nghĩa ulong là số 4 byte không dấu
uint x,y ;
ulong z ;
ulong sum ( uint a,uint b) ; // khai báo hàm tên sum output ulong,2 input uint
void main()
{
x = 0xffff ;
y = 0xffff ;
z = sum (x,y) ; // gọi hàm gán giá trị x=a,y=b ,z=output
}
ulong sum ( uint a,uint b) // định nghĩa hàm
{
return a+b ; // output =a+b
}
 Để khai báo số không dấu ta dùng 2 dòng định nghĩa ký hiệu uint và ulong viết tắt cho thuận tiện
 Ta khai báo hàm tên sum có giá trị trả về kiểu ulong 4 byte,ngõ vào 2 biến a và b kiểu uint 2 byte,
phần còn lại không khai báo trình biên dịch sẽ mặc định mô thức bộ nhớ small(RAM nội),không đệ
quy,không phải ISR và dùng bank 0.
 Ta mong muốn chương trình trả về kết quả phép cộng >16 bit nên khai báo ulong 4 byte.Với x và y
gán như trên kết quả mong muốn là z=0001FFFEH
 Khi thực hiện hàm kết quả trả về trong các thanh ghi R4÷R7=0000fffe và do đó z= 0000fffe,không
như mong muốn!Tuy nhiên ta có thể đọc bit 17 bằng cờ carry như ví dụ 9.4.
 Nếu ta khai báo a và b kiểu ulong kết quả trả về sẽ đúng như mong muốn!

9.9.1: Chuyển thông số(Parameter Passing):


C51 quy định các giá trị ngõ vào hàm và lưu trữ trong quá trình thực hiện hàm thông qua các
thanh ghi trong bank thanh ghi hoặc các vị trí ấn định trong bộ nhớ.Thông thường mặc định là các thanh
ghi,nếu không đủ sẽ chuyển sang các ô nhớ.Bảng 9.6 mô tả các thanh ghi dùng để chuyển thông số.
Bảng 9.6: Các thanh ghi dùng để chuyển thông số

Số ngõ Char/1 byte Int/2 byte Long/float Pointer


vào pointer pointer chung
1 R7 R6&R7 R4-R7 R1-R3
2 R5 R4&R5 R4-R7
3 R3 R2&R3
9.9.2: Giá trị trả về(output):
C51 quy định giá trị trả về từ hàm luôn nằm trong các thanh ghi.Bảng 9.7 mô tả các thanh ghi
chứa giá trị trả về từ hàm với các liểu dữ liệu khác nhau.

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 11


Bảng 9.7: Các thanh ghi chứa giá trị trả về từ hàm

Kiểu trả về Thanh ghi Mô tả


bit Cờ carry (C)
char/unsigned char/ 1 byte pointer R7
int/unsigned int/2 byte pointer R6&R7 MSB R6,LSB R7
long/unsigned long R4-R7 MSB R4,LSB R7
float R4-R7 32 bit IEEE format
pointer chung R1-R3 Kiểu bộ nhớ trong R3,MSB R2,
LSB R1

9.9.3: Ngắt (Interrupt):


Trường hợp khai báo hàm là trình phục vụ ngắt(ISR),C51 quy định khai báo các loại ngắt theo
chỉ số như bảng 9.8.
Bảng 9.8: Các chỉ số tương ứng các loại ngắt trong 8051
Chỉ số ngắt Mô tả Địa chỉ vector ngắt
0 Ngắt ngoài INT0 0003H
1 Ngắt Timer0 000BH
2 Ngắt ngoài INT1 0013H
3 Ngắt Timer1 001BH
4 Ngắt cổng nối tiếp 0023H
5 Ngắt Timer2(8052) 002BH
Ví dụ muốn khai báo ngắt ISR Timer0,ta viết:
void T0ISR (void) interrupt 1
Dòng trên khai báo hàm tên T0ISR không cần input và output,là ISR Timer0(interrupt 1)
Các ví dụ sau minh họa thêm về cách viết hàm trong C51.
Ví dụ 9.14: Viết một chương trình đọc một số nhị phân 16 bit từ P1(MSB) và P0(LSB),chuyển sang số BCD
không nén,cất các số BCD này vào các ô nhớ RAM nội địa chỉ đầu là 30H(chứa decade thấp
nhất) đến 34H (chứa decade cao nhất).
Giải:
Ta đã biết giải thuật chuyển từ số nhị phân sang BCD là lần lượt chia liên tục số nhị phân và thương
số phép chia cho 0AH,cho đến khi thương số bằng 0,dư số phép chia đầu tiên là số BCD decade thấp nhất,dư
số phép chia cuối cùng là số BCD decade cao nhất.
#include<reg51.h>
#define uint unsigned int // định nghĩa ký hiệu uint thay cho unsigned int
uint x,y,z ;
char u,*p ; // u là biến toàn cục 1 byte,khai báo con trỏ địa chỉ 1 byte
uint Comb_16 ( uint a, uint b) ; // khai báo hàm Comb_16 tạo số nhị phân 16 bit
uint Div16_16 (uint a, uint b) ; // khai báo hàm Div16_16 chia 2 số nhị phân 16 bit
void main()
{
x = P0 ; // đọc data từ P0
y = P1 ; // đọc data từ P1
y = Comb_16 (y,x) ; // gọi hàm tạo số nhị phân 16 bit (y)= (P1-P0)
x=0x0a ; // số chia (x)=10
z=y; // z=số bị chia hoặc thương số ,lưu lại y
p = 0x30 ; // con trỏ địa chỉ đầu 30H
for (i=0;i<=4;i++) // xóa 5 ô nhớ địa chỉ đầu trỏ bởi p
{ *p=0 ;
p++ ; }

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 12


p = 0x30 ; // con trỏ địa chỉ đầu 30H
do // lặp vòng
{
z = Div16_16 (z,x) ; // gọi hàm trả kết quả z = thương số,u= dư số
*p = u ; // cất dư số vào ô nhớ trỏ bởi p
p++ ; // tăng địa chỉ ô nhớ
}
while (z !=0 ) ; // thoát khi thương số =0
}
uint Comb_16 ( uint a, uint b) // hàm tạo số nhị phân 16 bit
{
a = a<<8 ; // dịch byte thấp lên byte cao số thứ nhất
a= a|b; // OR byte cao với byte thấp số thứ hai
return a ; // trả về số nhị phân 16 bit
}
uint Div16_16 (uint a, uint b) // hàm chia 2 số nhị phân 16 bit
{
u = a%b ; // dư số phép chia,lưu ý u là biến toàn cục
return a/b; // trả kết quả về là thương số phép chia
}
 Ta thấy thực hiện phép chia 2 số 16 bit viết bằng C51 rất dễ chỉ bằng 2 phát biểu,trong khi điều này
viết bằng hợp ngữ khá khó khăn!(tất nhiên dung lượng mã và thời gian thực thi chương trình C51 dài
hơn nhiều so với hợp ngữ)
 Điểm cần nhấn mạnh ở đây là hàm chỉ trả về một kết quả hay một output,trong khi ta cần thương số
và dư số của phép chia!Để giải quyết việc cần trả về hai kết quả,ta dùng một biến toàn cục(global
variable) u khai báo trong chương trình chính(ngoài định nghĩa hàm) và chọn output của hàm là
thương số phép chia.Do đó,sau khi gọi hàm ta sẽ có đồng thời 2 output thương số và dư số phép chia.
 Thực hiện vòng lặp do… while và con trỏ gán địa chỉ để chuyển dư số vào các ô nhớ RAM nội định
trước,thoát khỏi vòng lặp khi thương số bằng 0.
 Ở đây C51 luôn xóa các ô nhớ RAM nội từ 00-7FH khi mới khởi động, tuy nhiên ta phải xóa 5 ô nhớ
chứa số bcd trỏ bởi p trước khi gọi hàm Div16_16,do phép chia dừng khi thương số bằng 0 và mặc
định các số bcd decade cao bằng 0.
Ví dụ 9.15: Viết một chương trình C51 tạo chuỗi xung vuông đối xứng tần số 10Khz xuất ra P1.7 dùng
Timer1 và tần số 1Khz xuất ra P1.6 dùng Timer0.Sử dụng ngắt cho cả 2 timer.
Giải:
#include<reg51.h>
sbit pulse_10k = P1^7 ; // đặt biến cho P1.7
sbit pulse_1k = P1^6 ; // đặt biến cho P1.6
void main()
{
TMOD = 0X21 ; // Timer1 mode 2,Timer0 mode 1 chạy timer
TH1 = 0XCE ; // giá trị reload = -50
TL1 = TH1 ; // reload Timer1 từ đầu
IE = 0X8A ; // cho phép ngắt 2 timer
PT1 = 1 ; // ưu tiên ngắt Timer1do tần số cao hơn
TR1 = 1; // chạy Timer1
TF0 = 1; // ép ngắt Timer0
while (1) ; // lặp vòng vô hạn
}
void T1ISR (void) interrupt 3 // hàm ISR Timer1 không sử dụng input,output

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 13


{
pulse_10k = !pulse_10k ; // đảo biến sau mỗi lần ngắt
}
void T0ISR (void) interrupt 1 // hàm ISR Timer0 không sử dụng input,output
{
TR0 = 0 ; // ngừng Timer0
TH0 = 0XFE ; // reload giá trị -500
TL0 = 0X0C ;
TR0 = 1 ; // chạy Timer0
pulse_1k = !pulse_1k ; // đảo biến sau mỗi lần ngắt
}
 Chương trình C51 được viết theo trình tự cũng tương tự như hợp ngữ.Ở đây ta lưu ý khai báo hàm là
trình phục vụ ngắt(ISR) cho Timer0(interrupt 1) và Timer1(interrupt 3).Vì đây là ISR nên không cần
phát biểu gọi hàm trong chương trình chính,do đó không cần khai báo hàm ở đầu chương trình!

9.10: Tạo thời gian trễ(delay) bằng C51:


Khi viết chương trình hợp ngữ,ta có thể sử dụng timer hoặc tính thời gian thực thi các lệnh tạo vòng
lặp dễ dàng để tạo thời gian trễ như mong muốn.Đối với chương trình C51,tạo thời gian trễ dùng timer cũng
tương tự,nhưng rất khó để tạo thời gian trễ bằng các lệnh lặp vòng vì ta không biết cụ thể thời gian thực thi.
Tuy nhiên,ta có thể tính thời gian thực thi các lệnh lặp vòng,thông qua quan sát thời gian thực thi lệnh
của trình biên dịch,từ đó tìm biểu thức tính thời gian trễ của các lệnh lặp vòng.
Ta xem ví dụ 9.16 minh họa cách tính thời gian lặp vòng lệnh for.
Ví dụ 9.16: Tìm biểu thức tính thời gian lặp vòng của phát biểu for,từ đó viết một hàm tạo thời gian trễ như
mong muốn.
Giải:
Trước tiên ta cho chạy chương trình sau và lưu ý thời gian thực thi lệnh,đặt fck=12Mhz.
#include <reg51.h>
unsigned int i,MAX ;
void main()
{
MAX= 1 ;
for ( i=1;i<=MAX;i++)
}
Lần lượt thay đổi MAX và chạy chương trình,ghi lại các thời điểm thực hiện lệnh ,ta có bảng
vd9.6.1
 Thời gian C51 khởi động : 389µs , thời gian bắt đầu lệnh for :T1= 393µs (lệnh gán biến MAX 4µs)
Bảng vd9.6.1: Tfor là thời điểm chương trình chạy xong lệnh for,Td là thời gian cho lệnh for
MAX Tfor(µs) Td=Tfor-T1(µs) Mô tả
1 417 24 24=1x11+13
2 428 35 35=2x11+13
3 439 46 46=3x11+13
10 516 123 123=10x11+13
50 956 563 563=50x11+13
100 1506 1113 1113=100x11+13
Từ bảng vd9.6.1 ta thấy thời gian thực thi 1 vòng lặp for là 11µs với offset 13µs
Ta suy ra biểu thức tính Td:
Td= MAX * 11+13 (9.1)
Như vậy muốn tạo trễ thời gian Td ta nạp cho vòng for giá trị MAX làm tròn đến 0.5:
MAX= (Td-13)/11 (9.2)
Từ bảng trên ta thấy vòng lặp for tạo thời gian trễ tối thiểu 24µs !

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 14


Thử lại công thức (9.2) và xem sai số bằng chương trình sau:
#include <reg51.h>
unsigned int i,Td,MAX ;
void main()
{
Td=50
MAX= (Td-13)/11 ;
for ( i=1;i<=MAX;i++)
}
Chạy chương trình trên và ghi lại các mốc thời gian với các giá trị Td khác nhau,ta có bảng vd9.6.2.
Bảng vd9.6.2: T2 là thời điểm bắt đầu chạy vòng for,T1=393µs bắt đầu lệnh tính MAX
Td(µs) T2(µs) Tfor(µs) Texe= Tfor-T2(µs) e=Texe-Td(µs)
50 423 469 46 -4
100 423 513 90 -10
200 423 623 200 0
500 538 1035 497 -3
1000 540 1532 992 -8
2000 540 2533 1993 -7
5000 540 5539 4999 -1
10000 540 10539 9999 -1
50000 536 50584 50048 +48
 Do muốn nạp giá trị Td để tính MAX cho C51 tính nên không làm tròn,sai số hơi cao ở vùng Td
<200µs
 Vùng Td cao từ 200µs trở lên sai số thấp
 Thời gian thực hiện lệnh tính giá trị MAX không cố định ,Td ≤200µs là 30µs,Td≥1000µs là 147µs.
Do đó ta không gom lệnh tính MAX vào trong phần tạo thời gian trễ được!Ta phải tính giá trị MAX
ngoài chương trình!
Bây giờ ta phát triển lên thành hàm tạo thời gian trễ bằng chương trình sau:
#include <reg51.h>
unsigned int Td ,MAX;
void DELAY ( unsigned int t)
{ unsigned int i ;
for (i=1; i <= t ; i++);
}
void main()
{
Td = 50;
MAX =(Td-13)/11;
DELAY(MAX) ;
}
Thực hiện tương tự như trên với Td thay đổi ta có bảng vd9.6.3.
Bảng vd9.6.3: T2 thời điểm gọi hàm DELAY,TDE thời điểm thoát khỏi hàm DELAY
Td(µs) T2(µs) TDE(µs) Texe=TDE-T2(µs) Mô tả
50 423 472 49 49=3x12+13
100 423 520 97 97=7x12+13
200 423 640 217 217=17x12+13
500 538 1079 541 541=44x12+13
1000 540 1621 1081 1081=89x12+13
 Tính MAX theo Td và viết biểu thức theo Texe,ta suy ra một vòng lặp for bây giờ là 12µs !
 Do đó muốn viết hàm DELAY như trên tạo thời gian trễ,ta điểu chỉnh biểu thức (9.2) thành:
Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 15
MAX=(Td-13)/12 (9.3)
Sau khi tính lại MAX theo (9.3),chạy lại chương trình trên với Td thay đổi,ta có bảng vd9.6.4.

Bảng vd9.6.4:
Td(µs) T2(µs) TDE(µs) Texe=TDE-T2(µs) E=TDE-Td(µs)
50 423 472 49 -1
100 423 520 97 -3
200 423 616 193 -7
500 536 1029 493 -7
1000 540 1535 997 -3
2000 540 2533 1993 -7
5000 544 5538 4994 -6
10000 534 10534 10000 0
50000 538 50547 50009 +9
 Với khai báo MAX là biến unsigned int hàm DELAY(MAX) có thể tạo thời gian trễ tối đa với
MAX=65535 là khoảng 0.786 s
Dựa vào các kết quả trên, ta phát triển lên hàm tạo thời gian trễ hàng giây.Ví dụ như viết một hàm tạo
thời gian trễ 1s.
#include <reg51.h>
unsigned int MAXi,MAXj;
unsigned long Td ; // khai báo Td long để tăng MAXi tối đa =65535
void DELAY_S ( unsigned int t1,t2)
{ unsigned int i ,j ;
for (j=1;j<=t2;j++)
{
for (i=1; i <= t1 ; i++);
}
}

void main()
{
Td = 50000; // vòng for i tạo trễ 50ms
MAXi=(Td-13)/11 ;
MAXj=20 ; // vòng for j tạo trễ =20x50ms=1s
DELAY_S(MAXi,MAXj) ;
}
 Hàm DELAY_S có 2 vòng for,khi chạy thực tế trên C51 thời gian thực thi 1 vòng lặp for cho i là
11µs như biểu thức (9.2).Do đó ta tính MAXi theo (9.2).
 Tính MAXj cho vòng for j như sau:
Gọi Tj là thời gian trễ cần tạo,Ti là thời gian thực thi xong vòng for i với MAXi theo (9.2).
Tj=MAXj*Ti
Suy ra: MAXj=Tj/Ti (9.4)
 Tóm lại, từ ví dụ trên ta có thể viết đoạn chương trình hoặc hàm tạo trễ từ 24µs đến hàng trăm s dựa
vào 3 biểu thức (9.1),(9.2),(9.3) và 3 mẫu chương trình trên!

9.11: Các ví dụ tổng quát viết bằng C51:

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 16


Phần sau đây sẽ trình bày các ví dụ tổng quát viết bằng C51 minh họa các phần đã học ở các chương
trước như xử lý data,giao tiếp bộ nhớ,ngoại vi định vị trực tiếp,gián tiếp,Timer,cổng nối tiếp và ngắt.Trong
các ví dụ này sẽ minh họa lại các lệnh lặp vòng,điều kiện,toán tử sử dụng trong C.
Ví dụ 9.17: Lập lại ví dụ 4.8,viết một hàm xuất data ra IC 74LS164 như hình 4.8.Viết một chương trình nhập
data từ P0 khi có tín hiệu từ P1.2 =0 và gọi hàm xuất data ra shift reg. 74LS164.
Giải:
#include <reg51.h>
#define inport P0 // định nghĩa biến inport=P0
sbit sda = P1^0 ; // ngõ data serial in shift reg. sda=P1.0
sbit sck = P1^1 ; // ngõ xung CK shift reg. sck=P1.1
sbit SW = P1^2 ; // điều khiển SW=P1.2
char x ;
void SHIFT_OUT (char a) // hàm dịch bit byte data ra shift reg., MSB dịch trước
{
char i, tam ;
tam = PSW ; // cất PSW
ACC = a ; // nạp byte data cần dịch vào Acc
sck = 0 ; // xóa xung CK
for (i=1;i<=8;i++) // vòng lặp dịch 8 lần
{
ACC = ACC << 1 ; // dịch trái Acc, C= Acc.7
sda = CY ; // xuất bit data ra sda
sck = 1 ; // tạo cạnh lên xung CK
sck = 0 ;
}
PSW = tam ; // phục hồi PSW
}
void main() // bắt đầu chương trình chính
{
for ( ; ;) // lặp vòng vô hạn
{
if ( SW == 0) // kiểm tra SW=0 ?
{
x = inport ; // đọc byte data
SHIFT_OUT (x) ; // gọi hàm dịch bit ra shift reg.
}
}
}
 Đầu chương trình ta khai báo và định nghĩa hàm SHIFT_OUT có input biến a là byte data cần dịch
nối tiếp ra shift reg. 71LS164.Trong chương trình chính ta gọi hàm mà không cần khai báo nữa.
 Trong phần thực hiện hàm,ta cất PSW để cất cờ C vì lệnh dịch bit chắc chắn sẽ sử dụng cờ C.Sử dụng
thanh ghi Acc dịch bit để thời gian thực hiện nhanh;lưu ý là lệnh ACC = ACC << 1 thực hiện chuyển
bit Acc.7 vào C và đồng thời dịch trái các bit trong Acc.Do đó tiếp theo ta chỉ cần chuyển nội dung C
ra bit sda và tạo xung CK để dịch vào shift reg..Quá trình dịch bit thực hiện 8 lần bằng vòng lặp
for.Cuối cùng ta phục hồi lại PSW trước khi thoát khỏi hàm.
 Trong phần chương trình chính, ta tạo vòng lặp vô hạn bằng lệnh for ( ; ;) thay cho các ví dụ trước
dùng lệnh while (1).Lệnh có điều kiện if (SW==0) kiểm tra nếu SW=0 sẽ đọc data từ P0 vào biến x
và gọi hàm dịch bit.
Ví dụ 9.18: Viết một chương trình bao gồm các tác vụ sau:
 Khởi động cổng nối tiếp phát/thu tốc độ baud=9600(fck=11.059Mhz)

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 17


 Một hàm phát ký tự ASCII có cẩn parity bit kiểm tra lẻ
 Một hàm thu ký tự ASCII và set cờ C nếu kiểm tra ký tự thu bị sai parity lẻ
 Nhập 1 ký tự từ P0,phát ra cổng nối tiếp,thu lại cũng từ cổng nối tiếp xuất ký tự thu được ra P1
và trạng thái cờ C ra P2.0
Giải:
#include <reg51.h>
#define outbyte P1 // định nghĩa biến outbyte là P1
#define inbyte P0 // định nghĩa biến inbyte là P0
sbit AccMSB = ACC^7 ; // khai báo biến AccMSB=Acc.7
sbit errFlag = P2^0 ; // khai báo biến errFlag = P2.0
void OUTCHAR () ; // Khai báo hàm phát ký tự ra cổng nối tiếp
void INCHAR () ; // khai báo hàm thu ký tự từ cổng nối tiếp
void main() // bắt đầu chương trình chính
{
SCON = 0X52 ; // khởi động cổng nối tiếp phát /thu cờ TI=1 sẵn sàng phát
TMOD = 0X20 ; // Timer1 mode 2 tạo baudrate
TH1 = -3 ; // baudrate 9600
TR1 = 1 ; // chạy Timer1
while (1) // lặp vòng vô hạn
{
ACC = inbyte ; // nhập ký tự vào Acc
OUTCHAR () ; // gọi hàm xuất ký tự ra cổng nối tiếp
INCHAR () ; // gọi hàm thu ký tự từ cổng nối tiếp
outbyte = ACC ; // xuất ký tự thu được ra outbyte
errFlag = CY ; // xuất cờ kiểm tra parity
}
}
void OUTCHAR () // định nghĩa hàm xuất ký tự
{
CY = P ; // lấy bit parity chẵn
CY = !CY ; // đảo bit parity thành parity lẻ
AccMSB = CY ; // gán bit parity lẻ vào Acc.7
while (TI !=1) ; // chờ TI=1
TI = 0 ; // xóa TI
SBUF = ACC ; // xuất ký tự ra cổng nối tiếp
AccMSB = 0 ; // trả nội dung Acc lại như cũ
}
void INCHAR () // định nghĩa hàm thu ký tự
{
while (RI!=1) ; // chờ RI=1
RI = 0 ; // xóa RI
ACC = SBUF ; // nạp ký tự thu được vào Acc
CY = P ; // lấy bit parity Acc,nếu nhận đúng P luôn luôn bằng 1
CY = ! CY ; // nếu parity sai C=1
AccMSB = 0 ; // xóa bit parity cẩn trong ký tự
}
 Trong chương trình trên,việc phát/thu ký tự qua cổng nối tiếp phần cứng chắc chắn sử dụng các thanh
ghi Acc,SBUB,các cờ TI,RI,và P,C để kiểm tra parity nên ta sử dụng trực tiếp các thanh ghi và cờ
này chương trình sẽ chạy nhanh và đơn giản hơn!

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 18


 Khai báo hàm OUTCHAR và INCHAR ở đầu chương trình chính vì ta viết hàm sau.Do hai hàm này
không cần giá trị trả về nên ta khai báo void trước nó để trình biên dịch không đưa ra cảnh báo.Danh
sách arguments trong ngoặc cũng ghi void hoặc để trống,tốt nhất nên để trống.Khi gọi hàm phần danh
sách arguments trong ngoặc phải để trống,nếu ghi void trình biên dịch sẽ báo lỗi!
 Lệnh while (TI!=1); tương đương như lệnh JNB TI,$ và while (RI!=1) ; tương đương như lệnh
JNB RI,$
Ví dụ 9.19: Viết một chương trình C51 thực hiện các công việc thu/phát ký tự qua cổng nối tiếp như sau:
 Đầu tiên chương trình chờ thu mã lệnh : nếu mã lệnh là R_COM sẽ tiến hành chương trình thu
ký tự,nếu mã lệnh là T_COM sẽ tiến hành chương trình phát ký tự.
 Chương trình thu ký tự được thực hiện bằng ngắt cổng nối tiếp,thu chuỗi ký tự và cất trong
RAM ngoài địa chỉ đầu 1000H,kết thúc thu ký tự khi thu được ký tự NULL =0 (cất luôn ký tự
NULL) hoặc thu đủ 50 ký tự tùy điều kiện nào thỏa mãn trước,báo kết thúc thu xong chuỗi ký
tự.
 Chương trình phát ký tự được thực hiện bằng ngắt cổng nối tiếp,phát chuỗi ký tự cất trong
RAM nội địa chỉ đầu 30H,kết thúc phát ký tự khi phát tới ký tự NULL(phát luôn ký tự
NULL) hoặc phát đủ 50 ký tự tùy điều kiện nào thỏa mãn trước,báo kết thúc phát xong chuỗi
ký tự.
 Trong quá trình phát/thu chuỗi ký tự,chương trình chính chờ báo kết thúc phát/thu xong chuỗi
ký tự và quay về từ đầu chờ thu lệnh mới.
 Cổng nối tiếp làm việc như UART 8 bit,baudrate= 9600bps,cho fck=11.059Mhz.
Giải:
Chương trình sau đây sẽ minh họa kỹ thuật sử dụng nhiều vòng lặp lồng vào nhau,biểu thức điều
kiện,con trỏ xác định vị trí ô nhớ RAM nội,RAM ngoài,kỹ thuật điều khiển ngắt cổng nối tiếp,phân biệt ngắt
phát hay ngắt thu.
#include <reg51.h>
const char i_addr =0x30 ; // khai báo hằng số biến i_addr 1 byte trỏ đ/c đầu RAM nội
const x_addr =0x1000; // khai báo hằng số biến x_addr mặc định 2 byte trỏ đ/c đầu RAM ngoài
const MAX = 50 ; // khai báo chiều dài chuỗi ký tự
const char R_COM = 0X52 ; // khai báo hằng số mã lệnh thu R_COM 1 byte=ký tự “R”
const char T_COM = 0X54 ; // khai báo hằng số mã lệnh phát T_COM 1 byte= ký tự “T”
bit R_Flag ,T_Flag ; // khai báo R_Flag cờ báo thu xong,T_Flag cờ báo phát xong
char data * pt ; // khai báo pt là con trỏ phát truy xuất data RAM nội
char xdata * pr ; // khai báo pr là con trỏ thu truy xuất data RAM ngoài
char i ,x; // khai báo i là chỉ số đếm,x là biến tạm
void main()
{
SCON = 0X50 ; // khởi động cổng nối tiếp UART 8 bit cho phép thu
TMOD = 0X20 ; // khởi động baudrate=9600
TH1 = -3 ;
TR1 = 1 ;
while (1) // lặp vòng vô hạn
{
R_Flag = 0 ; // xóa cờ báo kết thúc thu
T_Flag = 0 ; // xóa cờ báo kết thúc phát
IE = 0 ; // cấm ngắt
while (RI!=1) ; // chờ thu mã lệnh
RI = 0 ; // thu đủ ký tự xóa RI
x = SBUF ; // đọc ký tự thu được
if (x == R_COM) // nếu là lệnh R_COM thực hiện chương trình thu ký tự
{

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 19


TI = 0 ; // xóa cờ ngắt phát lần cuối trước đó
IE = 0x90 ; // cho phép ngắt cổng nối tiếp
i=0; // khởi động biến đếm =0
pr = x_addr ; // con trỏ thu chỉ địa chỉ đầu RAM ngoài
while (R_Flag != 1) ; // chờ cờ báo kết thúc thu =1
R_Flag = 0 ; // xóa cờ báo kết thúc thu
} // kết thúc điều kiện if và thoát
else // mã lệnh khác R_COM xét tiếp
if (x == T_COM) // mã lệnh= T_COM thực hiện chương trình phát ký tự
{
IE = 0x90 ; // cho phép ngắt cổng nối tiếp
i=0; // khởi động biến đếm=0
pt = i_addr ; // con trỏ phát chỉ địa chỉ đầu RAM nội
REN = 0 ; // cấm thu
TI = 1 ; // kích ngắt phát lần đầu do SBUF rỗng
while (T_Flag != 1) ; // chờ cờ báo kết thúc phát =1
T_Flag = 0 ; // xóa cờ báo kết thúc phát
REN = 1 ; // cho phép thu lại
} // kết thúc điều kiện else if và thoát
} // thoát về while(1)
} // kết thúc chương trình chính
void SENDCHAR () interrupt 4 // khai báo ngắt cổng nối tiếp
{
if ( TI == 1) // kiểm tra ngắt phát?
{ // thực hiện ngắt phát
TI = 0 ; // xóa TI
x = *pt ; // lấy data đ/c trỏ bởi pt
SBUF = x ; // phát ký tự
pt++ ; // con trỏ chỉ địa chỉ kế tiếp
i++ ; // tăng biến đếm
if (x==0 || i== MAX) // nếu ký tự=0 hoặc biến đếm=MAX kết thúc phát
{
T_Flag = 1; // Set cờ báo phát xong
}
} // thoát điều kiện TI=1
else // không phải ngắt phát
if ( RI == 1) // kiểm tra ngắt thu ?
{ // thực hiện ngắt thu
RI = 0 ; // xóa RI
x = SBUF ; // lấy ký tự thu được
*pr = x ; // cất ký tự vào RAM ngoài đ/c trỏ bởi pr
pr++ ; // con trỏ chỉ đ/c kế tiếp
i++ ; // tăng biến đếm
if (x == 0 || i == MAX) // nếu ký tự = 0 hoặc biến đếm=MAX kết thúc thu
{
R_Flag = 1 ; // Set cờ báo thu xong
}
} // thoát điều kiện RI=1
} // kết thúc ISR

 Đầu tiên ta phải lưu ý các khai báo:


Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 20
 Khai báo hằng số biến i_addr nên là dạng char 1 byte để chỉ địa chỉ RAM nội dài chỉ 1 byte
tiết kiệm được một ô nhớ RAM nội.Tương tự như vậy cho khai báo biến R_COM và T_COM.
 Khai báo con trỏ pt phải là kiểu data để trỏ vào bộ nhớ RAM nội
 Khai báo con trỏ pr phải là kiểu xdata để trỏ vào bộ nhớ RAM ngoài
 Các khai báo cho các biến trên không quy định kiểu bộ nhớ chứa biến nên mặc định các biến
này được chứa trong RAM nội.
 Trong trường hợp số biến quá nhiều có thể chiếm mất phần lớn bộ nhớ RAM nội,ta nên cân
nhắc lại việc này sau cho có đủ không gian RAM nội để sử dụng!
 Đầu chương trình,chưa cho phép ngắt cổng nối tiếp,chương trình chờ thu mã lệnh để thực hiện việc
phát hay thu chuỗi ký tự.
 Nếu mã lệnh là R_COM,chương trình sẽ khởi động biến đếm số ký tự và con trỏ địa chỉ đầu
bộ nhớ RAM ngoài để cất chuỗi ký tự thu được,khởi động ngắt cổng nối tiếp và chờ cờ
R_Flag set báo thu xong.
 Nếu mã lệnh là T_COM,chương trình sẽ khởi động biến đếm số ký tự và con trỏ địa chỉ đầu
RAM nội cất chuỗi ký tự cần phát,khởi động ngắt cổng nối tiếp và chờ cờ T_Flag set báo phát
xong,đồng thời cấm thu ký tự,khi nào báo phát xong sẽ cho phép thu trở lại.
 Trình ISR cổng nối tiếp SENCHAR kiểm tra là cờ báo ngắt phát TI hay ngắt thu RI set để thực hiện
phần phát hay thu ký tự tương ứng.Sau đó tăng con trỏ chỉ địa chỉ kế tiếp và tăng chỉ số đếm,nếu ký
tự bằng 0 hay chỉ số đếm bằng MAX sẽ set cờ báo phát xong T_Flag hay cờ báo thu xong R_Flag
tương ứng.
 Trong thực tế thay vì đứng tại chỗ kiểm tra cờ T_Flag hay R_Flag,chương trình chính có thể thực
hiện công việc khác,vì trình phát thu ký tự là trình ngắt,và liên tục kiểm tra các cờ tương ứng!
 Khi đọc ví dụ này,chú ý kỹ việc khai báo biến và các vòng lặp,biểu thức điều kiện và con trỏ bộ nhớ.
Ví dụ 9.20: Lập lại ví dụ 4.10,viết một chương trình C51 hiển thị 4 số BCD nén trong hai biến char ra 4 LED
theo sơ đồ hình 4.10.
Giải:
#include <reg51.h>
const DISP_LED = 0x1000 ;
char x,y ;
void SCAN_LED (char h_byte , l_byte) ; // k/ báo hàm SCAN_LED có 2 input là 4 số BCD nén
void DELAY ( unsigned int t) ; // k/báo hàm DELAY 1ms
void main()
{
while (1)
{
x = 0x12 ; // gán số BCD hàng ngàn và trăm để chạy chương trình
y = 0x34 ; // gán số BCD hàng chục và đơn vị
SCAN_LED (x,y) ; // gọi hàm hiển thị ra LED
}
}
void SCAN_LED (char h_byte,l_byte) // định nghĩa hàm SCAN_LED
{
char tam ;
char xdata*ptr = DISP_LED ; // k/báo con trỏ ptr chỉ đ/c ngoại vi lái LED
tam = h_byte >> 4 & 0x0f ; // lấy decade hàng ngàn
tam = tam | 0x70 ; // ghép nibble cao lái LED0
*ptr = tam ; // xuất ra ngoại vi
DELAY (82) ; // delay 1ms đủ sáng LED tính giá trị theo công thức (9.2)
*ptr = 0xff ; // tắt LED
tam = h_byte & 0x0f ; // lấy decade hàng trăm

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 21


tam = tam | 0xb0 ; // ghép nibble cao lái LED1
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
tam = l_byte >> 4 & 0x0f ; // lấy decade hàng chục
tam = tam | 0xd0 ; // ghép nibble cao lái LED2
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
tam = l_byte & 0x0f ; // lấy decade hàng đơn vị
tam = tam | 0xe0 ; // ghép nibble cao lái LED3
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
}
void DELAY ( unsigned int t) // k/báo hàm delay 1 ms
{ unsigned int i ;
for (i=1; i <= t ; i++);
}
Ví dụ 9.21: Ghép sơ đồ giao tiếp ADC0809 hình 4.11b của ví dụ 4.11 vào chung sơ đồ quét LED hình 4.10
của ví dụ 4.10. Viết chương trình C51 thực hiện các công việc sau:
 Đọc data từ ADC,cho địa chỉ truy xuất ADC là 4000H,nhân data đọc từ ADC cho 20 để
chuyển sang số nhị phân 16 bit biểu diễn giá trị mV của input ADC.
 Chuyển đổi số nhị phân 16 bit này sang số BCD 4 digit
 Hiển thị số BCD này ra LED 7 đoạn biểu diễn giá trị đo tính bằng mV.
Giải:
Ta sử dụng chương trình chuyển đổi số nhị phân 16 bit sang số BCD(chỉ điều chỉnh 1 ít) trong ví dụ
9.14 tạo thành hàm binary16_BCD và hàm SCAN_LED trong ví dụ 9.20.Ta viết thêm hàm ADC đọc data
từ ADC.
#include <reg51.h>
#define uint unsigned int // định nghĩa uint viết tắt của unsigned int
const DISP_LED = 0x1000 ; // đ/chỉ LED
const ADC_ADDR =0x4000 ; // đ/chỉ ADC
char x,y,u ,ubcd_arr[5]; // u dùng trong hàm Div16_6, array trong hàm binary16_BCD
unsigned char ADC_dat ; // ADC_dat chứa byte data không dấu đọc từ ADC
uint num ;
void SCAN_LED (char h_byte , l_byte) ; // k/báo hàm quét LED
char ADC () ; // k/báo hàm đọc ADC
void binary16_BCD ( uint z) ; // k/báo hàm chuyển số nhị phân 16 bit sang số BCD 4 digit
void DELAY ( uint t) ; // k/báo hàm DELAY
void main()
{
while (1)
{
ADC_dat = ADC () ; // đọc data từ ADC
num = ADC_dat * 20 ; // nhân cho 20 thành số nhị phân 16 bit
binary16_BCD (num) ; // chuyển sang BCD
ubcd_arr[1] = ubcd_arr[1] << 4 ; // ghép số BCD hàng chục
y = ubcd_arr[1] | ubcd_arr[0] ; // với số BCD hàng đơn vị
ubcd_arr[3] = ubcd_arr[3] << 4 ; // ghép số BCD hàng ngàn
x = ubcd_arr[3] | ubcd_arr[2] ; // với số BCD hàng trăm
Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 22
SCAN_LED (x,y) ; // xuất ra LED
}
}
char ADC () // định nghĩa hàm ADC
{
char xdata *ptr = ADC_ADDR ; // con trỏ đ/chỉ ADC
char tam ;
tam = 0 ;
*ptr = tam ; // xuất xung START ADC
DELAY (9) ; // delay 120µs
return *ptr ; // trả về data đọc từ ADC
}
void SCAN_LED (char h_byte,l_byte) // định nghĩa hàm SCAN_LED(xem lại vd 9.20)
{
char tam ;
char xdata*ptr = DISP_LED ;
tam = h_byte >> 4 & 0x0f ;
tam = tam | 0x70 ;
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
tam = h_byte & 0x0f ;
tam = tam | 0xb0 ;
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
tam = l_byte >> 4 & 0x0f ;
tam = tam | 0xd0 ;
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
tam = l_byte & 0x0f ;
tam = tam | 0xe0 ;
*ptr = tam ;
DELAY (82) ;
*ptr = 0xff ;
}
void binary16_BCD ( uint z) // định nghĩa hàm binary16_BCD
{
uint Div16_16 (uint a, uint b) ; // k/báo hàm chia 2 số 16 bit
char i ;
for (i=0;i<=4;i++)
{ ubcd_arr[i] = 0 ; }
i=0;
do
{
z = Div16_16 (z,10) ; // chia cho 10
ubcd_arr[i] = u ; // gán arr[]=dư số phép chia ,i thấp số BCD decade thấp
i++ ;
}
while (z !=0 ) ; // kết thúc phép chia khi thương số =0
Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 23
return ;
}
uint Div16_16 (uint a, uint b) // định nghĩa hàm chia 2 số nhị phân 16 bit
{
u = a%b ;
return a/b;
}
void DELAY ( unsigned int t) // định nghĩa hàm DELAY
{ unsigned int i ;
for (i=1; i <= t ; i++);
}
 Trong chương trình này ta sử dụng 2 biến toàn cục để trả kết quả về từ hàm binary_BCD:
 Biến u là dư số phép chia 2 số 16 bit trong hàm Div16_16 (như trong vd 9.14)
 Biến ubcd_arr[5] là dãy chứa tối đa 5 số BCD không nén decade thấp trước.
 Do data đọc từ ADC là 8 bit nên nếu giá trị max là 255 nhân với 20 cho kết quả 5100,nên ta bỏ qua
decade cao nhất ubcd_arr[4]=0.

BÀI TẬP CHƯƠNG 9

Làm lại các bài tập chương 4,5,6,7 bằng C51.

Chương 9 Giáo Trình Vi Xử Lý Lưu Phú Page 24

You might also like