ĐIỀU KHIỂN RGB LED CUBE 8x8x8 BẰNG PHƯƠNG PHÁP BAM 4 BIT VỚI 74HC595

I. GIỚI THIỆU

Với khối RGB CUBE 8x8x8, chúng ta có 512 LED RED, 512 LED GREEN và 512 LED BLUE, tổng cộng là: 512x3 = 1,536 LED. Để điều khiển chúng riêng biệt với màu sắc mong muốn, chúng ta dùng 24 con 74HC595 và 8 con TIP42C và áp dụng phương pháp BAM – 4bit (Bit Angle Modulation). Về BAM, các bạn có thể tham khảo tại:

Bằng phương pháp 4 bit BAM, chúng ta có thể tạo ra 4,096 màu riêng biệt (16^3) cho từng RGB LED.

II. BOM CHO RGB CUBE 8x8x8

No. Items Specification Q'ty Remarks
1 Arduino Mega 2560 R3   1  
2 74HC595   24  
3 ULN2803   24  
4 TIP42C   8  
5 2N2222A   8  
6 Đế IC 16P DIP16 24 Dùng cho 74HC595
7 Đế IC 18P DIP18 24 Dùng cho ULN2803
8 RGB Led 5mm 512  
9 Resistors R100   250  
10 Resistors R1K   10  
11 Resistors R10K   20  
12 Capacitor 0.1uF   30  
13 Bus 8   30  
14 Bus 4   8  
15 Hàng rào đực đơn thẳng dài (40pin) Chân dài 7 Dùng cho cube base
16 Hàng rào đực đơn thẳng ngắn (40pin) Chân ngắn 10  
17 Jack nguồn DC đực 5V 5.5x2.1mm 1 Cấp nguồn cho Arduino
18 Board đồng 2 lớp Khổ A4 1 Dùng cho cube base
19 Board đồng 1 lớp Khổ A4 2  
20 Mica trong khổ A3   1 Làm hộpchứa đèn
21 Mica đục khổ A3   1 Làm hộp chứa board
22 Công tắc gạt ON/OFF 1  
23 Quạt nhỏ   1 Làm quạt hút cho hộp chứa board
24 Dây kẽm/ dây đồng Cuộn 1  
25 Pin 9V   1 Dùng để test led.
26 Hộp đế pin 9V   1  
27 Dung dịch rửa mạch in   1  
28 Nhựa thông   1  
29 Xăng thơm   1  
30 Mũi khoan mạch các loại   1  
31 Keo 502 Hộp lớn 1 Dán hộp mica
32 Dao cắt mạch   1 Cắt mạch và mica

III. PHẦN CỨNG

1. CUBE TEMPLATE

Các bạn dùng mũi khoan 5mm (RGB LED 5mm) để tạo ra 64 lỗ trên tấm gỗ với khoảng cách giữa các LED là 20mm.

2. HÀN KHỐI CUBE

Dựa vào TEMPLATE trên, các bạn sẽ tạo ra được một PLANE của LED CUBE. Mỗi PLANE có 64 RGB LED. Đây là công đoạn mất nhiều thời gian nhất và dễ sai sót nhất. Do vậy, trước và sau khi hàn LED các bạn phải kiểm tra lại, nếu không sẽ rất khó sửa sau khi làm thành khối CUBE.

3. SHIFT REGISTER BOARD & LAYER BOARD

Các bạn có thể tham khảo tại: 

Lưu ý: Chúng ta cần 3 board mạch in SHIFT REGISTER BOARD, mỗi mạch in đều khiển một màu cho 64 RGB LED. Các mạch in này mắc nối tiếp với nhau để mỗi lần chốt ta có 24 byte data được shift out theo thứ tự:

4. CUBE BASE

Hộp chứa nguồn, các board, arduino và bên ngoài có thêm các công tắc, switch, hay nút nhấn. Các bạn có thể tham khảo trên internet cách làm một cube.

III. CHƯƠNG TRÌNH

Chương trình bên dưới tham khảo tại trang: http://www.kevindarrah.com/?cat=99 của Kevin Darrah.

