Trong bài này chúng ta sẽ tới lập trình 8051 GPIO, có thể nói đây là một ngoại vi cơ bản và quan trọng nhất của vi điều khiển. GPIO cũng tương tự như tay chân của vi điều khiển vậy, để vi điều khiển có thể tác động tới các phần tử bên ngoài như led, nút nhấn…
Bài 3 trong Serie Học lập trình 8051 từ A tới Z
Tổng quan về khối GPIO trong 8051
GPIO là gì?
GPIO (General – purpose input/output) là ngõ vào ra cơ bản mà bất kỳ vi điều khiển nào cũng có, nó đơn giản chỉ là xuất ra điện áp hoặc đọc điện áp 5V (ứng với mức logic 1) hoặc điện áp 0V (ứng với mức logic 0).
- Các tín hiệu này dùng cho mục đích điều khiển các bóng đèn led, điều khiển relay, động cơ…
- Đơn giản nhất vẫn là: Giao tiếp Led đơn = Output, Đọc nút nhấn = Input
- Với Led đơn, ta sẽ sử dụng các chân ở mode output Push-Pull, thay vì Open drain. Như vậy, ta sẽ dùng các pin ở port 1, 2, 3.
- Lưu ý: muốn dùng pin ở port 0 thì cần sử dụng điện trở kéo kết nối bên ngoài, vì Port 0 không có điện trở nội kéo lên.
Sơ đồ chân 8051
Vi điều khiển 8051 có tất cả 40 chân – tương ứng với 4 cổng, mỗi cổng 10 chân. Các phép toán đọc/ghi dữ liệu được thực hiện thông qua 32 chân trên 4 cổng (Port 0, 1, 2, 3). Trong khi 8 chân còn lại là các chân: Vcc, GND, XTAL1, XTAL2, RST, EA, ALE, PSEN.
Cổng vào/ra
Tất cả các vi điều khiển 8051 đều có 4 cổng vào/ra 8 bit có thể thiết lập như cổng vào hoặc ra. Như vậy có tất cả 32 chân I/O cho phép vi điều khiển có thể kết nối với các thiết bị ngoại vi.
Cách set chế độ Input/Output cho 8051 GPIO
Sau khi thiết bị reset, tất cả các cổng được mặc định là input.
- Nếu muốn 1 port/1 chân bất kỳ thành input, ta gán nó = 1. Ví dụ: P1_1 = 1;
- Nếu muốn nó là output, ta gán nó = 0.
Xuất mức 0
Chân vào/ra (I/O)
Hình trên mô tả sơ đồ đơn giản của mạch bên trong các chân vi điều khiển trừ cổng P0 là không có điện trở kéo lên (pull-up).
Chân ra: Một mức logic 0 đặt vào bit của thanh ghi P làm cho transistor mở, nối chân tương ứng với đất.
Xuất mức 1
- Chân vào: Một bit 1 đặt vào một bit của thanh ghi cổng, transistor đóng và chân tương ứng được nối với nguồn Vcc qua trở kéo lên.
Port 0
Port 0 là port có 2 chức năng ở các chân 32 – 39
– Chức năng I/O (xuất/nhập): dùng cho các thiết kế nhỏ. Tuy nhiên, khi dùng chức năng này thì Port 0 phải dùng thêm các điện trở kéo lên (pull-up), giá trị của điện trở phụ thuộc vào thành phần kết nối với Port.
– Khi dùng làm ngõ vào, Port 0 phải được set mức logic 1 trước đó.
– Chức năng địa chỉ / dữ liệu đa hợp: khi dùng các thiết kế lớn, đòi hỏi phải sử dụng bộ nhớ ngoài thì Port 0 vừa là bus dữ liệu (8 bit) vừa là bus địa chỉ (8 bit thấp) : Port 0 cũng được thiết kế vừa được dùng cho dữ liệu, vừa dùng cho địa chỉ (AD0 – AD7). Để điều chỉnh giữa 2 chế độ dữ liệu và địa chỉ, ta thay đổi giá trị chân ALE – chân số 31. ALE = 0 tức Port 0 đang ở chế độ dữ liệu, ALE = 1 thì Port 0 ở chế độ địa chỉ.
Port 1
Port1 (chân 1 – 8) chỉ có một chức năng là I/O, không dùng cho mục đích khác (chỉ trong 8032/8052/8952 thì dùng thêm P1.0 và P1.1 cho bộ định thời thứ 3). Tại Port 1 đã có điện trở kéo lên nên không cần thêm điện trở ngoài.
Port 1 có khả năng kéo được 4 ngõ TTL và còn dùng làm 8 bit địa chỉ thấp trong quá trình lập trình hay kiểm tra.
Khi dùng làm ngõ vào, Port 1 phải được set mức logic 1 trước đó.
Port 2
Port 2 (chân 21 – 28) là port có 2 chức năng:
– Chức năng I/O (xuất / nhập)
– Chức năng địa chỉ: dùng làm 8 bit địa chỉ cao khi cần bộ nhớ ngoài có địa chỉ 16 bit. Khi đó, Port 2 cần được kết hợp với Port 0 vầ không được dùng cho mục đích I/O.
– Khi dùng làm ngõ vào, Port 2 phải được set mức logic 1 trước đó.
– Khi lập trình, Port 2 dùng làm 8 bit địa chỉ cao hay một số tín hiệu điều khiển.
Port 3
Port 3 (chân 10 – 17) là port có 2 chức năng:
– Chức năng I/O. Khi dùng làm ngõ vào, Port 3 phải được set mức logic 1 trước đó.
– Chức năng khác: mô tả như sau:
Các chân khác
- Vcc: chân thứ 40, nối với cực dương nguồn điện nuôi chip, điện thế chuẩn là 5V ± 20%
- Gnd: chân thứ 20, nói với cực âm nguồn.
- XTAL1, XTAL2: chân 18-19: Là nơi nối với thạch anh ngoài để cấp xung đồng hồ cho 8051.
- RST – chân 9: dùng để reset 8051. Cụ thể, khi truyền điện áp mức cao, 8051 sẽ reset. Quá trình này gọi là POR – Power On Reset, nó dẫn đến dữ liệu trên tất cả thanh ghi sẽ “bay hơi”. PC được set về 0.
- EA – External Access – chân 31: đây là chân input. Chân này thuộc loại active low, tức là khi đưa mức điện áp thấp vào EA thì nó nhận giá trị 1 (điện áp thấp ~ 0v).
- EA được nối với Vcc do 8051 có ROM tích hợp trên chip. Khi nối EA với GND, nó nhận điện áp mức thấp và do đó sẽ enable kết nối tới bộ nhớ ngoài.
- PSEN – Program Store Enable – chân 29: Chân này cũng thuộc kiểu active low. Sử dụng kết hợp với EA
- ALE – Address Latch Enable: Đây là chân output, kiểu active high. Nó đóng vai trò chuyển đổi giữa chế độ I/O và chế độ địa chỉ cho Port 0. Nếu ALE = 1, Port 0 là cổng I/O, dữ liệu, nếu ALE = 0, Port 0 là cổng địa chỉ.
Cách lập trình GPIO 8051
Để hiểu về GPIO trong vi điều khiển, ta cần làm một số ví dụ đơn giản. Đơn giản nhất vẫn là: Giao tiếp Led đơn = Output, Đọc nút nhấn = Input
Với Led đơn, ta sẽ sử dụng các chân ở mode output Push-Pull, thay vì Open drain. Như vậy, ta sẽ dùng các pin ở port 1, 2, 3; còn muốn dùng pin ở port 0 thì cần sử dụng điện trở kéo kết nối bên ngoài.
Hai kiểu kết nối Sink & Source
Kiểu Source hay còn gọi là xả: lấy dòng ra từ vi điều khiển (hình 1). Kiểu này thưởng dùng cho các tải có dòng ăn nhỏ như led, không nên mắc kiểu này khi điều khiển tải có dòng ăn lớn. Sẽ làm cháy mosfet điều khiển GPIO của 8051.
Kiểu Sink hay còn gọi là hút hay treo: lấy nguồn ngoài, dòng đi vào chân vi điều khiển (hình 2). Khi mắc kiểu này, các bạn cũng phải quan tâm tới dòng max mà chân vi điều khiển có thể chịu được, nếu không chân vi điều khiển sẽ hỏng.
Lưu ý: Với kiểu source dòng, ta dùng dòng ra từ chân vi điều khiển (Với 8051 đó là điện áp 5V) nhưng dòng rất nhỏ (<100mA), chỉ đủ dùng cho 1 LED.
Lập trình 8051 GPIO – Ghi giá trị ra các chân
Để lập trình các thanh ghi trong 8051, ta cần include thư viện <REGX51.h> hoặc <REGX52.h>
Để tác động vào 1 Port, ra dùng thanh ghi P0, P1, P2, P3.
P3 = 0xFF; // Set tất cả pin ở port 3 lên 1.
Lập trình vi điều khiển thường dùng hệ 16 để tác động vào port: 0xFF = 0b11111111 (hệ nhị phân) đại diện cho 8 pin của vi điều khiển 8051.
Chẳng hạn: 0x55 = 0b01010101 thì pin 8 là bit đầu tiên được nhắc đến và nó = 0, pin 0 là bit cuối và nó = 1.
8051 cho phép người lập trình tác động đến từng bit (từng pin của vi điều khiển)
Chẳng hạn, muốn đảo giá trị chân số 0 của port 3:
P3_0 = !P3_0;
Tương tự đối với các chân khác thôi và khi muốn làm mấy trò như chớp tắt led, đèn giao thông, hay led matrix, … Ta cần biết một chút về hàm delay (để ngưng hoạt động của vi điều khiển 1 lúc). Chúng ta sẽ tìm hiểu kỹ hơn trong phần ví dụ dưới đây:
Hàm delay
Ví dụ là nháy LED mỗi 1s. Xem hình vẽ với code và mô phỏng. Ta sẽ nối Led với chân P1.0 – kiểu Source.
Vậy hãy xem cách viết hàm delay_ms() ở đây, đó là 2 vòng lặp for lồng nhau:
void delay_ms (unsigned int t) { unsigned int x, y; for (x = 0; x < t; x++) { for (y = 0; y < 125; y++); //delay 1ms } }
Hàm delay tức là không làm gì cả trong 1 khoảng thời gian. Trong ngôn ngữ assembly, ta có lệnh “NOP” là không làm gì trong khoảng thời gian thực hiện lệnh => Điều ta cần quan tâm.
Vậy chẳng hạn thời gian thực hiện 1 lệnh là 0,01ms thì để delay 1ms, ta cần thực hiện 100 lệnh NOP như thế. Vậy tính thời gian thực hiện 1 lệnh như thế nào?
Chu kỳ máy
Chu kỳ máy là khoảng thời gian cần thiết được quy định để vi điều khiển thực hiện một lệnh cơ bản.
Một chu kỳ máy bằng 12 lần chu kỳ dao động của nguồn xung dao động cấp cho nó.
Đối với 89Sxx, có thể sử dụng thạch anh tạo dao động tần số f từ 2MHz đến 33MHz.
Thời gian thực hiện 1 lệnh, hay chu kỳ máy T = 12* (1/f).
Với ví dụ trên, f = 12MHz => T = 12* (1/(12*10^6)) = 1us.
- 1 lệnh cơ bản thực hiện trong 1us.
- Để delay khoảng thời gian 1s thì cần có 1/1u = 10^6 lệnh NOP
- Nhưng lệnh x++ hay y++ không phải lệnh cơ bản, và với kiểu dữ liệu càng lớn của x, y thì tính toán càng lâu.
- Nếu muốn tính toán delay chính xác -> cần dùng asm.
- Thích hợp với tính toán Timer.
- Với 2 vòng for lồng nhau -> thực nghiệm -> ra con số 125
Lập trình 8051 GPIO – Đọc giá trị từ nút nhấn
Để giao tiếp nút bấm, ta quan tâm đến chế độ input của 8051.
Mặc định, Port 1, 2, 3 input ở mode pullup, trong khi port 0 là floating nên cần điện trở kéo bên ngoài.
Để đọc nút bấm, ta đọc giá trị bit tương ứng trong thanh ghi của chân trong port. Ví dụ, nút bấm ở chân P2.1 thì ra đọc bit P2_1 của thanh ghi P2 😃
Do mode Pullup (port 1,2,3) hoặc điện trở kéo ngoài (port 0), nên mặc định, bit đọc được ban đầu là 1. Khi bấm nút, bit này = 0.
Vì vậy, ta dùng câu lệnh điều kiện if để đọc nút bấm:
if (bit == 0) // nút được bấm
{
//Thực hiện điều cần làm
}
else // nút không được bấm
{ …. }
Ví dụ: Đọc nút bấm tại chân P2.1 điều khiển led tại chân P2.0. Ban đầu nút tắt. Khi nút được bấm, Led sẽ đảo trạng thái.
Có một vấn đề: Khi mô phỏng, nút bấm đôi khi được bấm quá lâu, làm LED đảo liên tục. Ngoài thực tế, vấn đề này còn xảy ra nhiều hơn (có cả phím lởm !!)
- Hiện tượng dội phím = “SWITCH BOUNCE” → Cần phải giải quyết
Debounce nút nhấn
Có thể thấy dạng sóng của hiện tượng dội phím.
Từ đó, ta có 2 cách giải quyết hiện tượng này:
- Debounce bằng phần cứng: Khử dạng răng cưa bằng tụ điện – Ta lắp một tụ gốm (khoảng 100nF) song song với nút bấm.
- Debounce bằng phần mềm: Đọc lại giá trị nút bấm sau khoảng răng cưa ~ 20ms.
if (bit == 0) // nút được bấm { delay_ms(20); if (bit == 0) // Kiểm tra lại nút bấm { //Thực hiện điều cần làm } }
Chúng ta có thể dùng while để phát hiện việc nhả nút. Tuy nhiên trương trình sẽ treo bên trong while khi giữ nút. Vậy nên hãy cẩn thận khi sử dụng phương pháp này
if (bit == 0) // nút được bấm { delay_ms(20); while (bit == 0) {}; //Thực hiện điều cần làm }
Các ví dụ minh họa về lập trình 8051 GPIO
Ví dụ 1: Lập trình 8051 GPIO điều khiển một led và nút nhấn
Mạch nguyên lí như sau:
Chân 3.7 khi chưa nhấn nút sẽ có điện áp vào 5V, khi nhấn nút sẽ là 0V. LED nối với chân P2.0, LED sáng khi chân P2.0 ở 0V, tắt khi chân P2.0 ở 5V. Chương trình đọc và xuất dữ liệu như sau:
#include <regx52.h> sbit LED = P2 ^ 0; sbit BUTTON = P3 ^ 7; int main() { while (1) { LED = BUTTON; } }
Đây là 1 chương trình đơn giản, khi nhấn nút thì chân P3.7=0V => chân P2.0=0V dẫn đến LED sáng.
Ngược lại khi không nhấn nút, chân P3.7=5V => chân P2.0=5V dẫn đến LED tắt.
Bài toán 1: Chúng ta sử dụng 2 nút nhấn để điều khiển 1 đèn LED. Nhấn nút ON thì đèn sáng, nhấn nút OFF thì đèn tắt.
Mạch nguyên lí như sau:
Chân 3.7 tương ứng nút ON, 3.6 tương ứng nút OFF.
Chương trình như sau:
#include <regx52.h> sbit LED = P2 ^ 0; sbit ON = P3 ^ 7; sbit OFF = P3 ^ 6; int main() { LED = 1; while (1) { if (ON == 0) //Nhan nut ON { LED = 0; //LED sang } if (OFF == 0) //Nhan nut OFF { LED = 1; //LED tat } } }
Trong thực tế, chúng ta hay gặp hiện tượng “Dội phím” (nút nhấn dao động liên tục mức 0-1). Vi điều khiển có thể hiểu là rất nhiều lần nhấn-thả liên tiếp. Để khắc phục vấn đề này, khi đọc trạng thái nút nhấn, chúng ta delay 1 khoảng để xác định chính xác nút được nhấn hay chưa. Viết lại chương trình như sau:
#include <regx52.h> sbit LED = P2 ^ 0; sbit ON = P3 ^ 7; sbit OFF = P3 ^ 6; void Delay(int time); int main() { LED = 1; while (1) { if (ON == 0) //Nhan nut ON { Delay(20); LED = 0; //LED sang } if (OFF == 0) //Nhan nut OFF { Delay(20); LED = 1; //LED tat } } } void Delay(int time) { int i, j; for (i = 0; i < time; i++) for (j = 0; j < 125; j++); }
Bài toán 2
Dùng 1 nút nhấn để điều khiển LED, nhấn 1 lần thì LED sáng, nhấn lần tiếp theo thì LED tắt.
Cách 1 : Dùng hàm While() để chờ chuyển mức
Sau khi chúng ta phát hiện dữ liệu xuống mức 0, chúng ta sẽ dùng vòng lặp while để chờ cho đến khi dữ liệu thoát khỏi mức 0 để xác nhận nút đã được nhấn. Lúc đó ta có thể thực hiện các chức năng khác tương ứng với việc nhấn nút.
Mạch nguyên lí như sau:
Chương trình như sau:
#include <regx52.h> sbit LED = P2 ^ 0; sbit BUTTON = P3 ^ 7; void Delay(int time); int main() { LED = 1; while (1) { if (BUTTON == 0) { Delay(20); if (BUTTON == 0) { while (BUTTON == 0) {} LED = !LED; } } } } void Delay(int time) { int i, j; for (i = 0; i < time; i++) for (j = 0; j < 125; j++); }
Tuy nhiên, khi làm việc với nhiều nút nhấn, cách này không phù hợp mà nên sử dụng cách 2.
Cách 2 : So sánh dữ liệu trước và sau để phát hiện chuyển mức.
Ý tưởng: Chúng ta so sánh giữa mức ngay trước đó và mức mới. Nếu có sự sai khác giữa mức mới và mức ngay sau đó thì đồng nghĩa với việc có sự thay đổi về giá trị nút nhấn. Tùy thuộc vào mục đích của người lập trình chọn sườn lên hay sườn xuống để thực hiện lập trình theo ý muốn. Ở đây, mình chọn sườn xuống của nút nhấn để thực hiện.
Mạch nguyên lí như sau:
Chương trình:
#include <regx52.h> sbit LED = P2 ^ 0; sbit BUTTON = P3 ^ 7; bit BUTTON_OLD; void Delay(int time); int main() { LED = 1; while (1) { if (BUTTON == 0) { Delay(20); if ((BUTTON == 0) && (BUTTON_OLD == 1)) { LED = !LED; } } //Cap nhat trang thái moi cua nut nhan BUTTON_OLD = BUTTON; Delay(20); } } void Delay(int time) { int i, j; for (i = 0; i < time; i++) for (j = 0; j < 125; j++); }
Chúng ta cùng thử lăp nhiều đèn LED hơn để mô phỏng và quan sát nhé:
Ví dụ 2: Lập trình 8051 GPIO điều khiển nhiều led và nút nhấn
Sử dụng 2 nút nhấn BT1 và BT2 lập trình điều khiển: Khi nhấn nút nhấn BT1, 8 led đơn D1-D8 sáng. Khi nhấn nút nhấn BT2, 8 led đơn D1-D8 tắt.
Bài toán này khá đơn giản, chỉ cần kiểm tra mức logic trên 2 chân nối vào nút nhấn (P1.0 và P1.7), chân nào có mức logic 0 (tương ứng với nút được nhấn) thì mình sẽ thực hiện lệnh theo yêu cầu khi nút nhấn đó được nhấn. Vì ở đây đề bài không nói đến yêu cầu nhấn và giữ hay nhấn rồi nhả ra, nên mình chưa cần quan tâm vấn đề đó, chỉ cần biết rằng đã nhấn nút nhấn thì sẽ thực hiện lệnh, sau đó còn giữ hay thả ra thì chưa cần quan tâm.
Để kiểm tra được mức logic trên chân VĐK (mức 0 hay mức 1), mình sẽ dùng câu lệnh “if“. Lưu đồ thực hiện bài toán được cho như hình sau.
Sau đây là đoạn code trong chương trình chính thực hiện bài toán đã cho.
Ví dụ 3 Điều khiển nhiều led bằng các trạng thái của nút nhấn
Sử dụng nút nhấn BT1 điều khiển theo yêu cầu: Ban đầu 8 led D1-D8 tắt, Khi nhấn nút nhấn BT1 1 lần (nhấn rồi nhả ra) thì 8 led D1-D8 sáng, sau đó tiếp tục nhấn nút BT1 thêm 1 lần nữa thì 8 led D1-D8 tắt, và tiếp tục lặp đi lặp lại như vậy.
Với bài toán này, mấu chốt là mình phải đếm được số lần nhấn nút. Sau đó dựa vào số lần nhấn này điều khiển led. Để thực hiện bài toán này, mình sẽ dùng 1 biến để đếm số lần nhấn. Biến sẽ khởi tạo bằng 0, nếu nhấn lần thứ nhất biến tăng giá trị lên 1, lần thứ 2, biến tăng giá trị lên 2, nhưng khi nhấn lần thứ 3, mình sẽ reset giá trị biến về 1. Nghĩa là biến sẽ chỉ có 2 giá trị 1 hoặc 2. Nếu bằng 1 thì cho 8 led sáng, bằng 2 thì cho 8 led tắt.
Sau đây là lưu đồ thuật toán cho ý tưởng trên.
Code được viết trong chương trình chính như dưới đây.
Trên đây là cách sử dụng nút nhất với vi điều khiển 8051 của mình. Các bạn có thể tùy biến chương trình theo yêu cầu bài toán. Cái quan trọng là hiểu được yêu cầu bài toán để đưa ra thuật toán phù hợp.
Mô phỏng trên Proteus
Mô phỏng ví dụ 1:
- Kết quả khi nút hấn được nhấn LED sáng
Mô phỏng bài toán 1:
- Khi ta nhấn ON, thì đèn sáng
- Khi ta nhấn OFF thì đèn tắt
Việc mô phỏng lên trên phần mềm Proteus khá đơn giản, các bạn cùng thực hành với các ví dụ và bài toán bên trên nhé
Kết
Trên đây mình đã giới thiệu các bạn về GPIO của 8051 và cách lập trình GPIO để điều khiển led và nút nhấn. Đây là những ví dụ cơ bản nhất khi chúng ta giao tiếp với led và nút nhấn.
Tiếp tục tới các bài sau về lập trình 8051 nhé. Đừng quên gia nhập Hội Anh Em Nghiện Lập trình để giao lưu và học hỏi nhé!!!