Tải bản đầy đủ (.pdf) (219 trang)

BÁO CÁO THỰC TẬP-CÙNG NGHIÊN CỨU AVR

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (6.33 MB, 219 trang )

SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 1


















CÙNG NGHIÊN CỨU AVR











SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 2

BÀI 1 - LÀM QUEN AVR 3
BÀI 2 - CẤU TRÚC AVR 14

BÀI 3 - NGẮT NGOÀI 27

BÀI 4 – TIMER - COUNTER 40

BÀI 5 – GIAO TIẾP UART 64

BÀI 6 - CHUYỂN ĐỔI ADC 81

BÀI 7 – GIAO TIẾP SPI 97

BÀI 8 – GIAO TIẾP TWI - I2C 110

BÀI 9 - KEYPAD 138

BÀI 10 - TEXT LCD 144

BÀI 11 - GRAPHIC LCD 171

BÀI 12 - C CHO AVR 199

BÀI 13 - THIẾT LẬP FUSE BIT 211
















SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 3
Bài 1 - Làm quen AVR


I. Giới thiệu.
AVR là một họ vi điều khiển do hãng Atmel sản xuất (Atmel cũng là nhà
sản xuất dòng vi điều khiển 89C51 mà có thể bạn đã từng nghe đến). AVR là
chip vi điều khiển 8 bits với cấu trúc tập lệnh đơn giản hóa-RISC(Reduced
Instruction Set Computer), một kiểu cấu trúc đang thể hiện ưu thế trong các
bộ xử lí.
Tại sao AVR: so với các chip vi điều khiển 8 bits khác, AVR có nhiều
đặc tính hơn hẳn, hơn cả trong tính ứng dụng (dễ sử dụng) và đặc biệt là về
chức năng:
• Gần như chúng ta không cần mắc thêm bất kỳ linh kiện phụ nào khi
sử dụng AVR, thậm chí không cần nguồn tạo xung clock cho chip (thường
là các khối thạch anh).
• Thiết bị lập trình (mạch nạp) cho AVR rất đơn giản, có loại mạch nạp

chỉ cần vài điện trở là có thể làm được. một số AVR còn hỗ trợ lập trình on –
chip bằng bootloader không cần mạch nạp…
• Bên cạnh lập trình bằng ASM, cấu trúc AVR được thiết kế tương
thích C.
• Nguồn tài nguyên về source code, tài liệu, application note…rất lớn
trên internet.
• Hầu hết các chip AVR có những tính năng (features) sau:

Có thể sử dụng xung clock lên đến 16MHz, hoặc sử dụng
xung clock nội lên đến 8 MHz (sai số 3%)

Bộ nhớ chương trình Flash có thể lập trình lại rất nhiều lần và
dung lượng lớn, có SRAM (Ram tĩnh) lớn, và đặc biệt có bộ nhớ lưu
trữ lập trình được EEPROM.

Bộ nhớ chương trình Flash có thể lập trình lại rất nhiều lần và
dung lượng lớn, có SRAM (Ram tĩnh) lớn, và đặc biệt có bộ nhớ lưu
trữ lập trình được EEPROM.

Nhiều ngõ vào ra (I/O PORT) 2 hướng (bi-directional).

8 bits, 16 bits timer/counter tích hợp PWM.

Các bộ chuyển đối Analog – Digital phân giải 10 bits, nhiều
kênh.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 4

Chức năng Analog comparator.


Giao diện nối tiếp USART (tương thích chuẩn nối tiếp RS-
232).

Giao diện nối tiếp Two –Wire –Serial (tương thích chuẩn I2C)
Master và Slaver.

Giao diện nối tiếp Serial Peripheral Interface (SPI)


Một số chip AVR thông dụng:

AT90S1200

AT90S2313

AT90S2323 and AT90S2343

AT90S2333 and AT90S4433

AT90S4414 and AT90S8515

AT90S4434 and AT90S8535

AT90C8534

ATtiny10, ATtiny11 and ATtiny12

ATtiny15

ATtiny22


ATtiny26

ATtiny28

ATmega8/8515/8535

ATmega16

ATmega161

ATmega162

ATmega163

ATmega169

ATmega32

ATmega323

ATmega103

ATmega64/128/2560/2561

AT86RF401.


Trong bài viết này tôi sử dụng chip ATmega8 để làm ví dụ, tôi chọn
ATmega8 vì đây là loại chip thuộc dòng AVR mới nhất, nó có đầy đủ các

SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 5
tính năng của AVR nhưng lại nhỏ gọn (gói PDIP có 28 chân) và low cost
nên các bạn có thể mua để tự mình tạo ứng dụng.
Tại sao Assembly (ASM): bạn có thể không cần biết về cấu trúc của
AVR vẫn có thể lập trình cho AVR bằng các phần mềm hỗ trợ ngôn ngữ cấp
cao như BascomAVR (Basic) hay CodevisionAVR (C), tuy nhiên đó không
phải là mục đích của bài viết này. Để hiểu thấu đáo về AVR bạn phải lập
trình bằng chính ngôn ngữ của nó, ASM. Như vậy lập trình bằng ASM giúp
bạn hiểu tường tận về AVR, và tất nhiên để lập trình được bằng ASM bạn
phải hiểu về cấu trúc AVR….Một lý do khác bạn mà tôi khuyên bạn nên lập
trình bằng ASM là các trình dịch (compiler) ASM cho AVR là hoàn toàn
miễn phí, và nguồn source code cho AVR viết bằng ASM là rất lớn. Tuy
nhiên một khi bạn đã thành thạo AVR và ASM bạn có thể sử dụng các ngôn
ngữ cấp cao như C để viết ứng dụng vì ưu điểm của ngôn ngữ cấp cao là
giúp bạn dễ dàng thực hiện các phép toán đại số 16 hay 32 bit (vốn là vấn đề
khó khăn khi lập trình bằng ASM).
II. Công cụ.
Trình biên dịch: có rất nhiều trình biên dịch bạn có thể sử dụng đế biên
dịch code của bạn thành file intel hex để nạp vào chip, một số trình dịch
quen thuộc có thể kể đến như sau:
• AvrStudio: là trình biên dịch ASM chính thức cung cấp bởi Atmel, đây là trình biên dịch hoàn toàn
miễn phí và tất nhiên là tốt nhất cho lập trình AVR bằng ASM. Phiên bản hiện tại là 4.18 SP1, bạn có thể
download phần mềm AvrStudio tạitrang web chính thức của Atmel hoặc bản 4.623 tại đây.
• Wavrasm: cũng được cung cấp bởi Atmel, nó chính là tiền thân của AvrStudio. Hiện tại wavrasm
không còn được sử dụng nhiều vì so với AvrStudio trình biên dịch này có nhiều hạng chế, nếu bạn quan tâm có
thể download tại đây.
• WinAVR hay avr-gcc: là bộ trình dịch được phát triển bởi gnu, ngôn ngữ sử dụng là C và có thể được
dùng tích hợp với AvrStudio (dùng Avrstudio làm trình biên tập – editor). Đặc biệt bộ biên dịch này cũng miễn
phí và đa số nguồn source code C được viết bằng bộ này, vì vậy nó rất lí tưởng cho bạn khi viết các ứng dụng

