Phía sau lệnh xuất cổng của vi điều khiển

Print

Trong bài viết này mình sẽ cố gắng đưa ra một góc nhìn khác về chương trình khởi đầu của mỗi người học lập trình vi điều khiển – chương trình nhấp nháy led. Hi vọng, bài viết có thể giúp những bạn mới có được cái nhìn sinh động hơn với lập trình C trên vi điều khiển, hiểu những gì diễn ra sau mỗi cậu lệnh và nắm được mối liên hệ giữa 2 môn học: Kỹ thuật lập trình C và Vi xử lý, đang được giảng dạy trong nhà trường.

I. Chương trình nhấp nháy LED – “Hello world!” của thế giới vi điều khiển

Đối với những người mới học vi điều khiển, làm sao để kiểm tra được chương trình đơn giản đầu tiên mà mình viết đã được nạp và chạy đúng trên vi điều khiển hay chưa? Rất đơn giản thôi, chúng ta hãy xuất tín hiệu logic ra một chân của vi điều khiển (đã được nối sẵn với 1 con led), và chờ đợi. Khi ánh sáng dầu tiên phát ra, đó chính là thời điểm vi điều khiển của chúng ta – từ 1 con chip trắng – cất tiếng khóc trào đời với thế giới thực. Và đồng thời, cánh của của thế giới vi điều khiển cũng mở ra, và con đường tìm hiểu vi điều khiển của chúng ta bắt đầu.

Dòng lệnh xuất tín hiệu ra cổng có thể rất đơn giản, kiểu như:

            PORTD = 0xFF;

Với việc đưa toàn bộ 8 chân của cổng D lên mức cao (mức điện áp nguồn 5V, 3.3V, …). Hoặc là lệnh:

            PORTD |= (1 << 3);

Là lệnh đưa chân số 3 của cổng D lên mức cao và giữ nguyên trạng thái của các chân còn lại bằng phương pháp mặt nạ bit (1).

Vậy cơ chế nào để một lệnh trông rất hiển nhiên ấy tạo ra hiệu ứng thật trên mạch vật lý? Quá trình biến đổi ấy diễn ra như thế nào? Khi học môn vi điều khiển, các thầy thường nói rằng: quá trình hoạt động của vi điều khiển, chung quy lại có thể tóm gọn trong thao tác truy xuất bộ nhớ. Vậy điều đó liên quan như thế nào với 1 chương trình viết bằng ngôn ngữ C? Bây giờ chúng ta sẽ cùng nhau tìm câu trả lời.

Để cụ thể, mình sẽ minh họa nội dung của bài viết thông qua chương trình điều khiển cổng ra của vi điều khiển Atmega16 sử dụng trình biên dịch AVR GCC. Chương trình này đơn giản chỉ là liên tục bật tắt  led được nối vào chân 6 của cổng D (PD6). Để xuất tín hiệu ra PD6, trước hết ta phải cấu hình PD6 là chân output, sau đó ghi giá trị mong muốn vào bit 6 thanh ghi PORTD bằng phương pháp mặt nạ bit.

#include <avr/io.h>    // Defines pins, ports, ..

#include <util/delay.h> // Functions to delay time

 

#define LED      PD6     // Led connect to port D pin 6

#define LED_DDR  DDRD     // Define direction register for Led pin

#define LED_PORT PORTD   // Define output register for LED pin

 

int main(void)

{

    LED_DDR |= (1 << LED);  // Pin diretion control: enable output for Led pin

       // —————– Event loop ——————-

       while (1)

       {

           LED_PORT |= (1 << LED); // Turn on Led pin – Pin 6 PortD

           _delay_ms(500);

           LED_PORT &= ~(1 << LED); // Turn off Led pin

           _delay_ms(500);

       }

       return 0;

}


Để thuận tiện cho các bạn không làm việc với vi điều khiển AVR, mình khái quát lại một chút về điều khiển chân ra của Atmega16:

 

