asembly

471
Bài 1 - Làm quen AVR 1 2 3 4 5 ( 388 Votes ) Nội dung Các bài cần tham khảo trước 1. Giới thiệu. 2. Công cụ. 3. Ví dụ. 4. Mô phỏng. Download ví dụ AVR Studio . Mô phỏng với Proteus . 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:

description

lap trinh asemly hay nhat cho vi xu li

Transcript of asembly

Page 1: asembly

Bài 1 - Làm quen AVR

1 2 3 4 5

 ( 388 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu.

2. Công cụ.

3. Ví dụ.

4. Mô phỏng.

Download ví dụ

AVR Studio .

Mô phỏng với Proteus .

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… 

Page 2: asembly

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.

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.

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

Page 3: asembly

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 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,

Page 4: asembly

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ại trang

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.

Page 5: asembly

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).

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,

Page 6: asembly

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.

1234567891011121314151617181920212223242526272829303132

.CSEG

.INCLUDE "M8DEF.INC"

.ORG 0x000    RJMP BATDAU

.ORG 0x020BATDAU:; 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 CHINHMAIN:    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

Page 7: asembly

33343536373839404142434445464748495051525354555657585960 61

    LDI R16, 0B00010000    OUT PORTB, R16    RCALL DELAY

    LDI R16, 0B00100000    OUT PORTB, R16    RCALL DELAY

    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 DELAY0RET

     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.

Page 8: asembly

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:

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.

Page 9: asembly

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.

.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”.

Page 10: asembly

.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

Page 11: asembly

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

     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

Page 12: asembly

(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

     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

Page 13: asembly

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

Page 14: asembly

     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.

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.

Page 15: asembly

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.

Bài 2 - Cấu Trúc AVR

1 2 3 4 5

 ( 581 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu. Làm quen AVR.

Page 16: asembly

2. Tổ chức AVR.

3. Stack.

4. Thanh ghi trạng thái.

5. Ví dụ.

Download ví dụ

Assembly cho AVR.

AVR Studio .

Mô phỏng với Proteus .

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

Page 17: asembly

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:.ORG 0x000RJMP 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.

Page 18: asembly

      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 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.

Page 19: asembly

      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.

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ị

Page 20: asembly

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.     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

Page 21: asembly

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 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.

Page 22: asembly

      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 DAULDI R16, HIGH(RAMEND)LDI R17, LOW(RAMEND)OUT SPH, R16OUT 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,

Page 23: asembly

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 R16, 1PUSH R16LDI R16, 5PUSH R16LDI R16, 8PUSH R16       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),

Page 24: asembly

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).

Page 25: asembly

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.

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. 

Page 26: asembly

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 delay4NOPNOPNOPNOP.ENDMACRO

Page 27: asembly

       Đ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.

Page 28: asembly

       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  R16MAIN:OUT PORTB, R16 ; XUAT GIA TRI TRONG R16 RA PORTBRCALL DELAY ; GOI CHUONG TRINH CON DELAYROL R16 ; XOAY THANH GHI R16 SANG TRAI 1 VI TRIRJMP 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.      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).

Page 29: asembly

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

1234567891011121314151617181920212223

.INCLUDE "M8DEF.INC"

.CSEG.

.ORG 0x0000      RJMP BATDAU.ORG 0x0020BATDAU:;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      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 PINBMAIN:      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

Page 30: asembly

24252627282930313233343536373839404142434445464748495051525354555657

      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 demSOSANH:      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 QUAYVERESET0:      LDI R25,$0 ;TRA GIA TRI DEM VE 0      RJMP QUAYVERESET9:      LDI R25,$9 ;GAN 9 CHO GIA TRI DEMQUAYVE:      RET; ************************************************************; **************subroutine tang so dem 1 don vi neu dieu kien thoaTANG:      SBRS R20,0      RET      INC R25      RET; **************subroutine giam so dem 1 don vi neu dieu kien thoaGIAM:      SBRS R20,1      RET      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õ

Page 31: asembly

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.

Page 32: asembly

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 bạn nhấn và giữ 1 phím trên bàn phím máy tính, lý do là vì chúng ta sử dụng phương pháp kiểm tra mức để đếm, thời gian quét của chương trình  rất ngắn so với thời gian chúng ta giữ button. Để khắc phục, chúng ta dùng phương pháp kiểm tra cạnh xuống, chỉ khi nào phát hiện chân PB0 thay đổi từ 1 xuống 0 thì mới tăng giá trị đếm 1 đơn vị, kết quả là mỗi lần nhấn button thì giá trị đếm chỉ tăng 1 (ngay cả khi ta nhấn và giữ button), thanh ghi R20 được sử dụng để lưu trạng thái trước đó của PINB (cũng là trạng thái của các button).      Trong chương trình, tôi sử dụng 2 istruction mới là SBRC và SBRS để kiểm tra trạng thái các chân của PORTB (button). SBRC – Skip if Bit in Register is Clear, lệnh này sẽ bỏ qua 1 dòng lệnh ngay sau đó (chỉ bỏ qua 1 dòng duy nhất) nếu 1 bit trong thanh ghi ở mức 0, SBRC – Skip if Bit in Register is Set- hoạt động tương tự SBRC nhưng skip sẽ xảy ra nếu bit trong thanh ghi ở mức 1. Dựa vào đây chúng ta giải thích 4 dòng sau:SBRS R21,0 ;NEU BIT 0 CUA R21 (TUC CHAN PB0) =1 THI BO QUA DONG ;TIEP THEORCALL TANG ;NHAY DEN CHUONG TRINH CON TANG GIA TRI DEMSBRS R21,1 ;NEU BIT 1 CUA R21 (TUC CHAN PB1) =1 THI BO QUA DONG ;TIEP THEORCALL GIAM ;NHAY DEN CHUONG TRINH CON GIAM GIA TRI DEM       Dòng 1 dùng kiểm tra trạng thái bit 0 trong R21 (chú ý R21 chứa giá trị của PINB), nếu bit này bằng 1 (set), tức chân PB0=1 hay button không được nhấn, thì nhảy bỏ qua dòng lệnh tiếp theo để đến dòng 3. Ở dòng 3 chương trình kiểm tra trạng thái chân PB1 (button thứ 2). Quay lại dòng 1, nếu chương trình kiểm tra phát hiện chân PB0=0 (button thứ nhất được nhấn) thì dòng lệnh thứ 2 được thực thi, kết quả là chương trình nhảy đến chương trình con TANG.TANG:SBRS R20,0

Page 33: asembly

RETINC R25RET       Dòng đầu tiên của chương trình con TANG là kiểm tra trạng thái trước đó của chân PB0 (được lưu ở bit 0 trong thanh ghi R20), nếu trạng thái này bằng 0, nghĩa là không có sự chuyển từ 1 xuống 0 ở chân PB0, dòng 2 (lệnh RET) sẽ được thực thi để quay về chương trình chính. Nhưng nếu PB0 trước đó bằng 1, nghĩa là có sự thay đổi từ 1->0 ở chân này, giá trị đếm sẽ được tăng thêm 1 nhờ INC R25, sau đó quay về chương trình chính.Tóm lại muốn tăng giá trị đếm thêm 1 đơn vị cần thỏa mãn 2 điều kiện: chân PB0 hiện tại =0 (button đang được nhấn) và trạng thái trước đó của PB0 phải là 1 (tránh trường hợp tăng liên tục). Phương pháp  này có thể áp dụng cho rất nhiều trường hợp đếm dạng đếm xung.      Quá trình giảm giá trị đếm được hiểu tương tự, phần còn lại của ví dụ này bạn đọc hãy tự giải thích theo những gợi ý trên. Bài 3 - Ngắt ngoài

1 2 3 4 5

 ( 138 Votes )Nội dung Các bài cần tham khảo trước

1. Ngắt trên AVR.

2. Ngắt ngoài.

3. Ví dụ ngắt ngoài với C.

Download ví dụ

Cấu trúc AVR .

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Ngắt trên AVR.

     Interrupts, thường được gọi là ngắt, là một tín hiệu khẩn cấp gởi đến bộ xử lí, yêu cầu bộ xử lí tạm ngừng tức khắc các hoạt động hiện tại để “nhảy” đến một nơi khác thực hiện một nhiệm vụ khẩn cấp nào đó, nhiệm vụ này gọi là trình phục vụ

Page 34: asembly

ngắt – isr (interrupt service routine ). Sau khi kết thúc nhiệm vụ trong isr, bộ đếm chương trình sẽ được trả về giá trị trước đó để bộ xử lí quay về thực hiện tiếp các nhiệm vụ còn dang dở. Như vậy, ngắt có mức độ ưu tiên xử lí cao nhất, ngắt thường được dùng để xử lí các sự kiện bất ngờ nhưng không tốn quá nhiều thời gian. Các tín hiệu dẫn đến ngắt có thể xuất phát từ các thiết bị bên trong chip (ngắt báo bộ đếm timer/counter tràn, ngắt báo quá trình gởi dữ liệu bằng RS232 kết thúc…) hay do các tác nhân bên ngoài (ngắt báo có 1 button được nhấn, ngắt báo có 1 gói dữ liệu đã được nhận…).

     Ngắt là một trong 2 kỹ thuật “bắt” sự kiện cơ bản là hỏi vòng (Polling) và ngắt. Hãy tưởng tượng bạn cần thiết kế một mạch điều khiển hoàn chỉnh thực hiện rất nhiều nhiệm vụ bao gồm nhận thông tin từ người dùng qua các button hay keypad (hoặc keyboard), nhận tín hiệu từ cảm biến, xử lí thông tin, xuất tín hiệu điều khiển, hiển thị thông tin trạng thái lên các LCD…(bạn hoàn toàn có thể làm được với AVR), rõ ràng trong các nhiệm vụ này việc nhận thông tin người dùng (start, stop, setup, change,…) rất hiếm xảy ra (so với các nhiệm vụ khác) nhưng lại rất “khẩn cấp”, được ưu tiên hàng đầu. Nếu dùng Polling nghĩa là bạn cần viết 1 đoạn chương trình chuyên thăm dò trạng thái của các button (tôi tạm gọi đoạn chương trình đó là Input()) và bạn phải chèn đoạn chương trình Input() này vào rất nhiều vị trí trong chương trình chính để tránh trường hợp bỏ sót lệnh từ người dùng, điều này thật lãng phí thời gian thực thi. Giải pháp cho vấn đề này là sử dụng ngắt, bằng cách kết nối các button với đường ngắt của chip và sử dụng chương trình Input() làm trình phục vụ ngắt - isr của ngắt đó, bạn không cần phải chèn Input() trong lúc đang thực thi và vì thế không tốn thời gian cho nó, Input() chỉ được gọi khi người dùng nhấn các button. Đó là ý tưởng sử dụng ngắt.

     Hình 1 minh họa cách tổ chức ngắt thông thường trong các chip AVR. Số lượng ngắt trên mỗi dòng chip là khác nhau, ứng với mỗi ngắt sẽ có vector ngắt, vector ngắt là các thanh ghi có địa chỉ cố định được định nghĩa trước nằm trong phần đầu của bộ nhớ chương trình. Ví dụ vector ngắt ngoài 0 (external interrupt 0) của chip atmega8 có địa chỉ là 0x001 (theo datasheet từ Atmel). Trong lúc chương trình chính đang thực thi, nếu có một sự thay đổi dẫn đến ngắt xảy ra ở chân INT0 (chân 4), bộ đếm chương trình (Program Counter) nhảy đến địa chỉ 0x001, giả sử ngay tại địa chỉ 0x001 chúng ta có đặt 1 lệnh RJMP đến một trình phục vụ ngắt (IRS1 chẳng hạn), một lần nữa bộ đếm chương trình nhảy đến IRS1 để thực thi trình phục vụ ngắt, kết thúc ISR1, bộ đếm chương trình lại quay về vị trí trước đó trong chương trình chính, quá trình ngắt kết thúc. Không mang tính bắt buộc nhưng tôi khuyên bạn nên tổ chức chương trình ngắt theo cách này để tránh những lỗi liên quan đến địa chỉ chương trình.

Page 35: asembly

Hình 1. Ngắt.

     Bảng 1 tóm tắt các vector ngắt có trên chip atmega8, cho các chip khác bạn hãy tham khảo datasheet để biết thêm.

Bảng 1 các vector ngắt và Reset trên chip Atmega8.

Page 36: asembly

II. Ngắt ngoài (External Interrupt).

      Phần này tôi dành giới thiệu các bạn cách cài đặt và sử dụng ngắt ngoài vì đây là loại ngắt duy nhất độc lập với các thiết bị của chip, các ngắt khác thường gắn với hoạt động của 1 thiết bị nào đó như Timer/Counter, giao tiếp nối tiếp USART,

Page 37: asembly

chuyển đổi ADC…chúng ta sẽ khảo sát cụ thể khi tìm hiểu về hoạt động của các thiết bị này.

      Ngắt ngoài là cách rất hiệu quả để thực hiện giao tiếp giữa người dùng và chip. Trên chip atmega8 có 2 ngắt ngoài có tên là INT0 và INT1 tương ứng 2 chân số 4 (PD2) và số 5 (PD3). Như tôi đã đề cập trong bài AVR2, khi làm việc với các thiết bị ngoại vi của AVR, hầu như chúng ta chỉ thao tác trên các thanh ghi chức năng đặc biệt - SFR (Special Function Registers) trên vùng nhớ IO, mỗi thiết bị bao gồm một tập hợp các thanh ghi điều khiển, trạng thái, ngắt…khác nhau, điều này đồng nghĩa chúng ta phải nhớ tất cả các thanh ghi của AVR. Lúc này datasheet phát huy tác dụng, bạn phải nhanh chóng download file datasheet của chip mình đang sử dụng, có rất nhiều nơi để download như tại www.atmel.comhay trên các trang web chuyên cung cấp IC datasheet miễn phí (www.alldatasheet.com là 1 ví dụ). Quay về với ngắt ngoài, có 3 thanh ghi liên quan đến ngắt ngoài đó là MCUCR, GICR và GIFR. Cụ thể các thanh ghi được trình bày bên dưới.

      Thanh ghi điều khiển MCU – MCUCR (MCU Control Register) là thanh ghi xác lập chế độ ngắt cho ngắt ngoài, quan sát hình 2 trước khi tìm hiểu thanh ghi này.

Hình 2. Kết nối ngắt ngoài cho atmega8.

      Giả sử chúng ta kết nối các ngắt ngoài trên AVR mega8 như phía trái hình 2, các button dùng tạo ra các ngắt. Có 4 khả năng (tạm gọi là các MODES) có thể xảy ra khi chúng ta nhấn và thả các button. Nếu không nhấn, trạng thái các chân INT là HIGH do điện trở kéo lên, khi vừa nhấn 1 button, sẽ có chuyển trạng thái từ HIGH sang LOW, chúng ta gọi là cạnh xuống - Falling Edge, khi button được nhấn và giữ, trạng thái các chân INT được xác định là LOW và cuối cùng khi thả các button, trạng thái chuyển từ LOW sang HIGH, gọi là cạnh lên – Rising Edge.  Trong những trường hợp cụ thể, 1 trong 4 MODES trên đều hữu ích, ví dụ trong các ứng dụng đếm xung (đếm encoder của servo motor chẳng hạn) thì 2 MODE “cạnh” phải được dùng. Thanh ghi MCUCR chứa các bits cho phép chúng ta chọn

Page 38: asembly

1 trong 4 MODE trên cho các ngắt ngoài. Dưới đây là cấu trúc thanh ghi MCUCR được trích ra từ datasheet của chip atmega8.

      MCUCR là một thanh ghi 8 bit nhưng đối với hoạt động ngắt ngoài, chúng ta chỉ quan tâm đến 4 bit thấp của nó (4 bit cao dùng cho Power manager và Sleep Mode). Bốn bit thấp là các bit Interrupt Sense Control (ISC) trong đó 2 bit ISC11:ISC10 dùng cho INT1 và 2 bit ISC01:ISC00 dùng cho INT0. Hãy nhìn vào bảng tóm tắt bên dưới để biết chức năng của các bit trên, đây là bảng “chân trị” của 2 bit ISC11, ISC10. Bảng chân trị cho các bit ISC01, ISC00 hoàn toàn tương tự.

Bảng 2: INT1 Sense Control

      Thật dễ dàng để hiểu chức năng của các bit Sense Control, ví dụ bạn muốn set cho INT1 là ngắt cạnh xuống (Falling Edge) trong khi INT0 là ngắt cạnh lên (Rising Edge), hãy đặt dòng lệnh MCUCR =0x0B (0x0B = 00001011 nhị phân) trong chương trình của bạn.

      Thanh ghi điều khiển ngắt chung – GICR (General Interrupt Control Register) (chú ý trên các chip AVR cũ, như các chip AT90Sxxxx, thanh ghi này có tên là thanh ghi mặt nạ ngắt thông thường GIMSK, bạn tham khảo thêm datasheet của các chip này nếu cần sử dụng đến). GICR cũng là 1 thanh ghi 8 bit nhưng chỉ có 2 bit cao (bit 6 và bit 7) là được sử dụng cho điều khiển ngắt, cấu trúc thanh ghi như bên dưới (trích datasheet).

Page 39: asembly

      Bit 7 – INT1 gọi là bit cho phép ngắt 1(Interrupt Enable), set bit này bằng 1 nghĩa bạn cho phép ngắt INT1 hoạt động, tương tự, bit INT0 điều khiển ngắt INT0.

      Thanh ghi cờ ngắt chung – GIFR (General Interrupt Flag Register) có 2 bit INTF1 và INTF0 là các bit trạng thái (hay bit cờ - Flag) của 2 ngắt INT1 và INT0. Nếu có 1 sự kiện ngắt phù hợp xảy ra trên chân INT1, bit INTF1 được tự động set bằng 1 (tương tự cho trường hợp của INTF0), chúng ta có thể sử dụng các bit này để nhận ra các ngắt, tuy nhiên điều này là không cần thiết nếu chúng ta cho phép ngắt tự động, vì vậy thanh ghi này thường không được quan tâm khi lập trình ngắt ngoài. Cấu trúc thanh ghi GIFR được trình bày trong hình ngay bên dưới.

      Sau khi đã xác lập các bit sẵn sàng cho các ngắt ngoài, việc sau cùng chúng ta cần làm là set bit I, tức bit cho phép ngắt toàn cục, trong thanh ghi trạng thái chung của chip (thanh ghi SREG, xem lại bài AVR2). Một chú ý khác là vì các chân PD2, PD3 là các chân ngắt nên bạn phải set các chân này là Input (set thanh ghi DDRD). Quá trình thiết lập ngắt ngoài được trình bày trong hình 10.

Hình 3. Thiết lập ngắt ngoài.

Page 40: asembly

      Ngắt ngoài với ASM: Dưới đây tôi trình bày cách viết chương trình  sử dụng ngắt ngoài bằng ngôn ngữ ASM, đối với các ngắt khác bạn chỉ cần thêm các DIRECTIVE để định vị các vector ngắt tương ứng và viết chương trình phục vụ ngắt tương ứng.

List 1. Ngắt với ASM.

12345678910111213141516171819202122232425262728293031323334

.CSEG

.INCLUDE "M8DEF.INC"

.ORG 0x000 ; Định vị vị trí đầu tiên      RJMP BATDAU

.ORG 0x001; Định vị vector ngắt ngoài 0 - INT0 (xem bảng vector)      RJMP INT0_ISR ; Nhảy đến INT0_ISR nếu có ngắt INT0 xảy ra.ORG 0x002 ; Định vị vector ngắt ngoài 1 – INT1 (xem bảng vector)      RJMP INT1_ISR ; Nhảy đến INT1_ISR nếu có ngắt INT1 xảy ra

;Tương tự, định vị các vector ngắt khác ở đây………………..;………………………………………………………………..

.ORG 0x020 ; Định vị chương trình chínhBATDAU:; khởi tạo Stack      LDI R16, HIGH(RAMEND)      LDI R17, LOW(RAMEND)      OUT SPH, R16      OUT SPL, R17

; set chân PD2 và PD3 như các chân input      LDI R16, 0Bxxxx00xx      ; x là trạng thái do bạn tự chọn, 0 hoặc 1      OUT DDRD, R16            ; PD2 và PD3 là input      LDI R16, 0Bxxxx11xx      ; x là trạng thái do bạn tự chọn, 0 hoặc 1      OUT PORTD, R16          ; mắc điện trở kéo lên cho PD2, PD3

; khởi động  ngắt      LDI R16, $0B       ; $0B=00001011,  INT1: ngắt cạnh xuống, INT0: ngắt cạnh lên      OUT MCUCR, R16  ; xuất giá trị điều khiển  ra thanh ghi MCUCR      LDI R16, $C0      ;$C0=11000000: Enable INT1 và INT0      OUT GICR, R16 ;xuất giá trị điều khiển  ra thanh ghi  GICR      SEI ;set bit cho phép ngắt toàn cục; Chương trình chính

Page 41: asembly

35363738394041424344454647

MAIN:;các công việc mà chương trình chính cần thực hiện………………;…………………………………………………………………….      RJMP MAIN

;và đây là định nghĩa trình phục vụ ngắt INT0_ISR…………………INT0_ISR:; các công việc cần thực hiện khi có ngắt ……………………;……………………………………………………………….      RETI ; phải dùng lệnh RETI để quay về chương trình chính

;và đây là định nghĩa trình phục vụ ngắt INT1_ISR…………………INT1_ISR:; các công việc cần thực hiện khi có ngắt ……………………;……………………………………………………………….      RETI ; phải dùng lệnh RETI để quay về chương trình chính

      Bạn thấy các các ngắt được định vị nằm giữa vị trí 0x0000, khi mới khởi động, tại ví trí 0x000 là lệnh “RJMP BATDAU”, như thế các lệnh RJMP tại các vector ngắt và các ISR đều không được thực hiện, chúng chỉ được thực hiện một cách tự động khi có ngắt.

       Ngắt ngoài với C: Avr-libc hỗ trợ một thư viện hàm cho ngắt khá hoàn hảo, để sử dụng ngắt trong chương trình viết bằng C (avr-gcc) bạn chỉ cần include file “interrupt.h” nằm trong thư mục con “avr” là xong. file header interrupt.h chứa định nghĩa các hàm và phương thức phục vụ cho viết trình phục vụ ngắt, các vector ngắt không được định nghĩa trong file này mà trong file iom8.h (cho atmega8). Nếu bạn vô tình tìm thấy 1 chương trình ngắt nào đó không include file interrupt.h mà include file signal.h thì bạn đừng ngạc nhiên, đó là cách viết cũ trong avr-gcc, thật ra bạn hoàn toàn có thể sử dụng cách viết cũ vì các phiên bản mới của avr-libc (đi cùng với các bản WinAVR mới) vẫn hỗ trợ cách viết này nhưng không khuyên khích bạn dùng.

       Trong C, các trình phục vụ ngắt có dạng là ISR(vector_name). Trong các phiên bản cũ trình phục vụ ngắt có tên SIGNAL(vector_name), nhưng cũng như file header signal.h, cách viết này vẫn được hỗ trợ trong phiên bản mới nhưng không được khuyến khích.

List 2. Ngắt với C.

12

#include <avr/interrupt.h>

Page 42: asembly

3456

ISR (vector_name){//user code here}

      Trong đó vector_name là tên của các vector ngắt định nghĩa sẵn avr-libc, ISR là tên bắt buộc, bạn không được dùng các tên khác tùy ỳ (nhưng có thể dùng SIGNAL như đã trình bày ở trên). Đặc biệt, bạn có thể đặt ISR ở trước hoặc sau chương trình chính đều không ảnh hưởng vì thật ra, đã có khá nhiều “công đoạn” được thực hiện khi bạn gọi ISR (nhưng bạn không thấy và cũng không cần quan tâm). ISR luôn được trình biên dịch đặt ở ngoài vùng vector ngắt như cách chúng ta thực hiện trong ASM, như thế một chương trình sử dụng nhiều loại ngắt sẽ phải có số lượng trình ISR tương ứng nhưng với vector_name khác nhau, mỗi khi có ngắt xảy ra, tùy thuộc vào giá trị của vector_name mà 1 trong các trình ISR được thực thi. Đối với các vector_name, để biết được vector_name cho mỗi loại ngắt, bạn cần tham khảo tài liệu “avr-libc manual”. Bảng 10 tóm tắt các vector_name của một số ngắt thông dụng trên atmega8, bạn chú ý rằng các vector_name trong avr-libc được định nghĩa rất khác nhau cho từng loại chip, bạn nhất thiết  phải sử dụng tài liệu “avr-libc manual” để biết chính xác các vector_name cho loại chip mà bạn đang dùng.

Bảng 3: vector_name cho atmega8.

Vector name Old vector name DescriptionADC_vect SIG_ADC ADC Conversion CompleteANA_COMP_vect SIG_COMPARATOR Analog ComparatorEE_RDY_vect SIG_EEPROM_READY EEPROM ReadyINT0_vect SIG_INTERRUPT0 External Interrupt 0INT1_vect SIG_INTERRUPT1 External Interrupt Request 1SPI_STC_vect SIG_SPI Serial Transfer CompleteSPM_RDY_vect SIG_SPM_READY Store Program Memory ReadyTIMER0_OVF_vect SIG_OVERFLOW0 Timer/Counter0 OverflowTIMER1_CAPT_vect SIG_INPUT_CAPTURE1 Timer/Counter Capture EventTIMER1_COMPA_vect SIG_OUTPUT_COMPARE1A Timer/Counter1 Compare Match ATIMER1_COMPB_vect SIG_OUTPUT_COMPARE1B Timer/Counter1 Compare MatchBTIMER1_OVF_vect SIG_OVERFLOW1 Timer/Counter1 OverflowTIMER2_COMP_vect SIG_OUTPUT_COMPARE2 Timer/Counter2 Compare MatchTIMER2_OVF_vect SIG_OVERFLOW2 Timer/Counter2 OverflowTWI_vect SIG_2WIRE_SERIAL 2-wire Serial Interface

Page 43: asembly

USART3_UDRE_vect SIG_USART3_DATA USART3 Data register Empty

III. Ví dụ ngắt ngoài với C.

      Để thực hiện ví dụ sử dụng ngắt ngoài bằng C, tôi sẽ viết lại chương trình ví dụ của bài "cấu trúc AVR" nhưng bằng ngôn ngữ C và sử dụng ngắt. Trong chương trình ví dụ của bài AVR2, chúng ta thực hiện việc đếm lên và đếm xuống dùng 2 button, chúng ta sẽ vẫn thực hiện trên ý tưởng này nhưng có chút thay đổi trong kết nối, trước hết bạn vẽ 1 mạch điện mô phỏng trong Proteus như hình 4.

Hình 4. Mạch điện mô phỏng ngắt.

      Kết nối button đếm lên với ngắt INT0, button đếm xuống với INT1, PORTB được chọn làm PORT xuất. Hãy chạyProgrammer Notepad,  tạo 1 Project mới tên AVR2-INT, type đoạn code bên dưới vào 1 file new và lưu với tên main.c, add file này vào Project của bạn, sau đó tạo một Makefile cho Project.

List 3. ví dụ ngắt ngoài bằng C.

12345678

#include <avr/io.h>#include <avr/interrupt.h>#include <avr/delay.h>

volatile int8_t  val=0;    //khai báo 1 biến val 8 bit, có dấu và giá trị khởi tạo bằng 0.int main(void){

    DDRD=0x00;     //khai báo PORTD là Input để sử dụng 2 chân ngắt.

Page 44: asembly

9101112131415161718192021222324252627282930313233343536

    PORTD=0xFF;  //sử dụng điện trở nội kéo lên.    DDRB=0xFF;    //PORTB là Output để xuất LED 7 đoạn        MCUCR|=(1<<ISC11)|(1<<ISC01); //cả 2 ngắt là ngắt cạnh xuống         GICR    |=(1<<INT1)|(1<<INT0);    //cho phép 2 ngắt hoạt động    sei();                                            //set bit I cho phép ngắt toàn cục        DDRC=0xFF;                                   //PORTC là Output     while (1){                                         //vòng lặp vô tận            PORTC++;                                //quét PORTC        _delay_loop_2(60000);    }    return 0;}

//Trình phục vụ ngắt của  INT0ISR(INT0_vect){    val++;                                       //nếu có ngắt INT0 xảy ra, tăng val thêm 1    if (val>9) val=0;                        //giới hạn không vượt quá 9    PORTB=val;}

//Trình phục vụ ngắt của  INT1ISR(INT1_vect){    val--;                                        //nếu có ngắt INT1 xảy ra, giảm val đi 1    if (val<0) val=9;                       //giới hạn không nhỏ hơn 0    PORTB=val;}

      Có lẽ đoạn code này khá dễ hiểu nếu các bạn theo dõi từ đầu bài học, tôi chỉ giải thích những nét cơ bản và “mới”. Ý tưởng là chúng ta sử dụng 1 biến tạm 8 bit, có dấu để lưu giá trị đếm, tên biến val,  mỗi khi có ngắt trên chân INT0, tăng val 1 đơn vị và ngược lại khi có ngắt trên INT1, giảm val đi 1, đó là nội dung của 2 trình phục vụ ngắt. Trong chương trình chính, trước hết chúng ta thực hiện việc xác lập hoạt động cho 2 ngắt, sau đó đưa chương trình vào 1 vòng lặp vô tận while(1), PORTC được dùng để kiểm tra rằng chương trình trong vòng lặp vô tận vẫn đang hoạt động. Có lẽ phần khó hiểu nhất trong đoạn code là cách mà tôi dùng để khai báo cho 2 thanh ghi điều khiển ngắt MCUCR và GICR.

Page 45: asembly

      Nếu xem lại bảng tóm tắt các toán tử của C, toán tử “<<” được gọi là toán tử “dịch trái” dùng trên dạng nhị phân của các con số, nếu bạn thấy x=5<<3 nghĩa là dịch các bit nhị phân của 5 sang trái 3 vị trí và gán cho x, như mô tả như sau:

     Bạn thấy toàn bộ các bit của 5 đã dịch sang trái 3 vị trí và giá trị của số mới thu được là x=40, chú ý 40=5x8=5x2^3 . Hãy nhìn câu lệnh MCUCR|=(1<<ISC11)|(1<<ISC01), giờ thì bạn đã hiểu (1<<ISC11) nghĩa là dịch số 1 sang trái ISC11 vị trí, và (1<<ISC01) là dịch số 1 sang trái ISC01 vị trí, nhưng ISC11 và ISC01 ở đâu ra và giá trị của chúng là bao nhiêu? Bạn chú ý, khi bạn include file “io.h” thì file “iom8.h” được chèn vào, và trong file này chứa khai báo địa chỉ các thanh ghi của chip atmega8, các tên bit cũng được khai báo sẵn trong file này, nếu bạn mở file iom8.h (thường nằm trong thư mục ~\WinAVR\avr\include\avr) bằng 1 chương trình text editor như notepad, dùng chức năng find bạn sẽ thấy các dòng định nghĩa như sau:

/* MCUCR */#define SE         7#define SM2      6#define SM1      5#define SM0      4#define ISC11   3#define ISC10   2#define ISC01   1#define ISC00   0

        Đây là định nghĩa vị trí các bit trong thanh ghi MCUCR, vậy là đã rõ, ISC11=3, ISC01=1, do đó: (1<<ISC11) tương đương (1<<3) = 00001000 (Binary) và (1<<ISC01) = 00000010,  bạn hãy tưởng tượng rằng bạn đã mang số 1 đến các vị trí của ISC11 và ISC01 trong thanh ghi MCUCR. Bây giờ đến lượt toán tử OR bitwise “|”.

(1<<ISC11)                        = 00001000(1<<ISC01)                        = 00000010--------------------------------------------------(1<<ISC11)|(1<<ISC01)     = 00001010

Page 46: asembly

       Gán giá trị này cho MCUCR, đối chiếu với bảng các giá trị của các bit ISC (bảng 9) bạn sẽ thấy chúng ta đang set cho 2 ngắt là falling edge. Điều cuối cùng của câu lệnh set MCUCR là cách rút gọn câu lệnh MCUCR|=(1<<ISC11)|(1<<ISC01) thực chất là MCUCR= MCUCR|  ((1<<ISC11)|(1<<ISC01)), đây là cách set một số bit trong một thanh ghi mà không muốn làm ảnh hưởng đến các bit khác (nhưng bạn phải thật cẩn thận với cách làm này vì có thể sẽ phản tác dụng nếu bạn không nắm rõ), bạn có thể gán trực tiếp MCUCR=(1<<ISC11)|(1<<ISC01), hay nhanh hơn MCUCR=0x0A (0x0A=00001010). Vậy lí do nào khiến tôi biến 1 câu lệnh gán đơn giản thành một “bài toán” khó hiểu, câu trả lời chính là tính tổng quát. Trong các chip AVR khác nhau, vị trí các bit trong các thanh ghi là rất khác nhau, câu lệnh MCUCR=0x0A đúng cho atmega8 nhưng không áp dụng được cho các chip khác trong khi câu lệnh MCUCR=(1<<ISC11)|(1<<ISC01) thì hoạt động tốt, một lí do khác là cách viết gián tiếp này giúp người khác (hay chính bạn sau này) khi đọc code có thể dễ dàng hiểu được ý đồ người viết…      Tôi nghĩ bạn đã quá hiểu dòng lệnh tiếp theo, GICR |=(1<<INT1)|(1<<INT0). Tôi dừng giải thích đoạn code ở đây và cũng dừng bài AVR3, bạn hãy thực tập bằng cách viết lại đoạn code trên bằng ASM. 

Bài 4 - Timer - Counter

1 2 3 4 5

 ( 352 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu.

2. Tổng quan Timer/Counter trên AVR.

3. Sử dụng Timer/Counter.

1. Timer/Counter0

2. Timer/Counter1

Cấu trúc AVR .

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

Page 47: asembly

Download ví dụ

I. Giới thiệu.

       Trong bài 3 tôi đã giới thiệu khái quát phương pháp lập trình bằng ngôn ngữ C cho AVR với WinAVR và cách sử dụng ngắt trong AVR. Bài 4 này chúng ta sẽ khảo sát các chế độ hoạt động của  phương pháp điều khiển các bộ định thời, đếm (Timer/Counter) trong AVR. Công cụ phục vụ cho bài này vẫn là bộ công cụ WinAVR và phần mềm mô phỏng Proteus. Tôi vẫn dùng chip Atmega8 để làm ví dụ. Một điều không may mắn là không phải tất cả các bộ Timer/Counter trên tất cả các dòng chip AVR là như nhau, vì thế những gì tôi trình bày trong bài này có thể sẽ không đúng với các dòng AVR khác như AT90S…Tuy nhiên tôi cũng sẽ cố gắng chỉ ra một số điểm khác biệt cơ bản để các bạn có thể tự mình điều khiển các chip khác. Nội dung bài học này bao gồm:

Nắm bắt cơ bản các bộ Timer/Counter có trên AVR.

Sử dụng các Timer/Counter như các bộ định thời.

Sử dụng các Timer/Counter như các bộ đếm.

Sử dụng các Timer/Counter như các bộ tạo xung điều rộng PWM.

Viết một ví dụ điều khiển động cơ RC servo bằng PWM.

II. Tổng quan các bộ Timer/Counter trên chip Atmega8.

       Timer/Counter là các module độc lập với CPU. Chức năng chính của các bộ Timer/Counter, như tên gọi của chúng, là định thì (tạo ra một khoảng thời gian, đếm thời gian…) và đếm sự kiện.  Trên các chip AVR, các bộ Timer/Counter còn có thêm chức năng tạo ra các xung điều rộng PWM (Pulse Width Modulation), ở một số dòng AVR, một số Timer/Counter còn được dùng như các bộ canh chỉnh thời gian (calibration) trong các ứng dụng thời gian thực. Các bộ Timer/Counter được chia theo độ rộng thanh ghi chứa giá trị định thời hay giá trị đếm của chúng, cụ thể trên chip Atmega8 có 2 bộ Timer 8 bit (Timer/Counter0 và Timer/Counter2) và 1 bộ 16 bit (Timer/Counter1). Chế độ hoạt động và phương pháp điều khiển của từng Timer/Counter cũng không hoàn toàn giống nhau, ví dụ ở chip Atmega8:       Timer/Counter0: là một bộ định thời, đếm đơn giản với 8 bit. Gọi là đơn giản vì bộ này chỉ có 1 chế độ hoạt động (mode) so với 5 chế độ của bộ Timer/Counter1. Chế độ hoat động của Timer/Counter0 thực chất có thể coi như 2 chế độ nhỏ (và cũng là 2 chức năng cơ bản) đó là tạo ra một khoảng thời gian và

Page 48: asembly

đếm sự kiện. Chú ý là trên các chip AVR dòng mega sau này như Atmega16,32,64…chức năng của Timer/Counter0 được nâng lên như các bộ Timer/Counter1…       Timer/Counter1: là bộ định thời, đếm đa năng 16 bit. Bộ Timer/Counter này có 5 chế độ hoạt động chính. Ngoài các chức năng thông thường, Timer/Counter1 còn được dùng để tạo ra xung điều rộng PWM dùng cho các mục đích điều khiển. Có thể tạo 2 tín hiệu PWM  độc lập trên các chân OC1A (chân 15) và OC1B (chân 16) bằng Timer/Counter1. Các bộ Timer/Counter kiểu này được tích hợp thêm khá nhiều trong các chip AVR sau này, ví dụ Atmega128 có 2 bộ, Atmega2561 có 4 bộ…       Timer/Counter2: tuy là một module 8 bit như Timer/Counter0 nhưng Timer/Counter2 có đến 4 chế độ hoạt động như Timer/Counter1, ngoài ra nó nó còn được sử dụng như một module canh chỉnh thời gian cho các ứng dụng thời gian thực (chế độ asynchronous).       Trong phạm vi bài 4 này, tôi chủ yếu hướng dẫn cách sử dụng 4 chế độ hoạt động của các Timer/Counter. Chế độ asynchronous của Timer/Counter2 sẽ được bỏ qua vì có thể chế độ này không được sử dụng phổ biến.Trước khi khảo sát hoạt động của các Timer/Counter, chúng ta thống nhất cách gọi tắt tên gọi của các Timer/Counter là T/C, ví dụ T/C0 để chỉ Timer/Counter0…

II. Sử dụng Timer/Counter.

       Có một số định nghĩa quan trọng mà chúng ta cần nắm bắt trước khi sử dụng các T/C trong AVR:

BOTTOM: là giá trị thấp nhất mà một T/C có thể đạt được, giá trị này luôn là 0.

MAX: là giá trị lớn nhất mà một T/C có thể đạt được, giá trị này được quy định bởi

bởi giá trị lớn nhất mà thanh ghi đếm của T/C có thể chứa được. Ví dụ với một bộ

T/C 8 bit thì giá trị MAX luôn là 0xFF (tức 255 trong hệ thập phân), với bộ T/C 16

bit thì MAX bằng 0xFFFF (65535). Như thế MAX là giá trị không đổi trong mỗi

T/C.

TOP: là giá trị mà khi T/C đạt đến nó sẽ thay đổi trạng thái, giá trị này không nhất

thiết là số lớn nhất 8 bit hay 16 bit như MAX, giá trị của TOP có thể thay đổi bằng

cách điều khiển các bit điều khiển tương ứng hoặc có thể nhập trừ tiếp thông qua

một số thanh ghi. Chúng ta sẽ hiểu rõ về giá trị TOP trong lúc khảo sát T/C1.

Page 49: asembly

1. Timer/Counter0:       Thanh ghi: có 4 thanh ghi được thiết kế riêng cho hoạt động và điều khiển T/C0, đó là:

TCNT0 (Timer/Counter Register): là 1 thanh ghi 8 bit chứa giá trị vận hành của T/C0. Thanh ghi này cho phép bạn đọc và ghi giá trị một cách trực tiếp.

TCCR0 (Timer/Counter Control Register): là thanh ghi điều khiển hoạt động của T/C0. Tuy là thanh ghi 8 bit nhưng thực chất chỉ có 3 bit có tác dụng đó là CS00, CS01 và CS02.

       Các bit CS00, CS01 và CS02 gọi là các bit chọn nguồn xung nhịp cho T/C0 (Clock Select). Chức năng các bit này được mô tả trong bảng 1.

Bảng 1: chức năng các bit CS0X

TIMSK (Timer/Counter Interrupt Mask Register): là thanh ghi mặt nạ cho ngắt của tất cả các T/C trong Atmega8, trong đó chỉ có bit TOIE0 tức bit số 0 (bit đầu tiên) trong thanh ghi này là liên quan đến T/C0, bit này có tên là bit cho phép ngắt khi có tràn ở T/C0. Tràn (Overflow) là hiện tượng xảy ra khi bộ giá trị trong thanh ghi TCNT0 đã đạt đến MAX (255) và lại đếm thêm 1 lần nữa.

Page 50: asembly

       Khi bit TOIE0=1, và bit I trong thanh ghi trạng thái được set (xem lại bài 3 về điều khiển ngắt), nếu một “tràn” xảy ra sẽ dẫn đến ngắt tràn.

TIFR (Timer/Counter Interrupt Flag Register): là thanh ghi cờ nhớ cho tất cả các bộ T/C. Trong thanh ghi này bit số 0, TOV0 là cờ chỉ thị ngắt tràn của T/C0. Khi có ngắt tràn xảy ra, bit này tự động được set lên 1. Thông thường trong điều khiển các T/C vai trò của thanh ghi TIFR không quá quan trọng.

       Hoạt động: T/C0 hoạt động rất đơn giản, hoạt động của T/C được “kích” bởi một tín hiệu (signal), cứ mỗi lần xuất hiện tín hiệu “kích” giá trị của thanh ghi TCNT0 lại tăng thêm 1 đơn vị, thanh ghi này tăng cho đến khi nó đạt mức MAX là 255, tín hiệu kích tiếp theo sẽ làm thanh ghi TCNT0 trở về 0 (tràn), lúc này bit cờ tràn TOV0 sẽ tự động được set bằng 1. Với cách thức hoạt động như thế có vẻ T/C0 vô dụng vì cứ tăng từ 0 đến 255 rồi lại quay về 0, và quá trình lặp lại. Tuy nhiên, yếu tố tạo sự khác biệt chính là tín hiệu kích và ngắt tràn, kết hợp 2 yếu tố này chúng ta có thể tạo ra 1 bộ định thời gian hoặc 1 bộ đếm sự kiện. Trước hết bạn hãy nhìn lại bảng 1 về các bit chọn xung nhịp cho T/C0. Xung nhịp cho T/C0 chính là tín hiệu kích cho T/C0. Xung nhịp này có thể tạo bằng nguồn tạo dao động của chip (thạch anh, dao động nội trong chip…). Bằng cách đặt giá trị cho các bit CS00, CS01 và CS02 của thanh ghi điều khiển TCCR0, chúng ta sẽ quyết định bao lâu thì sẽ kích T/C0 một lần. Ví dụ mạch ứng dụng của bạn có nguồn dao động clk = 1MHz tức  chu kỳ 1 nhịp là 1us (1 micro giây), bạn đặt thanh ghi TCCR0=5 (tức SC02=1, CS01=0, CS00=1). Căn cứ theo bảng 1, tín hiệu kích cho T/C0 sẽ bằng clk/1024 nghĩa là sau 1024us thì T/C0 mới được kích 1 lần, nói cách khác giá trị của TCNT0 tăng thêm 1 sau 1024us (chú ý là tần số được chia cho 1024 thì chu kỳ sẽ tăng 1024 lần). Quan sát 2 dòng cuối cùng trong bảng 1 bạn sẽ thấy rằng tín hiệu kích cho T/C0 có thể lấy từ bên ngoài (External clock source), đây chính là ý tưởng cho hoạt động của chức năng đếm sự kiện trên T/C0. Bằng cách thay đổi trạng thái chân T0 (chân 6 trên chip Atmega8) chúng ta sẽ làm tăng giá trị thanh ghi TCNT0 hay nói cách khác T/C0 có thể dùng để đếm sự kiện xảy ra trên chân T0. Dưới đây chúng ta sẽ xem xét cụ thể cách điều khiển T/C0 theo 1 chế độ định thời gian và đếm.

1.1 Bộ định thời gian. 

       Chúng ta có thể tạo ra 1 bộ định thì để cài đặt một khoảng thời gian nào đó. Ví dụ bạn muốn rằng cứ sau chính xác 1ms thì chân PB0 thay đổi trạng thái 1 lần (nhấp nháy), bạn lại không muốn dùng các lệnh delay như trước nay vẫn dùng vì nhược điểm của delay là “CPU không làm gì cả” trong lúc delay, vì thế trong nhiều trường hợp các lệnh delay rất hạn chế được sử dụng. Bây giờ chúng ta dùng T/C0 để làm việc này, ý tưởng là chúng ta cho bộ đếm T/C0 hoạt động, khi nó đếm đủ

Page 51: asembly

1ms thì nó sẽ tự kích hoạt ngắt tràn, trong trình phục vụ ngắt tràn chúng tat hay đổi trạng thái chân PB0. Tôi minh họa ý tưởng như trong hình 1.

Hình 1. So sánh 2 cách làm việc.       (CPU nop: trong khoảng thời gian này CPU không làm gì cả)       Một vấn đề nảy sinh lúc này, như tôi trình bày trong phần trước, T/C0 chỉ đếm từ 0 đến 255  rồi lại quay về 0 (xảy ra 1 ngắt tràn), như thế dường như chúng ta không thể cài đặt giá trị mong muốn bất kỳ cho T/C0? Câu trả lời là chúng ta có thể bằng cách gán trước một giá trị cho thanh ghi TCNT0, khi ấy T/C0 sẽ đếm từ giá trị mà chúng ta gán trước và kết thúc ở 255. Tuy nhiên do khi tràn xảy ra, TCNT0 lại được tự động trả về 0, do đó việc gán giá trị khởi tạo cho TCNT0 phải được thực hiện liên tục sau mỗi lần xảy ra tràn, vị trí tốt nhất là đặt trong trình phục vụ ngắt tràn.       Việc còn lại và cũng là việc quan trọng nhất là việc tính toán giá trị chia (prescaler) cho xung nhịp của T/C0 và việc xác định giá trị khởi đầu cần gán cho thanh ghi TCNT0 để có được 1 khoảng thời gian định thì chính xác như mong muốn. Trước hết chúng ta sẽ chọn prescaler  sao cho hợp lí nhất (chọn giá trị chia bằng cách set 3 bit CS02,CS01,CS00). Giả sử nguồn xung clock  “nuôi” chip của chúng ta là clkI/O=1MHz tức là 1 nhịp mất 1us, nếu chúng ta để prescaler=1, tức là tần số của T/C0 (tạm gọi là fT/C0) cũng bằng clkI/O=1MHz, cứ 1us T/C0 được kích và TCNT0 sẽ tăng 1 đơn vị. Khi đó giá trị lớn nhất mà T/C0 có thể đạt được là 256 x 1us=256us, giá trị này nhỏ hơn 1ms mà ta mong muốn. Nếu chọn prescaler=8 (xem bảng 1) nghĩa là cứ sau 8 nhịp (8us) thì TCNT0 mới tăng 1 đơn vị, khả năng lớn nhất mà T/C0 đếm được là 256 x 8us=2048us, lớn hơn 1ms, vậy ta hoàn toàn có thể sử dụng prescaler=8 để tạo ra một khoảng định thì 1ms. Bước tiếp theo là xác định giá trị khởi đầu của TCNT0 để T/C0 đếm đúng 1ms (1000us). Ứng với prescaler=8 chúng ta đã biết là cứ 8us thì TCNT0 tăng 1 đơn vị, dễ dàng tính được bộ đếm cần đếm 1000/8=125 lần để hết 1ms, do đó  giá trị ban đầu của TCNT0 phải là 256-125=131. Bạn có thể quan sát hình 2 để hiểu thấu đáo hơn.

Page 52: asembly

Hình 2. Quá trình thực hiện.       Hãy tạo 1 Project bằng Programmer Notepad với tên gọi TIMER0 và viết đoạn code cho Project này như trong list 1.List 1. Định thì 1ms với T/C0.123456789101112131415161718192021222324

#include <avr/io.h>#include <avr/interrupt.h>#include <util/delay.h>

int main(void){       DDRB=0xFF;                //PORTB la output PORT       PORTB=0x00;

       TCCR0=(1<<CS01);// CS02=0, CS01=1, CS00=0: chon Prescaler = 8       TCNT0=131;              //gan gia tri khoi tao cho T/C0       TIMSK=(1<<TOIE0);//cho phep ngat khi co tran o T/C0       sei();                       //set bit I cho phep ngat toan cuc

       while (1){           //vòng lặp vô tận               //do nothing       }       return 0;}

//trinh phuc vu ngat tran T/C0ISR (TIMER0_OVF_vect ){              TCNT0=131; //gan gia tri khoi tao cho T/C0        PORTB^=1; //doi trang thai Bit PB0}

       Đoạn code rất đơn giản, bạn chỉ cần chú ý đến 3 dòng khai báo cho T/C0 (dòng  9, 10, 11). Với dòng 9: TCCR0=(1<<CS01) là 1 cách set bit CS01 trong thanh ghi điều khiển TCCR0 lên 1, 2 bit CS02 và CS00 được để giá trị 0 (bạn xem

Page 53: asembly

lại bài 3 về cách set các bit đặc biệt trong các thanh ghi), tóm lại dòng này tương đương TCCR0=2, giá trị Prescaler được chọn bằng 8 (tham khảo bảng 1). Dòng 10 chúng ta gán giá trị khởi tạo cho thanh ghi TCNT0. Và dòng 11 set bit TIOE0 lên 1 để cho phép ngắt xảy ra khi có tràn ở T/C0. Trong trình phục vụ ngắt tràn T/C0, chúng ta sẽ thực hiện đổi trạng thái chân PB0 bằng toán từ XOR (^), chú ý đến ý nghĩa của toán tử XOR: nếu XOR một bit với số 1 thì bit này sẽ chuyển trạng thái (từ 0 sang 1 và ngược lại). Cuối cùng và quan trọng là chúng ta cần gán lại giá trị khởi tạo cho T/C0.     Bạn có thể vẽ môt mạch điện mô phỏng đơn giản dùng 1 Oscilloscope như trong hình 3 để kiểm tra hoạt động của  đoạn code.

Hình 3. Mô phỏng định thì của T/C0.1.2 Bộ đếm sự kiện.        Như tôi trình bày trong phần hoạt động của T/C0, chúng ta có thể dùng T/C0 như một bộ đếm (counter) để đếm các sự kiện (sự thay đổi trạng thái) xảy ra trên chân T0. Bằng cách đặt giá trị cho thanh ghi TCCR0 = 6 (CS02=1, CS01=1, CS00=0) cho phép đếm “cạnh xuống” trên chân T0, nếu TCCR0 = 7 (CS02=1, CS01=1, CS00=1) thì “cạnh lên” trên chân T0 sẽ được đếm. Có sử dụng ngắt hay không phụ thuộc vào mục đích sử dụng. Khảo sát 1 ví dụ đơn giản gần giống với ví dụ đếm trong bài AVR2 nhưng sử dụng T/C0 và chỉ đếm 1 chiều tăng. Kết nối mạch điện như trong hình 4, mỗi lần Button 1 được nhấn, giá trị đếm tăng thêm 1. Button 2 dùng reset giá trị đếm về 0. Đoạn code cho ví dụ thứ 2 này được trình bày trong List 2. 

Page 54: asembly

Hình 4. Đếm 1 chiều bằng T/C0.List 2. Đếm sự kiện với T/C012345678910111213141516171819

#include <avr/io.h>#include <avr/interrupt.h>

int main(void){       DDRB=0xFF;                //PORTB la output PORT       PORTB=0x00;       DDRD=0x00; //khai bao PORTD la input de ket noi Button kich vao chan T0       PORTD=0xFF; //su dung dien tro keo len cho PORTD

       TCCR0=(1<<CS02)|(1<<CS01);// CS02=1, CS01=1, CS00=0: xung nhip tu  chan T0, down       TCNT0=0;

       while (1){           //vòng lặp vô tận              if (TCNT0==10) TCNT0=0;             PORTB=TCNT0;   //xuat gia tri dem ra led 7 doan             if (bit_is_clear(PIND,7)) TCNT0=0;  //Reset bo dem neu chan PD7=0        }       return 0;}

       Nội dung trong chương trình chính là khai báo các hướng giao tiếp cho các PORT, PORTB là ouput để xuất kết quả đếm ra led 7 đoạn, PORTD được khái báo input vì các button được nối với PORT này. T/C0 được khai báo sử dụng nguồn kích ngoài từ T0, dạng cạnh xuống thông qua dòng TCCR0=(1<<CS02)|(1<<CS01), bạn cũng có thể khai báo tương đương là TCCR0=6 (tham khảo bảng 1). Giá trị của bộ đếm sẽ được xuất ra PORTB để kiểm tra. Điểm chú ý trong đoạn chương trình này là macro “bit_is_clear”, đây là một macro được định nghĩa trong

Page 55: asembly

file “sfr_defs.h” dùng để kiểm tra 1 bit trong một thanh ghi đặc biệt có được xóa (bằng 0) hay không, trong trường hợp của đoạn code trên: “if(bit_is_clear(PIND,7)) TCNT0=0;” nghĩa là kiểm tra xem nếu chân PD7 được kéo xuống 0 (button 2 được nhấn) thì sẽ reset bộ đếm về 0.       Như vậy việc sử dụng T/C0 là tương đối đơn giản, bạn chỉ cần khai báo các giá trị thích hợp cho thanh ghi điều khiển TCCR0 bằng cách tham khảo bảng 1, sau đó khởi tạo giá trị cho TCNT0 (nếu cần thiết), khai báo có sử dụng ngắt hay không bằng cách set hay không set bit TOIE0 trong thanh ghi TIMSK là hoàn tất.2. Timer/Counter1:       Timer/Counter1 là bộ T/C 16 bits, đa chức năng. Đây là bộ T/C rất lý tưởng cho lập trình đo lường và điều khiển vì có độ phân giải cao (16 bits) và có khả năng tạo xung điều rộng PWM (Pulse Width Modulation – thường dùng để điều khiển động cơ).       Thanh ghi: có khá nhiều thanh ghi liên quan đến T/C1. Vì là T/C 16 bits trong khi độ rộng bộ nhớ dữ liệu của AVR là 8 bit (xem lại bài 2) nên đôi khi cần dùng những cặp thanh ghi 8 bits tạo thành 1 thanh ghi 16 bit, 2 thanh ghi 8 bits sẽ có tên kết thúc bằng các ký tự L và H trong đó L là thanh ghi chứa 8 bits thấp (LOW) và H là thanh ghi chứa 8 bits cao (High) của giá trị 16 bits mà chúng tạo thành.

TCNT1H và TCNT1L (Timer/Counter Register): là 2 thanh ghi 8 bit tạo thành

thanh ghi 16 bits (TCNT1) chứa giá trị vận hành của T/C1. Cả 2 thanh ghi này cho

phép bạn đọc và ghi giá trị một cách trực tiếp. 2 thanh ghi được kết hợp như sau:

TCCR1A và TCCR1B (Timer/Counter Control Register): là 2 thanh ghi điều khiển

hoạt động của T/C1. Tất cả các mode hoạt động của T/C1 đều được xác định thông

qua các bit trong 2 thanh ghi này. Tuy nhiên, đây không phải là 2 byte cao và thấp

của một thanh ghi mà là 2 thanh ghi hoàn toàn độc lập. Các bit trong 2 thanh ghi

này bao gồm các bit chọn mode hay chọn dạng sóng (Waveform Generating Mode

– WGM), các bit quy định dạng ngõ ra (Compare Output Match – COM), các bit

Page 56: asembly

chọn giá trị chia prescaler cho xung nhịp (Clock Select – CS)…Cấu trúc của 2

thanh ghi được trình bày như bên dưới.

       Nhìn chung để “thuộc” hết cách phối hợp các bit trong 2 thanh ghi TCCR1A và TCCR1B là tương đối phức tạp vì T/C1 có rất nhiều mode hoạt động, chúng ta sẽ khảo sát chúng trong phần các chế độ hoạt động của T/C1 bên dưới. Ở đây, trong thanh ghi TCCR1B có 3 bit khá quen thuộc là CS10, CS11 và CS12. Đây là các bit chọn xung nhịp cho  T/C1 như truong T/C0. Bảng 2 sẽ tóm tắt các chế độ xung nhịp trong T/C1.Bảng 2: chức năng các bit CS12, CS11 và CS10.

OCR1A và OCR1B (Ouput Compare Register A và B): có một số khái niệm mới

mà chúng ta cần biết khi làm việc với T/C1, một trong số đó là Ouput Compare

(sorry, I don’t wanna translate it to Vietnamese). Trong lúc T/C hoạt động, giá trị

thanh ghi TCNT1 tăng, giá trị này được liên tục so sánh với các thanh ghi OCR1A

và OCR1B (so sánh độc lập với từng thanh ghi), việc so sánh này trên AVR gọi là

gọi là Ouput Compare. Khi giá trị so sánh bằng nhau thì 1 “Match” xảy ra, khi đó

Page 57: asembly

một ngắt hoặc 1 sự thay đổi trên chân OC1A (hoặc/và chân OC1B) xảy ra (đây là

cách tạo PWM bởi T/C1). Tại sao lại có A và B? Đó là vì người thiết kế AVR

muốn mở rộng khả năng ứng dụng T/C1 cho bạn. A và B đại diện cho 2 kênh

(channel)  và B. Cũng vì điều này mà chúng ta có thể tạo 2 kênh PWM bằng T/C1.

Tóm  lại, cơ bản 2 thanh ghi này chứa các giá trị để so sánh, chức năng và các chế

độ hoạt động cụ thể của chúng sẽ được khảo sát trong các phần sau.

ICR1 (InputCapture Register 1): khái niệm mới thứ 2 của T/C1 là Input Capture.

Khi có 1 sự kiện trên chân ICP1 (chân 14 trên Atmega8), thanh ghi ICR1sẽ

“capture” giá trị của thanh ghi đếm TCNT1. Một ngắt có thể xảy ra trong trường

hợp này, vì thế Input Capture có thể được dùng để cập nhật giá trị “TOP” của

T/C1.

TIMSK (Timer/Counter Interrupt Mask Register): các bộ T/C trên AVR dùng

chung thanh ghi mặt nạ ngắt, vì thế TIMSK cũng được dùng để quy định ngắt cho

T/C1. Có điều lúc này chúng ta chỉ quan tâm đến các bit từ 2 đến 5 của TIMSK.

Có tất cả 4 loại ngắt trên T/C1 (nhớ lại T/C0 chỉ có 1 loại ngắt tràn)

Page 58: asembly

Bit 2 trong TIMSK là TOIE1, bit quy định ngắt tràn cho thanh T/C1 (tương tự trường hợp của T/C0).Bit 3, OCIE1B là bit cho phép ngắt khi có 1 “Match” xảy ra trong việc so sánh TCNT1 với OCR1B.Bit 4, OCIE1A là bit cho phép ngắt khi có 1 “Match” xảy ra trong việc so sánh TCNT1 với OCR1A.Bit 5, TICIE1 là bit cho phép ngắt trong trường hợp Input Capture được dùng.

       Cùng với việc set các bit trên, bit I trong thanh ghi trạng thái phải được set nếu muốn sử dụng ngắt (xem lạibài 3 về điều khiển ngắt).

TIFR (Timer/Counter Interrupt Flag Register): là thanh ghi cờ nhớ cho tất cả các

bộ T/C. Các bit từ 2 đến 5 trong thanh ghi này là các cờ trạng thái của T/C1.

       Các mode hoạt động: có tất cả 5 chế độ hoạt động chính trên T/C1. Các chế độ hoạt động cơ bản được quy định bởi 4 bit Waveform Generation Mode (WGM13, WGM12, WGM11 WGM10) và một số bit phụ khác. 4 bit Waveform Generation Mode lại được bố trí nằm trong 2 thanh ghi TCCR1A và TCCR1B (WGM13 là bit 4, WGM12 là bit 3 trong TCCR1B trong khi WGM11 là bit 1 và WGM10 là bit 0 trong thanh ghi TCCR1A) vì thế cần phối hợp 2 thanh ghi TCCR1 trong lúc điều khiển T/C1. Các chế độ hoạt động của T/C1 được tóm tắt trong bảng sau 3:Bảng 3: các bit WGM và các chế độ hoạt động của T/C1.

Page 59: asembly

2.1 Normal mode (Chế độ thường).        Đây là chế độ hoạt động đơn giản nhất của T/C1. Trong chế độ này, thanh ghi đếm TCNT1 được tăng giá trị từ 0 (BOTTOM) đến 65535 hay 0xFFFF (TOP) và quay về 0. Chế độ này hoàn toàn giống cách mà Timer0 hoạt động chỉ có khác là giá trị đếm cao nhất là 65535 thay vì 255 như trong timer0. Nhìn vào bảng 3, để set T/C1 ở Normal mode chúng ta cần set 4 bit WGM về 0, vì 0 là giá trị mặc định của các thanh ghi nên thực tế chúng ta không cần tác động đến các bit WGM. Duy nhất một việc quan trọng cần làm là set các bit Clock Select (CS12, SC11, CS10) trong thanh ghi TCCR1B (xem thêm bảng 2). Bạn có thể tham khảo ví dụ của Timer0. Đoạn code trong  list 3 là 1 ví dụ tạo 1 khoảng thời gian 10ms bằng T/C1, normal mode:List 3. Định thì 10ms với T/C1.12345678910

#include <avr/io.h>#include <avr/interrupt.h>#include <util/delay.h>

int main(void){       DDRB=0xFF;                //PORTB la output PORT       PORTB=0x00;

       TCCR1B=(1<CS10);// CS12=0, CS11=0, CS10=1: chon Prescaler =1       // thanh ghi TCCR1B duoc dung thay vi TCCR0 cua Timer0

Page 60: asembly

1112131415161718192021222324

       TCNT1=55535;              //gan gia tri khoi tao cho T/C1       TIMSK=(1<<TOIE1);//cho phep ngat khi co tran o T/C1        sei();                       //set bit I cho phep ngat toan cuc

       while (1){           //vòng lặp vô tận              //do nothing       }       return 0;}//trinh phuc vu ngat tran T/C1ISR (TIMER1_OVF_vect ){              TCNT1=55535; //gan gia tri khoi tao cho T/C1       PORTB ^=1;  //doi trang thai Bit PB0}

2.2 Clear Timer on Compare Match (xóa timer nếu xảy ra bằng trong so sánh)-CTC.        Một cách gọi tắt của chế độ hoạt động này là CTC, một chế độ hoạt động mới trên T/C1. Nhìn vào bảng 3 bạn sẽ thấy có 2 mode CTC (mode 4 và mode 12). Tôi lấy ví dụ mode 4 để giải thích hoạt động của CTC. Khi bạn set các bit Waveform Generation Mode tuong ứng: WGM13=0, WGM12=1, WGM11=0, WGM10=0 thì mode 4 được chọn. Trong mode này, thanh ghi OCR1A chứa giá trị TOP (giá trị so sánh do người dùng đặt), thanh ghi đếm TCNT1 tăng từ 0, khi TCNT1 bằng giá trị chứa trong OCR1A thì một “Compare Match” xảy ra. Khi đó, một ngắt có thể xảy ra nếu chúng ta cho phép ngắt Compare Match (set bit OCF1A trong thanh ghi TIMSK lên 1). Mode này cũng tương đối đơn giản, một ứng dụng cơ bản của mode này là đơn giản hóa việc đếm các sự kiện bên ngoài. Ví dụ bạn kết nối 1 sensor  đếm số người đi vào 1 căn phòng với chấn T1 (chân counter source  của T/C1), bạn muốn rằng cứ sau khi đếm 5 người thì sẽ thông báo 1 lần. List 4 là đoạn code mô tả ví dụ này:List 4. Phối hợp CTC với đếm sự kiện.12345678910

#include <avr/io.h>#include <avr/interrupt.h>#include <util/delay.h>volatile  usigned char val=0;  //khai bao 1 bien tam val va khoi tao =0int main(void){       DDRB=0xFF;                //PORTB la output PORT       PORTB=0x00;          TCCR1B=(1<<WGM12)|(1<<CS12)|(1<<CS11); //xung nhip tu chan T1, canh xuong       OCR1A=4;             //gan gia tri can so sanh        TIMSK=(1<OCIE1A);//cho phep ngat khi gia tri dem bang 4 

Page 61: asembly

11121314151617181920212223

       sei();                       //set bit I cho phep ngat toan cuc

       while (1){           //vòng lặp vô tận               //do nothing       }       return 0;}//trinh phuc vu ngat compare matchISR (TIMER1_COMPA_vect){              val++;         if (val==10) val=0;  //gioi han bien val tu 0 den 9       PORTB =val;        //xuat gia tri ra PORTB}

    Tôi chỉ giải thích những điểm mới trong List 4. Thứ nhất là “attribute” volatile dùng trước khai báo biến val, biến val được khai báo là unsigned char (8 bit, không dấu) dùng chứa giá trị tạm thời để xuất ra PORTB khi có ngắt xảy ra. Điều đặc biệt là từ khóa volatile đặt trước nó,  volatile  là một thuộc tính (attribute) của bộ biên dịch gcc-avr, nó nói với trình dịch rằng biến val sẽ được dùng trong chương trình chính và cả trong các trình phục vụ ngắt. Nếu bạn muốn cập nhập giá trị 1 biến toàn cục trong các trình phục vụ ngắt mà biến đó không được chỉ định thuộc tính volatile trước thì quá trình cập nhật thất bại. Một cách dễ hiểu hơn, bạn xem trình ISR trong ví dụ trên, cứ mỗi lần có ngắt Compare Match xảy ra, biến val được tăng thêm 1 (dòng 21) sau đó kiểm tra điều kiện bằng 10 hay không và cuối cùng là gán cho PORTB. Nếu trong khai báo của val (dòng 4) chúng ta không chỉ định volatile thì giá trị xuất ra PORTB sẽ luôn là 1 khi có ngắt. Chú ý là điều này chỉ đúng it nhất là với phiên bản WinAVR tháng 12 năm 2007, các phiên bản sau có thể không cần dùng volatile (tôi sẽ cập nhật sau).

       Dòng 8 set các bit điều khiển: TCCR1B=(1<<WGM12)|(1<<CS12)|(1<<CS11); bạn thấy tôi chỉ set bit WGM12 trong 4 bit WGM vì tôi muốn chọn mode CTC 4 (xem bảng 3). Hai bit CS12 và CS11 được set bằng 1 trong khi CS10 được giữ ở 0 để chọn  xung clock là từ bên ngoài, chân T1 (xem bảng 2). Trong dòng 10, OCR1A=4; là giá trị cần so sánh, chúng ta biết rằng TCNT1 tăng lên từ 0, vì thế để đếm 5 sự kiện thì cần đặt giá trị so sánh là 4 (0, 1, 2, 3, 4). Dòng 11 set bit cho phép ngắt khi có Compare match xảy ra (dùng cho channel A).       Mode 12 của CTC (WGM13=1, WGM12=1, WGM11=0, WGM10=0) cũng tương tự mode 4 nhưng cái khác là giá trị cần so sánh được chứa trong thanh ghi ICR1 (không phải OCR1A hay OCR1B). Khi đó nếu muốn dùng ngắt thì bạn phải dùng ngắt Input capture. Cụ thể dòng 8 trong list 4 đổi thành:

Page 62: asembly

TCCR1B=(1<<WGM13)|( (1<<WGM12)|(1<<CS12)|(1<<CS11);  dòng 10: ICR1=4 và dòng  20: ISR (TIMER1_CAPT_vect ){       Một khả năng khác của CTC là xuất tín hiệu xung vuông trên chân OC1A (chân 15 trên Atmega8) bằng cách set các bit Compare Output Mode trong thanh ghi TCCR1A. Tuy nhiên việc tạo các tín hiệu output trong mode CTC không thật sự thú vị. Vì vậy chúng ta sẽ khảo sát cách tạo tín hiệu output trong 1 chế độ chuyên nghiệp và thú vị hơn, chế độ PWM.       Trước khi bắt đầu làm việc với các chế độ PWM tôi nghĩ cần thiết giới thiệu thế nào là PWM và nhắc lại các khái niệm giá trị đếm của Timer1 (hay bất kỳ timer nào khác) trên AVR. Trước hết, PWM hay Pulse Width Modulation được hiểu theo nghĩa tiếng Việt là “xung điều rộng” là khái niệm chỉ tín hiệu xung mà thường thì chu kỳ (Time period) của nó được cố định, duty cycle (thời thời gian tín hiệu ở mức HIGH) của nó có thể được thay đổi. Bạn xem 1 ví dụ về PWM  trong hình  5.

Hình 5. Ví dụ về tín hiệu PWM.       Tạo ra PWM tức là tạo ra những tín hiệu xung mà ta có thể điều khiển duty cycle (và cả tần số ~ Time period nếu cần thiết). Timer 1 trêsn Atmega8 là 1 module lý tưởng để tạo ra các tín hiệu dạng này. Nhưng PWM dùng để làm gì và cách mà nó được sử dụng như thế nào? Tôi lấy một ví dụ như trong hình 6: một động cơ DC và một switch button. 

Page 63: asembly

Hình 6. Motor và switch.       Nếu nhấn button thì động cơ hoạt động, thả button thì động cơ dừng. Tuy nhiên  do tốc độ nhấn và thả của con người có hạn, bạn sẽ thấy động cơ hoạt động hơi “sượng” (ripple). Điều gì xảy ra nếu bạn nhấn và thả button với vận tốc 5000 lần/giây. Câu trả lời là tay bạn sẽ bị gãy và button sẽ bị hỏng (^^). 5000 lần/s là điều không tưởng, tuy nhiên nếu bạn làm được như thế thì tổng thời gian cho 1 lần nhấn+thả là 1:5000=0.0002s = 200us. Có sự khác biệt nào không giữa trường hợp thời gian nhấn = 150us, thời gian thả 50us và trường hợp thời gian nhấn là 50us còn thời gian thả là 150us. Bạn sẽ dễ dàng tìm câu trả lời, trong trường hợp 1 động cơ sẽ quay với vận tốc nhanh hơn trường hợp 2. Đó là ý tưởng cơ bản để sử dụng PWM điều khiển vận tốc động cơ (và điều khiển nhiều thứ khác nữa). để biến cái không tưởng trên (5000 lần/s) thành hiện thực, chúng ta sẽ thay thế cái button cơ khí kia bằng 1 công tắc điện tử (electronics switch). Thường thì các chip MOSFET được dùng làm các khóa điện tử. MOSFET thường có 3 chân G (gate), D (drain) và S (source). Ví dụ 1 MOSFET kênh N ở trạng thái thông thường 2 chân D và S ko có dòng điện chạy qua, nếu điện áp chân G lớn hơn chân S khoảng 3V trở lên thì dòng điện có thể chạy từ D sang S. hãy xem cách mô tả tương đương 1 MOSFET với 1 button trong hình 7.

Page 64: asembly

Hình 7. MOSFET và button.       Việc “kích” các MOSFET có thể thực hiện bằng các tín hiệu PWM. Vì thế ý tưởng điều khiền động cơ trong hình 6 có thể được thực hiện lại thông qua PWM như trong hình 8.

Hình 8. Mô hình điều khiển tốc độ động  cơ bằng PWM đơn giản.       Như vậy là xong phần giới thiệu về PWM, bây giờ chúng ta sang các khái niệm số đếm trong Timer. Hình 9 minh họa cách bố trí các số đếm trong Timer1 trên hệ trục đếm.

Page 65: asembly

Hình 9: các mốc giá trị của T/C1.       BOTTOM luôn được cố định là 0 (giá trị nhỏ nhất), MAX luôn là 0xFFFF (65535). TOP là giá trị đỉnh do người dùng định nghĩa, giá trị của TOP có thể được cố định là 0xFF (255), 0x1FF (511), 0x3FF 91023) hoặc định nghĩa bởi các thanh ghi ICR1 hoặc OCR1A. thực chất đối với ứng dụng PWM thì TOP chính là Time period của PWM. Do mục đích sử dụng mà có thể chọn TOP là các giá trị cố định hay các thanh ghi, riêng với tôi, cho mục đích tạo tín hiệu PWM tôi chọn TOP định nghĩa bởi thanh ghi ICR1. Ouput Compare là giá trị so sánh của bộ Timer. Trong chế độ PWM thì Output Compare quy định Duty cycle. Với T/C1, Output Comapre là giá trị trong các thanh ghi OCR1A và OCR1B. Do có 2 thanh ghi độc lập A và B, tương ứng chúng ta có thể tạo ra 2 tín hiệu PWM trên 2 chân OC1A và OC1B bằng T/C1. Đã đến lúc chúng ta tìm hiểu cách tạo PWM trên AVR.2.3 Fast PWM (PWM tần số cao).        Trong chế độ Fast PWM, 1 chu kỳ được tính trong 1 lần đếm từ BOTTOM lên TOP (single-slope), vì thế mà chế độ này gọi là Fast PWM (PWM nhanh). Có tất cả 5 mode trong Fast PWM tương ứng với 5 cách chọn giá trị TOP khác nhau (tham khảo bảng 3).  Việc xác lập chế độ hoạt động cho Fast PWM thực hiện thông qua 4 bit WGM và các bit chọn dạng xung ngõ ra, Compare Output Mode trong thanh ghi TCCR1A, nhìn lại 2 thanh ghi TCCR1A và TCCR1B.

       Chú ý các bit COM1A1, COM1A0 và COM1B1, COM1B0 là các bit chọn dạng tín hiệu ra của PWM (Compare Output Mode bits). COM1A1, COM1A0 dùng cho kênh A và  COM1B1, COM1B0 dùng cho kênh B. Hãy đối chiếu bảng 4.Bảng 4: mô tả các bit COM trong chế độ fast PWM.

Page 66: asembly

       Tôi sẽ giải thích hoạt động của Fast PWM kênh A thông qua 1 trường hợp cụ thể, mode 14 (WGM13=1, WGM12=1, WGM11=1, WGM10=0). Trong mode 14, giá trị TOP (cũng là chu kỳ của PWM) được chứa trong thanh ghi ICR1, khi hoạt động thanh ghi TCNT1 tăng giá trị từ 0, giả sử các bit phụ  COM1A=1, COM1A0=0, lúc này trạng thái của chân OC1A (chân 15) là HIGH (5V), khi TCNT1 tăng đến bằng giá trị của thanh ghi OCR1A thì chân OC1A được xóa về mức LOW (0V), thanh ghi đếm TCNT1 vẫn tiếp tục tăng đến khi nào nó bằng giá trị TOP chứa trong thanh ghi ICR1 thì TCNT1 tự động  reset về 0 và chân OC1A trở về trạng thái HIGH, cái này gọi là “Clear OC1A/OC1B on Compare Match, set OC1A/OC1B at TOP” mà bạn thấy trong hàng 4 bảng 4. Hình 10 mô tả cách tạo xung PWM trên chân OC1A ở mode 14.

Hình 10: Fast PMW mode 14.       Rõ ràng chúng ta có thể điều khiển cả time period và duty cycle của PWM bằng 2 thanh ghi ICR1 và OCR1A. Thông thường giá trị của ICR1 được tính toán và gán cố định, giá trị của OCR1A được thay đổi để thực hiện mục đích điều khiển (như thay đổi vận tốc động cơ). Chú ý là nếu chúng ta set các bit phụ ngược lại: COM1A=0, COM1A0=1, thì tín hiệu PWM trên chân OC1A sẽ có phần “LOW” từ 0 đến OCR1A và “HIGH” từ OCR1A đến ICR1, đây gọi là “set OC1A/OC1B on Compare Match, clear OC1A/OC1B at TOP” (ngược với tín hiệu trên hình 10). Hoạt động của fast PWM kênh B hoàn toàn tương tự, trong đó thanh ghi ICR1 cũng chứa TOP của PWM kênh B và thanh ghi ICR1B chứa duty cycle. Như vậy 2 kênh A và B có cùng tần số hay Time period và duty cycle được điều khiển độc lập. Chân xuất tín hiệu PWM của kênh B là chân OC1B (chân 16 trên Atmega8).Các mode 5, 6 và 7 của Fast PWM hoạt động hoàn toàn tương tự mode 14. Điểm khác nhau cơ bản là giá trị TOP(Time period). Trong các mode này giá trị TOP không do thanh thi ICR1 định nghĩa mà là các hằng số không đổi. Với mode 5, tức

Page 67: asembly

mode 8 bits, (WGM13=0, WGM12=1, WGM11=0, WGM10=1) giá trị TOP là 1 hằng số, TOP = 255 (số 8 bits lớn nhất). Với mode 6, tức mode 9 bits, (WGM13=0, WGM12=1, WGM11=1, WGM10=0) giá trị TOP là 1 hằng số, TOP = 511 (số 9 bits lớn nhất). Và với mode 7, tức mode 10 bits, (WGM13=0, WGM12=1, WGM11=1, WGM10=1) TOP =1023 (số 10 bits lớn nhất). Mode 15 cũng là Fast PWM trong đó TOP do OCR1A quy định, vì thế mà tín hiệu ra ở kênh A hầu như không phải là 1 xung, nó chỉ thay đổi trạng thái trong 1 clock. Theo tôi, để sử dụng Fast PWM bạn nên dùng mode 14 đã được giải thích trên. Các mode 5, 6, 7 cũng có thể dùng nhưng không nên dùng mode 15.       Chúng ta tiến hành viết 1 ví dụ minh họa dùng 2 kênh ở chế độ fast PWM điều khiển 2 động cơ RC servo (gọi tắt là Servo). Mạch điện minh họa như trong hình 11.

Hình 11: Điều khiển 2 RC servo bằng PWM.       Hai button được nối với 2 ngõ ngắt ngoài INT0 và INT1 để điều khiển góc xoay của 2 Servo. Tên của Servo trong phần mềm Proteus là “MOTOR-PWMSERVO”. Trước khi viết code điều khiển các Servo, bạn cần biết cách điều khiển chúng, tôi giới thiệu ngắn gọn như sau:       RC servo là một tổ hợp gồm 1 động cơ DC công suất nhỏ, hộp giảm tốc và bộ điều khiển góc quay. Có 2 loại chính là Servo thường và digital Servo, trong ví dụ này tôi giới thiệu Servo thường (phổ biến). Servo thường có 3 dây, dây màu đen là dây GND, dây đỏ là dây nguồn (thường là 5V) và 1 dây trắng hoặc vàng và dây điều khiển (có một số loại Servo có màu dây khác, bạn cần tham khảo datasheet của chúng). Vì các Servo đã có sẵn mạch điều khiển góc quay bên trong nên chúng ta không cần bất cứ giải thuật gì mà chỉ cần cấp tín hiệu PWM cho dây điều khiển là Servo có thể xoay đến 1 vị trí nào đó (chú ý là Servo thường chỉ xoay nữa vòng, điều khiển servo là điều khiển góc xoay chứ không phải điều khiển cận tốc xoay). Hình 12 là hình ảnh servo và cách điều khiển servo.

Page 68: asembly

Hình 12. Servo và cách điều khiển.       Bạn xem hình 12b, để điều khiển servo bạn cần cấp cho dây điều khiển một tín hiệu PWM có Time Period khoảng 20ms, duty cycle của PWM sẽ quyết định góc xoay của servo. Với Duty cycle là 1ms, servo xoay về vị trí 0o, khi duty cycle =2ms, góc xoay sẽ là 180o, từ đó bạn có thể tính được duty cycle cần thiết khi bạn muốn servo xoay đến 1 vị trí bất kỳ giữa 0o và 180o. Sau khi hiểu cách điều khiển servo, chúng ta có thể dễ dàng viết code điều khiển chúng, chỉ cần tạo các xung PWM bằng T/C1. Đoạn code cho ví dụ này được trình bày trong list 5.List 5. Điều khiển Servo bằng PWM.123456789101112131415

#include <avr/io.h>#include <avr/interrupt.h>

int main(void){       DDRB=0xFF;                //PORTB la output PORT       PORTB=0x00; 

       MCUCR|=(1<<ISC11)|(1<<ISC01); //ngat canh xuong       GICR |=((1<<INT1)|(1<<INT0); //cho phép 2 ngat hoat dong 

       TCCR1A=(1<<COM1A1)|(1<<COM1B1)|(1<<WGM11);       TCCR1B=(1<<WGM13)|(1<<WGM12)|(1<<CS10);        OCR1A=1000;              //Duty cycle servo1=1000us=1ms (0 degree)       OCR1B=1500;              //Duty cycle servo2=1500us=1.5ms (90 degree)       ICR1=20000;                  //Time period = 20000us=20ms

Page 69: asembly

1617181920212223242526272829303132

       sei();                       //set bit I cho phep ngat toan cuc        while (1){           //vòng lặp vô tận               //do nothing       }       return 0;}

//trinh phuc vu ngat ngoaiISR (INT0_vect ){               if (OCR1A==1000) OCR1A=1500;  //thay doi goc xoay servo1 den 90 do       else OCR1A = 1000;  // thay doi goc xoay servo1 den 0 do}ISR (INT1_vect ){               if (OCR1B==1000) OCR1B=1500;  //thay doi goc xoay servo1 den 90 do       else OCR1B = 1000;  // thay doi goc xoay servo1 den 0 do}

       Với ví dụ này tôi chỉ cần giải thích các dòng từ 11 đến 15 liên quan đến việc xác lập chế độ hoạt động Fast PWM mode 14 inverse, phần còn lại bạn đọc tự đối chiếu với các bài trước. Dòng 11 và 12 thực hiện set các bit điều khiển Timer1, trước hết là các bit COM. Bạn thấy tôi chỉ set 2 bit COM1A1 và COM1B1: (1<<COM1A1)|(1<<COM1B1). Hai bit COM1A0 và COM1B0 không set tức mặc định bằng 0. Đối chiếu với bảng 4 bạn thấy chúng ta sẽ dùng “Clear OC1A/OC1B on Compare Match, set OC1A/OC1B at TOP” cho tất cả 2 kênh A và B. Chúng ta set 3 bit  WGM13, WGM12 (thanh ghi TCCR1B, dòng 12) và WGM11 (thanh ghi TCCR1A, dòng 11) như thế thu được tổ hợp (WGM13=1, WGM12=1, WGM11=1, WGM10=0) tức là mode 14 được chọn (bảng 3). Còn lại chúng ta set bit CS10 để khai báo rằng nguồn xung clock cho Timer1 bằng clock cho vi điều khiển (prescaler=1) tức là 1us trong tường hợp f=1Mhz. (nếu bạn dùng các trình biên dịch khác không hỗ trợ định nghĩa tên các bit thì 2 dòng 11 và 12 tương đương: TCCR1A=0xA2; TCCR1B=0x19).       Dòng 15 chúng ta khai nhập giá trị cho ICR1 cũng là Time period cho PWM, ICR1=20000 chúng ta thu được Time period =20000 us = 20ms thỏa yêu cầu của servo. Hai dòng 13 và 14 khai báo giá trị ban đầu của các duty cycle của 2 kênh PWM, các giá trị này định vị trí góc xoay của các servo. Trong 2 trình phục vụ ngắt, các giá trị này được thay đổi khi các button được nhấn.2.3 Phase correct PWM (PWM với pha chính xác).        Phase correct PWM cung cấp một chế độ tạo xung PWM có độ phân giải cao (high resolution) nên được gọi là Phase correct PWM. Tương tự Fast PWM, cũng có 5 mode hoạt động thuộc Phase correct PWM đó là các mode 1, 2, 3, 10 và 11

Page 70: asembly

(xem bảng 3). Năm mode này tương ứng các mode 5, 6, 7, 14 và 15 của fast PWM. Về cách điều khiển, Phase correct hầu như giống fast PWM, nghĩa là nếu bạn đã biết cách sử dụng các mode của fast PWM thì bạn sẽ hoàn toàn điều khiển được Phase correct PWM. Khác nhau cơ bản của 2 chế độ này là trong cách hoạt động, nếu Fast PWM có chu kỳ hoạt động trong 1 single-slope (một sườn) thì Phase correct PWM lại dual-slope (hai sườn). Lấy ví dụ mode 10 của Phase correct PWM tương ứng với mode 14 của Fast PWM, trong mode này thanh ghi ICR1 chứa TOP và OCR1A (hoặc OCR1B đối với kênh B) chứa giá trị so sánh. Khi hoạt động, thanh ghi TCNT1 tăng từ 0, khi TCNT1 bằng với OCR1A thì chân OC1A được xóa xuống mức LOW (tôi đang nói trường hợp COM1A1=1, COM1A0=0), TCNT1 tiếp tục tăng đến TOP, khi TCNT1=TOP thì TCNT1 KHÔNG được tự động reset về 0 như trường hợp Fast PWM mà TCNT1 bắt đầu đếm ngược, tức giảm từng giá trị từ TOP về 0. Trong lúc TCNT1 giảm, đến 1 lúc nó sẽ bằng giá trị của OCR1A lần thứ 2, và lần này, chân OC1A được set lên mức HIGH, TCNT1 tiếp tục giảm đến 0 thì 1 chu kỳ hoàn tất. Rõ ràng 1 chu kỳ là quá trình đếm trong 2 “sườn” nên ta gọi Phase correct PWM là dual-slope. Cũng vì tính chất dual-slope mà tín hiệu PWM trong chế độ này có tính đối xứng, thích hợp cho các ứng dụng điều khiển động cơ. Hình 13 mô tả cách mà Phase correct PWM hoạt động tron mode 10 với ngõ ra đảo (COM1A1=1, COM1A0=0).

Hình 13. Phase correct PWM mode 10.       Việc viết code cho chế độ Phase correct PWM gần như tương tự fast PWM, bạn chỉ cần thay đổi tổ hợp các bit WGM dựa theo bảng 3 và sau đó nhâp các giá trị phù hợp cho ICR1 và ORC1A, OCR1B là được.2.3 Phase correct and frequency correct PWM.        Chế độ này có 2 mode là 8 và 9. Về hầu hết các phương diện, 2 mode này giống với 2 mode 10 và 11 của Phase correct PWM. Cái khác nhau duy nhất là thời điểm mà thanh ghi OCR1A và OCR1B được cập nhật dữ liệu nếu có sự thay đổi. Việc này, nhìn chung không ảnh hưởng đến hầu hết người dùng PWM để điều khiển. Bạn sẽ rất khó để thấy sự khác biệt nếu bạn không phải đang viết 1 ứng dụng mà sai số trong 1 micro giây là điều tệ hại. Vì thế tôi không đề cập chi tiết

Page 71: asembly

chế độ này, bạn đọc có thể tham khảo datasheet của chip để hiểu rõ hơn nếu cần thiết.       Ngoài ra trên chip atmega8 còn có bộ timer2 8 bits có PWM và asynchronous operation. Về mặt chức năng timer2 giống như phiên bản 8 bit của timer1 (độ phân giải thấp hơn nhưng có cùng chế độ và phương thức hoạt động). Điểm khác biệt và cũng là điểm đặc biệt của Timer2 là khả năng hoạt động không đồng bộ với chip, nó giống như việc bạn tách timer2 ra thành 1 chip timer riêng, vì thế cần cung cấp 1 nguồn xung clock khác cho timer này (1 thạch anh khác). Chế độ này có thể được dùng để calip (calibrate), canh chỉnh sai số  và bù cho nguồn xung clock chính trên chip. Bài 5 - Giao tiếp UART

1 2 3 4 5

 ( 139 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu.

2. Truyền thông nối tiếp không đồng bộ.

3. Truyền thông nối tiếp không đồng bộ với AVR (UART).

1. Thanh ghi.

2. Sử dụng UART.

Download ví dụ

Cấu trúc AVR

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Giới thiệu.

       Bài này giúp các bạn biết cách sử dụng cách truyền thông nối tiếp UART trên AVR. Công cụ chính cũng là 2 bộ phần mềm quen thuộc WinAVR và Proteus nhưng trong bài này (và các bài sau nữa) chúng ta sẽ sử dụng chip Atmega32 làm chip minh họa. Về cơ bản việc thay đổi chip minh họa không ảnh hưởng lớn đến tính mạch lạc của loạt bài vì sự khác biệt của hai chip Atmega8 và Atmega32 là

Page 72: asembly

không đáng kể. Tuy nhiên, nếu có sự khác biệt lớn ở phần nào đó tôi sẽ kể ra cho bạn tiện so sánh.

       Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:

Nguyên lý truyền thông nối tiếp đồng bộ và không đồng bộ. Module truyền thông nối tiếp USART trên AVR. Truyền thông đa xử lí bằng UART.

II. Truyền thông nối tiếp không đồng bộ.

       Thuật ngữ USART trong tiếng anh là viết tắt của cụm từ: Universal Synchronous & Asynchronous serial Reveiver and Transmitter, nghĩa là bộ truyền nhận nối tiếp đồng bộ và không đồng bộ. Cần chú ý rằng khái niệm USART (hay UART nếu chỉ nói đến bộ truyền nhận không đồng bộ) thường để chỉ thiết bị phần cứng (device, hardware), không phải chỉ một chuẩn giao tiếp. USART hay UART cần phải kết hợp với một thiết bị chuyển đổi mức điện áp để tạo ra một chuẩn giao tiếp nào đó. Ví dụ, chuẩn RS232 (hay COM) trên các máy tính cá nhân là sự kết hợp của chip UART và chip chuyển đổi mức điện áp. Tín hiệu từ chip UART thường theo mức TTL: mức logic high là 5, mức low là 0V. Trong khi đó, tín hiệu theo chuẩn RS232 trên máy tính cá nhân thường là -12V cho mức logic high và +12 cho mức low (tham khảo hình 1). Chú ý là các giải thích trong tài liệu này theo mức logic TTL của USART, không theo RS232.

Hình 1. Tín hiệu tương đương của UART và RS232.

Page 73: asembly

       Truyền thông nối tiếp: giả sử bạn đang xây dựng một ứng dụng phức tạp cần sử dụng nhiều vi điều khiển (hoặc vi điều khiển và máy tính) kết nối với nhau. Trong quá trình làm việc các vi điều khiển cần trao đổi dữ liệu cho nhau, ví dụ tình huống Master truyền lệnh cho Slaver hoặc Slaver gởi tín hiệu thu thập được về Master xử lí…Giả sử dữ liệu cần trao đổi là các mã có chiều dài 8 bits, bạn có thể sẽ nghĩ đến cách kết nối đơn giản nhất là kết nối 1 PORT (8 bit) của mỗi  vi điều khiển với nhau, mỗi line trên PORT sẽ chịu trách nhiệm truyền/nhận 1 bit dữ liệu. Đây gọi là cách giao tiếp song song, cách này là cách đơn giản nhất vì dữ liệu được xuất và nhận trực tiếp không thông qua bất kỳ một giải thuật biến đổi nào và vì thế tốc độ truyền cũng rất nhanh. Tuy nhiên, như bạn thấy, nhược điểm của cách truyền này là số đường truyền quá nhiều, bạn hãy tưởng tượng nếu dữ liệu của bạn có giá trị càng lớn thì số đường truyền cũng sẽ nhiều thêm. Hệ thống truyền thông song song thường rất cồng kềnh và vì thế kém hiệu quả. Truyền thông nối tiếp sẽ giải quyết vần đề này, trong tuyền thông nối tiếp dữ liệu được truyền từng bit trên 1 (hoặc một ít) đường truyền. Vì lý do này, cho dù dữ liệu của bạn có lớn đến đâu bạn cũng chỉ dùng rất ít đường truyền. Hình 2 mô tả sự so sánh giữa 2 cách truyền song song và nối tiếp trong việc truyền con số 187 thập phân (tức 10111011 nhị phân).

Hình 2. Truyền 8 bit theo phương pháp song song và nối tiếp.

       Một hạn chế rất dễ nhận thấy khi truyền nối tiếp so với song song là tốc độ truyền và độ chính xác của dữ liệu khi truyền và nhận. Vì dữ liệu cần được “chia nhỏ” thành từng bit khi truyền/nhận, tốc độ truyền sẽ bị giảm. Mặt khác, để đảm bảo tính chính xác của dữ liệu, bộ truyền và bộ nhận cần có những “thỏa hiệp” hay những tiêu chuẩn nhất định. Phần tiếp theo trong chương này giới thiệu các tiêu chuẩn trong truyền thông nối tiếp không đồng bộ.       Khái niệm “đồng bộ” để chỉ sự “báo trước” trong quá trình truyền. Lấy ví dụ thiết bị 1 (tb1) kết với với thiết bị 2 (tb2) bởi 2 đường, một đường dữ liệu và 1 đường xung nhịp. Cứ mỗi lần tb1 muốn send 1 bit dữ liệu, tb1 điều khiển đường xung nhịp chuyển từ mức thấp lên mức cao báo cho tb2 sẵn sàng nhận một bit. Bằng cách “báo trước” này tất cả các bit dữ liệu có thể truyền/nhận dễ dàng với ít

Page 74: asembly

“rủi ro” trong quá trình truyền. Tuy nhiên, cách truyền này đòi hỏi ít nhất 2 đường truyền cho 1 quá trình (send or receive). Giao tiếp giữa máy tính và các bàn phím (trừ bàn phím kết nối theo chuẩn USB) là một ví dụ của cách truyền thông nối tiếp đồng bộ.       Khác với cách truyền đồng bộ, truyền thông “không đồng bộ” chỉ cần một đường truyền cho một quá trình. “Khung dữ liệu” đã được chuẩn hóa bởi các thiết bị nên không cần đường xung nhịp báo trước dữ liệu đến. Ví dụ 2 thiết bị đang giao tiếp với nhau theo phương pháp này, chúng đã được thỏa thuận với nhau rằng cứ 1ms thì sẽ có 1 bit dữ liệu truyền đến, như thế thiết bị nhận chỉ cần kiểm tra và đọc đường truyền mỗi mili-giây để đọc các bit dữ liệu và sau đó kết hợp chúng lại thành dữ liệu có ý nghĩa. Truyền thông nối tiếp không đồng bộ vì thế hiệu quả hơn truyền thông đồng bộ (không cần nhiều lines truyền). Tuy nhiên, để quá trình truyền thành công thì việc tuân thủ các tiêu chuẩn truyền là hết sức quan trọng. Chúng ta sẽ bắt đầu tìm hiểu các khái niệm quan trọng trong phương pháp truyền thông này.       Baud rate (tốc độ Baud): như trong ví dụ trên về việc truyền 1 bit trong 1ms, bạn thấy rằng để việc truyền và nhận không đồng bộ xảy ra thành công thì các thiết bị tham gia phải “thống nhất” nhau về khoảng thời dành cho 1 bit truyền, hay nói cách khác tốc độ truyền phải được cài đặt như nhau trước, tốc độ này gọi là tốc độ Baud. Theo định nghĩa, tốc độ baud là số bit truyền trong 1 giây. Ví dụ nếu tốc độ baud được đặt là 19200 thì thời gian dành cho 1 bit truyền là 1/19200 ~ 52.083us.          Frame (khung truyền): do truyền thông nối tiếp mà nhất là nối tiếp không đồng bộ rất dễ mất hoặc sai lệch dữ liệu, quá trình truyền thông theo kiểu này phải tuân theo một số quy cách nhất định. Bên cạnh tốc độ baud, khung truyền là một yếu tốc quan trọng tạo nên sự thành công khi truyền và nhận. Khung truyền bao gồm các quy định về số bit trong mỗi lần truyền, các bit “báo” như bit Start và bit Stop, các bit kiểm tra như Parity, ngoài ra số lượng các bit trong một data  cũng được quy định bởi khung truyền. Hình 1 là một ví dụ của một khung truyền theo UART, khung truyền này được bắt đầu bằng một start bit,  tiếp theo là 8 bit data, sau đó là 1 bit parity dùng kiểm tra dữ liệu và cuối cùng là 2 bits stop.        Start bit: start là bit đầu tiên được truyền trong một frame truyền, bit này có chức năng báo cho thiết bị nhận biết rằng có một gói dữ liệu sắp được truyền tới. Ở module USART trong AVR, đường truyền luôn ở trạng thái cao khi nghỉ (Idle), nếu một chip AVR muốn thực hiện việc truyền dữ liệu nó sẽ gởi một bit start bằng cách “kéo” đường truyền xuống mức 0. Như vậy, với AVR bit start là mang giá trị 0 và có giá trị điện áp 0V (với chuẩn RS232 giá trị điện áp của bit start là ngược lại). start là bit bắt buộc phải có trong khung truyền.       Data: data hay dữ liệu cần truyền là thông tin chính mà chúng ta cần gởi và nhận. Data không nhất thiết phải là gói 8 bit, với AVR bạn có thể quy định số

Page 75: asembly

lượng bit của data là 5, 6, 7, 8 hoặc 9 (tương tự cho hầu hết các thiết bị hỗ trợ UART khác). Trong truyền thông nối tiếp UART, bit có ảnh hưởng nhỏ nhất (LSB – Least Significant Bit, bit bên phải) của data sẽ được truyền trước và cuối cùng là bit có ảnh hưởng lớn nhất (MSB – Most Significant Bit, bit bên trái).        Parity bit: parity là bit dùng kiểm tra dữ liệu truyền đúng không (một cách tương đối). Có 2 loại parity là parity chẵn (even parity) và parity lẻ (odd parity). Parity chẵn  nghĩa là số lượng số 1 trong dữ liệu bao gồm bit parity luôn là số chẵn. Ngược lại tổng số lượng các số 1 trong parity lẻ luôn là số lẻ. Ví dụ, nếu dữ liệu của bạn là 10111011 nhị phân, có tất cả 6 số 1 trong dữ liệu này, nếu parity chẵn được dùng, bit parity sẽ mang giá trị 0 để đảm bảo tổng các số 1 là số chẵn (6 số 1). Nếu parity lẻ được yêu cầu thì giá trị của parity bit là 1. Hình 1 mô tả ví dụ này với parity chẵn được sử dụng. Parity bit không phải là bit bắt buộc và vì thế chúng ta có thể loại bit này khỏi khung truyền (các ví dụ trong bài này tôi không dùng bit parity).       Stop bits: stop bits là một hoặc các bit báo cho thiết bị nhận rằng một gói dữ liệu đã được gởi xong. Sau khi nhận được stop bits, thiết bị nhận sẽ tiến hành kiểm tra khung truyền để đảm bảo tính chính xác của dữ liệu. Stop bits là các bits bắt buộc xuất hiện trong khung truyền, trong AVR USART có thể là 1 hoặc 2 bits (Trong các thiết bị khác Stop bits có thể là 2.5 bits).  Trong ví dụ ở hình 1, có 2 stop bits được dùng cho khung truyền.Giá trị của stop bit luôn là giá trị nghỉ (Idle) và là ngược với giá trị của start bit, giá trị stop bit trong AVR luôn là mức cao (5V).       (Chú ý và gợi ý: khung truyền phổ biến nhất là : start bit+ 8 bit data+1 stop bit)       Sau khi nắm bắt các khái niệm về truyền thông nối tiếp, phần tiếp theo chúng ta sẽ khảo sát cách thực hiện phương pháp truyền thông này trên chip AVR (cụ thể là chip Atmega32).

III. Truyền thông nối tiếp không đồng bộ với AVR (UART).

       Vi điều khiển Atmega32 có 1 module truyền thông nối tiếp USART. Có 3 chân chính liên quan đến module này đó là chân xung nhịp - XCK (chân số 1), chân truyền dữ liệu – TxD (Transmitted Data) và chân nhận dữ liệu – RxD (Reveived Data). Trong đó chân XCK chỉ được sử dụng như là chân phát hoặc nhận xung giữ nhịp trong chế độ truyền động bộ. Tuy nhiên bài  này chúng ta không khảo sát chế độ truyền thông đồng bộ, vì thế bạn chỉ cần quan tâm đến 2 chân TxD và RxD. Vì các chân truyền/nhận dữ liệu chỉ đảm nhiệm 1 chức năng độc lập (hoặc là truyền, hoặc là nhận), để kết nối các chip AVR với nhau (hoặc kết nối AVR với thiết bị hỗ trợ UART khác) bạn phải đấu “chéo” 2 chân này. TxD của thiết bị thứ nhất kết nối với RxD của thiết bị 2 và ngược lại. Module USART trên

Page 76: asembly

chip Atmega32 hoạt động “song công” (Full Duplex Operation), nghĩa là quá trình truyền và nhận dữ liệu có thể xảy ra đồng thời.

1. Thanh ghi:

       Cũng như các thiết bị khác trên AVR, tất cả hoạt động và tráng thái của module USART được điều khiển và quan sát thông qua các thanh ghi trong vùng nhớ I/O. Có 5 thanh ghi được thiết kế riêng cho hoạt động và điều khiển của USART, đó là:

UDR: hay thanh ghi dữ liệu, là 1 thanh ghi 8 bit chứa giá trị nhận được và phát đi của USART. Thực chất thanh ghi này có thể coi như 2 thanh ghi TXB (Transmit data Buffer) và RXB (Reveive data Buffer) có chung địa chỉ. Đọc UDR thu được giá trị thanh ghi đệm dữ liệu nhận, viết giá trị vào UDR tương đương đặt giá trị vào thanh ghi đệm phát, chuẩn bị để gởi đi. Chú ý trong các khung truyền sử dụng 5, 6 hoặc 7 bit dữ liệu, các bit cao của thanh ghi UDR sẽ không được sử dụng

UCSRA (USART Control and Status Register A): là 1 trong 3 thanh ghi điều khiển hoạt động của module USART.

       Thanh ghi UCSRA chủ yếu chứa các bit trạng thái như bit báo quá trình nhận kết thúc (RXC), truyền kết thúc (TXC), báo thanh ghi dữ liệu trống (UDRE), khung truyền có lỗi (FE), dữ liệu tràn (DOR), kiểm tra parity có lỗi (PE)…Bạn chú ý một số bit quan trọng của thanh ghi này:* UDRE (USART Data Register Empty) khi bit bày bằng 1 nghĩa là thanh ghi dữ liệu UDR đang trống và sẵn sàng cho một nhiệm vụ truyền hay nhận tiếp theo. Vì thế nếu bạn muốn truyền dữ liệu đầu tiên bạn phải kiểm tra xem bit UDRE có bằng 1 hay không, sau khi chắc chắn rằng UDRE=1 hãy viết dữ liệu vào thanh ghi UDR để truyền đi.* U2X là bit chỉ định gấp đôi tốc độ truyền, khi bit này được set lên 1, tốc độ truyền so cao gấp 2 lần so với khi bit này mang giá trị 0.* MPCM là bit chọn chế độ hoạt động đa xử lí (multi-processor).

Page 77: asembly

UCSRB (USART Control and Status Register B): đây là thanh ghi quan trọng điều khiển USART. Vì thế chúng ta sẽ khảo sát chi tiết từng bit của thanh ghi này.

* RXCIE (Receive Complete Interrupt Enable) là bit cho phép ngắt khi quá trình nhận kết thúc. Việc nhận dữ liệu truyền bằng phương pháp nối tiếp không đồng bộ thường được thực hiện thông qua ngắt, vì thế bit này thường được set bằng 1 khi USART được dung nhận dữ liệu.* TXCIE (Transmit Complete Interrupt Enable) bit cho phép ngắt khi quá trình truyền kết thúc.* UDRIE (USART Data Register Empty Interrupt Enable) là bit cho phép ngắt khi thanh ghi dữ liệu UDR trống.* RXEN (Receiver Enable) là một bit quan trọng điều khiển bộ nhận của USART, đề kích hoạt chức năng nhận dữ liệu bạn phải set bit này lên 1.* TXEN (Transmitter Enable) là bit điều khiển bộ phát. Set bit này lên 1 bạn sẽ khởi động bộ phát của USART.* UCSZ2 (Chracter size) bit này kết hợp với 2 bit khác trong thanh ghi UCSRC quy định độ dài của dữ liệu truyền/nhận. Chúng ta sẽ khảo sát chi tiết khi tìm hiểu thanh ghi UCSRC.* RXB8 (Receive Data Bit 8) gọi là bit dữ liệu 8. Bạn nhớ lại rằng USART trong AVR có hỗ trợ truyền dữ liệu có độ dài tối đa 9 bit, trong khi thanh ghi dữ liệu là thanh ghi 8 bit. Do đó, khi có gói dữ liệu 9 bit được nhận, 8 bit đầu sẽ chứa trong thanh ghi UDR, cần có 1 bit khác đóng vai trò bit thứ chín, RXD8 là bit thứ chín này. Bạn chú ý là các bit được đánh số từ 0, vì thế bit thứ chín sẽ có chỉ số là 8, vì lẽ đó mà bit này có tên là RXD8 (không phải RXD9).* TXB8 (Transmit Data Bit 8), tương tự như bit RXD8, bit TXB8 cũng đóng vai trò bit thứ 9 truyền thông, nhưng bit này được dung trong lúc truyền dữ liệu.    

UCSRC (USART Control and Status Register C): thanh ghi này chủ yếu quy định khung truyền và chế độ truyền. Tuy nhiên, có một rắc rối nho nhỏ là thanh ghi này lại có cùng địa chỉ với thanh ghi UBRRH (thanh ghi chứa byte cao dùng để xác lập tốc độ baud), nói một cách khác 2 thanh ghi này là 1. Vì thế bit 7 trong thanh ghi này, tức bit URSEL là bit chọn thanh ghi. Khi URSEL=1, thanh ghi này được chip AVR hiểu là thanh ghi điều khiển UCSRC, nhưng nếu bit URSEL=0 thì thanh ghi UBRRH sẽ được sử dụng.

Page 78: asembly

       Các bit còn lại trong thanh ghi UCSRC được mô tả như sau:* UMSEL (USART Mode Select) là bit lựa chọn giữa 2 chế độ truyền thông đồng bộ và không đồng bộ. Nếu  UMSEL=0, chế độ không đồng bộ được chọn, ngược lại nếu UMSEL=1, chế độ đồng bộ được kích hoạt.* Hai bit UPM1 và UPM0( Parity Mode) được dùng để quy định kiểm tra pariry. Nếu UPM1:0=00, parity không được sử dụng (mode này khá thông dụng), UPM1:0=01 không được sử dụng, UPM1:0=10 thì parity chẵn được dùng, UPM1:0=11 parity lẻ được sử dụng (xem thêm bảng 1).Bảng 1: chọn kiểm tra parity.

* USBS (Stop bit Select), bit Stop trong khung truyền bằng AVR USART có thể là 1 hoặc 2 bit, nếu USBS=0 thì Stop bit chỉ là 1 bit trong khi USBS=1 sẽ có 2 Stop bit được dùng.* Hai bit UCSZ1 và UCSZ2 (Character Size) kết hợp với bit UCSZ2 trong thanh ghi UCSRB tạo thành 3 bit quy định độ dài dữ liệu truyền. Bảng 2 tóm tắt các giá trị có thể có của tổ hợp 3 bit này và độ dài dữ liệu truyền tương ứng.Bảng 2: độ dài dữ liệu truyền.

* UCPOL (Clock Pority) là bit chỉ cực của xung kích trong chế độ truyền thông đồng bộ. nếu UCPOL=0, dữ liệu sẽ thay đổi thay đổi ở cạnh lên của xung nhịp, nếu UCPOL=1, dữ liệu thay đổi ở cạnh xuống xung nhịp. Nếu bạn sử dụng chế độ truyền thông không đồng bộ, hãy set bit này bằng 0..

Page 79: asembly

UBRRL và UBRRH (USART Baud Rate Register): 2 thanh ghi thấp và cao quy định tốc độ baud.

       Nhắc lại là thanh ghi UBRRH dùng chung địa chỉ thanh ghi UCSRC, bạn phải set bit này bằng 0 nếu muốn sử dụng thanh ghi UBRRH. Như bạn quan sát trong hình trên, chỉ có 4 bit thấp của UBRRH được dùng, 4 bit này kết hợp với 8 bit trong thanh ghi UBRRL tạo thành thanh ghi 12 bit quy định tốc độ baud. Chú ý là nếu bạn viết giá trị vào thanh ghi UBRRL, tốc độ baud sẽ tức thì được cập nhật, vì thế bạn phải viết giá trị vào thanh ghi UBRRH trước khi viết vào thanh ghi UBRRL.       Giá trị gán cho thanh ghi UBRR không phải là tốc độ baud, nó chỉ được USART dùng để tính tốc độ baud. Bảng 3 hướng dẫn cách tính tốc độ baud dựa vào giá trị của thanh ghi UBRR và ngược lại, cách tính giá trị cần thiết gán cho thanh ghi UBRR khi đã biết tốc độ baud.Bảng 3: tính tốc độ baud.

       Trong các công thức trong bảng 3, fOSC là tốc tần số xung nhịp của hệ thống (thạch anh hay nguồn xung nội…). Để tiện cho bạn theo dõi, tôi đính kèm bảng ví dụ cách đặt giá trị cho UBRR theo tốc độ baud mẫu.Bảng 4: một số tốc độ baud mẫu.

Page 80: asembly
Page 81: asembly

2. Sử dụng UART:.

       Thông thường, để sử dụng module USART trên AVR bạn phải thực hiện 3 việc quan trọng, đó là: cài đặt tốc độ baud (thanh ghi UBRR), định dạng khung truyền (UCSRB, UCSRC) và cuối cùng kích hoạt bộ truyền, bộ nhận, ngắt…Như đã đề cập, trong tài liệu này tôi chủ yếu đề cập đến phương pháp truyền thông không đồng bộ, việc xác lập các thông số hoạt động chủ yếu dựa trên chế độ này.

Page 82: asembly

Trong hầu hết các ứng dụng, tốc độ baud và khung truyền thường không đổi, trong trường hợp này chúng ta có thể khởi tạo trực tiếp USART ở phần đầu trong main và sau đó chỉ cần truyền hoặc nhận dữ liệu mà không cần thay đổi các cài đặt. Tuy nhiên, nếu trường hợp giao tiếp “linh hoạt” ví dụ bạn đang chế tạo một thiết bị có khả năng giao tiếp với một thiết bị đầu cuối khác (như máy tính chẳng hạn), lúc này bạn nên cho phép người dùng thay đổi tốc độ baud hoặc các thông số khác để phù hợp với thiết bị đầu cuối. Đối với những ứng dụng kiểu này bạn nên viết 1 chương trình con để khởi động USART và có thể gọi lại nhiều lần khi cần thay đổi.  Phần tiếp theo chúng ta sẽ viết một số chương trình ví dụ minh họa cách sử dụng module truyền thông USART từ đơn giản đến phức tạp. Các ví dụ sẽ được thực hiện cho chip Atmega32 với giả sử nguồn xung nhịp hệ thống là 8MHz.

2.1 Truyền dữ liệu.

       Trước hết chúng ta sẽ thực hiện một ví dụ rất đơn giản để hiểu cách khởi động USART và truyền các gói dữ liệu 8 bit. Mạch điện mô phỏng trong hình 3. Giả sử chúng ta muốn định dạng cho khung truyền gồm 1 bit start, 8 bit dữ liệu, không kiểm tra parity và 1 bit stop. Tốc độ baud  57600 (57.6k). Dữ liệu cần truyền là các giá trị liên tục của bảng mã ASCII. Đoạn code trong list 1 trình bày cách thực hiện ví dụ này.

List 1. Khởi động và truyền dữ liệu không đồng bộ bằng USART

123456789101112131415161718

#include <avr/io.h>#include <avr/delay.h>

//chuong trinh con phat du lieuvoid uart_char_tx(unsigned char chr){    while (bit_is_clear(UCSRA,UDRE)) {}; //cho den khi bit UDRE=1                UDR=chr;}

int main(void){    //set baud, 57.6k ung voi f=8Mhz, xem bang 70 trang 165, Atmega32 datasheet    UBRRH=0;        UBRRL=8;        //set khung truyen va kich hoat bo nhan du lieu    UCSRA=0x00;    UCSRC=(1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);    UCSRB=(1<<TXEN);    

Page 83: asembly

1920212223242526

        while(1){        for (char i=32; i<128; i++){            uart_char_tx(i);    //phat du lieu            _delay_ms(100);        }    }    }

       Trước hết tôi sẽ giải thích cách khởi động USART trong các dòng code từ 12 đến 18. Nếu bạn xem lại bảng 3 trong trang 9 của tài liệu này (hoặc bảng 70, trang 165 datasheet của chip atmega32), ứng với tần số xung nhịp 8Hhz, không sử dụng chế độ nhân đôi tốc độ (U2X=0), để đạt được tốc bộ baud 57600 thì giá trị cần gán cho thanh ghi UBRR là 8 (xem cột 2, bảng 3). Hai dòng 12 và 13 trong list 1 thực hiện gán 8 cho thanh ghi UBRR thông qua 2 thanh ghi UBRRH và UBRRL. Trong dòng 16, thanh ghi UCSRA được gán bằng 0. Nếu bạn xem lại phần giải thích bạn sẽ thấy thanh ghi UCSRA chủ yếu chứa các bit trạng thái, riêng 2 bit U2X và MPCM là 2 bit điều khiển, 2 bit này bằng 0 nghĩa là chúng ta không sử dụng chế độ nhân đôi tốc độ và không sử dụng truyền thông đa xử lí. Phần quan trọng nhất chính là đặt giá trị cho 2 thanh ghi USCRB và UCSRC. Với thanh ghi UCSRC (dòng 17) trước hết chúng ta phải set bit URSEL để báo rằng chúng ta không muốn truy cập thanh ghi UBRRH mà là thanh ghi UCSRC (2 thanh ghi này có cùng địa chỉ), tiếp theo chúng ta chỉ set 1 cho 2 bit UCSZ1 và UCSZ0, bạn xem lại bảng 2 để thấy rằng nếu UCSZ1=1, UCSZ0=1 cùng với việc bit UCSZ2=0(nằm trong thanh ghi UCSRB) thì độ dài dữ liệu truyền được chọn là 8 bit. Các bit trong thanh ghi UCSRC không được set sẽ mặc định mang giá trị 0, bao gồm UMSEL = 0 (chế độ truyền thông không đồng bộ), UPM1:0=00 ( không sử dụng kiểm tra parity, xem bảng 1), USBS=0 (1 bit stop) và  UCPOL=0 (bit này không sử dụng khi truyền không đồng bộ). Sau cùng, trong dòng 18, chúng ta chỉ set bit TXEN =1 nghĩa là chỉ kích hoạt bộ phát dữ liệu, các thành phần khác như bộ nhận, các ngắt…không được sử dụng trong ví dụ này.

       Trong các bài trước tôi đã giới thiệu bạn về trình phục vụ ngắt và trong phần này tôi sẽ trình bày cách viết một chương trình con bằng ngôn ngữ C trong WinAVR, đó là đoạn chương trình uart_char_tx ở dòng 5. Chương trình con là 1 đoạn code bao gồm các câu lệnh cùng thực hiện một nhiệm vụ chung cụ thể nào đó. Trong trường hợp này là nhiệm vụ truyền 1 tham số 8 bit ra đường TxD của USART thông qua thanh ghi UDR. Như trình bày trong phần mô tả bit UDRE của thanh ghi UCSRA, quá trình truyền chỉ được bắt đầu khi bit UDRE bằng 1, vì thế

Page 84: asembly

dòng code 6 làm nhiệm vụ kiểm tra bit UDRE, câu lệnh while (bit_is_clear(UCSRA,UDRE)) {}; được hiểu là quá trình lặp sẽ “lẩn quẩn”  nếu bit UDRE bằng 0 (bit_is_clear). Khi bit UDRE bằng 1 thì dòng code 7 sẽ xuất biến chr ra thanh ghi UDR cũng là xuất ra chân TxD của module USART. Trong ngôn ngữ C có 2 cách cơ bản để viết chương trình con. Với cách 1 chương trình con được khai báo và viết trực tiếp phía trước chương trình chính main như cách mà tôi thực hiện trong ví dụ 1 này. Cách viết này đễ hiểu và thích hợp cho các đoạn chương trình con ngắn nhưng chúng có thể làm tổng quan chương trình của bạn trở nên rắc rối khi có quá nhiểu chương trình con viết trước main. Bạn có thể khắc phục nhược điểm này bằng cách đặt các chương trình con phía sau main như cách mà chúng ta đã làm với các trình phục vụ ngắt. Nếu theo đúng  quy cách của ngôn ngữ C, khi đặt chương trình con sau main bạn phải khai báo tên chương trình phía trước main, nếu bạn đặt chương trình con uart_char_tx phía sau main thì phần trước main bạn sẽ đặt dòng khai báo trước: void uart_char_tx(unsigned char chr);. Tuy WinAVR cho phép bạn bỏ qua khai báo trước này nhưng tôi khuyên bạn nên viết đúng cách để tạo thói quen và cũng như để dễ chuyển chương trình sang các trình biên dịch C khác sau này nếu cần thiết. Phần cuối cùng trong đoạn code là gọi lại chương trình uart_char_tx để truyền các dữ liệu là các số từ 32 đến 127.

       Để thực hiện mô phỏng bằng proteus bạn hãy vẽ một mạch điện đơn giản như trong hình 3. Chip Atmega32 có thể được tìm với từ khóa mega32. Trong mạch điện mô phỏng có một thiết bị đầu cuối ảo (Virtual Terminal) là một thiết bị kết nối và hiển thị  kết quả truyền thông không đồng bộ, chúng ta dùng để kiểm tra dữ liệu được truyền bằng chip AVR. Bạn có thể tìm thiết bị này trong trong danh sách các dụ cụ ảo (virtual instruments), nhấn vào nút công cụ   và sau đó chọn terminal trong danh sách để chọn thiết bị đầu cuối ảo. Kết nối thiết bị ảo với chip Atmega32 như trong hình 3, chú ý là phải “đấu chéo” 2 chân TxD và RxD. Bên cạnh việc gán chương trình cho chip AVR, bạn phải set thông số cho thiết bị ảo trước khi thực hiện mô phỏng. Hãy mở hộp thoại “edit component” của thiết bị ảo (bằng cách right click rồi left click trên thiết bị ảo). Theo mặc định thiết bị đầu cuối được định dạng khung truyền là 1 bit start+8 bit dữ liệu+1 bit stop tương tự như cách chúng ta cài đặt cho AVR trong vì dụ 1, vì thế bạn chỉ cần thay đổi tốc độ baud thành 57600 trong hộp thoại “edit component” là hoàn tất (xem hình 4). Khi chạy mô phỏng, thiết bị đầu cuối ảo sẽ hiển thị các ký tự ASCII của các số từ 32 đến 127.

Page 85: asembly

Hình 3. Mô phỏng ví dụ 1.

Hình 4. Cài đặt thông số cho thiết bị ảo.

2.2 Nhận dữ liệu.

Page 86: asembly

       Quá trình nhận dữ liệu chỉ xảy ra khi bit RXEN trong thanh ghi UCSRB được set bằng 1 và tất nhiên chân nhận dữ liệu RxD phải được nối với một nguồn phát (chân TxD của một chip UART khác chẳng hạn). Các thông số truyền thông như tốc độ baud và khung truyền trong bộ nhận phải được cài đặt như của bộ phát. Nếu không có lỗi trong quá trình truyền và nhận dữ liệu, sau khi nhận dữ liệu sẽ được chứa trong thanh ghi UDR và bit RXC (Reveice Complete) trong thanh ghi UCSRA sẽ tự động được set lên 1. Sau khi thanh ghi UDR được đọc, bit RXC lại tự động reset về 0 để chuẩn bị cho quá trình nhận dữ liệu kế tiếp. Như thế về cơ bản chúng ta có 2 cách đọc dữ liệu nhận về. Cách thứ nhất là cách  hỏi vòng (polling), kiểm tra nếu bit RXC = 1 thì đọc giá trị thanh ghi UDR (và đọc cả bit RXB8 trong thanh ghi UCSRB nếu frame truyền 9 bit được dùng). Cách thứ hai là sử dụng ngắt “nhận hoàn tất” (Receive Complete Interrupt), bằng cách set bit cho pháp ngắt nhận hoàn tất, tức bit RXCIE trong thanh ghi UCSRB, và bit cho phép ngắt toàn cục (bit I, xem lại bài 3) thì một ngắt sẽ xảy ra khi dữ liệu đã được nhận và chứa trong thanh ghi UDR, chúng ta chỉ cần đọc giá trị của thanh ghi UDR trong trình phục vụ ngắt là xong. Theo kinh nghiệm, sử dụng ngắt là phương pháp tốt nhất cho đa số các trường hợp nhận dữ liệu UART, vì chúng ta không cần quan tâm thời điểm mà dữ liệu gởi đến, tránh lãng phí thời gian dành cho việc “hỏi vòng”.  Vì thế trong phần tiếp theo tôi sẽ trình bày một ví dụ minh họa quá trình nhận dữ liệu bằng phương pháp ngắt. Để phục vụ cho ví dụ này, chúng ta sẽ khảo sát một mạch mô phỏng gồm 2 chip Atmega32 nối với nhau qua các đường TxD và RxD. Chip thứ là chip phát dữ liệu, nhiệm vụ của chip này là phát chuỗi dữ liệu từ 32 đến 127 như chip Atmega32 trong ví dụ 1. Chân phát TxD của chip 1 sẽ được nối với chân nhận RxD của chip thứ 2 (chip thứ 2 được gọi là chip nhận dữ liệu). Chip thứ 2 sau khi nhận dữ liệu sẽ phát dữ liệu này ra chân TxD của chính nó để có thể hiển thị lên thiết bị đầu cuối ảo cho chúng qua quan sát và so sánh kết quả. Bạn xem mạch điện mô phỏng trong hình 5 để hiểu rõ hơn. Chúng ta sử dụng đoạn code trong ví dụ 1 cho chip thứ nhất vì thế chỉ cần viết đoạn code nhận và phát lại dữ liệu cho chip thứ hai. List 2 trình bày đoạn code cho chip thứ hai..

List 2. Nhận dữ liệu USART không đồng bộ bằng phương pháp ngắt.

123456789

#include <avr/io.h>#include <avr/interrupt.h>#include <util/delay.h>//chuong trinh con phat du lieuvoid uart_char_tx(unsigned char chr){      while (bit_is_clear(UCSRA,UDRE)) {}; //cho den khi bit UDRE=1       UDR=chr;}volatile unsigned char u_Data;

Page 87: asembly

10111213141516171819202122232425262728

int main(void){    //set baud, 57.6k ung voi f=8Mhz, xem bang 70 trang 165, Atmega32 datasheet      UBRRH=0;          UBRRL=8;//set khung truyen va kich hoat bo nhan du lieu      UCSRA=0x00;      UCSRC=(1<<URSEL)|(1<<UCSZ1)|(1<<UCSZ0);      UCSRB=(1<<RXEN)|(1<<TXEN)|(1<<RXCIE);//cho phep ca 2 qua trinh nhan va//truyen, va chophep ngat sau khi nhan xong       sei(); //cho phep ngat toan cuc

      while(1){      }    }ISR(SIG_UART_RECV){ //trinh phuc vu ngat USART hoan tat nhan       u_Data=UDR;      uart_char_tx(u_Data);}

       Đoạn code trong ví dụ nhận và phát dữ liệu không khác đoạn code trong ví dụ 1 là mấy. Ở dòng thứ 3 tôi include file header interrupt.h vì chúng ta sẽ sử dụng ngắt để nhận dữ liệu. Chúng ta khai báo một biến u_Data  dạng 8 bit không dấu để lưu dữ liệu nhận được, do biến này sẽ được truy cập trong trình phục vụ ngắt nên chúng ta đặt attribute volatile (dòng 9). Điểm quan trọng khi khởi động UART trong ví dụ này là dòng code 18, nếu trong ví dụ 1 chúng ta chỉ khởi động duy nhất bộ phát bằng cách set bit TXEN trong thanh ghi UCSRB (UCSRB=(1<<TXEN);) thì trong ví dụ này chúng ta set thêm 2 bit cho phép nhận RXEN và cho phép ngắt RXCIE trong thanh ghi UCSRB. Bit RXEN khởi động bộ nhận và bit RXCIE khởi động chế độ ngắt khi dữ liệu đã nhận trong UDR, tuy nhiên để có thể sử dụng ngắt, chúng ta cần set them bit I trong thanh ghi trạng thái bằng dòng code 20 (sei();). Phần quan trọng nhất trong đoạn code trên là trình phục ngắt nhận dữ liệu ISR. Khi dữ liệu đã được nhận đầy trong UDR, trình ngắt ISR(SIG_UART_RECV) sẽ được thực hiện, chúng ta sẽ đọc giá trị vừa nhận được vào biến u_Data (dòng 26) và sau đó phát giá trị này ra chân TxD để hiển thị lên thiết bị đầu cuối ảo bằng dòng lệnh 27.

       Phần mạch điện mô phỏng được trình bày trong hình 5. Chương trình cho chip TRANSMITTER là chương trình trong ví dụ 1 và chương trình cho chip RECEIVER là chương trình trong đoạn code trên. Bạn phải set xung clock cho

Page 88: asembly

cả 2 chip là 8MHz và set tốc độ baud cho thiết bị đầu cuối ảo là 56700. Nếu khi chạy mô phỏng, thiết bị đầu cuối hiển thị các ký tự ASCII của các số từ 32 đến 127 như trong hình 5 thì mọi thứ đã được thực hiện chính xác.

Hình 5. Truyền và nhận bằng UART.

Bài 5.1 - Giao tiếp UART chế độ Multi-Processor

1 2 3 4 5

 ( 30 Votes )Nội dung Các bài cần tham khảo trước

1. Chế độ Multi-Processor trong AVR UART. Cấu trúc AVR .

Page 89: asembly

2. Sử dụng Multi-Processor.

Download ví dụ

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

Giao tiếp UART

I. Chế độ Multi-Processor trong AVR UART.

       AVR hỗ trợ một khả năng giao tiếp UART ở chế độ “đa xử lí” (Multi-processor) hay Master-Slaves. Điều đầu tiên bạn cần biết là chế độ này không phải là chuẩn của UART mà chỉ đặc biệt trên các chip AVR (và có thể trên một số chip khác của Atmel). Bit MPCM (bit 0) trong thanh ghi UCSRA là nhân tố quan trọng nhất để quyết định chế độ hoạt động này. Cấu hình mạng Master-Slave dùng UART được tóm tắt như sau:       - Trên mạng này chỉ có 1 Master và có thể có nhiều Slaves, các đường TxD và RxD của các Slaves được nối chung với nhau (nối song song). Các Slaves và Master được nối với nhau theo kiểu “bắt chéo”, TxD chung của Slaves nối với RxD của Master và ngược lại. Mỗi Slave mang 1 địa chỉ riêng do người dùng gán, đặc biệt có thể có nhiều Slave trùng địa chỉ vẫn không ảnh hưởng đến hoạt động của mạng.      - Các Slaves và Master phải được cài đặt khung truyền và baudrate như nhau (cũng như truyền thông UART thông thường). Khung truyền trong chế độ Master-Slaves có thể 5, 6, 7,8 hay 9 bit nhưng thông thường khung 9 bit được chọn. Bài này cũng hướng dẫn dựa trên khung 9 bit. Trong khung truyền 9 bit, 8 bit đầu tiên được chứa trong thanh ghi dữ liệu UDR như thông thường và bit thứ cao nhất là bit TXB8 trong thanh ghi USCRB (trường hợp phát) hay bit RXB8 trong thanh ghi UCSRB (trường hợp thu).      - Bit MPCM (bit 0) trong thanh ghi UCSRA cho phép một chip làm việc ở chế độ Master-Slave. Tuy nhiên bit này chỉ  có tác dụng ở chip Slaves, để một chip làm việc như một Slave (chờ lệnh từ Master) thì bit MPCM của chip này phải được set lên 1. Bit MPCM của Master không cần set.     Cơ chế làm việc của chế độ Master-Slaves được giải thích như sau: lúc đầu, các bit MPCM trên tất cả các Slaves đều được set lên 1, ngắt nhận dữ liệu RXCIE của các  Slaves được kích hoạt và chúng đang ở chế độ chờ “lệnh” từ Master. Khi chip Master muốn thực hiện một “cuộc gọi” với một Slave nào đó, nó sẽ phát ra một “gói địa chỉ” bao gồm 8 bits chứa địa chỉ của Slave cần gọi và bit cao nhất (TXB8) luôn bằng 1 (xem hình 1).

Page 90: asembly

Hình 1. Gói địa chỉ.

     Khi tất cả 9 bit được các Slaves nhận, bit cao nhất sẽ được Slaves chứa trong bit RXB8. Nếu bit này bằng 1 các Slaves biết rằng đây là gói địa chỉ, ngắt RXCIE sẽ xảy ra trên tất cả các Slaves. Quá trình này được chip thực hiện một cách hoàn toàn tự động. Trong trình phục vụ ngắt RXCIE (SIG_UART_RECV) người lập trình sẽ thực hiện so sánh giá trị 8 bits địa chỉ nhận về với địa chỉ của từng Slave. Nếu một Slave nhận thấy địa chỉ mà Master gởi khớp với địa chỉ của nó, người lập trình cần reset bit MPCM về 0 để tách Slave này ra khỏi chế độ chờ (chờ địa chỉ). Tiếp theo Master sẽ gởi liên tiếp các “gói dữ liệu” trên đường truyền. Khác với gói địa chỉ, bit cao nhất (TXB8) trong gói dữ liệu bằng 0 chứ không bằng 1. Trên chip Master, người lập trình cần viết 2 đoạn chương trình phát gói địa chỉ và gói dữ liệu riêng biệt. Đối với các Slaves, do bit cao nhất nhận về RXB8=0, ngắt RXCIE chỉ duy nhất xảy ra trên Slave có bit MPCM=0. Như thế, tất cả các Slaves khác sẽ bỏ qua gói này (ngắt RXCIE không xảy ra, không ảnh hưởng đến các việc khác) chỉ duy nhất Slave có địa chỉ trùng trước đó nhận dữ liệu. Một chú ý rất quan trọng là sau khi byte dữ liệu cuối cùng được nhận, Slave (chip được chọn) phải set lại bit MPCM lên 1 (do người lập trình thực hiện) để đưa Slave trở lại trạng thái chờ các cuộc gọi tiếp theo.      Như vậy, bằng cách nào đó Slave phải biết trước được số lượng bytes dữ liệu mà Master muốn gởi để kịp thời set bitMPCM lên 1 sau byte cuối. Có một số cách để biết trước số lượng bytes mà Master sẽ gởi như “thỏa thuận” trước số bytes cố định cho mỗi cuộc gọi; hoặc đơn giản Master dùng byte dữ liệu đầu tiên (sau byte địa chỉ) để báo số lượng bytes sẽ gởi tiếp theo; hoặc hay hơn có thể ghép thông số chỉ lượng bytes cần truyền vào gói địa chỉ nếu như không có quá nhiều Slaves trên mạng và số lượng bytes truyền cũng không quá lớn. Nhưng dù cách nào đi nữa, cần có sự “thỏa thuận” khi lập trình cho Master và Slave. Có một “dấu hiệu” khác có thể được dùng để phân biệt giữa gói dữ liệu và gói địa chỉ đó là trạng thái bit RXB8, bằng việc kiểm tra trạng thái bit này chúng ta sẽ biết được gói nào là dữ liệu (RXB8=0) và gói nào là địa chỉ (RXB8=1). Tuy nhiên cách này không nhận

Page 91: asembly

biết được byte dữ liệu cuối cùng được gởi vì vậy không được sử dụng để set bit MPCM lên 1.

II. Sử dụng Multi-Processor. 

     Trong ví dụ bài này tôi dùng phương pháp đơn giản là “thỏa thuận” trước giữa Master và Slave số lương bytes trong một lần truyền, cụ thể chúng ta sẽ thiết lập một mạng Master-Slaves với 1 Master và 2 Slaves. Các Slave có địa chỉ lần lượt là 1 và 2, chúng ta dùng 2 chân PC0 và PC1 để set địa chỉ cho Slaves (việc này giúp chúng ta có thể sử dụng 1 chương trình chung cho 2 Slaves). Master chỉ đơn giản gởi đến mỗi Slave 1 gói địa chỉ và 2 bytes dữ liệu. Các Slaves sẽ hiển thị 2 bytes dữ liệu lên 2 dòng của LCD. Mạch điện mô phỏng ví dụ trình bày trong hình 2. 

Hình 2. Ví dụ mạng Master-Slaves dùng UART.

Page 92: asembly

Chúng ta cần viết 2 đoạn chương trình riêng cho Master và Slaves. Đoạn chương trình cho Master được trình bày trong List1.List 1. Chương trình cho Master.

Page 93: asembly
Page 94: asembly

     Với chip Master, như đã trình bày chúng ta cần viết riêng 2 đoạn chương trình con phục vụ phát gói dữ liệu và gói địa chỉ. Trong list 1, hai đoạn chương trình này có tên uart_char_tx và uart_address_tx nằm từ dòng 38 đến 48. Đây chỉ đoạn code phát uart thông thường (xem bài AVR5 – Giao tiếp UART) cộng thêm với việc set và reset bit TXB8. Trong đoạn chương trình phát gói dữ liệu, bit TXB8 được reset về 0 bằng câu lệnh UCSRB &= ~(1<<TXB8); trong khi ở đoạn chương trình phát gói địa chỉ bit này được set lên 1, UCSRB |= (1<<TXB8); (chú ý bit TXB8 nằm trong thanh ghi USCRB).     Phần cài đặt cho UART (từ dòng 14 đến dòng 20) bạn đọc hãy xem lại bài AVR5. Chú ý đến các dòng từ 24 đến 30. Đây là phần gởi địa chỉ và dữ liệu đến các Slave. Trước khi muốn gởi dữ liệu đến Slave1, chúng ta cần gọi chương trình con phát địa chỉ uart_address_tx(1) như trong dòng 24, tiếp theo là phát 2 bytes dữ liệu theo cách thông thường (ví dụ byte1=200, byte2=123). Tương tự chúng ta có thể phát2 bytes dữ liệu đến Slave2 theo cách này (dòng 28, 29 và 30).

Page 95: asembly

List 2. Chương trình cho Slaves.

Page 96: asembly
Page 97: asembly

     Do chúng ta sử dụng TextLCD để hiển thị kết quả nhận về từ Master, cần include thư viện myLCD.h (dòng 6). Thư viện stdio.h chứa các hàm xử lí chuỗi ký tự giúp ích cho việc hiển thị LCD (chúng ta sẽ dùng hàm sprintf) nên cũng cần được include vào (dòng 5). Biến my_address chứa địa chỉ của Slave, u_data chứa giá trị nhận về từ UART, biến ind là chỉ số chỉ số bytes nhận về. Gói dữ liệu nhận về chứa trong mảng alldata[3], mảng dis[5] là mảng ký tự tạm thời hiển thị lên LCD (xem các khai báo biến trong 2 dòng 14. 15). Địa chỉ Slave do 2 chân PC0 và PC1 quyết định, việc đọc đỉa chỉ này được thực hiện với dòng lệnh my_address=PINC & 0x03. Bằng cách chọn địa chỉ “động” như thế chúng ta không cần viết riêng chương trình cho mỗi Slave.      Các dòng lệnh từ 24 đến 30 cài đặt thông số cho UART, chú ý cần cho phép ngắt RXCIE xảy ra dòng 29 và 30). Phần nội dung quan trọng nhất được viết trong trình phục vụ ngắt ISR(SIG_UART_RECV) (từ dòng 44 đến 60). Khi một ngắt RXCIE việc đầu tiên cần làm là đọc giá trị nhận về vào biến u_data (dòng 45), nếu đây là byte đầu tiên nhận về (tức ind=0, byte địa chỉ) thì chúng ta cần so sánh xem địa chỉ có khớp không (dòng 47). Nếu đúng là địa chỉ của Slave này thì cần reset bit MPCM về 0 để sẵn sàng nhận dữ liệu (dòng 48), tăng biến ind lên 1. Nếu byte nhận về không phải là byte đầu tiên mà là byte dữ liệu (biến ind khác 0) chúng ta sẽ gán byte nhận về vào mảng alldata và tăng biến chỉ số ind (các dòng từ 52 đến 54). Vì trong ví dụ này chúng ta thỏa thuận trước Master chỉ gởi 2 bytes dữ liệu đến mỗi Slave nên khi biến ind bằng 3, tức là đã nhận đủ 2 bytes dữ liệu chúng ta cần set lại bit MPCM để kết thúc quá trình nhận, đưa Slave về lại trạng thái chờ, đồng thời trả biến chỉ số ind về 0 (làm lại từ đầu) (xem các dòng 55 đến 57).     Khi mô phỏng, bạn hãy nạp chương trình trong List 1 cho chip Master và list2 cho 2 Slaves. Cần set xung clock 8MHz. Nếu bạn thực hiện đúng kết quả sẽ hiển thị như trong hình 1. 

Bài 6 - Chuyển đổi ADC

1 2 3 4 5

 ( 95 Votes )Nội dung Các bài cần tham khảo trước

Page 98: asembly

1. Bạn sẽ đi đến đâu.

2. Chuyển đổi tín hiệu tương tự sang tín hiệu số (ADC).

3. Bộ chuyển đổi ADC trên AVR.

Download ví dụ

Cấu trúc AVR

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Bạn sẽ đi đến đâu.

      Bài học này, như tên của nó, sẽ giới thiệu cách sử dụng bộ chuyển đổi tương tự - số (analog to digital converter - ADC). Công cụ chính cũng là 2 bộ phần mềm quen thuộc WinAVR và Proteus.

Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:-         Nguyên lý chuyển đổi AD.-         Chuyển đổi ADC đơn kênh trên AVR.       -         Sử dụng chuyển đổi ADC đơn kênh trên AVR, hiển thị số 4 digit bằng LED 7 đoạn.

II. Chuyển đổi dữ liệu tương tự (analog) sang dữ liệu số (digital).

      Trong các ứng dụng đo lường và điều khiển bằng vi điều khiển bộ chuyển đổi tương tự-số (ADC) là một thành phần rất quan trọng. Dữ liệu trong thế giới của chúng ta là các dữ liệu tương tự (analog). Ví dụ nhiệt độ không khí buổi sáng là 25oC và buổi trưa là 32oC, giữa hai mức giá trị này có vô số các giá trị liên tục mà nhiệt độ phải “đi qua” để có thể đạt mức 32oC từ 25oC, đại lượng nhiệt độ như thế gọi là một đại lượng analog. Trong khi đó, rõ ràng vi điều khiển là một thiết bị số (digital), các giá trị mà một vi điều khiển có thể thao tác là các con số rời rạc vì thực chất chúng được tạo thành từ sự kết hợp của hai mức 0 và 1. Ví dụ chúng ta muốn dùng một thanh ghi 8 bit trong vi điều khiển để lưu lại các giá trị nhiệt độ từ 0oC đến 255 oC, như chúng ta đã biết, một thanh ghi 8 bit có thể chứa tối đa 256 (28) giá trị nguyên từ 0 đến 255, như thế các mức nhiệt độ không nguyên như 28.123 oC sẽ không được ghi lại. Nói cách khác, chúng ta đã “số hóa” (digitalize) một dữ liệu analog thành một dữ liệu digital. Quá trình “số hóa” này thường được thực hiện bởi một thiết bị gọi là “bộ chuyển đổi tương tự - số hay đơn giản là ADC (Analog to Digital Converter).

      Có rất nhiều phương pháp chuyển đổi ADC, tôi không có ý định giải thích cụ thể các nguyên lý chuyển đổi này trong bài học về AVR, tuy nhiên tôi sẽ giới thiệu một cách chuyển đổi rất cơ bản và phổ biến để các bạn phần nào nắm được cách mà một bộ ADC làm việc. Phương pháp chuyển đổi mà tôi nói là phương pháp

Page 99: asembly

chuyển đổi trực tiếp (direct converting)  hoặc flash ADC.  Các bộ chuyển đổi ADC theo phương pháp này được cấu thành từ một dãy các bộ so sánh (như  opamp), các bộ so sánh được mắc song song và được kết nối trực tiếp với tín hiệu analog cần chuyển đổi. Một điện áp tham chiếu (reference) và một mạch chia áp được sử dụng để tạo ra các mức điện áp so sánh khác nhau cho mỗi bộ so sánh. Hình 1 mô tả một bộ chuyển đổi flash ADC có 4 bộ so sánh, Vin là tín hiệu analog cần chuyển đổi và giá trị sau chuyển đổi là các con số tạo thành từ sự kết hợp các mức nhị phân trên các chân Vo. Trong hình 1, bạn thấy rằng do anh hưởng của mạch chia áp (các điện trở mắc nối tiếp từ điện áp +15V đến ground), điện áp trên chân âm (chân -) của các bộ so sánh sẽ khác nhau. Trong lúc chuyển đổi, giả sử điện áp Vin lớn hơn điện áp “V-“ của bộ so sánh 1 (opamp ở phía thấp nhất trong mạch) nhưng lại nhỏ hơn điện áp V- của các bộ so sánh khác, khi đó ngõ Vo1 ở mức 1 và các ngõ Vo khác ở mức 0, chúng ta thu được một kết quả số. Một cách tương tự, nếu tăng điện áp Vin ta thu được các tổ hợp số khác nhau. Với mạch điện có 4 bộ so sánh như trong hình 1, sẽ có tất cả 5 trường hợp có thể xảy ra, hay nói theo cách khác điện áp analog Vin được chia thành  5 mức số khác nhau. Tuy nhiên, bạn chú ý là các ngõ Vo không phải là các bit của tín hiệu số ngõ ra, chúng chỉ là đại diện để tổ hợp thành tín hiệu số ngõ ra, dễ hiểu hơn chúng ta không sử dụng được các bit Vo trực tiếp mà cần một bộ giải mã (decoder). Trong bảng 1 tôi trình bày kết quả sau khi giải mã ứng với các tổ hợp của các ngõ Vo.

Hình 1. Mạch flash ADC với 4 bộ so sánh.

Page 100: asembly

Bảng 1 Giá trị số ngõ ra sau khi giải mã.

      Độ phân giải (resolution): như trong ví dụ trên, nếu mạch điện có 4 bộ so sánh, ngõ ra digital sẽ có 5 mức giá trị. Tương tự nếu mạch điện có 7 bộ so sánh thì sẽ có 8 mức giá trị có thể ở ngõ ra digital, khoảng cách giữa các mức tín hiệu  trong trường hợp 8 mức sẽ nhỏ hơn trường hợp 4 mức. Nói cách khác, mạch chuyển đổi với 7 bộ so sánh có giá trị digital ngõ ra “mịn” hơn khi chỉ có 4 bộ, độ “mịn” càng cao  tức độ phân giải (resolution) càng lớn. Khái niệm độ phân giải được dùng để chỉ số bit cần thiết để chứa hết các mức giá trị digital ngõ ra. Trong trường hợp có 8 mức giá trị ngõ ra, chúng ta cần 3 bit nhị phân để mã hóa hết các giá trị này, vì thế mạch chuyển đổi ADC với 7 bộ so sánh sẽ có độ phân giải là 3 bit. Một cách tổng quát, nếu một mạch chuyển đổi ADC có độ phân giải n bit thì sẽ có 2n mức giá trị có thể có ở ngõ ra digital. Để tạo ra một mạch chuyển đổi flash ADC có độ phân giải n bit, chúng ta cần đến 2n-1 bộ so sánh, giá trị này rất lớn khi thiết kế bộ chuyển đổi ADC có độ phân giải cao, vì thế các bộ chuyển đổi flash ADC thường có độ phân giải ít hơn 8 bit. Độ phân giải liên quan mật thiết đến chất lượng chuyển đổi ADC, việc lựa chọn độ phân giải phải phù hợp với độ chính xác yêu cầu và khả năng xử lý của bô điều khiển. Trong 2 mô tả một ví dụ “số hóa” một hàm sin analog thành dạng digital.

Page 101: asembly

Hình 2. Analog và digital của hàm sin.

      Điện áp tham chiếu (reference voltage): Cùng một bộ chuyển đổi ADC nhưng có người muốn dùng cho các mức điện áp khác nhau, ví dụ người A muốn chuyển đổi điện áp trong khoảng 0-1V trong khi người B muốn dùng cho điện áp từ 0V đến 5V. Rõ ràng nếu hai người này dùng 2 bộ chuyển đổi ADC đều có khả năng chuyển đổi đến điện áp 5V thì người A đang “phí phạm” tính chính xác của thiết bị. Vấn đề sẽ được giải quyết bằng một đại lượng gọi là điện áp tham chiếu - Vref (reference voltage). Điện áp tham chiếu thường là giá trị điện áp lớn nhất mà bộ ADC có thể chuyển đổi. Trong các bộ ADC, Vref thường là thông số được đặt bởi người dùng, nó là điện áp lớn nhất mà thiết bị có thể chuyển đổi. Ví dụ, một bộ ADC 10 bit (độ phân giải) có Vref=3V, nếu điện áp ở ngõ vào là 1V thì giá trị số thu được sau khi chuyển đổi sẽ là: 1023x(1/3)=314. Trong đó 1023 là giá trị lớn nhất mà một bộ ADC 10 bit có thể tạo ra (1023=210-1). Vì điện áp tham chiếu ảnh hưởng đến độ chính xác của quá trình  chuyển đổi, chúng ta cần tính toán để chọn 1 điện áp tham chiếu phù hợp, không được nhỏ hơn giá trị lớn nhất của input nhưng cũng đừng quá lớn.

II. Chuyển đổi ADC trên AVR.

      Chip AVR ATmega32 của Atmel có tích hợp sẵn các bộ chuyển đổi ADC với độ phân giải 10 bit. Có tất cả 8 kênh đơn (các chân ADC0 đến ADC7), 16 tổ hợp chuyển đổi dạng so sánh, trong đó có 2 kênh so sánh có thể khuyếch đại. Bộ chuyển đổi ADC trên AVR không hoạt động theo nguyên lý flash ADC mà tôi đề cập ở phần trên, ADC trong AVR là loại chuyển đổi xấp xỉ lần lượt (successive approximation ADC).

      ADC trên AVR cần được “nuôi” bằng nguồn điện áp riêng ở chân AVCC, giá trị điện áp cấp cho AVCC không được khác nguồn nuôi chip (VCC) quá +/-0.3V. Nhiễu (noise) là vấn đề rất quan trọng khi sử dụng các bộ ADC, để giảm thiểu sai số chuyển đổi do nhiễu, nguồn cấp cho ADC cần phải được “lọc” (filter) kỹ càng. Một cách đơn giản để tạo nguồn AVCC là dùng một mạch LC kết nối từ nguồn VCC của chip như minh họa trong hình 3, đây là cách được gợi ý bởi nhà sản xuất AVR.

Page 102: asembly

Hình 3. Tạo nguồn AVCC từ VCC.

      Điện áp tham chiếu cho ADC trên AVR có thể được tạo bởi 3 nguồn: dùng điện áp tham chiếu nội 2.56V (cố định), dùng điện áp AVCC hoặc điện áp ngoài đặt trên chân VREF. Một lần nữa, bạn cần chú ý đến noise khi đặt điện áp tham chiếu, nếu dùng điện áp ngoài đặt trên chân VREF thì điện áp này phải được lọc thật tốt, nếu dùng điện áp tham chiếu nội  2.56V hoặc AVCC thì chân VREF cần được nối với một tụ điện. Việc chọn điện áp tham chiếu sẽ được đề cập chi tiết trong phần sử dụng ADC.      Các chân trên PORTA của chip ATmega32 được dùng cho bộ ADC, chân PA0 tương ứng kênh ADC0 và chân PA7 tương ứng với kênh ADC7.

Page 103: asembly

1. Thanh ghi.

      Có 4 thanh trong bộ ADC trên AVR trong đó có 2 thanh ghi data chứa dữ liệu sau khi chuyển đổi, 2 thanh ghi điều khiển và chứa trạng thái của ADC.

      - ADMUX (ADC Multiplexer Selection Register): là 1 thanh ghi 8 bit điều khiển việc chọn điện áp tham chiếu, kênh và chế độ hoạt động của ADC. Chức năng của từng bit trên thanh ghi này sẽ được trình bày cụ thể như sau:

Bit 7:6- REFS1:0 (Reference Selection Bits): là các bit chọn điện áp tham chiếu cho ADC, 1 trong 3 nguồn điện áp tham chiếu có thể được chọn là: điện áp ngoài từ chân VREF, điện áp tham chiếu nội 2.56V hoặc điện áp AVCC. Bảng 2 tóm tắt giá trị các bit và điện áp tham chiếu tương ứng.

Bảng 2: Chọn điện áp tham chiếu

Bit 5-ADLAR (ADC Left Adjust Result): là bit cho phép hiệu chỉnh trái kết quả chuyển đổi. Sở dĩ có bit này là vì ADC trên AVR có độ phân giải 10 bit, nghĩa là kết quả thu được sau chuyển đổi là 1 số có độ dài 10 bit (tối đa 1023), AVR bố trí 2 thanh ghi data 8 bit để chứa giá trị sau chuyển đổi. Như thế giá trị chuyển đổi sẽ không lắp đầy 2 thanh ghi  data, trong một số trường hợp người dùng muốn 10 bit kết quả nằm lệch về phía trái trong khi cũng có trường hợp người dùng muốn kết quả nằm về phía phải. Bit ADLAR sẽ quyết định vị trí của 10 bit kết quả trong 16 bit của 2 thanh ghi data. Nếu ADLAR=0 kết quả sẽ được hiệu chỉnh về phía phải (thanh ghi ADCL chứa trọn 8 bit thấp và thanh ghi ADCH chứa 2 bit cao trong 10 bit kết quả), và nếu ADLAR=1 thì kết quả được hiệu chỉnh trái (thanh ghi ADCH chứa trọn 8 bit cao nhất, các bit từ 9 đến 2, và thanh ADCL chứa 2 bit thấp nhất trong 10 bit kết quả (bạn xem hình cách bố trí 2 thanh ghi ADCL và ADCH bên dưới để hiểu rõ hơn).

Page 104: asembly

Bits 4:0-MUX4:0 (Analog Channel and Gain Selection Bits): là 5 bit cho phép chọn kênh, chế độ và cả hệ số khuyếch đại cho ADC. Do bộ ADC trên AVR có nhiều kênh và cho phép thực hiện chuyển đổi ADC kiểu so sánh (so sánh điện áp giữa 2 chân analog) nên trước khi thực hiện chuyển đổi, chúng ta cần set các bit MUX để chọn kênh và chế độ cần sử dụng. Bảng 3 tóm tắt các chế độ hoạt động của ADC thông qua các giá trị của các bit MUX. Trong bảng này, ứng với các giá trị từ 00000 đến 00111 (nhị phân), các kênh ADC được chọn ở chế độ đơn kênh (tín hiệu input lấy trực tiếp từ các chân analog và so sánh với 0V), giá trị từ 01000 đến 11101 tương ứng với chế độ chuyển đổi so sánh.

Bảng 3: Chọn chế độ chuyển đổi.

Page 105: asembly

      - ADCSRA (ADC Control and Status RegisterA): là thanh ghi chính điều khiển hoạt động và chứa trạng thái của module ADC.

Page 106: asembly

      Từng bit của thanh ghi ADCSRA được mô tả như bên dưới:

Bit 7 - ADEN(ADC Enable): viết giá trị 1 vào bit này tức bạn đã cho phép module ADC được sử dụng. Tuy nhiên khi ADEN=1 không có nghĩa là ADC đã hoạt động ngay, bạn cần set một bit khác lên 1 để bắt đầu quá trình chuyển đổi, đó là bit ADSC.

Bit 6 - ADSC(ADC Start Conversion): set bit này lên 1 là bắt đầu khởi động quá trình chuyển đổi. Trong suốt quá trình chuyển đổi, bit ADSC sẽ được giữ nguyên giá trị 1, khi quá trình chuyển đổi kết thúc (tự động), bit này sẽ được trả về 0. Vì vậy bạn không cần và cũng không nên viết giá trị 0 vào bit này ở bất kỳ tình huống nào. Để thực hiện một chuyển đổi, thông thường chúng ta sẽ set bit ADEN=1 trước và sau đó set ADSC=1.

Bit 4 – ADIF(ADC Interrupt Flag): cờ báo ngắt. Khi một chuyển đổi kết thúc, bit này tự động được set lên 1, vì thế người dùng cần kiểm tra giá trị bit này trước khi thực hiện đọc giá trị chuyển đổi để đảm bảo quá trình chuyển đổi đã thực sự hoàn tất.

Bit 3 – ADIE(ADC Interrupt Enable): bit cho phép ngắt, nếu bit này được set bằng 1 và bit cho phép ngắt toàn cục (bit I trong thanh ghi trạng thái của chip) được set, một ngắt sẽ xảy ra khi một quá trình chuyển đổi ADC kết thúc và các giá trị chuyển đổi đã được cập nhật (các giá trị chuyển đổi chứa trong 2 thanh ghi ADCL và ADCH).

Bit 2:0 – ADPS2:0(ADC Prescaler Select Bits): các bit chọn hệ số chia xung nhịp cho ADC. ADC, cũng như tất cả các module khác trên AVR, cần được giữ nhịp bằng một nguồn xung clock. Xung nhịp này được lấy từ nguồn xung chính của chip thông qua một hệ số chia. Các bit ADPS cho phép người dùng chọn hệ số chia từ nguồn clock chính đến ADC. Tham khảo bảng 4 để biết cách chọn hệ số chia.

Bảng 4: Hệ số chia xung nhịp cho ADC.

Page 107: asembly

      - ADCL và ADCH (ADC Data Register): 2 thanh ghi chứa giá trị của quá trình chuyển đổi. Do  module ADC trên AVR có độ phân giải tối đa 10 bits nên cần 2 thanh ghi để chứa giá trị chuyển đổi. Tuy nhiên tổng số bít của 2 thanh ghi 8 bit là 16, con số này nhiều hơn 10 bit của kết quả chuyển đổi, vì thế chúng ta được phép chọn cách ghi 10 bit  kết quả vào 2 thanh ghi này. Bit ADLAR trong thanh ghi ADMUX quy định cách mà kết quả được ghi vào.

ADLAR=0:

ADLAR=1:

      Thông thường, 2 thanh ghi data được sắp xếp theo định dạng ADLAR=0, ADCL chứa 8 bit thấp và 2 bit thấp của ADCH chứa 2 bit cao nhất của giá trị thu được. Chú ý thứ tự đọc giá trị từ 2 thanh ghi này, để tránh đọc sai kết quả, bạn cần đọc thanh ghi ADCL trước và ADCH sau, vì sau khi ADCH được đọc, các thanh ghi data có thể được cập nhật giá trị tiếp theo.

      - SFIOR(Special FunctionIO Register C): thanh ghi chức năng đặc biệt, 3 bit cao trong thanh ghi này quy định nguồn kích ADC nếu chế độ Auto Trigger được sử dụng. Đó là các bit ADTS2:0 (Auto Trigger Source 2:0). Các loại nguồn kích được trình báy trong bảng 5.

Page 108: asembly

Bảng 5: Nguồn kích ADC trong chế độ Auto Trigger.

2. Sử dụng ADC- Chuyển đổi đơn kênh.

      Khái niệm đơn kênh được hiểu là đại lượng cần chuyển đổi là các điện áp đặt trực tiếp trên các chân analog của chip, giá trị điện áp này được so sánh với 0V của chip, hay nói một cách khác, điện áp cần chuyển đổi và chip AVR có “mass chung”. Chúng ta sẽ minh họa cách sử dụng ADC trên AVR ở chế độ đơn kênh bằng ví dụ đọc và hiển thị giá trị ADC trên các LED 7 đoạn. Như minh họa trong hình 4, chúng ta sẽ dùng 4 LED để hiển thị 4 chữ số của kết quả, do chúng ta đều biết ADC trên AVR có độ phân giải 10 bit nên kết quả chuyển đổi tối đa là 1023, 4 LED là đủ để hiển thị kết quả này. 4 chip 7447 được dùng để điều khiển 4 LED, chúng ta cần 16 đường để xuất dữ liệu hiển thị lên 4 LED vì thế PORTB và PORTC sẽ được dùng cho mục đích này. 4 bit cao của PORTC(PC4:7) chứa chữ số hàng nghìn của kết quả, 4 bit thấp PC0:3 chứa chữ số hàng trăm, 4 bit cao của PORTB(PB4:7) dùng xuất chữ số hàng chục và 4 bit PB0:3 dành cho chữ số hàng đơn vị. Đại lượng cần chuyển đổi là điện áp trên chân ADC0 (kênh 0 của ADC, chân 0 trong PORTA chip ATmega32), điện áp được tạo ra bằng một biến trở RV1. Thay đổi giá trị biến trở, điện áp rơi trên ADC0 thay đổi và được cập nhật trực tiếp trên các LED. Giá trị hiển thị trên LED không phải là giá trị điện áp mà là giá trị tương đối sau khi chuyển đổi. Trong ví dụ này, tôi sẽ trình bày dạng tổng quát, việc đọc ADC và hiển thị LED được viết trong các chương trình con tương ứng. Bằng cách này, các bạn có thể dễ dàng sửa đổi và mở rộng ví dụ sau này.

Page 109: asembly

Hình 4. Đọc ADC đơn kênh.

      List 1 trình bày đoạn code minh họa đọc ADC đơn kênh và hiển thị kết quả trên LED 7 đoạn.

List 1. Đọc ADC đơn kênh và hiển thị bằng LED 7 đoạn.

Page 110: asembly
Page 111: asembly

        Tôi tạm thời chia đoạn chương trình thành 4 phần, phần 1 là các định nghĩa (dòng 4 đến 7), phần 2 là chương trình con đọc ADC đơn kênh (dòng 10 đến 14), phần 3 là chương trình con hiển thị môt giá trị 4 chữ số lên 4 LED 7 đoạn (từ dòng 17 đến 30) và phần 4 là chương trình chính. Chúng ta sẽ tìm hiểu theo từng phần.

      - Phần 1: ba dòng 4, 5 và 6 chúng ta định nghĩa 3 biến đại diện tên của 3 mode điện áp tham chiếu có thể dùng cho ADC. Xem lại bảng 2 chúng ta biết rằng điện áp tham chiếu được chọn thông qua 2 bit REFS trong thanh ghi ADMUX, có 3 loại điện áp có thể được chọn. Biến AREF_MODE tương ứng với trường hợp chúng ta muốn lấy điện áp trên chân AREF  làm điện áp tham chiếu, đối chiếu bảng 2 chúng ta cần set 2 bit REFS bằng 0, và dòng 4 “ #define AREF_MODE          0”  thực hiện việc này. Tương tự, biến INT_MODE đại diện cho trường hợp điện áp tham chiếu nội 2.56V và được định nghĩa cho phép set 1 bit REFS lên 1  “#define INT_MODE       (1<<REFS1)|(1<<REFS0)”. Biến AVCC_MODE đại diện trường hợp điện áp tham chiếu lấy từ chân AVCC. Cuối cùng, biến ADC_VREF_TYPE được định nghĩa  là biến chọn mode mà chúng ta thực sự  muốn dùng cho ADC, trong ví dụ này tôi chọn điện áp tham chiếu lấy từ chân AVCC vì thế tôi định nghĩa “#define ADC_VREF_TYPE AVCC_MODE”. Bit ADC_VREF_TYPE sẽ được gán cho thanh ghi ADMUX khi khởi động ADC trong chương trình chính.

      - Phần 2-chương trình con đọc ADC đơn kênh “uint16_t  read_adc(unsigned char adc_channel)”: tên chương trình là read_adc và adc_channel là tham số cần truyền cho chương trình con, tham số này là chỉ số kênh muốn đọc (từ kênh 0 đến kênh 7). Giá trị trả về là một số nguyên không dấu 16 bit (kiểu unsigned int của C), tuy nhiên trong ví dụ này tôi dùng kiểu dữ liệu uint16_t thay cho unsigned int, uint16_t là một cách định nghĩa kiểu dữ liệu nguyên không dấu 16 bit của riêng thư viện gcc-avr. Dòng đầu tiên của đoạn chương trình con (dòng 11) là khai báo kênh muốn đọc bằng cách ghép giá trị kênh cho thanh ghi ADMUX “ADMUX =adc_channel | ADC_VREF_TYPE ;”. Xem lại cấu trúc thanh ghi ADMUX, trong thanh ghi này, ngoài các bit chọn nguồn điện áp tham chiếu REFS thì 5 bit thấp MUX4:0 cho phép chọn kênh ADC cần đọc. Tham khảo thêm bảng 3 chúng ta thấy rằng 8 giá trị đầu tiên của các bit MUX4:0 (từ 00000 đến 00111 nhị phân) tương ứng với 8 kênh đơn ADC0:7. Chính sự sắp xếp này cho phép chúng ta ghép trực tiếp giá trị kênh muốn đọc vào thanh ghi ADMUX thông qua dòng lệnh ADMUX =adc_channel | ADC_VREF_TYPE. Chúng ta dùng phép OR "|" để ghép giá trị kênh muốn đọc và chế độ tham chiếu của ADC trước khi gán cho thanh ghi ADMUX. Một chú ý quan trọng là giá trị của tham số adc_channel chỉ trong khoảng từ 0 đến 7 tương ứng với 8 chế độ đọc đơn kênh ADC trong bảng 3. Sau khi kênh đã được chọn, dòng 12 set bit ADCS trong thanh ghi ADCSRA để bắt đầu quá trình chuyển đổi “ADCSRA|=(1<<ADSC);”. Như đã đề cập trong khi khảo sát chức năng của bit ADIF trong

Page 112: asembly

thanh ghi ADCSRA, sau khi quá trình chuyển đổi kết thúc bit ADIF sẽ được tự động set lên 1, vì thế dòng code 13 được dùng để chờ cho bit này lên 1, tức chờ cho quá trình chuyển đổi kết thúc. Câu lệnh “loop_until_bit_is_set(ADCSRA,ADIF);” được hiểu là lặp cho đến khi bit ADIF trong thanh ghi ADCSRA được set lên 1, lệnh “loop_until_bit_is_set”  này được định nghĩa sẵn trong thư viện gcc-avr. Nếu quá trình chuyển đỗi đã kết thúc, kết quả chuyển đổi sẽ được chứa trong 2 thanh ghi ADCL và ADCH, 2 thanh ghi này được tự động gọp thành thanh ghi 16 bit ADCW (ADC WORD), dòng 14 “return ADCW” trả về kết quả chuyển đổi.

      - Phần 3-chương trình con hiển thị số có 4 chữ số lên 4 LED 7 đoạn “void LED7_out(uint16_t val)” :  val là số cần hiển thị, chúng ta khai báo 4 biến tạm “dvi, chuc, tram, nghin” đại diện cho các chữ số đơn vị, chục, trăm và nghìn ở dòng 18. Đồng thời, một biến tạm temp_val được dùng để lưu giá trị tạm thời của số val như trong dòng 19 “temp_val=val;”, cách làm này nhằm tránh thay đổi giá trị của bản thân val trong quá trình thao tác. Các dòng code từ 21 đến 26 thực hiện quá trình tách số val ra thành 4 các chữ số hàng đơn vị, chục, trăm và nghìn. Đây chỉ là phương pháp đại số thông thường nên tôi sẽ không giải thích thêm cho đoạn này. Hai dòng 28 và 29 xuất giá trị ra 4 LED 7 đoạn. Bốn LED 7 đoạn được điều khiển bởi các IC chuyển mã 7447, giá trị input choc các IC 7447 là các số BCD 4 bit. Vì thế, để xuất 4 chữ số ra 4 LED thông qua 7447 chúng ta cần 4x4=16 bit, trong ví dụ này tôi dùng PORTB và PORTC cho nhiệm vụ này. Bốn bit cao của PORTC sẽ chứa chữ số hàng nghìn, bốn bit thấp chứa chữ số hàng trăm, bốn bit cao của PORTB chứa chữ số hàng chục và bốn bit thấp PORTB chứa số đơn vị. Dòng code 28 “PORTB=(chuc<<4)+dvi;” xuất 2 chữ số chục và đơn vị ra PORTB, trong đó hàm “chuc<<4” nghĩa là dịch chữ số hàng chục sang trái 4 vị trí để đưa chữ số này lên 4 bit cao của PORTB, sau đó cộng chữ số đơn vị vào 4 bit thấp và cuối cùng là xuất ra PORTB. Tương tự chúng ta có thể xuất 2 chữ số hàng nghìn và hàng trăm ra PORTC thông qua dòng code 29 “PORTC=(nghin<<4)+tram”.

      - Phần 4-chương trình chính: do hầu hết các nhiệm vụ đã được thực hiện trong các đoạn chương trình con nên chương trình chính trong ví dụ này khá đơn giản. Hai dòng code 32 và 33 set các thông số cho ADC, dòng 32 “ADCSRA=(1<<ADEN)|(1<<ADPS2)|(1<<ADPS0);” set các bit trong thanh ghi điều khiển ADCSRA, ADC được cho phép hoạt động bởi bit ADEN, các bit ADPS2:0 để chọn prescaler xung clock (xem lại phần mô tả thanh ghi ADCSRA), trong ví dụ này tôi chọn prescaler = 32 (bạn có thể chọn giá trị khác). Dòng 33 “ADMUX=ADC_VREF_TYPE;” cho phép chọn điện áp tham chiếu bằng cách gán biến ADC_VREF_TYPE mà chúng ta đã định nghĩa trong dòng code 7 cho thanh ghi ADMUX. Bạn cần chú ý là sau khi thực hiện 2 dòng code này, ADC chỉ mới ở tư thế “sẵn sàng” nhưng vẫn chưa hoạt động, ADC sẽ hoạt động khi chúng

Page 113: asembly

ta gọi chương trình con đọc adc. Trong vòng lặp while của chương trình chính chúng ta lần lượt đọc giá trị ADC ở kênh 0 bằng cách gọi chương trình con “read_adc(0)” ở dòng lệnh 39 “ADC_val=read_adc(0);” sau đó hiển thị ra LED 7 đoạn ở dòng 40 “LED7_out(ADC_val);” và cuối cùng là delay 1 khoảng thời gian nhỏ (100ms) trước khi lặp lại quá trình đọc và hiển thị.

Mô phỏng ví dụ: Tạo 1 project bằng Programmer Notepad và type đoạn code trên vào file source (xem phần tạo Project với WinAVR). Biên dịch và chạy mô phỏng với mạch điện trong hình 4. Điều chỉnh giá trị biến trở RV1 để thay đổi giá trị điện áp input của ADC kênh 0 và xem giá trị hiển thị trên các LED 7 đoạn. Hãy thay đổi giá trị biến ADC_VREF_TYPE trong dòng code 7 sang các mode khác như INT_MODE, biên dịch và mô phỏng lại chương trình, quan sát và so sánh sự khác nhau giữa các mode điện áp tham chiếu. Bạn sẽ dễ dàng nhận thấy rằng khi chọn điện áp tham chiếu nội 2.56V, khi tăng biến trở đến khoảng giữa thì kết quả chuyển đổi sẽ là 1023(giá trị lớn nhất của số 10 bit) và nếu tiếp tục tăng biến trở giá trị này sẽ không thay đổi. Điều này có nghĩa là nếu điện áp input lớn hơn điện áp tham chiếu thì kết quả chuyển đổi sẽ là 1023.

      Phần chuyển đổi ADC ở chế độ so sánh sẽ được trình bày trong 1 dịp khác ở phần ứng dụng. 

Bài 7 - Giao tiếp SPI

1 2 3 4 5

 ( 76 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu .

2. Chuẩn truyền thông SPI .

3. Truyền thông SPI trên AVR .

Download ví dụ

Cấu trúc AVR .

AVR Studio .

C cho AVR.

Mô phỏng với Proteus.

Page 114: asembly

Text LCD

I. Giới thiệu.

      Bài này giúp các bạn biết cách sử dụng cách truyền thông nối tiếp đồng bộ SPI. Công cụ chính cũng là 2 bộ phần mềm AVRStudio (+gcc-avr) và Proteus. Thực chất ngôn ngữ lập trình vẫn là gcc-avr nhưng tôi không dùng Programmer Notepad để biết code như thông thường, thay vào đó tôi dùng AVRStudio làm trình biên tập, bạn tham khảo thêm phần “Lập trình C bằng AVRStudio” trong bài hướng dẫn sử dụng AVRStudio để biết thêm cách thực hiện. Tôi sẽ dùng chip ATmega32 làm minh họa.Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:

Nguyên lý truyền thông nối tiếp SPI. Sử dụng module SPI trong AVR ở các chế độ Master và Slave.

II. Chuẩn truyền thông SPI,

       SPI (Serial Peripheral Bus) là một chuẩn truyền thông nối tiếp tốc độ cao do hang Motorola đề xuất. Đây là kiểu truyền thông Master-Slave, trong đó có 1 chip Master điều phối quá trình tuyền thông và các chip Slaves được điều khiển bởi Master vì thế truyền thông chỉ xảy ra giữa Master và Slave. SPI là một cách truyền song công (full duplex) nghĩa là tại cùng một thời điểm quá trình truyền và nhận có thể xảy ra đồng thời. SPI đôi khi được gọi là chuẩn truyền thông “4 dây” vì có 4 đường giao tiếp trong chuẩn này đó là SCK (Serial Clock), MISO (Master Input Slave Output), MOSI (Master Ouput Slave Input) và SS (Slave Select). Hình 1 thể hiện một kết SPI giữa một chip Master và 3 chip Slave thông qua 4 đường.

       SCK: Xung giữ nhịp cho giao tiếp SPI, vì SPI là chuẩn truyền đồng bộ nên cần 1 đường giữ nhịp, mỗi nhịp trên chân SCK báo 1 bit dữ liệu đến hoặc đi. Đây là điểm khác biệt với truyền thông không đồng bộ mà chúng ta đã biết trong chuẩn UART. Sự tồn tại của chân SCK giúp quá trình tuyền ít bị lỗi và vì thế tốc độ truyền của SPI có thể đạt rất cao. Xung nhịp chỉ được tạo ra bởi chip Master. 

       MISO– Master Input / Slave Output: nếu là chip Master thì đây là đường Input còn nếu là chip Slave thì MISO lại là Output. MISO của Master và các Slaves được nối trực tiếp với nhau..          MOSI – Master Output / Slave Input: nếu là chip Master thì đây là đường Output còn nếu là chip Slave thì MOSI là Input. MOSI của Master và các Slaves được nối trực tiếp với nhau. 

Page 115: asembly

       SS – Slave Select: SS là đường chọn Slave cần giap tiếp, trên các chip Slave đường SS sẽ ở mức cao khi không làm việc. Nếu chip Master kéo đường SS của một Slave nào đó xuống mức thấp thì việc giao tiếp sẽ xảy ra giữa Master và Slave đó. Chỉ có 1 đường SS trên mỗi Slave nhưng có thể có nhiều đường điều khiển SS trên Master, tùy thuộc vào thiết kế của người dùng.

.

Hình 1. Giao diện SPI.

       Hoạt động: mỗi chip Master hay Slave có một thanh ghi dữ liệu 8 bits. Cứ mỗi xung nhịp do Master tạo ra trên đường giữ nhịp SCK, một bit trong thanh ghi dữ liệu của Master được truyền qua Slave trên đường MOSI, đồng thời một bit trong thanh ghi dữ liệu của chip Slave cũng được truyền qua Master trên đường MISO. Do 2 gói dữ liệu trên 2 chip được gởi qua lại đồng thời nên quá trình truyền dữ liệu này được gọi là “song công”. Hình 2 mô tả quá trình truyền 1 gói dữ liệu thực hiện bởi module SPI trong AVR, bên trái là chip Master và bên phải là Slave.

Page 116: asembly

 

Hình 2. Truyền dữ liệu SPI.

       Cực của xung giữ nhịp, phase và các chế độ hoạt động: cực của xung giữ nhịp (Clock Polarity) được gọi tắt là CPOL là khái niệm dùng chỉ trạng thái của chân SCK ở trạng thái nghỉ. Ở trạng thái nghỉ (Idle), chân SCK có thể được giữ ở mức cao (CPOL=1) hoặc thấp (CPOL=0). Phase (CPHA) dùng để chỉ cách mà dữ liệu được lấy mẫu (sample) theo xung giữ nhịp. Dữ liệu có thể được lấy mẫu ở cạnh lên của SCK (CPHA=0) hoặc cạnh xuống (CPHA=1). Sự kết hợp của SPOL và CPHA làm nên 4 chế độ hoạt động của SPI. Nhìn chung việc chọn 1 trong 4 chế độ này không ảnh hưởng đến chất lượng truyền thông mà chỉ cốt sao cho có sự tương thích giữa Master và Slave.

III. Truyền thông SPI trên AVR.

      Module SPI trong các chip AVR hầu như hoàn toàn giống với chuẩn SPI mô tả trong phần trên. Vì thế, nếu đã hiểu cách truyền thông SPI thì sẽ khống quá khó để thực hiện việc truyền thông này với AVR. Phần bên dưới tôi trình bày một số điểm quan trọng khi điều khiển SPI trên AVR.

Các chân SPI: Các chân giao tiếp SPI cũng chính là các chân PORT thông thường, vì thế nếu muốn sử dụng SPI chúng ta cần xác lập hướng cho các chân này. Trên chip ATmega32, các chân SPI như sau:

SCK    – PB7 (chân 8)MISO  – PB6 (chân 7)MOSI  – PB5 (chân 6)SS       – PB4 (chân 5)

      Khi chip AVR được sử dụng làm Slave, bạn cần set các chân SCK input, MOSI input, MISO output và SS input. Nếu là Master thì SCK output, MISO output, MOSI input và khi này chân SS không quan trọng, chúng ta có thể dùng chân này để điều khiển SS của Slaves hoặc bất kỳ chân PORT thông thường nào.

Page 117: asembly

       Thanh ghi: SPI trên AVR được vận hành bởi 3 thanh ghi bao gồm thanh ghi điều khiển SPCR , thanh ghi trạng thái SPSR và thanh ghi dữ liệu SPDR. 

       SPCR (SPI Control Register): là 1 thanh ghi 8 bit điều khiển tất cả hoạt động của SPI.

      

      * Bit 7- SPIE (SPI Interrupt Enable) bit cho phép ngắt SPI. Nếu bit này được set bằng 1 và bit I trong thanh ghi trạng thái được set bằng 1 (sei), 1 ngắt sẽ xảy ra sau khi một gói dữ liệu được truyền hoặc nhận. Chúng ta nên dùng ngắt (nhất là đối với chip Slave) khi truyền nhận dữ liệu với SPI.     * Bit 6 – SPE (SPI Enable). set bit này lên 1 để cho phép bộ SPI hoạt động. Nếu SPIE=0 thì module SPI dừng hoạt động.     * Bit 5 – DORD (Data Order)  bit này chỉ định thứ tự dữ liệu các bit được truyền và nhận trên các đường MISO và MOSI, khi DORD=0 bit có trọng số lớn nhất của dữ liệu được truyền trước (MSB) ngược lại khi DORD=1, bit LSB được truyền trước. Thật ra khi giao tiếp giữa 2 AVR với nhau, thứ tự này không quan trọng nhưng phải đảm bảo các bit DORD giống nhau trên cả Master và Slaves.     * Bit 4 – MSTR (Master/Slave Select) nếu MSTR =1 thì chip được nhận diện là Master, ngược lại MSTR=0 thì chip là Slave..     * Bit 3 và 2 – CPOL và CPHA đây chính là 2 bit xác lập cực của xung giữ nhịp và cạnh sample dữ liệu mà chúng ta đã khảo sát trong phần đầu. Sự kết hợp 2 bit này tạo thành 4 chế độ hoạt động của SPI. Một lần nữa, chọn chế độ nào không quan trọng nhưng phải đảm bảo Master và Slave cùng chế độ hoạt động. Vì thế có thể để 2 bit này bằng 0 trong tất cả các chip. Hình 3 trình bày cách sample dữ liệu trong 4 chế độ của SPI trên AVR.       

 ra khi giao tiếp giữa 2 AVR với nhau, thứ tự này không quan trọng nhưng phải đảm bảo các bit DORD giống nhau trên cả Master và Slaves.

 CPHA=0

Page 118: asembly

  CPHA=1

   

Hình 3. Các chế độ hoạt động của SPI. 

        * Bit 1:0 – CPR1:0 hai bit này kết hợp với bit SPI2X trong thanh ghi SPSR cho phép chọn tốc độ giao tiếp SPI, tốc độ này được xác lập dựa trên tốc độ nguồn xung clock chia cho một hệ số chia. Bảng 1 tóm tắt các tốc độ mà SPI trong AVR có thể đạt. Thông thường, tốc bộ này không được lớn hơn 1/4 tốc độ xung nhịp cho chip.

Page 119: asembly

 

       SPSR (SPI Status Register): là 1 thanh ghi trạng thái của module SPI. Trong thanh ghi này  chỉ có 3 bit được sử dụng. Bit 7 – SPIF là cờ báo SPI, khi một gói dữ liệu đã được truyền hoặc nhận từ SPI, bit SPIF sẽ tự động được set len 1. Bit 6 – WCOL là bít báo va chạm dữ liệu (Write Colision), bit này được AVR set lên 1 nếu chúng ta cố tình viết 1 gói dữ liệu mới vào thanh ghi dữ liệu SPDR trong khi quá trình truyền nhận trước chưa kết thúc. Bit 0 – SPI2X gọi là bit nhân đôi tốc độ truyền, bit này kết hợp với 2 bit SPR1:0 trong thanh ghi điều khiển SPCR xác lập tốc độ cho SPI.

       SPDR (SPI Data Register):  là thanh ghi dữ liệu của SPI. Trên chip Master, ghi giá trị vào thanh ghi SPDR sẽ kích quá trình tuyền thông SPI. Trên chip Slave, dữ liệu nhận được từ Master sẽ lưu trong thanh ghi SPDR, dữ liệu được lưu sẵn trong SPDR sẽ được truyền cho Master.

     Sử dụng SPI trên AVR: SPI trên AVR hoạt động không khác nguyên lý chung của chuẩn SPI là mấy. Vận hành SPI trên AVR được thực hiện dựa trên việc ghi và đọc 3 các thanh ghi SPCR, SPSR và SPDR. Trước khi truyền nhận bằng SPI chúng ta cần khởi động SPI, quá trình khởi động thường bao gồm chọn hướng giao tiếp cho các chân SPI, chọn loại giao tiếp: Master hay Slave, chọn chế độ SPI (SPOL, SPHA) và chọn tốc độ giao tiếp. Truyền thông SPI luôn được khởi xướng bởi chip Master, khi Master muốn giao tiếp với 1 Slave nào đó, nó sẽ kéo chân SS của Slave xuống mức thấp (gọi là chọn địa chỉ) và sau đó viết dữ liệu cần truyền vào thanh ghi dữ liệu SPDR, khi dữ liệu vừa được viết vào SPDR xung giữ nhịp sẽ được tự động tạo ra trên SCK và quá trình truyền nhận bắt đầu. Đối với các chip Slave, khi chân SS bị kéo xuống nó sẽ sẵn sàng cho quá trình truyền nhận. Khi phát hiện xung giữ nhịp trên SCK, Slave sẽ bắt đầu sample dữ liệu đến trên đường MOSI và gởi dữ liệu di trên MISO.

Page 120: asembly

      Để minh họa cho cách truyền và nhận dữ liệu SPI trên AVR, tôi sẽ thực hiện một ví dụ truyền nhân 1 chiều với 1 chip Master và 3 chip Slaves. Tất cả các chip được dùng là ATmega32, chip Master sẽ điều khiển các chip Slaves thông qua 3 đường chọn chip PB0, PD1 và PD2. Công việc thực hiện trong ví dụ này như sau: Master sẽ lần lượt chọn 1 trong 3 chip Slaves và gởi các gói dữ liệu tương ứng đến chúng, chip Slave0 sẽ nhận được các con số từ 0 đến 80, Slave1 nhận 80 đến 160 và Slave2 nhận dữ liệu từ 160 đến 240. Các Slave sẽ hiển thị giá trị mà mình nhận được trên các Text LCD kết nối với PORTD ở mỗi Slave. Sơ đồ mạch điện vẽ bằng Proteus cho ví dụ này được trình bày trong hình 4.

Page 121: asembly

       Hình 4. Mô phỏng ví dụ giao tiếp SPI trên AVR.

      Trong bài này, tôi sẽ dùng phần mềm AVRStudio kết hợp với gcc-avr trong WinAVR để lập trình bằng ngôn ngữ C cho AVR. Bạn hãy tham khảo thêm bài

Page 122: asembly

AVRStudio để biết cách tạo 1 Project lập trình C cho AVR bằng AVRStudio. Hãy tạo 2 Project riêng, 1 Project có tên SPI_Master cho chip Master và 1 Project có tên SPI_Slave dùng chung cho cả 3 Slaves. Copy file myLCD.h dùng cho điều khiển Text LCD được tạo trong bài “Text LCD” vào cả 2 thư mục chứa 2 Projects mới tạo. Viết đoạn code trong list 0 vào file SPI_Master.c và đoạn code trong list 1 vào file SPI_Slave.c.

List 1. Đoạn code cho SPI Master.

Page 123: asembly
Page 124: asembly

      

       Tôi sẽ giải thích sơ lượt một số điểm chính trong đoạn code cho chip Master. Các phần định nghĩa từ dòng thứ 10 đến dòng 17 chỉ có tác dụng làm cho chương trình dễ đọc hiểu hơn và có tính tương thích cao hơn, ví dụ nếu bạn muốn sử dụng ví dụ này cho các chip khác bạn chỉ cần thay đổi các định nghĩa này mà không phải thay đổi trong nội dung các chương trình con. Chúng ta định nghĩa để chọn PORTB điều khiển các đường chọn chip SS của Slave (gọi là các đường địa chỉ), dòng 18 định nghĩa Slave(i) là thứ tự chân trên PORT dùng cho chip Slave thứ i. Dễ hiểu hơn, đường SS trên Slave0 sẽ được kết nối và điều khiển bởi chân 0 của PORTB (chân PB0 và tương tự cho các Slaves còn lại. Biến wData định nghĩa trên dòng 20 là một mảng 3 phần tử chứa các con số 8 bits sẽ truyền đến các Slaves.

     Chương trình con “void SPI_MasterInit(void)”: Chương trình này khởi động cho chip Master, việc khởi động trước hết là set hướng cho các chân SPI. Đối với Master, các chân tạo xung giữ nhịp SCK và chân truyền dữ liệu MOSI cần được set Output như trong dòng 24, các chân SPI còn lại là input. Dòng 25 giúp kéo điện trở kéo lên ở chân nhận dữ liệu MISO của Master. Dòng lệnh 26 “SPCR=(1<<SPIE)|(1<<SPE)|(1<<MSTR)|(1<<CPHA)|(1<<SPR1)|(1<<SPR0); ” thật sự khởi động SPI với việc set bit SPIE: cho phép ngắt SPI=1, bit SPE=1 cho phép SPI hoạt động, MSTR=1 xác lập chip là chip Master. CPHA=1 tức chân SCK sẽ ở mức thấp khi SPI không hoạt động, trong khi CPOL=0 (không set CPOL thì mặc định là 0) thì dữ liệu sẽ được sample (lấy mẫu) ở cạnh xuống của xung SCK. Cuối cùng cả 2 bit SPR1 và SPR0 đều được set lên 1, tốc độ SPI sẽ bằng tốc độ nguồn cung nuôi chip chia cho 128 (xem bảng 1). Dòng code 29 set hướng Output cho các chân dùng làm chân địa chỉ chọn chip Slaves (các chân PB0, PB1, PB2), sau đó kéo các chân này lên mức cao để disable tất cả các Slaves (sau này sẽ kích hoạt sau).

      Chương trình con “void SPI_Transmit(uint8_t  i, uint8_t  data)”: chương trình truyền dữ liệu qua SPI của chip Master, chương trình có 2 tham số là địa chỉ chip Slave (biến i) và dữ liệu cần truyền (biến data). Trước khi truyền dữ liệu, Master sẽ thực hiện việc chọn Slave, dòng 35 “cbi(ADDRESS_PORT, Slave(i));” thực hiện việc này. Thực chất dòng này là kéo chân “i” của PORTB xuống mức thấp, cũng là kéo chân SS của Slave xuống mức thấp. Dòng 36 gán giá trị cần truyền cho thanh ghi dữ liệu “SPDR=data”, sau khi gán giá trị cho SPDR, xung clock sẽ tự động được Master tạo ra trên SCK, quá trình truyền bắt đầu. Quá trình truyền kết thúc thì bit cờ SPIF trong thanh ghi trạng thái SPSR được set lên 1, dòng 36 thực hiện việc chờ bit cờ SPIF để kết thúc quá trình truyền. Khi kết thúc truyền 1 byte cho Slave, set chân SS của Slave lên mức cao để vô hiệu hóa SPI, dòng 37. 

Page 125: asembly

       Chương trình chính: chương trình chính cho chip Master SPI tương đối đơn giản, trước hết chúng ta cần gọi chương trình con khởi động SPI ở dòng 43. Trong vòng lặp vô tận while, lần lượt gởi các giá trị đến các Slaves. Dòng 46 gọi chương trình con gởi giá trị biến wData[0] đến Slave0, dòng 50 truyền biến wData[1] cho Slave1 và dòng 54 truyền biến wData[2] cho Slave2

 List 2.Đoạn code cho Slave SPI. 

Page 126: asembly
Page 127: asembly

      Đoạn code trong list 2 là đoạn code cho chip Slaves, chú ý dòng 3 chúng ta include file header “interrupt.h” vì việc nhận dữ liệu SPI của SLave được thực hiện bằng ngắt SPI. Các định nghĩa biến trong các dòng  code từ 8 đến 15 tương tự như trong chương trình cho chip Master. Tôi sẽ tập trung giải thích các điểm khác biệt cho Slaves.

      Chương trình con “void SPI_SlaveInit(void)”: Chương trình này khởi động cho chip Slave, cũng giống như trường hợp của Master, việc khởi động trước hết là set hướng cho các chân SPI. Đối với Slave, chỉ có chân truyền dữ liệu MISO là cần được set Output như trong dòng 19, các chân SPI còn lại là input. Dòng 20 giúp kéo điện trở kéo lên ở các chân nhận dữ liệu MOSI của Slave, và chân chọn Slave SS. Việc tiếp theo là cài đặt các thanh ghi SPI như trong dòng lệnh 21, “SPCR=(1<<SPIE)|(1<<SPE)|(1<<CPHA)|(1<<SPR1)|(1<<SPR0); ”, nếu quan sát dòng lệnh 26 trong List 1 chop chip Master, dòng này không khác là mấy, quá trình khởi động SPI cho Slave tương tự Master với một điểm khác duy nhất là bit MSTR, bit này không được set lên 1 đối với Slaves.

      Trình phục vụ ngắt “ISR(SPI_STC_vect)”: SPI trên AVR chỉ có duy nhất một sự kiện gây ra ngắt đó là khi quá trình truyền-nhận kết thúc. Tên vector ngắt SPI trong ngôn ngữ lập trình avr-gcc là “SPI_STC_vect. Trong ví dụ này, khi một ngắt SPI xảy ra ở Slave, chúng ta sẽ đọc thanh ghi SPDR và sau đó hiển thị giá trị đọc được trên LCD. Dòng 37, rData=SPDR, gán thanh ghi SPDR cho biến rData. Từ dòng 38 đến 42 là cách hiển thị giá trị đọc về trên Text LCD bằng thư viện myLCD (xem bài Text LCD). Dòng 39 chúng ta khai báo 1 biến tạm dạng mảng động, dis, làm buffer chứa giá trị ascii của các ký tự cần hiển thị lên LCD. Chú ý là giá trị nhận về là 1 con số 8 bit, muốn hiển thị giá trị này lên LCD chúng ta không thể hiển thị trực tiếp bằng lệnh putChar_LCD vì hàm putChar_LCD  xem tham số nhập vào là mã Ascii, ví dụ chúng ta nhận về số rData=65, nếu dùng hàm putChar_LCD(rData) thì trên LCD chỉ thấy ký tự  ‘A’ vì 65 là mã Ascii của ký tự ‘A’. Để LCD hiển thị  “65” chúng ta xem 65 là một chuỗi các ký tự, trước hết cần chuyển số 65 thành các ký tự ‘6’ và ‘5’, hàm “sprintf(dis,"%i",rData)” trong dòng code 40 thực hiện việc định dạng lại biến rData thành chuỗi các ký tự và chứa trong buffer dis, “%i” là “cờ” định dạng, báo cho hàm sprintf xem rData là một số nguyên. Sau dòng 40, ví dụ rData=65, thì dis=”65”. Dòng 42 in chuỗi dis lên LCD: print_LCD(dis);.

        Chương trình chính: chương trình chính cho chip Slave không làm nhiều việc vì các việc chính như nhận và hiển thị đã được thực hiện trong trình phục vụ ngắt SPI. Dòng 27 sei() cho phép ngắt toàn cục, điều này là cần thiết để ngắt SPI có thể xảy ra, dòng 28 gọi chương trình con khởi động SPI cho Slave, sau đó khởi

Page 128: asembly

động LCD ở dòng 29 và kết thúc. Không có việc gì cần thực hiện trong vòng lặp while().

Bài 8 - Giao tiếp TWI - I2C

1 2 3 4 5

 ( 41 Votes )Nội dung Các bài cần tham khảo trước

1. Bạn sẽ đi đến đâu .

2. Giao diện TWI – I2C .

3. TWI trên AVR .

4. Điều khiển AVR TWI.

Download ví dụ

Cấu trúc AVR .

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Bạn sẽ đi đến đâu.

      Bài này giới thiệu cách giao tiếp bằng truyền thông nối tiếp đồng bộ Two-Wire Serial (TWI) tương thích với chuẩn I2C. Trong bài này chúng ta sẽ khảo sát 2 mode truyền và nhận trên chip Master cùng với 2 mode truyền và nhận trên chip Slave. Công cụ chính cũng là 2 bộ phần mềm WinAVR và Proteus. Vi điều khiển ATmega32 sẽ được dùng làm minh họa.

Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:

-         Nguyên lý truyền thông nối tiếp TWI và I2C.-         Sử dụng module TWI trong AVR ở các chế độ Master.-         Sử dụng module TWI trong AVR ở các chế độ Slave.-         Ví dụ giao tiếp giữa các AVR bằng TWI.

II. Giao diện TWI – I2C.

Page 129: asembly

      TWI (Two-Wire Serial Intereafce) là một module truyền thông nối tiếp đồng bộ trên các chip AVR dựa trên chuẩn truyền thông I2C. I2C là viết tắc của từ Inter-Integrated Circuit là một chuẩn truyền thông do hãng điện tử Philips Semiconductor sáng lập và xây dựng thành chuẩn năm 1990. Phiên bản mới nhất của I2C là V3.0 phát hành năm 2007. Để hiểu thêm về I2C  bạn có thể tham khảo các tài liệu “I2C Specification” từ trang web của NXP- http://www.nxp.com(lập bởi Philips).  Trong phạm vi bài học này tôi chỉ giới thiệu giao thức TWI được giới thiệu trong datasheet của các chip AVR từ Atmel. Tuy nhiên, về cơ bản TWI trong AVR hoàn toàn tương thích  I2C, do đó tìm hiểu TWI của AVR không chỉ giúp bạn giao tiếp giữa các AVR với nhau mà có thể dùng TWI để điều khiển bất kỳ một thiết bị nào theo chuẩn I2C (các chip nhớ, bộ chuyển đổi ADC, DCA, đồng hồ thời gian thực…).      TWI (I2C) là một truyền thông nối tiếp đa chip chủ (tạm dịch của cụm từ multi-master serial computer bus).  Khái niệm “multi-master” (tôi sẽ dùng từ tiếng anh multi-master thay vì dùng “đa chip chủ”) được hiểu là trong trên cùng một bus có thể có nhiều hơn một thiết bị làm Master, đồng thời một Slave có thể trở thành một Master nếu nó có khả năng. Ví dụ trong một mạng TWI của nhiều AVR kết nối với nhau, bất kỳ một AVR nào đều có thể trở thành Master ở một thời điểm nào đó. Tuy nhiên nếu một mạng dùng một AVR điều khiển các chip nhớ (như EEPROM AT24C1024 chẳng hạn) thì khái niệm “multi-master” không tồn tại vì các chip nhớ được thiết kế sẵn là Slave, không có khả năng trở thành master. TWI (I2C) được thực hiện trên 2 đường SDA (Serial DATA) và SCL (Serial Clock) trong đó SDA là đường truyền/nhận dữ liệu và SCL là đường xung nhịp. Căn cứ theo chuẩn I2C, các đường SDA và SCL trên các thiết bị có cấu hình “cực góp mở” (open-drain hoặc open-collector, tham khảo các mạch số dùng transistor để hiểu thêm), nghĩa là cần có các “điện trở kéo lên” (pull-up resistor) cho các đường này. Ở trạng thái nghỉ (Idle), 2 chân SDA và SCL ở mức cao. Hình 1 mô tả một mô hình mạng TWI (I2C) cơ bản.

Hình 1. Mạng TWI (I2C) với nhiều thiết bị và 2 điện trở kéo lên cho SDA, SCL.

Page 130: asembly

          Tiếp theo chúng ta tìm hiểu một số khái niệm và đặc điểm của TWI. Các khái niệm và đặc điểm tôi đề cập dưới đây được dùng cho cả TWI và I2C, nếu có sự khác biệt tôi sẽ giải thích thêm.         Master: là chip khởi động quá trình truyền nhận, phát đi địa chỉ của thiết bị cần giao tiếp và tạo xung giữ nhịp trên đường SCL.        Slave: là chip có một địa chỉ cố định, được gọi bởi Master và phục vụ yêu cầu từ Master.        SDA- Serial Data: là đường dữ liệu nối tiếp, tất cả các thông tin về địa chỉ hay dữ liệu đều được truyền trên đường này theo thứ tự từng bit một. Chú ý là trong chuẩn I2C, bit có trọng số lớn nhất (MSB) được truyền trước nhất, đặc điểm này ngược lại với chuẩn UART.        SCL –Serial Clock: là đường giữ nhịp nối tiếp. TWI (I2C) là chuần truyền thông nối tiếp đồng bộ, cần có 1 đường tạo xung giữ nhịp cho quá trình truyền/nhận, cứ mỗi xung trên đường giữ nhịp SCL, một bit dữ liệu trên đường SDA sẽ được lấy mẫu (sample). Dữ liệu nối tiếp trên đường SDA được lấy mẫu khi đường SCL ở mức cao trong một chu kỳ giữ nhịp, vì thế đường SDA không được đổi trạng thái khi SCL ở mức cao (trừ START và STOP condition). Chân SDA có thể được đổi trạng thái khi SCL ở mức thấp.

 

        START Condition-Điều kiện bắt đầu: từ trạng thái nghỉ, khi cả SDA và SCL ở mức cao nếu Master muốn thực hiện một “cuộc gọi”, Master sẽ kéo chân SDA xuống thấp trong khi SCL vẫn cao. Trạng thái này gọi là START Condition (chúng ta gọi tắt là S).        STOP Condition-Điều kiện kết thúc: sau khi thực hiện truyền/nhận dữ liệu, nếu Master muốn kết thúc quá trình nó sẽ tạo ra một STOP condition. STOP condition được Master thực hiện bằng cách kéo chân SDA lên cao khi đường SCL đang ở mức cao. STOP condition chỉ được tạo ra sau khi địa chỉ hoặc dữ liệu đã được truyền/nhận.

Page 131: asembly

        REPEAT START – Bắt đầu lặp lại: khoảng giữa START và STOP condition là khoảng bận của đường truyền, các Master khác không tác động được vào đường truyền trong khoảng này. Trường hợp sau khi kết thúc truyền/nhận mà Master không gởi STOP condition lại gởi thêm 1 START condition gọi là REPEAT START. Khả năng này thường được dùng khi Master muốn lấy dữ liệu liên tiếp từ các Slaves. Hình bên dưới mô tả các Master tạo ra START, STOP và REPEAT START.

         Address Packet Format – Định dạng gói địa chỉ: trên mạng TWI (I2C), tất cả các thiết bị (chip) đều có thể là Master hay Slave. Mỗi thiết bị có một địa chỉ cố định gọi là Device address. Khi một Master muốn giao tiếp với một Slave nào đó, nó trước hết tạo ra một START condition và tiếp theo là gởi địa chỉ Device address của Slave cần giao tiếp trên đường truyền, vì thế xuất hiện khái niệm “gói địa chỉ” (Address Packet). Gói địa chỉ trong TWI (I2C) có định dạng 9 bits trong đó 7 bit đầu (gọi là SLA, được gởi liền sau START condition) chứa địa chỉ Slave, một bit READ/WRITE và một bit ACK-Ackknowledge (xác nhận).  Do bit địa chỉ có độ dài 7 bits nên về mặt lý thuyết, trên 1 mạng TWI (I2C) có thể tồn tại tối đa 2^7=128 thiết bị có địa chỉ riêng biệt. Tuy nhiên, có một số địa chỉ không được sử dụng như các địa chỉ có định dạng 1111xxx (tức các địa chỉ lớn hơn hoặc bằng 120 không được dùng). Riêng địa chỉ 0 được dùng cho “cuộc gọi chung” (General call). Bit READ/WRITE (R/W) được truyền tiếp sau 7 bit địa chỉ là bit báo cho Slave biết Master muốn “đọc” hay “ghi” vào Slave. Nếu bit này bằng 0 (gọi là W) thì quá trình “Ghi” dữ liệu từ Master đến Slave được yêu cầu, nếu bit này bằng 1 (gọi là R) thì Master muốn “đọc” dữ liệu từ Slave về. Tám bits trên (SLA+R/W) được Master phát ra sau khi phát START condition, nếu một Slave trên mạng nhận ra rằng địa chỉ mà Master yêu cầu trùng khớp với Device address của chính mình, nó sẽ “đáp trả” lại Master bằng cách phát ra 1 tín hiệu “xác nhận” ACK bằng cách kéo chân SDA xuống thấp trong xung thứ 9. Ngược lại, nếu không có Slave đáp ứng lại, chân SDA vẫn ở mức cao trong xung giữ nhịp thứ 9 thì gọi là tín hiệu “không xác nhận” – NOT ACK, lúc này Master cần có những ứng xử phù hợp tùy theo mỗi

Page 132: asembly

trường hợp cụ thể, ví dụ Master có thể gởi STOP condition và sau đó phát lại địa chỉ Slave khác…Như vậy, trong 9 bit của gói địa chỉ thì chỉ có 8 bit được gởi bởi Master, bit còn lại là do Slave. Ví dụ Master muốn yêu cầu “đọc” dữ liệu từ Slave có địa chỉ 43,  nó cần phát đi một byte như sau trên đường truyền: (43<<1)+1, trong đó (43<<1) là dịch số 43 về bên trái 1 vị trí vì 7 bit địa chỉ nằm ở các vị trí cao trong gói địa chỉ, sau đó cộng giá trị này với “1” tức là quá trình “đọc” được yêu cầu.

 

         General call – Cuộc gọi chung: khi Master phát đi gói địa chỉ có dạng 0 (thực chất là 0+W) tức nó muốn thực hiện một cuộc gọi chung đến tất cả các Slave. Tất nhiên, cho phép hay không cho phép cuộc gọi chung là do Slave quyết định. Nếu các Slave được cài đặt cho phép cuộc gọi chung, chúng sẽ đáp lại Master bằng ACK. Cuộc gọi chung thường xảy ra khi Master muốn gởi dữ liệu chung đến các Slaves. Chú ý là cuộc gọi chung có dạng 0+R là vô nghĩa vì không thể có chuyện Master nhận dữ liệu từ tất cả các Slave cùng thời điểm.         Data Packet Format – Định dạng gói dữ liệu: sau khi địa chỉ đã được phát đi, Slave đã đáp lại Master bằng ACK thì quá trình truyền/nhận dữ liệu sẽ diễn ra giữa cặp Master/Slave này. Tùy vào bit R/W trong gói địa chỉ, dữ liệu có thể được truyền theo hướng từ Master đến Slave hay từ Slave đến Master. Dù di chuyển theo hướng nào, gói dữ liệu luôn bao gồm 9 bits trong đó 8 bits đầu là dữ liệu và 1 bit cuối là bit ACK. Tám bits dữ liệu do thiết bị phát gởi và bit ACK do thiết bị nhận tạo ra. Ví dụ khi Master thực hiện quá trình gởi dữ liệu đến Slave, nó sẽ phát ra 8 bits dữ liệu, Slave nhận và phát lại ACK (kéo SDA xuống 0 ở xung thứ 9), sau đó Master sẽ quyết định gợi tiếp byte dữ liệu khác hay không. Nếu Slave phát tín hiệu NOT ACK (không tác động SDA ở xung thứ 9) sau khi nhận dữ liệu thì Master sẽ kết thúc quá trình gởi bằng cách phát đi STOP condition. Hình bên dưới mô tả định dạng gói dữ liệu trong TWI (I2C).

Page 133: asembly

 

       Phối hợp gói địa chỉ và dữ liệu: một quá trình truyền/nhận TWI (I2C) thường được bắt đầu từ Master, Master phát đi một START condition sau đó gởi gói địa chỉ SLA+R/W trên đường truyền. Tiếp theo nếu có một Slave đáp ứng lại, dữ liệu có thể truyền/nhận liên tiếp trên đường truyền (1 hoặc nhiều byte liên tiếp). Khung truyền thông thường được mô tả như hình bên dưới.

      Multi-Master Bus –Đường truyền đa chip chủ: như đã trình bày ở trên, TWI (I2C) là chuẩn truyền thông đa chip chủ, nghĩa là tại một thời điểm có thể có nhiều hơn 1 chip làm Master nếu các chip này phát ra START condition cùng lúc. Nếu các Master có cùng yêu cầu và thao tác đối với Slave thì chúng có thể “cùng tồn tại” và quá trình truyền/nhận có thể thành công. Tuy nhiên, trong đa số trường hợp sẽ có một số Master bị “thất lạc” (lost). Một Master bị lost khi nó truyền/nhận 1 mức cao trên SDA trong khi các Master khác truyền/nhận 1 mức thấp. Truyền thông đa chip chủ tương đối phức tạp và vì thế tôi sẽ không đề cập trường hợp này trong lúc thực hiện ví dụ giao tiếp trong bài học này.      Nắm được các khái niệm và đặc điểm trên của truyền thông TWI (I2C) là bạn đã sẵn sàng để điều khiển module TWI trên AVR. Phần tiếp theo tôi sẽ hướng dẫn cách thao tác module TWI trên AVR thông qua một ví dụ cụ thể.

III. TWI trên AVR.

      1.   Thanh ghi:

Page 134: asembly

      TWI trên AVR được vận hành bởi 5 thanh ghi bao gồm thanh ghi tốc độ giữ nhịp TWBR, thanh ghi điều khiển TWCR , thanh ghi trạng thái TWSR, thanh ghi địa chỉ TWAR và thanh ghi dữ liệu TWDR.

      - TWBR (TWI Bit Rate Register): là 1 thanh ghi 8 bit quy định tốc độ phát xung giữ nhịp trên đường SCL của chip Master.

      

       Tốc độ phát xung giữ nhịp được tính theo công thức:

       Trong đó CPU Clock frequency là tần số hoạt động chính của AVR, TWBR là giá trị thanh thi TWBR và TWPS là giá trị của 2 bits TWPS1 và TWPS0 nằm trong thanh thi trạng thái TWSR. Hai bits này được gọi là bit prescaler, thông thường người ta hay set TWPS1:0 =00 để chọn Prescaler là 1 (40=1). Bảng 1 tóm tắt tốc độ xung giữ nhịp tạo ra trên SCL đối với các giá trị của tham số:Bảng 1. Tốc độ xung giữ nhịp tham khảo.

       

Page 135: asembly

       - TWCR (TWI Control Register): là thanh ghi 8 bit điều khiển hoạt động của TWI.

         

Bit 7- TWINT (TWI Interrupt Flag): là một cờ báo rất quan trọng. TWINT được tự động set lên 1 khi TWI kết thúc một quá trình bất kỳ nào đó (như phát/nhận START, phát nhận địa chỉ…). Chú ý là bit này không tự động được xóa bởi phần cứng như các cờ báo trong các module khác. Vì thế, khi lập trình điều khiển TWI chúng ta luôn phải xóa TWINT trước khi muốn thực hiện một quá trình nào đó. Một điểm quan trọng cần lưu ý là bit TWINT được xóa khi chúng ta viết giá trị 1 vào nó. Trong khi lập trình cho TWI, chúng ta thường xóa TWINT bằng cách viết 1 vào nó, sau đó liên tục kiểm tra TWINT, nếu bit này được set lên 1 thì quá trình đã hoàn thành.

Bit 6 – TWEA (TWI Enable Acknowledge Bit): tạm hiểu là bit kích hoạt tín hiệu xác nhận. Đối với  chip Slave, nếu bit này được set thì tín hiệu xác ACK sẽ được gởi trong các trường hợp sau: địa chỉ do Master phát ra trùng khớp với địa chỉ của Slave; một cuộc gọi chung đang xảy ra và Slave này cho phép cuộc gọi chung; dữ liệu đã được Slave nhận từ Master. Như thế, khi set một chip ở chế độ Slave, chúng ta cần set bit này để nó có thể đáp ứng lại Master bất cứ khi nào được gọi. Đối với chip Master, tín hiệu ACK chỉ được phát trong 1 trường hợp duy nhất đó là khi Master nhận dữ liệu từ Slave, Master phát ACK để báo cho Slave là mình đã nhận được và muốn tiếp tục nhận từ Slave.

Bit 5 – TWSTA (TWI START Condition Bit): là bit tạo START condition. Khi một chip muốn trở thành Master để thực hiện 1 cuộc gọi, bit này cần được set và một START condition được tạo ra trên đường truyền nếu đường truyền đang rảnh. Nếu đường truyền không rảnh, TWI sẽ chờ cho đến khi nó rảnh (nhận ra 1 STOP condition) và tiếp tục gởi START condition. Chú là là bit nay cần được xóa bởi phần mềm sau khi START condition đã được gởi (viết 0 vào bit này để xóa nó).

Bit 4 – TWSTO (TWI STOP Condition Bit): là bit tạo STOP condition cho TWI. Khi Master muốn kết thúc một cuộc gọi, nó sẽ phát STOP condition bằng cách viết giá trị 1 vào bit TWSTO. Slave cũng có thể tác động vào bit này, nếu một cuộc gọi bị lỗi, viết 1 vào TWSTO trên Slave sẽ reset đường truyền về trạng thái rảnh ban đầu.

Bit 3 – TWWC (TWI Write Collision Flag): khi cờ TWINT đang ở mức thấp tức TWI đang bận, nếu chúng ta viết dữ liệu vào thanh ghi dữ liệu (TWDR) thì một lỗi xảy ra, khi đó bit TWWC tự động được set lên 1. Vì thế, trong quá trình truyền dữ

Page 136: asembly

liệu, bit TWINT cần được giữ mức cao khi ghi dữ liệu vào thanh ghi TWDR và sau đó xóa khi dữ liệu đã sẵn sàng.

Bit 2 – TWEN (TWI Enable Bit): bit kích hoạt TWI trên AVR, khi TWEN được set lên 1, TWI sẵn sàng hoạt động.

Bit 1 – Reserve: không sử dụng. Bit 0 – TWIE (TWI Interrupt Enable Bit): bit cho phép ngắt TWI, khi bit nay được

set bằng 1 đồng thời bit I trong thanh ghi trạng thái chung được set, một ngắt TWI xảy ra khi bit TWINT được set bởi phần cứng. Ngắt TWI có thể xảy ra  sau bất kỳ hoạt động nào liên quan đến TWI.  Do đó cần sử dụng ngắt hợp lý. Thông thường, ngắt chỉ được sử dụng cho Slave, đối với Master ngắt không cần thiết vì Master chủ động khởi động một cuộc gọi. 

       Một điều cần chú ý là các bit trong thanh ghi TWCR không cần được set cùng lúc, tùy vào từng giai đoạn trong quá trình giao tiếp TWI các bit có thể được set riêng lẻ.    

      - TWSR (TWI Status Register): là 1 thanh ghi 8 bit trong đó có 5 bit chứa code trạng thái của TWI  và 2 bit chọn prescaler.

      

         Có rất nhiều bước, nhiều tình huống xảy ra khi giao tiếp bằng TWI cho cả Master và Slave. Ứng với mỗi trường hợp TWI sẽ tạo ra 1 code trong thanh ghi TWSR . Lập trình cho TWI cần xét code trong 5 bit cao của thanh ghi TWSR và đưa ra các ứng xử hợp lý ứng với từng code.

        - TWDR (TWI Data Register): là thanh ghi dữ liệu chính của TWI. Trong quá trình nhận, dữ liệu nhận về sẽ được lưu trong TWDR. Trong quá trình gởi, dữ liệu chứa trong TWDR sẽ được chuyển ra đường SDA.

        - TWAR (TWI Address Register): là thanh ghi chứa device address của chip Slave. Cấu trúc thanh ghi được trình bày trong hình dưới.

       

       Nhớ lại địa chỉ Slave được tạo thành từ 7 bits, trên thanh ghi TWAR 7 bits địa chỉ này nằm ở 7 vị trí cao. Trước khi sử dụng TWI như Slave, chúng ta phải gán địa chỉ cho chip, việc viết địa chỉ thường được thực hiện bằng lệnh TWAR = (Device_address<<1)+TWGCE. Trong đó TWGCE (TWI General Call Enable) là

Page 137: asembly

bit cho phép cuộc gọi chung. Như tôi đề cập bên trên, Slave co quyền cho phép Master thực hiện cuộc gọi chung với nó hay không. Nếu TWGCE=1, Slave  sẽ đáp ứng lại cuộc gọi chung nếu có, nếu TWGCE=0 thì Slave sẽ bỏ qua cuộc gọi chung. 

         2.   Hoạt động của TWI:

         TWI trên  AVR được gọi là byte-oriented (tạm dịch là hướng byte) và interrupt-based (dựa trên ngắt). Bất kỳ một sự kiện nào trong quá trình truyền/nhận TWI cũng có thể gây ra 1 ngắt TWI. TWI trên AVR vì thế hoạt động tương đối độc lập với chip. Tuy nhiên, cần khai thác ngắt trên AVR một cách hơp lý. Ví dụ, đối với Master, chúng ta không cần sử dụng ngắt vì chip này hoàn toàn chủ động trong việc truyền và nhận. Riêng với Slave, sử dụng ngắt để tránh  bỏ lỡ các cuộc gọi là cần thiết.Tất cả các AVR trên mạng TWI đều có thể là Master hay Slave, cả Master và Slave đều có thể truyền và nhận dữ liệu. Vì thế, có tất cả 4 mode trong hoạt động của TWI trên AVR. Chúng ta sẽ lần lượt khảo sát các mode này như sau: Master Transmitter (chip chủ truyền), Master Receiver (Chip chủ nhận), Slave Reicever (chip tớ nhận) và Slave Transmitter (Chip tớ truyền).Trước khi khảo sát các chế độ hoạt động của TWI chúng ta qui ước một số ký hiệu thường dùng (đây cũng là các ký hiệu dùng trong datasheet của các chip AVR).

S:                START condition – điều kiện bắt đầuRs:              REPEAT START – bắt đầu lặp lạiR:                READ Bit, bit này bằng 1 được gởi kèm với gói địa chỉW:              WRITE Bit, bit này mang giá trị 0, gởi kèm gói địa chỉACK:          Ackowledge, bit xác nhận, chân SDA được kéo xuống 0 ở xung thứ 9NACK:        Not Acknowledge, không xác nhận, SDA ở mức cao ở bit thứ 9Data:           8 bits dữ liệuP:               STOP condition – điều kiện kết thúc.SLA:          Slave address, địa chỉ của Slave cần giao tiếp.

         A. Master Transmitter mode – Master truyền dữ liệu:

        Trong chế độ này, Master truyền 1 hoặc một số byte dữ liệu đến một hoặc các Slave. Để bắt đầu, Master tạo ra một START condition trên đường SDA, nếu đường truyền đang rảnh, Master sẽ tiếp tục phát đi địa chỉ của Slave cần giao tiếp cùng với bit W (ghi) theo định dạng như sau: SLA+W. Nếu Slave đáp lại bằng một ACK trong xung giữ nhịp thứ 9, Master sẽ tiếp tục gởi 1 hoặc liên tiếp các byte dữ liệu trên SDA. Cứ sau mỗi byte dữ liệu, Master sẽ kiểm tra ACK từ Slave. Nếu Slave gởi một NACK hoặc Master không muốn gởi thêm dữ liệu đến Slave nó sẽ

Page 138: asembly

phát đi một STOP condition hoặc một REPEAT START (Rs). Nếu STOP được phát, cuộc gọi kết thúc, nếu Rs được phát, một cuộc gọi mới bắt đầu, sau Rs là địa chỉ của Slave mới…Đó là về mặt lý thuyết, trên thực tế làm sao để kiểm tra môt START condition có được gởi chưa? làm sao biết có nhận được ACK sau khi phát địa chỉ hoặc dữ liệu? Tất cả được TWI mã hóa thành các code chứa trong thanh ghi TWSR (chỉ 5 bit cao). Chúng ta chỉ thanh ghi này và đối chiếu với bảng code quy định sẵn để biết trạng thái đường truyền và đưa ra quyết định tiếp theo. Hình 2 mô tả một quá trình Master truyền dữ liệu, các khả năng có thể xảy ra và giá trị tương ứng của thanh ghi TWSR. Ý nghĩa các code trong thanh ghi TWSR trong lúc Master truyền dữ liệu có thể tham khảo thêm datasheet của chip.

Page 139: asembly

Hình 2. Master truyền dữ liệu.

Page 140: asembly

        Từ hình 2, chúng ta nhận thấy khi Master truyền dữ liệu, dãy code 0x08 -> 0x18 -> 0x28 ->… -> 0x28 (-> 0x30) là dãy code thành công nhất. Code 0x08 báo rằng START codition được truyền thành công, code 0x18 báo địa chỉ truyền thành công và đã có Slave xác nhận bằng ACK, code 0x28 tức dữ liệu được Master truyền thành công và Slave đã nhận được, báo ACK lại cho Master, code 0x30 tức dữ liệu đã được truyền nhưng Slave không xác nhận lại, lúc này Master có thể phát đi một STOP codition sau code 0x30. Ngoài ra còn một số code khác tương ứng với các trường hợp khác như gởi địa chỉ thất bại (code 0x20), Master bị lost (code 0x38)…Đối với mỗi loại ứng dụng, cách “hành xử” sẽ khác nhau đối với các trường hợp thất bại này. Trong bài này, tôi sẽ bỏ qua tất cả các trường hợp thất bại, nếu một trong các code thất bại xảy ra chúng ta sẽ thoát khỏi cuộc gọi và đưa đường truyền về trạng thái nghỉ.

       B. Master Receiver mode – Master nhận dữ liệu: 

      Trong chế độ này, Master nhận một hoặc một số byte dữ liệu từ một Slave. Để bắt đầu, Master tạo ra một START condition trên đường SDA, nếu đường truyền đang rảnh, Master sẽ tiếp tục phát đi địa chỉ của Slave cần giao tiếp cùng với bit R (đọc) theo định dạng như sau: SLA+R. Nếu Slave đáp lại bằng một ACK trong xung giữ nhịp thứ 9, Master sẽ bắt đầu sample dữ liệu trên SDA. Cứ sau mỗi byte dữ liệu, nếu Master muốn nhận tiếp byte khác nó phải phát ra 1 ACK ở xung thứ 9 báo cho Slave. Khi Master muốn kết thúc quá trình nhận nó sẽ phát một NOT ACK sau khi nhận dữ liệu, liền sau đó Master phát STOP để kết thúc cuộc gọi hoặc phát đi một REPEAT START nếu nó muốn tiếp tục gọi các Slaves khác. Hình 3 mô tả một quá trình Master nhận dữ liệu, các khả năng có thể xảy ra và giá trị code tương ứng của thanh ghi TWSR. Ý nghĩa các code trong thanh ghi TWSR trong lúc Master truyền dữ liệu có thể tham khảo thêm datasheet của chip.

Page 141: asembly

Hình 3. Master nhận dữ liệu.

       Từ hình 3, trong quá trình  Master nhận dữ liệu, dãy code 0x08 -> 0x40 -> 0x50 ->… -> 0x58 là dãy code thành công nhất. Code 0x08 báo rằng START codition được truyền thành công, code 0x40 báo địa chỉ + R đã được truyền thành công và đã có Slave xác nhận bằng ACK, code 0x50 báo dữ liệu được Master nhận thành công và Master cũng đã phát một ACK bit sau khi nhận, code 0x58 xảy ra khi Master nhận dữ liệu thành công nhưng nó không phát ACK mà phát NOT ACK, báo cho Slave rằng Master không muốn nhận thêm dữ liệu, tiếp theo Master

Page 142: asembly

sẽ phát một STOP condition hoặc một REPEAT START. Các trường hợp khác chúng ta không khảo sát.

      C. Slave Receiver mode – Slave nhận dữ liệu: 

       Hình 4 mô tả một quá trình Slave nhận dữ liệu, các khả năng có thể xảy ra và giá trị code tương ứng của thanh ghi TWSR. Chế độ Slave nhận dữ liệu xảy ra khi Master thực hiện một cuộc gọi phát dữ liệu (SLA+W). Như quan sát trong hình 4, Slave chỉ nhận ra cuộc gọi này khi địa chỉ của nó trùng với địa chỉ của Master (Own address mode) hoặc khi Master thực hiện một cuộc gọi chung. Khi đó, bit TWINT của Slave sẽ được set lên 1. Nếu Slave cho phép ngắt TWI (bit TWIE trong thanh ghi TWCR được set từ lúc đầu) thì một ngắt xảy ra báo có một sự kiện TWI. Nếu code trong thanh ghi TWSR là 0x60 thì một cuộc gọi địa chỉ riêng được yêu cầu và Slave cũng đã đáp ứng lại Master bằng một ACK, Slave sau đó bắt đầu nhận dữ liệu từ đường SDA. Cứ sau một byte dữ liệu Slave phải xác nhận một ACK nếu nó còn muốn tiếp tục nhận. Nếu vì một lý do nào đó mà Slave không thể tiếp tục nhận nó có thể phát một NOT ACK sau một byte dữ liệu. Cuộc gọi kết thúc khi Slave nhận được STOP condition, tương ứng code 0xA0. Cuộc gọi chung cũng diễn ra hoàn toàn tương tự cuộc gọi địa chỉ riêng nhưng code có giá trị khác. Khi viết chương trình cho Slave trong chế độ nhận dữ liệu, chúng ta cần xét cả 2 trường hợp cuộc gọi địa chỉ riêng và cuộc gọi chung.

Page 143: asembly

Hình 4. Slave nhận dữ liệu.

      D. Slave Transmitter mode – Slave truyền dữ liệu: 

      Đây là chế độ cuối cùng trong 4 chế độ của AVR TWI. Hình 5 mô tả một quá trình Slave truyền dữ liệu, các khả năng có thể xảy ra và giá trị code tương ứng của thanh ghi TWSR. Chế độ Slave phát dữ liệu xảy ra khi Master muốn nhận dữ liệu

Page 144: asembly

từ Slave, Master thực hiện một cuộc gọi nhận dữ liệu (SLA+R). Như quan sát trong hình 5, Slave chỉ nhận ra cuộc gọi này khi địa chỉ của nó trùng với địa chỉ của Master (Own address mode). Khi đó, bit TWINT của Slave sẽ được set lên 1. Nếu Slave đáp lại bằng một ACK ở xung nhịp thứ 9, code trong thanh ghi TWSR sẽ là 0xA8, Slave sau đó bắt đầu phát dữ liệu lên đường SDA. Cứ sau mỗi byte dữ liệu, Master sẽ xác nhận một ACK nếu nó còn muốn tiếp tục nhận, code 0xB8 sẽ xuất hiện trong trường hợp này. Nếu Master không muốn tiếp tục nhận dữ liệu từ Slave, một NOT ACK sẽ được phát và code 0xC0 xuất hiện, Slave kết thúc quá trình phát dữ liệu. Một trường hợp đặc biệt khi bit TWEA (bit ACK) trong thanh ghi TWCR của Slave được reset về 0 trước khi Slave truyền dữ liệu, trường hợp Slave muốn báo rằng nó đã hết dữ liệu để truyền, byte tiếp theo cũng là byte cuối cùng. Sau khi Master nhận byte này, nó có thể xác nhận 1 ACK cho Slave (vì thật ra Master không hề biết Slave đang truyền byte cuối), code trên Slave trong trường hợp này là 0xC8 và Slave sẽ tự hết thúc quá trình truyền mà không cần chờ Master. Khi lập trình cho Slave trong chế độ phát, cần phải có sự “thỏa hiệp” với Master trước để tránh code 0xC8 vì code này không có nhiều ý nghĩa.

 

Hình 5. Slave truyền dữ liệu.

Page 145: asembly

        Kỹ thuật chính dùng cho Master khi truyền hay nhận cuộc gọi là hỏi vòng và chờ (polling and waiting). Ứng với mỗi code nhận về từ thanh ghi TWSR (hay ứng với mỗi trạng thái của cuộc gọi) mà Master set các bit tương ứng trong thanh ghi điều khiển TWCR và sau đó chờ bit TWINT được set (quá trình kết thúc) để tiếp tục đọc và xét code TWSR. Quá trình chờ và xét này lặp lại cho đến khi Master kết thúc cuộc gọi bằng STOP condition. Tuy nhiên Slave thì khác, Slave không chủ động thực hiện cuộc gọi mà nó phải chờ yêu cầu từ Master để phục vụ. Vì thế, nếu dùng “hỏi vòng” cho Slave thì sẽ tốn thời gian chờ vô ích và đôi khi còn bỏ lỡ các cuộc gọi. Đối  với Slave, ngắt là phương pháp bắt cuộc gọi tối ưu nhất. Trong bài học này, việc truyền và nhận của Slave sẽ được thực hiện trong các trình phục vụ ngắt TWI.

 IV. Điều khiển AVR TWI.

        Phần này tôi hướng dẫn lập trình điều khiển module TWI AVR bằng WinAVR. Các hình 2, 3, 4 và 5 cần được tham khảo kèm kỹ vì code trong phần này được phát triển từ các hình này. Để đơn giản, chúng ta sẽ viết các hàm giao tiếp TWI trong 1 file riêng gọi là “myTWI.h”, đây có thể coi là thư viện cho TWI dùng trong trang web này. Như đã trình bày, chuẩn I2C thì duy nhất nhưng cách sắp xếp dữ liệu của các chip I2C thì rất đa dạng. Vì thế, khi muốn giao tiếp với một chip I2C nào bạn nhất thiết phải đọc datasheet của chip đó để hiểu định dạng dữ liệu. Các hàm trong thư viện myTWI chỉ phục vụ giao tiếp giữa các AVR với nhau, nếu muốn sử dụng chúng giao chip với một chip EEPROM 24C1004 chẳng hạn, bạn phải viết thêm các hàm mở rộng khác dựa trên các hàm này.Nội dung file myTWI.h được chia thành 3 phần, phần đầu là các định nghĩa biến, tham số chung, phần 2 gồm các hàm truyền/nhận cho Master và phần 3 là trình phục vụ ngắt TWI cho Slave. List 1 trình bày các định nghĩa chung trong file “myTWI.h”.List 1. Định nghĩa chung.

Page 146: asembly
Page 147: asembly

       Phần này chủ yếu định nghĩa các code trạng thái trong quá trình thao tác TWI trên AVR mà chúng ta đã biết khi khảo sát các chế độ hoạt động của TWI. Thật ra bạn có thể tham khảo các hình 2-5 và các bảng code trong datasheet của AVR và sử dụng các code trạng thái trực tiếp trong lúc lập trình, tôi định nghĩa như trên chỉ để tiện theo dõi trong lúc lập trình. Các dòng từ 12 đến 25 định nghĩa các code trạng thái cho Slave (cả truyền và  nhận). Chúng ta cũng định nghĩa một số biến toàn cục dùng cho Slave, biến SLAVE_wData[100] là một mảng 100 phần tử dùng chứa dữ liệu mà Slave sẽ truyền, biến Tran_Num là chỉ số của phần tử trong mảng SLAVE_wData sẽ được truyền đi. Biến SLAVE_buff[100] là dữ liệu nhận về từ TWI và Rec_Num là chỉ số của dữ liệu sau cùng do TWI nhận về (dữ liệu SLAVE_buff[Rec_Num]). Biến Device_Addr chứa địa chỉ mà khi là Slave của chính AVR chúng ta đang lập trình. Tương tự, các dòng từ 47 đến 57 định nghĩa code trạng thái cho Master mode. Trước đó, chúng ta cũng định nghĩa các giá trị tốc độ phát xung giữ nhịp sẽ gán cho thanh ghi TWBR (dòng 37, 38). Hai biến TWI_R và TWI_W đại diện cho 2 bit R/W được truyền trong gói địa chỉ (báo cho Slave biết Master muốn truyền hay nhận dữ liệu). Một số macro trong các dòng  42 đến 45 bao gồm START, STOP condition và xóa bit TWINT bằng cách gán các giá trị tương ứng cho thanh ghi điều khiển TWI.        Cuối cùng là chương trình con void TWI_Init(void) khởi động TWI. Quá trình khởi động bao gồm set tốc độ xung giữ nhịp cho Master (dòng 61, 62), gán địa chỉ device (dòng 63) và xác lập TWI sẵn sàng ở chế độ Slave. Xem lại thanh ghi TWAR, do 7 bit địa chỉ nằm ở vị trí cao nên chúng ta cần phải dịch trái địa chỉ 1 vị trí trước khi gán cho TWAR (Device_Addr <<1), đồng thời set bit 0 trong TWAR để cho phép nhận cuộc gọi chung khi được yêu cầu. Dòng 64 khởi động TWI với bit ACK sẵn sàng và cho phép xảy ra ngắt TWI. Như thế, sau khi khởi động TWI sẵn sàng ở chế độ Slave.

List 2. Code cho Master.

Page 148: asembly
Page 149: asembly

        Hàm TWI_Master_Send_array(uint8_t Addr, uint8_t Data[], uint8_t len) thực hiện truyền 1 dãy các byte dữ liệu trong mode Master. Tham số Addr là địa chỉ của Slave cần giao tiếp, Data[] là mảng dữ liệu và len là chiều dài (số byte) của dữ liệu cần truyền. Việc đầu tiên khi chúng ta vào Master mode là “tắt” ngắt TWI bằng cách xóa bit  TWIE (dòng 3). Trình tự Master truyền dữ liệu hoàn toàn tương tự trình tự trong hình 2. Dòng 5, TWCR=TWI_START, Master bắt đầu phát 1 START condition. Nếu xem lại định nghĩa của macro TWI_START trong list 1 bạn sẽ thấy dòng TWCR=TWI_START tương đương TWCR=(1<<TWINT)|(1<<TWSTA)|(1<<TWEN) tức chúng ta thực hiện xóa bit TWINT (bit này phải luôn được xóa trước khi muốn thực hiện viêc gì) bằng cách ghi 1 vào TWINT, set bit START (bit TWSTA) và cho phép TWI hoạt động bằng bit TWEN. Dòng code 6 chờ cho đến khi bit TWINT được phần cứng set lên 1 (kết thúc), sau đó chúng ta kiểm tra code trong thanh ghi trạng thái TWSR. Chú ý là chỉ có 5 bit cao trong thanh ghi TWSR chứa trạng thái nên chúng ta cần dùng giải thuật mặt nạ che các bit thấp lại, TWSR & 0xF8 chính là cách để che 3 bit thấp của TWSR. So sánh code đọc được với code tương ứng trong hình 1, trong trường hợp này chúng ta so sánh với _START_Sent, chính là so sánh với 0x80 (xem lại định nghĩa của _START_Sent trong list 1). Nếu các code không trùng nhau, một lỗi truyền xảy ra và chúng ta sẽ thoát khỏi chương trình truyền, giá trị trả về chính là code có lỗi (xem dòng code 7). Các dòng code từ 10 đến 13 thực hiện truyền địa chỉ + W, chú ý trong lúc phát, dữ liệu cần phát phải được ghi sẵn vao thanh ghi dữ liệu  TWDR trước khi xóa bit TWINT (dòng 10 và 11). Sau khi truyền địa chỉ chúng ta truyền mảng dữ liệu liên tiếp và cuối cùng là phát STOP condition, TWCR=TWI_STOP tương đương TWCR=(1<<TWINT)|(1<<TWSTO)|(1<<TWEN). Cần khởi động lại TWI để đưa nó về chế độ Slave trước khi thoát khỏi chương trình con truyền dữ liệu của chế độ Master (dòng 24).        Hàm TWI_Master_Read_array(uint8_t Addr, uint8_t Data[], uint8_t len) thực hiện nhận dữ liệu về Master. Cách giải thích cho hàm này không khác nhiều so với hàm đọc dữ liệu nên bạn đọc tự tìm hiểu. Một điểm cần chú ý là khi nhận 1 dãy byte chúng ta nên đọc n-1 byte đầu bình thường, có trả ACK cho Slave và byte cuối cùng sẽ được nhận riêng, trả NOT ACK để báo cho Slave rằng Master không muốn nhận thêm(đoạn code từ dòng 55 đến 59 dùng đọc byte cuối cùng).

List 3. Code cho Slave.

Page 150: asembly
Page 151: asembly

        Như tôi đã trình bày, toàn bộ quá trình truyền và nhận của Slave được thưc hiện trong chương trình phục vụ ngắt TWI. Khi ngắt TWI xảy ra, trình phục vụ ngắt sẽ đọc và kiểm tra code trong thanh ghi TWSR để thực hiện các công việc phù hợp. Bạn đọc lại tham khảo thêm hình 4 và hình 5 cùng với các code trong những “case” tương ứng của List 3 để hiểu đoạn chương trình này. Điểm lưu ý lớn nhất mà tôi muốn nói là các biến được dùng cho chế độ Slave truyền và nhận. Tôi dùng 2 mảng SLAVE_wData và SLAVE_buff để chứa biến truyền và nhân. Hai biến Tran_Num và Rec_Num là chỉ số của byte hiện hành. Vì thế SLAVE_wData[Tran_Num] chính là byte tiếp theo sẽ được truyền đi nếu Slave được yêu cầu truyền, và SLAVE_buff[Rec_Num] là byte cuối cùng mà Slave nhận về trong chế độ Slave nhận dữ liệu. Hãy khái thác các biến này trong các chương trình ứng dụng.         Để minh họa cho các sử dụng các hàm trong thư viện myTWI, tôi thực hiện một mạch điện mô phỏng mạng TWI gồm 3 chip ATmega32. Chip thứ nhất là Master, 2 chip còn lại là Slaves. Tôi tạo 2 Project, một cho Master và một cho 2 Slaves dùng chung. PORTD được set input có điện trở kéo lên. Tôi dùng 2 chân PD6 và PD7 để chọn địa chỉ cho 2 Slaves, Slave thứ nhất tôi nối chân PD6 xuống GND, do đó chip này có địa chỉ Device_Addr là  PD7:PD6=10=2 (thập phân). Slave còn lại tôi để 2 chân PD6 và PD7 trống nên địa chỉ của nó là PD7:PD6=11=3. Trong chương trình của Slave có phần đọc 2 chân PD6:PD7 và gán cho biến Device_Addr mà chúng ta đã khai báo trong List 1, như vậy có thể dùng các này để set địa chỉ cho Slaves mà chúng ta gọi là “set địa chỉ cứng”. Trên chip Master, một swich được nối với chân PD0 để chọn Slave cần giao tiếp, nếu switch đóng thì SLAVE có địa chỉ 2 được chọn, nếu switch mở thì SLAVE có địa chỉ 3 được chọn để giao tiếp. Một nút nhấn được nối với ngắt INT0 của chip Master, khi nhấn nút này chương trình còn đọc dữ liệu từ Slave được gọi, tùy theo switch đóng hay mở mà Slave tương ứng được gọi để gởi dữ liệu cho Master. Dữ liệu nhận về sẽ hiển thị trên 1 Character LCD. Hình 6 là sơ đồ mạch điện mô phỏng bằng phần mềm Proteus và List 4, List 5 lần lượt trình bày đoạn code cho chương trình chính của Master và Slave.

Page 152: asembly

Hình 6. Demo TWI.

List 4. Chương trình chính cho Master.

Page 153: asembly
Page 154: asembly

        Ví dụ của Master minh họa cách dùng 2 hàm Master truyền và nhận mảng dữ liệu.  Ở dòng 27 tôi dùng hàm TWI_Master_Send_array để gởi 40 phần tử của mảng Data đến Slave có địa chỉ 2, TWI_Master_Send_array(2,Data,40). Tương tự, dòng 31 gởi 50 phần tử của mảng Data đến Slave có địa chỉ 3. Khi button trên mạch mô phỏng được nhấn, ngắt INT0 xảy ra, trong trình phục vụ ngắt INT0 chúng ta dùng hàm TWI_Master_Read_array để đọc dữ liệu từ một trong 2 Slaves, xem dòng code 43: TWI_Master_Read_array(Slave_Addr,rData,1). Địa chỉ của Slave cần đọc sẽ do switch nối với chân PD0 quyết định (xem dòng 42). Địa chỉ của Slave đang giao tiếp sẽ hiển thị trên dòng 1 của LCD, dữ liệu được hiển thị trên dòng 2.

List 5. Chương trình chính cho Slaves.

Page 155: asembly
Page 156: asembly

       Chương trình demo của Slaves minh họa các chế độ Slave truyền và nhận dữ liệu. Tuy nhiên do các quá trình truyền và nhận dữ liệu của Slave được thực hiện trong trình phục vụ ngắt TWI được viết sẵn trong file myTWI.h. trong chương trình chính của Slave chúng ta không cần phải gọi bất kỳ hàm nào trong myTWI. Công việc cần làm trong chương trình demo cho Slave là khởi động TWI sau đó gán giá trị cho các biến toàn cục của Slave (dòng 21 gán giá trị cho mảng SLAVE_wData).      Tôi có đính kèm ví dụ demo cho TWI, tôi thực hiện 2 Projetc trong 2 thư mục: TWI1 cho AMster và TWI2 cho Slave. Để chạy demo, chạy file TWI bằng Proteus, dùng switch SW1 để chọn Slave cần giao tiếp, nhấn button để nhận dữ liệu từ Slave. Thay đổi vị trí switch và kiểm tra kết quả.

Ma trận LED

1 2 3 4 5

 ( 101 Votes )Nội dung Các bài cần tham khảo trước

1. Ma trận LED .

2. AVR và ma trận LED.

Download ví dụ

Cấu trúc AVR .

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

 I. Ma trận LED. 

       Ma trận LED tức Dot Matrix LED là tập hợp nhiều đèn LED được bố trí thành dạng “ma trận” hình chữ nhật hoặc vuông với số hàng là a và số cột là b. Ma trận LED được dùng rất nhiều trong các ứng dụng hiển thị như các biển quảng cáo, hiển thị thay thế LCD hoặc thậm chí dùng hiển thị video…Để giảm số lượng các đường điều khiển, trong các ma trận LED các LED được nối chung với nhau theo hàng và cột. Số lượng LED trên ma trận LED là axb trong khi số lượng ngõ ra bằng tổng số

Page 157: asembly

hàng và cột: a + b. Việc điều khiển 1 ma trận LED kích thước lớn đòi hỏi thiết kế một mạch driver và điều khiển rất phức tạp. Với mục đích giúp bạn đọc làm quen khái niệm ma trận LED, trong phạm vi bài này tôi chỉ trình bày thao tác với 1 ma trận LED có kích thước 7x5 (7 hàng, 5 cột). ma trận LED 7x5 thường được dùng để hiển thị các ký tự trong bảng mã ASCII thay cho Text LCD. Tuy nhiên, bạn có thể ghép các ma trận LED này lại để hiển thị các loại hình ảnh bất kỳ có độ phân giải thấp. Hình 1 mô tả một cấu trúc của một ma trận LCD 7x5 với 12 ngõ ra được đặt tên từ C0…C4 và D0…D6 (C đại diện cho Control line và D là Data line).

Hình 1. Ma trận LED 7x5.

       Bên trong các ô của ma trận LED là các LED phát sang. Trong mô hình trên, Cathod (cực âm) của các LED trên mỗi hàng được nối chung với nhau và ngõ ra chung là các ngõ D (Data). Các Anod của các LED trên mỗi cột được nối chung tạo thành các đường C (Control). Thông thường, các đường D và C được chọn sao số số lượng đường D nhiều hơn đường C hoặc sao cho số lương các đường D gần nhất với số 8, 16, 32…(lũy thừa của 2). Lý do của việc chọn này nhằm giảm kích thước bộ font chứa các ký tự hoặc hình ảnh hiển thị lên ma trận LED, bạn sẽ hiểu rõ hơn khi tìm hiểu các điều khiển ma trận LED 7x5 bên dưới.

Page 158: asembly

a)                                                             b)

Hình 2 mô tả cách mà ma trận LED 7x5 được dùng để hiển thị số 4. 

       Trước hết chúng ta sẽ khảo cách cho sang các LED mà không cần quan tâm đến bảng font. Quan sát cột thứ nhất (cột C0) trong hình 2a, trong cột này chỉ có 2 LED ở hàng D2 và D3 là sang, các LED còn lại tắt. Điều này được thực hiện bằng cách kích chân C0 (Anod) lên mức cao, kéo các chân D2, D3 xuống mức 0 trong khi các chân Data khác được giữ ở mức cao. Các cột khác được thực hiện tương tự. Tuy nhiên, câu hỏi ở đây là làm sao hiển thị các cột với các đèn LED sáng khác nhau trong khi các ngõ Cathod của chúng đều được nối chung (thành các chân D). Ví dụ một người kéo tất cả 5 chân C0…C4 lên mức cao vào xuất tín hiệu ra các chân D, khi đó tất cả các LED trên dùng một hàng sẽ sáng hoặc tắt như nhau. “Bí quyết” ở đây chính là kỹ thuật “quét”, chúng ta sẽ hiển thị tuần tự các cột với các giá trị tương ứng của chúng chứ không hiển thị đồng thời. Trong ví dụ hiển thị số ‘4’, trước hết hãy kích chân C0 lên cao trong khi các chân C1…C4 ở mức thấp, xuất tín hiệu ra các chân D để hiển thị lên cột C0. Tiếp theo kéo chân C1 lên cao và các chân Control khác ở mức thấp, xuất dữ liệu ra các chân D để hiển thị cột C1…Cứ như thế cho đến khi hiển thị hết các cột thì quay lại cột C0. Quá trình này gọi là “quét LED”. Do tốc độ “quét” rất cao nên chúng ta sẽ không có cảm giác “nhấp

Page 159: asembly

nháy”, các cột của ma trận như được hiển thị đồng thời. Chú ý là độ sáng của LED phụ thuộc vào số cột LED, nếu bạn “quét” quá nhiều cột LED, tỉ lệ thời gian “ON” của mỗi cột sẽ rất nhỏ so với thời gian “OFF” vì phải chờ quét các cột khác. Vì thế nếu ma trận LED có nhiều cột hoặc khi ghép nhiều ma trận, các mạch driver cần được sử dụng để đảm bảo độ sáng của LED.

       Giả sử mỗi LED đại diện cho 1 bit và các LED sáng đại diện cho giá trị nhị phân 1 trong khi các LED tắt là số 0. Hình 2b thể hiện mô hình số nhị phân cho trường hợp hiển thị số 4 trên ma trận LED 7x5. Nếu xem mỗi cột của ma trận là 1 con số 7 bit thì 5 giá trị cần thiết để hiền thị số ‘4’ là: 0x0C, 0x14, 0x24, 0x7F, 0x04. Bộ 5 giá trị này tạo thành mã font cho ký tự ‘4’, chúng sẽ được định nghĩa trước và lưu trong bộ nhớ của chip điều khiển (AVR), mỗi lần một ký tự được yêu cầu hiển thị, bộ font tương ứng của ký tự đó sẽ được “load” ra và xuất lần lượt trên các đường Data, đây chính là lý do tại sao chúng ta gọi các đường D là các đường Data. Cách “quét” LED tôi vừa trình bày là cách “quét ngang”, bạn có thể thực hiện “quét dọc” nếu ứng dụng yêu cầu. Trong phương pháp quét dọc, các chân hàng chung sẽ được dùng để chọn hàng cần hiển thị, dữ liệu sẽ xuất ra theo từng hàng trên 5 cột và lần lượt thay đổi hàng (hàng 0 trước, đến 1…và cuối cùng là 6). So sánh 2 cách quét cho trường hợp ma trận LED 7x5, rõ ràng trong cách quét ngang chúng ta chỉ cần quet 5 cột cho mỗi lần LED nên tỉ lệ thời gian ON sẽ cao hơn (1/5 so với 1/8 của cách quét dọc). Mặt khác, nếu thực hiện quét dọc chúng ta cần 8 số số để tạo thành 1 bộ font cho một ký tự và vì thế tốn nhiều bộ nhớ hơn cho việc lưu trữ bảng font. Trong bài học này tôi thực hiện theo cách quét ngang và bảng font cũng được xây dựng cho cách quét này.

II. AVR và Ma trận LED.

       Phần này tôi minh họa cách hiển thị ma trận LED 7x5 bằng AVR. Chúng ta sẽ thực hiện trên duy nhất một ma trận LED, cho các ứng dụng cần nhiều LED bạn đọc hãy tự phát triển từ ý tưởng trong phần này. Hãy vẽ một mạch điện mô phỏng bằng phần mềm Proteus như trong hình 3.

Page 160: asembly

Hình 3. Hiển thị ma trận LED bằng AVR.

       Các chân C của ma trận được nối với các chân trên PORTC của chip AVR ATmega32, và các chân D được nối với PORTD. Hãy tạo 1 Project bằng Programmer Notepad tên DotMatrix và tạo 2 file tên font.h cùng dotmatrix.c trong Project này. File font.h chứa bảng font của các ký tự và file dotmatrix.c là file chính cho chương trình demo. List 1 trình là một phần nội dung của file font.h và List 2 là nội dung file dotmatrix.c.

List 1. Bảng font.

Page 161: asembly

List 2. Chương trình demo.

Page 162: asembly

       Điều cần quan tâm đầu tiên là kích thước bảng font, trong ví dụ này bảng font được xây dụng cho 223 symbol có mã ASCII từ 32 đến 255 (do các mã ASCII trước 32 không có symbol tương ứng nên có thể bỏ qua để tiết kiệm bộ nhớ), mỗi symbol cần 5 số 8 bits, như thế chúng ta cần tổng cộng 1115 byte cho bảng font trong khi kích thước SRAM của chip ATmega32 chỉ là 2KB (2048 byte). Nếu dùng SRAM chứa bảng font sẽ rất phí phạm vì đây là 1 bảng tĩnh, giá trị trong bảng hoàn toàn không thay đổi mà chỉ được truy xuất đọc. Vì thế chúng ta có thể tận dụng bộ nhớ chương trình (Flash) để lưu bảng font này. Dòng đầu tiên trong List 1 chúng ta include header “pgmspace.h” để sử dụng

Page 163: asembly

các thao tác trên bộ nhớ chương trình. Tiếp theo chúng ta khai báo 1 mảng tĩnh có tên font7x5 với kiểu dữ liệu là prog_char tức là kiểu char nhưng chứa trong bộ nhớ chương trình (Program memory). Giá trị chứa trong mảng font7x5 chính là dữ liệu của bảng font, thực chất mảng font7x5 là mảng 1 chiều liên tục, việc tách ra trên nhiều dòng có mục đích giúp người đọc dễ hình dung khi truy cập các giá trị của mảng để xuất ra sau này. Bạn hãy hiểu rằng cứ một tổ hợp 5 số sẽ tạo thành một symbol hiển thị cho ma trận LED. Dữ liệu trong bảng font được sắp xếp theo trình tự ASCII và để tạo điều kiện thuận lợi khi truy xuất bảng font theo mã ASCII của ký tự cần hiển thị. Tuy nhiên cần chú ý là bảng font được bắt đầu cho symbol có mã ASCII là 32 chứ không bắt đầu từ mã ASCII 0, vì thế khi truy cận bảng font từ mã ASCII chúng ta cần lấy mã ASCII trừ đi 32 để được vị trí chính xác trong bảng.

       Tiếp theo chúng ta sẽ tìm hiểu chương trình chính, dòng 3 trong list 2 include file font.h để sử dụng bảng font trong chương trình chính. Các dòng từ 5 đến 9 định nghĩa các PORT kết nối với ma trận LED, PORTD là Data bus trong khi PORTC là control lines. Chương trình con void DOTputChar75(uint8_t chr) trong dòng 11 là thủ tục đọc dữ liệu từ bảng font và hiển thị trên ma trận LED. Tham số chr của chương trình này chính là mã ASCII của ký tự cần hiển thị trên ma trận LED. Dòng 12 khai báo 2 biến phụ, trong đó biến line chứa tín hiệu điều khiển cho các đường Control. Dòng 13 khai báo một biến tạm tchr dùng chứa địa chỉ dữ liệu cần lấy ra từ bảng font để xuất ra các đường Data, vì mã ASCII là một số 8 bit trong khi số lượng dữ liệu trong bảng font lớn gấp 5 lần số lương ký tự, vì thế cần khai báo biến tchr có kiểu dữ liệu 16 bit. Nội dung chính của đoạn chương trình này nằm trong vòng lặp for, biến i đại diện cho số thứ tự của các chân Control được cho chạy từ 0 đến 4, trong dòng 15 “CTRL_PORT=line;” xuất tín hiệu điều khiển ra CTRL_PORT tức ra các chân C. Do biến line được khởi tạo bằng 1 nên ở lần lặp đầu tiên giá trịCTRL_PORT=0b00000001, tức chân C0 ở mức cao trong khi các chân còn lại ở mức thấp, cột đầu tiên được chọn. Sau khi 1 cột đã được chọn, dòng 16 “DATA_PORT=~pgm_read_byte(&font7x5[((tchr - 32) * 5) + i]);” đọc và xuất dữ liệu từ bảng font ra các chân Data. Trước hết là cách tính địa chỉ của dữ liệu trong bảng font. Như trình bày trong phần giải thích cho bảng font, bảng này được chúng ta bắt đầu từ ký tự có mã 32 nên chúng ta cần trừ đi 32 để tham chiếu đến vị trí chính xác trong bảng font: tchr-32. Ví dụ muốn hiển thị ký tự có mã chr = 48 (mã của ký tự ‘0’), vị trí của tổ hợp dữ liệu tạo nên số ‘0’ được chứa trong bảng font ở vị trí 16, giá trị này được tính 48-32=16. Tiếp theo, do mỗi ký tự được tạo thành từ 5 số nên địa chỉ thực chất của số đầu tiên trong tổ hợp sẽ là (tchr-32)*5. Để di chuyển trong phạm vi 5 dữ liệu ứng với 6 cột của

Page 164: asembly

ma trận LED, biến i được cộng dồn vào địa chỉ này và chúng ta có: tchr - 32) * 5) + i. Để đọc dữ dữ liệu dạng byte từ bộ nhớ chương trình, chúng ta cần dùng hàm pgm_read_byte, hàm này được định nghĩa trong header pgmspace.h được khai báo trong file font.h.  Như vậy saiu khi thực hiện “pgm_read_byte(&font7x5[((tchr - 32) * 5) + i])” chúng ta thu được dữ liệu 1 byte tương ứng với cột thứ i của ký tự chr từ bảng font, việc cuối cùng có thể là xuất giá trị này ra DATA_PORT. Tuy nhiên, trước khi xuất byte đọc được ra DATA_PORT, chúng ta cần đảo các bit của byte này bằng toán tử “~”, lý do được giải thích là do các LED trong ma trận trong ví vụ này có các hàng nối với cực âm Cathode, để một LED sáng thì giá trị cần cấp cho bit D tương ứng là 0 nghĩa là ngược lại so với cách chúng ta tạo bảng font (sáng là 1). Chỉ bằng một thao tác đơn giản là toán tử “~” chúng có thể dễ dàng vượt qua trở ngại này. Trong trường hợp ma trận LED có các hàng nối với cực dương Anode thì chúng ta không cần đảo giá  trị đọc về. Dòng 17 thực hiện dịch chuyển giá trị của biến line sang trái 1 vị trí, việc làm có tác dụng chuẩn bị cho lần kế tiếp chân C kế tiếp sẽ được kích. Hàm delay trong dòng 18 giúp các LED trong cột hiện tại sáng trong 1 khoảng thời gian trước khi chuyển qua cột khác.

       Chương trình chính trong ví dụ này thật sự rất đơn giản, chúng ta trước hết cần khởi động hướng xuất nhập cho các PORT và sau đó gọi hàm DOTputChar75() trong vòng lặp vô tận while(1). Ở ví dụ trên, ký tự ‘4’ được xuất ra và kết quả hiển thị như trong hình 3. Chú ý là hàm DOTputChar75() chỉ “quét” qua các cột 1 lượt, vì thế muốn hiển thị một ký tự trong một khoảng thời gian chúng ta cần gọi hàm DOTputChar75() lặp lại trong khoảng thời gian đó.

KeyPad

1 2 3 4 5

 ( 29 Votes )Nội dung Các bài cần tham khảo trước

1. Keypad 4x4 . Cấu trúc AVR .

Page 165: asembly

2. Đọckeypad 4x4 bằng AVR .

Download ví dụ

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

Text LCD

I. Keypad 4x4.

       Keypad là một "thiết bị nhập" chứa các nút nhấn cho phép người dùng nhập các chữ số, chữ cái hoặc ký hiệu vào bộ điều khiển. Keypad không chứa tất cả bảng mã ASCII như keyboard và vì thế keypad thường được tìm thấy trong các thiết bị chuyên dụng. Các nút nhấn trên các máy tính điện tử cầm tay là một ví dụ về keypad. Số lượng nút nhấn của một keypad thay đổi phụ thuộc vào yêu cầu ứng dụng. Trong bài này tôi giới thiệu cách điều khiển của một loại keypad đơn giản, keypad 4x4.

       Gọi là keypad 4x4 vì keypad này có 16 nút nhấn được bố trí dạng ma trận 4 hàng và 4 cột. Cách bố trí ma trận hàng và cột là cách chung mà các keypad sử dụng. Cũng giống như các ma trận LED, các nút nhấn cùng hàng và cùng cột được nối với nhau, vì thế với keypad 4x4 sẽ có tổng cộng 8 ngõ ra (4 hàng và 4 cột). Mô hình Keypad 4x4 được thể hiện trong hình 1.

Page 166: asembly

a)                                                                 b)

Hình 1. Keypad 4x4.

       Hình 1b là mô hình thật của 1 keypad 4x4 và hình 1a là cấu hình bên trong của nó. Bốn hàng của keypad được đánh dấu là A, B, C và D trong khi 4 cột được gọi là 1, 2, 3 và 4.

       Hoạt động của keypad: Giả sử nhút '2' được nhấn, khi đó đường C và 2 được nối với nhau. Giả sử đường 2 được nối với GND (mass, 0V) thì C cũng sẽ là GND. Tuy nhiên, câu hỏi đặt ra là bằng cách kiểm tra trạng thái đường C chúng ta sẽ có kết luận nút '2' được nhấn? Giả sử tất cả các đường 1, 2, 3, 4 đều nới với GND, nếu C= GND thì rõ ràng chúng ta không thể kết luận nút '1',= hay nút '2' hay nút '3' hay nút  '-' được nhấn. Kỹ thuật để khắc phục vấn đề này chính là kỹ thuật "quét" keypad. Kỹ thuật quét keypad bằng AVR được trình bày như sau:

- Nối tất cả 8 chân của keypad với 1 PORT của AVR, ví dụ PORTB theo thứ tự bên dưới:

Page 167: asembly

       - Các chân 1, 2, 3, 4 được set như các chân Output và giữ ở mức cao, các chân A, B, C, D là Input và có điện trở kéo lên. Lần lượt kéo chân 1, 2, 3, 4 xuống thấp (lần lượt xuất giá trị 0 ra từng chân), đọc trạng thái các chân A, B, C, D để kết luận nút nào được nhấn. Ví dụ như trong hình 1, nút '2' được nhấn thì quá trình quét sẽ cho kết quả như sau:

Bước 1: kéo chân 1 xuống 0 (các chân 2,3,4 vẫn ở mức cao), kiểm tra 4 chân A, B, C, D thu được kết quả D=1, C=1, B=1, A=1. (giá trị đọc về của PINB là 00001111 nhị phân)

Bước 2: kéo chân 2 xuống 0, kiểm tra lại A, B, C, D, kết quả thu được D=1, C=0, B=1, A=1 (giá trị đọc về của PINB là 0b00001011 nhị phân). Chân C=0 tức có 1 nút ở hàng thứ 3 được nhấn, chúng ta lại đang ở Bước thứ 2tức nút nhấn thuộc cột thứ 2. Chúng ta có thể dừng quá trình quét tại đây và kết quả thu về nút ở hàng 3, cột 2 (tức nut '2' được) được nhấn.

       Quá trình quét cho các nút khác cũng xảy ra tương tự. Chú ý, nếu có 1 nút nào đó được nhấn thì có 4 khả năng cò thể đọc về từ 4 A,B,C,D đó là:

D=1, C=1, B=1, A=0: nút ở hàng A được nhấn, giá trị đọc về là 0x0E (các đường A,B,C,D được nối với 4 bit thấp của PORT trên AVR).

D=1, C=1, B=0, A=1: nút ở hàng B được nhấn, giá trị đọc về là 0x0D . D=1, C=0, B=1, A=1: nút ở hàng C được nhấn, giá trị đọc về là 0x0B . D=0, C=1, B=1, A=1: nút ở hàng D được nhấn, giá trị đọc về là 0x07 .

       Để tiện lợi khi so sánh kết quả đọc về, khi lập trình đọc keypad chúng ta nên lập 1 mảng 4 phần tử chứa 4 số có thể đọc về từ keypad. Ví dụ uint8_t scan_code[4]={0x0E,0x0D,0x0B,0x07};

       Trong phần tiếp theo chúng ta sẽ khảo sát cách đọc keypad 4x4 bằng 1 chip AVR Atmega32. 

Page 168: asembly

II. Đọc Keypad 4x4 bằng AVR.

       Chúng ta sẽ mô phỏng cách đọc và hiển thị giá trị từ keypad 4x4 bằng phần mêm Proteus. Các mã đọc được từ keypad sẽ hiễn thị lên 1 Text LCD 16x2. Thư viện myLCD.h được dùng để hiển thị lên LCD (xem lại bài Text LCD). Mạch điện mô phỏng thể hiện trong hình 2. 

Hình 2. Đọc và hiển thị từ Keypad 4x4.

Page 169: asembly

       Hãy tạo 1 Project bằng WinAVR với tên gọi KEYPAD, tạo file main.c và add vào Project, tạo Makefile, đồng thời copy file myLCD.h từ bài học Text LCD vào thư mục chứa Project KEYPAD. Mở file myLCD.h và sửa phần khai báo PORT như List0. 

List 0. Khai báo PORT trong file myLCD.h

0102030405060708

....#define CTRL                       PORTC#define DDR_CTRL             DDRC

#define DATA_O                  PORTC#define DATA_I                    PINC#define DDR_DATA             DDRC....

     Viết đoạn code trong List1 vào file main.c

List 1. Nội dung file main.c

Page 170: asembly
Page 171: asembly

       Ở dòng 3 chúng ta include file myLCD.h để sử dụng các hàm thao tác Text LCD. Trong các dòng 5, 6 và 7 chúng ta định nghĩa PORT giao tiếp với Keypad, theo đó PORTB được dùng cho Keypad. Dòng 9 khai báo một mảng 4 phần tử chứa mã đọc về từ Keypad như đã thảo luận trong phần trên. Các dòng code từ 10 đến 13 khai báo một mảng 2 chiều có 16 phần tử chứa mã ASCII của các ký tự đại diện cho các Button, tôi sắp xếp các ký tự dạng ma trận để dễ dàng tương ứng với các nút trên keypad. Dòng 14 khai báo biến key loại 8 bit không dấu, đây là biến chứa mã ascii khi đọc keypad. Dòng 15 khai báo hàm quét Keypad có tên checkpad(). Tất cả giải thuật quét và đọc keypad đều năm trong hàm này, giá trị trả về của hàm là mã ascii của nút được nhấn.

       Trước khi khảo sát đoạn code trong chương trình main, chúng ta sẽ tìm hiểu chương trình con checkpad(). Ở dòng 31 trong chương trình con checkpad, chúng ta khai báo 3 biến phụ 8 bit không dấu, i là biến đại diện cho cột của keypad và j là hàng, keyin là giá trị đọc về từ các chân A, B, C, D. Vòng lặp for 4 lần trong dòng 32 của biến i chính là 4 bước quét mà tôi đã trình bày trong ví dụ trên. Ở bước 1, biến i=0, nếu chúng ta dịch trái số 1 như (1<<(4+i)) thì giá trị thu được là (1<<(4+i))=0b00010000, kết hợp với dòng code 33: KEYPAD_PORT=0xFF-(1<<(4+i)); chúng ta thu được KEYPAD_PORT=0xEF. Số 4 trong phép dịch xuất hiện vì các cột của Keypad được nối với 4 bit cao của PORT trên AVR. Tóm lại, sau bước đầu tiên cột thứ nhất của Keypad được kéo xuống mức 0, sẵn sàng cho quá trình kiểm tra các hàng A,B,C,D trong các dòng tiếp theo. Dòng 35 đọc giá trị từ Keypad về biến keyin, vì chúng ta kết nối các chân A,B,C,D của Keypad với 4 bit thấp của PORT nên chúng ta chỉ quan tâm đến giá trị của 4 bit thấp này, việc AND (&) giá trị đọc về với 0x0F cho phép chúng ta bỏ qua 4 bit cao. Trong dòng 36, chúng ta kiểm tra xem nếu giá trị đọc về khác 0x0F thì thực hiện các dòng tiếp theo. Nếu keyin =0x0F nghĩa là không có bất kỳ nút nào trên cột 1 được nhấn, các dòng tiếp theo không thực hiện, vòng lặp for cho  biến i được tiếp tục giá trị tiếp theo. Nếu biến keyin khác 0x0F thì chúng ta biết rằng có 1 nút nào đó trên cột i được nhấn, các dòng tiếp theo sẽ xác định chính xác nút nào được nhấn. Dòng 37 cho biến hàng j chạy từ 0 đến 4, dòng 38 kiểm tra giá trị keyin, nếu keyin bằng phần tử thứ j trong mảng scan_code mà chúng ta đã định nghĩa trước đó thì nút trên hàng j đã được nhấn, tóm lại nút được nhấn là nút hàng j và cột i, chúng ta trả về giá trị mã ascii của nút này bằng cách lấy giá trị tương ứng của mảng ascii_code được định nghĩa trước đó: return ascii_code[j][i]. Nếu quá trình quét thất bại chúng ta trả về giá trị 0.

Page 172: asembly

       Nội dung của chương trình chính là khởi động chip và thực hiện demo quá trình đọc Keypad, Dòng 19 chúng ta khai báo sử dụng 4 bit thấp của KEYPAD_PORT làm input (các chân A,B,C,D là input) và 4 bit cao làm output. Dòng 18 khởi động các điện trở kéo lên cho 4 bit thấp. Hai dòng 21 và 22 khởi động và xóa Text LCD. Trong vòng lặp vô tận while(1), chúng ta quét keypad ở dòng 24 và hiển thị lên LCD ở dòng 25 (chỉ hiển thị nếu quá trình quét thành công).

       Trong ví dụ này tôi chỉ trình bày giải thuật quét Keypad cơ bản, vẫn còn một số vấn đề khác như kiểm tra sự kiện nhấn (key down), thả (key up)...bạn đọc hãy tự giải tuyết theo cách của riêng mình. 

C cho AVR

1 2 3 4 5

 ( 109 Votes )Nội dung Các bài cần tham khảo trước

1. Một số khái niệm C cho AVR.

2. Cấu trúc điều khiển và hàm.

3. Ví dụ minh họa.

Làm quen AVR.

Cấu trúc AVR .

WinAVR .

     Như tôi đã trình bày ở các bài học trước, khi bạn đã hiểu AVR, để thực hiện các ứng dụng, bạn có thể không nhất thiết phải luôn lập trình bằng Assembly(ASM). Ngôn ngữ cấp cao như C sẽ giúp cho bạn xây dựng các ứng dụng nhanh chóng và dễ dàng hơn, tuy nhiên không vì thế mà bạn “quên” ASM, lập trình bằng C kết hợp ASM là giải pháp hay nhất. Một chú ý là chúng ta chỉ sử dụng C để đơn giản hóa lập trình tính toán, cấu trúc điều khiển…lập trình C cho AVR không có nghĩa là bạn không cần biết cấu trúc và cách thức hoạt động của chip. Tôi không có ý định nói về ngôn ngữ C ở đây nhưng chỉ giới thiệu một cách cơ bản nhất về cách viết chương trình cho AVR bằng C, cụ thể là C trong

Page 173: asembly

avr-gcc. Để có thể hiểu và viết những chương trình phức tạp hơn, bạn cần tự trang bị kiến thức về C, tài liệu này sẽ không giúp bạn phần đó. Tuy nhiên, nếu bạn chưa từng lập trình bằng C thì bạn cũng yên tâm đọc tài liệu này, vì ít ra tôi sẽ giải thích những gì tôi viết.

I. Một số khái niệm C cho AVR.

      Một chương trình C cho AVR thường bao gồm các thành phần như: chú thích (comments), biểu thức (expressions), câu lệnh (statements), khối (blocks), toán tử, cấu trúc điều khiển (Flow controls), hàm (functions)…

      Chú thích (comments): có 2 cách để tạo phần chú thích trong C là chú thích từng dòng bằng 2 dấu “//” như trong dòng đầu của đoạn ví dụ “//day la chu thich, khong duoc bien dich” hoặc chú thích block bằng cách kẹp block cần chú thích vào giữa /* ….*/ ví dụ:

/* Ban co the type bat ky chu thich nao trong block nayNgay ca khi ban xuong dongPhan chu thich thuong co mau chu la green*/

      Tiền xử lí (preprocessor):  là một tiện ích của ngôn ngữ C, các preprocessor được trình biên dịch xử lí trước tất cả các phần khác, các preprocessor có chức năng tương tự các Directive trong ASM cho AVR.Các preprocessor được bắt đầu bằng dấu “#”, trong số các preprocessors trong ngôn ngữ C có hai preprocessors được sử dụng phổ biến nhất là#include và #define. Preprocessor #include chỉ định 1 file được đính kèm trong quá trình biên dịch (tương đương .INCLUDE trong ASM) và #define để định nghĩa 1 chuổi thay thế hoặc 1 macro. Xem các ví dụ sau:

#include "avr/io.h"  *đính kèm nội dung file io.h trong lúc biên dịch (file io.h nằm trong thư mục con avr của thư mục include trong thư mục cài đặt của WinAVR).*/#define max (a,b)   ((a)>(b)? (a): (b))  /*định nghĩa một macro tìm số lớn nhất trong  2 số a và b, trong chương trình nếu bạn gọi x=max(2,3) thì kết quả thu được x=3.*/

      Biểu thức (Expressions):  là 1 phần của các câu lệnh, biểu thức có thể bao gồm biến, toán tử, gọi hàm…, biểu thức trả về 1 giá trị đơn. Biểu thức không phải là 1 câu lệnh hoàn chỉnh. Ví dụ: PORTB=val.

Page 174: asembly

      Câu lệnh (Statement): thường là 1 dòng lệnh hoàn chỉnh, có thể bao gồm các keywords, biểu thức và các câu lệnh khác và được kết thúc bằng dấu “;”. Ví dụ: unsigned char val=1; val*=2; …là các câu lệnh.

      Khối (Blocks):  là sự kết hợp của nhiều câu lệnh để thực hiện chung 1 nhiệm vụ nào đó, khối được bao bởi 2 dấu mở khối “{“ và đóng khối “}”: ví dụ 1 khối:

while(1){              PORTB=val;      _delay_loop_2(65000);      val*=2;      if (!val) val=1;        }

      Toán tử (Operators):  là những ký hiệu báo cho trình biên dịch các nhiệm vụ cần thực hiện, các bảng bên dưới tóm tắt các toán tử C dùng cho lập trình AVR:

Bảng 1 các toán tử đại số: dùng thực hiện các phép toán đại số quen thuộc, trong

đó đáng chú ý là các toán tử “++” (tăng thêm 1) và “--“ (bớt đi 1), chú ý phân biệt  

y=x++  và y=++x, ví dụ ta có x=3 trong khi y=x++  nghĩa là gán x cho y rồi sau đó

tăng x thêm 1, điều này không ảnh hưởng đến y (cuối cùng y=3, x=4) trong khi

y=++x nghĩa là tăng x trước rồi mới gán cho y (cuối cùng y=x=4), tương tự cho

các trường hợp của toán tử “--“ .

Page 175: asembly

Bảng 2 Toán tử truy cập và kích thức: toán tử [] thường được sử dụng khi bạn

dùng mảng trong lúc lập trình, phần tử thứ của mảng sẽ được truy xuất thông qua

[i], chú ý mảng trong C bắt đầu từ 0.

Bảng 3 Toán tử Logic và quan hệ: thực hiện các phép so sánh và logic, thường

được dùng làm điều kiện trong các cấu trúc điều khiển, chú ý toán tử so sánh bằng

“==”, toán tử này khác với toán tử gán “=”, trong khi y = x nghĩa là lấy giá trị của

x gán cho y thì (y== x) nghĩa là “nếu y bằng x”.

Bảng 4 Toán tử thao tác Bit (Bitwise operator): là các toán tử thực hiện trên

từng bit nhị phân của các con số, các toán tử dịch trái “<<” và dịch phải ">>" rất

thường được sử dụng khi xử lí số.

Page 176: asembly

Bảng 5 các toán tử khác: là 1 số toán tử đặc biệt rất hay sử dụng nhưng chúng ta

thường không để ý vì vai trò của chúng rất dễ nhận thấy. Đặc biệt chú ý toán tử

“?:” là 1 toán tử rất đặc biệt của C so với các ngôn ngữ lập trình khác, “?:” là toán

tử 3 ngôi duy nhất có thể dùng thay thế cho cấu trúc “if” đơn giản.

II. Cấu trúc điều khiển và hàm.

2.1 Cấu trúc điều khiển (Flow Controls).

      Các cấu trúc điều khiển biến ý tưởng của bạn thành hiện thực. Một số cấu trúc điều khiển cơ bản trong C như sau:

      “If (điều kiện) statement;”:  nếu điều kiện là đúng thì thực hiện statement theo sau, statement có thể được trình bày cùng dòng hoặc dòng sau điều khiển If. Điều kiện có thể là một biểu thức bất kỳ, có thể là sự kết hợp của nhiều điều kiện bằng các toán tử quan hệ AND (&&), OR (||)…Điều kiện được cho là đúng khi nó khác 0, ví dụ if (1) thì điều kiện hiển nhiên là đúng. Xét một vài ví dụ dùng cấu trúc if như sau:

      If (!val) val=1; nghĩa là nếu val bằng 0 thì chương trình sẽ gán cho val giá trị là 1,  “!” là toán tử NOT, NOT của một số khác 0 thì bằng 0, ngược lại, NOT của 0

Page 177: asembly

thì thu được kết quả là 1. Trong ví dụ này, nếu val bằng 0 thì !val sẽ bằng 1, như thế điều kiện sẽ trở thành đúng và câu lệnh “val=1” được thực thi.

      If (x==1 && y==2) result=’A’; nghĩa là nếu x bằng 1 và y bằng 2 thì gán ký tự ‘A’ cho biến result. Trong ví dụ này, toán tử logic “&&” được sử dụng để “nối” 2 điều kiện lại, bạn hoàn toàn có thể sử dụng nhiều toán tử logic khác nếu cần thiết.

      Trong trường hợp bạn muốn thực thi nhiều câu lệnh cùng lúc nếu một điều kiện nào đó thỏa thì bạn cần đặt tất cả các câu lệnh đó trong 1 khối như bên dưới:

If (điều kiện) {      Statement1;      Statement2;      …}

      “If (điều kiện ) statement1; else statement2; ”: nếu điều kiện đúng thì thực hiện statement1, ngược lại thực thi statement2.  Việc đặt các statement và else..trên cùng 1 dòng hay trên những dòng khác nhau đều không ảnh hưởng đến kết quả. Tương tự trường hợp trên, nếu có nhiều statements thì cần đặt chúng trong 1 khối.

If (điều kiện) {      Statement1;      Statement2;      …}else {      Statement1;      Statement2;      …}

      Ngoài ra, bạn cũng có thể đặt nhiều cấu trúc if…else… lồng vào nhau.

      Cấu trúc switch: trong trường hợp có nhiều khả năng có thể xảy ra cho 1 biểu thức (hay 1 biến), ứng với mỗi khả năng bạn cần chương trình thực hiện một việc nào đó, khi này bạn nên sử dụng cấu trúc switch. Cấu trúc này được trình bày như bên dưới.

switch (biểu thức) {case hằng_số_1:       các statement1;break;case hằng_số_2: 

Page 178: asembly

      các statement2;break;…default:       các statement khác;}

      Hãy xét 1 ví dụ bạn kết nối 2 chip AVR với nhau, 1 chip làm Master sẽ ra các lệnh điều khiển chip Slave, chip Slave nhận mã lệnh từ Master và thực hiện các công việc được thoả hiệp trước. Giả sử mã lệnh được lưu trong biến Command, dưới đây là chương trình ví dụ cách xử lí của chip Slave ứng với từng mã lệnh.

switch (Command) {case 1:       PWM=255;      ON_Motor();      break;case 2:       PWM=0;      OFF_Motor();;      break;…default:       Get_Cmd();break;}Ngoài ra, bạn cũng có thể đặt nhiều cấu trúc if…else… lồng vào nhau.

      Nếu Command=1, gán giá trị 255 cho biến PWM và gọi chương trình con ON_Motor(). Trong trường hợp này, break được sử dụng, break nghĩa là thoát khỏi cấu trúc điều khiển hiện tại ngay lập tức, như vậy sau khi thực hiện 2 lệnh, switch kết thúc mà không cần xét đến các trường hợp khác. Bây giờ, nếu Command=2, gán giá trị 0 cho biến PWM và gọi chương trình con OFF_Motor(), trong tất cả các trường hợp còn lại (default), thực hiện chương trình con Get_Cmd().

      “while (điều kiện ) statement1;”: là một cấu trúc lặp (Loop), ý nghĩa của cấu trúc while là khi điều kiện còn đúng thì sẽ thực hiện statement1 (hoặc các statements nếu chúng được đặt trong 1 khối {} như trong trường hợp của if được giới thiệu ở trên). Cẩn thận, bạn rất dễ rơi vào một vòng lặp “không lối thoát” với while nếu điều kiện luôn luôn đúng.

      “for (biểu_thức_1; biểu_thức_2; biểu_thức_3) statement;”: là một cấu trúc lặp khác, trong cấu trúc for, biểu_thức_1 thường được hiểu là khởi tạo,

Page 179: asembly

biểu_thức_2 là điều kiện và biểu_thức_3 là biểu thức được thực hiện sau. Cấu trúc for này tương đương với cấu trúc while sau: 

biểu_thức_1;while (biểu_thức_2){       statement;      biểu_thức_3;}

         Các biểu thức trong cấu trúc for có thể vắng mặt trong cấu trúc nhung các dấu “;” thì không được bỏ. Nếu bạn viết for( ; ; ) tương đương với vòng lặp vô tận while (1).

      Cấu trúc for thường được dùng để thực hiện 1 hay những công việc nào đó trong số lần nào đó, ví dụ bên dưới thực hiện xuất các giá trị từ 0 đến 200 ra PORTB, sau mỗi lần xuất sẽ gọi lệnh delay trong 65000 chu kỳ máy.

for (uint8_t  i=0; i<=200; i++){PORTB=i;_delay_loop_2(65000);}

      Chú ý, bạn có thể thực hiện việc khai báo 1 biến (xem phần khai báo biến bên dưới) ngay trong cấu trúc for nếu biến lần đầu được sử dụng. Ví dụ trên được hiểu như sau: khai báo 1 biến i kiểu byte không âm, gán giá trị khởi đầu cho i=0 (chỉ thực hiện 1 lần duy nhất), kiểm tra điều kiện i<=200 (nhỏ hơn hoặc bằng 200), nếu điều kiện còn đúng, thực hiện 2 statements trong block {}, sau đó quay về để thực hiện i++ (tăng i thêm 1) rồi lại kiểm tra điều kiện i<=200 và quá trình lặp lại. Như thế đoạn code trong {} được thực thi khoảng 201 lần trước khi biến i bằng 201 và điều kiện i<=200 sai.

2.2 Hàm (Functions).

      Ngôn ngữ C bao gồm tập hợp của rất nhiều hàm, mỗi hàm thực hiện một chức năng cụ thể, các hàm trong C thường được thiết kết rất nhỏ gọn, để có các hàm phức tạp người dùng cần tự tạo ra. Hàm C cho AVR được định nghĩa trong thư viện avr-libc, ngoài các hàm C thông thường, avr-libc còn chứa rất nhiều các hàm riêng dùng riêng cho chip AVR, các hàm này được khai báo trong các file header riêng, để sử dụng hàm nào, bạn cần #include file header tương ứng (tham khảo tài liệu “avr-libc user manual” để biết thêm chi tiết, trong tài liệu này, khi cần sử dụng một hàm nào tôi sẽ nói rõ file header cần thiết).

Page 180: asembly

      Ví dụ: _delay_loop_2(65000) là một hàm được định nghĩa trong file “delay.h” (trong thư mục C:\WinAVR\avr\include\util), hàm này thực hiện việc delay khoảng 65000 chu kỳ máy. Có 4 hàm delay bạn có thể sử dụng sau khi include file đó là:

_delay_loop_1(uint8_t  __count) : delay theo một số lần chu kỳ máy nhất định

(biến __count), số lượng chu kỳ delay là số 8 bit (từ 0 đến 255).

_delay_loop_2(uint16_t  __count) : delay theo một số lần chu kỳ máy nhất định

(biến __count), số lượng chu kỳ delay là số 16 bit (từ 0 đến 65535).

(Chú ý: thực chất 2 hàm delay trên được định nghĩa trong file header

“delay_basic.h”).

_delay_us(double  __us): delay 1 microsecond.

_delay_ms(double  __ms): delay 1 milisecond.

      Chú ý: để dùng 2 hàm _delay_us và _delay_ms cần định nghĩa tần số xung clock trong Makefile (biến F_CPU), sử dụng 2 hàm này trực tiếp thường cho kết quả không như mong muốn, tôi sẽ trình bày cách sử dụng 2 hàm này trong ví dụ bên dưới.

      Main:  một chương trình C cho AVR phải bao gồm 1 chương trình chính main, tất cả các nội dung chính sẽ được đặt bên trong chương trình chính. Cấu trúc chương trình chính có thể như sau:

int main(void){      //noi dung chinh      return 0; //gia tri tra ve cho chuong trinh chinh}

      Trong đó, int là kiểu giá trị trả về của main, từ khóa void  nói rằng chương trình chính của chúng ta không cần bất kỳ tham số nào kèm theo.

      Còn rất nhiều các vấn đề liên quan đến C cho AVR, chúng ta sẽ tìm hiểu trong lúc viết các ví dụ cụ thể.

III. Ví dụ minh họa.

Page 181: asembly

      Để minh họa các khái niệm và phương pháp lập trình C cho AVR, tôi sẽ giải thích ví dụ quét LED viết bằng C mà chúng ta thực hiện trong bài hướng dẫn WinAVR. Đoạn code được trình bày trong  List 1.

List 1. ví dụ quét LED bằng C.

123456789101112131415

//file: main.c//Description: Cung hoc avr, www.hocavr.com#include <avr/io.h>#include <util/delay.h>unsigned char val=1;int main(void){      DDRB=0xFF; //xử dụng PORTD làm đường xuất dữ liệu      while(1){            PORTB=val;            _delay_loop_2(65000);            val*=2;            if (!val) val=1;          }      return 0;}

      Trước hết là preprocessor đính kèm các file khi biên dịch, #include là đính kèm file header io.h, file này thực ra không phải là file chứa các thông tin về chip nhưng nó sẽ làm một nhiệm vụ trung gian là đính kèm 1 file khác tương ứng với biến MCU trong Makefile, ví dụ trong Makefile, MCU=atmega8 thì dòng “#include ” được thực thi, file iom8.hđược tự động đính kèm kèm vào và file iom8.h mới thực chất chứa các định nghĩa cho chip ATmega8 (các định nghĩa về địa chỉ thanh ghi, kích thước bộ nhớ,…). Điều này giúp bạn không cần nhớ hết tất cả các file header của từng chip AVR. Nếu “không an tâm”, bạn có thể thêm dòng  #include iom8.h sau  khi include io.h (điều này không thật sự cần thiết). Ngoài ra, mỗi lần include file io.h sẽ có 4 file header khác được tự động đính kèm là “avr/sfr_defs.h”, “avr/portpins.h”, “avr/common.h”, và  “avr/version.h”. Tóm lại bạn cần (hoặc phải) include file io.h và khai báo loại chip AVR trong file Makefile (dùng MFile, như hướng dẫn ở trên) là có thể an tâm viết chương trình C cho AVR.

      - Dòng thứ 4 include file header delay.h để sử dụng lệnh delay như đã đề cập ở trên.

Page 182: asembly

      - Dòng 5 : khai báo 1 biến tên val trong bộ nhớ SRAM, kiểu của val là unsigned char là kiểu dữ liệu 8 bit không dấu có khoảng giá trị từ 0 đến 255. Biến val được dùng làm biến tạm để chứa giá trước khi xuất ra PORTB. Biến trong C được khai báo bằng cách đặt kiểu biến trước sau đó tên biến. Một số kiêu dữ liệu cơ bản trong C được tóm tắt trong bảng 6.

Bảng 6 các kiểu dữ liệu trong C.

Tên kiểu dữ liệu (Data type)

Số byte Khoảng dữ liệu (Range)

char 1 –127 to 127 or 0 to 255

unsigned char 1 0 to 255

signed char 1 –127 to 127

int 2 –32,767 to 32,767

unsigned int 2 0 to 65,535

signed int 2 Như kiểu int

short int 2 Như kiểu int

unsigned short int 2 0 to 65,535

signed short int 2 Như kiểu short int

long int 4 –2,147,483,647 to 2,147,483,647

signed long int 4 Như kiểu long int

unsigned long int 4 0 to 4,294,967,295

long long int 8 –(263–1) to 263–1 (C99 only)

Page 183: asembly

Tên kiểu dữ liệu (Data type)

Số byte Khoảng dữ liệu (Range)

signed long long int 8 same as long long int (C99 only)

unsigned long long int 8 0 to 264–1 (C99 only)

float 4 6 digits of precision

double 8 10 digits of precision

long double 12 10 digits of precision

      Một số kiểu dữ liệu thông dụng nhất là char (1 byte), int (2 byte) và float. Từ khóa unsigned được thêm trước 1 kiểu dữ liệu nguyên để chỉ định các số nguyên dương, khi đó khoảng giá trị nguyên sẽ được tăng lên gần 2 lần. Ví dụ char chỉ các số nguyên từ -127 đến 127 thường được dùng để chỉ mã ASCII của các ký tự trong bảng mã ASCII, nhưng unsigned char sẽ bao gồm các số nguyên dương từ 0 đến 255 và thường được dùng khi làm việc với các thanh ghi 8 bit.

      Ngoài ra, avr-libc còn định nghĩa một số kiểu dữ liệu thay thế, chúng ta có thể dùng các kiểu dữ liệu này thay cho các kiểu thông thường, xem tóm tắt như bên dưới.

Page 184: asembly

      Một khai báo uint8_t  val  tương đương usigned char val, sử dụng kiểu khai báo nào là do thói quen của người sử dụng. Chú ý là theo mặc định, một biến mới được khai báo theo cách thông thường như trên sẽ được đặt trong SRAM, như các bạn đã biết SRAM trong AVR tương đối nhỏ vì thế nên khai báo và sử dụng hợp lí biến, đừng khai báo quá nhiều biến nếu bạn không sử dụng hết, đừng khai báo kiểu biến quá lớn so với giá trị thật sử dụng, tuy nhiên cũng không được khai báo kiểu dữ liệu có kích thước quá nhỏ so với giá trị mà biến đó có thể vươn tới. Sử dụng bộ nhớ chương trình (flash program memory) để lưu trữ dữ liệu không đổi là một kỹ thuật khác để tiết kiệm bộ SRAM, tôi sẽ đề cập vấn đề này trong 1 bài khác.

      Cuối cùng về việc khai báo biến, một biến có thể được gán giá trị khởi tạo ngay lúc khai báo như trong trường hợp của chúng ta, biến val=1 lúc được khai báo.

      -  Dòng 6 “int main(void){” bắt đầu chương trình chính.

      - Dòng 7: “DDRB=0xFF” gán giá trị hexadecimal 0xFF (11111111) cho thanh thi điều khiển của Port B, DDRB, Port B khi đó sẽ trở thành Port xuất

      -  Dòng 8 “while (1){”: bắt đầu 1 vòng lặp vô tận.

      -  Dòng 9 và dòng 10: xuất val ra PORTB và gọi lệnh delay.

      - Bạn cần chú ý 11 và 12, 2 dòng này có chức năng “xoay” giá trị của biến val để xuất ra PORTB tạo hiệu ứng xoay vòng. val*=2 được hiểu là val=val*2, đây là 1 kiểu viết thu gọn của C, nếu toán hạng thứ nhất và kết quả trả về là cùng 1 biến, chúng ta có thể bỏ bớt 1 tên biến và di chuyển toán tử về bên phải toán tử gán “=”. Ví dụ: i = i + 6 được rút gọn thành i + = 6.

      Như thế sau câu lệnh val*=2  giá trị của val được tăng lên 2 lần. Ý nghĩa thật sự của việc gấp đôi biến val là gì? Hãy nhìn vào giá trị nhị phân của val, lúc khai báo val, chúng ta gán cho val = 1 hay val = 00000001 (nhị phân), sau khi gấp đôi lần thứ nhất, val = 2=00000010, tiếp tục gấp đôi lần thứ hai, val = 4=00000100…có thể bạn đã thấy chuyện gì xảy ra? Đây là câu trả lời: “trong thao tác với số nhị phân, gấp đôi một số nghĩa là di chuyển số đó sang trái 1 vị trí”…Quá trình gấp đôi sẽ tiếp diễn đến lúc val = 128=10000000, nếu tiếp tục gấp đôi, bạn nghĩ val = 256 ? Tuy nhiên bạn nhớ rằng chúng ta đã khai báo biến val có kiểu unsigned char (8 bits), trong khi đó 256=100000000 (9 bits), nếu gán val = 256, chỉ có 8 bits thấp (00000000) của 256 sẽ được gán cho val, kết quả là val = 0. Nói một cách khác,

Page 185: asembly

sau khi val=128, val = 0, câu lệnh: “ if (!val) val=1; ” sẽ giúp cho quá trình quét lặp quay lại từ đầu nếu  val = 0. Mọi thứ đã rõ.

      Cuối cùng vì chương trình chính của chúng ta có kiểu int (int main…) chúng ta cần “trả về” một giá trị nào đó, “return 0;” thực hiện trả về 0 (bạn có thể trả về giá trị nào tùy ý). 

Text LCD

1 2 3 4 5

 ( 100 Votes )Nội dung Các bài cần tham khảo trước

1. Bạn sẽ đi đến đâu.

2. Text LCD.

3. AVR và Text LCD.

4. Ví dụ   điều khiển Text LCD   bằng thư viện   myLCD.

Download ví dụ

Cấu trúc AVR

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Bạn sẽ đi đến đâu.

       Bài này nằm trong phần ứng dụng AVR thuộc loạt bài cùng học AVR. Trong bài ứng dụng này chúng ta không khảo sát nhiều cấu trúc AVR mà chủ yếu là tìm hiểu Text LCD cách điều khiển bằng AVR. Công cụ chính cũng là 2 bộ phần mềm quen thuộc WinAVR và Proteus.       Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:       - Cấu trúc Text LCD.       - Nguyên lý hoạt động Text LCD       - Phát triển 1 thư viện điều khiển Text LCD bằng AVR cả 2 chế độ 8 bit và 4 bit.       - Ví dụ điều khiển Text LCD bằng AVR.

Page 186: asembly

II. Text LCD.

       Text LCD là các loại màn hình tinh thể lỏng nhỏ dùng để hiển thị các dòng chữ hoặc số trong bảng mã ASCII. Không giống các loại LCD lớn, Text LCD được chia sẵn thành từng ô và ứng với mỗi ô chỉ có thể hiển thị một ký tự ASCII. Cũng vì lý do chỉ hiện thị được ký tự ASCII nên loại LCD này được gọi là Text LCD (để phân biệt với Graphic LCD có thể hiển thị hình ảnh). Mỗi ô của Text LCD bao gồm các “chấm” tinh thể lỏng, việc kết hợp “ẩn” và “hiện” các chấm này sẽ tạo thành một ký tự cần hiển thị. Trong các Text LCD, các mẫu ký tự được định nghĩa sẵn. Kích thước của Text LCD được định nghĩa bằng số ký tự có thể hiển thị trên 1 dòng và tổng số dòng mà LCD có. Ví dụ LCD 16x2 là loại có 2 dòng và mỗi dòng có thể hiển thị tối đa 16 ký tự. Một số kích thước Text LCD thông thường gồm 16x1, 16x2, 16x4, 20x2, 20x4…Hình 1 là một ví dụ Text LCD 16x2.

Hình 1. Text LCD 16x2.

       Text LCD có 2 cách giao tiếp cơ bản là nối tiếp (như I2C) và song song. Trong phạm vi bài học này tôi chỉ giới thiệu loại giao tiếp song song, cụ thể là LCD 16x2 điều khiển bởi chip HD44780U của hãng Hitachi. Đối với các LCD khác bạn cần tham khảo datasheet riêng của từng loại. Tuy nhiên, HD44780U cũng được coi là chuẩn chung cho các loại Text LCD, vì thế bạn có thể dùng chương trình ví dụ trong bài này để test trên các LCD khác với rất ít hoặc không cần chỉnh sửa.       HD44780U là bộ điều khiển cho các Text LCD dạng ma trận điểm (dot-matrix), chip này có thể được dùng cho các LCD có 1 hoặc 2 dòng hiển thị. HD44780U có 2 mode giao tiếp là 4 bit và 8 bit. Nó chứa sẵn 208 ký tự mẫu kích thước font 5x8 và 32 ký tự mẫu font 5x10 (tổng cộng là 240 ký tự mẫu khác nhau).

1. Sơ đồ chân.

Page 187: asembly

       Các Text LCD theo chuẩn HD44780U thường có 16 chân trong đó 14 chân kết nối với bộ điều khiển và 2 chân nguồn cho “đèn LED nền”. Thứ tự các chân thường được sắp xếp như sau: Bảng 1. Sơ đồ chân.

       Trong một số LCD 2 chân LED nền được đánh số 15 và 16 nhưng trong một số trường hợp 2 chân này được ghi là A (Anode) và K (Cathode). Hình 2 mô tả cách kết nối LCD với nguồn và mạch điều khiển.

Page 188: asembly

Hình 2. Kết nối Text LCD.

       Chân 1 và chân 2 là các chân nguồn, được nối với GND và nguồn 5V. Chân 3 là chân chỉnh độ tương phản (contrast), chân này cần được nối với 1 biến trở chia áp như trong hình 2.Trong khi hoạt động, chỉnh để thay đổi giá trị biến trở để đạt được độ tương phản cần thiết, sau đó giữ mức biến trở này. Các chân điều khiển RS, R/W, EN và các đường dữ liệu được nối trực tiếp với vi điều khiển. Tùy theo chế độ hoạt động 4 bit hay 8 bit mà các chân từ D0 đến D3 có thể bỏ qua hoặc nối với vi điều khiển, chúng ta sẽ khảo sát kỹ càng hơn trong các phần sau.

2. Thanh ghi và tổ chức bộ nhớ.

       HD44780U có 2 thanh ghi 8 bits là INSTRUCTION REGISTER (IR) và DATA REGISTER (DR). Thanh ghi IR chứa mã lệnh điều khiển LCD và là thanh ghi “chỉ ghi” (chỉ có thể ghi vào thanh ghi này mà không đọc được nó). Thanh ghi DR chứa các các loại dữ liệu như ký tự cần hiển thị hoặc dữ liệu đọc ra từ bộ nhớ LCD…Cả 2 thanh ghi đều được nối với các đường dữ liệu D0:7 của Text LCD và được lựa chọn tùy theo các chân điều khiển RS, RW. Thực tế để điều khiển Text LCD chúng ta không cần quan tâm đến cách thức hoạt động của 2 thanh ghi này, vì

Page 189: asembly

thế cũng không cần khảo sát chi tiết chúng.HD44780U có 3 loại bộ nhớ, đó là bộ nhớ RAM dữ liệu cần hiển thị DDRAM (Didplay Data RAM), bộ nhớ chứa ROM chứa bộ font tạo ra ký tự CGROM (Character Generator ROM) và bộ nhớ RAM chứa bộ font tạo ra các symbol tùy chọn CGRAM (Character Generator RAM). Để điều khiển hiển thị Text LCD chúng ta cần hiểu tổ chức và cách thức hoạt động của các bộ nhớ này:

2.1 DDRAM.

       DDRAM là bộ nhớ tạm chứa các ký tự cần hiển thị lên LCD, bộ nhớ này gồm có 80 ô được chia thành 2 hàng, mỗi ô có độ rộng 8 bit và được đánh số từ 0 đến 39 cho dòng 1; từ 64 đến 103 cho dòng 2. Mỗi ô nhớ tương ứng với 1 ô trên màn hình LCD. Như chúng ta biết LCD loại 16x2 có thể hiển thị tối đa 32 ký tự (có 32 ô hiển thị), vì thế có một số ô nhớ của DDRAM không được sử dụng làm các ô hiển thị. Để hiểu rõ hơn chúng ta tham khảo hình 3 bên dưới

Hình 3. Tổ chức của DDRAM.

       Chỉ có 16 ô nhớ có địa chỉ từ 0 đến 15 và 16 ô địa chỉ từ 64 đến 79 là được hiển thị trên LCD. Vì thế muốn hiển thị một ký tự nào đó trên LCD chúng ta cần viết ký tự đó vào DDRAM ở 1 trong 32 địa chỉ trên. Các ký tự nằm ngoài 32 ô nhớ trên sẽ không được hiển thị, tuy nhiên vẫn không bị mất đi, chúng có thể được dùng cho các mục đích khác nếu cần thiết.

2.2 CGROM.

       CGROM là vùng nhớ cố định chứa định nghĩa font cho các ký tự. Chúng ta không trực tiếp truy xuất vùng nhớ này mà chip HD44780U sẽ tự thực hiện khi có yêu cầu đọc font để hiện thị. Một điều đáng lưu ý là địa chỉ font của mỗi ký tự  vùng nhớ CGROM chính là mã ASCII của ký tự đó. Ví dụ ký tự ‘a’ có mã ASCII là 97, tham khảo tổ chức của vùng nhớ CGROM trong hình 4 bạn sẽ nhận thấy địa chỉ font của ‘a’ có 4 bit thấp là 0001 và 4 bit cao là 0110, địa chỉ tổng hợp là 01100001 = 97.       CGROM và DDRAM được tự động phối hợp trong quá trình hiển thị của LCD. Giả sử chúng ta muốn hiển thị ký tự ‘a’ tại vị trí đầu tiên, dòng thứ 2 của LCD thì các bước thực hiện sẽ như sau: trước hết chúng ta biết rằng vị trí đầu tiên của dòng 2 có địa chỉ là 64 trong bộ nhớ DDRAM (xem hình 3), vì thế chúng ta sẽ ghi vào ô nhớ có địa chỉ 64 một giá trị là 97 (mã ASCII của ký tự ‘a’). Tiếp theo,

Page 190: asembly

chip HD44780U đọc giá trị 97 này và coi như là địa chỉ của vùng nhớ CGROM, nó sẽ tìm đến vùng nhớ CGROM có địa chỉ 97 và đọc bảng font đã được định nghĩa sẵn ở đây, sau đó xuất bản font này ra các “chấm” trên màn hình LCD tại vị trí đầu tiên của dòng 2 trên LCD. Đây chính là cách mà 2 bộ nhớ DDRAM và CGROM phối hợp với nhau để hiển thị các ký tự. Như mô tả, công việc của người lập trình điều khiển LCD tương đối đơn giản, đó là viết mã ASCII vào bộ nhớ DDRAM tại đúng vị trí được yêu cầu, bước tiếp theo sẽ do HD44780U đảm nhiệm.

Page 191: asembly
Page 192: asembly

Hình 4. Vùng nhớ CGROM.

2.3 CGRAM.

       CGRAM là vùng nhớ chứa các symbol do người dùng tự định nghĩa, mỗi symbol được có kích thước 5x8 và được dành cho 8 ô nhớ 8 bit. Các symbol thường được định nghĩa trước và được gọi hiển thị khi cần thiết. Vùng này có tất cả 64 ô nhớ nên có tối đa 8 symbol có thể được định nghĩa. Tài liệu này không đề cập đến sử dụng bộ nhớ CGRAM nên tôi sẽ không đi chi tiết phần này, bạn có thể tham khảo datasheet của HD44780U để biết thêm.

3. Điều khiển hiển thị Text LCD.

3.1 Các chân điều khiển LCD.

       Các chân điều khiển việc đọc và ghi LCD bao gồm RS, R/W và EN.        RS (chân số 3): Chân lựa chọn thanh ghi (Select Register), chân này cho phép lựa chọn 1 trong 2 thanh ghi IR hoặc DR để làm việc. Vì cả 2 thanh ghi này đều được kết nối với các chân Data của LCD nên cần 1 bit để lựa chọn giữa chúng. Nếu RS=0, thanh ghi IR được chọn và nếu RS=1 thanh ghi DR được chọn. Chúng ta đều biết thanh ghi IR là thanh ghi chứa mã lệnh cho LCD, vì thế nếu muốn gởi 1 mã lệnh đến LCD thì chân RS phải được reset về 0. Ngược lại, khi muốn ghi mã ASCII của ký tự cần hiển thị lên LCD thì chúng ta sẽ set RS=1 để chọn thanh ghi DR. Hoạt động của chân RS được mô tả trong hình 5.

Hình 5. Hoạt động của chân RS.

       R/W (chân số 4): Chân lựa chọn giữa việc đọc và ghi. Nếu R/W=0 thì dữ liệu sẽ được ghi từ bộ điều khiển ngoài (vi điều khiển AVR chẳng hạn) vào LCD. Nếu R/W=1 thì dữ liệu sẽ được đọc từ LCD ra ngoài. Tuy nhiên, chỉ có duy nhất 1 trường hợp mà dữ liệu có thể đọc từ LCD ra, đó là đọc trạng thái LCD để biết LCD có đang bận hay không (cờ Busy Flag - BF). Do LCD là một thiết bị hoạt động tương đối chậm (so với vi điều khiển), vì thế một cờ BF được dùng để báo LCD

Page 193: asembly

đang bận, nếu BF=1 thì chúng ta phải chờ cho LCD xử lí xong nhiệm vụ hiện tại, đến khi nào BF=0 một thao tác mới sẽ được gán cho LCD. Vì thế, khi làm việc với Text LCD chúng ta nhất thiết phải có một chương trình con tạm gọi là wait_LCD để chờ cho đến khi LCD rảnh. Có 2 cách để viết chương trình wait_LCD. Cách 1 là đọc bit BF về kiểm tra và chờ BF=0, cách này đòi hỏi lệnh đọc từ LCD về bộ điều khiển ngoài, do đó chân R/W cần được nối với bộ điều khiển ngoài. Cách 2 là viết một hàm delay một khoảng thời gian cố định nào đó (tốt nhất là trên 1ms). Ưu điểm của cách 2 là sự đơn giản vì không cần đọc LCD, do đó chân R/W không cần sử dụng và luôn được nối với GND. Tuy nhiên, nhược điểm của cách 2 là khoảng thời gian delay cố định nếu quá lớn sẽ làm chậm quá trình thao tác LCD, nếu quá nhỏ sẽ gây ra lỗi hiển thị. Trong bài này tôi hướng dẫn bạn cách tổng quát là cách 1, để sử dụng cách 2 bạn chỉ cần một thay đổi nhỏ trong chương trình wait_LCD (sẽ trình bày chi tiết sau) và kết nối chân R/W của LCD xuống GND.       EN (chân số 5): Chân cho phép LCD hoạt động (Enable), chân này cần được kết nối với bộ điều khiển để cho phép thao tác LCD. Để đọc và ghi data từ LCD chúng ta cần tạo một “xung cạnh xuống” trên chân EN, nói theo cách khác, muốn ghi dữ liệu vào LCD trước hết cần đảm bảo rằng chân EN=0, tiếp đến xuất dữ liệu đến các chân D0:7, sau đó set chân EN lên 1 và cuối cùng là xóa EN về 0 để tạo 1 xung cạnh xuống.

3.2 Tập lệnh của LCD.

Bảng 2 tóm tắt các lệnh có thể ghi vào LCD

Page 194: asembly

       Danh sách lệnh trên được tôi tô 2 màu khác nhau, các lệnh màu đỏ sẽ được dùng thường xuyên trong lúc hiển thị LCD và các lệnh màu xanh thường chỉ được dùng 1 lần trong lúc khởi động LCD, riêng lệnh Read BF có thể được dùng hoặc không tùy theo cách viết chương trình wait_LCD. Phần tiếp theo tôi giải thích ý nghĩ của các lệnh và tham số kèm theo chúng.       Trước hết là nhóm lệnh đỏ:       - Clear display – xóa LCD: lệnh này xóa toàn bộ nội dung DDRAM và vì thế

Page 195: asembly

xóa toàn bộ hiển thị trên LCD. Vì đây là 1 lệnh ghi Instruction nên chân RS phải được reset về 0 trước khi ghi lệnh này lên LCD. Mã lệnh xóa LCD là 0x01(ghi vào D0:D7).       - Cursor home – đưa con trỏ về vị trí đầu, dòng 1 của LCD: lệnh này thực hiện việc đưa con trỏ về vị trí đầu tiên của bộ nhớ DDRAM, vì thế nếu sau lệnh này một biến được ghi vào DDRAM thì biến này sẽ nằm ở vị trí đầu tiên (1;1). RS cũng phải bằng 0 trước khi ghi lệnh. Mã lệnh là 0x02 hoặc 0x03(chọn 1 trong 2 mã lệnh, tùy ý).       - Set DDRAM address – định vị trí con trỏ cho DDRAM: di chuyển con trỏ đến một vị trí tùy ý trong DDRAM và vì thế có thể được dùng để chọn vị trí cần hiển thị trên LCD. Để thực hiện lệnh này cần reset RS=0. Bit MSB của mã lệnh (D7) phải bằng 1, 7 bit còn lại của mã lệnh chính là địa chỉ DDRAM muốn di chuyển đến. Ví dụ chúng ta muốn di chuyển con trỏ đến vị trí thứ 3 trên dòng 2 của LCD (địa chỉ 42) chúng ta cần ghi mã lệnh 0xAA vì 0xAA=10101010 (binary) trong đó bit MSB bằng 1, bảy bit còn lại là 0101010=42, địa chỉ của ô nhớ muốn đến.       - Write to CGRAM or DDRAM – ghi dữ liệu vào CGRAM hoặc DDRAM: vì đây không phải là lệnh ghi instruction mà là 1 lệnh ghi dữ liệu nên chân RS cần được set lên 1 trước khi ghi lệnh vào LCD. Lệnh này cho phép ghi mã ASCII của một ký tự cần hiển thị vào thanh ghi DDRAM. Trường hợp ghi vào CGRAM không được khảo sát.

       Kế đến là nhóm lệnh màu xanh: nhóm lệnh này thường chỉ thực hiện 1 lần (ít nhất là trong bài học này) và thường được viết chung trong 1 chương trình con khởi động LCD ( chúng ta gọi là init_LCD trong bài học này).       - Entry mode set – xác lập các hiện thị liên tiếp cho LCD: nói một cách dễ hiểu, lệnh này chỉ ra cách mà bạn muốn hiển thị một ký tự tiếp theo 1 ký tự trước đó. Ví dụ nếu bạn muốn hiện thị 2 ký tự liên tiếp AB, trước hết bạn viết A tại vị trí 5, dòng 1. Sau đó bạn ghi B vào LCD, lúc này có 4 cách mà LCD có thể hiển thị B như sau: hiển thị B bên phải A tại vị trí số 6 (cách 1); B cũng có thể được hiển thị bên trái A, tại vị trí số 4(cách 2); hoặc LCD có thể tự dịch chuyển A về bên trái đến vị trí 4 sau đó hiển thị B bên phải A, tại vị trí 5(cách 3); và khả năng cuối cùng là LCD dịch chuyển A về bên phải đến vị trí 6 sau đó hiển thị B bên trái A, tại vị trí 5(cách 4). Chúng ta có thể chọn 1 trong 4 cách hiển thị trên thông qua lệnh Entry mode set. Đây là lệnh ghi Instruction nên RS=0, 5 bit cao D7:3=00000, bit D2=1, hai bit còn lại D1:0 chứa mã lệnh để lựa chọn 1 trong 4 cách hiển thị. Xem lại bảng 2, bit D1 chứa giá trị I/D và D0 chứa S. Trong đó I/D nghĩa là tăng hoặc giảm (Increment or Decrement). I/D= 1 là hiển thị tăng tức ký tự sau sẽ hiển thị bên phải ký tự trước, nếu I/D=0 thì hiển thị giảm, tức ký tự sau hiển thị bên trái  ký tự trước. S là giá trị Shift, nếu S=1 thì các ký tự trước đó sẽ được “đẩy” đi, ký tự

Page 196: asembly

sau chiếm chỗ ký tự trước, ngược lại nếu S=0 thì vị trí hiển thị của các ký tự trước đó không thay đổi. Có thể tóm tắt 4 mode hiển thị ứng với 4 mã lệnh như sau:       + D7:0 = 0x04 (00000100) : hiển thị giảm và không shift (như cách 2 trong ví dụ).       + D7:0 = 0x05 (00000101) : hiển thị giảm và shift (như cách 4 trong ví dụ).       + D7:0 = 0x06 (00000110) : hiển thị tăng và không shift (như cách 1, khuyến khích).       + D7:0 = 0x07 (00000111) : hiển thị tăng và shift (như cách 3 trong ví dụ).       - Display on/off control – xác lập cách hiện thị cho LCD: lệnh này bao gồm các thông số cho phép LCD hiển thị, cho phép hiển thị cursor và mở/tắt blinking. Đây cũng là một lệnh ghi Instrcution nên RS phải bằng 0. Mã lệnh cho lệnh này có dạng  00001DCB trong đó D (Display) cho phép hiển thị LCD nếu mang giá trị 1, C (Cursor) bằng 1 thì cursor sẽ được hiển thị và B là blinking cho cursor tại vị trí hiển thị (blinking là dạng 1 ô đen nhấp nháy tại vị trí ký tự đang hiển thị). Mã lệnh được dùng phổ biến cho lệnh này là 0x0E (00001110 - hiển thị cursor nhưng không hiển thị blinking).       - Function set – xác lập chức năng cho LCD: đây là lệnh thiết lập phương thức giao tiếp với LCD, kích thước font chữ và số lượng line của LCD. RS cũng phải bằng 0 khi sử dụng lệnh này. Mã lệnh function set có dạng 001D¬¬LNFxx. Trong đó nếu DL=1 (DL: Data Length) thì mode giao tiếp 8 bit sẽ được dùng, lúc này tất cả các chân từ D0 đến D7 phải được kết nối với bộ điều khiển ngoài. Nếu DL=0 thì mode 4 bit được dùng, trong trường hợp này chỉ có 4 chân D4:7 được dùng để truyền nhận dữ liệu và kết nối với bộ điều khiển ngoài, các chân D0:3 được để trống. N quy định số dòng của LCD, vì chúng ta đang khảo sát LCD loại hiển thị 2 dòng nên N=1 (N=0 cho trường hợp LCD 1 dòng). F là kích thước font chữ hiển thị, do LCD có 2 bộ font chữ có sẵn trong CGROM nên chúng ta cần lựa chọn thông qua bit F, nếu F=1 bộ font 5x10 được sử dụng và nếu F=0 thì font 5x8 được hiển thị. 2 bit thấp trong mã lệnh này có thể được gán giá trị tùy ý. Mã lệnh được dùng phổ biến cho lệnh function set là 0x38 (00111000 – giao tiếp 8 bit, 2 dòng với font 5x8 ) hoặc 0x28 (00101000 – giao tiếp 4 bit, 2 dòng với font 5x8 ). Ví dụ trong bài này sử dụng cả 2 mã lệnh trên.

3.3 Giao tiếp 8 bit và 4 bit.

       Như trình bày trong lệnh function set, có 2 mode để ghi và đọc dữ liệu vào LCD đó là mode 8 bit và mode 4 bit:       - Mode 8 bit: Nếu bit DL trong lệnh function set bằng 1 thì mode 8 bit được dùng. Để sử dụng mode 8 bit, tất cả các lines dữ liệu của LCD từ D0 đến D7 (từ chân 7 đến chân 14) phải được nối với 1 PORT của chip điều khiển bên ngoài (ví dụ PORTC của ATmega32 trong ví dụ của bài này) như trong hình 3. Ưu điểm của phương pháp giao tiếp này là dữ liệu được ghi và đọc rất nhanh và đơn giản vì chip

Page 197: asembly

điều khiển chỉ cần xuất hoặc nhận dữ liệu trên 1 PORT. Tuy nhiên, phương pháp này có nhược điểm là tổng số chân dành cho giao tiếp LCD quá nhiều, nếu tính luôn cả 3 chân điều khiển thì cần đến 11 đường cho giao tiếp LCD.       - Mode 4 bit: LCD cho phép giao tiếp với bộ điều khiển ngoài theo chế độ 4 bit. Trong chế độ này, các chân D0, D1, D2 và D3 của LCD không được sử dụng (để trống), chỉ có 4 chân từ D4 đến D7 được kết  nối với chip bộ điều khiển ngoài. Các instruction và data 8 bit sẽ được ghi và đọc bằng cách chia thành 2 phần, gọi là các Nibbles, mỗi nibble gồm 4 bit và được giao tiếp thông qua 4 chân D7:4, nibble cao được xử lí trước và nibble thấp sau. Ưu điểm lớn nhất của phương pháp này tối thiểu số lines dùng cho giao tiếp LCD. Tuy nhiên, việc đọc và ghi từng nibble tương đối khó khăn hơn đọc và ghi dữ liệu 8 bit. Trong bài học này, tôi sẽ trình bày 2 chương trình con được viết riêng để ghi và đọc các nibbles gọi là Read2Nib và Write2Nib.

III. AVR và Text LCD.

1. Trình tự giao tiếp Text LCD.

       Trình tự giao tiếp với LCD được trình bày trong flowchart ở hình 6.

Page 198: asembly

Hình 6. Trình tự giao tiếp với Text LCD.

       Để sử dụng LCD chúng ta cần khởi động LCD, sau khi được khởi động LCD đã sẵn sàng để hiển thị. Quá trình khởi động chỉ cần thực hiện 1 lần ở đầu chương trình. Trong bài này, quá trình khởi động được viết trong 1 chương trình con tên int_LCD, khởi động LCD thường bao gồm xác lập cách giao tiếp, kích thước font, số dòng LCD (funcstion set), cho phép hiển thị LCD, sursor…(Display control), chế độ hiển thị tăng/giảm,  shift (Entry mode set). Các thủ tục khác như xóa LCD, viết ký tự lên LCD, di chuyển con trỏ…được sử dụng liên tục trong quá trình hiển thị LCD và sẽ được trình bày trong các đoạn chương trình con riêng.

2. AVR giao tiếp với Text LCD trong WinAVR.

       Phần này tôi trình bày cách điều khiển hiển thị Text LCD bằng vi điều khiển AVR trong môi trường C của WinAVR. Hình thức là một thư viện hàm giao tiếp Text LCD trong 1 file header có tên là myLCD.h. Các hàm trong thư viện bao gồm (chú ý là phần code trong List 0 không nằm trong file myLCD.h).

List 0. Các hàm có trong thư viện myLCD.

12345678910

char Read2Nib();  //đọc 2 nibbles từ LCDvoid Write2Nib(uint8_t chr); //ghi 2 nibbles vào LCDvoid Write8Bit(uint8_t  chr); //ghi trự tiếp 8 bit và LCDvoid wait_LCD();             //chờ LCD rảnhvoid init_LCD();  //khởi động LCDvoid clr_LCD();  //xóa LCDvoid home_LCD();  //đưa cursor về home void move_LCD(uint8_t y, uint8_t x);  //di chuyển cursor đế vị trí mong muốn (dòng, cột)void putChar_LCD(uint8_t chr);                         //ghi 1 ký tự lên LCDvoid print_LCD(char* str, unsigned char len); //hiển thị chuỗi ký tự

       Tuy nhiên, trước khi viết các hàm giao tiếp LCD chúng ta cần định nghĩa một số macro và biến. Hãy tạo 1 file Header có tên myLCD.h và viết các đoạn code bên dưới vào file này (bắt đầu từ List 1).

List 1. Định nghĩa các biến thay thế.

010203040506

#include <util/delay.h>#define sbi(sfr,bit) sfr|=_BV(bit)#define cbi(sfr,bit) sfr&=~(_BV(bit))#define EN                            2#define RW                           1#define RS                            0

Page 199: asembly

070809101112131415161718

#define CTRL                       PORTB#define DDR_CTRL             DDRB

#define DATA_O                  PORTB#define DATA_I                    PINB#define DDR_DATA             DDRB/*#define LCD8BIT#define DATA_O                  PORTD#define DATA_I                    PIND#define DDR_DATA             DDRD*/

       cbi và sbi là 2 macro được dụng để xóa và set 1 bit trong 1 thanh ghi. Ví dụ cbi(PORTA, 5) là xóa bit 5 trong thanh ghi PORT về 0. Do WinAVR không hỗ trợ tuy xuất trực tiếp các bit nên cần định nghĩa 2 macro này hỗ trợ.

       Các biến EN, RW và RS định nghĩa số thứ tự của chân trên 1 PORT của AVR được dùng để kết nối với các chân EN, R/W và RS của LCD. CTRL là biến cho biết PORT nào của AVR được dùng để kết nối với các chân điều khiển của LCD. DDR_CTRL là thanh ghi điều khiển hướng của PORT kết nối với các chân điều khiển, DDR_CTRL luôn phụ thuộc vào biến CTRL. Trong trường hợp của bài này, bạn thấy tôi định nghĩa CTRL là PORTB nghĩa là PORTB được dùng để kết nối với các chân điều khiển LCD, vì CTRL là PORTB nên DDR_CTRL phải là DDRB (thanh ghi điều khiển hướng của PORTB). EN định nghĩa bằng 2 nghĩa là chân EN của LCD được nối với chân 2 của PORTB (PB2), tương tự chân R/W nối với chân 1 PORTB (PB1) và chân RS nối với chân 0 PORTB (PB0). Việc chọn các PORT giao tiếp và thứ tự chân phụ thuộc vào kết nối thật trong mạch điện giao tiếp, bạn phải thay đổi các định nghĩa này cho phù hợp với thiết kế mạch điện của bạn. Lý do cho việc định nghĩa các biến thay thế kiểu này là nhằm tạo ra tính tổng quát cho thư viện hàm. Ví dụ, một người không muốn dùng PORTB để điều khiển LCD mà dùng PORTA thì người này chỉ cần thay đổi định nghĩa ở 2 dòng 7 và 8, không cần thay đổi nội dung các hàm vì trong các hàm này chúng ta chỉ dùng tên thay thế là CTRL và DDR_CTRL. Tương tự, tôi định nghĩa 3 biến thay thế là DATA_O nghĩa là PORT xuất dữ liệu, DATA_I là PORT nhập dữ liệu và DDR_DATA là thanh ghi điều khiển hướng. DATA_O và DATA_I là PORT nối với các chân D0:7 (mode 8 bit) hoặc D4:7 (mode 4 bit) của LCD, đây là các đường truyền và nhận dữ liệu. Trong ví dụ trên, tôi dùng chính PORTB làm đường data vì đây là trường hợp giao tiếp 4 bit, do 3 chân đầu của PORTB kết nối với các chân

Page 200: asembly

điều khiển nên PORTB chỉ còn thừa lại 5 chân, chúng ta sẽ nối 4 chân PB4, PB5, PB6 và PB7 tương ứng với D4, D5, D6 và D7 của LCD. Hình  7 mô tả cách kết nối AVR và LCD theo ví dụ này. Tất nhiên bạn có thể sử dụng PORT khác làm đường data nhất là khi bạn muốn sử dụng mode 8 bit, vì trong mode này cần tới 11 đường giao tiếp (3 điều khiển + 8 data). Phần được che trong 2 dấu comment /* */ là trường hợp bạn muốn dủng mode 8 bit. Để sử dụng mode 8 bit, bạn cần định nghĩa 1 biến có tên LCD8BIT, bit này sẽ báo cho các đoạn chương trình con thực hiện ghi và đọc dữ liệu theo cách 8 bit. Đồng thời, bạn phải định nghĩa lại đường giao tiếp data (DATA_O, DATA_I, DDR_DATA).

Hình 7. Ví dụ Kết nối LCD với AVR trong mode 4 bit (chip mega8).

       Phần bên dưới là phần định nghĩa các hàm trong thư viện myLCD. Bốn hàm đầu tiên (xem lại List 0) là các hàm hỗ trợ, chúng chỉ được dùng bởi các hàm khác trong thư viện và không được gọi trong các chương trình ứng dụng bên ngoài.List 2. Đọc 2 nibbles từ LCD.

0102030405

char Read2Nib(){       char HNib, LNib;       DATA_O |=0xF0;

       sbi(CTRL,EN); //enable 

Page 201: asembly

06070809101112131415

       DDR_DATA &=0x0F; //set 4 bits cao cua PORT DATA lam input        HNib=DATA_I & 0xF0;       cbi(CTRL,EN); //disable

       sbi(CTRL,EN); //enable       LNib = DATA_I & 0xF0;       cbi(CTRL,EN); //disable       LNib>>=4;       return (HNib|LNib);}

       Hàm này thực hiện việc đọc dữ liệu từ LCD ra ngoài, đọc theo từng nibble 4 bit, kết quả trả về là 1 số 8 bit. Hàm này chỉ được dùng duy nhất khi đọc cờ Busy (BF) trong chương trình chờ LCD rảnh (wait_LCD) ở mode 4 bit. Trước hết cần định nghĩa 1 biến tạm HNib (high nibble) và LNib (Low nibble) để chứa 2 nibbles đọc về (dòng 2, List 2). Dòng 5 set chân EN lên mức 1 để chuẩn bị cho LCD làm việc. Chúng ta cần đổi hướng của PORT dữ liệu trên AVR để sẵn sàng nhận dữ liệu về, do chỉ có 4 bit cao của PORT data kết nối với các đường data của LCD (vì đây là mode 4 bit) nên chỉ cần set hướng cho 4 bit này trên AVR, dòng 6 thực hiện việc set hướng. Trong chế độ 4 bit, LCD sẽ truyền và nhận nibble cao trước vì thế dòng 7 đọc dữ liệu từ LCD thông qua các chân DATA_I vào biến HNib, chú ý là chúng ta chỉ cần lấy 4 bit cao của DATA_I nên cần phải dùng giải thuật mặt nạ (mask) che các bit thấp lại (and với 0xF0). Dòng 8 xóa chân EN để chuẩn bị cho bước tiếp theo. Tương tự, các dòng 10, 11 và 12 đọc nibble thấp vào biến LNib. Hai dòng 13 và 14 kết hợp 2 nibbles để tạo thành số 8 bit và trả kết quả về cho đoạn chương trình.

List 3. Ghi 2 nibbles vào LCD.

0102030405060708091011

void Write2Nib(uint8_t chr){       uint8_t HNib, LNib, temp_data;        temp_data=DATA_O & 0x0F; //doc 4 bit thap cua DATA_O de mask,

       HNib=chr & 0xF0;       LNib=(chr<<4) & 0xF0; 

       DATA_O =(HNib |temp_data);        sbi(CTRL,EN); //enable       cbi(CTRL,EN); //disable 

Page 202: asembly

12131415

       DATA_O =(LNib|temp_data);         sbi(CTRL,EN); //enable       cbi(CTRL,EN); //disable}

       Hàm Write2Nib thực hiện ghi một biến 8 bit có tên chr vào LCD theo từng nibble, hàm này được sử dụng rất nhiều lần trong mode 4 bit. Dòng 2 định nghĩa 3 biến tạm là HNib, LNib và temp_data, không giống như khi đọc từ LCD, việc ghi vào LCD có thể làm ảnh hưởng đến các chân của PORT dùng làm đường dữ liệu nhất là khi các đường điều khiển và dữ liệu dùng chung 1 PORT (PORTB). Biến temp_data dùng trong giải thuật mặt nạ để không làm ảnh hưởng đến các bit khác khi ghi LCD. Dòng 3 đọc dữ liệu từ PORT DATA_O và che đi các bit cao, chỉ lưu lại các bit thấp vào biến temp_data vì các bit thấp này không được dùng xuất nhập dữ liệu (xem hình 7, các chân thấp của PORTB dùng làm các chân điều khiển). Để ghi 1 giá trị 8 bit có tên là chr theo cách ghi từng nibbles chúng ta cần tách biến chr thành 2 nibbles. Dòng 5 tách 4 bit cao của chr và chứa vào biến HNib. Dòng 6 thực hiện thêm việc di chuyển 4 bit thấp của chr qua trái rồi gán cho biến LNib. Như vậy sau 2 dòng này các biến HNib và LNib được mô tả như sau:

       Do dữ liêu đã được sắp xếp sẵn sàng ở các vị trí cao (ứng với các chân D4:7) nên công viêc tiếp theo chỉ đơn giản là xuất 2 biến HNib và LNib ra đường DATA_O, cần phải tạo 1 “xung cạnh xuống” ở chân EN mỗi lần xuất dữ liệu (dòng 9, 10). Chú ý là phải xuất nibble cao trước và nibble thấp theo sau.List 4. Ghi 8 bit trực tiếp vào LCD.

0102030405

void Write8Bit(uint8_t chr){       DATA_O=chr;   //out 8 bits to DATA Line       sbi(CTRL,EN); //enable       cbi(CTRL,EN); //disable}

       Đoạn này rất đơn giản là xuất dữ liệu 8 bit ra DATA_O, dùng trong mode 8 bit. Trong mode này, 8 chân data của LCD được nối với 8 đường DATA_O của AVR.

Page 203: asembly

List 5. Chờ LCD rảnh.

010203040506070809101112131415161718192021222324252627282930

void wait_LCD(){       #ifdef LCD8BIT               while(1){                     cbi(CTRL,EN); //xóa EN                     cbi(CTRL,RS);  //đây là Instruction                      sbi(CTRL,RW); //chiều từ LCD ra ngoài

                     DDR_DATA=0xFF; //hướng data out                     DATA_O=0xFF;    // gởi lệnh đọc BF                      sbi(CTRL,EN);     //enable

                     DDR_DATA=0x00; // Đổi hướng data in                     if(bit_is_clear(DATA_I,7)) break;              }              cbi(CTRL,EN); //disable for next step               cbi(CTRL,RW); //ready for next step               DDR_DATA=0xFF; //Ready to Out       #else              char temp_val;              while(1){                     cbi(CTRL,RS); //RS=0, the following data is COMMAND                      sbi(CTRL,RW); //LCD -> AVR                     temp_val=Read2Nib();                     if (bit_is_clear(temp_val,7)) break;              }              cbi(CTRL, RW); //ready for next step               DDR_DATA=0xFF;//Ready to Out        #endif       //_delay_ms(1); }

       Hàm wait_LCD chỉ làm một việc đơn giản là chờ cho đến khi LCD rảnh để gán các công việc khác. Đoạn code trong list 5 trình bày cách 1: đọc cờ Busy Flag và chờ đến khi nó bằng 0 (LCD rảnh). Việc đọc cờ BF phụ thuộc và mode đang sử dụng là 8 bit hay 4 bit, vì thế lệnh #ifdef trong dòng số 2 kiểm tra mode phù hợp trước khi tiến hành đọc. #ifdef LCD8BIT nghĩa là nếu biến LCD8BIT đã được định nghĩa ở phía trên (mode 8 bit được dùng) thì sẽ tiến hành đọc BF theo mode này. Bằng cách kiểm tra sự có mặt của biến LCD8BIT chương trình sẽ biết cách

Page 204: asembly

ghi và đọc LCD phù hợp, phương pháp dùng #ifdef LCD8BIT được áp dụng cho tất cả các hàm sau này. Các đoạn code từ dòng 4 đến 17 thực hiện trong mode 8 bit. Trước khi đọc BF, chúng ta cần gởi 1 lệnh đọc BF ở dòng 9, sau đó ở dòng 12 thực hiện đổi hướng các chân data để nhận giá trị về. Trong dòng 10, kiểm tra bit thứ 7 của DATA_I, DATA_I chính là giá trị đọc về và bit thứ 7 trong giá trị nhận về chính là cờ Busy Flag. Nếu BF=0 (bit_is_clear(DATA_I,7)) thì kết thúc quá trình lặp chờ với lệnh break;. Trong trường hợp mode 4 bit được sử dụng (#else), quá trình kiểm tra cờ BF cũng tương tự, điểm khác nhau duy nhất là cách đọc dữ liệu về có khác, chúng ta dùng hàm Read2Nib đã được viết trước đó để nhận giá trị về (xem dòng 23). Như đã trình bày, chúng ta có thể viết hàm wait_LCD bằng cách dùng hàm delay một khoảng thời gian cố định, trong dòng 29 bạn thấy một hàm _delay_ms(1) không được sử dụng, nếu muốn bạn có thể xóa hết các dòng lệnh trước đó trong hàm wait_LCD và dùng hàm delay này để thay thế, LCD vẫn sẽ hoạt động tốt.

List 6. Khởi động LCD.

010203040506070809101112131415161718192021222324

void init_LCD(){       DDR_CTRL=0xFF;       DDR_DATA=0xFF;//Function set------------------------------------------------------------------------------       cbi(CTRL,RS);   // the following data is COMMAND       cbi(CTRL, RW); // AVR->LCD       cbi(CTRL, EN);       #ifdef LCD8BIT               Write8Bit(0x38);             wait_LCD();       #else             sbi(CTRL,EN); //enable              sbi(DATA_O, 5);               cbi(CTRL,EN); //disable              wait_LCD();                Write2Nib(0x28);//4 bit mode, 2 line, 5x8 font              wait_LCD();        #endif//Display control-------------------------------------------------------------------------        cbi(CTRL,RS); // the following data is COMMAND        #ifdef LCD8BIT               Write8Bit(0x0E);              wait_LCD();       #else

Page 205: asembly

25262728293031323334353637

              Write2Nib(0x0E);              wait_LCD();          #endif//Entry mode set------------------------------------------------------------------------       cbi(CTRL,RS); // the following data is COMMAND        #ifdef LCD8BIT              Write8Bit(0x06);              wait_LCD();       #else              Write2Nib(0x06);              wait_LCD();       #endif}

       Quá trình khởi động gồm 3 bước: function set, display control và entry mode set.        Với function set, ba dòng 5,6 và 7 xác lập các chân điều khiển để chuẩn bị gởi các lệnh. Hai dòng 9 và 10 viết lệnh function set vào LCD theo mode 8 bit. Giá trị 0x38, tức 00111000 là một lệnh xác lập mode 8 bit, LCD 2 dòng và font 5x8. Nếu mode 4 bit được dùng, chúng ta cần viết hàm function set khác đi một chút. Theo mặc định, khi vừa khởi động LCD thì mode 8 bit sẽ được chọn, vì thế nếu một hàm nào đó đươc ghi vào LCD đầu tiên, LCD sẽ cố gắng đọc hết các chân D0:7 để lấy dữ liệu, do trong mode 4 bit các chân D0:3 không được kết nối với AVR nên việc đọc lần đầu có thể dẫn đến sai số. Vì vậy, việc đầu tiên cần làm nếu muốn sử dụng mode 4 bit là gởi một lệnh function set với tham số DL=0 (0010xxxx) đến LCD để báo mode chúng ta muốn dùng. Dòng 13 làm việc này, dòng lệnh chỉ đơn giản set bit D5 nhưng đó chính là gởi lệnh dạng 0010xxxx đến LCD, vì thế LCD sẽ vào mode 4 bit sau lệnh này. Tiếp theo quá trình thao tác với LCD diễn ra bình thường, dòng 16 ghi vào LCD mã của function set, trong trường hợp này là mã 0x28, tức00101000: mode 4 bit, LCD 2 dòng và font 5x8.       Với Display control, mã lệnh được dùng là 0x0E, tức 00001110 trong đó 00001 là mã của lệnh display control, 3 bit theo sau xác lập hiển thị LCD, hiển thị cursor và không blinking.       Với Entry mode set, mã lệnh được dùng là 0x06 tức hiển thị tăng và không shift. Xem lại phần giải thích tập lệnh LCD để hiểu thêm ý nghĩa của mã lệnh 0x06.

List 7. Di chuyển cursor.

01 void home_LCD(){

Page 206: asembly

020304050607080910111213141516171819202122

       cbi(CTRL,RS); // the following data is COMMAND        #ifdef LCD8BIT               Write8Bit(0x02);              wait_LCD();        #else              Write2Nib(0x02);              wait_LCD();         #endif }void move_LCD(uint8_t y,uint8_t x){       uint8_t Ad;       Ad=64*(y-1)+(x-1)+0x80; // tính mã lệnh       cbi(CTRL,RS); // the following data is COMMAND       #ifdef LCD8BIT               Write8Bit(Ad);              wait_LCD();       #else              Write2Nib(Ad);              wait_LCD();       #endif }

       List 7 trình bày 2 hàm di chuyển cursor về home (home_LCD) và di chuyển đến 1 vị trí do người dùng đặt. Hàmhome_LCD tương đối đơn giản vì chỉ cần ghi mã lệnh 0x02 vào LCD thì cursor sẽ tự động di chuyển về  home (vị trí đầu tiên trên LCD).        Hàm move_LCD(uint8_t y,uint8_t x) cho phép di chuyển cursor đến vị trí dòng y, cột x. Điểm cần chú ý trong hàm này là cách tính mã lệnh cần ghi vào LCD. Thực chất đây là lệnh set DDRAM address. Xem lại bảng 2 ta thấy mã lệnh cho lệnh này có dạng 1xxxxxxx trong đó xxxxxxx là một số 7 bit chứa địa chỉ của ô DDRAM chúng ta cần di chuyển đến. Vì thế trước khi thực hiện ghi mã lệnh này, chúng ta cần tính tham số xxxxxxx theo dòng y, cột x. Xem lại tổ chức của DDRAM trong hình 3, giả sử một ô nhớ ở dòng y và cột x trên, do dòng 2 bắt đầu với địa chỉ 64, 2 ô nhớ ở cùng 1 cột trên 2 dòng sẽ cách nhau 64 vị trí (64*(y-1)). Mặt khác do vị trí ô nhớ được tính từ 0 trong khi chúng ta muốn gán tọa độ x bắt đầu từ 1, vì thế chúng ta cần thêm (x-1) vào công thức tính. Cuối cùng chúng ta cần phải thêm mã lệnh set địa chỉ DDRAM, mã 0x80. Giá trị cuối cùng của mã lệnh là : Ad=64*(y-1)+(x-1)+0x80 (dòng 13). Các dòng lệnh tiếp theo trong hàm move_LCD thực hiện ghi giá trị mã lệnh vào LCD. 

Page 207: asembly

       Cuối cùng là phần code hiển thị LCD được trình bày trong list 8. Phần hiển thị bao gồm 1 chương trình con: xóa LCd, hiển thị 1 ký tự và hiển thị 1 chuỗi các ký tự.

List 8. Hiển thị trên LCD.

010203040506070809101112131415161718192021222324252627

void clr_LCD(){ //xóa toàn bộ LCD       cbi(CTRL,RS); //RS=0 mean the following data is COMMAND (not normal DATA)       #ifdef LCD8BIT               Write8Bit(0x01);              wait_LCD();        #else              Write2Nib(0x01);              wait_LCD();         #endif }void putChar_LCD(uint8_t chr){ //hiển thị 1 ký tự chr lên LCD       sbi(CTRL,RS); //this is a normal DATA        #ifdef LCD8BIT               Write8Bit(chr);              wait_LCD();        #else              Write2Nib(chr);              wait_LCD();        #endif }void print_LCD(char* str, unsigned char len){ //Hiển thị 1 chuỗi ký tự       unsigned char i;       for (i=0; i<len; i++)              if(str[i] > 0) putChar_LCD(str[i]);             else putChar_LCD(' ');       }}

       Để xóa toàn bộ LCD chúng ta cần gởi 1 instruction có mã 0x01 đến LCD, hàm clr_LCD() thực hiện việc này. Lưu ý mã lệnh để xóa LCD là 1 instruction, vì thế cần xóa chân RS xuống 0 trước khi gởi mã này xuống LCD (dòng 2 xóa chân RS).  Hàm putChar_LCD(uint8_t chr) hiển thị 1 ký tự lên LCD, giá trị tham số của hàm này là mã ASCII của ký tự cần hiển thị, chr. Nội dung của hàm hoàn toàn giống hàm xóa LCD, chỉ khác đây không phải là 1 instruction nên cần set chân RS

Page 208: asembly

lên 1 trước khi gởi mã lệnh đến LCD (dòng 12). Mã lệnh cho hàm này chính là mã ASCII cần hiển thị. Cuối cùng hàm print_LCD(char* str, unsigned char len) cho phép hiển thị 1 chuỗi ký tự liên tiếp lên LCD, thực chất đây là quá trình lặp của hàm hiển thị 1 ký tự. Chú ý tham số len là chiều dài cần hiển thị của chuỗi.

IV. Ví dụ điều khiển Text LCD bằng thư viện myLCD.

       Phần này tôi sẽ minh họa cách sử dụng thư viện myLCD.h để hiển thị các ký tự lên 1 Text LCD. Sử dụng phần mềm Proteus vẽ một mạch điện gồm 1 LCD 2x16 (keyword: LM016L), 1 chip Atmega32 và 1 biến trở (POT-LIN) như trong hình 8. Tạo 1 Project bằng WinAVR có tên là TextLCD_Demo và tạo file source là main.c, tạo makefile với khai báo sữ dụng chip ATmega32 và clock 8MHz. Copy file myLCD.h vào thư mục của Project mới tạo. Viết code cho file main.c như trong list 9. Chú ý các định nghĩa chân kết nối với LCD trong phần đầu file myLCD.h phải giống với kết nối thật trong hình 8.

Page 209: asembly

Hình 8. Mạch điện mô phỏng LCD với AVR.

List 8. Chương trình demo điều khiển TextLCD, main.c.

#include <avr/io.h>#include <util/delay.h>#include "myLCD.h" //include thư viện myLCD

int main(){        init_LCD(); //khởi độ LCD

Page 210: asembly

       clr_LCD(); // xóa toà bộ LCD

       putChar_LCD(' '); //ghi 1 khoảng trắng       putChar_LCD(' '); //ghi 1 khoảng trắng        putChar_LCD('D'); //Hiển thị kýtự 'D'       print_LCD("emo of the",10); //hiển thị 1 chuỗi ký tự        move_LCD(2,1); //di chuyển cursor đến dòng 2, cột đầu tiên       print_LCD("2x16 LCD Display",16); //hiển thị chuỗi thứ 2       while(1){

       };}

       Để sử dụng thư viện myLCD, chúng ta cần include file myLCD.h vào Project như trong dòng 3, #include"myLCD.h". Hai dòng 6 và 7 thực hiện khởi động và xóa LCD. Sau đó, các dòng 9, 10 và 11 đặt 3 ký tự là các khoảng trắng và chữ cái D bằng hàm putChat_LCD. Dòng 12 in  chuỗi “emo of the” ngay tiếp theo chữ cái D trước đó bằng hàm print_LCD. Dòng 13 thực hiện di chuyển cursor đến vị trí dòng thứ 2, cột đầu tiên của LCD trước khi tiến hành in chuỗi thứ 2 “2x16 LCD Display” ở dòng code 14. Nếu bạn thực hiện đúng trình tự như trên, kết quả thu được sẽ như trong hình 8. 

Graphic LCD

1 2 3 4 5

 ( 23 Votes )Nội dung Các bài cần tham khảo trước

1. Bạn sẽ đi đến đâu.

2. Graphic LCD.

3. AVR và Graphic LCD .

Cấu trúc AVR

WinAVR .

C cho AVR.

Page 211: asembly

4. Ví dụ   điều khiển Graphic LCD   bằng thư viện   myGLCD .

Download ví dụ

Download phần mềm G.Edit

Mô phỏng với Proteus.

Giới thiệu phần mềm G.Edit

I. Bạn sẽ đi đến đâu.    

     Trong bài ứng dụng này tôi trình bày về cấu trúc và cách điều khiển Graphic LCD loại dot không màu. Công cụ chính cũng là 2 bộ phần mềm quen thuộc WinAVR, Proteus và phần mềm biên tập Graphic LCD, G.Edit.       Sau bài này, tôi hy vọng bạn có thể hiểu và thực hiện được:       - Cấu trúc Graphic LCD 128x64 và chip điều khiển KS0108.       - Nguyên lý hoạt động Graphic LCD.       - Phát triển 1 thư viện điều khiển Graphic LCD 128x64 cho AVR.       - Ví dụ điều khiển Graphic LCD 128x64 bằng AVR.

II. Graphic LCD.

      Graphic LCD (gọi tắt là GLCD) loại chấm không màu là các loại màn hình tinh thể lỏng nhỏ dùng để hiển thị chữ, số hoặc hình ảnh. Khác với Text LCD, GLCD không được chia thành các ô để hiển thị các mã ASCII vì GLCD không có bộ nhớ CGRAM (Character Generation RAM). GLCD 128x64 có 128 cột và 64 hàng tương ứng có 128x64=8192 chấm (dot). Mỗi chấm tương ứng với 1 bit dữ liệu, và như thế cần 8192 bits hay 1024 bytes RAM để chứa dữ liệu hiển thị đầy mỗi 128x64 GLCD. Tùy theo loại chip điều khiển, nguyên lý hoạt động của GLCD có thể khác nhau, trong bài này tôi giới thiệu loại GLCD được điều khiển bởi chip KS0108 của Samsung, có thể nói GLCD với KS0108 là phổ biến nhất trong các loại GLCD loại này (chấm, không màu). Hình 1 là hình ảnh thật của 1 GLCD 128x64 điều khiển bởi KS0108.

Page 212: asembly

Hình 1. Graphic LCD 128x64.

      Chip KS0108 chỉ có 512 bytes RAM (4096 bits = 64x64) và vì thế chỉ điều khiển hiển thị được 64 dòng x 64 cột. Để điều khiển GLCD 168x64 cần 2 chip KS0108, và thực thế trong các loại GLCD có 2 chip KS0108, GLCD 128x64 do đó tương tự 2 GLCD 64x64 ghép lại.  Chúng ta sẽ lần lượt khảo sát sơ đồ chân, cấu trúc bộ nhớ và nguyên lý hoạt động của GLCD, chip KS0108 trong phần tiếp theo.

1. Sơ đồ chân GLCD 128x64. 

       Các GLCD 128x64 dùng KS0108 thường có 20 chân trong đó chỉ có 18 chân là thực sự điều khiển trực tiếp GLCD, 2 chân (thường là 2 chân cuối 19 và 20) là 2 chân Anode và Cathode của LED nền. Trong 18 chân còn lại, có 4 chân cung cấp nguồn và 14 chân điều khiển+dữ liệu. Khác với các Text LCD HD44780U, GLCD KS0108 không hỗ trợ chế độ giao tiếp 4 bit, do đó bạn cần dành ra 14 chân để điều khiển 1 GLCD 128x64. Sơ đồ chân phổ biến của GLCD 128x64 được mô tả trong bảng 1. Bảng 1. Sơ đồ chân GLCD GDM-12864-04.

Page 213: asembly
Page 214: asembly

      Chú ý là trên một số GLCD, thứ tự các chân có thể khác (như GLCD WG12864A2…) nhưng số lượng và chức năng chân thì không đổi. Hình 2 mô tả cách kết nối GLCD với nguồn và mạch điều khiển.

Hình 2. Kết nối GLCD. 

     Chân VSS  được nối trực tiếp với GND, chân VDD nối với nguồn +5V, một biến trở khoảng 20K được dùng để chia điện áp giửa Vdd và Vee cho chân Vo, bằng cách thay đổi giá trị biến trở chúng ta có thể điều chỉnh độ tương phản của GLCD. Các chân điều khiển RS, R/W, EN và các đường dữ liệu được nối trực tiếp với vi điều khiển. Riêng chân Reset (RST) có thể nối trực tiếp với nguồn 5V.

      EN (Enable): cho phép một quá trình bắt đầu, bình thường chân EN được giữ ở mức thấp, khi một thực hiện một quá trình nào đó (đọc hoặc ghi GLCD), các chân điều khiển khác sẽ được cài đặt sẵn sàng, sau đó kích chân EN lên mức cao. Khi EN được kéo lên cao, GLCD bắt đầu làm thực hiện quá trình được yêu cầu, chúng ta cần chờ một khoảng thời gian ngắn cho GLCD đọc hoặc gởi dữ liệu. Cuối cùng là kéo EN xuống mức thấp để kết thúc quá trình và cũng để chuẩn bị chân EN cho quá trình sau này.

      RS (Register Select): là chân lựa chọn giữa dữ liệu (Data) và lệnh (Instruction), vì thế mà trong một số tài liệu bạn có thể thấy chân RS được gọi là chân DI (Data/Instruction Select). Chân RS=1 báo rằng tín hiệu trên các đường DATA (D0:7) là dữ liệu ghi hoặc đọc từ RAM của GLCD. Khi RS=0, tín hiệu trên đương DATA là một mã lệnh (Instruction).

Page 215: asembly

      RW (Read/Write Select): chọn lựa giữa việc đọc và ghi. Khi RW=1, chiều truy cập từ GLCD ra ngoài (GLCD->AVR). RW=0 cho phép ghi vào GLCD. Giao tiếp với GLCD chủ yếu là quá trình ghi (AVR ->GLCD), chỉ duy nhất trường hợp đọc dữ liệu từ GLCD là đọc bit BUSY và đọc dữ liệu từ RAM. Đọc bit BUSY thì chúng ta đã khảo sát cho Text LCD, bit này báo GLCD có đang bận hay không, việc đọc này sẽ được dùng để viết hàm wait_GLCD. Đọc dữ liệu từ RAM của GLCD là một khả năng mới mà Text LCD không có, bằng việc đọc ngược từ GLCD vào AVR, chúng ta có thể thực hiện nhiều phép logic hình (hay mặt nạ, mask) làm cho việc hiển thị GLCD thêm thú vị.      

     CS2 và CS1  (Chip Select): như tôi đã trình bày trong phần trên, mỗi chip KS0108 chỉ có khả năng điều khiển một GLCD có kích thước 64x64, trên các GLCD 128x64 có 2 chip KS0108 làm việc cùng nhau, mỗi chip đảm nhiệm một nữa LCD, 2 chân CS2 và CS1 cho phép chọn một chip KS0108 để làm việc. Thông thường nếu CS2=0, CS1=1 thì nửa trái được kích hoạt, ngược lại khi CS2=1, CS1=0 thì nửa phải được chọn. Chúng ta sẽ hiểu rõ hơn cách phối hợp làm việc của 2 nửa GLCD trong phần khảo sát bộ nhớ của LCD.

2. Tổ chức bộ nhớ. 

       Chip KS0108 có một loại bộ nhớ duy nhất đó là RAM, không có bộ nhớ chứa bộ font hay chứa mã font tự tạo như chip HD44780U của Text LCD. Vì vậy, dữ liệu ghi vào RAM sẽ được hiển thị trực tiếp trên GLCD. Mỗi chip KS0108 có 512 bytes RAM tương ứng với 4096 chấm trên một nửa (64x64) LCD. RAM của KS0108 không cho phép truy cập từng bit mà theo từng byte, điều này có nghĩa là mỗi lần chúng ta viết một giá trị vào một byte nào đó trên RAM của GLCD, sẽ có 8 chấm bị tác động, 8 chấm này nằm trên cùng 1 cột. Vì lý do này, 64 dòng GLCD thường được chia thành 8 pages, mỗi page có độ cao 8 bit và rộng 128 cột  (cả 2 chip gộp lại). Hình 3 mô tả “bề mặt” một GLCD và cũng là cách sắp xếp RAM của các chip KS0108.

Page 216: asembly

Hình 3. Tổ chức của RAM.  

      Tổ chức RAM của 2 chip KS0108 trái và phải hoàn toàn tương tự, việc đọc hay ghi vào RAM của 2 chip cũng được thực hiện như nhau. Chúng ta sẽ chọn nửa trái GLCD để khảo sát. Như bạn thấy trên hình 3, 64 dòng từ trên xuống dưới được chia thành 8 “dãy” mà ta gọi là 8 pages. Page trên cùng là page 0 và page  dưới cùng la page 7. Trong các GLCD, page còn được gọi là địa chỉ X (X address), hay nói cách khác X=0 là địa chỉ của page trên cùng, tương tự như thế, X=7 là địa chỉ của page dưới cùng. Mỗi page chứa 64 cột (chỉ xét 1 chip KS0108), mỗi cột là một byte RAM 8 bit, mỗi bit tương ứng với 1 chấm trên LCD, bit có trọng số thấp (LBS - tức bit D0 như trong hình 3) tương ứng với chấm trên cao nhất. Bit có trọng số cao nhất (MBS - tức bit D7 như trong hình 3) tương ứng với chấm thấp nhất trong 1 page. Thứ tự các cột trong 1 page gọi là địa chỉ Y (Y address), như thế cột đầu tiên có địa chỉ Y = 0 trong khi cột cuối cùng có địa chỉ Y là 63. Bằng cách phối hợp địa chỉ X và địa chỉ Y chúng ta xác định được vị trí của byte cần đọc hoặc ghi. Chip KS0108, tất nhiên, sẽ hỗ trợ các lệnh di chuyển đến địa chỉ X và Y để ghi hay đọc RAM. Hãy quan sát hình 4 để xem cách mà một chữ cái ‘a’ được hiển thị trên GLCD.

Page 217: asembly

Hình 4. Hiển thị chữ cái ‘a’ trên GLCD.  

     Trong cách hiển thị ở hình 4, chữ ‘a’ chỉ nằm trong page 0, tức X=0. Muốn hiển thị chữ cái ‘a’ chúng ta cần ghi vào các cột (địa chỉ Y) của page 0 lần lượt các giá trị như sau: 0, 228, 146, 74, 252 và 128…., xem bảng bên dưới.

 3. Tập lệnh cho chip KS0108. 

       Bảng 2 tóm tắt các lệnh của chip KS0108.

Page 218: asembly

      So với HD44780U của Text LCD, lệnh cho KS0108 của GLCD đơn giản và ít hơn và vì thế viết chương trình điều khiển GLCD cũng tương đối dễ hơn Text LCD. Có tất cả 7 lệnh (Instruction) có thể giao tiếp với KS0108. Tôi sẽ lần lượt giải thích ý nghĩa và cách sử dụng của từng lệnh.

      - Display ON/OFF – Hiển thị GLCD: lệnh này cho phép GLCD hiển thị nội dung trên RAM ra “bề mặt” GLCD. Để viết lệnh này cho GLCD, 2 chân RS và RW cần được kéo xuống mức thấp (RS=0: đây là Instrucion, RW=0: AVR->GLCD). Mã lệnh (code) được chứa trong 7 bit cao (D7:1) và bit D0 chứa thông số. Quan sát bảng 2, dễ thấy mã lệnh nhị phân cho Display ON/OFF là 0011111x (0x3E+x) trong đó x=1: cho phép GLCD hiển thị, x=0: tắt hiển thị.

       - Set Address – chọn địa chỉ: đúng hơn đây là lệnh chọn cột hay chọn địa chỉ Y. Hai bit D7 và D6 chứa mã lệnh (01000000=0x40=64) và 6 bit còn lại chứa chỉ số của cột muốn di chuyển đến. Chú ý là mỗi nửa GLCD có 64 cột nên cần 6 bit để chứa chỉ số này (26=64). Vậy lệnh này có dạng 0x40+Y. Ví dụ nếu chúng ta muốn di chuyển đến cột 36 chúng ta ghi vào GLCD mã lệnh: 0x40+36. Hai chân RS và RW được giữ ở mức thấp khi thực hiện lệnh này.

Page 219: asembly

      - Set Page – chọn trang: lệnh cho phép chọn page (hay địa chỉ X) cần di chuyển đến, do GLCD chỉ có 8 pages nên chỉ cần 3 bit để chứa địa chỉ page. Mã lệnh cho lệnh này có dạng 0xB8+X. Trong đó biến X là chỉ số page cần di chuyển đến. Hai chân RS và RW được giữ ở mức thấp khi thực hiện lệnh này.

     - Display Start Line – chọn line đầu tiên: hay còn gọi là lệnh “cuộn”, lệnh này cho phép di chuyển toàn bộ hình ảnh trên GLCD (hay RAM) lên phía trên một số dòng nào đó, chúng ta gọi là LOffset. Số lượng LOffset có thể từ 0 đến 63 nên cần 6 bit chứa giá trị này. Mã lệnh Display Start Line có dạng 0xC0+LOffset. Hai chân RS và RW được giữ ở mức thấp khi thực hiện lệnh này. Khi di chuyển GLCD lên phía trên, phần dữ liệu phía trên bị che khuất sẽ  “cuộn” xuống phía dưới. Hình 5 là một ví dụ “cuộn” GLCD lên 20 dòng. 

     - Status Read – đọc trạng thái GLCD: đây là một trong 2 lệnh đọc từ GLCD. Cũng giống như với Text LCD, lệnh đọc trạng thái GLCD chủ yếu để xét bit BUSY (bit  thứ 7) xem GLCD có đang bận hay không, lệnh này sẽ được dùng để viết một hàm wait_GLCD chờ cho đến khi GLCD rảnh. Vì đây là lệnh đọc từ GLCD nên chân RW phải được set lên mức 1 trước khi thực hiện, chân RS vẫn ở mức thấp (đọc Instruction).

      - Write Display Data – ghi dữ liệu cần hiển thị vào GLCD hay RAM: vì đây là 1 lệnh ghi dữ liệu hiển thị nên chân RS cần được set lên 1 trước khi thực hiện, chân RW giữ ở mức 0. Lệnh này cho phép ghi một byte dữ liệu vào RAM của KS0108 và cũng là dữ liệu sẽ hiển thị lên GLCD tại vị trí hiện hành của 2 con trỏ địa chỉ X và Y. 8 bit dữ liệu này sẽ tương ứng với 8 chấm trên cột Y ở page X. Chú ý là sau lệnh Write Display Data, địa chỉ cột Y tự động được tăng lên 1 và vì thế nếu có một dữ liệu mới được ghi, dữ liệu mới sẽ không “đè” lên dữ liệu cũ. Việc tăng tự động địa chỉ Y rất có lợi cho việc ghi dữ liệu liên tiếp, nó giúp giảm thời

Page 220: asembly

gian set lại địa chỉ cột Y. Sau khi thực hiện ghi ở cột Y=63 (cột cuối cùng trong 1 page, đối với 1 chip KS0108), Ysẽ về 0.

     - Read Display Data – đọc dữ liệu hiển thị từ GLCD (cũng là dữ liệu từ RAM của KS0108): lệnh đọc này mới so với Text LCD, nó cho phép chúng ta đọc ngược 1 byte dữ liệu từ RAM của KS0108 tại vị trí hiện hành về AVR. Sau khi đã đọc được giá trị tại vị trí hiện hành, chúng ta có thể thực hiện các phép Logic như đảo bit, or hay and…làm tăng khả năng thao tác hình ảnh. Trước khi thực hiện đọc chúng ta cần di chuyển đến vị trí muốn đọc bằng 2 lệnh set địa chỉ X và Y, sau khi đọc giá trị địa chỉ page X và cột Y không thay đổi, do đó nếu đọc tiếp mà không di chuyển địa chỉ thì vẫn thu được giá trị cũ. 

III. AVR và Graphic LCD.

1. Trình tự giao tiếp GLCD. 

         So với Text LCD thì việc giao tiếp với GLCD dễ hơn nhiều vì GLCD có ít Instruction hơn, GLCD chỉ có một loại bộ nhớ là RAM tương ứng trực tiếp với màn hình hiển thị, GLCD không có cursor nên không cần set cursor, GLCD chỉ hỗ trợ giao tiếp 8 bit nên không cần bận tâm chọn mode, quá trình khởi động cho GLCD vì thể rất đơn giản bằng cách gọi lênh DISPLAY ON/OFF. Trong hình 5 tôi trình bày quá trình khởi động và sử dụng GLCD.

Hình 5. Trình tự giao tiếp với GLCD. 

       Sau khi khởi động GLCD bằng hàm DISPLAY ON chúng ta có thể set địa chỉ X và Y để ghi dữ liệu, thậm chí có thể ghi dữ liệu mà không cần set X, Y. Tuy nhiên, cần nhắc lại là có đến 2 chip KS0108 trên GLCD 128x64, vì vậy tất cả các quá trình đều phải thực hiện cho 2 chip.

Page 221: asembly

2. AVR giao tiếp với GLCD trong WinAVR. 

       Phần này tôi trình bày cách điều khiển hiển thị GLCD 128x64 bằng vi điều khiển AVR trong môi trường C của WinAVR. Hình thức là một thư viện hàm giao tiếp GLCD trong 1 file header có tên là myGLCD.h. Các hàm trong thư viện bao gồm (chú ý là phần code trong List 0 không nằm trong file myGLCD.h): 

List 0. Các hàm có trong thư viện myGLCD.

       Trước khi viết các hàm giao tiếp LCD chúng ta cần định nghĩa một số macro và biến. Hãy tạo 1 file Header có tên myGLCD.h và viết các đoạn code bên dưới vào file này (bắt đầu từ List 1).

List 1. Định nghĩa các biến thay thế

Page 222: asembly

        Do GLCD không hỗ trợ bộ font, nếu muốn hiển thị các ký tự chúng ta cần định nghĩa chúng trong một bảng font (tương tự trường hợp ma trận LED), file font.h đã được tạo trước và include vào thư viện myGLCD (dòng 2). Đồng thời, bộ font sẽ được chứa trong bộ nhớ chương trình (FLASH) nên cần các hàm hỗ trợ đọc FLASH, chúng ta include file pgmspace.h phục vụ cho việc này (dòng 3). 

Page 223: asembly

       cbi và sbi là 2 macro được dụng để xóa và set 1 bit trong 1 thanh ghi. Ví dụ cbi(PORTA, 5) là xóa bit 5 trong thanh ghi PORTA về 0. Do WinAVR không hỗ trợ tuy xuất trực tiếp các bit nên cần định nghĩa 2 macro này hỗ trợ (dòng 5, 6). 

       Tám đường DATA sẽ được dành cho 1 PORT, các dòng 8, 9 và 10 định nghĩa PORT trên AVR dành cho DATA, trong ví dụ này là PORTB. Tương tự các đường điều khiển cũng nằm trên cùng 1 PORT, các dòng 14, 15, 16 định nghĩa PORT dành cho các đường điều khiển (PORTD chẳng hạn), sau đó chúng ta định nghĩa thứ tự chân trên PORT điều khiển kết nối với các chân EN, RW, RS, CS1 và CS2 của GLCD (xem các dòng từ 18 đến 22). Chúng ta định nghĩa tiếp 2 macro để kích hoạt và stop GLCD ở các dòng 25 và 26 vì các hoạt động này được dùng rất nhiều khi giao tiếp với GLCD. 

     Tiếp theo chúng ta định nghĩa 4 mã lệnh (Instruction code) của 4 hàm Display on/off, Set Address, Set page và Display Start Line mà tôi đã trình bày ở trên (các dòng từ 29 đến 32). Cuối cùng là định nghĩa vị trí bit BUSY khi đọc trạng thái GLCD.

     Sau phần định nghĩa chúng ta sẽ bắt đầu viết code truy cập GLCD, đoạn code trình bày trong List 2 chứa các hàm hỗ trợ.

List 2. Các hàm hỗ trợ.

Page 224: asembly

      Hàm GLCD_Delay() thực hiện delay khoảng 16 chu kỳ máy, hàm này được dùng để chờ LGCD đọc hay ghi dữ liệu sau khi chân EN được kích. Một nét mới ở đây là tôi sử dụng ngôn ngữ ASM chèn vào C, dù chỉ là chèn hàm nop nhưng nó nói cho bạn biết rằng avr-gcc cho phép chúng ta chèn ASM, tôi sẽ trình bày chi tiết các ví dụ chèn ASM phức tạp hơn trong một bài khác.

        Hàm GLCD_OUT_Set() ở dòng 5 set các PORT giao tiếp trên AVR (DATA và Control) có hướng Ouput. Hàm GLCD_IN_Set() ở dòng 12 set các PORT giao tiếp có hướng Input (dùng khi đọc từ GLCD -> AVR). Hàm GLCD_SetSide(char

Page 225: asembly

Side) ở dòng 19 chọn chip KS0108 trái hoặc phải để thao tác, trong đó Side=1 thì một nửa GLCD bên phải được chọn bằng cách reset bit CS1=0 và CS2=1 (các dòng 22, 23), ngược lại nửa bên trái được kích hoạt, CS1=1, CS2=0 (dòng 26 và 27).

       List 3 trình bày phần code cho 4 hàm truy cập Instruction GLCD cơ bản viết lại cho các hàm Status Read, Display On/Off, Set Address, Set page và Display Start Line trích từ bảng 2. 

List 3. Các hàm truy cập Instruction.

Page 226: asembly
Page 227: asembly

       Tất cả các hàm trong List 3 đều truy cập Instruction nên chân RS luôn được kéo xuống mức thấp, trong 5 hàm trên, hàm wait_GLCD sử dụng Instruction đọc trạng thái từ GLCD nên chân RW sẽ được kéo lên cao, trong 4 hàm còn lại chân RW ở mức thấp. 

      Hàm wait_GLCD(void), cũng tương tự như hàm wait_LCD() trong trường hợp Text LCD, hàm này chờ GLCD rảnh bằng cách đọc trạng thái GLCD và kiểm tra bit BUSY, nếu BUSY bằng 1 thì GLCD đang bận, BUSY=0 tức GLCD rảnh. Các dòng 4, 5, 6 chuẩn bị các đường DATA, RS, RW cho quá trình đọc Instruction từ GLCD (RS=0, RW=0), ở dòng 8 chân EN được kéo lên cao bằng macro GLCD_ENABLE (định nghĩa trong list 1). Nhắc lại chức năng của chân EN, khi EN=1 GLCD bắt đầu quá trình giao tiếp do các chân RS, RW xác lập (trong trường hợp này là đọc Instruction từ GLCD), chúng ta cần chờ một khoảng thời gian ngắn cho GLCD đẩy thanh ghi trạng thái ra các đường DATA bằng hàm GLCD_Delay() trong dòng 9. Tiếp theo gọi GLCD_DISABLE để kéo chân EN xuống mức 0 để kết thúc quá trình đọc (một xung đã được tạo trên chân EN), và bắt đầu kiểm tra bit BUSY. Dòng 12 là một vòng lặp while kiểm tra xem nếu bit BUSY trong giá trị đọc về (giá trị đọc về chứa trong thanh ghi PIN của PORT DATA trên AVR), nếu BUSY=1 (bit_is_set…) vòng lặp tiếp tục với việc tạo một xung khác trên chân EN (các dòng ) rồi quay lại kiểm tra bit BUSY. Nếu BUSY bằng 0, GLCD đã rảnh, vòng lặp while được giải thoát, quá trình chờ kết thúc.

      Hàm GLCD_SetDISPLAY(uint8_t ON) cho phép GLCD hiển thị khi tham số ON=1, hoặc tắt khi tham số ON=0. Trước khi set GLCD chúng ta cần chờ cho GLCD rảnh bằng cách gọi hàm wait_GLCD() ở dòng 20, sau đó xác lập các chân RS, RW sẵn sàng cho quá trình gởi mã lệnh vào GLCD (dòng 21, 22 và 23). Trước khi kích hoạt quá trình, cần chuẩn bị mã lệnh sẵn sàng trên đường dữ liệu, dòng 25: GLCD_DATA_O=GLCD_DISPLAY+ON, trong đó GLCD_DISPLAY là mã lệnh của hàm Display On/Off được định nghĩa trong List 1, biến ON báo GLCD tắt hay mở. Sau khi mọi thứ đã sẵn sàng, một xung được tạo ra trên chân EN (các dòng từ 26 đến 28). Quá trình set Display thực hiện và kết thúc.

     Hàm void GLCD_SetYADDRESS(uint8_t Col) là hàm viết lại cho Insrtuction chọn địa chỉ Y (cột) cần thao tác, tham số Col trong hàm này chính là chỉ số cột, Col có giá trị từ 0 đến 63. Nội dung hàm này hoàn toàn giống hàmGLCD_SetDISPLAY, chỉ có một điểm khác duy nhất là mã hàm khác, mã GLCD_YADDRESS được dùng (xem dòng 36: GLCD_DATA_O = GLCD_YADDRESS+Col).

      Hàm void GLCD_SetXADDRESS(uint8_t Line) là hàm viết lại cho Insrtuction chọn địa chỉ X (page) cần thao tác, tham số Line trong hàm này chính là chỉ số page, Line có giá trị từ 0 đến 8. Nội dung hàm này hoàn toàn giống

Page 228: asembly

hàmGLCD_SetXADDRESS, nhưng mã GLCD_XADDRESS được dùng thay cho GLCD_YADDRESS,(xem dòng 36:GLCD_DATA_O = GLCD_XADDRESS+Line).

      Hàm void GLCD_StartLine(uint8_t Offset) là hàm viết lại cho Insrtuction “cuộn” GLCD, chỉ số Offset là giá trị “cuộn” lên. Xem lại ví dụ hình cuộn GLCD trong phần giải thích của lệnh Display Start Line, với trường hợp này hàm GLCD_StartLine(20) đã được gọi.

List 4 trình bày 2 hàm viết và đọc dữ liệu hiển thị lên GLCD.

List 4. Các hàm thao tác dữ liệu.

Page 229: asembly

       Hai hàm trong list 4 thao tác dữ liệu hiển thị trên GLCD nên chân RS phải được set bằng 1.

      Hàm GLCD_WriteDATA(uint8_t DATA) ghi một byte vào RAM của KS0108, byte này cũng sẽ được hiển thị lên GLCD, vị trí ghi vào là vị trí hiện hành của con trỏ X và Y (ảnh hưởng bởi các quá trình ghi trước đó hoặc do các hàm set địa chỉ), tham số DATA là byte cần ghi. Nội dung bên trong hàm này cũng giống

Page 230: asembly

nhứ các hàm trong list 3. Điểm khác là chân RS được kéo lên để báo đây là quá trình thao tác dữ liệu (dòng 6: sbi(GLCD_CTRL_O, GLCD_RS)). Giá trị gởi đến GLCD chính là tham số DATA như trong dòng 8: GLCD_DATA_O=DATA.

       Hàm uint8_t GLCD_ReadDATA(void) đọc giá trị hiển thị trên từ GLCD vào AVR, chân RW cần được set lên 1 để báo quá trình này là đọc (dòng 22: sbi(GLCD_CTRL_O, GLCD_RW)). Chân EN được kích lên 1 trước (dòng 24:GLCD_ENABLE;) và chờ một khoảng thời gian ngắn trước khi đọc giá trị từ các đường DATA vào một biến tạm DATA như trong dòng 26: DATA=GLCD_DATA_I;. Sau khi trả giá trị về bằng dòng lệnh 30: return DATA, thì quá trịnh đọc kết thúc.

      Với các hàm đã tạo chúng ta đã có thể điều khiển để hiển thị GLCD, các chương trình con trong List 5 và List 6 sử dụng các hàm trên để thực hiện một số nhiệm vụ hiển thị cơ bản. Chúng ta gọi là các chương trình con mở rộng.

List 5. Các chương trình con mở rộng.

Page 231: asembly

       Hàm void GLCD_Init(void) khởi động GLCD. Trước hết, chúng ta phải chọn chip KS0108 để khởi động, dòng 4: GLCD_SetSide(0) nghĩa là chọn chip KS0108 bên trái tức nửa trái GLCD.  Chúng ta khởi động nửa trái GLCD bằng việc cho phép hiển thị (dòng 5: GLCD_SetDISPLAY(1)), di chuyển con trỏ về vị trí đầu tiên trên GLCD với 2 hàm chọn địa chỉ ở các dòng 6 và 7, chọn giá trị cuộn là 0 ở

Page 232: asembly

dòng 8: GLCD_StartLine(0). Sau đó lặp lại quá trình khởi động cho nửa phải của GLCD (xem các dòng từ 10 đến 14).

       Hàm void GLCD_GotoXY(uint8_t Line, uint8_t Col) di chuyển con trỏ hiển thị đến địa chỉ X và Y. Tham số Line là địa chỉ X (tức là page, giá trị từ 0 đến 7), tham số Col là địa chỉ Y hay chính là cột. Hàm này cho phép di chuyển trên toàn bộ GLCD, nghĩa là biến Col có khoảng giá trị từ 0 đến 127, vì thế trước hết chúng ta phải xác định vị trí cần duy chuyển đến thuộc nửa nào của GLCD, nếu Col<64 thì vị trí đó thuộc nửa trái, ngược lại nó thuộc về nửa phải. Dòng 19 chúng ta chia Col cho 64 và gán phần nguyên kết quả cho 1 biến tạm tên là Side (Side=Col/64 ), rõ ràng nếu Col<64 thì Side=0, ngược lại Side=1. Biến Side được dùng làm tham số cho hàm GLCD_SetSide(Side) ở dòng 20, với cách thực hiện này chúng ta đã tự động chọn nửa GLCD mà điểm cần di chuyển đến thuộc vào. Do hàm chọn địa chỉ Y (hàm GLCD_SetYADDRESS xét ở trên) chỉ chọn địa chỉ trong phạm vi 1 nửa LCD, nên chúng ta cần cập nhật lại giá trị của cột Col, dòng 21 thực hiện việc này: Col -= 64*Side. Sau dòng 21, giá trị Col được cập nhật lại từ 0 đến 63 và được chọn làm cột khi hàm GLCD_SetYADDRESS(Col) ở dòng 22 được gọi. Cuối cùng là chọn địa chỉ X ở dòng 23:GLCD_SetXADDRESS(Line).

       Hàm void GLCD_Clr(void) xóa toàn bộ màn hình GLCD (cả 2 nửa GLCD). Mấu chốt của việc xóa GLCD là viết giá trị 0 vào tất cả các vị trí trong RAM, câu lệnh: GLCD_WriteDATA(0) ở 2 dòng 29 và 33 thực hiện điều này. Quá trình xóa được thực hiện trên từng chip KS0108, có 2 vòng vặp for được dùng là vì thế, chú ý dòng lệnh 28:GLCD_GotoXY(Line,0) đưa con trỏ về cột đầu của page thứ “Line”, nửa trái GLCD. Trong khi đó, dòng lệnh 32:GLCD_GotoXY(Line,64) đưa con trỏ về cột đầu của page thứ “Line”, nửa phải GLCD (cột 64 của GLCD là cột đầu tiên của nửa bên phải).

List 6. Các chương trình con mở rộng (tt)..

Page 233: asembly
Page 234: asembly

       Đây là 3 chương trình con cuối cùng trong thư viện myGLCD. Trong đó có 2 hàm in các ký có kích thước 7x8 (7 cột, 8 dòng) được định nghĩa trong bảng font và 1 hàm in toàn bộ màn hình GLCD với một hình kích thước 128x64. 

      Hàm void GLCD_PutChar78(uint8_t Line, uint8_t Col, uint8_t chr) cho phép in ký tự có mã ascii là biến “chr”, biến “Line” là địa chỉ X (0 đến 7) và biến Col là địa chỉ cột Y (0 đến 127). Phần phức tạp nhất trong chương trình con này là việc xét trường hợp có sự chuyển bên (trái qua phải) khi in. Vì mỗi ký tự được định nghĩa bằng 7 bytes trong bảng font, tương ứng với 7 cột trên GLCD, nếu chúng ta muốn in ký tự tại vị trí 60 trên GLCD, các byte thứ 0 1, 2, 3 nằm ở vị trí cột 60, 61, 62 và 63 của nửa trái trong khi các byte thứ 4, 5 và 6 lại nằm ở các cột 0, 1 và 2 của nửa bên phải. Chúng ta phải nhận ra sự chuyển bên này để chuyển chip KS0108 cần thao tác. Chúng ta chia quá trình in ra 2 trường hợp, trường hợp có sự chuyển bên và trường hợp còn lại không chuyển bên (ký tự nằm trọn bên trái hoặc phải). Cấu trúc If dùng trong dòng 4 kiểm tra xem có sự chuyển bên xảy ra hay không: if ((Col>57) && (Col<64)),  nếu cột Col lớn hơn 57 và nhỏ hơn 63 thì sẽ có một sự chuyển bên xảy ra (vì 1 ký tự chiếm 7 cột trên GLCD). Chia quá trình in thành 2 vòng lặp for, vòng for thứ nhất (dòng 6) in từ vị trí Col đến vị trí cột của nửa trái và vòng lặp for thứ 2 ở dòng 9 in từ cột đầu tiên của nửa GLCD bên phải đến byte cuối cùng của ký tự cần in. Trường hợp ngược lại, không có sự chuyển bên xảy ra, chúng ta in bình thường (xem các dòng từ 12 đến 15). Chú ý là dữ liệu ghi vào GLCD lấy từ bảng font7x8 được định nghĩa trong file font.h, bảng font được viết sẵn trong bộ nhớ FLASH của AVR, việc đọc nội dung FLASH thực hiện bằng hàm pgm_read_byte, bạn xem lại bài điều khiển ma trận LED để hiểu thêm. 

       Hàm void GLCD_Print78(uint8_t Line, uint8_t Col, char* str) cho phép in một chuỗi ký tự hay 1 câu lên GLCD, hàm này cũng giống hàm in chuỗi mà chúng ta đã thực hiện trong trường hợp của Text LCD (modified code), một điểm khác tôi thêm vào là cho phep xuống dòng nếu câu cần in vượt quá 1 dòng. Các câu lệnh bên trong điều kiện if (dòng 23 đến 27) thực hiện xuống dòng nếu cần thiết. Quá trình in sau đó diễn ra bình thường bằng cách gọi hàmGLCD_PutChar78.

       Cuối cùng là hàm void GLCD_PutBMP(char *bmp) thực hiện in một hình có kích thước 128x64 được định nghĩa trước lên toàn bộ màn hình GLCD (in đè). Quá trình in cũng khá đơn giản với việc đọc nội dung hình trong FLASH và gởi đến GLCD. Cần chia thành 2 quá trình in cho 2 nửa trái và phải (2 vòng lặp for trong 2 dòng 38 và 43). Dữ liệu hình được ghi trong FLASH có định kích thước 128x8 pages= 1024 bytes, định dạng là 1 mảng có 1024 phần tử, mỗi phần tử là 1 con số dạng byte, mỗi số tương ứng 8 chấm của 1 cột trong 1 page. Các con số được sắp xếp thành 8 dòng tương ứng 8 pages, mỗi dòng có 128 phần tử tương ứng 128 cột GLCD.

Page 235: asembly

       GLCD có khả năng tùy biến hiển thị cao, đó là cơ hội cho bạn thể hiện sự sáng tạo ~, trong thư viện myGLCD tôi chỉ trình bày một số chương trình con cơ bản, phần còn lại thuộc về bạn. Hãy sử dụng các hàm truy xuất trong myGLCD để viết các chương trình con hiển thị khác như vẽ đường thẳng, đường tròn, hàm sine, cosine hay bất kỳ hàm số nào…Hope to hear from you soon.

IV. Ví dụ điều khiển Graphic LCD bằng thư viện myGLCD.

        Phần này tôi sẽ minh họa cách sử dụng thư viện myGLCD.h để in trực tiếp dữ liệu lên GLCD, hiển thị các ký tự trong bảng font7x8 và hình ảnh lên GLCD. Sử dụng phần mềm Proteus vẽ một mạch điện gồm 1 GLCD 128x64 (keyword: LGM12641BS1R), 1 chip Atmega32 và 1 biến trở (keyword: POT-LIN) như trong hình 6. Tạo 1 Project bằng WinAVR có tên là myGLCD và tạo file source là main.c, tạo Makefile với khai báo sữ dụng chip ATmega32 và clock 8MHz. Copy file myGLCD.h và font.h vào thư mục của Project mới tạo. Viết code cho file main.c như trong list 7. Chú ý các định nghĩa chân kết nối với LCD trong phần đầu file myGLCD.h phải giống với kết nối thật trong hình 6.

Page 236: asembly

Hình 6. Mạch điện mô phỏng Graphic LCD với AVR.  

List 7. Chương trình demo giao tiếp GLCD.

Page 237: asembly
Page 238: asembly

        Để sử dụng thư viện myGLCD, chúng ta cần include file myGLCD.h vào Project như trong dòng 4, #include "myGLCD.h". Hai dòng 9 và 10 thực hiện khởi động và xóa LCD. Tôi thực hiện 4 demo in lên GLCD. Trong các dòng từ 12 đến 17 thực hiện ghi trực tiếp giá trị lên GLCD bằng hàm GLCD_WriteDATA, kết quả là 1 dãy các chấm có độ rộng 8 bit nằm ở page 4 của GLCD (xem hình bên dưới). Chú ý là để in hết cả chiều ngang của GLCD cần thực hiện 2 lần in trên 2 nửa GLCD. 

       Các dòng lệnh từ 21 đến 27 thực hiện in 97 ký tự trong bảng font7x8 bắt đầu bằng mã ascii 33 (ký tự “!”), biến Line là địa chỉ page được khởi tạo bằng 0 khi khai báo trong dòng 7. Biến Col là địa chỉ cột, Col cũng được khởi tạo bằng 0. Dòng 32 in ký tự có mã “i” lên GLCD tại vị trí page=Line, cột=Col. Sau khi một ký tự được in, Col sẽ được tăng lên  8 vị trí (dòng 24), chúng ta dành 8 cột trên GLCD cho một ký tự 7x8 để tránh các ký tự “dính” với nhau. Nếu Col lớn hơn 127 thì một quá trình xuống dòng cần thực hiện, khi đó reset Col về 0 và tăng biến Line thêm 1 (dòng 25). Các ký tự sẽ được in lần lượt trên GLCD với 1 khoảng delay.

      Các dòng từ 31 đến 35 mô tả cách dùng hàm GLCD_Print78 để in các chuỗi ký tự hay các câu. Dòng 23 in từ “code” lên GLCD tại vị trí page=4, cột=20. Chú ý hàm sprintf trong dòng 33, đây là một hàm của ngôn ngữ C, hàm này cho phép chuyển một số thành một chuỗi các ký tự, trong ví dụ này tôi thực hiện chuyển số 8205 thành chuỗi “8205”, kết quả chứa trong biến “dis”, biến này là 1 mảng các ký tự hay con trỏ đến mảng các ký tự. Sau đó, dòng 34 in chuỗi “dis” lên GLCD.

      Demo cuối cùng là in 1 hình 128x64 lên GLCD bằng hàm GLCD_PutBMP(hiGLCD) và sau đó thực hiện animation (một kiểu hoạt hình) bằng hàm GLCD_StartLine. Dòng 38 in một hình có tên hiGLCD được định nghĩa trước trong file font.h ra GLCD. Các dòng 40 đến 44 cuộn màn hình GLCD lên trên để thực hiện animation. Biến i là biến offset được cho chạy từ 1 đến 63, sau mỗi lần cuộn chúng ta delay một khoảng thời gian ngắn để thấy GLCD “cuộn”.

      Hãy tham khảo thêm bài giới thiệu phần mềm G.Edit để biết cách tạo code hình ảnh cho Graphic LCD.

Điều khiển Động cơ DC servo (PID)

Page 239: asembly

1 2 3 4 5

 ( 159 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu

2. Incremental Optical Encoder

3. Chip driver L298D

4. Mạch logic cho L298D

5. Giải thuật điều khiển PID

6. Điều khiển DC Motor bằng AVR

Download ví dụ

Cấu trúc AVR .

WinAVR .

C cho AVR.

Mô phỏng với Proteus.

I. Giới thiệu

     Điều khiển động cơ DC (DC Motor) là một ứng dụng thuộc dạng cơ bản nhất của điều khiển tự động vì DC Motor là cơ cấu chấp hành (actuator) được dùng nhiều nhất trong các hệ thống tự động (ví dụ robot). Điều khiển được DC Motor là bạn đã có thể tự xây dựng được cho mình rất nhiều hệ thống tự động. Khái niệm Servo mà tôi dùng trong bài học này để chỉ một hệ thống hồi tiếp. DC servo motor là động cơ DC có bộ điều khiển hồi tiếp.     Bài này là một bài tổng hợp nhiều vấn đề ứng dụng AVR bao gồm nhận dữ liệu từ người dùng, điều khiển motor, đọc encoder, hiển thị LCD, cả giải thuật điều khiển PID và mạch công suất cho Motor…Do đó, ít nhất bạn phải nắm được các vấn đề cơ bản như Timer-Counter, TexLCD, mạch cầu H. Phần còn lại tôi sẽ giải thích trong lúc học bài này. Có 2 phương pháp điều khiển động cơ DC là analog và digital. Mục đích chính của chúng ta là dùng AVR điều khiển động cơ DC nên phương pháp số mà cụ thể là phương pháp điều rộng xung (PWM) sẽ được giới thiệu. Ngoài ra, khi nói đến điều khiển động cơ DC có 2 đại lương điều khiển chính là vị trí (số vòng quay) và vận tốc. Trong phần giải thích về bộ điều khiển PID tôi sẽ điều khiển vị trí làm ví dụ, tuy nhiên trong phần ví dụ lập trình cho AVR

Page 240: asembly

chúng ta sẽ thực hiện điều khiển vận tốc cho DC Motor. Bằng cách này, bạn có thể tự tin để mở rộng ví dụ để điều khiển cho cả 2 đại lượng. Vì là điều khiển một cách tự động nên chúng ta cần đọc về đại lượng điều khiển (cụ thể là vị trí hoặc vận tốc motor) và hồi tiếp (feedback) về để “hiệu chỉnh” PWM cấp cho động cơ. Chúng ta sẽ dùng incremental optical encoder để đọc số vòng quay và hồi tiếp về cho AVR. Bộ điều khiển PID sẽ được dùng và vận hành bởi AVR. Tổng quát, bài học này bao gồm:       - AVR phát PWM điều chỉnh vận tốc động cơ: phần này bạn xem lại bài 4 về Timer-Counter. Điều cơ bản cần nắm là bằng cách thay đổi độ rộng của xung PWM chúng ta sẽ thay đổi được vận tốc Motor.       - Xung PWM không trực tiếp làm quay động cơ mà thông qua một mạch công suất gọi là dirver. Driver cho DC Motor chính là mạch cầu H mà chúng ta đã tìm hiểu trong bài “Mạch cầu H”. Trong bài học này, tôi giới thiệu một chip có tích hợp sẵn mạch cầu H, chip L298D.      - Để việc điều khiển chip driver L298D dễ dàng, chúng ta sẽ tạo một mạch logic dùng các cổng NOT và AND.      - Động cơ DC mà chúng ta sử dụng có tích hợp sẵn một encoder 3 ngõ ra, chúng ta sẽ dùng AVR để đọc số xung (hay số vòng quay) và tính ra vận tốc của Motor. Việc đọc encoder sẽ được thực hiện bằng ngắt ngoài.      - Một giải thuật PID được xây dựng trong AVR để hiệu chỉnh vận tốc động cơ.      - Người dùng sẽ nhập vận tốc cần điều khiển vào AVR thông qua các switches. Vận tốc mong muốn và vận tốc thực của động cơ được hiển thị trên Text LCD.       Mạch điện ví dụ được trình bày trong hình 1.

Page 241: asembly
Page 242: asembly

Hình 1. Hệ thống điều khiển động cơ DC servo.

     Trong mạch điện hình 1, tôi chia hệ thống thành 3 nhóm: nhóm CONTROL bao gồm AVR vận hành giải thuật điều khiển PID và việc nhập, xuất. Nhóm LOGIC thực hiện việc biến đổi các tín hiệu điều khiển để tạo ra các tín hiệu phù hợp cho chip driver. Nhóm POWER bao gồm chip driver L298D và DC Motor. Ngoài ra còn có một Encoder được tích hợp sẵn trên DC Motor.      Phần tiếp theo chúng ta sẽ tìm hiểu riêng từng nhóm, cuối cùng là viết chương trình cho AVR điều khiển hệ thống DC Servo Motor

II. Incremental Optical Encoder

      Để điều khiển số vòng quay hay vận tốc động cơ thì chúng ta nhất thiết phải đọc được góc quay của motor. Một số phương pháp có thể được dùng để xác định góc quay của motor bao gồm tachometer (thật ra tachometer đo vận tốc quay), dùng biến trở xoay, hoặc dùng encoder. Trong đó 2 phương pháp đầu tiên là phương pháp analog và dùng optiacal encoder (encoder quang) thuộc nhóm phương pháp digital. Hệ thống optical encoder bao gồm một nguồn phát quang (thường là hồng ngoại – infrared), một cảm biến quang và một đĩa có chia rãnh. Optical encoder lại được chia thành 2 loại: encoder tuyệt đối (absolute optical encoder) và encoder tương đối (incremental optical encoder). Trong đa số các DC Motor, incremental optical encoder được dùng và mô hình động cơ servo trong bài này cũng không ngoại lệ. Từ bây giờ khi tôi nói encoder tức là incremental encoder. Hình 2 là mô hình của encoder loại này. 

Hình 2. Optical Encoder (trích từ [1]).

Page 243: asembly

      Encoder thường có 3 kênh (3 ngõ ra) bao gồm kênh A, kênh B và kênh I (Index). Trong hình 2 bạn thấy hãy chú ý một lỗ nhỏ bên phía trong của đĩa quay và một cặp phat-thu dành riêng cho lỗ nhỏ này. Đó là kênh I của encoder. Cữ mỗi lần motor quay được một vòng, lỗ nhỏ xuất hiện tại vị trí của cặp phát-thu, hồng ngoại từ nguồn phát sẽ xuyên qua lỗ nhỏ đến cảm biến quang, một tín hiệu xuất hiện trên cảm biến. Như thế kênh I xuất hiện một “xung” mỗi vòng quay của motor. Bên ngoài đĩa quay được chia thành các rãnh nhỏ và một cặp thu-phát khác dành cho các rãnh này. Đây là kênh A của encoder, hoạt động của kênh A cũng tương tự kênh I, điểm khác nhau là trong 1 vòng quay của motor, có N “xung” xuất hiện trên kênh A. N là số rãnh trên đĩa và được gọi là độ phân giải (resolution) của encoder. Mỗi loại encoder có độ phân giải khác nhau, có khi trên mỗi đĩa chĩ có vài rãnh nhưng cũng có trường hợp đến hàng nghìn rãnh được chia. Để điều khiển động cơ, bạn phải biết độ phân giải của encoder đang dùng. Độ phân giải ảnh hưởng đến độ chính xác điều khiển và cả phương pháp điều khiển. Không được vẽ trong hình 2, tuy nhiên trên các encoder còn có một cặp thu phát khác được đặt trên cùng đường tròn với kênh A nhưng lệch một chút (lệch M+0,5 rãnh), đây là kênh B của encoder. Tín hiệu xung từ kênh B có cùng tần số với kênh A nhưng lệch pha 90o. Bằng cách phối hợp kênh A và B người đọc sẽ biết chiều quay của động cơ. Hãy quan sát hình 3.

Hình 3. Hai kênh A và B lệch pha trong encoder (trích từ [1])

Page 244: asembly

       Hình trên cùng trong hình 3 thể hiện sự bộ trí của 2 cảm biến kênh A và B lệch pha nhau. Khi cảm biến A bắt đầu bị che thì cảm biến B hoàn toàn nhận được hồng ngoại xuyên qua, và ngược lại. Hình thấp là dạng xung ngõ ra trên 2 kênh. Xét trường hợp motor quay cùng chiều kim đồng hồ, tín hiệu “đi” từ trái sang phải. Bạn hãy quan sát lúc tín hiệu A chuyển từ mức cao xuống thấp (cạnh xuống) thì kênh B đang ở mức thấp. Ngược lại, nếu động cơ quay ngược chiều kim đồng hồ, tín hiệu “đi” từ phải qua trái. Lúc này, tại cạnh xuống của kênh A thì kênh B đang ở mức cao. Như vậy, bằng cách phối hợp 2 kênh A và B chúng ta không những xác định được góc quay (thông qua số xung) mà còn biết được chiều quay của động cơ (thông qua mức của kênh B ở cạnh xuống của kênh A).      Câu hỏi bây giờ là làm thế nào để đọc encoder bằng AVR?       Tùy theo đại lượng điều khiển (vị trí hay vận tốc) và đặc điểm encoder (độ phân giải) chúng ta có các giải pháp sau để đọc encoder bằng AVR     - Dùng input capture: một số bộ timer-counter trên AVR có chức năng Input capture, hiểu nôm na như sau. Cứ mỗi lần có một tín hiệu (cạnh lên hoăc cạnh xuống) trên chân ICP (Input Capture Pin), giá trị thời gian của timer được tự động gán cho thanh ghi ICR (Input capture Register). So sánh giá trị thanh ghi ICR trong 2 lần liên tiếp sẽ đọc được chu kỳ của tín hiệu kích chân ICP. Từ đó suy ra tần số tín hiệu. Nếu một kênh của encoder được nối với chân ICP thì chúng ta có thể đo được tần số tín hiệu của kênh này. Nói cách khác, chúng ta sẽ tính được vận tốc của động cơ. Chúng ta có thể dùng ngắt Input capture và khi ngắt xảy ra, có thể đếm số thêm số xung để biết được góc quay motor, cũng có thể xác định được hướng quay thông qua xác định mức kênh B trong trình phục vụ ngắt input capture. Đây là một phương pháp hay, nhưng có nhược điểm là khá phức tạp khi sử dụng chức năng input capture của AVR. Mặc khác trên các chip AVR từ mega32 trở xuống, Input capture chỉ có ở timer 1, trong khi Timer này thường dùng để tạo PWM điều khiển động cơ.     - Dùng chức năng counter: đặt các kênh của encoder vào các chân đếm (T0, T1…) của các bộ timer chúng ta sẽ đếm được số lượng xung của các kênh. Đây là phương pháp sử dụng ít tài nguyên nhất (ít tốn thời gian cho encoder). Nhược điểm lớn nhất của phương pháp này là không xác định được chiều quay, mặc khác phương pháp này không ổn định khi vận tốc động cơ có sự thay đổi lớn.     - Cuối cùng là sử dụng ngắt ngoài: đây là phương pháp dễ nhưng chính xác để đọc encoder và cũng là phương pháp được dùng trong bài học này. Ý tưởng của phương pháp rất đơn giản, chúng ta nối kênh A của encoder với 1 ngắt ngoài (INT2 chẳng hạn) và kênh B với một chân nào đó bất kỳ (không phải chân ngắt). Cứ mỗi lần ngắt ngoài xảy ra, tức có 1 xung xuất hiện trên ở kênh A thì trình phục vụ ngắt ngoài tự động được gọi. Trong trình phục vụ ngắt này chúng ta kiểm tra mức của kênh B, tùy theo mức của kênh B chúng ta sẽ tăng biến đếm xung lên 1 hoặc giảm đi 1. Tuy nhiên, bạn cần phải tính toán rất cẩn thận khi sử dụng phương

Page 245: asembly

pháp này. Ví dụ trường hợp encoder có độ phân giải 2000 xung/vòng, motor bạn quay với vận tốc 100 vòng/s thì tần số xung trên kênh A của encode là 2000x100=200KHz, nghĩa là cứ mỗi 5 us ngắt ngoài xảy ra một lần. Tần số ngắt như thế là quá cao cho AVR, điều này có nghĩa là AVR chỉ tập trung cho mỗi việc “đếm xung”, không có đủ thời gian để thực thi các việc khác. Trong bài này, chúng ta chọn độ phân giải của encoder là 112 (112 xung trên mỗi vòng quay). Vận tốc tối đa của động cơ được chọn vào khoảng 30 vòng/s nên tần số xung lớn nhất từ encoder là 112x30=3.36KHz. Giá trị này hợp lí vì tần số cho AVR trong bài này được chọn 8MHz. Kênh A của encoder được nối với ngắt INT2 của chip atmega32, kênh B được nối với chân PB0, chúng ta không sử dụng kênh I (xem hình 1).     Chú ý: các ngõ ra trên đa số (gần như tất cả) các encoder có dạng cực góp hở (Open collector), muốn sử dụng chúng cần mắc điện trở kéo lên VCC (5V).

III. Chip driver L298D

      L298D là một chip tích hợp 2 mạch cầu H trong gói 15 chân. Tất cả các mạch kích, mạch cầu đều được tích hợp sẵn. L298D có điện áp danh nghĩa cao (lớn  nhất 50V) và dòng điện danh nghĩa lớn hơn 2A nên rất thích hợp cho các các ứng dụng công suất nhỏ như các động cơ DC loại nhỏ và vừa. Vì là loại “all in one” nên là lựa chọn hoàn hảo cho những người chưa có nhiều kinh nghiệm làm mạch điện tử. Trong bài học này tôi dùng chip L298D để làm driver cho motor. Hình 4 thể hiện mô hình thật của chip và cấu trúc bên trong chip.

Page 246: asembly
Page 247: asembly

Hình 4. Chip L298D

     Hình phía trên là hình dáng bên ngoài và tên gọi các chân của L298D. Hình phía dưới là cấu trúc bên trong chip. Có 2 mạch cầu H trên mỗi chip L298D nên có thể điều khiển 2 đối tượng chỉ với 1 chip này. Mỗi mạch cầu bao gồm 1 đường nguồn Vs (thật ra là đường chung cho 2 mạch cầu), một đường  current sensing (cảm biến dòng), phần cuối của mạch cầu H không được nối với GND mà bỏ trống cho người dùng nối một điện trở nhỏ gọi là sensing resistor. Bằng cách đo điện áp rơi trên điện trở này chúng ta có thể tính được dòng qua điện trở, cũng là dòng qua động cơ (xem hình 4). Mục đích chính của việc đo dòng điện qua động cơ là để xác định các trường hợp nguy hiểm xảy ra trong mạch, ví dụ quá tải. Nếu việc đo dòng động cơ không thật sự cần thiết bạn có thể nối đường current sensing này với GND (trong mạch điện của bài này, tôi nối chân current sensing với GND). Động cơ sẽ được nối với 2 đường OUT1, OUT2 (hoặc OUT3, OUT4 nếu dùng mạch cầu bên phải). Một chân En (EnA và EnB cho 2 mạch cầu) cho phép mạch cầu hoạt động, khi chân En được kéo lên mức cao, mạch cầu sẵn sang hoạt động. Các đường kích mỗi bên của mạch cầu được kết hợp với nhau và nhưng mức điện áp ngược nhau do một cổng Logic NOT. Bằng cách này chúng ta có thể tránh được trường hợp 2 transistor ở cùng một bên được kích cùng lúc (ngắn mạch). Như vậy, sẽ có 2 đường kích cho mỗi cầu H gọi là In1 và In2 (hoặc In3, In4). Để motor hoạt động chúng ta phải kéo 1 trong 2 đường kích này lên cao trong khi đường kia giữ ở mức thấp, ví dụ In1=1, In2=0. Khi đảo mức kích của 2 đường In, động cơ sẽ đảo chiều quay. Tuy nhiên, do L298D không chỉ được dùng đề đảo chiều động cơ mà còn điều khiển vận tốc động cơ bằng PWM, các đường In cần được “tổ hợp lại” bằng các cổng Logic (xem phần tiếp theo). Ngoài ra, trên chip L298D còn có các đường Vss cấp điện áp cho phần logic (5V) và GND chung cho cả logic và motor.      Trong thực tế, công suất thực mà L298D có thể tải nhỏ hơn so với giá trị danh nghĩa của nó (V=50V, I=2A). Để tăng dòng điện tải của chip lên gấp đôi, chúng ta có thể nối 2 mạch cầu H song song với nhau (các chân có chức năng như nhau của 2 mạch cầu được nối chung).

II. Mạch logic cho L298D

      Thông thường, khi thiết kế một mạch driver cho motor người ta thường dành 3 đường điều khiển đó là PWM dùng điều khiển vận tốc, DIR điều khiển hướng và En cho phép mạch hoạt động. Chip L298D đã có sẵn đường En nhưng 2 đường điều khiển In1 và In2 không thật sự chức năng như chúng ta mong muốn. Vì thế, chúng ta sẽ thiết kế một mạch logic phụ với 2 ngõ vào là PWM và DIR trong khi 2 ngõ ra là 2 đường điều khiển In1 và In2. Bảng chân trị của mạch logic cần thiết kế được trình bày trong bảng 1.Bảng 1. bảng chân trị của mạch logic cho driver L298D. 

Page 248: asembly

PWM DIR In1 In20 0 0 00 1 0 01 0 1 01 1 0 1

      Từ bảng chân trị này, chúng ta có thể viết hàm bool cho 2 ngõ In1 và In2:      In1=PWM.NOT(DIR)      In2=PWM.DIR      Mạch logic vì thế sẽ có dạng như trong hình 5. 

Hình 5. Mạch logic cho L239

     Tôi sẽ không giải thích chi tiết phần này, tuy nhiên điều bạn cần nắm là với mạch logic này, đường DIR có chức năng đảo chiều động cơ trong khi đường PWM điều khiển vận tốc động cơ bằng tín hiệu PWM.

 V. Giải thuật điều khiển PID

      PID là cách viết tắc của các từ Propotional (tỉ lệ), Integral (tích phân) và Derivative (đạo hàm). Tuy xuất hiện rất lâu nhưng đến nay PID vẫn là giải thuật điều khiển được dùng nhiều nhất trong các ứng dụng điều khiển tự động. Để giúp bạn có cái hiểu rõ hơn bản chất của giải thuật PID tôi sẽ dùng một ví dụ điều khiển vị trí của một car (xe) trên đường thẳng. Giả sử bạn có một xe (đồ chơi...) có gắn một động cơ DC. Động cơ sinh ra một lực để đẩy xe chạy tới hoặc lui trên một đường thẳng như trong hình 6. 

Page 249: asembly

Hình 6. Ví dụ điều khiển vị trí xe trên đường thẳng

     Gọi F là lực do động cơ tạo ra điều khiển xe. Ban đầu xe ở vị trí A, nhiệm vụ đặt ra là điều khiển lực F (một cách tự động) để đẩy xe đến đúng vị trí O với các yêu cầu: chính xác (accurate), nhanh (fast response), ổn định (small overshot).      Một điều rất tự nhiên, nếu vị trí hiện tại của xe rất xa vị trí mong muốn (điểm O), hay nói cách khác sai số(error) lớn, chúng ta cần tác động lực F lớn để nhanh chóng đưa xe về O. Một cách đơn giản để công thức hóa ý tưởng này là dùng quan hệ tuyến tính:     F=Kp*e                                                                                                                            (1)     Trong đó Kp là một hằng số dương nào đó mà chúng ta gọi là hệ số P (Propotional gain), e là sai số cần điều khiển tức khoảng cách từ điểm O đến vị trí hiện tại của xe. Mục tiêu điều khiển là đưa e tiến về 0 càng nhanh càng tốt. Rõ ràng nếu Kp lớn thì F cũng sẽ lớn và xe rất nhanh chóng tiến về vị trí O. Tuy nhiên, lực F quá lớn sẽ gia tốc cho xe rất nhanh (định luật II của Newton: F=ma). Khi xe đã đến vị trí O (tức e=0), thì tuy lực F=0 (vì F=Kp*e=F=Kp*0) nhưng do quán tính xe vẫn tiếp tục tiến về bên phải và lệch điểm O về bên phải, sai số e lại trở nên khác 0, giá trị sai số lúc này được gọi là overshot (vượt quá). Lúc này, sai số e là số âm, lực F lại xuất hiện nhưng với chiều ngược lại để kéo xe về lại điểm O. Nhưng một lần nữa, do Kp lớn nên giá trị lực F cũng lớn và có thể kéo xe lệch về bên trái điểm O. Quá trình cứ tiếp diễn, xe cứ mãi dao động quanh điểm O. Có trường hơp xe dao động càng ngày xàng xa điểm O. Bộ điều khiển lúc này được nói là không ổn định. Một đề xuất nhằm giảm overshot của xe là sử dụng một thành phần “thắng” trong bộ điều khiển. Sẽ rất lý tưởng nếu khi xe đang ở xa điểm O, bộ điều khiển sinh ra lực F lớn nhưng khi xe đã tiến gần đến điểm O thì thành phần “thắng” sẽ giảm tốc độ xe lại. Chúng ta đều biết khi một vật dao động quanh 1 điểm thì vật đó có vận tốc cao nhất ở tâm dao động (điểm O). Nói một cách khác, ở gần điểm O sai

Page 250: asembly

số e của xe thay đổi nhanh nhất (cần phân biệt: e thay đổi nhanh nhất  không phải e lớn nhất). Mặt khác, tốc độ thay đổi của e có thể tính bằng đạo hàm của biến này theo thời gian. Như vậy, khi  xe từ A tiến về gần O, đạo hàm của sai số e tăng giá trị nhưng ngược chiều của lực F (vì e đang giảm nhanh dần). Nếu sử dụng đạo hàm làm thành phần “thắng” thì có thể giảm được overshot của xe. Thành phần “thắng” này chính là thành phần D (Derivative) trong bộ điều khiển PID mà chúng ta đang khảo sát. Thêm thành phần D này vào bộ điều khiển P hiện tại, chúng ta thu được bộ điều khiển PD nhu sau:     F=Kp*e + Kd*(de/dt)                                                                                                        (2)     Trong đó (de/dt) là vận tốc thay đổi của sai số e và Kd là một hằng số không âm gọi là hệ số D (Derivative gain).      Sự hiện diện của thành phần D làm giảm overshot của xe, khi xe tiến gần về O, lực F gồm 2 thành phần Kp*e > =0 (P) và Kd*(de/dt) <=0 (D). Trong một số trường hợp thành phần D có giá trị lớn hơn thành phần P và lực F đổi chiều, “thắng” xe lại, vận tốc của xe vì thế giảm mạnh ở gần điểm O. Một vấn đề nảy sinh là nếu thành phần D quá lớn so với thành phần P hoặc bản thân thành phần P nhỏ thì khi xe tiến gần điểm O (chưa thật sự đến O), xe có thể dừng hẳn, thành phần D bằng 0 (vì sai số e không thay đổi nữa), lực F = Kp*e. Trong khi Kp và e lúc này đều nhỏ nên lực F cũng nhỏ và có thể không thắng được lực ma sát tĩnh. Bạn hãy tưởng tượng tình huống bạn dùng sức của mình để đẩy một xe tải nặng vài chục tấn, tuy lực đẩy tồn tại nhưng xe không thể di chuyển. Như thế, xe sẽ đứng yên mãi dù sai số e vẫn chưa bằng 0. Sai số e trong tình huống này gọi là steady state error (tạm dịch là sai số trạng thái tĩnh). Để tránh steady state error, người ta thêm vào bộ điều khiển một thành phần có chức năng “cộng dồn” sai số. Khi steady state error xảy ra, 2 thành phần P và D mất tác dụng, thành phần điều khiển mới sẽ “cộng dồn” sai số theo thời gian và làm tăng lực F theo thời gian. Đến một lúc nào đó, lực F đủ lớn để thắng ma sát tĩnh và đẩy xe tiến tiếp về điểm O. Thành phần “cộng dồn” này chính là thành phần I (Integral - tích phân) trong bộ điều khiển PID. Vì chúng ta điều biết, tích phân một đại lượng theo thời gian chính là tổng của đại lượng đó theo thời gian. Bộ điều khiển đến thời điểm này đã đầy đủ là PID:     F=Kp*e + Kd*(de/dt)+Ki*∫edt                                                                                                            (3)     (chú ý: ∫edt là tích phân của biến e theo t)

     Như vậy, chức năng của từng thành phần trong bộ điều khiển PID giờ đã rõ. Tùy vào mục đích và đối tượng điều khiển mà bộ điều khiển PID có thể được lượt bớt để trở thành bộ điều khiển P, PI hoặc PD. Công việc chính của người thiết kế bộ điều khiển PID là chọn các hệ số Kp, Kd và Ki sao cho bộ điều khiển hoạt động tốt và ổn định (quá trình này gọi là PID gain tuning). Đây không phải là việc dễ

Page 251: asembly

dàng vì nó phụ thuộc vào nhiều yếu tố. Tôi tóm tắt một kinh nghiệm cơ bản khi chọn các hệ số cho PID như sau:     - Chọn Kp trước: thử bộ điều khiển P với đối tượng thật (hoặc mô phỏng), điều chỉnh Kp sao cho thời gian đáp ứng đủ nhanh, chấp nhận overshot nhỏ.     - Thêm thành phần D để loại overshot, tăng Kd từ từ, thử nghiệm và chọn giá trị thích hợp. Steady state error có thể sẽ xuất hiện.     - Thêm thành phần I để giảm steady state error. Nên tăng Ki từ bé đến lớn để giảm steady state error đồng thời không để cho overshot xuất hiện trở lại.     Có một phương pháp rất phổ biến dùng để chọn các hệ số cho bộ điều khiển PID gọi là Ziegler–Nichols, bạn quan tâm có thể tự tìm hiểu thêm.

     Điều khiển PID số     Công thức của bộ điều khiển PID trình bày trong (3) là dạng hàm liên tục của biến e, trong đó có cả thành phần tuyến tính, đạo hàm và tích phân. Tuy nhiên, hệ thống máy tính và vi điều khiển lại là hệ thống số. Muốn xây dựng bộ điều khiển PID trên máy tính hay trên vi điều khiển chúng ta phải biết cách xấp xỉ phương trình liên tục thành dạng rời rạc. Để thực hiện “số hóa” bộ điều khiển PID trước hết tôi nói sơ qua thế nào là hệ thống số (digital) so với hệ thống liên tục hay hệ thống tương tự (analog). Hãy quan sát hệ thống điều chỉnh nhiệt độ đơn giản như trong hình 7.

Hình 7. Tự động điều chỉnh nhiệt độ

      Giả sử chúng ta cần điều chỉnh nhiệt độ trong phòng ở một mức nào đó (tùy theo giá trị tham chiếu) bằng quạt. Cảm biến đo nhiệt độ và hồi tiếp về bộ khuyếch đại vi sai (so sánh và khuyếch đại). Nếu có sai số giữa giá trị tham chiếu và giá trị

Page 252: asembly

đo từ cảm biếm, bộ khuyếch đại vi sai sẽ tự động khuyếch đại sai số này và làm tăng hay giảm vận tốc của quạt để điều chỉnh nhiệt độ. Quá trình này xảy ra một cách liên tục. Bộ khuyếch đại vi sai trong trường hợp này chính là bộ điều khiển tương tự (analog controller). Bộ khuyếch đại này là một mạch điện tử thông thường như Opamp chẳng hạn. Nếu chúng ta thay bộ khuyếch đại này bằng một vi điều khiển AVR thì quá trình hiệu chỉnh không còn xảy ra liên tục nữa mà theo một chu kỳ nào đó. Ví dụ cứ mỗi 10 ms chúng ta đọc giá trị từ cảm biến một lần để tính toán sai số và xuất giá trị điều khiển quạt. Bộ điều khiển do AVR thực hiện gọi là bộ điều khiển số (digital controller) và khoảng thời gian 10ms này gọi là thời gian lấy mẫu (sampling time), đó là khoảng cách giữa 2 lần điều khiển liên tiếp. Rõ ràng thời gian lấy mẫu càng nhỏ (tấn số cao) thì việc hiệu chỉnh càng tiến gần đến sự “liên tục” và chất lượng điều khiển sẽ tốt hơn. Trong các bộ điều khiển số, thời gian lấy mẫu là một yếu tố rất quan trọng. Cần tính toán để thời gian này không quá lớn nhưng cũng đừng quá nhỏ, vì như thế sẽ hao phí thời gian thực thi.       Vì bộ điều khiển PID xây dựng trong AVR sẽ là bộ điều khiển số, chúng ta cần xấp xỉ công thức của bộ điều khiển này theo các khoảng thời gian rời rạc. Trước hết, thành phần P tương đối đơn giản vì đó là quan hệ tuyến tính Kp*e, chúng ta chỉ cần áp dụng trực tiếp công thức này mà không cần bất kỳ xấp xỉ nào. Tiếp đến là xấp xỉ cho đạo hàm của biến e. Vì thời gian lấy mẫu cho các bộ điều khiển thường rất bé nên có thể xấp xỉ đạo hàm bằng sự thay đổi của e trong 2 lần lấy mẫu liên tiếp:      de/dt =(e(k) – e(k-1))/h.      Trong đó e(k) là giá trị hiện tại của e, e(k-1) là giá trị của e trong lần lấy mẫu trước đó và h là khoảng thời gian lấy mẫu (h là hằng số).

Page 253: asembly

Hình 8. Xấp xỉ đạo hàm của biến sai số e

     Thành phần tích phân được xấp xỉ bằng diện tích vùng giới hạn bởi hàm đường biểu diễn của e và trục thời gian. Do việc tính toán tích phân không cần quá chính xác, chúng ta có thể dùng phương pháp xấp xỉ đơn giản nhất là xấp xỉ hình chữ nhật (sai số của phương pháp này cũng lớn nhất). Ý tưởng được trình bày trong hình 9.

Page 254: asembly

Hình 9. Xấp xỉ tích phân của biến sai số e

     Tích phân của biến e được tính bằng tổng diện tích các hình chữ nhật tại mỗi thời điểm đang xét. Mỗi hình chữ nhật có chiều rộng bằng thời gian lấy mẫu h và chiều cao là giá trị sai số e tại thời điểm đang xét. Tổng quát:

                                                                                                                  (4)     Tổng hợp các xấp xỉ, công thức của bộ điều khiển PID số được trình bày trong (5)

                                         (5)     Trong đó u là đại lượng output từ bộ điều khiển. Để đơn giản hóa việc tính thành phần tích phân, chúng ta nên dùng phương pháp “cộng dồn” (hay đệ quy):

                                                                                                         (6)     Với I(k) là thành phần tích phân hiện tại và I(k-1) là thành phần tích phân trước đó. 

Page 255: asembly

     Các công thức (5) và (6) rất dễ dàng để thực hiện bằng AVR. Do đó, đến lúc này chúng ta đã sẵn sàng để đưa ý tưởng vào lập trình cho chip.

VI. Điều khiển DC Motor bằng AVR

     Phần này chúng ta sẽ vận dụng tất cả phần lý thuyết giới thiệu ở trên để viết chương trình cho AVR. Mục đích là điều khiển vận tốc của DC Motor bằng giải thuật PID. Mạch điện mô phỏng được trình bày trong hình 1. Mô hình Motor dùng trong ví dụ là loại 12V có vận tốc không tải tối đa là 720rpm (revolute per minute) tức 20 vòng/s. Encoder dùng cho motor được chọn có độ phân giải 112 pulse/vòng. Kênh A của encoder được nối với ngắt ngoài INT2 để đếm xung, kênh B nối với chân PB0 (chân 1) của chip Atmega32 để xét hướng quay. Bốn switches được nối với 4 bit cao của PORTB để cài đặt vận tốc mong muốn cần điều khiển. Một Text LCD dùng hiển thị vận tốc thực của motor đọc từ Encoder (Actual speed) và vận tốc cài đặt (Desired speed). Do Text LCD được nối với PORTC nên nếu bạn muốn dùng chương trình này cho ứng dụng thật thì phải nạp lại fuses để vô hiệu hóa JTAG. Giải thuật PID số được vận hành bởi AVR trong thời gian lấy mẫu là 25ms. Timer 2 được dùng để tạo khoảng thời gian 25ms. Timer 1 (16 bit) là bộ tạo PWM điều khiển vận tốc động cơ. Toàn bộ nội dung chương trình được trình bày trong list 1.List 1. Điều khiển vận tốc động cơ DC

Page 256: asembly
Page 257: asembly

     Các dòng từ 14 đến 17 chúng ta định nghĩa các chân điều khiển DC Motor, chân DIR điều khiển hướng và EN kích hoạt hoặc dừng Motor (thực ra là dừng L298D). Do mục đích của chúng ta là điều khiển vận tốc động cơ, 2 chân này chỉ được “kích” một lần duy nhất trong chương trình chính (không cần đổi hướng quay của Motor). Dòng 18 định nghĩa thời gian lấy mẫu, Sampling_time là 25 ms (.025s). Biến inv_Sampling_time ở dòng 19 là nghịch đảo của Sampling_time, 1/0.025 = 40, vì đây cũng là hằng số, chúng ta định nghĩa trước để sau này không cần thực hiện phép nghịch đảo trong chương trình chính (tiết kiệm thời gian thực thi). PWM dùng điều khiển động cơ được chọn có tần số 1KHz nên chu kỳ la 1ms. Do chúng ta dùng nguồn xung giữ nhịp 8MHz, để tạo thời gian 1ms cần 8000 xung, giá trị này được định nghĩa trong dòng 20 và sẽ được gán cho thanh ghi ICR1 (TOP của PWM, xem lại bài Timer-Counter, Timer1, Fast PWM) trong chương trình chính (dòng 81). Các dòng code từ 22 đến 27 khai báo một số biến toàn cục dùng trong chương trình chính. Do các biến này sẽ được dùng cả trong trình phục vụ ngắt và chương trình chính nên cần khai báo đặc tính volatile, kiểu biến là long int tức số nguyên 32 bit (để tránh bị tràn khi tính toán sau này). Biến Pulse và pre_Pulse là số xung hiện tại và lần lấy mẫu trước đó đọc từ encoder. Các biến trong dòng 23 và 24 dùng cho bộ điều khiển PID, biến Ctrl_Speed là vận tốc mong muốn (set point) toàn cục và biến Output chứa giá trị tính được từ bộ điều khiển PID.      Trước khi đi tìm hiểu chương trình con chứa giải thuật PID, chúng ta sẽ khảo sát nội dung chương trình main và các trình phục vụ ngắt trước để hiểu tổng quan cách thức thực hiện. Chương trình chính bắt đầu từ dòng 45 và kết thúc ở dòng 103. Phần đầu của chương trình chính (ngoài vòng lặp while) khai báo và khởi tạo các module được sử dụng. 2 dòng 49 và 50 cài đặt hướng cho PORTB, do PORT này dùng đọc encoder và các switches chúng ta cần set nó là input và có điện trở kéo lên. Hai dòng 52 và 53 set hướng cho động cơ và sẽ giữ hướng này không đổi trong suất quá trình điều khiển sau này. Hai dòng 55 và 56 khai báo ngắt ngoài INT2 dùng đếm xung kênh A của encoder. Chú ý là INT2 chỉ có 2 mode là cạnh xuống và cạnh lên nên chỉ có 1 bit sense ISC2 để chọn mode. Bit ISC2 không nằm trong thanh ghi điều khiển MCUCR như các ngắt khác mà nằm trong thanh ghi điều khiển-trạng thái MCUCSR. Khi ISC2=0 thì chế độ ngắt cạnh xuống của INT2 được chọn (xem dòng 55). Sau đó INT2 được cho phép hoạt động ở dòng 56. Hãy tạm thời di chuyển đến dòng 109 để xem trình phục vụ ngắt INT2. Chức năng của INT2 trong bài này là “đếm xung encoder” vì thế trình phục vụ sẽ làm việc này. Khi có một ngắt INT2 xảy ra tức có 1 xung từ encoder vào thì trình phục vụ ngắt ISR(INT2 vect) tự động được gọi ra, dòng 110 trong trình phục vụ ngắt kiểm tra trạng thái chân PB0, tức kenh B của encoder. Nếu PB0=1 thì tăng biến xung đếm được Pulse lên 1, ngược lại nếu PB0=0 thì giảm Pulse đi 1 trong dòng 111. Quay về giải thích chương trình chính ở dòng 59, đây là các khai báo cho timer 2. Chúng ta sẽ dùng timer 2 tạo ra một khoảng thời gian lấy mẫu 25 ms, cứ sau 25 ms thì sẽ

Page 258: asembly

có ngắt tràn timer2 một lần và trong trình phục vụ ngắt tràn của timer2 chúng ta thực hiện tính toán PID. Dòng 59 chúng ta set các bit CS để chọn bộ chia tần số, bộ chia Prescaler=1024 được chọn vì 25 ms khá lớn so với thời gian 1 chu kỳ xung giữ nhịp (1/8 micro giây). Prescaler = 1024 nghĩa là sau 1024 nhịp của xung giữ nhịp, tức sau 128 micro giây (1024 *1/8=128 us) thì thanh ghi giá trị TCNT2 mới tăng 1 đơn vị. Do chúng ta muốn tạo khoảng thời gian 25 ms tương đương 25000/128=195 đơn vị đếm của thanh ghi TCNT2, chúng ta sẽ gán giá trị khởi tạo cho TCNT2 là 255-195=60 (timer 2 sẽ tràn một lần khi TCNT2 đếm đến 255, xem lại bài Timer-Counter). Điều này thực hiện ở dòng 60 TCNT2=60. Dòng 61 cho phép ngắt tràn timer2. Hai dòng 64 và 65 khởi động Timer 1 dùng như một bộ tạo xung Fast PWM, mode 14, trong đó thanh ghi ICR1 chứa chu kỳ PWM và 2 thanh ghi OCR1A, OCR1B chứa duty cycle (khoảng ON) của PWM. Các dòng từ 68 đến 70 ghi texts lên LCD. Các dòng từ 80 đến 83 khởi động PWM cho DC Motor và cho phép ngắt toàn cục sei();. Trong vòng lặp while chủ yếu là công việc kiểm tra và hiển thị, biến sample_count đếm số lần ngắt tràn timer2 xảy ra, nó được tăng 1 đơn vị khi có một ngắt tràn (xem dòng 106) tức sau 25ms. Dòng 86, chúng ta kiểm tra biến sample_count, việc hiển thị chỉ đượcthực hiện mỗi 250 ms một lần (sample_count=10) vì việc này tốn khá nhiều thời gian. Trong dòng 87 chúng ta kiểm tra các swiches để xem người dùng cho muốn thay đổi vận tốc tham chiếu cho điều khiển. Các dòng tiếp theo in biến rSpeed là số lượng xung đếm được từ encoder trong vòng 25 ms (cho tới hiện tại) ở dong 1 của LCD và in biến Ctrl_Speed là số xung/25ms mà người dùng mong muốn motor đạt được. Nội dung quan trọng nhất của list 1, tuy nhiên, không nằm trong chương trình chính mà nằm ở các trình phục vụ ngắt  và chương trình con Motor_Speed_PID(long int des_Speed).      Trước hết, trình phục vụ ngắt ISR(TIMER2_OVF_vect) được tự động gọi sau mỗi 25ms, trong trình này chúng ta cần set lại giá trị khởi động cho thanh ghi giá trị TCNT2 (xem lại bài Timer-counter) ở dòng 105. Sau đó tăng biến đếm sample_count lên 1 (cùng cho việc đếm thời gian để hiển thị, đã nói ở trên). Cuối cùng là gọi chương trình con tính toán giải thuật PID Motor_Speed_PID(long int des_Speed). Đây là đoạn chương trình tính toán giải thuật PID và xuất giá trị điều khiển Motor. Hãy quay lại dòng 30 để tìm hiểu chương trình con này. Do biến Pulse chứa tổng số xung đọc từ encode (trong ISR(INT2_vect) ), chúng ta lấy giá trị này trừ đi giá trị pre_Pulse, tức số lượng xung ở thời điểm 25 ms trước đó, để thu được tổng số xung thu được trong 25 ms qua. Đây chính là vận tốc motor tính trên 25 ms:rSpeed=Pulse-pre_Pulse. Sau khi tính được “vận tốc” rSpeed chúng ta gán lại giá trị Pulse cho pre_Pulse để lần lấy mẫu sau dùng đến (dòng 32). Sai số vận tốc được đặt tên là Err, biến này được tính bằng bằng cách lấy vận tốc mong muốn trừ vận tốc hiện tại: Err=des_Speed-abs(rSpeed) ở dòng 33. Dòng 34 tính thành phần P của bộ điều khiểnpPart=Kp*Err. Dòng 35 tính thành phần D của bộ

Page 259: asembly

điều khiển, như chúng ta đã thảo luận trong công thức (2) thì thành phần D được tính là: dPart=Kd*(Err-pre_Err)/Sampling_time, trong đó pre_Err là giá trị sai số ở lần lấy mẫu trước được lưu lại. Do 1/Sampling_time = inv_Sampling_time nên chúng ta có thể thay dòng tính dPart bằng công thức trong dòng 35: dPart=Kd*(Err-pre_Err)*inv_Sampling_time. Dòng 36 tính thành phần I (iPart), sử dụng phương pháp “cộng dồn” (đệ quy) chúng ta thu được iPart bằng iPart trước đó cộng với diện tích hình chữ nhật sai số hiện tại:iPart+=Ki*Sampling_time*Err/1000. Chúng ta phải chia iPart cho 1000 vì Sampling_time được tính theo ms trong khi đơn vị tính toán chuẩn trong là s. Cộng các thành phần này lại chúng ta được giá trị Output tổng hợp trong dòng 37. Tuy nhiên, theo lẽ thường thì công thức dòng 37 phải là Output=pPart+dPart+iPart nhưng ở đây lại là :Output+=pPart+dPart+iPart (để ý dấu + trước dấu =), nghĩa là Output được cộng dồn thay vì là tổng tức thời như chúng ta đã thảo luận trong phần giải thuật PID. Thật ra việc này cũng dễ hiểu. Trong bài toán điều khiển vị trí, khi sai số bằng 0 chúng ta có thể dừng bộ điều khiển (u=0) nhưng trong bài toán điều khiển vận tốc, khi sai số bằng 0 thì giá trị u vẫn phải được giữ là giá trị trước đó.Vì vậy, trong bài toán điều khiển vận tốc giá trị Output được cộng dồn thay vì gán trực tiếp, bạn phải ghi nhớ điều này trong các ứng dụng điều khiển của mình. Hai dòng 40 và 41 xét trường hợp bão hòa (saturation) khi Output vượt quá giới hạn cho phép của PWM (xén 2 đầu). Cuối cùng là gán giá trị tính toán được từ PID cho thanh ghi OCR1A để tăng hoặc giảm duty cycle của PWM trên chân OC1A (nối với PWM của Motor) và gán gái trị sai số Err cho biến pre_Err cho lần lấy mẫu sau dùng đến.       Chạy mô phỏng: toàn bộ chương trình và cả mạch điện mô phỏng đã được tôi tạo sẵn. Người đọc chỉ cần đọc hiểu và chạy mô phỏng mạch điện. Chú khi chạy mô phỏng hãy thay đổi các switches để thay đổi vận tốc cần điều khiển. Gái trị vận tốc thực chất là số xung encoder trong 25 ms, người đọc hãy tự tính ra số vòng /s. Do mô hình motor trong phần mềm mô phỏng không hoàn hảo lắm nên đáp ứng bộ điều khiển hơi chậm, bạn có thể phải chờ một khoảng thời gian để thấy vận tốc Motor đạt đến vận tốc yêu cầu. Hay thay giá trị Kd trong dòng 23 thành 1 hoặc 0, biên dịch lại chương trình và mô phỏng để quan sát và so sánh ovetshot (sự vượt quá) của hệ thống.

Đồng hồ thời gian thực DS1307

1 2 3

Page 260: asembly

4 5

 ( 103 Votes )Nội dung Các bài cần tham khảo trước

1. Chip DS1307 .

2. AVR và DS1307 .

Download ví dụ

Cấu trúc AVR .

WinAVR .

C cho AVR.

Text LCD

Giao tiếp TWI-I2C

I. Chip DS1307.

     DS1307 là chip đồng hồ thời gian thực (RTC : Real-time clock), khái niệm thời gian thực ở đây được dùng với ý nghĩa thời gian tuyệt đối mà con người đang sử dụng, tình bằng giây, phút, giờ…DS1307 là một sản phẩm của Dallas Semiconductor (một công ty thuộc Maxim Integrated Products). Chip này có 7 thanh ghi 8-bit chứa thời gian là: giây, phút, giờ, thứ (trong tuần), ngày, tháng, năm. Ngoài ra DS1307 còn có 1 thanh ghi điều khiển ngõ ra phụ và 56 thanh ghi trống có thể dùng như RAM. DS1307 được đọc và ghi thông qua giao diện nối tiếp I2C (TWI của AVR) nên cấu tạo bên ngoài rất đơn giản. DS1307 xuất hiện ở 2 gói SOIC và DIP có 8 chân như trong hình 1.

Hình 1. Hai gói cấu tạo chip DS1307.

       Các chân của DS1307 được mô tả như sau:       - X1 và X2: là 2 ngõ kết nối với 1 thạch anh 32.768KHz làm nguồn tạo dao động cho chip.       - VBAT: cực dương của một nguồn pin 3V nuôi chip.       - GND: chân mass chung cho cả pin 3V và Vcc.

Page 261: asembly

       - Vcc: nguồn cho giao diện I2C, thường là 5V và dùng chung với vi điều khiển. Chú ý là nếu Vcc không được cấp nguồn nhưng VBAT được cấp thì DS1307 vẫn đang hoạt động (nhưng không ghi và đọc được).       - SQW/OUT: một ngõ phụ tạo xung vuông (Square Wave / Output Driver), tần số của xung được tạo có thể được lập trình. Như vậy chân này hầu như không liên quan đến chức năng của DS1307 là đồng hồ thời gian thực, chúng ta sẽ bỏ trống chân này khi nối mạch.       - SCL và SDA là 2 đường giao xung nhịp và dữ liệu của giao diện I2C mà chúng ta đã tìm hiểu trong bài TWI của AVR.Có thể kết nối DS1307 bằng một mạch điện đơn giản như trong hình 2.

Hình 2. Mạch ứng dụng đơn giản của DS1307.

      Cấu tạo bên trong DS1307 bao gồm một số thành phần như mạch nguồn, mạch dao động, mạch điều khiển logic, mạch giao điện I2C, con trỏ địa chỉ và các thanh ghi (hay RAM). Do đa số các thành phần bên trong DS1307 là thành phần “cứng” nên chúng ta không có quá nhiều việc khi sử dụng DS1307. Sử dụng DS1307 chủ yếu là ghi và đọc các thanh ghi của chip này. Vì thế cần hiểu rõ 2 vấn đề cơ bản đó là cấu trúc các thanh ghi và cách truy xuất các thanh ghi này thông qua giao diện I2C. Phần này chúng ta tìm hiểu cấu trúc các thanh ghi trước và cách truy xuất chúng sẽ tìm hiểu trong phần 2, điều khiển DS1307 bằng AVR.       Như tôi đã trình bày, bộ nhớ DS1307 có tất cả 64 thanh ghi 8-bit được đánh địa chỉ từ 0 đến 63 (từ 0x00 đến 0x3F theo hệ hexadecimal). Tuy nhiên, thực chất chỉ có 8 thanh ghi đầu là dùng cho chức năng “đồng hồ” (tôi sẽ gọi là RTC) còn lại 56 thanh ghi bỏ trông có thể được dùng chứa biến tạm như RAM nếu muốn. Bảy thanh ghi đầu tiên chứa thông tin về thời gian của đồng hồ bao gồm: giây (SECONDS), phút (MINUETS), giờ (HOURS), thứ (DAY), ngày (DATE), tháng (MONTH) và năm (YEAR). Việc ghi giá trị vào 7 thanh ghi này tương đương với việc “cài đặt” thời gian khởi động cho RTC. Việc đọc giá từ 7 thanh ghi là đọc thời gian thực mà chip tạo ra. Ví dụ, lúc khởi động chương trình, chúng ta ghi vào thanh ghi “giây” giá trị 42, sau đó 12s chúng ta đọc thanh ghi này, chúng ta thu được giá trị 54. Thanh ghi thứ 8 (CONTROL) là thanh ghi điều khiển xung ngõ ra

Page 262: asembly

SQW/OUT (chân 6). Tuy nhiên, do chúng ta không dùng chân SQW/OUT nên có thề bỏ qua thanh ghi thứ 8. Tổ chức bộ nhớ của DS1307 được trình bày trong hình 3.

Hình 3. Tổ chức bộ nhớ của DS1307.

      Vì 7 thanh ghi đầu tiên là quan trọng nhất trong hoạt động của DS1307, chúng ta sẽ khảo sát các thanh ghi này một cách chi tiết. Trước hết hãy quan sát tổ chức theo từng bit của các thanh ghi này như trong hình 4.

Hình 4. Tổ chức các thanh ghi thời gian.

      Điều đầu tiên cần chú ý là giá trị thời gian lưu trong các thanh ghi theo dạng BCD. BCD là viết tắt của cụm từ Binary-Coded Decimal, tạm dịch là các số thập phân theo mã nhị phân. Ví dụ bạn muốn cài đặt cho thanh ghi MINUTES giá trị 42. Nếu quy đổi 42 sang mã thập lục phân thì chúng ta thu được 42=0x2A. Theo cách hiểu thông thường chúng ta chỉ cần gán MINUTES=42 hoặc

Page 263: asembly

MINUTES=0x2A, tuy nhiên vì các thanh ghi này chứa giá trị BCD nên mọi chuyện sẽ khác, tôi sẽ diễn giải bằng hình 5.

Hình 5. Số BCD.

      Với số 42, trước hết nó được tách thành 2 chữ số (digit) 4 và 2. Mỗi chữ số sau đó được đổi sang mã nhị phân 4-bit. Chữ số 4 được đổi sang mã nhị phân 4-bit là 0100 trong khi 2 được đổi thành 0010. Ghép mã nhị phân của 2 chữ số lại chúng ta thu được mốt số 8 bit, đó là số BCD. Với trường hợp này, số BCD thu được là 01000010 (nhị phân) = 66. Như vậy, để đặt số phút 42 cho DS1307 chúng ta cần ghi vào thanh ghi MINUTES giá trị 66 (mã BCD của 42). Tất cả các phần mềm lập trình hay thanh ghi của chip điều khiển đều sử dụng mã nhị phân thông thường, không phải mã BCD, do đó chúng ta cần viết các chương trình con để quy đổi từ số thập nhị phân (hoặc thập phân thường) sang BCD, phần này sẽ được trình bày trong lúc lập trình giao tiếp với DS1307. Thoạt nhìn, mọi người đều cho rằng số BCD chỉ làm vấn đền thêm rắc rối, tuy nhiên số BCD rất có ưu điểm trong việc hiển thị nhất là khi hiển thị từng chữ số như hiển thị bằng LED 7 đoạn chẳng hạn. Quay lại ví dụ 42 phút, giả sử chúng ta dùng 2 LED 7-đoạn để hiện thị 2 chữ số của số phút. Khi đọc thanh ghi MINUTES chúng ta thu được giá trị 66 (mã BCD của 42), do 66=01000010 (nhị phân), để hiển thị chúng ta chỉ cần dùng phương pháp tách bit thông thường để tách số 01000010 thành 2 nhóm 0100 và 0010 (tách bằng toán tử shift “>>” của C hoặc instruction LSL, LSR trong asm) và xuất trực tiếp 2 nhóm này ra LED vì 0100 = 4 và 0010 =2, rất nhanh chóng. Thậm chí, nếu chúng ta nối 2 LED 7-đoạn trong cùng 1 PORT, việc tách ra từng digit là không cần thiết, để hiển thị cả số, chỉ cần xuất trực tiếp ra PORT. Như vậy, với số BCD, việc tách và hiển thị digit được thực hiện rất dễ dàng, không cần thực hiện phép chia (rất tốn thời gian thực thi) cho cơ số 10, 100, 1000…như trong trường hợp số

Page 264: asembly

thập phân.      Thanh ghi giây (SECONDS): thanh ghi này là thanh ghi đầu tiên trong bộ nhớ của DS1307, địa chỉ của nó là 0x00. Bốn bit thấp của thanh ghi này chứa mã BCD 4-bit của chữ số hàng đơn vị của giá trị giây. Do giá trị cao nhất của chữ số hàng chục là 5 (không có giây 60 !) nên chỉ cần 3 bit (các bit SECONDS6:4) là có thể mã hóa được (số 5 =101, 3 bit). Bit cao nhất, bit 7, trong thanh ghi này là 1 điều khiển có tên CH (Clock halt – treo đồng hồ), nếu bit này được set bằng 1 bộ dao động trong chip bị vô hiệu hóa, đồng hồ không hoạt động. Vì vậy, nhất thiết phải reset bit này xuống 0 ngay từ đầu.      Thanh ghi phút (MINUTES): có địa chỉ 0x01, chứa giá trị phút của đồng hồ. Tương tự thanh ghi SECONDS, chỉ có 7 bit của thanh ghi này được dùng lưu mã BCD của phút, bit 7 luôn luôn bằng 0.      Thanh ghi giờ (HOURS): có thể nói đây là thanh ghi phức tạp nhất trong DS1307. Thanh ghi này có địa chỉ 0x02. Trước hết 4-bits thấp của thanh ghi này được dùng cho chữ số hàng đơn vị của giờ. Do DS1307 hỗ trợ 2 loại hệ thống hiển thị giờ (gọi là mode) là 12h (1h đến 12h) và 24h (1h đến 24h) giờ, bit6 (màu green trong hình 4) xác lập hệ thống giờ. Nếu bit6=0 thì hệ thống 24h được chọn, khi đó 2 bit cao 5 và 4 dùng mã hóa chữ số hàng chục của giá trị giờ. Do giá trị lớn nhất của chữ số hàng chục trong trường hợp này là 2 (=10, nhị phân) nên 2 bit 5 và 4 là đủ để mã hóa. Nếu bit6=1 thì hệ thống 12h được chọn, với trường hợp này chỉ có bit 4 dùng mã hóa chữ số hàng chục của giờ, bit 5 (màu orangetrong hình 4) chỉ buổi trong ngày, AM hoặc PM. Bit5 =0 là AM và bit5=1 là PM. Bit 7 luôn bằng 0. (thiết kế này hơi dở, nếu dời hẳn 2 bit mode và A-P sang 2 bit 7 và 6 thì sẽ đơn giản hơn).     Thanh ghi thứ (DAY – ngày trong tuần): nằm ở địa chĩ 0x03. Thanh ghi DAY chỉ mang giá trị từ 1 đến 7 tương ứng từ Chủ nhật đến thứ 7 trong 1 tuần. Vì thế, chỉ có 3 bit thấp trong thanh ghi này có nghĩa.     Các thanh ghi còn lại có cấu trúc tương tự, DATE chứa ngày trong tháng (1 đến 31), MONTH chứa tháng (1 đến 12) và YEAR chứa năm (00 đến 99). Chú ý, DS1307 chỉ dùng cho 100 năm, nên giá trị năm chỉ có 2 chữ số, phần đầu của năm do người dùng tự thêm vào (ví dụ 20xx).      Ngoài các thanh ghi trong bộ nhớ, DS1307 còn có một thanh ghi khác nằm riêng gọi là con trỏ địa chỉ hay thanh ghi địa chỉ (Address Register). Giá trị của thanh ghi này là địa chỉ của thanh ghi trong bộ nhớ mà người dùng muốn truy cập. Giá trị của thanh ghi địa chỉ (tức địa chỉ của bộ nhớ) được set trong lệnh Write mà chúng ta sẽ khảo sát trong phần tiếp theo, AVR và DS1307. Thanh ghi địa chỉ được tôi tô đỏ trong hình 6, cấu trúc DS1307.

Page 265: asembly

Hình 6. Cấu trúc DS1307.

 II. AVR và DS1307.

      Phần này tôi hướng dẫn lập trình điều khiển và giao tiếp với DS1307 bằng AVR, dùng WinAVR. Do DS1307 hoạt động như một Slave I2C, bạn nhất thiết phải đọc lại “Bài 8 -   Giao tiếp TWI-I2C ”, nhất là là 2 chế độ Master (Send và Reveive). Tôi sẽ không đề cập lại toàn bộ giao diện I2C nhưng tóm tắt cách thực hiện với AVR như sau: để thực hiện cuộc gọi ở chế độ Master, AVR sẽ gởi điều kiện START, tiếp theo là 7 bit địa chỉ Slave (SLA) +1 bit Write/Read, kế đến là quá trình đọc hay ghi dữ liệu giữa Master và Slave bằng các byte dữ liệu 8 bit (có thể chỉ 1 byte hoặc 1 dãy bytes), cứ sau mỗi byte sẽ có 1 bit ACK hoặc NOT ACK. Cuộc gọi kết thúc với việc Master phát điều kiện STOP. Cứ mỗi một quá trình, sẽ có 1 “code” được sinh ra trong thanh ghi trạng thái TWSR, kiểm tra giá trị code này để biết quá trình giao tiếp có thành công không. Bạn cần nhơ dãy code thành công khi Master truyền dữ liệu là: 0x08 -> 0x18 -> 0x28 ->…->0x28. Và dãy code

Page 266: asembly

thành công khi Master truyền dữ liệu là 0x08 - > 0x40 - > 0x50 ->…->0x50 -> 0x58. Nắm được cách  ghi và đọc của AVR Master là bạn đã nắm được 50% cách giao tiếp với DS1307, 50% còn lại chúng ta phải hiểu cách bố trí dãy dữ liệu của riêng DS1307. Hãy theo dõi phần tiếp theo..      Vì DS1307 là một Slave I2C nên chỉ có 2 mode (chế độ) hoạt động giao tiếp với chip này. Hai mode của DS1307 bao gồm Data Write (từ AVR đến DS14307) và Data Read (từ DS1307 vào AVR). Mode Data Write được dùng khi xác lập giá trị ban đầu cho các thanh ghi thời gian hoặc dùng để canh chỉnh thời gian. Trong chế độ này, AVR là 1 Master truyền dữ liệu đến DS1307 (Slave nhận dữ liệu). Mode Data Read được sử dụng khi đọc thời gian từ đồng hồ DS1307 vào AVR để hiển thị hoặc so sánh….Trong chế độ này, AVR là Master nhận dữ liệu và DS1307 là Slave truyền dữ liệu. Hình 7 mô tả cấu trúc dữ liệu trong chế độ Data Write.

Hình 7. Chế độ Data Write.

     Trước hết hãy nói về địa chỉ Slave Address (SLA) của DS1307 trong mạng I2C. Như chúng ta đều biết, trên mạng I2C mỗi thiết bị sẽ có một địa chỉ riêng gọi là SLA. SLA là con số 7 bit, như thế theo lý thuyết sẽ có tối đa 128 thiết bị trong 1 mạng I2C. Chip DS1307 là một I2C Slave nên cũng có một địa chỉ SLA, giá trị này được set cố định là 1101000  nhị phân, hay 0x68 thập lục phân. Do SLA của DS1307 cố định nên trong 1 mạng I2C sẽ không thể tồn tại cùng lúc 2 chip này (điều này thực sự không cần thiết) nhưng có thể tồn tại các thiết bị I2C khác hoặc tồn tại nhiều Master AVR. Quan sát hình 7, sau khi điều kiện START được gởi bởi Master (AVR) sẽ là 7 bit địa chỉ SLA của DS1307 (1101000). Do chế độ này là Data Write nên bit W (0) sẽ được gởi kèm sau SLA. Bit ACK (A) được DS1307 trả về cho Master sau mỗi quá trình giao tiếp. Tiếp theo sau địa chỉ SLA sẽ là 1 byte chứa địa chỉ của thanh ghi cần truy cập (tạm gọi là Addr_Reg). Cần phân biệt địa chỉ thanh ghi cần truy cập và địa chỉ SLA. Như tôi đã đề cập trên, địa chỉ của thanh ghi cần tuy cập sẽ được lưu trong thanh ghi địa chỉ (hay con trỏ địa chỉ), vì vậy byte dữ liệu đầu tiên sẽ được chứa trong thanh ghi địa chỉ của DS1307. Sau

Page 267: asembly

byte địa chỉ thanh ghi là một dãy các byte dữ liệu được ghi vào bộ nhớ của DS1307. Byte dữ liệu đầu tiên sẽ được ghi vào thanh ghi có địa chỉ được chỉ định bởi Addr_Reg, sau khi ghi 1 byte, Addr_Reg được tự động tăng nên các byte tiếp theo sẽ được ghi liên tiếp vào các thanh ghi kế sau. Số lượng bytes dữ liệu cần ghi do Master quyết định và không được vượt quá dung lương bộ nhớ của DS1307. Ví dụ sau khi gởi SLA+W, Master gởi 8 bytes gồm 1 byte đầu 0x00 và 7 bytes khác thì con trỏ địa chỉ sẽ trỏ đến thanh ghi đầu tiên (0x00 – thanh ghi SECONDS) và ghi liên tiếp 7 bytes vào 7 thanh ghi thời gian của SD1307. Đây là cách mà chúng ta sẽ thực hiện trong phần lập trình  giao tiếp ( xem chương trình con  TWI_DS1307_wblock phía sau). Quá trình ghi kết thúc khi Master phát ra điều kiện STOP.      Chú ý, nếu sau khi gởi byte Addr_Reg, Master không gởi các bytes dữ liệu mà gởi liền điều kiện STOP thì không có thanh ghi nào được ghi. Trường hợp này được dùng để set địa chỉ Addr_Reg phục vụ cho quá trình đọc. Tiếp theo, chúng ta khảo sát cách sắp xếp dữ liệu trong chế độ Data Read, xem hình 8.

Hình 8. Chế độ Data Read.

      Trong chế độ Data Read, bit R (1) được gởi kèm sau 7 bit SLA. Sau đó là liên tiếp các byte dữ liệu được truyền từ DS1307 đến AVR. Điểm khác biệt trong các bố trí dữ liệu của chế độ này so với chế độ Data Write là không có byte địa chỉ thanh ghi dữ liệu được gởi đến. Tất cả các bytes theo sau SLA+R đều là dữ liệu đọc từ bộ nhớ của DS1307. Vậy thì dữ liệu được đọc bắt đầu từ thanh  nào? Câu trả lời đó là thanh ghi được chỉ định bởi con trỏ địa chỉ, giá trị này được lưu lại trong các lần thao tác trước đo. Như vậy, muốn đọc chính xác dữ liệu từ một địa nào đó, chúng ta cần thực hiện quá trình ghi giá trị cho con trỏ địa chỉ trước. Để ghi giá trị vào con trỏ địa chỉ chúng ta sẽ gọi chương trình Data Write với chỉ 1 byte được ghi sau SLA+W như phần chú ý ở trên. 

Page 268: asembly

      Chúng ta đã chuẩn bị đầy đủ để giao tiếp với DS1307. Phần tiếp theo tôi sẽ trình bày chương trình và mô phỏng giao tiếp giữa AVR và DS1307. Hãy vẽ một mạch điện bằng Proteus như trong hình 9. Trong ví dụ này, ban đầu chúng ta sẽ cài đặt thời gian cho DS1307, sau đó tiến hành đọc thời gian từ chip đồng hồ này và hiển thị lên 1 Text LCD.

Hình 9. Ví dụ giao tiếp AVR – DS1307.

     Tôi sẽ chia chương trình thành 2 phần, phần giao tiếp với DS1307 thông qua I2C được viết trong file myDS1307RTC.h và phần ví dụ ghi-đọc, hiển thị được viết trong file DS1307RTC_Test.c. 

Page 269: asembly

List 1. myDS1307RTC.h. 

Page 270: asembly

     Các phần định nghĩa trước dòng 35 được trích từ bài TWI nên tôi không giải thích lại. Chúng ta bắt đầu từ dòng 36. Có 3 chương trình con được viết để giao tiếp giữa AVR với DS1307 đó là: ghi 1 dãy dữ liệu vào DS1307 tức chương trình con TWI_DS1307_wblock(uint8_t Addr, uint8_t Data[], uint8_t len), chương trình này được viết theo cách sắp xếp dữ liệu của chế độ Data Write trình bày ở trên. Chương trình con đọc dữ liệu từ DS1307 làTWI_DS1307_rblock(uint8_t Data[], uint8_t len ) và một chương trình con dùng để set địa chỉ thanh ghi cần truy cập có tên TWI_DS1307_wadr(uint8_t Addr).     Chương trình con TWI_DS1307_wblock(uint8_t Addr, uint8_t Data[], uint8_t len) nằm từ dòng 54 đến dòng 77. Trong chương trình con này, tham số Addr là địa chỉ thanh ghi cần truy cập, Data[] là mảng dữ liệu sẽ ghi vào DS1307 và len là số byte dữ liệu sẽ ghi (không tính byte Addr). Dòng 55, AVR phát ra điều kiện START để bắt 1 cuộc gọi I2C, sau đó chúng ta chờ cho bit TWINT được set lên 1 ở dòng 56 (TWINT = 1, công việc đã được thực hiện). Dòng 57 kiểm tra nếu điều kiện START đã gởi thành công hay không bằng cách so sánh thanh ghi trạng thái TWSR với “code” tương ứng (xem lại hình 2 trong bài giao tiếp TWI). Sau khi START được gởi, dòng 59 chúng ta gán địa chỉ SLA+W cho thanh ghi dữ liệu TWDR để phát ra trên I2C, TWDR=(DS1307_SLA<<1)+TWI_W. Trong dòng này, biến DS1307_SLA là SLA của DS1307 đã được định nghĩa trước ở dòng 15 trong khi TWI_W là bit W (=0) được định nghĩa ở dòng 20. Quá trình phát I2C chỉ bắt đầu khi bit TWINT được xóa, dòng 60 thực hiện việc này, sau đó phải chờ bit TWINT được set lên 1 chứng tỏ quá trình phát SLA kết thúc (dòng 61). Cuối cùng là kiểm tra code trong thanh ghi TWSR để xem quá trình phát SLA có thanh công, xem dòng 62 và hình 2 trong bài giao tiếp TWI. Chúng ta sẽ luôn theo cơ chế này khi làm việc với TWI của AVR, do đó trong các phần tiếp theo tôi chỉ giải thích nội dung truyền-nhận, không giải thích lại cơ chế. Sau khi phát SLA+W, các dòng 64 đến 65 phát địa chỉ thanh ghi cần truy cập (biến Addr) và sau đó phát mảng dữ liệu liên tiếp trong các dòng 69 đến 74. Cuối cùng là phát điện kiện STOP để kết thúc cuộc gọi.     Trong chương trình con ghi DS1307 trình bày ở trên, nếu tham số len=0 thì các dòng 69 đến 74 không được thực hiện, nghĩa là chỉ có địa chỉ Addr được phát mà không có dữ liệu nào kèm theo. Chúng ta có thể dùng đặc điểm này để set thanh ghi cho quá trình đọc. Tôi đã tách ra và viết thành 1 chương trình con tên TWI_DS1307_wadr(uint8_t Addr)trong các dòng từ 36 đến 52 dùng để thực hiện việc set địa chỉ này.     Chương trình con đọc DS1307 TWI_DS1307_rblock(uint8_t Data[], uint8_t len ) được trình bày trong các dòng từ 79 đến 99. Trong đó, tham số Data[] là mảng chứa dữ liệu đọc về, len là số bytes đọc về, đặc biệt không có tham số địa chỉ thanh ghi vì địa chỉ này sẽ được set riêng trước khi gọi chương trình con đọc

Page 271: asembly

DS1307. Dòng 84 một lệnh phát SLA+TWI_R được thực hiện, với bit TWI_R=1 (xem định nghĩa ở dòng 21), AVR đang báo cho DS1307 rằng nó muốn đọc dữ liệu từ DS1307. Quá trình đọc được chia thành 2 phần, trong phần 1 chúng ta đọc len-1 bytes đầu tiên (xem các dòng code từ 88 đến 92) và phần 2 đọc byte cuối cùng (dòng 94 đến 96). Chúng ta cần tách việc đọc byte cuối ra vì nếu nhìn lại chế độ đọc trình bày trong hình 8, sau mỗi byte được đọc, Master phải gởi 1 bit ACK đến DS1307, riêng byte cuối cùng Master phải gởi bit NOT ACK để báo DS1307 rằng Master không muốn đọc thêm (so sánh 2 dòng 89 và 94). Cuối cùng, Master gởi điều kiện STOP để kết thúc cuộc gọi.      Để kiểm tra các hàm giao tiếp DS1307, hãy tạo 1 Project bằng WinAVR với tên gọi DS1307RTC_Test, tạo file DS1307RTC_Test và viết code như trong list 2.List 2. DS1307RTC_Test.c.

Page 272: asembly
Page 273: asembly

      Chương trình demo DS1307 dùng các hàm trong file DS1307RTC.h trước đó, bạn cần copy file này vào cùng thư mục với chương trình demo này. Đồng thời, chép cả file myLCD.h vì ví dụ này có hiển thị LCD. Cơ chế của chương trình demo như sau: trong phần thân chương trình chính, ban đầu chúng ta ghi các thông số thời gian khởi tạo cho DS1307, tôi chọn thời điểm ghi vào là 11h:59p:55s của ngày 31, tháng 12 năm 09 (2009) cho mục đích kiểm tra. Với thời điểm này, sau khi chạy chương trình được 5s bạn sẽ thấy các thanh thời gian trong DS1307 tự động chuyển sang 0h:0p:0s ngày 1 tháng 1 năm 10. Chú ý là nguồn clock cho chip trong ví dụ này là 8MHz, Tôi dùng Timer0 để tạo ra 1 khoảng thời gian delay khoảng 32.7ms, cứ 10 lần ngắt Timer0 (tức khoảng 327ms) tôi sẽ đọc DS1307 và cập nhật kết quả lên LCD. Các biến phụ Second, Minute, Hour, Day, Date, Month, Year được khai báo ở dòng 8 và 9 chứa thời gian (số thập phân bình thường). Biến Mode chọn hệ thống giờ, Mode =0 là hệ thống 24h và Mode=1 là hệ thống 12h. Biến AP chứa buổi trong Mode 12h, AP=0 là buổi sáng (AM), AP=1 là buổi chiều (PM). Mảng tData[7] có 7 phần tử trong dòng 14 chứa 7 bytes tạm tương ứng với 7 thanh ghi thời gian để ghi vào DS1307 hoặc đọc ra từ chip này. Các dòng từ 17 đến 28 là 2 chương trình con đổi từ số BCD sang thập phân và ngược lại.      Chúng ta bắt đầu với chương trình con Display (void), hiển thị kết quả chứa trong mảng tData[7] lên LCD (dòng 30 đến 64). Các dòng từ 31 đến 37 dùng đọc giá trị trong mảng tData[7] ra các biến để hiển thị, vì tData[7] chứa giá trị đọc về từ các thanh ghi thời gian của DS1307 nên nó là các số BCD, chúng ta cần dùng hàm BCD2Dec để đổi sang số thập phân trước khi gán cho các biến như Second, Minute…hiển thị lên LCD. Riêng với thanh ghi HOURS (tương ứng với sData[2]) chúng ta cần kiểm tra hệ thống giờ, nếu là hệ thống 12h thì chỉ lấy 5 bit đầu của thanh ghi này gán cho biến Hour (xem lại phần tổ chức các thanh ghi thời gian ở hình 4), nếu là hệ thống 24h thì sẽ lấy 6 bit (xem 2 dòng 33 và 34). Các dòng từ 39 đến 64 in các biến thời gian lên LCD. Dòng đầu tiên của LCD dùng in giờ-phút-giây, dòng thứ 2 in năm-tháng-ngày.  Phần bố trí vị trí các giá trị in người đọc tự lý giải.      Chương trình chính main bắt đầu từ dòng 66 và kết thúc ở dòng 106. Các công việc thực hiện trong main bao gồm khởi động Text LCD, khởi động Timer0 ở chế độ thường, Prescaler=1024 và cho phép ngắt tràn (các dòng từ 77 đến 79). Với f=8MHz, giá trị định thì mỗi lần tràn Timer0 là : (1024(Prescaler)/8 (f))*256 (MAX)=32768 us =32.7ms. Các dòng từ 83 đến 90 gán giá trị các biến thời gian vào mảng tData để chuẩn bị ghi vào DS1307. Trước khi gán các biến này cho tData, chúng ta cần đổi giá trị thập phân của chúng thành BCD với hàm Dec2BCD. Dòng 91 khởi động I2C và dòng 92 ghi 7 phần tử của mảng tData vào DS1307 với hàm TWI_DS1307_wblock mà chúng ta đã định nghĩa trong file DS1307RTC.h. Chú ý là địa chỉ bắt đầu ghi là 0x00, vì thế 7 bytes của mảng tData sẽ được ghi chính xác vào 7 thanh ghi thời gian của DS1307. Sau khi ghi dữ liệu, cần 1 khoảng

Page 274: asembly

thời gian nhỏ để DS1307 xử lí, _delay_ms(1) là đủ. Các dòng từ 97 đến 100 tiến hành đọc thời gian từ DS1307 về và hiển thị lên LCD. Dòng 97 TWI_DS1307_wadr(0x00) dùng để set địa chỉ thanh ghi cần truy cập trước khi đọc, chúng ta muốn đọc hết 7 thanh ghi thời gian nên sẽ set địa chỉ về 0 (thanh ghi SECONDS). Phải delay 1 khoảng nhỏ trước khi tiếp tục đọc DS1307 (dòng 98). Dòng 99 chúng ta đọc 7 thanh ghi thời gian vào mảng tData và hiển thị lên LCD ở dòng 100. Chương trình chính kết thúc ở đây, việc còn lại cho trình phục vụ ngắt thực hiện.     Trong trình phục vụ ngắt tràn của Timer0 (từ dòng 107 đến 125), chúng ta tăng 1 biến tạm tên là Time_count, đến khi nào 10 ngắt xảy ra (khoảng 327ms) thì mới tiến hành đọc DS1307 một lần (các dòng từ 111 đến 113). Do cứ mỗi 327ms chúng ta đọc DS1307 1 lần nên sẽ có trường hợp 2 lần đọc  cùng 1 giá trị, chúng ta chỉ thực hiện việc cập nhật kết quả khi 1 giây đã qua. Dòng 115 so sánh kết quả đọc về với biến Second, tức là so sánh kết quả mới với kết quả cũ, nếu chúng khác nhau sẽ cập nhật giá trị giây trên LCD (các dòng từ 116 đến 119).  Chúng ta điều biết việc ghi lên LCD sẽ tốn khá nhiều thời gian, vì vậy chỉ nên cập nhật kết quả khi nào có sự thay đổi. Mặt khác, khi số giây thay đổi thì các biến thời gian khác thay đổi rất chậm, một cách tốt để tránh việc xóa và ghi LCD nhiều lần là cứ 60s hãy thực hiện hàm Display (trong hàm này có cả xóa và ghi các biến thời gian). Dòng 120 giúp thực hiện ý tưởng này, chỉ khi nào biến Second về 0 (đã qua 60s) mới gọi hàm Display().     Đến đây, toàn bộ việc truy cập DS1307 bằng AVR đã hoàn tất. Các ý tưởng mở rộng ứng dụng như thêm các nút chỉnh thời gian, cài đặt báo giờ…xin nhường lại cho bạn đọc tự phát triển.Giao tiếp AVR với máy tính (I)

1 2 3 4 5

 ( 23 Votes )Nội dung Các bài cần tham khảo trước

1. Giới thiệu

2. Sơ lược về cổng COM

C cho AVR .

UART

Page 275: asembly

3. Tạo cổng COM ảo cho mô phỏng

4. Sử dụng thư viện xuất nhập chuẩn stdio.h trong WinAVR

Bài 2

Download ví dụ

TextLCD

Mô phỏng với Proteus.

I. Giới thiệu

     Bài viết này sẽ nói về cách  giao tiếp giữa AVR và máy tính cá nhân (PC) theo một cách đơn giản nhưng khá toàn diện. Nó đơn giản vì tôi sẽ dùng một giao diện khá “cổ điển” để giao tiếp giữa AVR và PC, giao diện RS232 thông qua các cổng COM. Toàn diện vì tôi sẽ hướng dẫn các bạn từ cách mắc mạch chuyển giữa AVR và PC, cách viết chương trình giao tiếp theo chuẩn RS232 trên máy tính và trên AVR. Cụ thể bài này bao gồm:    -    Sơ lượt sơ đồ và chức năng các chân cổng COM trên máy tính.    -    Mạch chuyển kết nối AVR với PC qua cổng COM.    -    Tạo cổng COM ảo trên PC cho mục đích mô phỏng.    -    Sử dụng các hàm trong thư viện xuất nhập chuẩn của C như printf, scanf…trong WinAVR.    -    Viết chương trình giao tiếp RS232 cho AVR.    -    Sử dụng Hyper Terminal của Windows trong giao tiếp RS232.    -    Viết chương trình truy xuất cổng COM trên PC (Visual C++, Visual Basic)

II. Sơ lược về cổng COM

     Cổng COM hay cổng nối tiếp (COM Port, Serial Port) là cổng giao tiếp thuộc vào dạng “lão làng” trên PC, cả máy tính để bàn và Laptop. Ngày nay với sự xuất hiện và “bành trướng” của chuẩn USB thì cổng COM (và cả cổng LPT hay cổng song song) đang dần biến mất. Giao tiếp thông qua cổng COM là giao tiếp theo chuẩn nối tiếp RS232. Chuẩn này có tốc độ khá chậm nếu đem so sánh với USB. Tuy nhiên, với dân robotics hay control thì COM-RS232 lại rất được ưa chuộng vì tính đơn giản và cũng vì…sự chậm chạp này. Các cổng COM trên các máy tính hiện tại (nếu có) đa số là dạng cổng “đực” 9 chân  (male 9 pins). Tuy nhiên, đâu đó vẫn còn tồn tại loại cổng COM 25 chân, loại này về hình dáng khá giống cổng LPT nhưng là loại male trong khi cổng LPT là female. Hình 1 thể hiện 2 dạng của cổng COM và bảng 1 tóm tắt chức năng các chân của cổng này.

Page 276: asembly

Hình 1. Cổng COM 9 chân và 25 chân.

Page 277: asembly

 Bảng 1: Các chân trên cổng COM

      Đáng chú ý nhất trong các chân của cổng COM là 3 chân 0V SG (signal ground), chân phát dữ liệu TxD và chân nhận dữ liệu RxD. Đây là 3 chân cơ bản phục vụ truyền thông theo chuẩn RS232 và tương thích với UART trên AVR. Các chân còn lại cũng có thể được sử dụng nếu người dùng có 1 ít kiến thức về tổ chức thanh ghi của PC. Tuy nhiên, trong đa số trường hợp giao tiếp qua cổng COM thì chỉ 3 chân trên được sử dụng.

Page 278: asembly

    Như đã trình bày trong bài AVR5-UART, chuẩn RS232 và UART nhìn chung là như nhau về mặt khung truyền, tốc độ baud…nhưng khác nhau về mức điện áp và cực.  Xem lại ví dụ so sánh trong hình 2.

Hình 2. So sánh UART và RS232.

 Trong chuẩn UART (trên AVR), mức 1 tương ứng điện áp cao (5V, TTL) trong khi đối với RS232 thì mức 1 tương ứng với điện áp thấp (điện áp âm, có thể -12V). Như thế rõ ràng cần một “cầu chuyển” (converter) kết nối giữa 2 chuẩn này. May mắn là chúng ta không cần phải tự thiết kế cầu chuyển này vì đã có các IC chuyên dụng. MAX232 là một trong các IC chuyển UART-RS232 được sử dụng nhiều nhất. Tất nhiên, bạn hoàn toàn có thể tự tạo một mạch chuyển đơn giản chỉ với một vài linh kiện như tụ điện, điện trở, diode và transisotor nhưng tính ổn định thì tôi không đảm bảo. Hình 3 mô tả cách dùng IC Max232 để kết nối giữa UART trên AVR và cổng COM của PC.

Page 279: asembly

Hình 3. Kết nối AVR với PC thông qua Max232.

       Mạch điện trên chỉ có tác dụng thay đổi mức điện áp cho phù hợp giữa RS232 và UART, nó hoàn toàn không làm thay đổi phương thức giao tiếp của các chuẩn này và vì thế việc lập trình trên PC và AVR đều không có gì thay đổi. Thật ra Max232 có đến 2 cầu chuyển, trong hình 3 chúng ta chỉ sử dụng cầu chuyển 1. Chân phát TxD (chân 3) của cổng COM được nối với chân R1IN (Receive 1 Input) của Max232 thì tương ứng chân R1OUT (Receive 1 Output) phải nối với chân nhận RX của AVR. Tương tự cho trường hợp T1IN và T1OUT. Giá trị các tụ điện 10uF là tương đối chuẩn, tuy nhiên khi bạn thay bằng tụ 1uF thì mạch vẫn hoạt động nhưng khoảng cách truyền (cab nối) sẽ ngắn hơn (nếu dài quá sẽ phát sinh lỗi truyền thông). Các điện trở trong hình 3 chỉ có tác dụng làm bảo vệ cổng COM và các IC, bạn có thể không cần dùng các điện trở này vẫn không ảnh hưởng hoạt động của mạch. VCC và GND là nguồn của mạch AVR.

      Chú ý: nếu muốn thực hiện giao tiếp giữa 2 máy tính với nhau thông qua cổng COM, bạn cần dùng1 cab chéo (chân TxD của PC1 nối với RxD của PC2 và ngược lại) để nối 2 cổng COM lại với nhau.

 III. Tạo cổng COM ảo cho mô phỏng

Page 280: asembly

      Muốn thực hiện giao tiếp giữa AVR và PC thông qua cổng COM thì hiển nhiên bạn cần có cái cổng COM, ngoài ra bạn cần tự làm một mạch AVR và cầu chuyển Max232. Thật không may là không phải máy tính nào cũng có cổng này, nếu bạn chỉ muốn học cách giao tiếp AVR-PC hoặc chỉ muốn kiểm tra một giải thuật nào đó thì có lẽ mô phỏng là giải pháp được ưa thích hơn. Cho mục đích mô phỏng giao tiếp RS232, Proteus lại một lần nữa hữu ích khi cho phép mô phỏng truyền nhận dữ liệu với cổng COM. Như thế vấn đề còn lại là làm sao tạo các cổng COM ảo trên máy tính và kết nối chúng với nhau để thực hiện mô phỏng giao tiếp. Do tính chất của các cổng COM là chỉ được “mở” (open) 1 lần duy nhất, nghĩa là 2 phần mềm không thể cùng mở 1 cổng. Ý tưởng của chúng ta là tạo ra 2 cổng COM ảo được “nối chéo” sẵn với nhau (ví dụ COM2 và COM3). Trong phần mềm Proteus ngõ ra của UART được nối với COM2. Trong phần mềm trên PC (ví dụ Hyper Terminal) chúng ta kết nối với COM3. Bằng cách này chúng ta đã có thể thực hiện giao tiếp giữa AVR (mô hình Proteus) với PC (phần mềm Hyper Terminal).    Có một vài phần mềm tốt có khả năng tạo cổng COM ảo và kết nối ảo giữa chúng đúng như yêu cầu của chúng ta. Trong phần này tôi sẽ giới thiệu 2 phần mềm như thế, trong đó có 1 phần mềm miễn phí (Virtual Serial Port Emulator) và 1 phần mềm thu phí (Eltima Virtual Serial Port Driver).    Virtual Serial Port Emulator (VSPE): là một phần mềm tạo cổng COM và kết nối ảo tốt của Eterlogic. Điều đặc biệt là phiên bản dành cho Windows 32 bits hoàn toàn miễn phí, vì vậy đây là phần mêm đầu tiên bạn phải để ý khi muốn tạo dùng cho mục đích học tập.      Trước tiên bạn hãy download phần mềm VSPE bản mới nhất tại website chính thức của Eterlogic:http://www.eterlogic.com/Products.VSPE.html (nhấn vào nút Download ở phía cuối trang web). Giải nén file zip và chạy file SetupVSPE.exe để cài đặt. Sau khi cài đặt hãy tìm và chạy chương trình VSPE. Giao diện của VSPE như trong hình 4.

Page 281: asembly

Hình 4. Giao diện phần mềm VSPE.

     Sử dụng VSPE khá đơn giản, bạn chỉ việc nhấn vào nút “Create New Device” được tô đỏ trong hình 4, hoặc vào menu “Device” và chọn “Create”,  để tạo 1 cổng COM ảo. Trong hộp thoại “Specify device type” bạn chọn như bên dưới và nhấn Next. Sau đó bạn có thể chọn tên cho cổng COM mình muốn tạo trong (ví dụ COM2).

Page 282: asembly

Hình 5. Tạo cổng COM2 ảo bằng VSPE.

     Bạn có thể tiến hành tạo bao nhiêu cổng COM ảo tùy thích. Ví dụ bạn tạo 2 cổng COM2 và COM3, bước tiếp theo chúng ta sẽ “đấu chéo” 2 cổng này với nhau để mô phỏng việc truyền dữ liệu qua RS232. Từ giao diện chính của VSPE bạn nhấn tiếp vào nút “Create new Device…”.  Lần này, trong hộp thoại “Specify device type” bạn không chọn Connector nữa mà chọn “Serial Redirector” như

Page 283: asembly

trong hình 6. Sau đó nhấn next, chọn 2 cổng COM ảo đã tạo lúc trước và nhấn vào nút Finish.  

 

Hình 6. Tạo kết nối giữa 2 cổng COM.

    Sau khi hoàn tất bạn sẽ thấy các cổng COM ảo và kết nối giữa chúng được thể hiện trong giao diện VSPE như ở hình 7. Bạn có thể “minimize” giao diện VSPE để ẩn nó vào taskbar. Chú ý là nếu bạn đóng chương trình VSPE lại (tắt) thì các cổng COM ảo cũng biến mất. 

Page 284: asembly

Hình 7. Các cổng COM ảo và kết nối tạo bằng VSPE.

     Virtual Serial Port Driver (VSPD): là một phần mềm tạo cổng COM và kết nối ảo tốt của Eltima Software. Đây là phần mềm có thu phí, bạn có thể download bản dùng thử 14 ngày tại website chính thức của Eltima Software:http://www.eltima.com/products/vspdxp/     So với VSPE thì VSPD dễ sử dụng và ổn định hơn (vì là phần mềm thương mại). Sau khi download bản trial và tiến hành cài đặt, bạn hãy tìm và chạy file “Configure Virtual Serial Port Driver”. Giao diện của VSPD như trong hình 8.

Page 285: asembly

Hình 8. Giao diện phần mềm VSPD.

      Trong tab “Manager ports” phần mềm tự động đề nghị 1 cặp cổng COM ảo có thể được tạo ra, bạn có thể chọn lại tùy thích và nhấn “Add pair” để tạo 2 cổng COM này. Khác với VSPE, cổng COM ảo do VSPD tạo ra sẽ xuất hiện trong “Device list” của Windows và không bị mất đi khi người dùng tắt phần mềm VSPD. Hãy chạy trình “Device manager” của Windows, trong mục Ports (COM & LPT) bạn sẽ thấy các cổng COM ảo được tạo thành (xem ví dụ trong hình 9).

Hình 9. Các cổng COM ảo và kết nối giữa chúng được tạo bởi VSPD.

 IV. Sử dụng thư viện xuất nhập chuẩn stdio.h trong WinAVR

      Những ai đã từng học ngôn ngữ lập trình C chắc sẽ không quên chương trình “hello world” đầu tiên của

Page 286: asembly

mình: 

     Chương trình này chỉ làm 1 việc đơn giản là in dòng chữ “hello, world” lên màn hình. Việc in dòng chữ được thực hiện bởi lệnh “printf” trong dòng 3. Lệnh printf nằm trong thư viện stdio gọi là thư viện xuất nhập chuẩn (standard input/output). Lệnh printf trong stdio không chỉ được dùng để in lên màn hình mà có thể in lên bất kỳ thiết bị xuất nào (output device), ngay cả in ra 1 file trên ổ cứng máy tính…Cho AVR, nếu bạn sử dụng trình dịch CodevisionAVR của HPinfotech, khi bạn gọi lệnh printf thì chuỗi dữ liệu sẽ được in ra (xuất ra) module UART (tất nhiên bạn phải cài đặt các thanh ghi của UART để kích hoạt UART trước). Như thế CodevisionAVR tự hiểu UART là thiết bị xuất/nhập mặc định cho các lệnh trong thư viện stdio (printf, scanf…). Tuy nhiên, với WinAVR (avr-gcc) mọi chuyện lại khác, để sử dụng các lệnh xuất nhập chuẩn chúng ta cần khai báo một thiết bị xuất nhập và hàm xuất nhập “cơ bản”. Hàm xuất nhập cơ bản là hàm do người dùng định nghĩa, nhiệm vụ của nó là xuất (hoặc nhập) một ký tự ra một thiết bị xuất nhập nào đó. Ví dụ trong bài AVR5 - giao tiếp UART chúng ta định nghĩa một hàm “uart_char_tx” xuất ký tự ra UART như sau:

 Hoặc trong bài TextLCD chúng ta khảo sát hàm “putChar_LCD” xuất một ký tự ra LCD như bên dưới:

Page 287: asembly

 

      Cả 2 hàm “uart_char_tx” và “putChar_LCD” như ví dụ trên đều có thể được dùng làm hàm xuất nhập “cơ bản” cho các hàm như printf...trong thư viện xuất nhập chuẩn sdtio. Nếu giả sử hàm “uart_char_tx” được dùng thì khi gọi hàm hàm printf, chuỗi dữ liệu sẽ được xuất ra UART. Ngược lại, trường hợp hàm “putChar_LCD” được sử dụng như hàm cơ bản thì hàm printf của stdio sẽ xuất chuỗi dữ liệu lên LCD. Bằng phương thức này, trình dịch avr-gcc cho phép chúng ta tiếp cận thư viện stdio một cách mềm dẻo hơn, bạn có thể dùng các hàm của stdio để xuất/nhập dữ liệu vào bất kỳ thiết bị nào như UART terminal, TextLCD, Graphic LCD hay thậm chí SD, MMC card…một khi bạn định nghĩa được hàm xuất nhập “cơ bản”.      Để minh họa cho cách sử dụng các hàm trong thư viện stdio, tôi sẽ trình bày một ví dụ xuất dữ liệu ra TextLCD và uart bằng các hàm printf…của stdio. Mạch điện mô phỏng cho ví dụ được này thể hiện trong hình 10 bên dưới.

Page 288: asembly

Hình 10. Mô phỏng ví dụ xuất dữ liệu với thư viện stdio.

     Tất cả các dữ liệu hiển thị trên LCD và uart terminal trong hình 10 đều được thực hiện thông qua các hàm printf và fprintf. Ngoài ra trong ví dụ này, người dùng có thể nhập 1 ký tự từ bàn phím và mã ASCII của phím đó sẽ được in ra trên Terminal. Đoạn code trình bày trong List1.List 1. Xuất dữ liệu ra LCD và UART bằng thư viện xuất nhập chuẩn stdio

Page 289: asembly

  

Page 290: asembly
Page 291: asembly

     Để sử dụng các hàm trong thư viện xuất nhập chuẩn, chúng ta cần include file header của thư viện như trong dòng code 4 “#include <stdio.h>”. Chú ý khi sử dụng avr-gcc,  các hàm liên quan đến avr (avr-libc) nằm trong thư mục con “/avr/” của thư mục include nên khi kính kèm phải chỉ rõ thư mục con này. Ví dụ header io.h hoặc interrupt.h chứa các hàm chuyên biệt cho avr, khi đính kèm các file này chúng ta ghi cụ thể như: “#include <avr/io.h>”…Tuy nhiên, các file header của ngôn ngữ C chuẩn (như stdio.h, math.h, …) thì nằm trực tiếp ở thư mục include, khi đính kèm các file này phải ghi trực tiếp như trong dòng code 4. Ngoài ra do ví dụ này có sử dụng LCD, bạn cần copy và include thư viện myLCD.h như trong dòng 5 (xem lại bài TextLCD).     Như đã trình bày ở trên, để sử dụng các hàm trong stdio chúng ta cần có các hàm xuất/nhập cơ bản. Các dòng code từ 7 đến 11 là hàm xuất dữ liệu ra uart có tên “uart_char_tx”, hàm này sẽ được dùng làm hàm cơ bản cho các hàm xuất của stdio sau này. Thực chất hàm “uart_char_tx” đã được trình bày trong bài học về UART, ở đây có một thay đổi nhỏ là dòng code 8 “if (chr==’\n’) uart_char_tx(‘\r’)”, dòng này có nghĩa là khi gặp người dùng muốn xuất ra “ký tự” ‘\n’ thì hàm “uart_char_tx” sẽ xuất ra thêm ký tự ‘\r’ . Như thế, nếu sau này bắt gặp một dấu hiệu xuống dòng ‘\n’ (có mã ASCII là 10, gọi là Line Feed – LF) ở cuối câu thì một tổ hợp mã '\r'+'\n' (mã '\r' = 13 gọi là Carriage Return – CR) sẽ được gởi để thực hiện xuống dòng. Để nắm rõ hơn vấn đề này bạn tìm hiểu thêm về CRLF (Carriage Return Line Feed) trong Windows.      Hai dòng code 13 và 14 rất quan trọng khi muốn sử dụng thư viện stdio. Ý nghĩa của 2 dòng này là tạo 2 “FILE” ảo (hay còn gọi là stream) dành cho việc xuất dữ liệu. Chúng ta khảo sát dòng 14: tạo stream cho UART. static FILE uartstd= FDEV_SETUP_STREAM(uart_char_tx, NULL,_FDEV_SETUP_WRITE);

     Chúng ta tạo 1 biến tên uartstd (người dùng tự đặt tên tùy ý) có kiểu là FILE (một dạng thiết bị ảo), sau đó dùng macro “FDEV_SETUP_STREAM” để khởi tạo và cài đặt các thông số cho uartstd. Macro này có chức năng mở 1 thiết bị xuất nhập (fdevopen) và gán các “công cụ” cho việc xuất nhập ra thiết bị. #define FDEV_SETUP_STREAM(put,  get,  rwflag)     Các thông số kèm theo “FDEV_SETUP_STREAM” bao gồm 1 hàm cơ bản gọi là “put”, một hàm cơ bản gọi là “get” và một cờ chỉ chức năng xuất hoặc nhập của thiết bị được mở. Cụ thể, trong dòng code 13, biến uartstd là một “thiết bị ảo” được dùng cho việc xuất dữ liệu (do thông số _FDEV_SETUP_WRITE). Công cụ để xuất ra uartstd là hàm “uart_char_tx” mà chúng ta đã tạo phía trên. Không có hàm nhận dữ liệu về từ uartstd (thông số get = NULL). Bạn có thể hình dung thế này: biến uartstd là một tờ giấy, hàm “uart_char_tx” là một “con dấu” (stamp) cho phép in một ký tự lên tờ giấy uartstd. Chúng ta gán “uart_char_tx” cho

Page 292: asembly

usrtstd thì sau này tất cả việc in ấn lên tờ giấy uartstd sẽ do “con dấu” “uart_char_tx” thực hiện. Hàm “uart_char_tx” vì thế gọi là hàm xuất cơ bản.     Tương tự như thế, trong dòng code 13 chúng ta tạo 1 “tờ giấy” khác tên lcdstd và hàm cơ bản cho nó lá hàm “putChar_LCD”, hàm này đã được định nghĩa sẵn trong thư viện myLCD.h.       Các dòng code trong chương trình chính từ dòng 17 đến 25 dùng khởi động UART và TextLCD, bạn có thể xem lại các bài liên quan để hiểu thêm. Sau khi khởi động, UART và LCD đã sẵn sàng cho việc xuất dữ liệu. Bây giờ chúng ta có thể dùng các hàm trong thư viện stdio như printf hay sprint…để xuất dữ liệu. Bạn hay quan sát hình 10 vì tôi sẽ dùng nó để so sánh đối chiếu với các dòng code sau. Dòng 27 “printf("In lan 1")”, mục đích là in chuỗi “In lan 1” lên LCD bằng hàm printf. Tuy nhiên, xem trên hình 10 bạn không nhìn thấy chuỗi này xuất hiện. Xem tiếp dòng code 28 “fprintf(&lcdstd," www.hocavr.com ")” và xem lại hình 10, lần này bạn đã thấy chuỗi ký tự  “www.hocavr.com” xuất hiện trên LCD, nghĩa là việc in đã thành công với hàm fprintf. Hàm fprintf là hàm xuất dữ liệu ra một thiết bị ảo, trong đó tham số thứ nhất của hàm trỏ đến thiết bị và tham số thứ hai là chuỗi dữ liệu cần in. Trong trường hợp này chúng ta đã dùng fprintf để xuất chuỗi “www.hocavr.com” ra thiết bị ảo lcdstd và đã thành công. Vậy với hàm printf ở dòng 27 thì sao? Hãy khảo sát tiếp các dòng từ 30 đến 32. Dòng 30 chúng ta lại một lần nữa dùng hàm printf “printf("In lan 3")” để in dòng “In lan 3” lên LCD nhưng vẫn không thành công (xem LCD trong hình 10). Ở dòng 31 chúng ta gán “stdout=&lcdstd” trong đó stdout là một biến (thật ra là 1 stream hay một thiết bị ảo) có sẵn của ngôn ngữ C, biến này qui định thiết bị mặc định dùng cho việc xuất nhập dữ liệu, khi gán stdout trỏ đến lcdstd như dòng 31 nghĩa là chúng ta khai báo LCD là thiết bị xuất nhập mặc định. Vì vậy, trong dòng 32 chúng ta gọi hàm printf “printf("In lan 4: %i", x)” chúng ta đã thành công. Lần này, quan sát trên LCD bạn sẽ thấy dòng “In lan 4: 8205” xuất hiện. Ở đây 8205 là giá trị của biến x trong câu lệnh ở dòng 32. Tóm lại, hàm fprintf cho phép in trực tiếp ra một thiết bị ảo được chỉ định trong khi đó muốn dùng hàm printf chúng ta cần gán thiết bị xuất nhập mặc định trước cho biến stdout. Hãy quan sát đoạn code từ dòng 34 đến 37 và ba dòng đầu trong Terminal ở hình 10, chắc chắn bạn đã tự lý giải được các dòng code này.      Cuối cùng là trình phục vụ ngắt nhận dữ liệu của UART trong các dòng code từ 41 đến 44. Trong trình này, chúng ta chỉ thực hiện việc đơn giản là in dòng “Ma ASCII:” kèm theo đó là giá trị nhận về từ UART chứa trong thanh ghi UDR: “fprintf(&uartstd,"Ma ASCII: %i\n", UDR)”.Để tìm hiểu đầy đủ về thư viện stdio trong WinAVR bạn cần đọc tài liệu “avr-libc Manual”, phần Standard IO facilities.  

Page 293: asembly

    Trong bài 2 chúng ta sẽ tìm hiểu về Terminal trên máy tính và cách viết chương trình giao tiếp trên máy tinh bằng Visual Basic và Visual C++ 6.

Giao tiếp AVR với máy tính (II)

1 2 3 4 5

 ( 15 Votes )Nội dung Các bài cần tham khảo trước

Bài 1

1. RS232 Terminal

2. Lập trình giao tiếp với cổng COM bằng VB và Visual C++

Download ví dụ

C cho AVR .

UART

TextLCD

Mô phỏng với Proteus.

IV. RS232 Terminal

     RS232 Terminal là thuật ngữ dùng chỉ các phần mềm máy tính có khả năng nhận và phát dữ liệu ra cổng COM (như một thiết bị đầu cuối). Các RS232 Terminal rất hữa dụng để kiểm tra các chương trình truyền nhận dữ liệu qua cổng COM. Hệ điều hành Windows có sẵn một RS232 Terminal gọi là “Hyper Terminal”. Công cụ này khá tốt cho mục đích giao tiếp thông thường. Để sử dụng Hyper Terminal bạn hãy vào “All Programs/ Accessories/Communications/Hyper Terminal” hoặc đơn giản là vào Run và gõ lệnh “hypertrm”. Một hộp thoại có tên “Connection Description” xuất hiện, hãy điền một tên bất kỳ cho cuộc gọi và nhấn OK. Trong hộp thoại tiếp theo, Connect to, hãy chọn cổng COM mà bạn muốn giao tiếp, và nhấn OK. Cuối cùng là hộp thoại COM Properties cho phép bạn thiết lập các thông số giao tiếp như Baudrate, Parity bit, Stop bit như trong hình 11, chú ý hãy chọn Flow control là "none"…và nhấn OK.

Page 294: asembly

Hình 11. Thiết lập cuộc gọi.

   Giả sử bạn chạy chương trình ví dụ trong phần demo của stdio, bạn thu được giao diện HyperTerminal như trong hình 12.

Page 295: asembly

Hình 12. Giao diện Hyper Terminal.

     Trong bài học này, tôi giới thiệu một chương trình Terminal có tên Hercules của HW group (http://www.hw-group.com/products/hercules/index_en.html). Đây là một Terminal miễn phí rất tốt, dễ sử dụng và ổn định. Ngoài chức năng RS232 Terminal, Hercules còn được dùng cho các giao diện khác như TCP, UDP…Bạn chỉ cần download chương trình về và chạy file Hercules.exe. Bạn thu được giao diện Hercules như sau:

Page 296: asembly

Hình 13. Giao diện phần mềm Hercules.

     Hãy chọn tab Serial để giao tiếp với cổng COM, thiết lập các thông số như tên cổng, Baudrate, Data frame…rồi nhấn nút Open, bạn đã sẵn sàng để sử dụng Hercules.      Giả sử bạn có 3 cổng COM ảo tên là COM2, COM3 nối với nhau. Hãy sử dụng ví dụ trong phần stdio, trong mạch điện mô phỏng Proteus của ví dụ hãy xóa thiết bị ảo Terminal. Hãy thêm vào một thiết bị tên là COMPIM bằng cách search với keyword là COMPIM (hoặc chạy file AVR_STD_Terminal.DSN trong thư mục AVR_STD của ví dụ trên). Kết nối như trong hình 14. Sau đó right click vào COMPIM để vào hộp thoại “Edit component”, đổi thông số Physical port thành CÒM, đổi Virtual Baud Rate thành 38400. Chạy lại mô phỏng bạn sẽ thấy kết quả hiển thị trên Hercules như trong hình 14. Type 1 phím bất kỳ để thấy mã ASCII.      Đây là ví dụ cho phép bạn giao tiếp giữa chương trình AVR mô phỏng trong Proteus và ứng dụng chạy trên Windows thông qua các cổng COM ảo. Nó thực

Page 297: asembly

chất là một dạng giao tiếp máy tính bằng cổng COM, dành cho trường hợp bạn chưa có mạch AVR thật. Mấu chốt nằm ở thiết bị COMPIM trong Proteus. COMPIM thực chất là mô hình cổng COM  tồn tại trên máy tính của bạn. Trong trường hợp này chúng ta dùng Eltima VSPE (hoặc VSPD) để tạo 2 cổng COM ảo trên máy tính là COM2 và COM3, chúng được đấu chéo với nhau. Chúng ta set COMPIM trong Proteus là  COM2 trong khi cổng trên Hercules là COM3. Khi chạy mô phỏng, AVR sẽ gởi dữ liệu ra COMPIM (tức COM2), COM2 truyền đến COM3 và hiển thị trên Hercules. Chúng ta có thể tự viết các chương trình trên Windows để nhận và gởi giá trị qua COM thay cho Hercules. Trong phần tiếp theo tôi sẽ hướng dẫn bạn tạo chương trình như thế.

Page 298: asembly

Hình 14. Kết hợp mô phỏng và Hercules. 

V. Lập trình giao tiếp với cổng COM bằng Visual Basic và Visual C++

     Các chương trình Terminal đề cập ở trên là một dạng ứng dụng giao tiếp  giữa máy tính và vi điều khiển ở mức độ đơn giản. Trong nhiều trường hợp, yêu cầu

Page 299: asembly

giao tiếp đòi hỏi mức độ phức tạp cao hơn, ví dụ lưu trữ dữ liệu hay vẽ đồ thị biến thiên, thì người dùng cần phải tự viết các chương trình trên máy tính của riêng mình. Phần này tôi hướng dẫn bạn các viết chương trình trên máy tính để truyền và nhận dữ liệu từ cổng COM bằng 2 ngôn ngữ lập trình Visual Basic và Visual C++ (6.0) trên nền Windows. Chú ý, mục đích bài viết này là về AVR nên phần viết ứng dụng trên Windows tôi chỉ trình bày một cách đơn giản cốt cho bạn nắm được nguyên lý. Để phát triển các ứng dụng phức tạp hơn người đọc cần tự trang bị cho mình kiến thức về lập trình trên Windows. Trong tất cả các hướng dẫn bên dưới tôi giả sử là người đọc ít nhất biết được cách tạo Project trong Visual Basic hoặc/và Visual C++.

1. Viết chương trình giao tiếp cổng COM bằng Visual Basic 6.0

    Kể từ các phiên bản Windows 2000 về sau, việc giao tiếp với các cổng máy tính truyền thống, như cổng LPT, trong Windows tương đối khó khăn. Tuy nhiên, với cổng COM thì có điều may mắn là Microsoft có cung cấp một công cụ (thật ra là một control – điều khiển) có tên gọi là “Microsoft Communication Control” hay viết tắt là MSComm. MSComm xuất hiện trong các phần mềm lập trình nổi tiếng của MS như Visual Basic hay Visual C++ dưới dạng một “điều khiển”. Vì là một “điều khiển” được thiết kế sẵn cho cổng COM nên MSComm chứa tất cả các công cụ cần thiết để giao tiếp với cổng này, công việc của người viết chương trình chỉ đơn giản là khai báo và sử dụng. Để minh họa cách sử dụng MSComm trong Visual Basic, hãy làm theo hướng dẫn bên dưới.     Chạy Visual Basic 6, vào menu “File/New Project” và tạo một “Standard EXE”. Bạn sẽ thấy một Project có tên là “Project1” kèm một hộp thoại nền (form chính) có tên Form1 xuất hiện. Bạn có thể đặt tên bất kỳ cho Project và form chính. Hãy quan sát ví dụ trong hình 15. Từ thanh công cụ Toolbox hãy click vào control “textbox” và vẽ lên “form” chính 2 textbox tên là txtOuput và txtInput (xem hình 15) (đổi tên các textbox trong cửa sổ Properties  nằm ở gốc thấp, bên phải).  với txtOutput, hãy set thông số Multiple thành True và ScrollBars thành  “3 – Both”

Page 300: asembly

Hình 15. Giao diện Visual Basic 6.

Tiếp theo hãy đưa control MSComm vào form chính.  Theo mặc định, control MSComm không có sẵn trong Toolbox của Visual Basic, chúng ta cần thêm vào Toolbox trước khi sử dụng. Để thêm MSComm vào Toolbox, chọn Menu “Project/Components” bạn sẽ thấy một hộp thoại tên Components xuất hiện như trong hình 16. Tìm và click chọn vào ô “Microsoft Comm Control 6.0” như trong hình và nhấn OK. Lúc này, quan trong Toolbox của VB bạn sẽ thấy icon của MSComm xuất hiện. Click vào icon này và vẽ 1 đối tượng MSComm lên form chính (xem lại hình 15). Giữ tên mặc định của đối tượng này là MSComm1.

Page 301: asembly

     

Hình 16. Thêm công cụ MSComm vào Project.

Page 302: asembly

Viết code:     Mục đích của ví dụ này như sau: dữ liệu nhận về từ cổng COM sẽ hiển thị trên textbox txtOutput, và khi người dùng type 1 ký tự vào txtInput ký tự sẽ được truyền đi qua cổng COM.Trước hết, hãy doubleclick vào form chính, viết đoạn code sau vào sự kiện Form_Load():

      Mục đích của đoạn code này là cài đặt các thông số cho MSComm1.       - Thông số CommPort = 3 nghĩa là chúng ta muốn kết nối với cổng COM3. Thông số này do người dùng thay đổi tùy theo cổng COM chúng ta muốn giao tiếp.       - Thông số Setting = “38400, N, 8,1”  nghĩa là tốc độ Baud=38400, không sử dụng bit Parity, độ dài khung truyền bằng 8 và có 1 bit Stop.        -  RThreshold = 1 nghĩa là khi có 1 ký tự đến cổng COM, ngắt nhận dữ liệu sẽ xảy ra.      -  InputLen = 1 nghĩa là khi đọc dữ liệu từ bộ đệm nhận, chúng ta sẽ đọc lần lượt 1 ký tự (1 byte).      -  PortOpen = True tức cho phép “mở” cổng COM để sẵn sàng giao tiếp.     Tiếp theo, doubleclick vào biểu tượng của MSComm1 trên form chính để viết code vào sự kiệnMSComm1_onComm():

Page 303: asembly

      Sự kiện onComm() thực chất là trình phục vụ ngắt nhận dữ liệu của MSComm. Khi có 1 byte dữ liệu gởi đến bộ đệm của cổng COM (số lượng byte do RThreshold quy định) thì sự kiện onComm sẽ xảy ra (ngắt xảy ra), trong sự kiện này chúng ta sẽ viết code để nhận và xử lý dữ liệu. Dòng 2 chúng ta khai báo 1 biến tạm thời tên là InputText với kiểu dữ liệu string. Chú ý là sự kiện onComm có thể xảy ra do nhiều nguyên nhân, ở đây chúng ta chỉ quan tâm đến trường hợp dữ liệu truyền đến, dòng 3 là một dạng “lọc” sự kiện, chúng ta chỉ thực hiện các dòng code bên trong khi mà sự kiện comEvReceive xảy ra (dữ liệu được nhận về): If Me.MSComm1.CommEvent = comEvReceive Then. Việc quan trọng duy nhất để đọc dữ liệu được gởi đến COM là đọc bộ đệm Input của MSComm như trong dòng code 4: InputText = MSComm1.Input. Sau dòng lệnh này dữ liệu sẽ được chứa trong biến tạm InputText. Tiếp theo chúng ta chỉ cần cộng dồn các ký tự nhận về vào nội dung của Textbox txtOutput để hiển thị lên màn hình (dòng 5) : txtOutput.Text = txtOutput.Text + InputText. Dòng code 6 làm nhiệm vụ đưa con trỏ đến cuối nội dung của txtOutput để tiện cho việc quan sát dữ liệu.Cuối cùng, doubleclick vào Textbox txtInput và tìm sự kiện KeyPress để viết các dòng code sau:

     Sự kiện txtInput_KeyPress xảy ra khi người dùng nhấn 1 phím nào đó vào txtInput. Dòng codeMe.MSComm1.Output = Chr(KeyAscii) thực hiện việc gởi giá trị của KeyAscii ra  cổng COM, trong đó KeyAscii là mã Ascii của phím được nhấn.     Bạn đã hoàn tất viết chương trình truyền nhận dữ liệu qua cổng COM bằng Visual Basic. Để kiểm tra chương trình của bạn, hãy thực hiện mô phỏng theo các bước sau:      -  Dùng 1 trong 2 phần mềm VSPD hoặc VSPE để tạo 2 cổng COM ảo là COM2 và COM3, đấu chéo chúng với nhau (xem lại phần cổng COM ảo).       -  Tìm trong thư mục chứa ví dụ AVR_STD và chạy file mô phỏng bằng phần mềm Proteus  AVR_STD_Terminal.DSN.      -  Quay lại Visual Basic, nhấn nút Run hoặc F5 để chạy Project vừa mới viết.       -  Nhấn Run trong Proteus để mô phỏng mạch điện AVR_STD_Terminal.DSN. Bạn sẽ thấy kết một số text xuất hiện trông txtOutput như trong hình 17. Click vào txtInput và type bất kỳ một phím nào đó để xem kết quả. So sánh với mô phỏng trong hình 14 bạn thấy nét tương đồng. Như thế bạn đã thành công khi tự viết cho mình 1 ứng dụng giap tiếp với cổng COM bằng Visual Basic.

Page 304: asembly

2. Viết chương trình giao tiếp cổng COM bằng Visual C++ 6.0

     Phần này chúng ta sẽ thực hiện một ví dụ truyền nhận qua cổng COM tương tự như ví dụ ở phần trên nhưng sử dụng Visual C++ (VC++) của Microsoft. Mục đích chính là hướng dẫn cách sử dụng MSComm trong VC++, vì thế tôi sẽ trình bày rất sơ sài những phần như tạo Project trong VC++. Bạn đọc cần tự trang bị thêm kiến thức về lập trình VC++. Một trong những tài liệu rất hay cho người mới học lập trình VC là “Teach Yourself Visual C++ 6 in 21 Days” của  “Sams Teach Yourself”, bạn có thể tìm đọc nếu thấy cần thiết.     Từ VC++ hãy vào menu “File/New” để tạo 1 Project mới. Chọn loại Project là “MFC AppWizard (exe)”, trong Ô Project Name đặt tên cho Project là AVR_PC, nhấn OK. Trong hộp thoại thứ 2 hãy chọn “Dialog based” cho loại Project, và nhấn Finish để tạo Project (các bước khác để mặc định).

 

Hình 17. Tạo Project MFC trong VC++6.

Page 305: asembly

      Khi Project mới được tạo sẽ có 1 hộp thoại chính (Dialog) xuất hiện với 2 button “OK” và “Cancel” trên đó. Dùng công cụ “Edit” để thêm vào 2 “Edit box” và sắp xếp lại giao diện như hình 19. Right click vào các Edit box và chọn Proterties từ các Popup_menu, lần lượt đổi ID của 2 Edit box thành IDC_OUTPUT và IDC_INPUT.      Cũng giống như trong VB, Control MSComm không xuất hiện mặc định trong Toolbox của VC++, chúng ta cần thêm vào khi muốn sử dụng control này. Hãy vào menu “Project/Add to Project/ Components and Controls…”. Khi hộp thoại “Components and Control Gallery” xuất hiện bạn chọn vào thư mục “Registered ActiveX Controls” và tìm đến file “Microsoft Communications Control, Version 6.0” rồi nhấn nút insert, nhấn OK khi được hỏi bất kỳ câu hỏi gì, sau đó nhấn nút Close để đóng hộp thoại lại. Lúc này icon của MSComm sẽ xuất hiện trong Toolbox của VC++ như trong hình 19. Click chọn icon của MSComm và vẽ 1 control vào Dialog chính của Project. Theo mặc định Control này có tên IDC_MSCOMM1. 

Page 306: asembly

   

   

Hình 18. Thêm Control MSComm vào Toolbox trong VC++.

Page 307: asembly

Hình 19. Giao diện chương trình trong Visual C++.

      Việc lập trình trong VC++ tương đối khó hơn VB (cho người mới tìm hiểu). Các thuộc tính của các Control như Edit box không được truy cập trực tiếp như Textbox trong VB. Ví dụ để gán và hiển thị một chuỗi hay số lên Edit box chúng ta phải thực hiện gán và cập nhật dữ liệu qua các biến trung gian. Ở bước này chúng ta đi tạo 2 biến cho 2 Edit box. Nhấn vào menu “View/ClassWizard” hoặc tổ hợp phím “Ctrl+W” , trong hộp thoại  “MFC ClasWizard” hãy chọn tab “Member

Page 308: asembly

Variables”. Click vào dòng IDC_OUTPUT (chính là edit phía trên), nhấn vào nút “Add vatiable…” và điền tên biến là “m_txtOutput” với kiểu biến là CString như trong hình 20. Lặp lại các bước trên để tạo 1 biến tên “m_txtInput” cho “IDC_INPUT”. Cuối cùng là tạo 1 biến có tên “m_comm” cho IDC MSCOMM1. Nhấn OK để đóng hộp thoại MFC ClassWizard. Từ bây giờ, chúng ta chỉ cần nhớ 3 biến “m_txtOutput”, “m_txtInput”, “m_comm” khi muốn truy cập các Edit boxes và MSComm trong lúc viết code.

 

Hình 20. Tạo biến txtOutput cho Edit box IDC_OUTPUT.

Viết Code:     Nhấn Ctrl+W để mở lại ClassWizard, lần này chọn tab “Message Maps",

Page 309: asembly

trong ô “Class name” đảm bảo rằng “CAVR_PCDlg” được chọn. Trong ô “Object IDS” hãy chọn "CAVR_PCDlg”, ô “Messages” tìm và chọn "WM_INITDIALOGS” sau đó nhấn vào nút “Edit Code” (xem hình 21).

 

Hình 21. Bắt đầu viết code.

      Bây giờ bạn có thể viết code cho sự kiện “OnInitDilaog()”, đây là sự kiện xảy ra khi bạn chạy chương trình và Dialog chính được khởi động. Vì thế chúng ta sẽ cài đặt các thông số cho m_comm vào đây (m_comm là tên biến đại diện cho control IDC_MSCOMM1 mà chúng ta đã tạo ở các bước trên). Hãy thêm các dòng sau vào sau dòng “// TODO: Add extra initialization here”: 

Page 310: asembly

 

     Năm dòng code trên tương ứng với 5 dòng trong phần Form_Load() khi viết Project bằng VB mà chúng ta đã khảo sát ở trên, vì thế tôi không cần giải thích thêm cho các dòng code này.     Tiếp theo chúng ta sẽ viết code cho sự kiện onComm (ngắt nhận) của control MSComm, trước khi viết code hãy nhấnCtrl+W để hiện hộp thoại ClassWizard và thực hiện 6 bước như trong hình 22 để thêm sự kiện onComm vào Project. 

 

Page 311: asembly

Hình 22. Thêm sự kiện onComm để nhận dữ liệu từ cổng COM. 

 Viết đoạn code sa vào sự kiện onComm:

      Như đã trình bày ở trên, m_comm là biến đại diện cho MSComm việc thao tác với cổng COM bây giờ thực hiên thông qua biến m_comm. Trong dòng 4 chúng ta khai báo 1 biến phụ strInput có kiểu CString dùng chứa giá trị nhận về sau này.  Cũng giống như trong VB, sự kiện onComm có thể xảy ra do nhiều nguyên nhân, chúng ta chỉ quan tâm đến trường hợp có dữ liệu đến bộ đệm, dòng 5 cho phép ‘lọc” ra sự kiện cần thiết: if (m_comm.GetCommEvent()==2 ). Dòng 6 chúng ta khai báo 1 biến phụ tên in_dat với kiểu COleVariant. COleVariant là lớp (class) của MFC, tên gọi của nó là sự kết hợp của C + OLE +VARIANT trong đó OLE là “Object Linking Embedded” là một kiểu đối tượng không có sẵn mà được “nhúng” vào, MSComm là một loại OLE. VARIANT là một kiểu biến chưa xác định. Khi bạn có môt biến x, đôi khi bạn muốn gán giá trị số cho x nhưng cũng có khi bạn lại muốn gán chuỗi ký tự cho x. Khi đó hãy khai bao x là VARIANT. Trong trường hợp MSComm, dữ liệu vào và ra của đối tượng này thuộc dạng “chưa xác định” hay VARIANT. Trong dòng 7 chúng ta chỉ đơn giản nhận giá trị từ m_comm về biến in_dat: in_dat = m_comm.GetInput().  Dòng tiếp theo chúng ta “trích” thành phần chuỗi ký tự từ biến in_dat và gán cho biến strInput:strInput=in_dat.bstrVal (một cách tương đối có thể hiểu đổi in_dat thành CString và gán cho strInput). Chúng ta phải trích CString vì các Edit box chỉ

Page 312: asembly

hiển thị được CString. Để hiển thị dữ liệu nhận về lên Edit box (IDC_OUTPUT) chúng ta cộng dồn biến m_txtOutput (biến đại diện của Edit box IDC_OUTPUT) bằng dòng lệnh 9:m_txtOutput+=strInput. Cuối cùng, để cho giá trị của biến m_txtOutput cập nhật lên Edit box chúng ta phải gọi hàm UpdateData với tham số FALSE như dòng 10: UpdateData(FALSE) (đây là cách làm việc của Visual C++).      Các dòng code từ 12 đến 14 được dùng với mục đích đưa con trỏ về cuối dòng của Edi box sau khi kết thúc quá trình nhận dữ liệu. Bạn có thể bỏ qua nếu thấy không cần thiết.     Viêc cuối cùng là viết code cho Edit box bên dưới (IDC_INPUT) để khi chúng ta gõ (type) vào đấy, ký tự sẽ được gởi đến cổng COM. Nhấn Ctrl+W và thực hiện các bước bên dưới để thêm vào sự kiện onChange. 

 

Hình 23. Thêm sự kiện onChange cho IDC_INPUT.

     Hãy viết đoạn code sau vào sự kiện onChange của Edit box Input: 

Page 313: asembly

     Khi người dùng type 1 ký tự nào đó vào Edit box, sự kiện onChange xảy ra, khi đó chúng ta sẽ trích ký tự cuối cùng trong nội dung của Edit box bên dưới mà đại diện là biến m_txtInput bằng dòng lệnh 11: tmpStr=m_txtInput.Right(1).Trong đó tmpStr là một biến tạm khai báo ở dòng 9. Chú ý rất quan trong khi muốn đọc nội dung của Edit box chúng ta cần gọi hàm UpdateData với tham số TRUE trước đó như trong dòng 10. Sau cùng, gọi phương phương thức SetOutput của đối tượng MSComm để gởi giá trị ra cổng COM: m_comm.SetOutput(COleVariant(tmpStr)). Để gởi một ký tự (hay chuỗi ký tự) ra cổng COM trước hết chúng ta cần “ép kiểu” ký tự đó về COleVariant vì như đã trình bày, MSComm chỉ làm việc với COleVaraint. Đoạn COleVariant(tmpStr) thực hiện việc “ép kiểu” này.       Sau khi viết xong đoạn code cho sự kiện onChange bạn có thể nhấn tổ hợp phím Ctrl+F5 để chạy chương trình. Dùng mạch điện AVR_STD_Terminal.DSN và chạy mô phỏng như trong phần lập trình với VB. Kết quả thu được sẽ như trong hình 24.

Page 314: asembly

 

Hình 24. Giao tiếp giữa AVR và Visual C++.

Mời bạn tham khảo thêm phần mềm gCOM, một công cụ giao tiếp, lưu trữ dữ liệu và vẽ đồ thị cổng COM

C cho AVR

1 2

Page 315: asembly

3 4 5

 ( 109 Votes )

Page 316: asembly

Phan chu thich thuong co mau chu la green*/

      Tiền xử lí (preprocessor):  là một tiện ích của ngôn ngữ C, các preprocessor được trình biên dịch xử lí trước tất cả các phần khác, các preprocessor có chức năng tương tự các Directive trong ASM cho AVR.Các preprocessor được bắt đầu bằng dấu “#”, trong số các preprocessors trong ngôn ngữ C có hai preprocessors được sử dụng phổ biến nhất là#include và #define. Preprocessor #include chỉ định 1 file được đính kèm trong quá trình biên dịch (tương đương .INCLUDE trong ASM) và #define để định nghĩa 1 chuổi thay thế hoặc 1 macro. Xem các ví dụ sau:

#include "avr/io.h"  *đính kèm nội dung file io.h trong lúc biên dịch (file io.h nằm trong thư mục con avr của thư mục include trong thư mục cài đặt của WinAVR).*/#define max (a,b)   ((a)>(b)? (a): (b))  /*định nghĩa một macro tìm số lớn nhất trong  2 số a và b, trong chương trình nếu bạn gọi x=max(2,3) thì kết quả thu được x=3.*/

      Biểu thức (Expressions):  là 1 phần của các câu lệnh, biểu thức có thể bao gồm biến, toán tử, gọi hàm…, biểu thức trả về 1 giá trị đơn. Biểu thức không phải là 1 câu lệnh hoàn chỉnh. Ví dụ: PORTB=val.

      Câu lệnh (Statement): thường là 1 dòng lệnh hoàn chỉnh, có thể bao gồm các keywords, biểu thức và các câu lệnh khác và được kết thúc bằng dấu “;”. Ví dụ: unsigned char val=1; val*=2; …là các câu lệnh.

      Khối (Blocks):  là sự kết hợp của nhiều câu lệnh để thực hiện chung 1 nhiệm vụ nào đó, khối được bao bởi 2 dấu mở khối “{“ và đóng khối “}”: ví dụ 1 khối:

while(1){              PORTB=val;      _delay_loop_2(65000);      val*=2;      if (!val) val=1;        }

      Toán tử (Operators):  là những ký hiệu báo cho trình biên dịch các nhiệm vụ cần thực hiện, các bảng bên dưới tóm tắt các toán tử C dùng cho lập trình AVR:

Bảng 1 các toán tử đại số: dùng thực hiện các phép toán đại số quen thuộc, trong

đó đáng chú ý là các toán tử “++” (tăng thêm 1) và “--“ (bớt đi 1), chú ý phân biệt  

Page 317: asembly

y=x++  và y=++x, ví dụ ta có x=3 trong khi y=x++  nghĩa là gán x cho y rồi sau đó

tăng x thêm 1, điều này không ảnh hưởng đến y (cuối cùng y=3, x=4) trong khi

y=++x nghĩa là tăng x trước rồi mới gán cho y (cuối cùng y=x=4), tương tự cho

các trường hợp của toán tử “--“ .

Bảng 2 Toán tử truy cập và kích thức: toán tử [] thường được sử dụng khi bạn

dùng mảng trong lúc lập trình, phần tử thứ của mảng sẽ được truy xuất thông qua

[i], chú ý mảng trong C bắt đầu từ 0.

Bảng 3 Toán tử Logic và quan hệ: thực hiện các phép so sánh và logic, thường

được dùng làm điều kiện trong các cấu trúc điều khiển, chú ý toán tử so sánh bằng

“==”, toán tử này khác với toán tử gán “=”, trong khi y = x nghĩa là lấy giá trị của

x gán cho y thì (y== x) nghĩa là “nếu y bằng x”.

Page 318: asembly

Bảng 4 Toán tử thao tác Bit (Bitwise operator): là các toán tử thực hiện trên

từng bit nhị phân của các con số, các toán tử dịch trái “<<” và dịch phải ">>" rất

thường được sử dụng khi xử lí số.

Bảng 5 các toán tử khác: là 1 số toán tử đặc biệt rất hay sử dụng nhưng chúng ta

thường không để ý vì vai trò của chúng rất dễ nhận thấy. Đặc biệt chú ý toán tử

“?:” là 1 toán tử rất đặc biệt của C so với các ngôn ngữ lập trình khác, “?:” là toán

tử 3 ngôi duy nhất có thể dùng thay thế cho cấu trúc “if” đơn giản.

Page 319: asembly

II. Cấu trúc điều khiển và hàm.

2.1 Cấu trúc điều khiển (Flow Controls).

      Các cấu trúc điều khiển biến ý tưởng của bạn thành hiện thực. Một số cấu trúc điều khiển cơ bản trong C như sau:

      “If (điều kiện) statement;”:  nếu điều kiện là đúng thì thực hiện statement theo sau, statement có thể được trình bày cùng dòng hoặc dòng sau điều khiển If. Điều kiện có thể là một biểu thức bất kỳ, có thể là sự kết hợp của nhiều điều kiện bằng các toán tử quan hệ AND (&&), OR (||)…Điều kiện được cho là đúng khi nó khác 0, ví dụ if (1) thì điều kiện hiển nhiên là đúng. Xét một vài ví dụ dùng cấu trúc if như sau:

      If (!val) val=1; nghĩa là nếu val bằng 0 thì chương trình sẽ gán cho val giá trị là 1,  “!” là toán tử NOT, NOT của một số khác 0 thì bằng 0, ngược lại, NOT của 0 thì thu được kết quả là 1. Trong ví dụ này, nếu val bằng 0 thì !val sẽ bằng 1, như thế điều kiện sẽ trở thành đúng và câu lệnh “val=1” được thực thi.

      If (x==1 && y==2) result=’A’; nghĩa là nếu x bằng 1 và y bằng 2 thì gán ký tự ‘A’ cho biến result. Trong ví dụ này, toán tử logic “&&” được sử dụng để “nối” 2 điều kiện lại, bạn hoàn toàn có thể sử dụng nhiều toán tử logic khác nếu cần thiết.

      Trong trường hợp bạn muốn thực thi nhiều câu lệnh cùng lúc nếu một điều kiện nào đó thỏa thì bạn cần đặt tất cả các câu lệnh đó trong 1 khối như bên dưới:

If (điều kiện) {      Statement1;      Statement2;      …}

      “If (điều kiện ) statement1; else statement2; ”: nếu điều kiện đúng thì thực hiện statement1, ngược lại thực thi statement2.  Việc đặt các statement và else..trên cùng 1 dòng hay trên những dòng khác nhau đều không ảnh hưởng đến kết quả. Tương tự trường hợp trên, nếu có nhiều statements thì cần đặt chúng trong 1 khối.

Page 320: asembly

If (điều kiện) {      Statement1;      Statement2;      …}else {      Statement1;      Statement2;      …}

      Ngoài ra, bạn cũng có thể đặt nhiều cấu trúc if…else… lồng vào nhau.

      Cấu trúc switch: trong trường hợp có nhiều khả năng có thể xảy ra cho 1 biểu thức (hay 1 biến), ứng với mỗi khả năng bạn cần chương trình thực hiện một việc nào đó, khi này bạn nên sử dụng cấu trúc switch. Cấu trúc này được trình bày như bên dưới.

switch (biểu thức) {case hằng_số_1:       các statement1;break;case hằng_số_2:       các statement2;break;…default:       các statement khác;}

      Hãy xét 1 ví dụ bạn kết nối 2 chip AVR với nhau, 1 chip làm Master sẽ ra các lệnh điều khiển chip Slave, chip Slave nhận mã lệnh từ Master và thực hiện các công việc được thoả hiệp trước. Giả sử mã lệnh được lưu trong biến Command, dưới đây là chương trình ví dụ cách xử lí của chip Slave ứng với từng mã lệnh.

switch (Command) {case 1:       PWM=255;      ON_Motor();      break;case 2:       PWM=0;

Page 321: asembly

      OFF_Motor();;      break;…default:       Get_Cmd();break;}Ngoài ra, bạn cũng có thể đặt nhiều cấu trúc if…else… lồng vào nhau.

      Nếu Command=1, gán giá trị 255 cho biến PWM và gọi chương trình con ON_Motor(). Trong trường hợp này, break được sử dụng, break nghĩa là thoát khỏi cấu trúc điều khiển hiện tại ngay lập tức, như vậy sau khi thực hiện 2 lệnh, switch kết thúc mà không cần xét đến các trường hợp khác. Bây giờ, nếu Command=2, gán giá trị 0 cho biến PWM và gọi chương trình con OFF_Motor(), trong tất cả các trường hợp còn lại (default), thực hiện chương trình con Get_Cmd().

      “while (điều kiện ) statement1;”: là một cấu trúc lặp (Loop), ý nghĩa của cấu trúc while là khi điều kiện còn đúng thì sẽ thực hiện statement1 (hoặc các statements nếu chúng được đặt trong 1 khối {} như trong trường hợp của if được giới thiệu ở trên). Cẩn thận, bạn rất dễ rơi vào một vòng lặp “không lối thoát” với while nếu điều kiện luôn luôn đúng.

      “for (biểu_thức_1; biểu_thức_2; biểu_thức_3) statement;”: là một cấu trúc lặp khác, trong cấu trúc for, biểu_thức_1 thường được hiểu là khởi tạo, biểu_thức_2 là điều kiện và biểu_thức_3 là biểu thức được thực hiện sau. Cấu trúc for này tương đương với cấu trúc while sau: 

biểu_thức_1;while (biểu_thức_2){       statement;      biểu_thức_3;}

         Các biểu thức trong cấu trúc for có thể vắng mặt trong cấu trúc nhung các dấu “;” thì không được bỏ. Nếu bạn viết for( ; ; ) tương đương với vòng lặp vô tận while (1).

      Cấu trúc for thường được dùng để thực hiện 1 hay những công việc nào đó trong số lần nào đó, ví dụ bên dưới thực hiện xuất các giá trị từ 0 đến 200 ra PORTB, sau mỗi lần xuất sẽ gọi lệnh delay trong 65000 chu kỳ máy.

for (uint8_t  i=0; i<=200; i++){PORTB=i;

Page 322: asembly

_delay_loop_2(65000);}

      Chú ý, bạn có thể thực hiện việc khai báo 1 biến (xem phần khai báo biến bên dưới) ngay trong cấu trúc for nếu biến lần đầu được sử dụng. Ví dụ trên được hiểu như sau: khai báo 1 biến i kiểu byte không âm, gán giá trị khởi đầu cho i=0 (chỉ thực hiện 1 lần duy nhất), kiểm tra điều kiện i<=200 (nhỏ hơn hoặc bằng 200), nếu điều kiện còn đúng, thực hiện 2 statements trong block {}, sau đó quay về để thực hiện i++ (tăng i thêm 1) rồi lại kiểm tra điều kiện i<=200 và quá trình lặp lại. Như thế đoạn code trong {} được thực thi khoảng 201 lần trước khi biến i bằng 201 và điều kiện i<=200 sai.

2.2 Hàm (Functions).

      Ngôn ngữ C bao gồm tập hợp của rất nhiều hàm, mỗi hàm thực hiện một chức năng cụ thể, các hàm trong C thường được thiết kết rất nhỏ gọn, để có các hàm phức tạp người dùng cần tự tạo ra. Hàm C cho AVR được định nghĩa trong thư viện avr-libc, ngoài các hàm C thông thường, avr-libc còn chứa rất nhiều các hàm riêng dùng riêng cho chip AVR, các hàm này được khai báo trong các file header riêng, để sử dụng hàm nào, bạn cần #include file header tương ứng (tham khảo tài liệu “avr-libc user manual” để biết thêm chi tiết, trong tài liệu này, khi cần sử dụng một hàm nào tôi sẽ nói rõ file header cần thiết).

      Ví dụ: _delay_loop_2(65000) là một hàm được định nghĩa trong file “delay.h” (trong thư mục C:\WinAVR\avr\include\util), hàm này thực hiện việc delay khoảng 65000 chu kỳ máy. Có 4 hàm delay bạn có thể sử dụng sau khi include file đó là:

_delay_loop_1(uint8_t  __count) : delay theo một số lần chu kỳ máy nhất định

(biến __count), số lượng chu kỳ delay là số 8 bit (từ 0 đến 255).

_delay_loop_2(uint16_t  __count) : delay theo một số lần chu kỳ máy nhất định

(biến __count), số lượng chu kỳ delay là số 16 bit (từ 0 đến 65535).

(Chú ý: thực chất 2 hàm delay trên được định nghĩa trong file header

“delay_basic.h”).

_delay_us(double  __us): delay 1 microsecond.

_delay_ms(double  __ms): delay 1 milisecond.

Page 323: asembly

      Chú ý: để dùng 2 hàm _delay_us và _delay_ms cần định nghĩa tần số xung clock trong Makefile (biến F_CPU), sử dụng 2 hàm này trực tiếp thường cho kết quả không như mong muốn, tôi sẽ trình bày cách sử dụng 2 hàm này trong ví dụ bên dưới.

      Main:  một chương trình C cho AVR phải bao gồm 1 chương trình chính main, tất cả các nội dung chính sẽ được đặt bên trong chương trình chính. Cấu trúc chương trình chính có thể như sau:

int main(void){      //noi dung chinh      return 0; //gia tri tra ve cho chuong trinh chinh}

      Trong đó, int là kiểu giá trị trả về của main, từ khóa void  nói rằng chương trình chính của chúng ta không cần bất kỳ tham số nào kèm theo.

      Còn rất nhiều các vấn đề liên quan đến C cho AVR, chúng ta sẽ tìm hiểu trong lúc viết các ví dụ cụ thể.

III. Ví dụ minh họa.

      Để minh họa các khái niệm và phương pháp lập trình C cho AVR, tôi sẽ giải thích ví dụ quét LED viết bằng C mà chúng ta thực hiện trong bài hướng dẫn WinAVR. Đoạn code được trình bày trong  List 1.

List 1. ví dụ quét LED bằng C.

123456789101112131415

//file: main.c//Description: Cung hoc avr, www.hocavr.com#include <avr/io.h>#include <util/delay.h>unsigned char val=1;int main(void){      DDRB=0xFF; //xử dụng PORTD làm đường xuất dữ liệu      while(1){            PORTB=val;            _delay_loop_2(65000);            val*=2;            if (!val) val=1;          }      return 0;}

Page 324: asembly

      Trước hết là preprocessor đính kèm các file khi biên dịch, #include là đính kèm file header io.h, file này thực ra không phải là file chứa các thông tin về chip nhưng nó sẽ làm một nhiệm vụ trung gian là đính kèm 1 file khác tương ứng với biến MCU trong Makefile, ví dụ trong Makefile, MCU=atmega8 thì dòng “#include ” được thực thi, file iom8.hđược tự động đính kèm kèm vào và file iom8.h mới thực chất chứa các định nghĩa cho chip ATmega8 (các định nghĩa về địa chỉ thanh ghi, kích thước bộ nhớ,…). Điều này giúp bạn không cần nhớ hết tất cả các file header của từng chip AVR. Nếu “không an tâm”, bạn có thể thêm dòng  #include iom8.h sau  khi include io.h (điều này không thật sự cần thiết). Ngoài ra, mỗi lần include file io.h sẽ có 4 file header khác được tự động đính kèm là “avr/sfr_defs.h”, “avr/portpins.h”, “avr/common.h”, và  “avr/version.h”. Tóm lại bạn cần (hoặc phải) include file io.h và khai báo loại chip AVR trong file Makefile (dùng MFile, như hướng dẫn ở trên) là có thể an tâm viết chương trình C cho AVR.

      - Dòng thứ 4 include file header delay.h để sử dụng lệnh delay như đã đề cập ở trên.

      - Dòng 5 : khai báo 1 biến tên val trong bộ nhớ SRAM, kiểu của val là unsigned char là kiểu dữ liệu 8 bit không dấu có khoảng giá trị từ 0 đến 255. Biến val được dùng làm biến tạm để chứa giá trước khi xuất ra PORTB. Biến trong C được khai báo bằng cách đặt kiểu biến trước sau đó tên biến. Một số kiêu dữ liệu cơ bản trong C được tóm tắt trong bảng 6.

Bảng 6 các kiểu dữ liệu trong C.

Tên kiểu dữ liệu (Data type)

Số byte Khoảng dữ liệu (Range)

char 1 –127 to 127 or 0 to 255

unsigned char 1 0 to 255

signed char 1 –127 to 127

int 2 –32,767 to 32,767

unsigned int 2 0 to 65,535

Page 325: asembly

Tên kiểu dữ liệu (Data type)

Số byte Khoảng dữ liệu (Range)

signed int 2 Như kiểu int

short int 2 Như kiểu int

unsigned short int 2 0 to 65,535

signed short int 2 Như kiểu short int

long int 4 –2,147,483,647 to 2,147,483,647

signed long int 4 Như kiểu long int

unsigned long int 4 0 to 4,294,967,295

long long int 8 –(263–1) to 263–1 (C99 only)

signed long long int 8 same as long long int (C99 only)

unsigned long long int 8 0 to 264–1 (C99 only)

float 4 6 digits of precision

double 8 10 digits of precision

long double 12 10 digits of precision

      Một số kiểu dữ liệu thông dụng nhất là char (1 byte), int (2 byte) và float. Từ khóa unsigned được thêm trước 1 kiểu dữ liệu nguyên để chỉ định các số nguyên dương, khi đó khoảng giá trị nguyên sẽ được tăng lên gần 2 lần. Ví dụ char chỉ các số nguyên từ -127 đến 127 thường được dùng để chỉ mã ASCII của các ký tự trong bảng mã ASCII, nhưng unsigned char sẽ bao gồm các số nguyên dương từ 0 đến 255 và thường được dùng khi làm việc với các thanh ghi 8 bit.

Page 326: asembly

      Ngoài ra, avr-libc còn định nghĩa một số kiểu dữ liệu thay thế, chúng ta có thể dùng các kiểu dữ liệu này thay cho các kiểu thông thường, xem tóm tắt như bên dưới.

      Một khai báo uint8_t  val  tương đương usigned char val, sử dụng kiểu khai báo nào là do thói quen của người sử dụng. Chú ý là theo mặc định, một biến mới được khai báo theo cách thông thường như trên sẽ được đặt trong SRAM, như các bạn đã biết SRAM trong AVR tương đối nhỏ vì thế nên khai báo và sử dụng hợp lí biến, đừng khai báo quá nhiều biến nếu bạn không sử dụng hết, đừng khai báo kiểu biến quá lớn so với giá trị thật sử dụng, tuy nhiên cũng không được khai báo kiểu dữ liệu có kích thước quá nhỏ so với giá trị mà biến đó có thể vươn tới. Sử dụng bộ nhớ chương trình (flash program memory) để lưu trữ dữ liệu không đổi là một kỹ thuật khác để tiết kiệm bộ SRAM, tôi sẽ đề cập vấn đề này trong 1 bài khác.

      Cuối cùng về việc khai báo biến, một biến có thể được gán giá trị khởi tạo ngay lúc khai báo như trong trường hợp của chúng ta, biến val=1 lúc được khai báo.

      -  Dòng 6 “int main(void){” bắt đầu chương trình chính.

      - Dòng 7: “DDRB=0xFF” gán giá trị hexadecimal 0xFF (11111111) cho thanh thi điều khiển của Port B, DDRB, Port B khi đó sẽ trở thành Port xuất

      -  Dòng 8 “while (1){”: bắt đầu 1 vòng lặp vô tận.

      -  Dòng 9 và dòng 10: xuất val ra PORTB và gọi lệnh delay.

      - Bạn cần chú ý 11 và 12, 2 dòng này có chức năng “xoay” giá trị của biến val để xuất ra PORTB tạo hiệu ứng xoay vòng. val*=2 được hiểu là val=val*2, đây là 1 kiểu viết thu gọn của C, nếu toán hạng thứ nhất và kết quả trả về là cùng 1 biến, chúng ta có thể bỏ bớt 1 tên biến và di chuyển toán tử về bên phải toán tử gán “=”. Ví dụ: i = i + 6 được rút gọn thành i + = 6.

Page 327: asembly

      Như thế sau câu lệnh val*=2  giá trị của val được tăng lên 2 lần. Ý nghĩa thật sự của việc gấp đôi biến val là gì? Hãy nhìn vào giá trị nhị phân của val, lúc khai báo val, chúng ta gán cho val = 1 hay val = 00000001 (nhị phân), sau khi gấp đôi lần thứ nhất, val = 2=00000010, tiếp tục gấp đôi lần thứ hai, val = 4=00000100…có thể bạn đã thấy chuyện gì xảy ra? Đây là câu trả lời: “trong thao tác với số nhị phân, gấp đôi một số nghĩa là di chuyển số đó sang trái 1 vị trí”…Quá trình gấp đôi sẽ tiếp diễn đến lúc val = 128=10000000, nếu tiếp tục gấp đôi, bạn nghĩ val = 256 ? Tuy nhiên bạn nhớ rằng chúng ta đã khai báo biến val có kiểu unsigned char (8 bits), trong khi đó 256=100000000 (9 bits), nếu gán val = 256, chỉ có 8 bits thấp (00000000) của 256 sẽ được gán cho val, kết quả là val = 0. Nói một cách khác, sau khi val=128, val = 0, câu lệnh: “ if (!val) val=1; ” sẽ giúp cho quá trình quét lặp quay lại từ đầu nếu  val = 0. Mọi thứ đã rõ.

      Cuối cùng vì chương trình chính của chúng ta có kiểu int (int main…) chúng ta cần “trả về” một giá trị nào đó, “return 0;” thực hiện trả về 0 (bạn có thể trả về giá trị nào tùy ý). 

Assembly cho AVR

1 2 3 4 5

 ( 31 Votes )

Page 328: asembly
Page 329: asembly

Chức năng: Load hằng số K vào thanh ghi Rd.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: LDI R16, 99 kết quả là thanh ghi R1 mang giá trị 99.

- MOV (MOVE).

Cú pháp: MOV Rd, Rr

Chức năng: Copy giá trị trong thanh ghi Rr vào thanh ghi Rd.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: MOV R15, R16 kết quả là R15 có cùng giá trị với R16 (R15=R16=99).

- CLR (CLEAR Register).

Cú pháp: CLR Rd

Chức năng: Copy giá trị trong thanh ghi Rr vào thanh ghi Rd.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: áp dụng cho tất cả các thanh ghi trong RF.

- SER (SET Register).

Cú pháp: SER Rd

Chức năng: set tất cả các bit tronh thanh ghi Rd lên 1, sau lệnh này thanh ghi

Rd=0xFF.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: SER R16 kết quả là R16 = 0xFF.

- CBR (CLEAR Bit in Register).

Cú pháp: CBR Rd, K

Page 330: asembly

Chức năng: xóa các bit trong thanh ghi Rd với “mặt nạ”  K, nếu Bit nào trong K là

1 thì Bit tương ứng trong Rd sẽ bị xóa.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: CBR R16, 0xF0  kết quả là 4 bit cao nhất của  R16 bị xóa vì K=11110000

(B).

- SBR (SET Bit in Register).

Cú pháp: SBR Rd, K

Chức năng: set các bit trong thanh ghi Rd với “mặt nạ”  K, nếu Bit nào trong K là

1 thì Bit tương ứng trong Rd sẽ được set lên 1.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: SBR R16, 0xF0  kết quả là 4 bit cao nhất của  R16 được set lên 1 vì

K=11110000 (B).

- BLD (Bit LoaD from T Flag).

Cú pháp: BLD Rd,b

Chức năng: Load giá trị trong cờ T của thanh ghi SREG vào bit thứ b trong thanh

ghi Rd. Đây cũng chính là chức năng chính của cờ T.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

SET ; set bit T lên 1

BLD R16, 4

Kết quả là bit 4 của thanh ghi R16 được set lên 1 vì giá trị của bit T là 1.

- BST (Bit Storage from T Flag).

Cú pháp: BST Rd,b

Page 331: asembly

Chức năng: Copy bit thứ b trong thanh ghi Rd vào trong cờ T của thanh ghi SREG.

Đây cũng chính là chức năng chính của cờ T.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: BST R16, 4 kết quả là cờ T chứa giá trị của bit 4 của thanh ghi R16.

- CPI (COMPARE with Immediate).

Cú pháp: CPI Rd, K

Chức năng: so sánh thanh ghi Rd với hằng số K, lệnh này làm thay đổi nhiều bit

trong thanh ghi SREG trong đó sự thay đổi của cờ Zero là quan trọng nhất, nếu Rd

= K cờ Z=1, ngược lại Z=0, sử dụng đặc điểm thay đổi của cờ Z kết hợp với lệnh

BRNE hoặc BREQ chúng ta có thể tạo thành một lệnh rẽ nhánh.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: 

LDI R16, 10

CPI R16, 10 

Kết quả là cờ Z được set thành 1 vì lúc này R16 =10.

- ANDI (AND with Immediate).

Cú pháp: ANDI Rd, K

Chức năng: thực hiện phép Logic AND giữa thanh ghi Rd với hằng số K và kết

quả đặt lại trong Rd.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: ANDI R17, 0x00 kết quả là R17 có 0x00.

- AND (Logical AND).

Cú pháp: AND Rd, Rr

Page 332: asembly

Chức năng: thực hiện phép Logic AND giữa 2 thanh ghi Rd và Rr , kết quả đặt lại

trong Rd.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R1, 0xFF ;(11111111)

LDI R17, 0xAA; (10101010)

AND R1, R17 

Kết quả là R1=0xAA vì 11111111 & 10101010 =10101010.

- ORI (Logical OR with Immediate).

Cú pháp: ORI Rd, K

Chức năng: thực hiện phép Logic OR giữa thanh ghi Rd với hằng số K và kết quả

đặt lại trong Rd.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: ORI R17, 0xFF kết quả là R17 có 0xFF.

- OR (Logical OR).

Cú pháp: OR Rd, Rr

Chức năng: thực hiện phép Logic OR giữa 2 thanh ghi Rd và Rr , kết quả đặt lại

trong Rd.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R1, 0xFF ;(11111111)

LDI R17, 0xAA; (10101010)

OR R1, R17 

Kết quả là R1=0xFF vì 11111111 or 10101010 =11111111.

Page 333: asembly

- LSL (Logical Shift Left).

Cú pháp: LSL Rd

Chức năng: dịch tất thanh ghi Rd sang trái 1 vị trí, Bit 7 (bit lớn nhất) của Rd sẽ

được chứa trong cờ nhớ C, bit 0 của Rd bị xóa thành 0. Thực chất LSL tương

đương với phép nhân thanh ghi Rd với 2. Bạn xem hình minh họa bên dưới.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R1, 0B11000011 ; (dạng nhị phân của 195)

LSL R1

Kết quả là R1=10000110 và cờ C =1 vì thanh ghi R1 đã được dịch sang trái 1 vị

trí, trước khi dịch bit 7 của R1 là 1 nên sau khi dịch bit này được chứa trong C, cho

nên C=1.

- LSR (Logical Shift Right).

Cú pháp: LSR Rd

Chức năng: dịch tất thanh ghi Rd sang phải 1 vị trí, Bit 0 (bit nhỏ nhất) của Rd sẽ

được chứa trong cờ nhớ C, bit 7 của Rd bị xóa thành 0. Thực chất LSR tương

đương với phép chia thanh ghi Rd cho 2. Bạn xem hình minh họa bên dưới.

Page 334: asembly

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R1, 0B11000110 ; (dạng nhị phân của 195)

LSR R1

Kết quả là R1=01100001 và cờ C =1 vì thanh ghi R1 đã được dịch sang phải 1 vị

trí, trước khi dịch bit 0 của R1 là 1 nên sau khi dịch bit này được chứa trong C, cho

nên C=1.

- ADD (ADD without Carry).

Cú pháp: ADD Rd, Rr

Chức năng: thực hiện phép cộng 2 thanh ghi Rd và Rr , kết quả đặt lại trong Rd.

Cờ nhớ C không được sử dụng.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R16, 30

LDI R17, 25

ADD R16, R17

Kết quả là R16=55.

- INC (INCrement).

Cú pháp: INC Rd

Chức năng: tăng thanh ghi Rd 1 đơn vị và kết quả đặt lại trong Rd. Lệnh này đặc

biệt thích hợp cho các ứng dụng lặp, kết hợp với BREQ hay BRNE có thể tạo

thành 1 vòng lặp FOR.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Page 335: asembly

Ví dụ: INC R17 kết quả là R17 được tăng thêm 1 đơn vị.

- SUB (SUBtract without Carry).

Cú pháp:  SUB Rd, Rr

Chức năng: thực hiện phép trừ 2 thanh ghi Rd - Rr , kết quả đặt lại trong Rd. Cờ

nhớ C không được sử dụng.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R16, 30

LDI R17, 25

SUB R16, R17 

Kết quả là R16=5.

- SUBI (SUBtract Immediate).

Cú pháp:  SUBI Rd, K

Chức năng: thực hiện phép trừ  thanh ghi Rd với hằng số K, kết quả đặt lại trong

Rd.

Giới hạn: chỉ áp dụng cho các thanh ghi từ R16 đến R31.

Ví dụ: 

LDI R16, 30

SUBI R16, 20

Kết quả là R16=10.

- DEC (DECrement).

Cú pháp: DEC Rd

Page 336: asembly

Chức năng: giảm thanh ghi Rd 1 đơn vị và kết quả đặt lại trong Rd. Lệnh này đặc

biệt thích hợp cho các ứng dụng lặp, kết hợp với BREQ hay BRNE có thể tạo

thành 1 vòng lặp FOR.

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: DEC R17 kết quả là R17 được giảm đi 1 đơn vị.

 

- MUL (MULtiply unsigned).

Cú pháp: MUL Rd, Rr

Chức năng: thực hiện phép nhân không dấu 2 thanh ghi 8 bit Rd, Rr, kết quả là 1

số 16 bit đặt trong 2 thanh ghi R1:R0. Chú ý nếu Rd và Rr là các thanh ghi R1 và

R0 thì kết quả sau khi tính được sẽ được viết đè lên. Xem hình minh họa

instruction MUL bên dưới.

 

Giới hạn: áp dụng cho tất cả các thanh ghi trong RF.

Ví dụ: 

LDI R16, 30

LDI R17, 25

MUL R16, R17 

Kết quả là R1=0x2, R0=0xEE, vì 30x25=750=0x02EE.

II. Instruction cho các thanh ghi I/O.

Page 337: asembly

Bốn instruction sau đây được thiết kế riêng để truy cập vùng nhớ I/O, các instruction này sử dụng địa chỉ I/O của các thanh ghi trong vùng nhớ này. Vì là thiết kế riêng cho vùng nhớ I/O, bạn không thể sử dụng các thanh ghi này để truy cập RF hay SRAM. Trong các cú pháp của instruction này, khái niệm địa chỉ A là địa chỉ I/O, 0 ≤ A ≤ 63, nếu trong ví dụ A=0x00 thì đó là thanh ghi đầu tiên của vùng I/O, không phải là thanh ghi R0.

- OUT (OUTPUT Data).

Cú pháp: OUT A, Rr

Chức năng:  xuất giá trị từ thanh ghi Rr ra thanh ghi có địa chỉ A trong vùng nhớ

I/O. đây là cách phổ biến nhất để xuất giá trị ra vùng I/O.

Giới hạn: Rr là thanh ghi RF bất kỳ, A bị giới hạn từ 0 đến 63.

Ví dụ:

LDI R16, 0xFF

OUT 0x11, R16

Kết quả là thanh ghi có địa chỉ 0x11 trong vùng I/O, tức thanh ghi DDRD, có giá

trị bằng 0xFF.

- IN (INPUT Data).

Cú pháp: IN Rr, A

Chức năng:  Load giá trị từ thanh ghi có địa chỉ A trong vùng nhớ I/O vào thanh

ghi Rr. Đây là cách phổ biến nhất để nhận giá trị từ vùng I/O.

Giới hạn: Rr là thanh ghi RF bất kỳ, A bị giới hạn từ 0 đến 63.

Ví dụ: 

IN R16, 0x10

Kết quả là thanh ghi R16 nhận được giá trị của thanh ghi có địa chỉ 0x11 trong

vùng I/O, tức thanh ghi PIND, đây chính là ví dụ đọc giá trị các chân của PORTD

vào R16.

Page 338: asembly

- SBI (Set Bit in I/O Register).

Cú pháp: SBI A, b

Chức năng:  Set bit thứ b trong thanh ghi có địa chỉ A trong vùng nhớ I/O. Tuy

nhiên lệnh này không có tác dụng trên toàn bộ vùng I/O mà chỉ có tác đối với 32

thanh ghi đầu (địa chỉ từ 0 đến 31).

Giới hạn: b là số thứ các bit trong thanh ghi, 0≤b≤7; A bị giới hạn từ 0 đến 31.

Ví dụ: 

SBI 0x12, 2

Kết quả là bit 2 của thanh ghi có địa chỉ 0x12 trong vùng I/O, tức thanh ghi

PORTD, được set lên 1. Đây chính là ví dụ set chân PD2 của PORTD.

- CBI (Clear Bit in I/O Register).

Cú pháp: CBI A, b

Chức năng:  xóa bit thứ b trong thanh ghi có địa chỉ A trong vùng nhớ I/O. Tuy

nhiên lệnh này không có tác dụng trên toàn bộ vùng I/O mà chỉ có tác đối với 32

thanh ghi đầu (địa chỉ từ 0 đến 31).

Giới hạn: b là số thứ các bit trong thanh ghi, 0≤b≤7; A bị giới hạn từ 0 đến 31.

Ví dụ: 

CBI 0x12, 2

Kết quả là bit 2 của thanh ghi có địa chỉ 0x12 trong vùng I/O, tức thanh ghi

PORTD, bị xóa thành 0. Đây chính là ví dụ xóa chân PB2 của PORTD.

III. Các con trỏ X, Y, Z và cách truy cập toàn bộ không gian bộ nhớ.

     Trong Register File của AVR, các thanh ghi từ R26 đến R31ngoài chứa năng thanh ghi thông thường còn có chức năng là con trỏ (Pointer) trong việc truy cập bộ nhớ (cả bộ nhớ data và bộ nhớ Program). Nếu được sử dụng như các Pointer, các thanh ghi trên được biết đến với tên gọi X, Y, Z. Định nghĩa như sau: X=R27:R26, Y=R29:R28, Z=R31:R30. Chúng là 3 thanh ghi 16 bit được định

Page 339: asembly

nghĩa trước cho tất cả các AVR. Ngoài ra trong các file định nghĩa cho chip chúng ta có thêm 6 định nghĩa khác là XL, XH, YL, YH, ZL, ZH cũng chính là tên gọi của R26-> R31. Phần này chúng ta khảo sát một số instruction dùng truy cập toàn bộ khồi nhớ của AVR bằng cách sử dụng địa chỉ trực tiếp và bằng cách sử dụng Pointer.

- LDS (LoaD direct from data Space).

Cú pháp: LDS Rd, k

Chức năng:  load giá trị 1 byte từ thanh ghi có địa chỉ k trong SRAM vào thanh ghi

Rd, k là dạng địa chỉ tuyệt đối có giới hạn từ 0 đến 65535(2^16-1).

Giới hạn: Rd là thanh ghi bất kỳ trong RF nhưng giá trị lớn nhất của k là 65535, vì

thế với lệnh này ta không thể truy cập vượt quá khoảng không gian 64KB. Nếu

muốn truy cập vùng không gian lớn hơn 64KB chúng ta cần một số hỗ trợ, tuy

nhiên ở đây tôi giả sử bộ nhớ của chip (thường là bộ nhớ data) không vượt quá

64KB (thực tế chưa có chip AVR nào có SRAM hay EEPROM vượt quá 64KB).

Ví dụ: 

LDS R2, 0x0060

Kết quả là thanh ghi R2 chứa giá trị của thanh ghi có địa chỉ 0x0060, đây là thanh

ghi đầu tiên trong khoảng SRAM (sau RF và vùng I/O) của AVR.

- STS (STorage  direc to data Space).

Cú pháp: STS k, Rr

Chức năng:  instruction này hoàn toàn giống LDS nhưng dùng để xuất dữ liệu từ

thanh ghi Rr ra RAM, ngươi đọc có thể tham khảo phần giải thích cho LDS.

Sử dụng địa chỉ trực tiếp thì câu lệnh sẽ đơn giản nhưng rất khó nhớ phần địa chỉ, thông thường SRAM là vùng chúng ta hay sử dụng để chứa biến tạm thời, trong các ngôn ngữ cấp cao ta chỉ cần nhớ tên biến nhưng với ASM chúng ta phải nhớ địa chỉ của chúng. Một cách tốt để tránh việc này là dùng chỉ thị (DIRECTIVE,

Page 340: asembly

bạn xem lại bài 1) . EQU để gán tên biến cho 1 địa chỉ, ví dụ  .EQU  bientam = 0x0060 và sau đó sử dụng bientam thay cho 0x0060.

Một cách khác được dùng để truy cập bộ nhớ mà không dùng địa chỉ tuyệt đối là sử dụng sử dụng con trỏ. Có 2 instruction hỗ trợ con trỏ là LD(LoaD indirec from data Space),  và ST (STorage indirec to data Space), LD đọc dữ liệu từ SRAM vào thanh ghi còn ST lưu dữ liệu từ thanh ghi vào SRAM. Cả 3 con trỏ X, Y và Z đều có thể được dùng nhưng có một số điểm lưu ý: cả 3 đều dùng được trong trường hợp truy xuất thông thường nhưng với cách truy cập có offset, con trỏ X không sử dụng được. Để truy xuất bộ nhớ chương trình bằng con trỏ thì Z là giải pháp duy nhất…Dưới đây là 1 số cách sử dụng LD, ST kết hợp với con trỏ, chúng ta xét thông qua các ví dụ.

Ví dụ 1:

CLR R27 ; xóa R27, tức xóa byte cao của pointer XLDI R26, 0x60 ; load giá trị 0x60 vào R26, tức byte thấp của pointer X; sau 2 dòng trên, giá pointer X là 0x0060, sẵn sàng để trỏ đến vị trí đầu tiên trong SRAM. 

LD R1, X+ ; Load giá trị ở ố nhớ 0x0060 vào R1 (vì X trỏ đến 0x0060), sao đó tăng giá trị ;X lên 1, như thế sau lệnh này X=0x0061 LD R2, X+ ; Load giá trị ở ố nhớ 0x0061 vào R2, sao đó tăng giá trị ;X lên 1, như thế sau lệnh này X=0x0062LD R3, X ; Load giá trị ở ô nhớ 0x0062 vào R3 và không thay đổi XLD R4, -X ; Giảm giá trị của X trước (X=0x0061), sau đó load giá trị ở ô nhớ 0x0061 vào R4

Từ ví dụ này chúng ta thấy có 3 cách cơ bản để load dữ liệu từ SRAM bằng con trỏ, cách Load trực tiếp trong trường hợp  LD R3, X, cách load post-increment (hoặc post-decrement) như trong trường hợp LD R1, X+ và cách load pre-decrement (hoặc pre-increment) trong trường hợp LD R4, -X.

Chúng ta có thể viết lại ví dụ trên nhưng sử dụng con trỏ Y hoặc Z thay cho X. Ví dụ viết cho instruction ST cũng hoàn toàn tương tự.

Tuy nhiên cách truy cập theo cách pre hay post đều làm thay đổi giá trị của con trỏ, điều này có 1 bất lợi là nếu chúng ta muốn quay lại vị trí ô nhớ nào đó, chúng ta phải tiếp tục thay đổi con trỏ. Để tránh việc làm này, 1 cách truy cập khác được hỗ trợ là truy cập “Offset”. Xét ví dụ sau:

LD R1, Y+1

Page 341: asembly

Đây chính là cách truy cập Offset dùng con trỏ Y, cách viết trên là tương đương với cách viết

LD R1, Y+

Nhưng điểm khác biệt ở đây là cách viết Offset không làm thay đổi giá trị của con trỏ Y. Sử dụng Offset có ưu điểm như sử dụng mảng (array) trong các ngôn ngữ lập trình cấp cao. Cần chú ý là giá trị offset không vượt quá 63 và phương pháp này chỉ dùng cho 2 thanh ghi Y và Z.

IV. Rẽ nhánh và vòng lặp.

Không giống như các ngôn ngữ cấp cao, khi lập trình bằng ASM bạn không được hỗ trợ các cấu trúc điều khiển như If, For, While…người lập trình ASM phải tự xây dựng cho mình các cấu trúc này từ những instruction cơ bản. Nếu bạn có trong tay tài liệu tra cứu instruction cho AVR bạn sẽ thấy có rất nhiều instruction có dạng BRxx, với BR là viết tắt của từ Branch (rẽ nhánh). Đây là các instruction cơ bản giúp bạn xây dựng các cấu trúc điều khiển tương đương If, For, While…cho riêng mình.

Trước hết ta sẽ khảo sát instruction BRNE bằng cách xem lại ví dụ trong bài "Làm quen AVR", đây là đoạn chương trình con DELAY:

DELAY:LDI R20, 0xFFDELAY0:LDI R21, 0xFFDELAY1:DEC R21BRNE DELAY1DEC R20BRNE DELAY0RET

Bạn hãy chú ý 4 dòng lệnh nằm giữa đoạn chương trình trên (bắt đầu từ dòng 4), dòng đầu tiên thì bạn đã biết - load giá trị 255 vào thanh ghi R21, sau đó tôi đặt 1 label DELAY1- xem như là 1 cột mốc, dòng 3, instruction DEC bạn mới được học hôm nay - giảm giá trị thanh ghi R21 đi 1 đơn vị, và cuối cùng BRNE DELAY1, BRNE là viết tắt của BRanch if Not Equal – rẽ nhánh nếu không bằng, thực ra bản chất của lệnh này là rẽ nhánh nếu cờ Zero không bằng 1. Như thế câu lệnh BRNE DELAY1 của chúng ta được AVR thực hiện như sau: kiểm tra cờ Z, nếu Z=1 tiếp tục thực hiện dòng tiếp theo sau mà không quan tâm đến nhãn DELAY1, nhưng nếu Z=0 thì nhảy đến nhãn DELAY1. Bạn thấy rằng ban đầu R21 =255, sau khi

Page 342: asembly

giảm 1 bởi DEC, thanh ghi R21=254≠0, cờ Z =0, rẽ nhánh xảy ra, bộ đếm chương trình nhảy về nhãn DELAY1. Quá trình này lặp lại khoảng 255 lần trước khi R21 =0 dẫn đến Z=1.

Bao bên ngoài vòng lặp của nhãn DELAY1 là vòng lặp của nhãn DELAY0, cách hiểu hoàn toàn tương tự nhưng trước khi lệnh DEC R20 được thực thi thì phải chờ cho vòng lặp DELAY1 kêt thúc. Bản thân DELAY0 cũng là 1 vòng lặp 255 lần. kết quả cuối cùng là ta thu được 1 vòng lặp khoảng 255x255 lần mà không làm gì cả, đó chính là ý nghĩa và cách hoạt động của đoạn chương trình con DELAY.

Bên cạnh BRNE chúng ta có 1 số instruction phục vụ rẽ nhánh khác như:

- BREQ (BRanch if EQual).

Cú pháp: BREQ LABEL

Chức năng:  Nhảy đến nhãn LABEL nếu cờ Z =1. Cờ Z chịu tác động của rất nhiều

instruction như CP, CPI, SUB, SUBI…vì thế BREQ thường được sử dụng sau các

instruction này.

Ví dụ:

LDI R16, 0xFF

LDI R17, 0xFF

CP R16, R17 ; so sanh 2 thanh ghi R16, R17

BREQ RENHANH

…..

RENHANH:

; thực hiện những việc khi rẽ nhánh.

Kết quả là việc rẽ nhánh xảy ra vì khi so sánh bằng CP,  R17=R16 nên cờ Z tự

động được set bằng 1, lệnh BREQ được thực thi và nhảy đến nhãn RENHANH. Ví

dụ này tương đương cấu trúc if (R16=R17) {thực hiện những việc khi rẽ nhánh}.

- BRLO (BRanch if LOwer).

Page 343: asembly

Cú pháp: BRLO LABEL

Chức năng:  bản chất của câu lệnh là nhảy đến nhãn LABEL nếu cờ C =1. Tuy

nhiên, thông thường lệnh này sử dụng theo sau các instruction như CP, CPI, SUB,

SUBI…khi đó việc rẽ nhánh sẽ xảy ra nếu thanh ghi Rd

Ví dụ:

EOR  R16, R16 ;XOR R16 với chính nó, tương đương CLR R16

VONG LAP:

INC R16 ;tăng R16  thêm 1 đơn vị

CPI R16, $10 ;so sánh R16 với số hexadecimal $10

BRLO VONGLAP ;nhảy về  VONGLAP nếu R16 <$10

NOP  ;câu lệnh này sẽ được thực thi nếu điều kiện rẽ nhánh ở trên không thỏa,

; NOP là 1 instruction, chức năng là không làm gì cả.

Kết quả là phần lệnh bên trong VONGLAP sẽ được thưc hiện khoảng 16 lần

($10=16) trước khi thực hiện lệnh NOP.

- BRSH (BRanch if Same or Higher).

Cú pháp: BRSH LABEL

Chức năng:  bản chất của câu lệnh là nhảy đến nhãn LABEL nếu cờ C =0. Tuy

nhiên, thông thường lệnh này sử dụng theo sau các instruction như CP, CPI, SUB,

SUBI…khi đó việc rẽ nhánh sẽ xảy ra nếu thanh ghi Rd ≥Rr.

Ví dụ:

SUBI  R16, 4 ;trừ R16 đi 4 đơn vị

BRSH RENHANH ; nhảy đến RENHANH nếu R16 ≥ 4

….

RENHANH:

Page 344: asembly

NOP 

Còn rất nhiều instruction rẽ nhánh bạn có thể sử dụng để tạo cấu trúc điều khiển,

chú ý là các instruction này đều hoạt động dựa trên trạng thái của 1 cờ nào đó, do

đó bạn cần lựa chọn 1 lệnh phù hợp để thực thi trước các instruction rẽ nhánh này,

để làm được như vậy bạn cần xem kỹ tài liệu hướng dẫn INSTRUCITON cho

AVR. 

Lập trình với AVR Studio

1 2 3 4 5

 ( 40 Votes )

Page 345: asembly

      Bắt đầu với AvrStudio4: bạn chạy AvrStudio từ “Start/ All Programs/ Atmel AVR Tools/ AvrStudio 4”. Ở lần đầu chạy AvrStudio, 1 dialog “Welcome to AvrStudio 4” xuất hiện, hãy bỏ check ở ô “show dialog at Startup” và nhấn cancel.

Hình 1.  Welcome to AVR studio 4 Diaolg.

      Bạn thấy giao diện AVR Studio 4 như sau:

Page 346: asembly

Hình 2. Giao diện AVR Studio.

      Giao diện AVR Studio rất dễ sử dụng, vì vậy chúng ta sẽ kết hợp tìm hiểu trong lúc viết ví dụ.

      Tạo Project mới: từ menu Project, chọn “Project/New Project”.

Page 347: asembly

Hình 3.  Tạo Project mới.

      Một dialog mới xuất hiện cho phép bạn setting Project của bạn, trong vùng “Project Type” chọn “Atmel AVR assembler”, tức lập trình bằng ngôn ngữ Assembly và trình dịch là Atmel AVR assembler (trình dịch tích hợp trong AVR Studio); “Location”, chọn nơi chứa Project (trong ví dụ này tôi chọn thư mục D/AVR1); “Project name”, tên Projetc của bạn, hãy đặt là avr1.

Hình 4.Setting Project.

      Nhấn Next để tiếp tục chọn Platform và device, việc này phục vụ cho mục đích debug chương trình hay mô phỏng bằng avr simulator. Bạn hãy chọn “AVR Simulator” trong ô Platform và Atmega8 trong ô device (chúng ta sẽ viết chương trình cho chip Atmega8).

Page 348: asembly

Hình 5. Chọn Platform và device.

      Nhấn finish để kết thúc setting project, bạn thấy các cửa số của “Project” chứa các thông tin Project của bạn, bạn thấy trong mục “Source files” có 1 file “avr1.asm” là source code của bạn. Bạn có thể nhấn vào switch tab bên dưới cửa sổ Project để xem cửa số “I/O View”, cửa số này chứa thông tin chip dùng khi mô phỏng. Cửa số Build chứa thông tin kết quả biên dịch. “Editor” là vùng  viết chương trình, trong trường hợp này đó là file “avr1.asm” của bạn.

Page 349: asembly

Hình 6. Cửa sổ lập trình.

      Việc còn lại là viết code vào cửa sổ Editor sau đó dịch chương trình bằng phím F7.

II. Lập trình C bằng AVRStudio.

      Về bản chất AVRStudio không hỗ trợ lập trình ngôn ngữ C vì  không có trình dịch C. Tuy nhiên nó cho phép tích hợp trình dịch C của bộ công cụ WinAVR. Vì thế, nếu muốn sử dụng AVRStudio để lập trình C cho AVR bạn phải cài đặt trình dịch và thư viện avr-gcc từ GNU hoặc đơn giản là cài đặt WinAVR cùng AVRStudio. Bạn tham khảo thêm bàihướng dẫn WinAVR để biết cách download cài đặt WinAVR. Các hướng dẫn bên dưới giả sử rằng bạn đã cài đặt thành công AVRStudio và WinAVR.

Page 350: asembly

      Việc tạo 1 Project lập trình bằng ngôn ngữ C trong AVR Studio không khác mấy so với việc tạo Project ASM. Điều duy nhất cần chú ý là bước chọn trình biên dịch. Xem lại hình 4 khi tạo Project ASM, chúng ta chọn Atmel AVR Assempler làm trình dịch chính, để tạo Project C chúng ta chọn AVR GCC làm trình biên dịch như trong hình 7. Cần lưu ý là trình dịch AVR GCC chỉ xuất hiện trong danh sách lựa chọn của AVR Studio khi bạn đã cài WinAVR vào máy trước đó.

Hình 7. Chọn AVR GCC làm trình biên dịch chính.

      Xem hình 7, giả sử bạn đặt tên Project là avr1 trong ô Project name, bạn sẽ thấy AVR Studio đề nghị tự tạo ra 1 file chương trình chính tên là avr1 có phần mở rộng là ".c", khác với phần mở rộng ".asm" khi tạo Project Assembly.       Các việc còn lại hoàn toàn tương tự trong trường hợp tạo Project ASM nên bạn có thể xem lại phần trên. Sau khi tạo Project lập trình C trong AVR Studio, bạn save Project rồi vào thư mục chứa Project mới tạo, bạn sẽ thấy 1 file Makefile được tự động tạo ra. Makefle được AVR Studio tạo tự động trong lúc tạo Project, bạn không cần dùng đến trình MFile. Ngôn ngữ C cho AVR Studio hoàn toàn là AVR GCC như trong WinAVR, vì thế bạn có thể copy, load 1 file source từ WinAVR vào mà không cần bất kỳ chỉnh sửa nào.      Một trong những ưu điểm khác khi bạn lập trình C trong AVR Studio là bạn có thể tận dùng trình AVR Simulator để debug code C trực tiếp. Đồng thời, trình biên

Page 351: asembly

tập (Editor) của AVR Studio cũng giúp bạn viết code thuận tiện hơn Programmer notepad. 

Page 352: asembly