chuyên nghiệp. Việc lập trình bằng avrgcc tôi sẽ đề cập trong những phần sau.
• CodeVisionAvr: một chương trình bằng ngôn ngữ C rất hay cho AVR, hỗ trợ nhiều thư viện lập trình.
Tuy nhiên là chương trình thương mại. Bạn có thể download bản demo (đầy đủ chức năng nhưng nhưng giới hạn
dung lượng bộ nhớ chương trình 2KB) tại Website hpinfotech
• ICCAVR: lập trình C cho avr, download bản demo.
• BascomAVR: lập trình cho AVR bằng basic, đây là trình biên dịch khá hay và dễ sử dụng, hỗ trợ rất
nhiều thư viện. Tuy nhiên rất khó debug lỗi và không thích hợp cho việc tìm hiểu AVR. Vì vậy tôi không bạn
khuyến khích bạn sử dụng trình dịch này. Bạn có thể download bản demo (4K limit).
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 6
• Và còn rất nhiều trình biên dịch khác cho AVR mà tôi không kể ra đây, nhìn chung tất cả các trình biên
dịch này hỗ trợ C hoặc Basic hoặc thậm chí Pascal. Việc chọn 1 trình biên dịch tùy thuộc vào mục đích, vào mức
độ ứng dụng, vào kinh nghiệm sử dụng và nhiều lý do khác nữa. Ví dụ tôi thường dùng Avrstudio và avrgcc khi
học sử dụng AVR và khi viết thư viện. Nhưng khi cần viết chương trình ứng dụng tôi thường chọn avrgcc và
CodeVisionAVR.
Trong bài viết này tôi hướng dẫn bạn sử dụng AvrStudio để viết chương
trình cho AVR bằng ASM.
Chương trình nạp (Chip Programmer): đa số các trình biên dịch
(AvrStudio, CodeVisionAVR, Bascom…) đều tích hợp sẵn 1 chương trình
nạp chip hỗ trợ nhiều loại mạch nạp nên bạn không quá lo lắng. Trong
trường hợp khác, bạn có thể sử dụng các chương trình nạp như Icprog hay
Ponyprog…là các chương trình nạp miễn phí cho AVR. Việc chọn và sử
dụng chương trình nạp sẽ được giới thiệu trong các bài sau.
Mạch nạp: tham khảo bài viết giới thiệu mạch nạp AVR.
Chương trình mô phỏng: avr simulator là trình mô phỏng và debbug
được tích hợp sẵn trong Avrstudio, avr simulator cho phép bạn quan sát
trạng thái các thanh ghi bên trong AVR nên rất phù hợp để bạn debug
chương trình. Proteus là chương trình thứ hai tôi muốn nói đến, Proteus
không những mô phỏng hoạt động bên trong chip mà còn mô phỏng mạch
điện tử. Proteus mô phỏng rất trực quan, nó là 1 công cụ hữu ích khi các bạn

chưa có điều kiện làm các mạch điện tử.
III. Ví dụ đầu tiên của bạn.
Sau khi download AvrStudio, bạn hãy cài đăt phần mềm trên máy của
bạn, quá trình cài đặt rất đơn giản, bạn hãy theo các mặc định và nhấn “next”
để cài đặt. Trong bài đầu tiên này chúng ta sẽ viết thử 1 chương trình đơn
giản cho AVR sau đó chạy mô phỏng bằng Proteus. Có thể có một số câu
lệnh các bạn sẽ không hiểu, nhưng đừng lo lắng quá, trong bài thứ 2 chúng
ta sẽ học về cấu trúc AVR các bạn sẽ được giải thich rõ hơn.
Để thực hiện ví dụ này, bạn hãy tạo một Project bằng AVRStudio, phần
hướng dẫn chi tiết cho việc tạo Project trong AVRStudio bạn hãy tham khảo
ở bài hướng dẫn AVRStudio.Đoạn code ví dụ trong bài đầu tiên này được
trình bày trong List1.
List 1. Đoạn code đầu tiên của bạn.
1

2

.CSEG
.INCLUDE "M8DEF.INC"
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 7
3

4

5

6

7


8

9

10

11

12

13

14

15

16

17

18

19

20

21

22


23

24

25

26

27

28

29

30

31

32

33

34

35

36

37


38

39

40

41

.ORG 0x000
RJMP BATDAU

.ORG 0x020
BATDAU:
; KHOI TAO CAC DIEU KIEN DAU
LDI R16, HIGH(RAMEND)
LDI R17, LOW(RAMEND)
OUT SPH, R16
OUT SPL, R17
LDI R16, 0xFF;
OUT DDRB, R16

; CHUONG TRINH CHINH
MAIN:
LDI R16, 0B00000001
OUT PORTB, R16
RCALL DELAY

LDI R16, 0B00000010
OUT PORTB, R16

RCALL DELAY

LDI R16, 0B00000100
OUT PORTB, R16
RCALL DELAY

LDI R16, 0B00001000
OUT PORTB, R16
RCALL DELAY

LDI R16, 0B00010000
OUT PORTB, R16
RCALL DELAY

LDI R16, 0B00100000
OUT PORTB, R16
RCALL DELAY

SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 8
42

43

44

45

46


47

48

49

50

51

52

53

54

55

56

57

58

59

60
61

LDI R16, 0B01000000

OUT PORTB, R16
RCALL DELAY

LDI R16, 0B10000000
OUT PORTB, R16
RCALL DELAY

RJMP MAIN
; CHUONG TRING CON DELAY 65535 chu ky (khoang 65535us
neu xung ;clock cho chip la 1M)
DELAY:
LDI R20, 0xFF
DELAY0:
LDI R21, 0xFF
DELAY1:
DEC R21
BRNE DELAY1
DEC R20
BRNE DELAY0
RET
Trước khi tìm hiểu ý nghĩa đoạn code, hãy nhìn 1 lượt qua đoạn code.
Trước hết việc viết HOA hay viết thường là không quan trọng, bạn có thể
viết đoạn code với bất cứ hình thức nào miễn đúng cú pháp, từ khóa là được.
Trong đoạn code:
• Bạn thấy 1 số từ có màu BLUE (ví dụ LDI, OUT, RJMP, RCALL, RET…)đó là các INSTRUCTiON,
tức là các câu lệnh của ngôn ngữ ASM, bạn có thể đọc tài liệu “AVR INSTRUCTION” để tìm hiểu tất cả các
INSTRUCTION. Các INSTRUCTION sau đó sẽ được trình dịch dịch thành các mã tương ứng.
• Một số từ bắt đầu bằng bằng dấu chấm “.” là các DIRECTIVE (ví dụ .INCLUDE hay .ORG )đó cũng là
những từ khóa mặc định của ASM AVR, các DIRECTIVE không phải là mã lệnh mà chỉ là các chỉ dẫn về địa chỉ
bộ nhớ, khởi động bộ nhớ, định nghĩa macro…và không được trình dịch dịch thành mã. Chi tiết về DIRECTIVE

có thể tìm thấy trong các tài liệu về ASM AVR, dưới đây tôi tóm tắt các DIRECTIVE và chức năng của chúng
như sau:
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 9

• Thông thường 1 INSTRUCTION được theo sau bởi 2 toán hạng – operand (tuy nhiên có nhiều trường
hợp chỉ có 1 toán hạng hoặc không có toán hạng), khi đó toán hạng thứ nhất sẽ là các THANH GHI. của AVR
(như đã đề cập, chúng ta sẽ khảo sát thanh ghi AVR trong các bài sau), ví dụ : “LDI R16, 0xFF;” trong đó toán
hạng “R16” là tên 1 thanh ghi trong AVR, và “0xFF” là 1 hằng số dạng hexadecimal có giá trị tương ứng là 255
dạng thập phân hay 11111111 nhị phân.
• Các từ theo sau bởi dấu “:” là các nhãn – label (ví dụ MAIN, DELAY…), đó là từ do chúng ta tự đặt,
nó thực chất là 1 vị trí trong bộ nhớ chương trình, có thể sử dụng nhãn như 1 chương trình con.
• Phần đi sau dấu “;” gọi là giải thích – comment, phần này không được biên dịch, bạn có thể ghi
comment ở bất cứ đâu trong chương trình với yêu cầu phải sử dụng dấu “;” trước nó.
Giải thích đoạn code:có thể chia đoạn code trên thành 4 phần: phần đầu
chứa các DIRECTIVE và lệnh RJMP dùng để xác định các địa chỉ bộ nhớ
chương trình, phần 2 là khởi tạo một số điều kiện đầu cho Stack Pointer và
PORT, phần 3 là chương trình chính, và phần 4 là chương trình con ( chú ý
đây chỉ là cách bố trí của riêng tôi, một khi đã quen thuộc, bạn có thể bố trí
chương trình theo cách riêng của bạn).
• Phần 1 và phần 2:
.CSEG
Chỉ thị .CSEG: Code Segment báo cho trình biên dịch rằng phần code theo sau là phần chương trình thực thi,
phần này sẽ được download vào bộ nhớ chương trình của chip.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 10
.INCLUDE "M8DEF.INC"
Chỉ thị .INCLUDE báo cho trình biên dịch bắt đầu đọc 1 file đính kèm, trong trường hợp trên là file
“M8DEF.INC”, đây là file chứa các khai báo cho chip Atmega8 như thanh ghi, ngắt…cho việc truy xuất trong
chương trình của bạn, đây là dòng bắt buộc, nếu bạn lập trình cho chip khác bạn hãy đổi tên file đính kèm, ví dụ