Hình 1: Sơ dồ khối I/O của Atmega16 

Mức logic của mỗi chân ra được cấu hình tại thanh ghi PORT (Port Data Register) tương ứng với chân ấy. Tính hiệu từ thanh ghi PORT chỉ được dẩy ra chân vật lý khi được sự cho phép của thanh ghi DDR định hướng tín hiệu (DDR – Data Direction Register). Thanh ghi DDRcho phép thanh ghi PORT gửi tín hiệu ra thông qua một bộ đầu ra 3 trạng thái (như trên hình 1) (2).

Hình 2: Sơ đồ đơn giản khối I/O của Atmega16

Như vậy, để điều khiển 1 chân bất kỳ, trước tiên ta phải cấu hình chân đó là chân ra thông qua thanh ghi DDRn (Data Direction Register):

    LED_DDR |= (1 << LED);  //Pin diretion control: enable output for Led pin

(Macro LED_DDR được gán bằng DDRD, LED được gán bằng PD6 tức bằng 6 tương ứng vị trí của pin trong port, sẽ trình bày sau).

Tiếp theo, để xuất một mức logic bất kỳ ra chân đó, ta ghi vào thanh ghi PORTn (Port Data Register) tương ứng.

    LED_PORT |= (1 << LED); // Turn on Led pin – Pin 6 PortD

Macro LED_PORT được gán bằng PORTD, macro LED được gán bằng  PD6  (tức bằng 6).     Do phạm vi của bài viết, ta chỉ quan tâm tơi lệnh: LED_PORT |= (1 << LED);

LED_PORT đã được định nghĩa bởi macro:

    #define LED_PORT PORTD  

Do đó, thực chất câu lệnh sẽ là: PORTD |= (1 << LED);

Ta hãy cùng tìm hiểu câu lệnh này.

II. Truy tìm thủ phạm

PORTD thực chất cũng là 1 macro được cung cấp bởi trình biên dịch, vậy chúng ta hãy tìm hiểu macro này là gì thông qua công cụ ‘Open Declaration’ của IDE Eclipse. ‘Open Declartion’ là tính năng rất hữu ích của Eclipse, cho phép bạn truy xuất đến vị trí khai báo 1 macro, 1 hàm bất kỳ nào trong project mà mình không hề biết trước. Điều này rất thuận tiện, đặc biệt với project do nhiều người cùng viết. Trong trường hợp, ta sử dụng một hàm (macro) nào đó mà muốn biết chúng được viết ở đâu, trong thư viện nào, cấu trúc ra sao.

2.1 Lần theo dấu vết

Để xem macro PortD, bạn tích chuột phải vào từ này và chọn mục ‘Open Declaration’.

Hình 3: Sử dụng công cụ ‘Open Declaration’ tìm định nghĩa macro PORTD

Sau đó, 1 tab khác được mở ra cùng với file iom16.h,  kết quả ta được là dòng định nghĩa:

    #define PORTD _SFR_IO8(0x12)

Macro này nằm trong thư viện iom16.h. Liếc xuống phía dưới ta cũng thấy định nghĩa các pin của port D tương ứng.

Hình 4: Định nghĩa macro PORTD

Dựa trên tên của macro, ta đoán rằng _SFR_IO8() có thể dùng để để truy cập đến thanh ghi  IO trong bộ nhớ. Và giá trị 0x12 có thể là địa chỉ của thanh ghi. Tiếp tục thực hiện thao tác trên với macro _SFR_IO8():

Hình 5: Sử dụng công cụ ‘Open Declaration’ tìm định nghĩa macro _SFR_IO8

 

Hình 6: Định nghĩa macro _SFR_IO8

Lúc này ta thấy file sfr_defs.h được mở ra, và macro _SFR_IO8 được định nghĩa:

    #define _SFR_IO8(io_addr) _MMIO_BYTE((io_addr) + __SFR_OFFSET)

