Điều khiển TV bằng điện thoại qua WiFi (học lệnh từ remote cũ)
Biến điện thoại thành remote TV, đầu thu, quạt, điều hòa đời cũ… dùng hồng ngoại. Mạch học lệnh từ remote gốc, sau đó bấm trên web điện thoại (không cần cài app). Tầm phát 6–8m nhờ 2 LED hồng ngoại có transistor khuếch dòng.
✅ Đã kiểm thử
Xem chi tiết 10 phép đo
Đang phát lệnh (chân GPIO4 mức cao — trường hợp xấu nhất)
- Q1: dẫn 77.4mA, Vce=0.08V (bão hòa tốt).
- D1: dòng 77.4mA (cho phép 100.0mA).
- D2: dòng 77.4mA (cho phép 100.0mA).
- Chân GPIO4 cấp 2.4mA (an toàn ≤ 20.0mA).
- Tổng dòng đỉnh từ USB 331.3mA (đủ với cục sạc 5V 1A).
- Dòng qua LED hồng ngoại (cần 50–100mA để phát xa 6–8m): 77.4mA ✓
- Dòng chân GPIO4 phải cấp: 2.4mA ✓
- Áp tín hiệu mắt thu vào ESP32: 3.16V ✓
Nghỉ (không phát)
- Tổng dòng đỉnh từ USB 251.4mA (đủ với cục sạc 5V 1A).
- LED tắt hẳn khi nghỉ: 0.0mA ✓
Nguyên lý hoạt động
ESP32 phát sóng WiFi và chạy một trang web nhỏ. Khi bạn bấm nút trên điện thoại, ESP32 bật tắt chân GPIO4 theo nhịp 38kHz đúng mã lệnh của TV. Dòng từ chân GPIO4 chỉ khoảng 2,5mA (qua trở 1kΩ) để mở transistor S8050; transistor mới là thứ cấp ~75mA cho 2 LED hồng ngoại mắc nối tiếp, nên phát xa mà không làm hỏng ESP32. Mắt thu VS1838B được cấp 3,3V (qua lọc 100Ω + 10µF) để tín hiệu ra không vượt 3,3V, dùng để học lệnh từ remote gốc.
Sơ đồ đấu dây
Mỗi màu là một dây. Chấm tròn là điểm hàn. Chạm vào tên dây trong bảng để làm nổi bật.
| # | Dây | Nối các điểm |
|---|---|---|
| 1 | đỏ 5V | U1 VIN ⟷ R2 1 |
| 2 | cam 3V3 | U1 3V3 ⟷ R3 1 |
| 3 | đen GND | U1 GND ⟷ Q1 E (phát) ⟷ U2 GND · chân 2 GND ⟷ C1 - |
| 4 | vàng IR_BASE_DRV | U1 GPIO4 (D4) ⟷ R1 1 |
| 5 | xanh lá IR_BASE | R1 2 ⟷ Q1 B (gốc) |
| 6 | xanh dương IR_A1 | R2 2 ⟷ D1 A · chân dài (+) |
| 7 | tím IR_MID | D1 K · chân ngắn (−) ⟷ D2 A · chân dài (+) |
| 8 | xanh ngọc IR_COL | D2 K · chân ngắn (−) ⟷ Q1 C (góp) |
| 9 | nâu RX_VCC | R3 2 ⟷ U2 VCC · chân 3 VCC ⟷ C1 + |
| 10 | hồng RX_OUT | U2 OUT · chân 1 OUT ⟷ U1 GPIO27 (D27) |
Danh sách linh kiện
| Ký hiệu | Linh kiện | Giá tham khảo | |
|---|---|---|---|
| U1 | ESP32 DevKit V1 30 chân (ESP32-WROOM-32, cổng USB-C hoặc Micro USB) | 110.000đ | Tìm mua |
| Q1 | Transistor S8050 (TO-92) — thay được bằng 2N2222 | 500đ | Tìm mua |
| R1 | Điện trở 1kΩ 1/4W (nâu-đen-đỏ) | 200đ | Tìm mua |
| R2 | Điện trở 27Ω 1/2W (đỏ-tím-đen) | 300đ | Tìm mua |
| D1 | LED phát hồng ngoại 940nm 5mm (vỏ trong suốt) | 1.000đ | Tìm mua |
| D2 | LED phát hồng ngoại 940nm 5mm (vỏ trong suốt) | 1.000đ | Tìm mua |
| U2 | Mắt thu hồng ngoại VS1838B 38kHz (3 chân) | 5.000đ | Tìm mua |
| R3 | Điện trở 100Ω 1/4W (nâu-đen-nâu) | 200đ | Tìm mua |
| C1 | Tụ hóa 10µF 16V (10u) | 500đ | Tìm mua |
| Tổng (chưa gồm board hàn, dây, thiếc) | 118.700đ |
Các bước làm
Code nạp cho mạch
Cài thư viện: IRremoteESP8266 (Arduino IDE → Library Manager). Chọn board DOIT ESP32 DEVKIT V1.
/*
Mạch Gọn — Điều khiển TV/thiết bị hồng ngoại bằng điện thoại qua WiFi
Board : ESP32 DevKit V1 (chọn "DOIT ESP32 DEVKIT V1")
Thư viện: IRremoteESP8266 (>= 2.9.0)
Chân : GPIO4 -> R 1k -> B transistor S8050 (2 LED IR ở cực C)
GPIO27 <- OUT mắt thu VS1838B (cấp 3,3V)
Lần đầu: vào WiFi "MachGon-TV" (mật khẩu machgon123) -> mở 192.168.4.1
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Preferences.h>
#include <nvs.h>
#include <nvs_flash.h>
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRsend.h>
#include <IRutils.h>
const uint16_t PIN_IR_TX = 4;
const uint16_t PIN_IR_RX = 27;
const uint8_t PIN_LED = 2; // LED xanh có sẵn trên board
const char* AP_SSID = "MachGon-TV";
const char* HOSTNAME = "tivi";
const uint32_t LEARN_MS = 10000;
const uint16_t RAW_MAX = 600; // số mốc thời gian tối đa của 1 lệnh thô
IRsend irsend(PIN_IR_TX);
IRrecv irrecv(PIN_IR_RX, 1024, 50, true);
decode_results results;
WebServer server(80);
Preferences codes, cfg;
enum LearnState { L_IDLE, L_WAIT, L_DONE, L_TIMEOUT };
LearnState learnState = L_IDLE;
String learnId, learnProto;
uint32_t learnStart = 0;
bool forceRaw = false;
// Bố cục: 1 byte kind | 2 byte type | 2 byte bits | 8 byte value | 2 byte len | payload
enum Kind : uint8_t { K_VALUE = 1, K_STATE = 2, K_RAW = 3 };
static bool validId(const String& id) {
if (id.length() < 1 || id.length() > 15) return false;
for (char ch : id) if (!(isalnum(ch) || ch == '_')) return false;
return true;
}
void blink(int n, int ms = 60) {
for (int i = 0; i < n; i++) { digitalWrite(PIN_LED, HIGH); delay(ms); digitalWrite(PIN_LED, LOW); delay(ms); }
}
bool saveCode(const String& id, const decode_results& r) {
std::vector<uint8_t> buf;
auto put16 = [&](uint16_t v) { buf.push_back(v & 0xFF); buf.push_back(v >> 8); };
uint8_t kind;
bool known = r.decode_type != decode_type_t::UNKNOWN && !forceRaw;
if (known && hasACState(r.decode_type)) kind = K_STATE;
else if (known && r.bits <= 64) kind = K_VALUE;
else kind = K_RAW;
buf.push_back(kind);
put16((uint16_t)(int16_t)r.decode_type);
put16(r.bits);
for (int i = 0; i < 8; i++) buf.push_back((r.value >> (8 * i)) & 0xFF);
if (kind == K_STATE) {
uint16_t nbytes = r.bits / 8;
put16(nbytes);
for (uint16_t i = 0; i < nbytes; i++) buf.push_back(r.state[i]);
} else if (kind == K_RAW) {
uint16_t len = getCorrectedRawLength(&r);
if (len > RAW_MAX || len < 8) return false;
uint16_t* raw = resultToRawArray(&r);
put16(len);
for (uint16_t i = 0; i < len; i++) put16(raw[i]);
delete[] raw;
} else {
put16(0);
}
return codes.putBytes(id.c_str(), buf.data(), buf.size()) == buf.size();
}
bool sendCode(const String& id) {
size_t n = codes.getBytesLength(id.c_str());
if (n < 15) return false;
std::vector<uint8_t> b(n);
codes.getBytes(id.c_str(), b.data(), n);
auto get16 = [&](size_t o) { return (uint16_t)(b[o] | (b[o + 1] << 8)); };
uint8_t kind = b[0];
decode_type_t type = (decode_type_t)(int16_t)get16(1);
uint16_t bits = get16(3);
uint64_t value = 0;
for (int i = 0; i < 8; i++) value |= (uint64_t)b[5 + i] << (8 * i);
uint16_t len = get16(13);
digitalWrite(PIN_LED, HIGH);
bool ok = false;
if (kind == K_VALUE) ok = irsend.send(type, value, bits);
else if (kind == K_STATE && n >= 15u + len) ok = irsend.send(type, &b[15], len);
else if (kind == K_RAW && n >= 15u + 2u * len) {
std::vector<uint16_t> raw(len);
for (uint16_t i = 0; i < len; i++) raw[i] = get16(15 + 2 * i);
irsend.sendRaw(raw.data(), len, 38);
ok = true;
}
digitalWrite(PIN_LED, LOW);
return ok;
}
bool sendPreset(const String& p) {
digitalWrite(PIN_LED, HIGH);
bool ok = true;
if (p == "samsung") irsend.sendSAMSUNG(0xE0E040BF, 32);
else if (p == "lg") irsend.sendNEC(0x20DF10EF, 32);
else if (p == "sony") irsend.sendSony(0xA90, 12, 2);
else if (p == "nec_generic") irsend.sendNEC(0x00FF02FD, 32);
else ok = false;
digitalWrite(PIN_LED, LOW);
return ok;
}
// ---------------- Trang web ----------------
const char PAGE[] PROGMEM = R"HTML(<!doctype html><html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Remote WiFi</title>
<style>
:root{--b:#0f172a;--c:#1e293b;--a:#22c55e;--t:#e2e8f0;--m:#94a3b8}
*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif;background:var(--b);color:var(--t)}
header{display:flex;gap:8px;align-items:center;padding:12px 14px;background:var(--c);position:sticky;top:0}
header b{flex:1}select,input,button{font:inherit}
.tabs{display:flex;gap:6px;overflow:auto;padding:10px 14px}.tabs button{background:var(--c);color:var(--t);border:1px solid #334155;border-radius:20px;padding:6px 14px}
.tabs button.on{background:var(--a);color:#052e16;border-color:var(--a)}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:6px 14px 14px}
.k{position:relative;background:var(--c);border:1px solid #334155;border-radius:14px;padding:16px 4px;text-align:center;color:var(--t);font-size:15px;min-height:58px}
.k.pwr{background:#7f1d1d}.k.no{opacity:.45}.k:active{transform:scale(.96)}
.k i{position:absolute;right:6px;top:4px;font-style:normal;font-size:11px;color:var(--m)}
.bar{padding:0 14px 10px;display:flex;gap:8px;flex-wrap:wrap}.bar button{background:#334155;color:var(--t);border:0;border-radius:10px;padding:8px 12px}
.bar button.on{background:#f59e0b;color:#111}
#msg{margin:0 14px 10px;padding:10px;border-radius:10px;background:#1e3a8a;display:none}
section{margin:14px;padding:12px;background:var(--c);border-radius:12px}section h3{margin:0 0 8px;font-size:16px}
section input{width:100%;padding:9px;margin:4px 0 8px;border-radius:8px;border:1px solid #475569;background:#0b1220;color:var(--t)}
section button{background:var(--a);border:0;border-radius:8px;padding:9px 14px;color:#052e16;font-weight:600}
small{color:var(--m)}
</style></head><body>
<header><b>📡 Remote WiFi</b><small id="ip"></small></header>
<div class="tabs" id="tabs"></div>
<div class="bar"><button id="mode">✋ Bấm để điều khiển</button><button id="add">+ Nút</button><button id="adddev">+ Thiết bị</button></div>
<div id="msg"></div>
<div class="grid" id="grid"></div>
<section><h3>Thử mã có sẵn (nút Nguồn)</h3><small>Dùng khi chưa có remote gốc. Nếu TV tắt/bật là đúng loại.</small><br><br>
<div class="bar" style="padding:0"><button onclick="preset('samsung')">Mã SAMSUNG</button><button onclick="preset('lg')">Mã LG (NEC)</button><button onclick="preset('sony')">Mã SONY</button><button onclick="preset('nec_generic')">NEC phổ thông</button></div></section>
<section><h3>Cài đặt WiFi nhà</h3><small id="wst"></small>
<input id="ssid" placeholder="Tên WiFi"><input id="pass" type="password" placeholder="Mật khẩu WiFi">
<button onclick="wifi()">Lưu & khởi động lại</button></section>
<section><h3>Nâng cao</h3><label><input type="checkbox" id="raw" style="width:auto"> Luôn lưu lệnh dạng thô (dùng cho điều hòa/thiết bị lạ)</label></section>
<script>
const DEF=[{dev:"TV",btns:[["pwr","⏻ Nguồn"],["src","Nguồn vào"],["mute","🔇"],["vup","Âm +"],["up","▲"],["chu","Kênh +"],["lf","◀"],["ok","OK"],["rt","▶"],["vdn","Âm −"],["dn","▼"],["chd","Kênh −"],["home","🏠"],["back","↩"],["menu","☰"]]}];
let L=DEF,cur=0,learned=new Set(),learnMode=false;
const $=s=>document.querySelector(s);
function msg(t,ms=2500){const m=$('#msg');m.textContent=t;m.style.display='block';clearTimeout(m._t);if(ms)m._t=setTimeout(()=>m.style.display='none',ms)}
async function api(p,o){const r=await fetch(p,o);if(!r.ok)throw new Error(await r.text());return r.headers.get('content-type')?.includes('json')?r.json():r.text()}
function key(d,b){return (d.slice(0,3).toLowerCase().replace(/[^a-z0-9]/g,'x')+'_'+b).slice(0,15)}
function render(){
$('#tabs').innerHTML=L.map((d,i)=>`<button class="${i==cur?'on':''}" onclick="cur=${i};render()">${d.dev}</button>`).join('');
const d=L[cur];if(!d)return;
$('#grid').innerHTML=d.btns.map(([id,lb])=>{const k=key(d.dev,id);return `<button class="k ${id=='pwr'?'pwr':''} ${learned.has(k)?'':'no'}" onclick="press('${k}','${lb}')">${lb}<i>${learned.has(k)?'':'chưa học'}</i></button>`}).join('');
$('#mode').textContent=learnMode?'🎓 Đang ở chế độ HỌC — bấm nút cần học':'✋ Bấm để điều khiển';$('#mode').className=learnMode?'on':'';
}
async function press(k,lb){
if(learnMode||!learned.has(k)){return learn(k,lb)}
try{await api('/api/send?id='+k,{method:'POST'});navigator.vibrate&&navigator.vibrate(30)}catch(e){msg('Lỗi: '+e.message)}
}
async function learn(k,lb){
msg('Chĩa remote gốc vào mắt thu và bấm nút "'+lb+'" (10 giây)…',0);
await api('/api/learn?id='+k,{method:'POST'});
for(let i=0;i<24;i++){await new Promise(r=>setTimeout(r,500));const s=await api('/api/learn');
if(s.status=='done'){learned.add(k);render();return msg('✅ Đã học "'+lb+'" ('+s.proto+')')}
if(s.status=='timeout')return msg('⏱ Hết giờ, chưa nhận được tín hiệu. Thử lại gần hơn.')}
}
async function preset(p){try{await api('/api/preset?p='+p,{method:'POST'});msg('Đã phát mã '+p)}catch(e){msg(e.message)}}
async function save(){await api('/api/layout',{method:'POST',body:JSON.stringify(L)})}
$('#mode').onclick=()=>{learnMode=!learnMode;render()};
$('#add').onclick=async()=>{const lb=prompt('Tên nút mới (vd: Hẹn giờ):');if(!lb)return;const id=('b'+Date.now().toString(36)).slice(-8);L[cur].btns.push([id,lb]);await save();render();learn(key(L[cur].dev,id),lb)};
$('#adddev').onclick=async()=>{const n=prompt('Tên thiết bị (vd: Quạt, Đầu thu, Máy lạnh):');if(!n)return;L.push({dev:n.slice(0,20),btns:[["pwr","⏻ Nguồn"]]});cur=L.length-1;await save();render()};
$('#raw').onchange=e=>api('/api/raw?on='+(e.target.checked?1:0),{method:'POST'});
async function wifi(){await api('/api/wifi',{method:'POST',body:new URLSearchParams({ssid:$('#ssid').value,pass:$('#pass').value})});msg('Đã lưu, mạch khởi động lại. Kết nối lại vào WiFi nhà rồi mở http://tivi.local',0)}
(async()=>{const s=await api('/api/state');learned=new Set(s.learned);if(s.layout&&s.layout.length)L=s.layout;$('#raw').checked=s.raw;
$('#ip').textContent=s.sta?('IP: '+s.ip):'Chế độ phát WiFi';$('#wst').textContent=s.sta?('Đã nối "'+s.ssid+'" — IP '+s.ip+' (hoặc http://tivi.local)'):'Chưa nối WiFi nhà';render()})();
</script></body></html>)HTML";
String jsonEscape(const String& s) { String o; for (char ch : s) { if (ch == '"' || ch == '\\') o += '\\'; o += ch; } return o; }
void handleState() {
String learned = "[";
nvs_iterator_t it = nullptr;
esp_err_t e = nvs_entry_find("nvs", "codes", NVS_TYPE_BLOB, &it);
bool first = true;
while (e == ESP_OK) {
nvs_entry_info_t info; nvs_entry_info(it, &info);
if (!first) learned += ','; first = false;
learned += '"'; learned += info.key; learned += '"';
e = nvs_entry_next(&it);
}
nvs_release_iterator(it);
learned += "]";
String layout = cfg.getString("layout", "[]");
bool sta = WiFi.status() == WL_CONNECTED;
String j = "{\"sta\":" + String(sta ? "true" : "false") + ",\"ip\":\"" + (sta ? WiFi.localIP().toString() : WiFi.softAPIP().toString()) +
"\",\"ssid\":\"" + jsonEscape(cfg.getString("ssid", "")) + "\",\"raw\":" + (forceRaw ? "true" : "false") +
",\"learned\":" + learned + ",\"layout\":" + layout + "}";
server.send(200, "application/json", j);
}
void setupWeb() {
server.on("/", []() { server.send_P(200, "text/html; charset=utf-8", PAGE); });
server.on("/api/state", HTTP_GET, handleState);
server.on("/api/send", []() {
String id = server.arg("id");
if (!validId(id)) return server.send(400, "text/plain", "id sai");
if (!sendCode(id)) return server.send(404, "text/plain", "Nút này chưa học lệnh");
server.send(200, "text/plain", "ok");
});
server.on("/api/preset", HTTP_POST, []() { server.send(sendPreset(server.arg("p")) ? 200 : 400, "text/plain", "ok"); });
server.on("/api/learn", HTTP_POST, []() {
String id = server.arg("id");
if (!validId(id)) return server.send(400, "text/plain", "id sai");
learnId = id; learnProto = ""; learnState = L_WAIT; learnStart = millis();
irrecv.enableIRIn();
digitalWrite(PIN_LED, HIGH);
server.send(200, "text/plain", "ok");
});
server.on("/api/learn", HTTP_GET, []() {
const char* st[] = {"idle", "waiting", "done", "timeout"};
server.send(200, "application/json", String("{\"status\":\"") + st[learnState] + "\",\"id\":\"" + learnId + "\",\"proto\":\"" + jsonEscape(learnProto) + "\"}");
});
server.on("/api/forget", HTTP_POST, []() { codes.remove(server.arg("id").c_str()); server.send(200, "text/plain", "ok"); });
server.on("/api/layout", HTTP_POST, []() {
String body = server.arg("plain");
if (body.length() > 3500 || !body.startsWith("[")) return server.send(400, "text/plain", "bố cục quá lớn");
cfg.putString("layout", body); server.send(200, "text/plain", "ok");
});
server.on("/api/raw", HTTP_POST, []() { forceRaw = server.arg("on") == "1"; cfg.putBool("raw", forceRaw); server.send(200, "text/plain", "ok"); });
server.on("/api/wifi", HTTP_POST, []() {
cfg.putString("ssid", server.arg("ssid")); cfg.putString("pass", server.arg("pass"));
server.send(200, "text/plain", "ok"); delay(500); ESP.restart();
});
server.onNotFound([]() { server.sendHeader("Location", "/"); server.send(302); });
server.begin();
}
void setup() {
Serial.begin(115200);
pinMode(PIN_LED, OUTPUT);
irsend.begin();
codes.begin("codes", false);
cfg.begin("cfg", false);
forceRaw = cfg.getBool("raw", false);
irrecv.setUnknownThreshold(12);
WiFi.mode(WIFI_AP_STA);
WiFi.softAP(AP_SSID, cfg.getString("appass", "machgon123").c_str());
String ssid = cfg.getString("ssid", "");
if (ssid.length()) {
WiFi.begin(ssid.c_str(), cfg.getString("pass", "").c_str());
uint32_t t = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t < 15000) { delay(250); digitalWrite(PIN_LED, !digitalRead(PIN_LED)); }
digitalWrite(PIN_LED, LOW);
}
if (WiFi.status() == WL_CONNECTED) Serial.printf("WiFi nha: %s IP: %s\n", ssid.c_str(), WiFi.localIP().toString().c_str());
Serial.printf("WiFi rieng: %s IP: %s\n", AP_SSID, WiFi.softAPIP().toString().c_str());
if (MDNS.begin(HOSTNAME)) MDNS.addService("http", "tcp", 80);
setupWeb();
blink(3);
}
void loop() {
server.handleClient();
if (learnState == L_WAIT) {
if (irrecv.decode(&results)) {
bool usable = !results.repeat && (results.decode_type != decode_type_t::UNKNOWN || results.rawlen > 12);
if (usable && saveCode(learnId, results)) {
learnProto = typeToString(results.decode_type, false);
if (forceRaw || results.decode_type == decode_type_t::UNKNOWN) learnProto += " (thô)";
learnState = L_DONE;
irrecv.disableIRIn();
digitalWrite(PIN_LED, LOW);
blink(2, 120);
Serial.printf("Da hoc %s: %s\n", learnId.c_str(), learnProto.c_str());
} else {
irrecv.resume();
}
} else if (millis() - learnStart > LEARN_MS) {
learnState = L_TIMEOUT;
irrecv.disableIRIn();
digitalWrite(PIN_LED, LOW);
}
}
// Mất WiFi nhà thì tự nối lại
static uint32_t lastCheck = 0;
if (millis() - lastCheck > 30000) {
lastCheck = millis();
if (cfg.getString("ssid", "").length() && WiFi.status() != WL_CONNECTED) WiFi.reconnect();
}
}
🔌 Cắm thử trên breadboard
Cắm nháp trên board cắm trước cho chắc, chạy đúng rồi hãy làm mạch in. Sơ đồ này do máy tự xếp từ chính netlist ở trên và đã đối chiếu lại: hai chân cùng một cột (cùng nửa board) là thông nhau, không có net nào bị chập.
- Mỗi cột 5 lỗ trong cùng một nửa board thông nhau; hai nửa cách nhau bởi rãnh giữa.
- Hai đường kẻ dọc mép board là đường nguồn: hàng + (vạch đỏ) và hàng − (vạch xanh). Nhớ bắc dây nối đường nguồn trên với đường nguồn dưới nếu dùng cả hai nửa.
- Màu dây trên hình trùng với màu trong bảng nối dây ở mục Sơ đồ, cắm theo màu cho dễ dò.
🧲 Mạch in để ủi (PCB 1 mặt)
Bản mạch in một mặt đồng, tự xếp linh kiện và tự đi dây, đã kiểm quy tắc khoảng cách và đối chiếu đúng 100% với sơ đồ nguyên lý ở trên. In tỉ lệ 1:1 rồi ủi lên tấm đồng.
Hướng dẫn ủi – ăn mòn – khoan – hàn (đầy đủ)
Hướng dẫn làm mạch in bằng phương pháp ủi
Mạch: Điều khiển TV bằng điện thoại qua WiFi (học lệnh từ remote cũ)
Kích thước board: 55,9×55,9 mm · 48 lỗ khoan (mũi 0,8, 0,9 và 1 mm) · 13 đoạn mạch, tổng 334,6 mm · 0 cầu nhảy.
Quy tắc đã kiểm: đường mạch rộng 0,8mm (nguồn và GND 1mm), khe cách tối thiểu 0,5mm. Mạch in đã được máy đối chiếu đúng 100% với sơ đồ nguyên lý.
1. In đúng tỉ lệ 1:1
- In trang "MẶT ĐỒNG" bằng máy in LASER, đặt tỉ lệ 100%, KHÔNG chọn "Fit to page" hay "Scale to fit".
- Dùng thước trên bản in đo lại đoạn 100mm: phải đúng 100mm mới dùng được.
- In trên giấy in ảnh bóng, giấy decal bóc, hoặc giấy tạp chí láng. Mực càng dày càng tốt.
- Trang "MẶT ĐỒNG" đã được LẬT GƯƠNG sẵn: úp mặt có mực vào tấm đồng là đúng chiều.
2. Chuẩn bị tấm đồng
- Cắt phíp đồng (board đồng một mặt) lớn hơn kích thước mạch mỗi bề khoảng 5mm.
- Chà mặt đồng bằng giấy nhám mịn (P800–P1200) rồi rửa sạch, lau khô. Mặt đồng phải sáng, không vết dầu mỡ, không chạm tay vào.
3. Ủi mực sang đồng
- Úp mặt có mực vào tấm đồng, dùng băng keo giấy dán cố định một cạnh.
- Bàn ủi để mức nóng nhất (cotton/linen), KHÔNG dùng chế độ phun hơi nước.
- Ủi đè mạnh và đều 3–5 phút, tập trung từng vùng nhỏ, chú ý các góc và mép.
- Để nguội hẳn rồi ngâm cả tấm vào nước ấm 5–10 phút, bóc giấy thật nhẹ. Giấy decal thì bóc khi còn ấm.
- Soi kỹ: chỗ nào mực bị thiếu hoặc đứt đường thì tô lại bằng bút lông dầu (bút CD) rồi để khô.
4. Ăn mòn (etching)
- Pha dung dịch sắt(III) clorua FeCl3 theo hướng dẫn trên bao bì, hoặc dung dịch oxy già + muối ăn + a-xít; dùng hộp nhựa, KHÔNG dùng đồ kim loại.
- Nhiệt độ dung dịch khoảng 40–50°C thì ăn nhanh hơn. Lắc nhẹ hộp cho đều, thường 10–25 phút là xong.
- An toàn: đeo găng tay cao su và kính bảo hộ, làm ở nơi thoáng. Dung dịch ăn mòn làm hỏng quần áo, bồn rửa và gây bỏng rát da.
- Xong thì rửa board bằng nước sạch, lau khô, dùng xăng thơm hoặc axeton lau sạch lớp mực.
- Dung dịch đã dùng KHÔNG đổ xuống bồn rửa hay cống; đựng vào can kín và bỏ đúng nơi thu gom chất thải.
5. Khoan lỗ
- Đối chiếu trang "BẢN VẼ LỖ KHOAN" để biết lỗ nào dùng mũi nào.
- Dùng mũi khoan 0,8mm, 0,9mm và 1mm, khoan từ MẶT ĐỒNG vào để mũi tự định tâm giữa pad.
- Khoan tốc độ cao, đẩy nhẹ; mũi nhỏ rất dễ gãy. Khoan xong chà nhẹ bavia hai mặt.
6. Hàn linh kiện
- Linh kiện cắm ở MẶT KHÔNG CÓ ĐỒNG, hàn ở mặt đồng. Theo trang "MẶT LINH KIỆN" để đặt đúng vị trí và đúng chiều.
- Hàn linh kiện thấp trước (điện trở, đi-ốt), cao sau (tụ, hàng chân, board vi điều khiển).
- Chú ý chiều: LED chân dài là (+); đi-ốt có vạch trắng là K; tụ hoá có vạch là (−); transistor theo mặt chữ như nét vẽ đỏ.
- Trước khi cấp điện: đo thông mạch giữa chân nguồn và GND — KHÔNG được kêu (không được chập).
Lưu ý với board vi điều khiển
- Board nhái có thể khác thứ tự chân. Đối chiếu tên chân in cạnh từng lỗ trên trang "MẶT LINH KIỆN" với chữ in trên board thật TRƯỚC KHI ăn mòn. Nếu khác, đừng hàn mà báo lại để sửa bản vẽ.
Bản vẽ lỗ khoan
Đo kiểm bằng đồng hồ vạn năng
| Giữa chân VCC và GND của mắt thu (khi đã cắm USB) | 3,2 – 3,3V |
| Giữa VIN và GND | 4,6 – 5,1V |
| Soi LED hồng ngoại bằng camera điện thoại (camera trước) khi bấm nút | Thấy LED nháy tím/trắng mờ |
| Giữa chân C và E của S8050 lúc nghỉ | khoảng 2,2V (LED tắt) |
Sửa lỗi thường gặp
Bấm mà TV không phản ứng
Soi LED bằng camera điện thoại; nếu không nháy kiểm tra chiều S8050 (E-B-C) và chiều LED. Nếu nháy mà TV không nhận: học lại lệnh, đứng gần hơn, hoặc dùng mục "Mã có sẵn".
Học lệnh báo hết giờ
Kiểm tra 3 chân mắt thu (OUT-GND-VCC), đưa remote gần hơn và tránh đèn huỳnh quang/ánh nắng chiếu thẳng.
Không thấy mạng WiFi MachGon-TV
Mở Serial Monitor tốc độ 115200 xem log; cắm lại USB; thử cáp USB khác (nhiều cáp chỉ sạc, không có dây dữ liệu).
Một số máy lạnh không học được
Lệnh điều hòa rất dài, mạch lưu ở dạng thô nên thường vẫn dùng được; nếu không được, chọn "Lưu dạng thô" trong trang Cài đặt rồi học lại.
⚠️ An toàn
- Mạch chỉ dùng điện 5V từ USB, an toàn khi chạm tay.
- Không mở TV để hàn vào bên trong.