“m32def.inc” cho chip ATmega32… bạn có thể tìm thấy các file này trong thư mục “C:\Program
Files\Atmel\AVR Tools\AvrAssembler2\Appnotes”.
.ORG 0x000
Chỉ thị .ORG: Set Program Origin, set vị trí trong bộ nhớ sẽ được tác động đến, trong trường hợp trên, .ORG
0x000 xác định phần code theo ngay sau sẽ nằm ở địa chỉ 000, vị trí đầu tiên, trong bộ nhớ chương trình. Và
dòng lênh trong vị trí đầu tiên đó là:
RJMP BATDAU
RJMP: Relative Jump là lệnh nhảy không điều kiện đến 1 vị trí trong bộ nhớ, trong trường hợp trên là nhảy đến
nhãn BATDAU, và nhãn BATDAU nằm ở vị trí 0x020 (số hexadecimal, 0x020 =32 decimal) vì nó được khai
báo ngay sau DIRECTIVE .ORG 0x020.
.ORG 0x020
BATDAU
Như thế phần bộ nhớ chương trình nằm giữa 0 và 0x020 không được sử dụng trong đoạn code của chúng ta, phần
này được sử dụng cho mục đích khác, đó là các vectơ ngắt ( không được đề cập ở đây). Tiếp theo:
; KHOI TAO CÁC DIEU KIEN DAU
LDI R16, HIGH(RAMEND)
LDI R17, LOW(RAMEND)
OUT SPH, R16
OUT SPL, R17
Bốn dòng code trên khởi tạo cho Stack Pointer, chúng ta sẽ tìm hiểu phần này trong các bài về Stack và chương
trình con.
Lời khuyên: các bạn nên khởi động 1 chương trình theo cách trên và chúng ta sẽ hiểu chúng rõ hơn sau
này !
LDI R16, 0xFF
OUT DDRB, R16
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 11
Bạn chú ý 2 dòng trên và những gì tôi giải thích sau đây, 2 dòng này có tác dụng khởi động PORTB của chip
ATmega8 tác dụng như các ngõ xuất tín hiệu (OUTPUT). Trước hết hãy quan sát chip ATmega8 trong hình sau


Hình 1: chip ATmega8.
Bạn có thể thấy chip này gồm 28 chân, trông đó có các chân được ghi là PB0(chân 14), PB1(chân
15),…,PB7(chân 10), đó là các chân của PORTB. PORT là khái niệm chỉ các ngõ xuất nhập. Trong AVR, PORT
có thể giao tiếp theo 2 hướng (bi – directional), có thể dùng để xuất hoặc nhận thông tin, mỗi PORT có 8 chân.
Chip Atmega8 có 3 PORT có tên tương ứng là PORTB, PORTC và PORTD (một số chip AVR khác có 4 hoặc 6
PORT). PORT được coi là “cửa ngõ” then chốt của vi điều khiển.
Trong AVR, mỗi PORT liên quan đến 3 thanh ghi (8 bits) có tên tương ứng là DDRx, PINx, và PORTx với “x”
là tên của PORT, mỗi bit trong thanh ghi tương ứng với mỗi chân của PORT. Trong trường hợp của Atmega8
“x” là B, C hoặc D. Ví dụ chúng ta quan tâm đến PORTB thì 3 thanh ghi tương ứng có tên là DDRB, PINB và
PORTB, trong đó 2 thanh ghi PORTB và PINB được nối trực tiếp với các chân của PORTB, DDRB là thanh ghi
điều khiển hướng ( Input hoặc Output). Viết giá trị 1 vào một bit trong thanh ghi DDRB thì chân tương ứng của
PORTB sẽ là chân xuất (Output), ngược lại giá trị 0 xác lập chân tương ứng là ngõ nhập. Sau khi viết giá trị điều
khiển vào DDRB, việc truy xuất PORTB được thực hiện thông qua 2 thanh ghi PINB và PORTB.
Quay lại với 2 dòng code của chúng ta, dòng đầu: “LDI R16, 0xFF”, với LDI – LoaD Immediately, dòng lệnh có
ý nghĩa là load giá trị 0xFF vào thanh ghi R16, R16 là tên 1 thanh ghi trong bộ nhớ của AVR, 0xFF là 1 hằng số
có dạng thập lục phân, ký hiệu “0x” nói lên điều đó, bạn cũng có thể dùng ký hiệu khác là “$” để chỉ 1 số thập
lục phân, ví dụ &FF, và 0xFF=255(thập phân)=0B11111111 (nhị phân). Như thế sau dòng đầu thanh ghi R16 có
giá trị là 11111111 (nhị phân). Dòng thứ 2: “OUT DDRB, R16” nghĩa là xuất giá trị từ thanh ghi R16 ra thanh
ghi DDRB, tóm lại sau 2 dòng trên giá trị DDRB như sau:
1 1 1 1 1 1 1 1
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 12
Có thể bạn sẽ hỏi tải sao chúng không sử dụng 1 dòng duy nhất là “LDI DDRB, 0xFF” hay “OUT DDRB,
0xFF”, chúng ta không thể vì lệnh LDI chỉ cho phép thực hiện trên các thanh ghi R16,…R31 và lệnh OUT không
thực hiện được với các hằng số.
Và vì DDRB=11111111 nên trong trường hợp này tất cả các chân của PORTB đã sẵn sàng cho việc xuất dữ liệu.
Lúc này thanh ghi PINB không có tác dụng, thanh ghi PORTB sẽ là thanh ghi xuất, ghi giá trị vào thanh ghi này
sẽ tác động đến các chân của PORTB.1
• Phần 3: Chương trình chính
MAIN:

LDI R16, 0B00000001
OUT PORTB, R16
RCALL DELAY
Bạn chỉ cần chú ý 4 dòng trên trong toàn bộ phần chương trình chính, trước hết “MAIN:” chỉ là 1 nhãn do chúng
ta tự đặt tên, giống như 1 “cột mốc” trong chương trình thôi. Dòng “LDI R16, 0B00000001” thì bạn đã hiểu, chỉ
có 1 khác biệt nhỏ là tôi sử dụng hằng số dạng nhị phân cho bạn dễ hiểu hơn. Và dòng “OUT PORTB, R16” để
xuất giá trị 0B00000001 có sẵn trong R16 ra thanh ghi PORTB, lúc này chân PB0 của chip sẽ lên 1 (5V) và các
chân còn lại sẽ ở mức 0 (0V). Dòng thứ 3: “RCALL DELAY” là lệnh gọi chương trình con DELAY, tạm hoãn
trước khi thực hiện các dòng lệnh tiếp theo:
LDI R16, 0B00000010
OUT PORTB, R16
RCALL DELAY
Ba dòng lệnh này cũng giống ba dòng trên, nhưng giá trị xuất ra lúc này là 0B00000010, chân PB1 sẽ lên 5V và
các chân khác xuống mức 0V. Và cứ như thế đến đoạn cuối:
LDI R16, 0B10000000
OUT PORTB, R16
RCALL DELAY
RJMP MAIN
• Sau khi kết thức 3 dòng trên chân PB7 sẽ lên 5V, kết thúc 1 vòng
xoay. Cuối cùng là quay vế đầu chương trình chính bằng dòng “RJMP
MAIN”
• Bây giờ chắc bạn đã đoán được chương trình của chúng ta thực hiện việc gì, đó là quét xoay vòng các
chân của PORTB, nếu chúng ta kết nối các chân của PORTB với các LED, chúng ta sẽ có 1 hiệu ứng quét LED
xoay vòng, chúng ta thực hiện điều này bằng phần mềm Proteus.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 13
• Phần 4: chương trinh con DELAY: đoạn chương trình này không làm gì cả ngoài việc trì hoãn 1
khoảng thời gian, tuy nhiên bạn chưa thể hiểu nó ngay được.
Đây chỉ là 1 ví dụ đơn giản, tôi cố gắng thực hiện nó theo cách dễ hiểu nhất cho bạn, vì thế đoạn code có vẻ hơi
dài dòng, bạn hãy thực hiện lại đoạn chương trình chính bằng đoạn code của bạn.

