Cấu tạo ngoại vi UART của Nuvoton
N76E885 được trang bị 2 module serial port giống nhau là serial port 0 (UART0) và serial port 1 (UART1).
Tên của các bit, thanh ghi của module 1 sẽ có thêm “_1” ở cuối so với module 0. Ví dụ như ở module 0 là SBUF, TI, RI… thì ở module 1 là SBUF_1, TI_1, RI_1… Trong bài viết này, mình chỉ dùng tên của các thanh ghi module 0, nếu các bạn muốn sử dụng module 1 thì chỉ cần thêm “_1” vào tên thanh ghi, bit tương ứng.
- Thanh ghi SCON chứa các bit cấu hình và cờ báo trạng thái liên quan.
- Thanh ghi SBUF chứa dữ liệu gửi đi và nhận về.
Mỗi module serial port có thể cấu hình 4 mode thông qua các bit SM0 và SM1 (tức SCON[7:6]):
Ta cũng có thể cấu hình cấm hoặc cho phép việc thu nhận data bằng bit REN (SCON.4).
Giới thiệu Nuvoton UART Mode 0
Mode 0 dùng truyền thông bất đồng bộ. Tại mode này, N76E885 luôn đóng vai trò làm MASTER, pin TXD (tức P0.3) đảm nhiệm việc cấp clock, còn pin RXD (tức P2.0) dùng để đọc/ghi data.
Vì chỉ có 1 pin RXD đọc/ghi dữ liệu, do đó mode 1 chỉ làm việc bán song công (nghĩa là chỉ có thể đọc hoặc ghi data tại một thời điểm).
Mỗi frame gồm 8 bit data, được truyền đi hoặc đọc vào theo từng bit (LSB trước):
- Quá trình truyền data bắt đầu sau lệnh ghi vào thanh ghi SBUF (SBUF = data). Sau khi truyền hết frame, cờ TI được tự động set lên 1 bằng phần cứng.
- Quá trình đọc data bắt đầu sau lệnh xoá cờ RI (RI = 0). Sau khi đọc xong frame, cờ RI được tự động set lên 1 bằng phần cứng. 8 bit data đọc vào được lưu trong SBUF.
Mode 0 hầu như ít được sử dụng, nên mặc dù module serial port cho phép cấu hình để hoạt động ở cả truyền thông đồng bộ và truyền thông bất đồng bộ, nhưng người ta vẫn thường gọi module này module UART (truyền thông bất đồng bộ). Có vài lý do khiến mode 0 ít được sử dụng như:
- Truyền thông đồng bộ như mode 0 có thể triển khai bằng phần mềm thay vì phần cứng, mặc dù tốc độ chậm hơn nhưng có sự linh hoạt về i/o.
- Nếu quan tâm về tốc độ, ta sẽ dùng module SPI được trang bị sẵn trên N76E885, có thể làm tốt hơn mode 0 trên module serial port.
Giới thiệu Nuvoton UART Mode 1
Ở mode 1, mỗi frame gồm 1 bit start + 8 bit data và 1 bit stop:
- Quá trình truyền data bắt đầu sau lệnh ghi vào thanh ghi SBUF (SBUF = data). Sau khi truyền hết 8 bit data, cờ TI được tự động set lên 1 bằng phần cứng.
- Quá trình đọc data diễn ra tự động bằng phần cứng. Sau khi đọc xong frame, cờ RI được tự động set lên 1 bằng phần cứng. 8 bit data đọc vào được lưu trong SBUF.
Giới thiệu Nuvoton UART Mode 2, 3
Ở mode 2 và 3, mỗi frame gồm 1 bit start + 8 bit data + 1 bit bổ sung và 1 bit stop:
- Quá trình truyền data bắt đầu sau lệnh ghi vào thanh ghi SBUF (SBUF = data). Sau khi truyền hết 9 bit data (8 bit của SBUF và 1 bit của TB8), cờ TI được tự động set lên 1 bằng phần cứng.
- Quá trình đọc data diễn ra tự động bằng phần cứng. Cờ RI tự động set lên 1 sau khi đọc hết 9 bit data hoặc sau bit stop (tuỳ thuộc cấu hình ở bit SMOD0 tức PCON.6). 9 bit data đọc vào được lưu trong SBUF (8 bit) và RB8 (1 bit).
So với mode 1 thì mode 2, 3 có bổ sung thêm 1 bit sau 8 bit data. Bit bổ sung này thường dùng làm parity hoặc phân biệt address và data khi giao tiếp với nhiều vi điều khiển khác.
Giới thiệu về Clock và tốc độ Baud trong UART
Khi ở mode 0 và 2, nguồn clock cấp cho module serial port là Fsys. Trong khi ở mode 1 và 3 thì:
- Nếu dùng module 0: clock cho mode 1 và 3 có thể từ timer 1 overflow hoặc timer 3 overflow, được chọn bằng bit BRCK (T3CON.5):
- Nếu dùng module 1: clock cho mode 1 và 3 lấy từ timer 3 overflow.
Tốc độ Baud của các mode được tóm tắt theo bảng sau:
Lưu ý: Pre-scale trong bảng là của timer 3, từ 1 đến 128.
Theo đó, đối với mode 0 và 2, ta chỉ có 2 mức tốc độ Baud cố định. Còn đối với mode 1 và 3, ta có thể tuỳ chọn tốc độ Baud mong muốn bằng cách cấu hình thời gian tràn của timer được chọn, hay cũng chính là cấu hình giá trị nạp lại cho timer. Điều này cũng có nghĩa là khi dùng clock từ timer 1 (đối với UART0) thì cần cấu hình timer 1 ở mode tự nạp lại (timer 1 mode 2).
Cũng theo bảng trên, ta thấy tốc độ Baud có thể tăng gấp đôi khi bit SMOD = 1 (kèm với các điều kiện được chú thích trong bảng).
Lập trình Nuvoton UART gửi chuỗi lên máy tính
Ví dụ 1: Gửi chuỗi “Hello, world!” lên máy tính qua cổng COM
Phần cứng:
Đối với máy tính (PC) có cổng COM: trên cổng COM có 2 chân Tx và Rx về bản chất hoạt động như của giao thức UART. Điểm khác biệt chỉ nằm ở điện áp thể hiện mức logic. Ta chỉ cần một module chuyển đổi từ COM sang UART là có thể giao tiếp N76E885 với máy tính:
Hầu hết máy tính hiện nay đều không còn tồn tại cổng COM. Thay và đó, ta có thể mua một thiết bị chuyển đổi USB-UART:
Kết nối sẽ là Rx (PC) – Tx (N76E885) (gửi lên PC) và Tx (PC) – Rx (N76E885) (đọc từ PC):
Ngoài ra, ta sử dụng phần mềm Hercules để giám sát cổng COM:
Khi cắm USB-UART vào PC, PC sẽ tạo một cổng COM ảo. Tại màn hình Serial của phần mềm Hercules:
- Chọn đúng tên cổng COM.
- Chọn tốc độ Baud, ở ví dụ này mình chọn Baud = 19200.
- Data size các bạn chọn 8-bit và Parity là none (ví dụ này mình sẽ dùng UART ở mode 1 nên không có parity).
Phân tích:
Trước hết, cần cấu hình các i/o tương ứng ở mode bi-directional.
Như đã nói, mình chọn UART1 và cấu hình ở mode 1.
Tính toán giá trị nạp lại cho timer 3, chọn Pre-scale = 1 (prescaler = 1/1):
Ở phía PC: khi đã hoàn tất các kết nối, tại màn hình Serial của phần mềm Hercules, mỗi lần máy tính nhận được một mã ASCII, nó sẽ hiện ký tự ấy lên màn hình Serial.
Ở phía MCU: N76E885 gửi chuỗi “Hello, world!” bằng cách gửi lần lượt từng ký tự trong chuỗi, mỗi ký tự có độ dài 1 byte. Cách thức gửi 1 byte đã được trình bày trong phần cấu tạo. Trước mỗi lần gửi, ta cần kiểm tra xem lần gửi trước đó đã hoàn tất chưa bằng việc kiểm tra cờ TI. Sau khi N76E885 reset (lúc khởi động), cờ TI được xoá về 0, do đó, ban đầu cần set cờ này lên 1 để đánh dấu rằng đã sẵn sàng cho lần gửi kế tiếp.
Chương trình được triển khai như sau:
#include <stdint.h> #include <mcs51/N76E885.h> #include <mcs51/Define.h> // #define LED1 P04 // #define LED2 P03 // #define ON_LED 0 // #define OFF_LED 1 void main(void) { // Insert code // // P2.4 (RXD) bi-directional mode // P2M1 &= CLR_BIT4; P2M2 &= CLR_BIT4; P24 = 1; // // P2.5 (TXD) bi-directional mode // P2M1 &= CLR_BIT5; P2M2 &= CLR_BIT5; P25 = 1; // // cấu hình serial port // // mode 1 - UART 8-bit SM0_1 = 0; SM1_1 = 1; // // cho phép nhận data // REN_1 = 1; // // khởi tạo cho các lần gửi tiếp theo TI_1 = 1; // // cấu hình tốc độ Baud = 19200 // // sử dụng tốc bộ Baud bình thường (không double) => bit SMOD (PCON.7) = 0 // T3CON &= CLR_BIT7; // // tính giá trị nạp lại // reload value = 65536 - 1/16 * Fsys/Pre-scale * 1/Baud RH3 = HIBYTE(65464); RL3 = LOBYTE(65464); // // run timer 3 T3CON |= SET_BIT3; uint8_t const str[] = "Hello, world!"; for (uint8_t i = 0; str[i] != 0; ++i) { while (!TI_1); TI_1 = 0; SBUF_1 = str[i]; } while (1) { // Do nothing } }
Biên dịch và nạp code:
Lập trình Nuvoton UART nhận chuỗi và bật tắt Led
Ví dụ 2: Bật – tắt LED1 bằng bàn phím máy tính.
Yêu cầu:
- Nhấn phím “y” trên bàn phím máy tính: bật LED1.
- Nhấn phím “n” trên bàn phím máy tính: tắt LED1.
- Nhấn các phím còn lại: giữ nguyên trạng thái LED1.
Phân tích:
Mình tiếp tục sử dụng UART1 mode 1 như ví dụ 1.
Phía PC: tại màn hình Serial của phần mềm Hercules, mỗi lần nhấn một phím trên bàn phím, máy tính sẽ gửi mã ASCII tương ứng của phím được nhấn qua cổng COM.
Phía MCU: quá trình đọc data diễn ra tự động như đã trình bày trong phần cấu tạo. Công việc của chúng ta chỉ là kiểm tra cờ RI và lấy data tại SBUF. Sau khi có data (cũng là mã ASCII của phím), ta thực hiện lệnh tương ứng với mã đọc được.
Chương trình được triển khai như sau:
#include <stdint.h> #include <mcs51/N76E885.h> #include <mcs51/Define.h> #define LED1 P04 // #define LED2 P03 #define ON_LED 0 #define OFF_LED 1 void main(void) { // Insert code // // P0.4 (LED1) bi-directional mode // P0M1 &= CLR_BIT4; P0M2 &= CLR_BIT4; LED1 = OFF_LED; // // P2.4 (RXD) bi-directional mode // P2M1 &= CLR_BIT4; P2M2 &= CLR_BIT4; P24 = 1; // // P2.5 (TXD) bi-directional mode // P2M1 &= CLR_BIT5; P2M2 &= CLR_BIT5; P25 = 1; // // cấu hình serial port // // mode 1 - UART 8-bit SM0_1 = 0; SM1_1 = 1; // // cho phép nhận data // REN_1 = 1; // // khởi tạo cho các lần gửi tiếp theo TI_1 = 1; // // cấu hình tốc độ Baud = 19200 // // sử dụng tốc bộ Baud bình thường (không double) => bit SMOD (PCON.7) = 0 // T3CON &= CLR_BIT7; // // tính giá trị nạp lại // reload value = 65536 - 1/16 * Fsys/Pre-scale * 1/Baud RH3 = HIBYTE(65464); RL3 = LOBYTE(65464); // // run timer 3 T3CON |= SET_BIT3; while (1) { // Insert code if (RI_1) { uint8_t key = SBUF_1; RI_1 = 0; switch (key) { case 'y': LED1 = ON_LED; break; case 'n': LED1 = OFF_LED; default:; } } } }
Biên dịch và nạp code:
Lập trình Nuvoton UART + Input Capture
Ví dụ 3: Giải mã điều khiển hồng ngoại giao thức NEC.
Trong bài Input Capture, mình đã đề cập đến vấn đề giải mã hồng ngoại giao thức NEC. Ở bài đó mình chỉ biết được 2 mã của nút bật và nút tắt LED1.
Trong bài này, mình sẽ giải mã và hiển thị thông tin của nút được nhấn trên điều khiển hồng ngoại mã hoá NEC.
Yêu cầu:
Khi nhấn một nút trên điều khiển hồng ngoại (mã hoá NEC), giải mã và hiển thị mã (4-byte) của nút đó lên màn hình máy tính.
Phần cứng:
Các bạn giữ nguyên kết nối module UART và gắn thêm với module thu hồng ngoại giống như ví dụ ở bài Input Capture.
Phân tích:
Chúng ta tận dụng lại chương trình trong ví dụ của bài Input Capture để giải mã. Ngoài ra, ta sẽ thêm vào phần cấu hình UART và sửa lại một chút vòng lặp while (1).
Mỗi lần giải mã hồng ngoại thành công, thay vì kiểm tra mã để bật – tắt LED1 thì ta sẽ gửi mã đó lên máy tính để hiển thị.
Mã có được sau khi giải mã là số (number), muốn hiển thị lên máy tính thì trước hết, mã ấy cần được đưa về dạng text. Dạng text là dạng có thể hiển thị lên màn hình máy tính dưới dạng ký tự. Để đơn giản, mình sẽ hiển thị mã ở dạng số hex với 4 bit thành 1 digit. Như vậy, mã hiển thị gồm 8 digit:
- 2 digit đầu: Address.
- 2 digit thứ 2: đảo bit của Address.
- 2 digit thứ 3: Command.
- 2 digit cuối: đảo bit của Command.
Chương trình được triển khai như sau:
#include <stdint.h> #include <mcs51/N76E885.h> #include <mcs51/Define.h> // #define LED1 P04 // #define LED2 P03 // #define ON_LED 0 // #define OFF_LED 1 #define STATE_INIT 0 #define STATE_BEG_START_BIT 1 #define STATE_MID_START_BIT 2 #define STATE_BEG_LOGIC_BIT 3 #define STATE_MID_LOGIC_BIT 4 #define STATE_BEG_STOP_BIT 5 typedef struct { uint8_t logic_bit : 7; uint8_t success : 1; uint8_t state; union { struct { uint8_t inverse_of_command; uint8_t command; uint8_t inverse_of_address; uint8_t address; }; uint32_t full; } code; } nec_t; // biến lưu kết quả giải mã nec_t decode; void necReset() { // // dừng timer TR2 = 0; // // capture kênh 2 tại cạnh xuống CAPCON1 &= CLR_BIT5 & CLR_BIT4; // decode.success = 0; decode.state = STATE_INIT; } void necHandler() { uint16_t capture = MAKEWORD(C2H, C2L); switch (decode.state) { case STATE_INIT: // // run timer TR2 = 1; // // capture kênh 2 tại bất kỳ cạnh xung CAPCON1 |= SET_BIT5; CAPCON1 &= CLR_BIT4; // decode.state = STATE_BEG_START_BIT; break; case STATE_BEG_START_BIT: if ((capture >= 47000) && (capture <= 60825)) { decode.state = STATE_MID_START_BIT; } else { decode.state = STATE_INIT; } break; case STATE_MID_START_BIT: if ((capture >= 22118) && (capture <= 27648)) { decode.logic_bit = 0; decode.success = 0; decode.state = STATE_BEG_LOGIC_BIT; } else { decode.state = STATE_INIT; } break; case STATE_BEG_LOGIC_BIT: if ((capture >= 2212) && (capture <= 4424)) { decode.state = STATE_MID_LOGIC_BIT; } else { decode.state = STATE_INIT; } break; case STATE_MID_LOGIC_BIT: if ((capture >= 2212) && (capture <= 4424)) { ++decode.logic_bit; decode.code.full <<= 1; } else if ((capture >= 8294) && (capture <= 9953)) { ++decode.logic_bit; decode.code.full <<= 1; decode.code.inverse_of_command |= SET_BIT0; } else { decode.state = STATE_INIT; break; } //---------------- if (decode.logic_bit == 32) { decode.state = STATE_BEG_STOP_BIT; } else { decode.state = STATE_BEG_LOGIC_BIT; } break; case STATE_BEG_STOP_BIT: if ((capture >= 2212) && (capture <= 4424)) { decode.success = 1; } decode.state = STATE_INIT; break; } if (decode.state == STATE_INIT) { // // dừng timer TR2 = 0; // // capture kênh 2 tại cạnh xuống CAPCON1 &= CLR_BIT5 & CLR_BIT4; } } void number2TextHex(uint8_t *text_hex, uint8_t number) { uint8_t digit; digit = number >> 4; if (digit > 9) { text_hex[0] = 'A' + (digit - 10); } else { text_hex[0] = '0' + digit; } digit = number & 0x0f; if (digit > 9) { text_hex[1] = 'A' + (digit - 10); } else { text_hex[1] = '0' + digit; } } void main(void) { // Insert code // // cấu hình serial port // // P2.4 (RXD) bi-directional mode // P2M1 &= CLR_BIT4; P2M2 &= CLR_BIT4; P24 = 1; // // P2.5 (TXD) bi-directional mode // P2M1 &= CLR_BIT5; P2M2 &= CLR_BIT5; P25 = 1; // // mode 1 - UART 8-bit SM0_1 = 0; SM1_1 = 1; // // cho phép nhận data // REN_1 = 1; // // khởi tạo cho các lần gửi tiếp theo TI_1 = 1; // // cấu hình tốc độ Baud = 19200 // // sử dụng tốc bộ Baud bình thường (không double) => bit SMOD (PCON.7) = 0 // T3CON &= CLR_BIT7; // // tính giá trị nạp lại // reload value = 65536 - 1/16 * Fsys/Pre-scale * 1/Baud RH3 = HIBYTE(65464); RL3 = LOBYTE(65464); // // run timer 3 T3CON |= SET_BIT3; // // khởi tạo input capture // // cấu hình input cho pin của input capture kênh 2 (P2.2) P2M1 &= CLR_BIT2; P2M2 &= CLR_BIT2; P22 = 1; // // cho phép kênh 2 CAPCON0 |= SET_BIT6; // // cho phép bộ lọc nhiễu kênh 2 CAPCON2 |= SET_BIT6; // // capture kênh 2 tại cạnh xuống CAPCON1 &= CLR_BIT5 & CLR_BIT4; // // khởi tạo timer 2 // // prescaler = 1/4 T2MOD &= CLR_BIT6 & CLR_BIT5; T2MOD |= SET_BIT4; // // mode không nạp lại CM_RL2 = 0; T2MOD &= CLR_BIT7; // // tự xoá timer khi capture T2MOD |= SET_BIT3; // // khởi tạo tiến trình giải mã mới // necReset(); while (1) { // // sự kiện capture // if (CAPCON0 & SET_BIT2) { // // xoá cờ capture kênh 2 CAPCON0 &= CLR_BIT2; // necHandler(); } // // sự kiện timer 2 tràn // if (TF2) { // // xoá cờ timer 2 TF2 = 0; // necReset(); } // // chương trình chính // if (decode.success) { decode.success = 0; uint8_t str[10]; number2TextHex(str + 0, decode.code.address); number2TextHex(str + 2, decode.code.inverse_of_address); number2TextHex(str + 4, decode.code.command); number2TextHex(str + 6, decode.code.inverse_of_command); str[8] = '\n'; // xuống dòng str[9] = 0; // null for (uint8_t i = 0; str[i] != 0; ++i) { while (!TI_1); TI_1 = 0; SBUF_1 = str[i]; } } } }
Biên dịch và nạp code:
Kết luận
Khi đã làm chủ được UART, chúng ta hoàn toàn có thể sử dụng màn hình máy tính như một cửa sổ output và bàn phím máy tính như một thiết bị input, kết nối lại vô cùng gọn chỉ với 2 i/o. Từ đó, các bạn có thể triển khai nhanh chóng các ví dụ mà không cần phải thiết lập phần cứng cồng kềnh.