#include <SPI.h> // Dùng thư viện SPI
#define latch_pin 4 // Chân LATCH
#define blank_pin 5 // Chân OE
#define data_pin 51 // Chân DATA  (MOSI, pin 51 on MEGA2560)
#define clock_pin 52 // Chân CLOCK (SCK, pin 52 on MEGA2560)
/* *Khai báo các layer* */
#define layer1 26 // bottom layer
#define layer2 27
#define layer3 28
#define layer4 29
#define layer5 30
#define layer6 31
#define layer7 32
#define layer8 33 // top layer
int layerArray[8] = { layer1, layer2, layer3, layer4, layer5, layer6, layer7, layer8 };
int lastAnode;
//This is how the brightness for every LED is stored,
//Each LED only needs a 'bit' to know if it should be ON or OFF, so 64 Bytes gives you 512 bits= 512 LEDs
//Since we are modulating the LEDs, using 4 bit resolution, each color has 4 arrays containing 64 bits each
byte red[4][64];
byte blue[4][64];
byte green[4][64];
//*********** Defining the Cube *************
#define BAM_RESOLUTION 4 // EG 4 bit colour = 15 variation of R, G & B (4,096 colours)
const byte Size_X = 8; //Number of LEDs X axis
const byte Size_Y = 8; //Number of Layers Y axis
const byte Size_Z = 8; //Number of LEDs Z axis
int level = 0; //keeps track of which level we are shifting data to
int anodeLevel = 0; //this increments through the anode levels
int BAM_Bit, BAM_Counter = 0; // Bit Angle Modulation variables to keep track of things
//****setup****setup****setup****setup****setup****setup****setup****setup****setup****setup****setup****setup****setup
void setup()
{
    SPI.setBitOrder(MSBFIRST); //Most Significant Bit First
    SPI.setDataMode(SPI_MODE0); // Mode 0 Rising edge of data, keep clock low
    // SPI.setClockDivider(SPI_CLOCK_DIV2);//Run the data in at 16MHz/2 - 8MHz
    SPI.setClockDivider(SPI_CLOCK_DIV2); //Run the data in at 16MHz/2 - 8MHz
    //Serial.begin(115200);// if you need it?
    noInterrupts(); // kill interrupts until everybody is set up
    //We use Timer 1 to refresh the cube
    TCCR1A = B00000000; //Register A all 0's since we're not toggling any pins
    TCCR1B = B00001011; //bit 3 set to place in CTC mode, will call an interrupt on a counter match
    //bits 0 and 1 are set to divide the clock by 64, so 16MHz/64=250kHz
    TIMSK1 = B00000010; //bit 1 set to call the interrupt on an OCR1A match
    OCR1A = 30; // you can play with this, but I set it to 30, which means:
    //our clock runs at 250kHz, which is 1/250kHz = 4us
    //with OCR1A set to 30, this means the interrupt will be called every (30+1)x4us=124us,
    // which gives a multiplex frequency of about 8kHz
    //finally set up the Outputs
    // pinMode(latch_pin, OUTPUT);//Latch
    pinMode(2, OUTPUT); // turn off PWM and set PortD bit 4 as output
    pinMode(3, OUTPUT); // turn off PWM and set PortD bit 5 as output
    pinMode(data_pin, OUTPUT); //MOSI DATA
    pinMode(clock_pin, OUTPUT); //SPI Clock
    //pinMode(blank_pin, OUTPUT);//Output Enable  important to do this last, so LEDs do not flash on boot up
    //*** Here layer pins are set as outputs
    pinMode(layer1, OUTPUT);
    pinMode(layer2, OUTPUT);
    pinMode(layer3, OUTPUT);
    pinMode(layer4, OUTPUT);
    pinMode(layer5, OUTPUT);
    pinMode(layer6, OUTPUT);
    pinMode(layer7, OUTPUT);
    pinMode(layer8, OUTPUT);
    SPI.begin(); //start up the SPI library
    interrupts(); //let the show begin, this lets the multiplexing start
} //***end setup***end setup***end setup***end setup***end setup***end setup***end setup***end setup***end setup***end setup
void loop()
{ //***start loop***start loop***start loop***start loop***start loop***start loop***start loop***start loop***start loop
    clearfast();
    LED(4, 5, 6, 3, 6, 13);
    delay(5000);
    clearfast();
    LED(0, 0, 0, 15, 0, 0);
    delay(5000);
    LED(7, 0, 0, 0, 15, 0);
    delay(5000);
    LED(7, 7, 0, 0, 0, 15);
    delay(5000);
    LED(0, 7, 0, 15, 15, 0);
    delay(5000);
    LED(0, 7, 7, 15, 0, 15);
    delay(5000);
    LED(7, 0, 7, 0, 15, 15);
    delay(5000);
    LED(0, 0, 7, 15, 15, 15);
    delay(5000);
    LED(7, 7, 7, 15, 9, 0);
    delay(5000);
}
void LED(int Z, int Y, int X, int R, int G, int B)
{ //****LED Routine****LED Routine****LED Routine****LED Routine
    // First, check and make sure nothing went beyond the limits, Limits are either 0 or Size of the Y,X,Z axis -1 for locations, and 0 or Bam_Resolution - 1 for brightness/colour
    X = constrain(X, 0, Size_X - 1); //Cube X axis
    Y = constrain(Y, 0, Size_Y - 1); //Cube Y axis
    Z = constrain(Z, 0, Size_Z - 1); //Cube Z axis
    R = constrain(R, 0, (1 << BAM_RESOLUTION) - 1); //Cube Red
    G = constrain(G, 0, (1 << BAM_RESOLUTION) - 1); //Cube Green
    B = constrain(B, 0, (1 << BAM_RESOLUTION) - 1); //Cube Blue
    //There are Y * X * Z LEDs (Size_Cube) in the cube, so when if we write to level (Y) 2, panel (X) 5, row 4 (Z), that needs to be translated into a number from 0 to Size_Cube then devided by the number of bits per byte
    // Since we use an integer we are left with the byte number, the remainder is dropped off. EG (2*64 + 5*8 + 4)/8 = 21.5, the int of this is WhichByte number 21.(Assuming a cube 8*8*8 LEDs)
    int WhichByte = int(((Z << 6) + (Y << 3) + X) >> 3);
    // This next variable is the same thing as before except we don't devide by 8, so we get the LED number 0 to Size_Cube.
    //Then we find which bit of the byte is required. EG (From Above) (2*64 + 5*8 + 4) = LED# 172 - (8 * WhichByte) = 4
    int WhichBit = ((Z << 6) + (Y << 3) + X) - (WhichByte << 3);
    // So WhichByte 21 WhichBit 4 is the location we will write too for LED 172.
    //Now we write to our Cube array if Red, Green and Blue are on or off, using Bit angular modulation.
    for (byte I = 0; I < BAM_RESOLUTION; I++) {
        //*** RED ***
        bitWrite(red[I][WhichByte], WhichBit, bitRead(R, I));
        //*** GREEN ***
        bitWrite(green[I][WhichByte], WhichBit, bitRead(G, I));
        //*** BLUE ***
        bitWrite(blue[I][WhichByte], WhichBit, bitRead(B, I));
    }
} //****LED ROUTINE END****
ISR(TIMER1_COMPA_vect)
{ //***MultiPlex BAM***MultiPlex BAM***MultiPlex BAM***MultiPlex BAM***MultiPlex BAM***MultiPlex BAM***MultiPlex BAM
    //This routine is called in the background automatically at frequency set by OCR1A
    //In this code, I set OCR1A to 30, so this is called every 124us, giving each level in the cube 124us of ON time
    //There are 8 levels, so we have a maximum brightness of 1/8, since the level must turn off before the next level is turned on
    //The frequency of the multiplexing is then 124us*8=992us, or 1/992us= about 1kHz
    PORTE |= 1 << blank_pin; //The first thing we do is turn all of the LEDs OFF, by writing a 1 to the blank pin
    //Note, in my bread-boarded version, I was able to move this way down in the cube, meaning that the OFF time was minimized
    //due to signal integrity and parasitic capcitance, my rise/fall times, required all of the LEDs to first turn off, before updating
    //otherwise you get a ghosting effect on the previous level
    //This is 4 bit 'Bit angle Modulation' or BAM, There are 8 levels, so when a '1' is written to the color brightness,
    //each level will have a chance to light up for 1 cycle, the BAM bit keeps track of which bit we are modulating out of the 4 bits
    //Bam counter is the cycle count, meaning as we light up each level, we increment the BAM_Counter
    if (BAM_Counter == 8)
        BAM_Bit++;
    else if (BAM_Counter == 24)
        BAM_Bit++;
    else if (BAM_Counter == 56)
        BAM_Bit++;

    BAM_Counter++; //Here is where we increment the BAM counter
    switch (BAM_Bit) { //The BAM bit will be a value from 0-3, and only shift out the arrays corresponding to that bit, 0-3
    //Here's how this works, each case is the bit in the Bit angle modulation from 0-4,
    //Next, it depends on which level we're on, so the byte in the array to be written depends on which level, but since each level contains 64 LED,
    //we only shift out 8 bytes for each color
    case 0:
        //Red
        mySPI(red[0][level]);
        mySPI(red[0][level + 1]);
        mySPI(red[0][level + 2]);
        mySPI(red[0][level + 3]);
        mySPI(red[0][level + 4]);
        mySPI(red[0][level + 5]);
        mySPI(red[0][level + 6]);
        mySPI(red[0][level + 7]);

        //Green
        mySPI(green[0][level]);
        mySPI(green[0][level + 1]);
        mySPI(green[0][level + 2]);
        mySPI(green[0][level + 3]);
        mySPI(green[0][level + 4]);
        mySPI(green[0][level + 5]);
        mySPI(green[0][level + 6]);
        mySPI(green[0][level + 7]);
        //Blue
        mySPI(blue[0][level]);
        mySPI(blue[0][level + 1]);
        mySPI(blue[0][level + 2]);
        mySPI(blue[0][level + 3]);
        mySPI(blue[0][level + 4]);
        mySPI(blue[0][level + 5]);
        mySPI(blue[0][level + 6]);
        mySPI(blue[0][level + 7]);
        break;
    case 1:
        //Red
        mySPI(red[1][level]);
        mySPI(red[1][level + 1]);
        mySPI(red[1][level + 2]);
        mySPI(red[1][level + 3]);
        mySPI(red[1][level + 4]);
        mySPI(red[1][level + 5]);
        mySPI(red[1][level + 6]);
        mySPI(red[1][level + 7]);
        //Green
        mySPI(green[1][level]);
        mySPI(green[1][level + 1]);
        mySPI(green[1][level + 2]);
        mySPI(green[1][level + 3]);
        mySPI(green[1][level + 4]);
        mySPI(green[1][level + 5]);
        mySPI(green[1][level + 6]);
        mySPI(green[1][level + 7]);
        //Blue
        mySPI(blue[1][level]);
        mySPI(blue[1][level + 1]);
        mySPI(blue[1][level + 2]);
        mySPI(blue[1][level + 3]);
        mySPI(blue[1][level + 4]);
        mySPI(blue[1][level + 5]);
        mySPI(blue[1][level + 6]);
        mySPI(blue[1][level + 7]);
        break;
    case 2:
        //Red
        mySPI(red[2][level]);
        mySPI(red[2][level + 1]);
        mySPI(red[2][level + 2]);
        mySPI(red[2][level + 3]);
        mySPI(red[2][level + 4]);
        mySPI(red[2][level + 5]);
        mySPI(red[2][level + 6]);
        mySPI(red[2][level + 7]);
        //Green
        mySPI(green[2][level]);
        mySPI(green[2][level + 1]);
        mySPI(green[2][level + 2]);
        mySPI(green[2][level + 3]);
        mySPI(green[2][level + 4]);
        mySPI(green[2][level + 5]);
        mySPI(green[2][level + 6]);
        mySPI(green[2][level + 7]);
        //Blue
        mySPI(blue[2][level]);
        mySPI(blue[2][level + 1]);
        mySPI(blue[2][level + 2]);
        mySPI(blue[2][level + 3]);
        mySPI(blue[2][level + 4]);
        mySPI(blue[2][level + 5]);
        mySPI(blue[2][level + 6]);
        mySPI(blue[2][level + 7]);
        break;
    case 3:
        //Red
        mySPI(red[3][level]);
        mySPI(red[3][level + 1]);
        mySPI(red[3][level + 2]);
        mySPI(red[3][level + 3]);
        mySPI(red[3][level + 4]);
        mySPI(red[3][level + 5]);
        mySPI(red[3][level + 6]);
        mySPI(red[3][level + 7]);
        //Green
        mySPI(green[3][level]);
        mySPI(green[3][level + 1]);
        mySPI(green[3][level + 2]);
        mySPI(green[3][level + 3]);
        mySPI(green[3][level + 4]);
        mySPI(green[3][level + 5]);
        mySPI(green[3][level + 6]);
        mySPI(green[3][level + 7]);
        //Blue
        mySPI(blue[3][level]);
        mySPI(blue[3][level + 1]);
        mySPI(blue[3][level + 2]);
        mySPI(blue[3][level + 3]);
        mySPI(blue[3][level + 4]);
        mySPI(blue[3][level + 5]);
        mySPI(blue[3][level + 6]);
        mySPI(blue[3][level + 7]);
        //Here is where the BAM_Counter is reset back to 0, it's only 4 bit, but since each cycle takes 8 counts,
        //, it goes 0 8 16 32, and when BAM_counter hits 64 we reset the BAM
        if (BAM_Counter == 120) {
            BAM_Counter = 0;
            BAM_Bit = 0;
        }
        break;
    } //switch_case
    // ** This routine selects layer without shift register.
    lastAnode = (anodeLevel - 1);
    if (anodeLevel == 0) {
        lastAnode = 7;
    } // if we are at the bottom, the last layer was the top
    digitalWrite(layerArray[lastAnode], LOW); // turn off the previous layer
    digitalWrite(layerArray[anodeLevel], HIGH); // turn on the current layer
    PORTE |= 1 << latch_pin; //Latch pin HIGH
    PORTE &= ~(1 << latch_pin); //Latch pin LOW
    delayMicroseconds(3); //???;
    PORTE &= ~(1 << blank_pin); //Blank pin LOW to turn on the LEDs with the new data
    //delayMicroseconds(3); //???;
    // Blank is the same as the OE or ENABLE pin
    anodeLevel++; //inrement the anode level
    level = level + 8; //increment the level variable by 8, which is used to shift out data, since the next level woudl be the next 8 bytes in the arrays
    if (anodeLevel == 8) //go back to 0 if max is reached
        anodeLevel = 0;
    if (level == 64) //if you hit 64 on level, this means you just sent out all 63 bytes, so go back
        level = 0;
    pinMode(blank_pin, OUTPUT); //moved down here so outputs are all off until the first call of this function
}
void clearfast()
{
    for (unsigned char j = 0; j < 64; j++) {
        red[0][j] = 0;
        red[1][j] = 0;
        red[2][j] = 0;
        red[3][j] = 0;
        green[0][j] = 0;
        green[1][j] = 0;
        green[2][j] = 0;
        green[3][j] = 0;
        blue[0][j] = 0;
        blue[1][j] = 0;
        blue[2][j] = 0;
        blue[3][j] = 0;
    }
}
inline static uint8_t mySPI(uint8_t mydata)
{
    SPDR = mydata;
    asm volatile("nop");
    asm volatile("nop");
}