Phần cuối cùng là biên dịch đoạn code thành file intel hex để đổ vào chip, nhấn phím F7 để biên dịch.
Sau khi biên dịch bạn sẽ có 1 file tên “avr1.hex” trong thưc mục project, chúng ta sẽ dùng file này đổ vào chip
sau này.
IV. Mô phỏng bằng Proteus.
Chúng ta hãy thử nghiệm đoạn chương trình của chúng ta bằng Proteus.
Nếu bạn thực hiện đúng kết quả sẽ như minh họa trong hình 2 Hướng dẫn
cụ thể cách vẽ mạch điện và mô phỏng bằng phần mềm Proteus bạn hãy xem
bài "Mô phỏng Proteus".

Hình 2. Mô phỏng.





SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 14
Bài 2 - Cấu Trúc AVR


I. Giới thiệu.
Bài này tiếp tục bài đầu tiên trong loạt bài giới thiệu về AVR, nếu sau
bài "Làm quen AVR" bạn đã phần nào biết cách lập trình cho AVR bằng
AVRStudio thì trong bài này, chúng ta sẽ tìm hiểu kỹ hơn về cấu trúc của
AVR. Sau bài này, bạn sẽ:
• Hiểu được cấu trúc AVR, cấu trúc bộ nhớ và cách thức hoạt động của
chip.
• Hiểu về Stack và cách hoạt động.
• Biết được một số instruction cơ bản truy xuất bộ nhớ.
• Học các instruction rẽ nhánh và vòng lặp.

• Chương trình con (Subroutine) và Macro.
• Cải tiến ví dụ trong bài 1.
• Viết 1 ví dụ minh họa cách sử dụng bộ nhớ và vòng lặp.
II. Tổ chức của AVR.
AVR có cấu trúc Harvard, trong đó đường truyền cho bộ nhớ dữ liệu
(data memory bus) và đường truyền cho bộ nhớ chương trình (program
memory bus) được tách riêng. Data memory bus chỉ có 8 bit và được kết nối
với hầu hết các thiết bị ngoại vi, với register file. Trong khi đó program
memory bus có độ rộng 16 bits và chỉ phục vụ cho instruction registers.
Hình 1 mô tả cấu trúc bộ nhớ của AVR.
Bộ nhớ chương trình (Program memory): Là bộ nhớ Flash lập trình
được, trong các chip AVR cũ (như AT90S1200 hay AT90S2313…) bộ nhớ
chương trình chỉ gồm 1 phần là Application Flash Section nhưng trong các
chip AVR mới chúng ta có thêm phần Boot Flash setion. Boot section sẽ
được khảo sát trong các phần sau, trong bài này khi nói về bộ nhớ chương
trình, chúng ta tự hiểu là Application section. Thực chất, application section
bao gồm 2 phần: phần chứa các instruction (mã lệnh cho hoạt động của chip)
và phần chứa các vector ngắt (interrupt vectors). Các vector ngắt nằm ở phần
đầu của application section (từ địa chỉ 0x0000) và dài đến bao nhiêu tùy
thuộc vào loại chip. Phần chứa instruction nằm liền sau đó, chương trình viết
cho chip phải được load vào phần này. Xem lại phần đầu của ví dụ trong bài
1:
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 15
.ORG 0x000
RJMP BATDAU
.ORG 0x020
Trong ví dụ này, ngay sau khi set vị trí 0x000 bằng chỉ thị
(DIRECTIVE) .ORG 0x000 chúng ta dùng instruction RJMP để nhảy đến vị
trí 0x020, như thế phần bộ nhớ chương trình từ 0x00 đến 0x01F không được

sử dụng (vì trong ví dụ này chúng ta không sử dụng các vector ngắt).
Chương trình chính được bắt đầu từ địa chỉ 0x020, con số 0x020 là do người
lập trình chọn, thật ra các vector ngắt của chip ATMEGA8 chỉ kéo dài đến
địa chỉ 0x012, vì vậy chương trình chính có thể được bắt đầu từ bất cứ vị trí
nào sau đó. Để biết độ dài các vector ngắt của từng chip bạn hãy tham khảo
datasheet của chip đó.
Vì chức năng chính của bộ nhớ chương trình là chứa instruction, chúng
ta không có nhiều cơ hội tác động lên bộ nhớ này khi lập trình cho chip, vì
thế đối với người lập trình AVR, bộ nhớ này “không quá quan trọng”. Tất cả
các thanh ghi quan trọng cần khảo sát nằm trong bộ nhớ dữ liệu của chip.

Hình 1. Tổ chức bộ nhớ của AVR.
Bộ nhớ dữ liệu (data memory): Đây là phần chứa các thanh ghi quan
trọng nhất của chip, việc lập trình cho chip phần lớn là truy cập bộ nhớ này.
Bộ nhớ dữ liệu trên các chip AVR có độ lớn khác nhau tùy theo mỗi chip,
tuy nhiên về cơ bản phần bộ nhớ này được chia thành 5 phần:
Phần 1: là phần đầu tiên trong bộ nhớ dữ liệu, như mô tả trong hình 1,
phần này bao gồm 32 thanh ghi có tên gọi là register file (RF), hay General
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 16
Purpose Rgegister – GPR, hoặc đơn giản là các Thanh ghi. Tất cả các thanh
ghi này đều là các thanh ghi 8 bits như trong hình 2.

Hình 2. Thanh ghi 8 bits.
Tất cả các chip trong họ AVR đều bao gồm 32 thanh ghi Register File có
địa chỉ tuyệt đối từ 0x0000 đến 0x001F. Mỗi thanh ghi có thể chứa giá trị
dương từ 0 đến 255 hoặc các giá trị có dấu từ -128 đến 127 hoặc mã ASCII
của một ký tự nào đó…Các thanh ghi này được đặt tên theo thứ tự là R0 đến
R31. Chúng được chia thành 2 phần, phần 1 bao gồm các thanh ghi từ R0
đến R15 và phần 2 là các thanh ghi R16 đến R31. Các thanh ghi này có các

đặc điểm sau:
• Được truy cập trực tiếp trong các instruction.
• Các toán tử, phép toán thực hiện trên các thanh ghi này chỉ cần 1 chu
kỳ xung clock.
• Register File được kết nối trực tiếp với bộ xử lí trung tâm – CPU của
chip.
• Chúng là nguồn chứa các số hạng trong các phép toán và cũng là đích
chứa kết quả trả lại của phép toán.
Để minh họa, hãy xét ví dụ thực hiện phép cộng 2 thanh ghi bằng instruction
ADD như sau:
ADD R1, R2
Bạn thấy trong dòng lệnh trên, 2 thanh ghi R1 và R2 được sử dụng trực tiếp với tên của chúng, dòng lệnh
trên khi được dịch sang opcode để download vào chip sẽ có dạng: 0000110000010010 trong đó 00001=1 tức
thanh ghi R1 và 00010 = 2 chỉ thanh ghi R2. Sau phép cộng, kết quả sẽ được lưu vào thanh ghi R1.
Tất cả các instruction sử dụng RF làm toán hạng đều có thể truy nhập tất cả các RF một cách trực tiếp trong 1
chu kỳ xung clock, ngoại trừ SBCI, SUBI, CPI, ANDI và LDI, các instruction này chỉ có thể truy nhập các thanh
ghi từ R16 đến R31.
Thanh ghi R0 là thanh ghi duy nhất được sử dụng trong instruction LPM (Load Program Memory). Các thanh
ghi R26, R27, R28, R29, R30 và R31 ngoài chức năng thông thường còn được sử dụng như các con trỏ (Pointer
register) trong một số instruction truy xuất gián tiếp. Chúng ta sẽ khảo sát vấn đề con trỏ sau này. Hình 3 mô tả
các chức năng phụ của các thanh ghi.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 17

