Chú ý: Các bạn đọc hiểu tất cả các bài trước thì đọc bài này mới hiểu nhé
Từ đầu tutorial tới giờ, chúng ta đã điều khiển led matrix ở dạng mono ( tức 1 bit màu – chỉ có trạng thái bật hoặc tắt các pixel) để điều khiển được nhiều màu sắc hơn chúng ta phải điều chế độ rộng xung trên từng con led, để làm được việc này, về cơ bản phải liên tục bơm 1 lượng rất lớn dữ liệu vào ma trận led
Thông thường, người ta sử dụng số bit pwm để mô tả số mức sáng khác nhau của đèn led. Ví dụ 1bit là chỉ có 2 mức ON OFF, 2 bit thì sẽ tạo ra 4 mức sáng, n bít thì tạo ra 2 mũ n mức sáng
Với phương pháp điều chế độ rộng xung thông thường việc tạo ra nhiều hơn 5 bit pwm là bất khả thi do số lượng dữ liệu cần bơm vào led matrix là rất lớn, do vậy mình đã giới thiệu phương pháp điều chế mã nhị phân để có thể giảm đi rất nhiều data cần gửi
Thực hành trên KIT Clock STM32F103 và led ma trận FULL P5
Bạn nào không có KIT clock P5 mà mình giới thiệu thì có thể tự mua kit stm32 blue pill rồi nối dây tương tự nhé
STM32F103C8 | HUB75D |
A2 | R1 |
A3 | G1 |
A4 | B1 |
A5 | R2 |
A6 | G2 |
A7 | B2 |
B0 | CLK |
B1 | A |
B2 | D |
B10 | C |
B11 | OE |
B12 | LAT |
B14 | B |
Module led P5 FULL hoạt động tương tự như P10 FULL, chỉ là hệ số quét sẽ là 1/16 chứ không phải 1/8 nữa ( có thêm chân D)
Khởi tạo code với cube mx
Enable thạch anh ngoài trong RCC và chọn tốc độ tối đa 72Mhz nhé
Kích hoạt timer 2 chanel 4 PWM Generation để tạo xung PWM vào chân OE, Cài đặt Perscaler là 0 hoặc 1 để có tần số pwm lớn nhất, Counter Period chính là giải độ sáng lớn nhất của chúng ta, mình để là 400
Tiếp tục bật thêm Timer 4 để phục vụ quét LED, kích hoạt chi phép ngắt, còn 2 thông số Perscaler và Counter Period lát nữa tính toán rồi sửa trong code sau
Viết chương trình
Theo nguyên tắc của B.A.M chúng ta cần 1 mảng lưu giá trị thời gian chênh lệch giữa mỗi lần ngắt quét led, khác với các chip AVR,PIC, dòng stm32 hỗ trợ rất nhiều hệ số chia cho timer nên chúng ta có thể căn thời gian ngắt của timer qua 2 cách :
- Thay đổi hệ số chia qua thanh ghi PSC
- Thay đổi giá trị tràn ở thanh ghi ARR
Mình sẽ thay đổi thời gian xảy ra ngắt bằng cách nạp lại hệ số chia vào thanh ghi PSC, do vậy mình sẽ tạo 1 mảng chứa 5 hệ số chia cho 5 bit pwm
1 |
unsigned int const timer_clock16S[5]={18,36,72,144,288}; |
Nếu các bạn giảm nhỏ hệ số chia này xuống để thời gian xảy ra ngắt diễn ra nhanh hơn, tần số quét led của led sẽ tăng và khi chiếu camera điện thoại sẽ càng giảm nhấp nháy, nhưng điều đó sẽ làm hao phí thời gian xử lí của vi điều khiển hơn vì phải ngắt quét led nhiều hơn
Đối với led ma trận bị điều khiển bởi pwm, tần số quét để mắt người nhìn ổn là 60Hz, nhưng để camera điện thoại quay không bị nháy thì phải đảm bảo > 1Khz
Bây giờ chúng ta cần 1 bộ đệm để lưu giá trị màu sắc, như đã nói ở trên mình sẽ tạo ra 5bit màu cho mỗi thành phần màu, tức số mà tối đa trên lí thuyết có thể tạo ra được là 2 mũ 5 mũ 3 = 32768 màu. Module led ma trận P5 có độ phân giải 64×32, vậy mình sẽ tạo ra buffer 3 chiều như sau:
1 |
unsigned char BufferRGB[3][32][64]; |
Với 64 led chiều ngang và 32 led chiều dọc cùng với 3 thành phần màu RGB, mỗi pixel sẽ chiếm 3byte, mỗi byte tương ứng 1 thành phần màu ( thực ra bắn 5bit pwm nên chỉ có 5 bit có ích trên mỗi byte thôi, mà kệ RAM nhiều mà :v)
Với 5 bit màu và tỉ số scan là 16s, chúng ta cần 5×16 = 80 lần ngắt để quét hết 1 bảng led
Cách quét
Có 2 cách quét bảng led
- Mỗi khi bật sáng 1 hàng thì chúng ta quét hết 5 bit pwm rồi mới chuyển hàng
- Chúng ta chuyển hết 16 hàng rồi mới chuyển sang quét bit pwm mới
Cách 1 màu sắc sẽ chuẩn hơn, nhưng khi quay camera sẽ nhấp nháy hơn
Cách 2 quay camera đỡ nháy hơn nhưng màu sắc bị sai đi chút
Ở đây mình dùng cách 1, đi qua hết 5 bit pwm rồi mới chuyển hàng mới
Cách gửi data ra các chân R1 R2 B1 B2 G1 G2
Với các dòng led 1 màu, chỉ có 1 chân data, chúng ta có thể dùng bộ SPI cứng để đẩy data cho nhanh, nhưng với ledd full thì có tận 6 chân data nên không có bộ SPI cứng nào đủ đáp ứng, vì vậy chúng ta phải gửi data bằng cách ghi mức logic vào các chân GPIO.
Ở bài 10 mình ghi data như này
Nếu làm như này thì mỗi lần ngắt truyền thì vi điều khiển phải thực hiện rất nhiều phép tính và phải ghi vào từng chân GPIO rất mất thời gian. Để đẩy nhanh tốc độ ghi data ra thì mình sẽ tính sẵn các dữ liệu đó rồi bỏ vào 1 buffer, khi ngắt xảy ra chỉ cần truyền dữ liệu đã được tính toán sẵn đi thôi. Mình cũng cố tình nối các chân RGB của led matrix vào chung 1 port, đẩy data 1 phát ra cả PORT thì nhanh hơn là ghi từng pin nhiều
Vậy, buff để lưu các dữ liệu này tạm gọi là bufferGPIO
1 |
unsigned char bufferGPIO[5][16][64] |
Giải thích: Do 1 lần xuất vào 6 chân data mà 1 byte có 8bit nên cứ 1 byte dữ liệu chỉ có 6bit có ích ( 2byte dư), chiều thứ 1 [5] đại diện cho 5 bit PWM, chiều thứ 2 [16] đại diện cho hệ số scan là 16 và [64] tức chiều ngang của bảng led 64
Chúng ta sẽ cần 1 hàm để chuyển đổi dữ liệu từ dạng RGB sang buffer cho GPIO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void update() { for(int hang=0;hang<16;hang++) { for(int bit_pos=0;bit_pos<5;bit_pos++) { for(int i=0;i<64;i++) { bufferGPIO[bit_pos][hang][i]=0; if((BufferRGB[1][16+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<G2);} //ghi bit G2 if((BufferRGB[1][0+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<G1);} //ghi bit G1 if((BufferRGB[0][0+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<R1);} //ghi bit R1 if((BufferRGB[2][0+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<B1);} //ghi bit B1 if((BufferRGB[0][16+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<R2);} //ghi bit R2 if((BufferRGB[2][16+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<B2);} //ghi bit B2 } } } } |
Như vậy mỗi lần có vẽ gì lên màn hình thì phải gọi hàm này để update hiển thị
Chương trình chọn hàng được sáng
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void scan16S(char line) { switch(line) { case 0: Control_P->BSRR= A_RSET|B_RSET|C_RSET|D_RSET; return; case 1: Control_P->BSRR= A_RSET|B_RSET|C_RSET|D_SSET; return; case 2: Control_P->BSRR= A_RSET|B_RSET|C_SSET|D_RSET; return; case 3: Control_P->BSRR= A_RSET|B_RSET|C_SSET|D_SSET; return; case 4: Control_P->BSRR= A_RSET|B_SSET|C_RSET|D_RSET; return; case 5: Control_P->BSRR= A_RSET|B_SSET|C_RSET|D_SSET; return; case 6: Control_P->BSRR= A_RSET|B_SSET|C_SSET|D_RSET; return; case 7: Control_P->BSRR= A_RSET|B_SSET|C_SSET|D_SSET; return; case 8: Control_P->BSRR= A_SSET|B_RSET|C_RSET|D_RSET; return; case 9: Control_P->BSRR= A_SSET|B_RSET|C_RSET|D_SSET; return; case 10:Control_P->BSRR= A_SSET|B_RSET|C_SSET|D_RSET; return; case 11:Control_P->BSRR= A_SSET|B_RSET|C_SSET|D_SSET; return; case 12:Control_P->BSRR= A_SSET|B_SSET|C_RSET|D_RSET; return; case 13:Control_P->BSRR= A_SSET|B_SSET|C_RSET|D_SSET; return; case 14:Control_P->BSRR= A_SSET|B_SSET|C_SSET|D_RSET; return; case 15:Control_P->BSRR= A_SSET|B_SSET|C_SSET|D_SSET; return; } } |
Mẹo nhỏ: Các bạn không nên quét từ hàng 0 đến hàng 15 mà quét theo thứ tự như này sẽ đỡ nháy hơn 0,8,4,12, 2,10,6,14, 1,9,5,13, 3,11,7,15,
1 2 3 4 |
const unsigned char scan_data[16]= { 0,8,4,12, 2,10,6,14, 1,9,5,13, 3,11,7,15, }; |
Chương trình ngắt quét led
Nhiệm vụ của chương trình ngắt là bắn data ra theo pwm và chuyển hàng mới ( gọi hàm này trong callback của timer)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void ngatquetled(void) { TIM4 -> PSC = timer_clock[vitri_bit]; // nap gia tri chia moi TIM4->EGR = 1; //set EGR len 1 de load lai PSC TIM4 ->SR = 0xFFFFFFFE; //xoa co ngat __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_4,0); //tắt hết LED for(int i=0;i<64;i++){data_PORT->ODR=bufferGPIO[vitri_bit][i];clk_P->BSRR=(1<<(clk+16));clk_P->BSRR=(1<<clk);} //gửi data đi scan16S(scan_data[hang_led]); //chọn hàng Control_P->BSRR=(1<<LAT); // chot data Control_P->BSRR=(1<<(LAT+16)); __HAL_TIM_SET_COMPARE(&htim2,TIM_CHANNEL_4,dosang); //bật LED vitri_bit++; if(vitri_bit==5){vitri_bit=0;hang_led++;if(hang_led==16)hang_led=0;} } |
Giải thích: Đầu chương trình mình sẽ nạp hệ số chia mới để căn chỉnh pwm, trước khi gửi data LED mình tắt hết led để đảm bảo không bị nhiễu ( hiện tượng bóng ma) sau đó gửi data trong bộ đệm GPIO ra PORT, bật sáng hàng và chốt data, trả độ sáng màn hình về ban đầu,
1 số định nghĩa và khai báo biến
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#define LAT 12 //B #define OE 11 //B #define clk 0 //B #define D 1 //B #define C 14 //B #define B 10 //B #define A 2 //B #define A_SSET 0x00000001<<A #define B_SSET 0x00000001<<B #define C_SSET 0x00000001<<C #define D_SSET 0x00000001<<D #define A_RSET 0x00000001<<(A+16) #define B_RSET 0x00000001<<(B+16) #define C_RSET 0x00000001<<(C+16) #define D_RSET 0x00000001<<(D+16) #define B2 7 //a6 #define R2 5 //a4 #define G2 6 //a5 #define B1 4 //a3 #define R1 2 //a1 #define G1 3 //a2 #define OE_P GPIOB #define xuat_P GPIOB #define clk_P GPIOB #define Control_P GPIOB #define data_PORT GPIOA unsigned char vitri_bit=0,hang_led=0; uint16_t dosang=100; |
Để căn chỉnh tần số quét, các bạn nạp giá trị phù hợp vào Counter Period của TIM4 ( ngắt quét led). Như trong demo này mình để Period là 40
DEMO hiện ảnh màu 5 bit
Dùng phần mềm chuyển ảnh thành mã hex dạng RGB tại đây
Dùng photoshop hoặc pain cắt bớt ảnh xuống còn 64×32 pixel
Copy dữ liệu đưa vào bô đệm RGB và gọi hàm update để chuyển đổi sang bộ đệm GPIO
Trong hàm update có dòng: if((BufferRGB[1][16+hang] [i] & (1<<ts)) != 0 ){bufferGPIO[bit_pos][hang][i]|=(0x0001<<G2);}, vậy cái "ts" là gì vậy ạ?
Với lại trong hàm ngatquetled, có lệnh: for(int i=0;iODR=bufferGPIO[vitri_bit][i];clk_P->BSRR=(1<BSRR=(1<<clk);}, vì sao mảng bufferGPIO[][][] có 3 chiều nhưng trong này a chỉ truyền vào 2 chiều thôi ạ?
Cùng chung thắc mắc
hic ! lúc copy lên chắc có lỗi 1 chút
1. ts các bạn sửa thành bit_pos
2. sửa thành data_PORT->ODR=bufferGPIO[vitri_bit][scan_data[hang_led]][i]
Chào anh, em thắc mắc ko biết đoạn
“Copy dữ liệu đưa vào bô đệm RGB” nghĩa là sao ạ
Anh ơi sao con stm32 của em không có chân B2 ạ