IV. GIẢI THÍCH CHƯƠNG TRÌNH

1. INPUT & OUTPUT ARDUINO MEGA 2560

Sơ đồ đấu nối như hình vẽ và RGB LED được dùng là loại Common Anode, điện trở hạn dòng được dùng là R100.

       

2. SƠ LƯỢC VỀ CHƯƠNG TRÌNH

Vậy để điều khiển một đèn, ta cần có 2 thông số: tọa độ và màu, ex: LED (z, y, x, red, green, blue).

2.1. XÁC ĐỊNH TỌA ĐỘ (z, y, x)

WhichByte = int (((z << 6) + (y << 3) + x) >> 3) --> Xác định được byte cần set  --> 74HC595 cần set.

WhichBit = ((z << 6) + (y << 3) + x) - (WhichByte << 3) --> Xác định được bit cần set --> Chân cần set của 74HC595 tương ứng ở trên.

Lấy ví dụ ta cần set led lại tọa độ (z=4, y=5, x=6). Như tính toán trong chương trình, vị trí cần set là byte thứ 37, bit thứ 6 của tọa độ không gian LED CUBE.

WhichByte = int ((4*2^6 + 5*2^3 + 6)/2^3) = 37

WhichBit = (4*2^6 + 5*2^3 + 6) - (37 * 2^3) = 6