Hình 3. Register file.
Tóm lại 32 RF của AVR được xem là 1 phần của CPU, vì thế chúng được CPU sử dụng trực tiếp và nhanh
chóng, để gọi các thanh ghi này, chúng ta không cần gọi địa chỉ mà chỉ cần gọi trực tiếp tên của chúng. RF
thường được sử dụng như các toán hạng (operand) của các phép toán trong lúc lập trình.
Phần 2: là phần nằm ngay sau register file, phần này bao gồm 64 thanh ghi được gọi là 64 thanh ghi
nhập/xuất (64 I/O register) hay còn gọi là vùng nhớ I/O (I/O Memory). Vùng nhớ I/O là cửa ngõ giao tiếp giữa

CPU và thiết bị ngoại vi. Tất cả các thanh ghi điều khiển, trạng thái…của thiết bị ngoại vi đều nằm ở đây. Xem
lại ví dụ trong bài 1, trong đó tôi có đề cập về việc điều khiển các PORT của AVR, mỗi PORT liên quan đến 3
thanh ghi DDRx, PORTx và PINx, tất cả 3 thanh ghi này đều nằm trong vùng nhớ I/O. Xa hơn, nếu muốn truy
xuất các thiết bị ngoại vi khác như Timer, chuyển đổi Analog/Digital, giao tiếp USART…đều thực hiện thông
qua việc điều khiển các thanh ghi trong vùng nhớ này.
Vùng nhớ I/O có thể được truy cập như SRAM hay như các thanh ghi I/O. Nếu sử dụng instruction truy xuất
SRAM để truy xuất vùng nhớ này thì địa chỉ của chúng được tính từ 0x0020 đến 0x005F. Nhưng nếu truy xuất
như các thanh ghi I/O thì địa chỉ của chúng đựơc tính từ 0x0000 đến 0x003F.
Xét ví dụ instruction OUT dùng xuất giá trị ra các thanh ghi I/O, lệnh này sử dụng địa chỉ kiểu thanh ghi, cấu
trúc của lệnh như sau: OUT A, Rr, trong đó A là địa chỉ của thanh ghi trong vùng nhớ I/O, Rr là thanh ghi RF,
lệnh OUT xuất giá trị từ thanh ghi Rr ra thanh ghi I/O có địa chỉ là A. Giả sử chúng ta muốn xuất giá trị chứa
trong R6 ra thanh ghi điều khiển hướng của PORTD, tức thanh ghi DDRD, địa chỉ tính theo vùng I/O của thanh
ghi DDRD là 0x0011, như thế câu lệnh của chúng ta sẽ có dạng: OUT 0x0011, R6. Tuy nhiên trong 1 trường hợp
khác, nếu muốn truy xuất DDRD theo dạng SRAM, ví dụ lệnh STS hay LDS, thì phải dùng địa chỉ tuyệt đối của
thanh ghi này, tức giá trị 0x0031, khi đó lệnh OUT ở trên được viết lại là STS 0x0031, R6.
Để thống nhất cách sử dụng từ ngữ, từ bây giờ chúng ta dùng khái niệm “địa chỉ I/O” cho các thanh ghi
trong vùng nhớ I/O để nói đến địa chỉ không tính phần Register File, khái niệm “địa chỉ bộ nhớ” của thanh ghi
là chỉ địa chỉ tuyệt đối của chúng trong SRAM. Ví dụ thanh ghi DDRD có “địa chỉ I/O” là 0x0011 và “địa chỉ bộ
nhớ” của nó là 0x0031, “địa chỉ bộ nhớ” = “địa chỉ I/O” + 0x0020.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 18
Vì các thanh ghi trong vùng I/O không được hiểu theo tên gọi như các Register file, khi lập trình cho các
thanh ghi này, người lập trình cần nhớ địa chỉ của từng thanh ghi, đây là việc tương đối khó khăn. Tuy nhiên,
trong hầu hết các phần mềm lập trình cho AVR, địa chỉ của tất cả các thanh ghi trong vùng I/O đều được định
nghĩa trước trong 1 file Definition, bạn chỉ cần đính kèm file này vào chương trình của bạn là có thể truy xuất các
thanh ghi với tên gọi của chúng. Giả sử trong ví dụ ở bài 1, để lập trình cho chip Atmega8 bằng AVRStudio,
dòng thứ 2 chúng ta sử dụng INCLUDE "M8DEF.INC" để load file định nghĩa cho chip ATMega8, file
M8DEF.INC. Vì vậy, trong sau này khi muốn sử dụng thanh ghi DDRD bạn chỉ cần gọi tên của chúng,
như: OUT DDRD,R6.
Phần 3: RAM tĩnh, nội (internal SRAM), là vùng không gian cho chứa các biến (tạm thời hoặc toàn cục)

trong lúc thực thi chương trình, vùng này tương tự các thanh RAM trong máy tính nhưng có dung lượng khá nhỏ
(khoảng vài KB, tùy thuộc vào loại chip).
Phần 4: RAM ngoại (external SRAM), các chip AVR cho phép người sử dụng gắn thêm các bộ nhớ ngoài để
chứa biến, vùng này thực chất chỉ tồn tại khi nào người sử dụng gắn thêm bộ nhớ ngoài vào chip.
Phần 5: EEPROM (Electrically Ereasable Programmable ROM) là một phần quan trọng của các chip AVR
mới, vì là ROM nên bộ nhớ này không bị xóa ngay cả khi không cung cấp nguồn nuôi cho chip, rất thích hợp cho
các ứng dụng lưu trữ dữ liệu. Như trong hình 1, phần bộ nhớ EEPROM được tách riêng và có địa chỉ tính từ
0x0000.
Câu hỏi bây giờ là AVR hoạt động như thế nào?
Hình 4 biểu diễn cấu trong bên trong của 1 AVR. Bạn thấy rằng 32 thanh ghi trong Register File được kết nối
trực tiếp với Arithmetic Logic Unit -ALU (ALU cũng được xem là CPU của AVR) bằng 2 line, vì thế ALU có
thể truy xuất trực tiếp cùng lúc 2 thanh ghi RF chỉ trong 1 chu kỳ xung clock (vùng được khoanh tròn màu đỏ
trong hình 4).

Hình 4. Cấu trúc bên trong AVR.
Các instruction được chứa trong bộ nhớ chương trình Flash memory dưới dạng các thanh ghi 16 bit. Bộ nhớ
chương trình được truy cập trong mỗi chu kỳ xung clock và 1 instruction chứa trong program memory sẽ được
load vào trong instruction register, instruction register tác động và lựa chọn register file cũng như RAM cho
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 19
ALU thực thi. Trong lúc thực thi chương trình, địa chỉ của dòng lệnh đang thực thi được quyết định bởi một bộ
đếm chương trình – PC (Program counter). Đó chính là cách thức hoạt động của AVR.
AVR có ưu điểm là hầu hết các instruction đều được thực thi trong 1 chu kỳ xung clock, vì vậy có thể nguồn
clock lớn nhất cho AVR có thể nhỏ hơn 1 số vi điều khiển khác như PIC nhưng thời gian thực thi vẫn nhanh hơn.
III. Stack.
Stack được hiểu như là 1 “tháp” dữ liệu, dữ liệu được chứa vào stack ở đỉnh “tháp” và dữ liệu cũng được lấy
ra từ đỉnh. Kiểu truy cập dữ liệu của stack gọi là LIFO (Last In First Out – vào sau ra trước). Hình 5 thể hiện
cách truy cập dữ liệu của stack.