Macro __SFR_OFFSET được định nghĩa ngay ở trên có giá trị bằng 0x20. Như vậy, giá trịio_addr (lúc trước được gán bằng 0x12) của _SFR_IO8(), đến đây được _MMIO_BYTE() xử lý tiếp bằng việc cộng thêm 1 giá trị __SFR_OFFSET = 0x20. Như vậy ta đoán rằng io_addr của_SSR_IO8() không phải là 1 giá trị địa chỉ tuyệt đối. Có thể tất cả các thanh ghi IO của Atmega16 được lưu trong 1 không gian nhớ nào đó ở trong bộ nhớ, và giá trị đầu tiên của không gian nhớ này là giá trị offset  __SFR_OFFSET = 0x20 . Chúng ta sẽ kiểm tra lại ý niệm này sau, khi đọc vào datasheet của Atmega16. Bây giờ ta lại tiếp tục lần theo macro_MIMO_BYTE() :

Hình 7: Định nghĩa macro _MMIO_BYTE

Đến  đây, ta thu được kết quả cuối cùng:

    #define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))

Như thế là mọi thứ đã rõ ràng, hóa ra cái lệnh PORTD = x; mà chúng ta thường hay dùng và vẫn nghĩ rằng cái cổng D đương nhiên sẽ xuất ra giá trị x mà không mảy may nghi ngờ gì (vì hiển nhiên tên nó là PORTD mà ^_^!). Thì ra là một lệnh truy xuất đến bộ nhớ thông qua phép truy xuất nội dung con trỏ. Một kỹ thuật kinh điển của ngôn ngữ C.

Con trỏ này (mem_addr) được ép kiểu là uint8_t (đương nhiên rồi thanh ghi io là 8 bit mà), gắn nhãn volatile (để không bị thay đổi tùy tiện bởi trình tối ưu – optimization) ([11], [12], [13], [14]). Trước khi truy xuất tới nội dung của nó => *mem_addr.

Bạn có thể đi đến luôn kết quả cuối cùng thông qua tính năng ‘Explore Macro Expansion’ của Eclipse, bằng cách tích vào từ LED_PORT, chuột phải và chọn ‘Explore Macro Expansion’.

Hình 8: Tính năng Explore Macro Expension

 

Hình 9: Biểu thức gán cho macro LED_PORT

Như vậy, thực chất lệnh điều khiển cổng của vi điều khiển là lệnh truy xuất tới vùng nhớ đã được quy định từ trước.  Như đối với ví dụ xuất ra cổng Port D này của chúng ta, thực chất là lệnh ghi giá trị vào thanh ghi có địa chỉ: 0x12 + 0x20 = 0x32 trong bộ nhớ. Khả năng đây chính là địa chỉ của thanh ghi PORTD (Port Data Register) trong hình 1, nhưng để thực sự chắc chắn, ta sẽ vào datasheet để kiểm tra, đồng thời tìm hiểu ý nghĩa của các thành phần io_addr:0x12 và __SFR_OFFSET0x20.

 

Một vài kết luận

1. Như vậy chúng ta đã trả lời được những câu hỏi đặt ra ở đầu bài viết. Không có điều gì huyền bí ở lệnh xuất tín hiệu ra cổng của vi xử lý, nó thật sự là lệnh ghi giá trị lên vùng nhớ của thanh ghi IO, thông qua phép truy xuất nội dung con trỏ của ngôn ngữ C.

2. Hung thủ (người viết ra trình biên dịch) thường che dấu hành vi truy xuất bộ nhớ thông qua các macro để đánh lừa người lập trình rằng họ đang ra lệnh cho vi xử lý thực hiện tính năng nào đó (ví dụ như PORTD = x như trong ví dụ trên). Nhưng các thám tử đập trai, tài ba (tức chúng ta đây) đã lần ra hành vi của y, thông qua việc lần theo manh mối là các macro ^_^!.