Hình trên biểu thị 512 RGB LED với mối liên hệ theo tọa độ không gian, byte và bit. Phần tô xanh, chữ đỏ là vị trí cần set cho RGB LED:

Layer (Z) = 4, Y = 5, X =6 --> LED thứ 302 --> Byte thứ 37, bit thứ 6.

2.2. XÁC ĐỊNH MÀU (red, green, blue)

Lấy ví dụ ta cần set led lại tọa độ (z=4, y=5, x=6) màu (red=3, green=6, blue=13), khi đó chương sẽ biên dịch như sau:

Như phân tích ở trên, ta thấy tại vị trí byte 37, bit thứ 6 (tại mỗi vị trí có 3 led bên trong đèn RGB), các đèn sẽ được phối màu như hình sau:

CÁC LƯU Ý

  • Trục tọa độ X trên hình vẽ biểu thị thời gian LED sáng/ tắt với Time_Base = 125us. Khoảng thời gian của Bam_Bit[0] = 8 x Time_Base = 1,000us (1kHz), của Bam_Bit[1]=16 x Time_Base = 2,000us, của Bam_Bit[2]=32 x Time_Base = 4,000us, của Bam_Bit[3]=64 x Time_Base = 8,000us.
  • Lưu ý các con số BAM_Counter = 8, 24, 56120BAM_Bit++. Cứ mỗi lần TIMER1 interupt (sau mỗi 125us), thì BAM_Counter sẽ tăng giá trị lên 1, đến giá trị 120 thì reset về 0. Mối liên hệ giữa BAM_Counter và BAM_Bit như sau:
    • BAM_Bit = 0, khi BAM_Counter nằm trong dãy: 0 ~ 7.
    • BAM_Bit = 1, khi BAM_Counter nằm trong dãy: 8 ~ 23.
    • BAM_Bit = 2, khi BAM_Counter nằm trong dãy: 24 ~ 55.
    • BAM_Bit = 3, khi BAM_Counter nằm trong dãy: 56 ~ 119.
  • Ở đây, mình không dùng hàm có sẵn trong thư viện SPI: SPI.transfer(mydata) mà tạo ra hàm mySPI(mydata). Lý do là để cải thiện tốc độ hiển thị LED. Sau này, mình dùng một số hiệu ứng phức tạp, nếu dùng hàm trong thư viện sẽ bị flicker (nhấp nháy) và rất chậm, nhìn không mượt mà.