Hình 5. Stack.

Khái niệm và cách thức hoạt động của stack có thể được áp dụng cho AVR, bằng cách khai báo một vùng
nhớ trong SRAM là stack ta có thể sử dụng vùng nhớ này như một stack thực thụ.
Để khai báo một vùng SRAM làm stack chúng ta cần xác lập địa chỉ đầu của stack bằng cách xác lập con trỏ
stack-SP (Stack Pointer). SP là 1 con trỏ 16 bit bao gồm 2 thanh ghi 8 bit SPL và SPH (chữ L là LOW chỉ thanh
ghi mang giá trị byte thấp của SP, và H = HIGH), SPL và SPH nằm trong vùng nhớ I/O. Giá trị gán cho thanh
ghi SP sẽ là địa chỉ khởi động của stack. Quay lại ví dụ ở bài 1, phần khởi tạo các điều kiện đầu.
; KHOI TAO CÁC DIEU KIEN DAU
LDI R16, HIGH(RAMEND)
LDI R17, LOW(RAMEND)
OUT SPH, R16
OUT SPL, R17
Bốn dòng khai báo trên mục đích là gán giá trị của RAMEND cho con trỏ SP, RAMEND (tức End of Ram)
là biến chứa địa chỉ lớn nhất của RAM nội trong AVR, biến này được định nghĩa trong file M8DEF.INC. Như
thế sau 4 dòng trên, con trỏ SP chứa giá trị cuối cùng của SRAM hay nói cách khác vùng stack bắt đầu từ vị trí
cuối cùng của bộ nhớ SRAM. Nhưng tại sao là vị trí cuối cùng mà không là 1 giá trị khác. Có thể giải thích như
sau: stack trong AVR hoạt động từ trên xuống, sau khi dữ liệu được đẩy vào stack, SP sẽ giảm giá trị vì thế khởi
động SP ở vị trí cuối cùng của SRAM sẽ tránh được việc mất dữ liệu do ghi đè. Bạn có thể khởi động stack với 1
địa chỉ khác, tuy nhiên vì lý do an toàn, nên khởi động stack ở RAMEND.
Hai instruction dùng cho truy cập stack là PUSH và POP, trong đó PUSH dùng đẩy dữ liệu vào stack và POP
dùng lấy dữ liệu ra khỏi stack. Dữ liệu được đẩy vào và lấy ra khỏi stack tại vị trí mà con trỏ SP trỏ đến. Ví dụ
cho chip ATMega8, RAMEND=0x045F, sau khi khởi động, con trỏ SP trỏ đến vị trí 0x045F trong SRAM, nếu ta
viết các câu lệnh sau:

LDI R1, 1
PUSH R1
LDI R1, 5
PUSH R1
LDI R1, 8
PUSH R1
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE

Ntinside 20
Khi đó nội dung của stack sẽ như trong hình 6.

Hình 6. Nội dung stack trong ví dụ.
Sau mỗi lần PUSH dữ liệu, SP sẽ giảm 1 đơn vị và trỏ vào vị trí tiếp theo.
Bây giờ nếu ta dùng POP để lấy dữ liệu từ stack, POP R2, thì R2 sẽ mang giá trị của ngăn nhớ 0x045D, tức
R2=8. Trước khi instruction POP được thực hiện, con trỏ SP được tăng lên 1 đơn vị, sau đó dữ liệu sẽ được lấy
ra từ vị trí mà SP trỏ đến trong stack.
Stack trong AVR không phải là “vô đáy”, nghĩa là chúng ta chỉ có thể PUSH dữ liệu vào stack ở 1 độ sâu
nhất định nào đấy (phụ thuộc vào chip). Sử dụng stack không đúng cách đôi khi sẽ làm chương trình thực thi sai
hoặc tốn thời gian thực thi vô ích. Vì thế không nên sử dụng stack chỉ để lưu các biến thông thường. Ứng dụng
phổ biến nhất của stack là sử dụng trong các chương trình con (Subroutine), khi chúng ta cần “nhảy” từ một vị trí
trong chương trình chính đến 1 chương trình con, sau khi thực hiện chương trình con lại muốn quay về vị trí ban
đầu trong chương trình chính thì Stack là phương cách tối ưu dùng để chứa bộ đếm chương trình trong trường
hợp này. Xem lại ví dụ trong bài 1, trong chương trình chính chúng ta dùng lệnh RCALL DELAY để nhảy đến
đoạn chương trình con DELAY, RCALL là lệnh nhảy đến 1 vị trí trong bộ nhớ chương trình, trước khi nhảy, PC
được cộng thêm 1 và PUSH một cách tự động vào stack. Cuối chương trình con DELAY, chúng ta dùng
instruction RET, instruction này POP dữ liệu từ stack ra PC một cách tự động, bằng cách này chúng ta có thể
quay lại vị trí trước đó. Chính vì các lệnh RCALL và RET sử dụng stack một cách tự động nên ta phải khởi động
stack ngay từ đầu, nếu không chương trình sẽ thực thi sai chức năng.
Tóm lại cần khởi động stack ở đầu chương trình và không nên sử dụng stack một cách tùy thích nếu chưa thật
cần thiết.
IV. Thanh ghi trạng thái - SREG (STATUS REGISTRY).
Nằm trong vùng nhớ I/O, thanh ghi SREG có địa chỉ I/O là 0x003F và địa chỉ bộ nhớ là 0x005F (thường đây
là vị trí cuối cùng của vùng nhớ I/O) là một trong số các thanh ghi quan trọng nhất của AVR, vì thế mà tôi dành
phần này để giới thiệu về thanh ghi này. Thanh ghi SREG chứa 8 bit cờ (flag) chỉ trạng thái của bộ xử lí, tất cả
các bit này đều bị xóa sau khi reset, các bit này cũng có thể được đọc và ghi bởi chương trình. Chức năng của
từng bit được mô tả như sau:

Hình 7. Thanh ghi trạng thái.

• Bit 0 – C (Carry Flag: Cờ nhớ): là bit nhớ trong các phép đại số hoặc logic, ví dụ thanh ghi R1 chứa
giá trị 200, R2 chứa 70, chúng ta thực hiện phép cộng có nhớ: ADC R1, R2, sau phép cộng, kết quả sẽ được lưu
lại trong thanh ghi R1, trong khi kết quả thực là 270 mà thanh ghi R1 lại chỉ có khả năng chứa tối đa giá trị 255
(vì có 8 bit) nên trong trường hợp này, giá trị lưu lại trong R1 thực chất chỉ là 14, đồng thời cờ C được set lên 1
(vì 270=100001110, trong đó 8 bit sau 00001110 =14 sẽ được lưu lại trong R1).
• Bit 1 – Z (Zero Flag: Cờ 0): cờ này được set nếu kết quả phép toán đại số hay phép Logic bằng 0.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 21
• Bit 2 – N (Negative Flag: Cờ âm): cờ này được set nếu kết quả phép toán đại số hay phép Logic là số
âm.
• Bit 3 – V (Two’s complement Overflow Flag: Cờ tràn của bù 2): hoạt động của cờ này có vẻ sẽ khó
hiểu cho bạn vì nó liên quan đến kiến thức số nhị phân (phần bù), chúng ta sẽ đề cập đến khi nào thấy cần thiết.
• Bit 4 – S (Sign Bit: Bit dấu): Bit S là kết quả phép XOR giữa 1 cờ N và V, S=N xor V.
• Bit 5 – H (Half Carry Flag: Cờ nhờ nữa): cờ H là cờ nhớ trong 1 vài phép toán đại số và phép Logic,
cờ này hiệu quả đối với các phép toán với số BCD.
• Bit 6 – T (Bit Copy Storage): được sử dụng trong 2 Instruction BLD (Bit LoaD) và BST (Bit
STorage). Tôi sẽ giải thích chức năng Bit T trong phần giới thiệu về BLD và BST.
• Bit 7 – I (Global Interrupt Enable) : Cho phép ngắt toàn bộ): Bit này phải được set lên 1 nếu trong
chương trình có sử dụng ngắt. Sau khi set bit này, bạn muốn kích hoạt loại ngắt nào cần set các bit ngắt riêng của
ngắt đó. Hai instruction dùng riêng để Set và Clear bit I là SEI và CLI.
Chú ý: tất cả các bit trong thanh ghi SREG đều có thể được xóa thông qua các instruction không toán hạng
CLx và set bởi SEx, trong đó x là tên của Bit.Ví dụ CLT là xóa Bit T và SEI là set bit I.
Tôi chỉ giải thích ngắn gọn chức năng của các bit trong thanh ghi SREG, cụ thể chức năng và cách sử dụng của
từng bit chúng ta sẽ tìm hiểu trong các trường hợp cụ thể sau này, người đọc có thể tự tìm hiểu thêm trong các tài
liệu về INSTRUCTION cho AVR.
Tôi cung cấp thêm 1 bảng tóm tắt sự ảnh hưởng của các phép toán đại số, logic lên các Bit trong thanh ghi
SREG.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 22
Hình 8. Ảnh hưởng của các phép toán lên SREG.