3.  Điều gì ở phía sau từ khóa PORTD, đó là nội dung của vùng nhớ có địa chỉ 0x32. Giá trị này đã được ai đó (người viết ra trình biên dịch) ngầm quy định thông qua các thư viện mà chúng ta vẫn thường include vào mà đôi khi không để ý đến. Thư viện này sẽ lưu địa chỉ của các thanh ghi đặc biệt trong vi xử lý bằng các từ khóa rất dễ hiểu với người lập trình (thông thường bằng tên của các thanh ghi ấy). Tương tự như việc xuất giá trị ra cổng D, việc đọc giá trị từ cổng D có thể là phép lấy nội dung của vùng nhớ thanh ghi PIND (Port D Input Pins), việc truyền dữ liệu lên máy tính bằng giao thức UART có thể là lệnh ghi dữ liệu vào vùng nhớ bộ đệm của khối UART trong vi điều khiển … Như thế: đúng là mọi hoạt động của vi xử lý đều có thể quy về thao tác truy xuất bộ nhớ như ta đã đề cập ở phần đầu. Nếu chương trình chạy có gì không ổn, hãy kiểm tra bộ nhớ.

4.  Quá trình truy xuất bộ nhớ là thao tác nền tảng của vi điều khiển, các thanh ghi chức năng đặc biệt nằm trong bộ nhớ RAM và có thể truy xuất như 1 biến thông thường (thường thì sẽ có thêm các cơ chế đặc biệt khác). Việc ghi giá trị n ra cổng D cũng có thể thực hiện giống như ghi giá trị n vào 1 ô nhớ 8 bit nào đó trong RAM. Ngoại trừ việc vùng nhớ 0x32 (tức thanh ghi PORTD) được nối tới chân ra bằng mạch phần cứng trong chip, nên nó tạo ra thêm chức năng điều khiển bên ngoài qua cổng D mà một vùng nhớ thường trong RAM không có. Điều này cũng tương tự với các khối ngoại vi khác như UART, SPI, I2C, PWM …

5. Quá trình truy xuất trên bộ nhớ được thực hiện bằng thao tác trên con trỏ của ngôn ngữ C. Thông qua con trỏ, người lập trình đọc ghi trực tiếp lên vùng nhớ xác định nào đó và tạo ra hiệu ứng. Điều đó cho thấy sức mạnh của ngôn ngữ C khi thao tác lên phần cứng, lý giải vì sao C là ngôn ngữ rất khó thay thế khi làm việc với các ứng dụng nhúng. Những ứng dụng cần đáp ứng kịp thời với tài nguyên hạn chế (có thể còn những lý do nào khác, nhưng cái đó mình không biết). Các bạn sinh viên khi học lập trình C cần chú ý với việc thao tác trên con trỏ.

6. Macro truy xuất bộ nhớ được khai báo trong file ‘.h’ như vậy nó là toàn cục với tất cả các hàm (chỉ cần include thư viện) trong đó có hàm ngắt. Để tránh việc trình tối ưu can thiệp vào nó (ví dụ như bỏ qua khi biên dịch), con trỏ địa chỉ được gắn nhãn volatile ([11], [12], [13], [14]). Ta có áp dụng kỹ thuật này cho các biến toàn cục  khác.

2.2 Phân tích hồ sơ vụ án

Như đã phân tích ở trên, ta dự đoán rằng vùng nhớ của thanh ghi PORTD có địa chỉ 0x32 trong bộ nhớ. Nhưng đó chỉ là phán đoán, để đảm bảo chính xác, chúng ta hãy vào datasheet của nhà sản xuất tìm hiểu bộ nhớ của Atmega16. Về nội dung này, trang hocavr.com cũng đã viết khá đầy đủ và chi tiết, các bạn có thể vào đó để đọc thêm. Mình chỉ khái quát lại 1 vài chi tiết quan trọng liên quan tới bài viết mà thôi.

Trong datasheet của Atmega16, mục SRAM Data Memory trang 17, có ghi một vài thông tin như sau:

