Bir önceki yazıda Arduino Uno ve Python kullanarak DDC/CI protokolü üzerinden monitör parlaklık ve kontrast kontrolünü iki potansiyometreyle nasıl yapabileceğimizi anlatmıştım. O yazının sonunda birkaç yapılacak madde listelemiştim. Bu yazı o listedeki en kritik iki maddenin hayata geçirilmesini anlatıyor: Arduino'dan bağımsız bir MCU'ya geçiş ve uygulamanın Win32 ile yeniden yazılması.
Neden Arduino değil?
Arduino Uno güzel bir prototipleme platformu, ancak bu proje için fazla büyük ve gereksiz. Bizim ihtiyacımız olan tek şey iki ADC kanalı ve USB iletişimi. Bunun için 28 pinli bir AVR ve ayrı bir USB-Serial çevirici çipi kullanmak israf.
Daha da önemlisi, Arduino Uno USB üzerinden HID olarak görünmüyor — seri port (CDC) üzerinden
haberleşiyor. Bu da Python tarafında pyserial kütüphanesi ve COM port yönetimi
gerektiriyor. Farklı bilgisayarlarda farklı COM port numaraları atanıyor, kullanıcı bunu elle
değiştirmek zorunda kalıyor.
Hedef: cihazı taktığında ek bir kurulum veya konfigürasyon gerektirmeden çalışan, native USB HID olarak görünen, mümkün olduğunca küçük bir devre.
CH552: USB HID yetenekli minimal MCU
WCH firmasının ürettiği CH552, bu iş için biçilmiş kaftan. Enhanced 8051 çekirdeği üzerine kurulu bu MCU'nun öne çıkan özellikleri:
- Dahili USB full-speed transceiver — harici USB-Serial çeviriciye gerek yok
- 4 kanallı 8-bit ADC (P1.1, P1.4, P1.5, P3.2)
- 16KB program ROM, 1KB xRAM
- DIP20 pakette mevcut — breadboard dostu
- Adet fiyatı 0.5-1 dolar civarı
- ch55xduino ile Arduino IDE desteği
Geliştirme için WeAct Studio'nun CH552 Core Board'unu kullandım. USB konektörü, boot ve reset butonu dahil, breadboard uyumlu, tüm pinler erişilebilir.
Bağlantı şeması
Devre son derece sade:
- POT1 (Parlaklık): Orta bacak → P1.1, uçlar → GND / 3.3V
- POT2 (Kontrast): Orta bacak → P1.4, uçlar → GND / 3.3V
- Her iki pot için 10kΩ değer ideal
- USB, hem güç hem veri için kullanılıyor
USB HID descriptor: Vendor HID
Önceki Arduino versiyonunda CDC (seri port) kullanıyorduk. Bu versiyonda cihaz native USB HID olarak görünüyor. Ancak burada önemli bir detay var: standart keyboard veya mouse descriptor kullanırsak Windows cihazı gerçek bir giriş aygıtı olarak tanımlıyor ve klavye girişlerini karıştırabiliyor.
Bunun yerine Vendor Defined HID (Usage Page 0xFF00) kullandım. Bu şekilde Windows cihazı tanıyor ama sistem giriş aygıtı olarak muamele etmiyor. Device Manager'da "HID-compliant device" olarak görünüyor, herhangi bir sürücü kurulumu gerekmiyor.
CH552 ADC: Dikkat edilmesi gereken detaylar
CH552'nin ADC'sini kullanırken birkaç önemli nokta var. Standart Arduino analogRead()
fonksiyonu çalışıyor, ancak kanal seçimi biraz farklı. ch55xduino'da pin numaralandırması
port.pin formatında: P1.1 için pin 11, P1.4 için pin 14.
Öte yandan ADC çıkışı 8-bit olmasına rağmen dahili referans voltajı nedeniyle değerler 0-255 aralığının tamamını kullanmıyor. Pratikte 4-174 arası bir aralık elde ettim. Python tarafında bu değerleri 0-100 aralığına map ediyorum:
ADC_MIN = 4
ADC_MAX = 174
def map_val(raw):
val = (ADC_MAX - raw) * 100 // (ADC_MAX - ADC_MIN)
return max(0, min(100, val))
Firmware: HID veri gönderimi
USB HID üzerinden veri göndermek için ch55xduino'nun HID keyboard örneğinin kaynak dosyalarını
kullandım. Descriptor'ı Vendor HID olarak değiştirdim, veri gönderimi için mevcut
USB_EP1_send() fonksiyonunu ve HIDKey[] buffer'ını kullandım.
Her HID paketi şu şekilde:
HIDKey[0]— Parlaklık (ADC P1.1)HIDKey[1]— Kontrast (ADC P1.4)
Firmware ana döngüsü 50ms aralıklarla çalışıyor:
void loop() {
uint8_t b = adc_read_ch(0); // P1.1
uint8_t c = adc_read_ch(1); // P1.4
HIDKey[0] = b;
HIDKey[1] = c;
USB_EP1_send();
delay(50);
}
Cihazı flash'lamak için WCHISPTool kullandım. Boot moduna girmek için P3.6 butonuna basılı tutarken USB bağlamak yeterli.
Cold boot sonrası sorunu ve çözümü
Projeyi tamamladıktan sonra kritik bir bug fark ettim: devre Windows yeniden başlatıldıktan sonra resetlemeden çalışmıyordu. Sök-tak veya board reset sonrası her şey normal çalışıyordu, ancak bilgisayar kapatılmadan yeniden başlatıldığında potansiyometreler tepki vermiyordu.
Başlangıçta USB enumerate sorunu olduğunu düşündüm. Device Manager'da cihaz görünüyordu,
hid.enumerate() ile descriptor bilgileri okunabiliyordu. Sorun farklı bir yerdeydi.
Kök neden UpPoint1_Busy flag'inin stuck kalmasıydı. Windows başlatma sırasında USB
host controller enumerate sürecinde bir veya birden fazla USB bus reset sinyali gönderiyor. Eğer
CH552 o anda EP1 üzerinden veri gönderiyorsa transfer yarıda kesiliyor; ancak
USB_EP1_IN() callback'i tetiklenmediği için UpPoint1_Busy flag'i 1
olarak kalıyor. Bundan sonra USB_EP1_send() her çağrıda sessizce return 0
dönüyor — cihaz çalışır görünüyor ama hiç HID raporu göndermiyor. Kullanıcı CH552'yi fiziksel
olarak resetleyince MCU yeniden başlar, flag sıfırlanır ve çalışır.
Çözüm USBhandler.c'deki UIF_BUS_RST interrupt handler'ına üç satır
eklemekten ibaretti:
// USBhandler.c - UIF_BUS_RST handler
extern volatile __xdata uint8_t UpPoint1_Busy;
// ... handler içinde:
UsbConfig = 0;
UpPoint1_Busy = 0; // Bus reset gelince stuck flag'i temizle
Bus reset geldiğinde UpPoint1_Busy sıfırlanıyor, bir sonraki
USB_EP1_send() çağrısında transfer yeniden başlıyor. Windows'un başlatma sırasında
gönderdiği bus reset sinyali artık sorunu çözmek için kullanılıyor. Soğuk açılışta artık
resetlemeye gerek kalmadı.
Python daemon: Win32 ve dxva2
Önceki versiyonda Python GUI için tkinter, DDC/CI için monitorcontrol kütüphanesi kullanıyordum. Bu versiyonda her ikisini de değiştirdim.
OSD: tkinter yerine doğrudan Win32 GDI ile layered window. Transparan arka plan, her zaman üstte, tıklanamaz. Sağ alt köşede 2 saniye görünüp kayboluyor.
DDC/CI: monitorcontrol kütüphanesi yerine Windows'un native
dxva2.dll API'si. SetMonitorBrightness ve
SetMonitorContrast fonksiyonları doğrudan çağrılıyor. Bu yöntem hem daha hızlı
hem de monitorcontrol kütüphanesinin yaşadığı "failed to get VCP feature" hatasını tamamen
ortadan kaldırıyor.
dxva2 = ctypes.windll.dxva2
def _ddc_worker():
while True:
brightness, contrast = _ddc_queue.get()
for h in _monitor_handles:
if brightness is not None:
dxva2.SetMonitorBrightness(h, brightness)
if contrast is not None:
time.sleep(0.05)
dxva2.SetMonitorContrast(h, contrast)
DDC/CI komutları bir queue üzerinden tek bir worker thread'e gönderiliyor. Queue kapasitesi 1 — yeni komut geldiğinde işlenmemiş eski komut atılıyor. Bu sayede monitör hiçbir zaman komut yağmuruna tutulmuyor.
Sonuç
Arduino versiyonuyla karşılaştırıldığında bu versiyon birçok açıdan daha iyi:
- COM port konfigürasyonu yok — USB tak, çalış
- Ayrı USB-Serial çevirici yok — tek çip, tek USB kablo
- OSD daha responsive ve görsel olarak daha temiz
- DDC/CI iletişimi daha stabil
- Soğuk açılışta resetleme gerekmiyor
- Devre boyutu ve maliyeti önemli ölçüde düştü
Yapılacaklar listesinde hala birkaç madde var: ADC gürültüsü için firmware tarafında smoothing buffer, Python daemon yerine standalone exe, ve nihai hedef olarak özel PCB tasarımı. Önümüzdeki yazılarda bunları ele alacağım.
Projenin kaynak kodlarına GitHub üzerinden ulaşabilirsiniz.