IV. Macro và chương trình con.
Macro là khái niệm chỉ một đoạn code nhỏ để thực hiện một công việc nào đó, nếu có 1 đoạn code nào đó
mà bạn rất hay sử dụng khi lập trình thì bạn nên dùng macro để tránh việc phải viết đi viết lại đoạn code đó. Lập
trình ASM cho AVR cho phép bạn sử dụng Macro, để tạo 1 Macro bạn sử dụng DIRECTIVE.
.MACRO delay4
NOP
NOP
NOP
NOP
.ENDMACRO
Đoạn Macro trên có tên delay4 thực hiện việc delay 4 chu kỳ máy bằng 4 lệnh NOP, nếu trong chương trình
bạn cần dùng Macro này thì chỉ cần gọi delay4 ở bất kỳ dòng nào.
[…] ; code của bạn
Delay4
[…] ; code của bạn
Mỗi lần tên của Macro được gọi, trình biên dịch sẽ tìm đến Macro đó và copy toàn bộ nội dung Macro vào vị
trí bạn gọi. Như vậy thực chất con trỏ chương trình không nhảy đến Macro, Macro không làm giảm dung lương
chưong trình mà chỉ làm cho việc lập trình nhẹ nhàng hơn. Đây chính là khác biệt lớn nhất của Macro và
Subroutine (chương trình con).
Chương trình con cũng là 1 đoạn code thực hiện 1 chức năng đặc biệt nào đó. Tuy nhiên khác với Macro,
mỗi khi gọi chương trình con, con trỏ chương trình nhảy đến chương trình con đề thực thi chương trình con và
sau đó quay về chương trình chính. Như thế chương trình con chỉ được biên dịch 1 lần và có thể sử dụng nhiều
lần, nó làm giảm dung lượng chưong trình. Đây là ưu điểm và cũng là điểm khác biệt lớn nhất giữa chương trình
con và Macro. Tuy nhiên cần chú ý là việc nhảy đến chương trình con và nhảy về chương trình chính cần vài chu
kỳ máy, có thể làm chậm chương trình, đây là nhược điểm của chương trình con so với macro.
Chương trình con cho AVR luôn được bắt đầu bằng 1 Label, đó cũng là tên và địa chỉ của chương trình con.
Chương trình con thường được kết thúc với câu lệnh RET (Return). Chúng ta đã biết về chương trình con qua ví
dụ của bài 1, trong đó DELAY là 1 chương trình con.
Để gọi chương trình con từ 1 vị trí nào đó trong chương trình, chúng ta có thể dùng
lệnh CALL hoặc RCALL (Relative CALL) (xem lại ví dụ bài 1 về cách sử dụng RCALL). Mỗi khi các lệnh này

được gọi, bộ đếm chương trình được tự động được PUSH vào stack và khi chương trình con kết thúc bằng lệnh
RET, bộ đếm chương trình được POP trở ra và quay về chương trình chính. Lệnh CALL có thể gọi 1 chương
trình con ở bất kỳ vị trí nào trong khi RCALL chỉ gọi trong khoảng bộ nhớ 4KB, nhưng RCALL cần ít chu kỳ
xung clock hơn khi thực thi.
Hai instruction khác có thể được dùng để gọi chương trình con đó là JMP (Jump) và RJMP (Relative Jump).
Khác với các lệnh call, các lệnh jump không cho phép quay lại vì không tự động PUSH bộ đếm chương trình vào
Stack, để sử dụng các lệnh này gọi chương trình con bạn cần một số lệnh jump khác ở cuối chương trình con.
Tóm lại bạn nên viết 1 chương trình con đúng chuẩn và dùng CALL hoặc RCALL để gọi chương các
chương trình này, chỉ những trường hợp đặc biệt hoặc bạn hiểu rất rõ về chúng thì có thể dùng các lệnh jump.
V. Ví dụ minh họa.
Nếu bạn đã đọc và hiểu đến thời điểm này thì bạn đã có thể hiểu hết hoạt động của chương trình ví dụ trong
bài 1, thật sự ví dụ đó rất đơn giản và dễ hiểu. Tuy nhiên, bạn có thề tối ưu hóa ví dụ đó theo hướng làm giảm
dung lượng chương trình và tất nhiên, chương trình sẽ khó hiểu hơn cho người khác. Các phần khởi động vị trí
bộ nhớ, stack và chương trình con DELAY chúng ta không thay đổi, chỉ thay đổi phần chương trình chính, 1
trong những cách viết chương trình chính như cách sau:
; CHUONG TRINH CHINH , BAI 1, VI DU 1, VERSION 2///////////////////////////////
LDI R16, $1 ;LOAD GIA TRI KHOI DONG CHO R16
MAIN:
OUT PORTB, R16 ; XUAT GIA TRI TRONG R16 RA PORTB
RCALL DELAY ; GOI CHUONG TRINH CON DELAY
ROL R16 ; XOAY THANH GHI R16 SANG TRAI 1 VI TRI
RJMP MAIN ; NEU R16 ≠0, NHAY VE MAIN, TIEP TUC QUET
;/////////////////////////////////////////////////////////////////////////////////////////
Có thể không cần giải thích bạn cũng đã có thể hiểu đoạn code trên, đây chỉ là 1 trong những cách có thể,
bạn hãy viết lại theo cách của riêng bạn với yêu cầu là chương trình phải thực hiện đúng chức năng và ngắn gọn.
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 23
Bây giờ chúng ta sẽ thực hiện một ví dụ minh họa cho những gì chúng ta đã học trong bài 2 này. Nội dung
của ví dụ thể hiện trong mạch điện hình 9. Hoạt động của mạch điện tử như sau: 1 chip ATMega8 được sử dụng
như một counter, có thể dùng để đếm lên và đếm xuống, 2 button trong mạch điện tác động như 2 “kicker”, nhấn

button 1 để đếm lên và button để đếm xuống, giá trị đếm nằm trong khoảng từ 0 đến 9. Giá trị đếm được hiển thị
trên 1 LED 7 đoạn loại anod chung (dương chung), chip 7447 được dùng để giải mã từ giá trị BCD xuất ra bởi
ATMega8 sang tín hiệu cho LED 7 đoạn anod chung, chúng ta cần sử dụng 7447 vì tín hiệu xuất ra từ chip
ATMega8 là dạng nhị phân hoặc BCD , tín hiệu này không thể hiển thị trực tiếp trên các LED 7 đoạn, chip 7447
có nhiệm vụ chuyển 1 dữ liệu dạng digit BCD sang mã phù hợp cho LED 7 đoạn.
Để thực hiện ví dụ, trước hết bạn hãy vẽ mạch điện như trong hình 9 bằng phần mềm Proteus (xem cách vẽ
mạch điện bằng Proteus), mạch điện chỉ có 5 loại linh kiện là chip ATMega8 (từ khóa mega8), 1 LED 7 đoạn
anod chung với tên đầy đủ trong Proteus là 7SEG-COM-AN-GRN (từ khóa 7SEG), 1 chip 7447 (từ khóa 7447),
1 điện trở 10 Ω và 2 button (từ khóa button).