Bộ nhớ RAM của Atmega16 có dung lượng 1Kbyte với 1120 đường địa chỉ, được chia làm 3 vùng:

1.Vùng thanh ghi chung Register File (vùng CPU có thể truy xuất trực không cần gọi địa chỉ)

2. Vùng các thanh ghi ngoại vi I/O Memory: chứa các thanh ghi phục vụ các khối ngoại vi như GPIO, UART, SPI, …

3. Vùng dữ liệu nội Internal Data SRAM: Vùng lưu các biến trong quá trình tính toán (biến toàn cục, biến tạm thời).

Hình 10: Không gian bộ nhớ  SRAM của Atmega16

Trong đó vùng nhớ Register File gồm 32 thanh ghi, nằm ở phần đầu của bộ nhớ RAM (có địa chỉ từ 0x0000 tới 0x001F). Tiếp sau đó là vùng nhớ I/O Memory với 64 thanh ghi có địa chỉ từ 0x0020 tới 0x005F. Cuối cùng là vùng nhớ của internal data SRAM với 1024 đường địa chỉ từ 0x0060 tới 0x045F.

Như vậy là đã rõ, quả nhiên thanh ghi PortD nằm trong 1 không gian nhớ riêng là vùng I/O Memory có địa chỉ offset là 0x20. Điều này khớp với macro mà ta phân tích ở trên. Để tìm vị trí của thanh ghi PORTD, ta tra cứu bảng “Register Summary” trang 331:

 

Hình 11: Trích bẳng Register Summary trong datasheet của Atmega16 trang 331 

Như vậy ta thấy thanh ghi PORTD quả nhiên có địa chỉ 0x32 (địa chỉ tương đối trong vùng nhớ I/O Memory là 0x12 cũng khớp với macro phân tích ở trên).

Liếc nhìn xuống phía dưới, ta thấy thanh ghi SPDR địa chỉ 0x2F của bộ SPI (SPI Data Register), thanh ghi UDR địa chỉ 0x2C của bộ UART (UART I/O Data Register) cũng nằm trong vùng nhớ này.

2.3 Đối chất

Như thế là tới đây, thì có thể tạm kết luận rằng lệnh xuất tín hiệu ra cổng D thực chất là thao tác ghi vào bộ nhớ, tại vị trí của thanh PORTD ở địa chỉ 0x32. Để đảm bảo tính chính xác, ta hãy kiểm chứng một lần nữa bằng kết quả chạy trực tiếp trên vi điều khiển Atmega16 thông qua bộ debug JTAG ICE trên IDE AVR Studio v4 (thật ra thì có thể debug luôn trên Eclipse nhưng tại hạ vẫn không tài nào cấu hình cho nó chạy debug được, hiện thời chỉ nạp được thôi nên mới dùng tới hạ sách này. Không biết có huynh đệ nào cấu hình được không, nếu được thì xin chỉ giáo giùm, tại hạ thật là cảm kích muôn phần ^_^!).

 

Hình 12: Debug Atmega16 bằng mạch Debug Jtag

Để xem được code assembly trên AVR studio bạn chỉ cần vào chương trình Debug, tích vào câu lệnh nào đó, chuột phải và chọn ‘Goto Disassembly’

 

Hình 13: Hiển thị mã assembly của chương trình

Bây giờ ta hãy quan sát kết quả chạy câu lệnh bật Pin 6 PortD ở ví dụ trên:

    LED_PORT |= (1 << LED); // Turn on Led pin – Pin 6 PortD

Ta đoán rằng sau khi chạy câu lệnh này bit 6 của thanh ghi PORTD sẽ được set lên 1. Quá trình ghi bit 6 theo phương pháp mặt nạ bit ở trên, thường sẽ gồm 3 thao tác Read – Modify- Write:

1. Read: Đọc giá trị từ thanh ghi PORTD ra thanh ghi trung gian

2. Modify: Thực hiện phép OR logic thanh ghi trung gian với số 0b0100 0000 (tức 1 << LED), để set bit 6 lên 1 và giữ nguyên trạng thái 7 bit còn lại.

