สวัสดีชาว Maker และสาย Tech ทุกคนครับ! วันนี้เรามีโปรเจกต์สุดเนิร์ดและน่าตื่นเต้นมานำเสนอ นั่นคือการสร้าง "Pico VGA Board"
หัวใจหลักของโปรเจกต์นี้คือการจับคู่บอร์ด Raspberry Pi PICO W เข้ากับพอร์ต D-SUB15 (พอร์ต VGA สมัยก่อน) เพื่อเนรมิตให้จอมอนิเตอร์ TFT ธรรมดาๆ กลายเป็นจอแสดงผลขนาดใหญ่ที่คุณจะเอาไปรันกราฟิก โชว์ข้อมูล หรือแม้แต่เล่นเกมเรโทรก็ทำได้สบายๆ!
โปรเจกต์นี้ดัดแปลงมาจาก VGA Library ดั้งเดิม ที่ออกแบบมาสำหรับจอ CRT จอตู้สมัยก่อน แต่ผู้สร้างได้ปรับจูนโค้ดใหม่ให้สามารถใช้งานร่วมกับจอ LCD VGA ในยุคปัจจุบันได้อย่างลื่นไหล ว่าแล้วเราไปดูสเตปการสร้างและโค้ดของโปรเจกต์นี้กันเลยครับ!
ไอเทมที่ต้องมี (Supplies)
เตรียมอุปกรณ์บนโต๊ะช่างของคุณให้พร้อมตามลิสต์นี้เลยครับ:
- แผ่นวงจร Custom PCB Board
- พอร์ต D-SUB 15 VGA Connector
- DC Barrel jack (พอร์ตเสียบอะแดปเตอร์)
- ตัวต้านทาน 330 Ohms (แพ็กเกจ 1206)
- ตัวต้านทาน 10k (แพ็กเกจ 0805)
- ตัวต้านทาน 1K (แพ็กเกจ 1206)
- LED สีเขียว (แพ็กเกจ 0603)
- พินเฮดเดอร์ตัวเมีย (Female header Pins CON20 x 2)
- บอร์ด PICO 1 หรือ PICO W (ชิป RP2040 ตัวไหนก็ได้)
💡 Maker's Tip: เห็นลิสต์ชิ้นส่วนอิเล็กทรอนิกส์แล้วอยากเริ่มทำเลยใช่ไหม? ถ้าคุณไม่อยากเปลือยบอร์ดให้ฝุ่นเกาะ หรืออยากทำเคสสไตล์เรโทรแบบเครื่องเกมคอนโซล แนะนำให้ใช้ เครื่อง 3D Printer ปริ้นท์เคสครอบบอร์ดดูสิครับ!
หากเพื่อนๆ กำลังมองหา บอร์ด Raspberry Pi Pico, ชิ้นส่วนอิเล็กทรอนิกส์, และเส้นพลาสติก 3D Print (Filament) คุณภาพสูง แวะไปช้อปปิ้งของแท้พร้อมส่งได้ที่ Globalbyte เลยครับ พร้อมลุยทุกโปรเจกต์แน่นอน!
Step 1: พอร์ต VGA มันทำงานยังไง? (How VGA Port Works)
ก่อนจะเริ่มทำ เรามาเข้าใจหลักการของ VGA (Video Graphics Array) กันก่อนครับ มันคืออินเทอร์เฟซแบบอนาล็อกที่ต่างจาก HDMI หรือ DisplayPort ที่ส่งข้อมูลเป็นแพ็กเกจดิจิทัล เพราะ VGA จะส่ง "แรงดันไฟฟ้า (Voltages)" อย่างต่อเนื่องเพื่อบอกสีและจังหวะเวลาครับ
VGA ใช้สายสัญญาณอนาล็อก 3 เส้นหลักคือ แดง (Red), เขียว (Green), และ น้ำเงิน (Blue) โดยแต่ละเส้นจะรับแรงดันตั้งแต่ 0 ถึง 0.7V
- ที่ 0V คือไม่มีแสง (มืดสนิท)
- ที่ 0.7V คือสว่างเต็มที่
- ถ้าเส้นสีแดงรับไฟ 0.7V ส่วนสีอื่น 0V จอจะแสดงสีแดงล้วน แต่ถ้ามา 0.7V พร้อมกันสามเส้น จอจะเป็นสีขาวครับ!
นอกจากนี้ยังมีสัญญาณดิจิทัลอีก 2 เส้นคือ HSYNC (Horizontal Sync) บอกจอว่าให้เริ่มวาดพิกเซลบรรทัดใหม่ และ VSYNC (Vertical Sync) บอกว่าให้เริ่มวาดเฟรมใหม่ ซึ่งกระบวนการนี้เกิดขึ้นราวๆ 60 ครั้งต่อวินาที (60 Hz) ครับ (พอร์ต VGA 15 พิน จะใช้ Pin 1-3 เป็น RGB, Pin 13 เป็น HSYNC และ Pin 14 เป็น VSYNC)
Step 2: จัดการ PICO VGA Library
โปรเจกต์นี้ใช้ Library ที่ดัดแปลงมาจากต้นฉบับ เพื่อให้มันเข้ากันได้กับจอ LCD ยุคใหม่ คุณสามารถเข้าไปดาวน์โหลดและติดตั้ง Custom Library ฉบับสมบูรณ์ได้จาก GitHub ลิงก์นี้เลยครับ: PICO-VGA-BOARD GitHub
Step 3: การเชื่อมต่อ D-SUB15 กับ Pico เบื้องต้น
ผังการต่อสายเข้ากับบอร์ด Raspberry Pi Pico มีดังนี้ครับ:
-
Pin 1 (Red) ต่อเข้า
GPIO18 ขนานด้วยตัวต้านทาน 1kΩ 4 ตัว
-
Pin 2 (Green) ต่อเข้า
GPIO19 ขนานด้วยตัวต้านทาน 1kΩ 4 ตัว
-
Pin 3 (Blue) ต่อเข้า
GPIO20 ขนานด้วยตัวต้านทาน 1kΩ 4 ตัว
- (ค่าความต้านทานจะเป็นตัวกำหนดความสว่าง ยิ่งโอห์มสูงภาพยิ่งมืด แนะนำให้รวมแล้วอยู่ที่ 200–400Ω)
-
Pin 13 (HSYNC) ต่อเข้า
GPIO16 และ Pin 14 (VSYNC) ต่อเข้า GPIO17
- Pin 5, 6, 7, 8 และ 10 ต่อลง
GND ให้หมด
Step 4: ทดสอบบน Breadboard Demo
ก่อนจะทำบอร์ดจริง เราเอาของมาเสียบสายจัมเปอร์บน Breadboard ดูก่อนครับ แล้วอัปโหลดโค้ด "RobCo Terminal" ซึ่งได้แรงบันดาลใจจากเทอร์มินัลสีเขียวสุดคลาสสิกในเกม Fallout! ลองก๊อปปี้โค้ดด้านล่างไปรันดูได้เลยครับ
#include "vga_graphics.h"
// ---------------- CONFIG ----------------
#define BORDER_THICKNESS 4
#define BORDER_MARGIN 14
// Fallout (big text)
#define BIG_CHAR_W 12
#define BIG_CHAR_H 16
// Linux (small text)
#define SMALL_CHAR_W 6
#define SMALL_CHAR_H 8
// Cursor
bool cursor_visible = true;
unsigned long last_blink = 0;
// ---------------- CURSORS ----------------
int big_col = 0;
int big_row = 0;
int small_col = 0;
int small_row = 18; // start lower on screen
// ---------------- BORDER ----------------
void drawBorder() {
for (int t = 0; t < BORDER_THICKNESS; t++) {
drawHLine(t, t, screenWidth - 2*t, GREEN);
drawHLine(t, screenHeight - 1 - t, screenWidth - 2*t, GREEN);
drawVLine(t, t, screenHeight - 2*t, GREEN);
drawVLine(screenWidth - 1 - t, t, screenHeight - 2*t, GREEN);
}
}
// ---------------- TEXT HELPERS ----------------
void big_print(const char* s) {
while (*s) {
if (*s == '\n') {
big_col = 0;
big_row++;
} else {
drawChar(
BORDER_MARGIN + big_col * BIG_CHAR_W,
BORDER_MARGIN + big_row * BIG_CHAR_H,
*s,
GREEN,
BLACK,
2
);
big_col++;
}
s++;
}
}
void small_print(const char* s) {
while (*s) {
if (*s == '\n') {
small_col = 0;
small_row++;
} else {
drawChar(
BORDER_MARGIN + small_col * SMALL_CHAR_W,
BORDER_MARGIN + small_row * SMALL_CHAR_H,
*s,
GREEN,
BLACK,
1
);
small_col++;
}
s++;
}
}
// ---------------- BLINKING CURSOR ----------------
void draw_cursor() {
int x = BORDER_MARGIN + big_col * BIG_CHAR_W;
int y = BORDER_MARGIN + big_row * BIG_CHAR_H;
if (cursor_visible) {
fillRect(x, y + BIG_CHAR_H - 2, BIG_CHAR_W, 2, GREEN);
} else {
fillRect(x, y + BIG_CHAR_H - 2, BIG_CHAR_W, 2, BLACK);
}
}
// ---------------- SETUP ----------------
void setup() {
initVGA();
clearScreen();
drawBorder();
// Fallout header (BIG)
big_print("ROBCO INDUSTRIES UNIFIED OS\n");
big_print("COPYRIGHT 2075-2077 ROBCO\n");
big_print("- VAULT TERMINAL -\n\n");
big_print("> INITIALIZING SYSTEM...\n");
big_print("> MEMORY BANKS: OK\n");
big_print("> VGA INTERFACE: OK\n");
big_print("> RP2040 STATUS: NOMINAL\n\n");
big_print("> ");
// Linux / RPi info (SMALL)
small_print("pico@rp2040:~$ neofetch\n\n");
small_print("OS: Pico Linux (demo)\n");
small_print("Host: Raspberry Pi Pico W\n");
small_print("Kernel: 0.1-retro\n");
small_print("Uptime: just now\n");
small_print("Resolution: 640x480 VGA\n");
small_print("Shell: bash (fake)\n");
}
// ---------------- LOOP ----------------
void loop() {
unsigned long now = millis();
if (now - last_blink > 500) {
last_blink = now;
cursor_visible = !cursor_visible;
draw_cursor();
}
}
สรุปการทำงานของโค้ด: เราใช้ initVGA() เปิดการเชื่อมต่อจอ กำหนดขนาดฟอนต์ (Scale 1 และ 2) และวาดกรอบสีเขียวด้วยฟังก์ชัน drawBorder() โค้ดส่วนใหญ่จะอยู่ในฟังก์ชัน setup() เพื่อให้พิมพ์ข้อความโชว์ตอนแรก ส่วนใน loop() จะปล่อยให้ตัว Cursor กระพริบสลับไปมาครับ
Step 5: ออกแบบ PCB (PCB DESIGN)
เมื่อ Breadboard ผ่านฉลุย ก็เข้าสู่การทำ PCB ครับ เราเดินวงจรเชื่อม VGA เข้ากับ Pico และใส่ตัวต้านทาน 330Ω จำนวน 3 ตัวขนานกันในแต่ละเส้นสี (RGB) เพื่อให้ปรับค่าความสว่างได้ง่ายๆ นอกจากนี้ยังเพิ่ม IC LM317 (ตัวแปลงแรงดันไฟฟ้า) เพื่อแปลงไฟจากอะแดปเตอร์ 12V ให้เป็นไฟ 5V นิ่งๆ ไปเลี้ยงบอร์ด Pico ด้วยครับ (ดู Datasheet ของ LM317 ที่นี่)
Step 6-8: สั่งผลิต PCB, เช็ค DFM, และประกอบวงจร!
ผู้สร้างใช้บริการจาก NextPCB ในการสั่งทำแผ่น PCB สีแดงสุดจี๊ด และใช้เครื่องมือ HQDFM (โปรแกรมตรวจสอบ Design for Manufacturing) เพื่อเช็คก่อนว่าแผ่นวงจรที่วาดมามีจุดผิดพลาดตรงไหนก่อนส่งโรงงานหรือไม่ ถือเป็นทริคที่ดีมากๆ สำหรับการทำโปรดักชันครับ
ส่วนการประกอบ (PCB Assembly) ก็เริ่มตั้งแต่การหยอดตะกั่วเหลว (Solder paste) วางชิ้นส่วน SMD ลงไป แล้วเอาไปวางบน Hotplate เพื่อละลายตะกั่ว จากนั้นก็บัดกรีพอร์ต VGA, พอร์ตไฟ และประกอบบอร์ด Pico ลงไปเป็นอันเสร็จ!
Step 9-10: เสียบปลั๊ก ลุย Demo 1 - Hello World
เตรียมสาย VGA เสียบเข้ากับจอมอนิเตอร์ขนาด 24 นิ้ว แล้วจ่ายไฟ 12V ผ่านแจ็ค DC เรามาทดสอบด้วยโค้ด "Hello World" แบบมีเอฟเฟกต์ CRT Scanline ให้ดูเหมือนจอตู้สมัยก่อนกันครับ!
#include "vga_graphics.h"
#include
/* ── Screen ───────────────── */
#define SW 640
#define SH 480
/* ── Text scale ──────────── */
#define SCALE 4
/* ── Centered print ───────── */
void printCentered(const char* text, int y, char color) {
int len = strlen(text);
int textWidth = len * 6 * SCALE;
int x = (SW - textWidth) / 2;
for (int i = 0; i < len; i++) {
drawChar(x + i * 6 * SCALE, y, text[i], color, BLACK, SCALE);
}
}
/* ── CRT Scanlines ───────── */
void drawScanlines() {
for (int y = 0; y < SH; y += 2) {
drawHLine(0, y, SW, BLACK); // dark line every other row
}
}
/* ── Setup ─────────────── */
void setup() {
initVGA();
clearScreen();
int lineHeight = 8 * SCALE;
int totalHeight = lineHeight * 3 + 20; // spacing included
int startY = (SH - totalHeight) / 2;
// Line 1 (RED)
printCentered("HELLO WORLD 1", startY, RED);
// Line 2 (GREEN)
printCentered("HELLO WORLD 2", startY + lineHeight + 10, GREEN);
// Line 3 (BLUE)
printCentered("HELLO WORLD 3", startY + (lineHeight + 10) * 2, BLUE);
// CRT overlay
drawScanlines();
}
/* ── Loop ─────────────── */
void loop() {
}
Step 11: Demo 2 - เกมงูในตำนาน (Snake Game)
ลองเอาโค้ดเกมงู (Snake Game Console) มาดัดแปลงลงบอร์ด Pico ดูครับ ในเวอร์ชันนี้เราเขียนให้มันเป็น AI งูเลื้อยสุ่มหาอาหารเองไปเรื่อยๆ (แต่ถ้าใครอยากต่อปุ่มบังคับเองก็ทำได้นะ)
#include "vga_graphics.h"
/* ── Screen ───────────────── */
#define SW 640
#define SH 480
/* ── Grid ───────────────── */
#define CELL 12
#define GRID_W (SW / CELL)
#define GRID_H (SH / CELL)
/* ── Snake ─────────────── */
struct Segment {
int x;
int y;
};
Segment snake[200];
int snakeLength = 5;
int dx = 1, dy = 0;
int foodX, foodY;
int prevTailX, prevTailY;
/* ── Random food ───────── */
void placeFood() {
foodX = random(2, GRID_W - 2);
foodY = random(2, GRID_H - 2);
}
/* ── Reset ─────────────── */
void resetGame() {
clearScreen();
snakeLength = 5;
dx = 1; dy = 0;
for (int i = 0; i < snakeLength; i++) {
snake[i].x = GRID_W / 2 - i;
snake[i].y = GRID_H / 2;
}
placeFood();
}
/* ── Random movement ───── */
void randomMove() {
if (random(0, 10) < 3) { // 30% chance
int dir = random(0, 4);
if (dir == 0 && dy == 0) { dx = 0; dy = -1; } // up
if (dir == 1 && dy == 0) { dx = 0; dy = 1; } // down
if (dir == 2 && dx == 0) { dx = -1; dy = 0; } // left
if (dir == 3 && dx == 0) { dx = 1; dy = 0; } // right
}
}
/* ── Collision ─────────── */
bool selfCollision() {
for (int i = 1; i < snakeLength; i++) {
if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) {
return true;
}
}
return false;
}
/* ── Draw one cell ─────── */
void drawCell(int gx, int gy, char color) {
fillRect(gx * CELL, gy * CELL, CELL - 1, CELL - 1, color);
}
/* ── Setup ─────────────── */
void setup() {
initVGA();
clearScreen();
randomSeed(analogRead(26)); // Pico randomness
resetGame();
}
/* ── Loop ─────────────── */
void loop() {
// random AI movement
randomMove();
// store tail
prevTailX = snake[snakeLength - 1].x;
prevTailY = snake[snakeLength - 1].y;
// move body
for (int i = snakeLength - 1; i > 0; i--) {
snake[i] = snake[i - 1];
}
// move head
snake[0].x += dx;
snake[0].y += dy;
// wrap screen
if (snake[0].x >= GRID_W) snake[0].x = 0;
if (snake[0].x < 0) snake[0].x = GRID_W - 1;
if (snake[0].y >= GRID_H) snake[0].y = 0;
if (snake[0].y < 0) snake[0].y = GRID_H - 1;
// self collision
if (selfCollision()) {
delay(500);
resetGame();
return;
}
// food
if (snake[0].x == foodX && snake[0].y == foodY) {
snakeLength++;
placeFood();
}
// erase tail
drawCell(prevTailX, prevTailY, BLACK);
// draw snake
for (int i = 0; i < snakeLength; i++) {
drawCell(snake[i].x, snake[i].y, GREEN);
}
// draw food
drawCell(foodX, foodY, WHITE);
delay(120);
}
Step 12: Demo 3 - Conway's Game of Life
แถมอีกหนึ่งเดโมสุดคลาสสิก นั่นคือ Game of Life ซึ่งเป็นการจำลองวิวัฒนาการการเกิด-ดับของเซลล์ (จุดสีเหลือง) ตามเงื่อนไขทางคณิตศาสตร์ครับ
#include "vga_graphics.h"
/* ── Screen ───────────────── */
#define SW 640
#define SH 480
/* ── Grid ───────────────── */
#define CELL 8
#define GRID_W (SW / CELL)
#define GRID_H (SH / CELL)
/* ── Buffers ───────────── */
bool grid[GRID_W][GRID_H];
bool nextGrid[GRID_W][GRID_H];
/* ── Draw cell ─────────── */
void drawCell(int x, int y, bool alive) {
fillRect(
x * CELL,
y * CELL,
CELL - 1,
CELL - 1,
alive ? YELLOW : BLACK
);
}
/* ── Random init ───────── */
void randomizeGrid() {
for (int x = 0; x < GRID_W; x++) {
for (int y = 0; y < GRID_H; y++) {
grid[x][y] = random(0, 2);
drawCell(x, y, grid[x][y]);
}
}
}
/* ── Count neighbors ───── */
int countNeighbors(int x, int y) {
int count = 0;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
int nx = x + dx;
int ny = y + dy;
// wrap around edges
if (nx < 0) nx = GRID_W - 1;
if (nx >= GRID_W) nx = 0;
if (ny < 0) ny = GRID_H - 1;
if (ny >= GRID_H) ny = 0;
if (grid[nx][ny]) count++;
}
}
return count;
}
/* ── Update simulation ─── */
void updateGrid() {
for (int x = 0; x < GRID_W; x++) {
for (int y = 0; y < GRID_H; y++) {
int neighbors = countNeighbors(x, y);
if (grid[x][y]) {
// alive
nextGrid[x][y] = (neighbors == 2 || neighbors == 3);
} else {
// dead
nextGrid[x][y] = (neighbors == 3);
}
}
}
// apply + redraw only changes
for (int x = 0; x < GRID_W; x++) {
for (int y = 0; y < GRID_H; y++) {
if (grid[x][y] != nextGrid[x][y]) {
drawCell(x, y, nextGrid[x][y]);
}
grid[x][y] = nextGrid[x][y];
}
}
}
/* ── Setup ─────────────── */
void setup() {
initVGA();
clearScreen();
randomSeed(analogRead(26)); // Pico randomness
randomizeGrid();
}
/* ── Loop ─────────────── */
void loop() {
updateGrid();
delay(80); // speed control
}
บทสรุป (Conclusion)
เป็นไงบ้างครับกับ Pico VGA Board 1.0! โปรเจกต์นี้ตอบโจทย์มากสำหรับใครที่อยากเอาบอร์ดไมโครคอนโทรลเลอร์ตัวเล็กๆ ไปต่อขึ้นจอใหญ่ๆ เพื่อทำ Sensor Interface, Dashboard หรือแม้แต่เครื่องเกมคอนโซลทำเอง เพียงแค่ลง Library ที่แจกไว้ให้ แล้วประกอบวงจรตามนี้ คุณก็สามารถเนรมิตทุกอย่างออกจอมอนิเตอร์ได้ง่ายๆ เลยครับ!
*คำเตือน: เนื้อหานี้เป็นการสรุปและเรียบเรียงแนวคิดจากบทความโปรเจกต์ต้นฉบับภาษาอังกฤษ ข้อมูลฉบับภาษาไทยและกระบวนการทำงานบางส่วนอาจถูกปรับให้เข้าใจง่ายและเห็นภาพรวมชัดเจนขึ้น โปรดตรวจสอบรายละเอียดเชิงวิศวกรรมการต่อวงจรและการเชื่อมต่อพอร์ตอย่างรัดกุมได้ที่
เว็บไซต์ต้นฉบับ ก่อนนำไปทดลองจริง