Hình 9. Ví dụ cho bài 2.
Sử dụng AVRStudio tạo 1 project mới với tên gọi avr2 (xem lại cách tạo Project mới trong AVRStudio). Viết
lại phần code bên dưới vào vào file avr2.asm
List 1. Ví dụ cấu trúc AVR









10

11

12

13


14

15

16

.INCLUDE "M8DEF.INC"
.CSEG.
.ORG 0x0000
RJMP BATDAU
.ORG 0x0020
BATDAU:
;KHOI DONG STACK POINTER
LDI R17, HIGH(RAMEND)
LDI R16, LOW(RAMEND)
OUT SPL, R16
OUT SPH,R17
; KHOI DONG CAC PORT
CLR R16 ; XOA R16, R16=0
OUT DDRB, R16 ; DDRB=0, PORTB LA NGO NHAP
LDI R16, 0xFF ; SET TAT CA CAC BIT CUA R16 LEN 1
OUT PORTB,R16 ;DDRB=0, PORTB =0xFF, KEO LEN CAC CHAN PORTB
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 24
17

18

19


20

21

22

23

24

5

26

27

28

29

30

31

32

33

34


35

36

37

38

39

40

41

42

43

44

45

46

47

48

49


50

51

52

53

54

55

OUT DDRD, R16 ;DDRD=0xFF, PORTD LA NGO XUAT
CLR R25 ;XOA R25, R25 LA THANH GHI DUNG CHUA SO DEM
SER R20 ; R20 LA THANH GHI TAM CHUA GIA TRI TRUOC DO CUA PINB
MAIN:
IN R21,PINB ;DOC GIA TRI TU PINB, TUC TU CAC BUTTON
RCALL SOSANH ;GOI CHUONG TRINH CON SOSANH
OUT PORTD, R25 ;XUAT GIA TRI DEM RA PORTD
SBRS R21,0 ;NEU BIT 0 CUA R21 (TUC CHAN PB0) =1 THI BO QUA DONG ;TIEP THEO

RCALL TANG ;NHAY DEN CHUONG TRINH CON TANG GIA TRI DEM
SBRS R21,1 ;NEU BIT 1 CUA R21 (TUC CHAN PB1) =1 THI BO QUA DONG ;TIEP THEO

RCALL GIAM ;NHAY DEN CHUONG TRINH CON GIAM GIA TRI DEM
MOV R20,R21 ;LUU LAI TRANG THAI PINB
RJMP MAIN
;**********************CHUONG TRINH CON************************
; **************subroutine kiem tra gioi hang (tu 0 den 9) cua so dem

SOSANH:
CPI R25, 10
BREQ RESET0 ;NEU GIA TRI DEM=10 THI TRA VE 0
CPI R25, 255
BREQ RESET9 ;NEU GIA TRI DEM =255 THI TRA VE 9
RJMP QUAYVE ;NHAY DEN NHAN QUAYVE
RESET0:
LDI R25,$0 ;TRA GIA TRI DEM VE 0
RJMP QUAYVE
RESET9:
LDI R25,$9 ;GAN 9 CHO GIA TRI DEM
QUAYVE:
RET
; ************************************************************
; **************subroutine tang so dem 1 don vi neu dieu kien thoa
TANG:
SBRS R20,0
RET
INC R25
RET
; **************subroutine giam so dem 1 don vi neu dieu kien thoa
GIAM:
SBRS R20,1
RET
SƯU TẦM VÀ BIÊN SOẠN BỞI NTINSIDE
Ntinside 25
56

57


DEC R25
RET
Trong ví này này, chúng ta sử dụng 2 PORT của chip ATMega8, PORTD dùng xuất dữ liệu (số đếm) ra chip
7447 và sau đó hiển thị trên LED 7 đoạn. PORTB dùng như ngõ nhập, tín hiệu từ các button sẽ được chip
ATMega8 nhận thông qua 2 chân PB0 và PB1 của PORTB.
Hoạt động của cac PORT và việc xác lập 1 PORT như các ngõ xuất chúng ta đã khảo sát trong bài 1. Ở đây
chúng ta khảo sát thêm về xác lập PORT như 1 ngõ nhập, trước hết bạn hãy quan sát mạch điện tương đương của
1 chân trong các PORT xuất nhập của AVR trong hình 10.

Hình 10. Cấu trúc chân trong PORT của AVR.
Trong mạch điện hình 10, các diode và tụ điện chỉ có chức năng bảo vệ chân PORT, nhưng điện trở Rpu (R
Pull up) đóng vai trò quan trọng như là điện trở kéo lên khi chân của PORT làm nhiệm vụ nhận tín hiệu (ngõ
nhập). Tuy nhiên trong AVR, điện trở kéo lên này không phải luôn kích hoạt, chúng ta biết rằng mỗi PORT của
AVR có 3 thanh ghi: DDRx, PORTx và PINx, nếu DDRx=0 thì PORT x là ngõ nhập, lúc này thanh ghi PINx là
thanh ghi chứa dữ liệu nhận về, đặc biệt thanh ghi PORTx vẫn được sử dụng trong mode này, đó là thanh ghi xác
lập điện trở kéo lên, như thế nếu DDRx=0 và PORTx=0xFF thì các chân PORTx là ngõ nhập và được kéo lên
bởi 1 điện trở trong chip, nghĩa là các chân của PORTx luôn ở mức cao, muốn kích để thay đồi trạng thái chân
này chúng ta cần nối chân đó trực tiếp với GND, đấy là lý do tại sao các button trong mạch điện của chúng ta có
1 đầu nối với chân của chip còn đầu kia được nối với GND. Đây cũng là ý nghĩa của khái niệm điện trở kéo lên
(Pull up resistor) trong kỹ thuật điện tử. Đoạn code trong phần “KHOI DONG CAC PORT” của ví dụ này xác
lập PORTD là ngõ xuất (DDRD=0xFF) , PORTB là ngõ nhập có sử dụng điện trở kéo lên (DDRB=0,
PORTB=0xFF).
Chúng ta sẽ giải thích hoạt động của đoạn chương trình chính và các đoạn chương trình con. Trước hết, trong
chương trình này, chúng ta sử dụng 3 thanh ghi chính là R20, R21 và R25, trong đó R25 là thanh ghi chứa số
đếm, giá trị của thanh ghi R25 sẽ được xuất ra PORTD của chip, thanh ghi R21 chứa trạng thái của thanh ghi
PINB và cũng là trạng thái của các button, thanh ghi R20 kết hợp với thanh ghi R21 tạo thành 1 “bộ đếm cạnh
xuống” của các button. Để hiểu thấu đáo hoạt động đếm (cũng là hoạt động chính của ví dụ này) chúng ta xét
trạng thái chân PB0 như trong hình 11.

Hình 11. Thay đổi trạng thái ở các chân I/O.

Trong trạng thái bình thường (button không được nhấn), chân PB0 ở mức cao (do điện trở kéo lên), bộ đếm
không hoạt động, giá trị đếm không thay đổi, bây giờ nếu nhấn button, chân PB0 được nối trực tiếp với GND,
chân này sẽ bị kéo xuống mức thấp, bằng cách kiểm tra trạng thái chân PB0, nếu PB0=0 ta tăng giá trị đếm 1 đơn
vị. Ý tưởng như thế có vẻ hợp lý, tuy nhiên nếu áp dụng thì chương trình sẽ hoạt động không đúng chức năng,
khi bạn nhấn 1 lần giá, trị đếm có thể tăng đến cả trăm hoặc không kiểm soát được, hiệu ứng này tương tự khi

×