3. Write: Ghi kết quả phép OR logic trên lại thanh ghi PORTD

Ta hãy quan sát kết quả debug để kiểm tra ý niệm trên:

 

Hình 14: Kết quả debug chương trình nháy chân PD6 

Chú ý vị trí mũi tên mầu và là vị trí hiện tại của chương trình. Sau khi chạy xong lệnh trên, ta thấy thanh ghi PORTD ở địa chỉ tương đối 0x12 (trong vùng I/O Memory) đã set bit thứ 6 lên 1 tương ứng với giá trị 0x40. Tuy nhiên, phần câu lệnh assembly có vẻ không giống như những gì ta suy đoán, khi câu lệnh LED_PORT |= (1 << LED); trong C chỉ tương ứng với 1 lệnh assembly duy nhất: SBI 0x12,6        

Ta xem lại trong datasheet mục “I/O Ports” có viết: Tất cả cổng của AVR đều có chức năng Read-Modify-Write khi được sử dụng như là một cổng I/O thông thường. Điều đó có nghĩa rằng hướng của 1 chân xác định có thể thay đổi mà không tạo ra sự thay đổi vô ý lên hướng của các chân khác với các câu lệnh SBI và CBI. Trong mục “I/O Memory” trang 23 cũng viết “I/O Registers nằm trong dải địa chỉ  0x00 – 0x1F (địa chỉ tương đối trong vùng I/O space) dều có thể truy cập trực tiếp đến từng bit bằng cách sử dụng lệnh SBI và CBI. Với các thanh ghi này giá trị của từng bit cũng có thể được kiểm tra thông qua lệnh SBIS và SBIC

    Lệnh: SBI A,b

Là lệnh set bit ở vị trí b của thanh ghi A lên 1. Lệnh chỉ áp dụng cho các thanh ghi I/O có địa chỉ A nằm trong dải 0x00 – 0x1F (32 thanh ghi đầu của vùng I/O Space). Như ở hình trên ta thấy lệnh assembly tương ứng là lệnh SBI 0x12,6 tức lệnh set bit 6 của thanh ghi PORTD (0x12) lên 1. 

Như thế chúng ta thấy rằng toàn bộ 3 thao tác Read-Modify-Write mà ta thấy trong câu lệnh ngôn ngữ C ở trên, khi biên dịch ra mã máy lại chỉ còn lại 1 lệnh duy nhất và chạy trong 2 chu kỳ máy. Để làm được điều này, phần cứng của AVR phải hỗ trợ chức năng cấu hình từng bit.

Thêm 1 vài kết luận

1.   Qua khảo sát chương trình trên, ta thấy rằng không phải lúc nào vi điều khiển cũng hoạt động đúng như những gì ta yêu cầu bằng ngôn ngữ C. Vì ta sẽ không biết được quá trình biên dịch sẽ diễn ra như thế nào. Vậy nên, nếu có lỗi gì mà tìm mãi không ra nguyên nhân, hãy thử debug vào chương trình assembly xem sao. Cuốn “An Embedded Software Primer” [13], chương 4 Interrupt cũng trình bày khá tỷ mỷ và nhiều ví dụ liên quan đến khía cạnh này, các bạn có thể đọc thêm.