2.3. HÌNH ẢNH THỰC TẾ LED (4, 5, 6, 3, 6, 13)

2.4. MỘT SỐ VIDEO VÀ HÌNH ẢNH CỦA DỰ ÁN

Các hiệu ứng trong Video bên dưới đa số mình dùng ColorWheel. Nếu có thời gian, mình rất vui khi cùng chia sẻ với các bạn ở các phần tiếp theo.

THANKS.heartheartheart.

Youtube: 
[RGB LED CUBE 8x8x8] - PART1- FIRST TEST
[RGB LED CUBE 8x8x8] - PART2- UPDATE #1 ANIMATIONS
[RGB LED CUBE 8x8x8] - PART3 - UPDATE #2 ANIMATIONS
Những hình ảnh về dự án: 
Bài viết truyền cảm hứng: 
lên
18 thành viên đã đánh giá bài viết này hữu ích.
Các dự án được truyền cảm hứng

Select any filter and click on Apply to see results

Các bài viết cùng tác giả

P.I.D - SPEED & POSITION CONTROL

Bộ điều khiển PID là một bộ điều khiển vòng kín được sử dụng rộng rãi trong hệ thống điện, hệ thống tự động, điện tử. Mục tiêu của bộ điều khiển PID là điều chỉnh giá trị điều khiển ở ngõ ra Ouput sao cho sai lệch Error e(t) = (SP – PV) giữa giá trị đo được của hệ thống PV (Process Variable) với giá trị cài đặt SP (SetPoint) nhỏ nhất có thể (

lên
13 thành viên đã đánh giá bài viết này hữu ích.

ĐIỀU KHIỂN BẢNG LED BICOLOR 32x32 BẰNG B.A.M

Về LED ma trận, mình thấy trên diễn đàn cũng đã có rất nhiều bài viết. Để tiếp tục chuỗi bài về B.A.M, mình sẽ chia sẻ các bạn cách để Bicolor Led Matrix có thể hiển thị 256 màu riêng biệt với phương pháp B.A.M – 4 bit, tất nhiên nếu mắt người có thể phân biệt được sự khác biệt này...

lên
4 thành viên đã đánh giá bài viết này hữu ích.