2.   Thông thường, vi điều khiển sẽ không đọc/ghi dữ liệu theo từng bit mà theo từng byte, word … điều đó lý giải vì sao ngôn ngữ C không có câu lệnh thao tác trên bit. Để thao tác lên từng bit (mà không ảnh hưởng lên các bit khác trong 1 byte) thông thường ta phải dùng đến phương pháp mặt nạ bit (1). Phương pháp này tương ứng với 3 quá trình Read-Modify-Write trên byte (word) dữ liệu muốn thao tác. Read-Modify-Write là thao tác khá nhạy cảm cần hết sức chú ý. Vấn đề có thể nảy sinh khi chương trình có các hàm ngắt và gây ra lỗi Share Data Problem (điều gì sẽ xảy ra khi ta vừa đọc dữ liệu, đang sửa đổi thì 1 ngắt diễn ra xen vào giữa quá trình đó và thay đổi dữ liệu ban đầu). Vì vậy để đảm bảo độ tin cậy người ta thường bổ xung thêm các thao tác (ví dụ như cấm ngắt toàn cục trước khi đọc (Read), và cho phép ngắt lại sau khi ghi (Write)) để chắc chắn thao tác bit của chúng ta là atomic (vấn đề Share Data Problem, Atomic function nằm ngoài phạm vi của bài viết, hi vọng sẽ có thời gian để trình bày sau). Việc bổ xung thêm này có thể làm code trở nên khá cồng kềnh, giảm khả năng đáp ứng của hệ thống, nhưng cũng có thể là đáng giá trong việc giúp cho hệ thống hoạt động ổn định, tin cậy.

3.   AVR cung cấp khả năng thao tác lên từng bit của các thanh ghi I/O bằng phần cứng mà datasheet viết là ‘true Read-Modify-Write funtionality’. Đây là tính năng rất hữu dụng, tăng khả năng đáp ứng của hệ thống lên rất nhiều. Tất nhiên, cùng với những tính năng đặc biệt này, AVR phải có tập lệnh riêng để hỗ trợ cho chúng (SBI, CBI, SBIS, SBIC).

Thêm một phép thử khác

Trong chương trình trên, ta bật tắt chân 6 của PortD bằng phép mặt nạ bit trong ngôn ngữ C, để xem quá trình Read-Modify-Write được biên dịch ra mã máy như thế nào. Rốt cục lại chỉ thấy mỗi 1 lệnh set bit SBI. Nhưng chúng ta sẽ không từ bỏ dã tâm của mình. Trong chương trình sau, chúng ta sẽ bật tắt 3 chân của PortD là chân 4, 5, 6. Xem trình biên dịch có cho ra 3 lệnh Read-Modify-Write không, hay là 3 lệnh set bit SBI.

#include <avr/io.h>

#include <util/delay.h>

 

#define LED      PD6   // Led connect to port D pin 6

#define LED1     PD4   // Led 1 connect to port D pin 4

#define LED2     PD5   // Led 2 connect to port D pin 5

#define LED_DDR  DDRD  // Define direction register for Led pin

#define LED_PORT PORTD // Define output register for LED pin

 

int main(void)

{

    // Pin diretion control: enable output for Led pins (pins 6,4,5 portD)

    LED_DDR |= ((1 << LED) | (1 << LED1) | (1 << LED2));

 

       // —————– event loop ——————-

       while (1)

       {

              // Turn on Led pins – Pins 6,4,5 PortD

             LED_PORT |= ((1 << LED) | (1 << LED1) | (1 << LED2));

              _delay_ms(500);

 

              // Turn off Led pins – Pins 6,4,5 PortD

              LED_PORT &= ~((1 << LED) | (1 << LED1) | (1 << LED2));

              _delay_ms(500);

       }

 

       return 0;

}

 Kết quả debug như ở hình dưới:

 

Hình 15: Kết quả debug chương trình nháy 3 chân PD4, PD5, PD6 

Như thế ta thấy kết quả 3 chân 4, 5, 6 của PortD (0x32 (0x12 trong vùng IO space)) đều đã được set lên 1, tương ứng với giá trị 0x70.

Câu lệnh mặt nạ bit trong C:

    LED_PORT |= ((1 << LED) | (1 << LED1) | (1 << LED2));

Khi biên dịch, được 3 câu lệnh assembly tương ứng:

    IN        R24,0x12

    ORI       R24,0x70

    OUT       0x12,R24

 

Các lệnh này cho thấy quá trình Read-Modify-Write trong vi điều khiển. Trước hết mình giải thích qua về các câu lệnh:

Lệnh INOUT là lệnh đọc/ghi dữ liệu giữa các thanh ghi dùng chung (vùng Register File – Rr) và các thanh ghi ngoại vi (vùng I/O Space). Đây là cách thông thường để đọc/ghi các thanh ghi ngoại vi. Cụ thể:

    Lệnh: IN Rd, A

Tải dữ liệu từ thanh ghi có địa chỉ A vùng I/O Space (Ports, Timers, Configuration Registers, …) vào thanh ghi Rd trong Register File.

    Lệnh: OUT A, Rr

Truyền dữ liệu từ thanh ghi Rr trong Register File vào thanh ghi địa chỉ A trong I/O Space (Ports, Timers, Configuration Registers, …).

Lệnh còn lại là lệnh ORI (Logical OR with Immediate) là lệnh thực hiện phép OR logic thanh ghi dùng chung với 1 hằng số:

    Lệnh: ORI Rd, K

Thực hiện phép OR logic thanh ghi Rd với hằng số K, kết quả lưu vào thanh ghi Rd.

Như vậy, quá trình thực thi lệnh set 3 bit 4, 5, 6 trong thanh ghi PORTD ở mã máy như sau:

1. Đầu tiên, nội dung của thanh ghi PORTD (địa chỉ 0x12 trong vùng I/O Space) được đọc vào thanh ghi R24 thông qua câu lệnh IN R24, 0x12.

2. Các bit tại vị trí 4, 5, 6 trong thanh ghi R24 được set lên 1 (các bit còn lại dữ nguyên) thông qua phép OR logic với số 0b0111 0000 (tương ứng 0x70 hệ hexa) thông qua câu lệnh ORI R24, 0x70.

3.  Kết quả sau khi thực hiện phép logic, được ghi lại thanh ghi PORTD thông qua câu lệnh OUT 0x12, R24. Lúc này các bit 4, 5, 6 đã được bật, tương ứng các chân PD4, PD5, PD6 chuyển lên mức cao.

Như thế, qua phân tích các câu lệnh assembly trên, ta thấy quá trình Read-Modify-Write trong câu lệnh diễn ra cụ thể như thế nào. Lần này, quá trình giống như cách ta viết trên ngôn ngữ C.

Câu hỏi đặt ra là tại sao khi biên dịch lại không cho ra 3 câu lệnh set bit như lần trước, kiểu như:

    SBI 0x12, 6

    SBI 0x12, 4

    SBI 0x12, 5

Mình đoán rằng có thể do khi set nhiều bit, trình biên dịch sẽ chọn phương pháp Read-Modify-Write để set 3 bit ra được đồng thời.

 III. Tạm kết

Như vậy, qua bài viết mình đã cố gắng minh họa mối liên hệ giữa chương trình viết cho vi điều khiển bằng ngôn ngữ C và chương trình thực chạy trên chip. Kết quả quá trình biên dịch như thế nào? Điều gì đứng sau những từ khóa tưởng chừng như rất hiển nhiên được cung cấp bởi thư viện? Đồng thời, bài viết cũng cung cấp 1 số kỹ thuật để phân tích, debug chương trình viết trên vi điều khiển, hi vọng sẽ hữu ích với các bạn mới trên con đường học tập và làm việc trong lĩnh vực nhúng.

Thanh Phong

Bạn Có Đam Mê Với Vi Mạch hay Nhúng      -     Bạn Muốn Trau Dồi Thêm Kĩ Năng

Mong Muốn Có Thêm Cơ Hội Trong Công Việc

    Và Trở Thành Một Người Có Giá Trị Hơn

Bạn Chưa Biết Phương Thức Nào Nhanh Chóng Để Đạt Được Chúng

Hãy Để Chúng Tôi Hỗ Trợ Cho Bạn. SEMICON  

 

Hotline: 0972.800.931 - 0938.838.404 (Mr Long)

 

 

Last Updated ( Sunday, 28 July 2019 18:51 )