This is an old revision of the document!
Proiectul constă în implementarea unui “PC” inspirat de cele old school pentru care interfața cu utilizatorul se rezuma la introducerea unor comenzi în terminal. O sa îl numesc RetroPM.
Prin acest proiect, mi-am propus sa replic un sistem ce poate reproduce într-un mod minimal gestionarea de fișiere și utilizatori.
RetroPM se bazează în principal pe comunicarea dintre 3 microcontrollere cu scopuri diferite:
Lista componente:
Schema pentru Raspberry Pi Pico (partea de “tastatura”):
Schema pentru ESP32 (celelalte periferice: display, microSD):
(Notita: Am uitat sa pun in schema I2C-ul pentru ESP32, are rezistenta de pull-up ca STM32 si foloseste un singur bus pentru ambele display-uri)
Schema pentru STM32 (microcontroller principal):
Pentru STM32 și Raspeberry Pi Pico am folosit Rust, cu biblioteci de Hardware Layer Abstraction. Pentru ESP32, am folosit Arduino IDE și C++. Lista bibliotecilor folosite:
Pentru partea de procesare de tastatura, a trebuit în primul rând sa implementez o funcție asemănătoare cu cea de shiftOut din Arduino.h (nu am găsit-o în vreu crate de Rust). Folosind 2 shift registeri ca output-uri (fiecare cu 5 coloane din matricea de butoane asignate), și 4 coloane ca input-uri pentru a reduce numărul de pini folosiți pe placa, am folosit o tehnica de matrix scanning pentru a decide ce buton a fost apăsat de utilizator. Astfel am putut sa scanez mai multe butoane în același ciclu de loop. (Lucrul acesta a fost folosit pentru a reproduce efectul de shift + lowercase = uppercase).
Code snippet: Prima parte din loop se ocupa cu scanarea și stocarea intr-o matrice de tip bool dacă tasta de la poziția i,j a fost apăsată.
// Bits 9:5 for first shift register // Bits 4:0 for second shift register let mut bits = 0x0200u16; // 0000_0010_0000_0000 // Mask for second shift register -> 0000_0000_0001_1111 let mask = 0x001Fu16; for col in (0u8..10u8).rev() { // Byte to serial input for first shift register. let byte_higher = !((bits >> 5).to_be_bytes()[1]); // Byte to serial input for second shift register. let byte_lower = !((bits & mask).to_be_bytes()[1]); latch_pin_1.set_low().unwrap(); latch_pin_2.set_low().unwrap(); // Shift out value from register kb_lib::shift_out(&byte_lower, &mut data_pin_2, &mut clock_pin_2, &mut delay, kb_lib::BitOrder::LSBFIRST); kb_lib::shift_out(&byte_higher, &mut data_pin_1, &mut clock_pin_1, &mut delay, kb_lib::BitOrder::LSBFIRST); // Read from the input pins for scanning: let idx = if col == 9 { 9usize } else { 8 - col as usize }; if rows0.is_low().unwrap() { keyState[0][idx] = true; } else { keyState[0][idx] = false; } if rows1.is_low().unwrap() { keyState[1][idx] = true; } else { keyState[1][idx] = false; } if rows2.is_low().unwrap() { if idx == 0 { is_shift_pressed = true; } else { keyState[2][idx] = true; } } else { keyState[2][idx] = false; } if rows3.is_low().unwrap() { keyState[3][idx] = true; } else { keyState[3][idx] = false; } bits >>= 1; latch_pin_1.set_high().unwrap(); latch_pin_2.set_high().unwrap(); }
A doua parte se ocupa cu comunicarea către STM32:
for i in 0..4 { for j in 0..10 { if keyState[i][j] { if is_shift_pressed { last_key_pressed = kb_lib::SHIFT_KEYS[i][j]; is_shift_pressed = !is_shift_pressed; } else { last_key_pressed = KEYS[i][j]; } delay.delay_ms(100); write!(uart,"{}", &last_key_pressed).unwrap(); delay.delay_ms(100); break; } } }
Pentru partea de logica principala, atât în STM32, cât și în ESP32 am folosit logica unor FSM-uri.
ESP32 are FSM-uri separate pentru procesarea de SSID/Parola (pentru conectarea cu serverul NTP), procesarea comenzilor speciale pentru SD, rulării comenzilor:
enum State { MW_LOADING, MW_RUNNING, MW_RUNNING_SETUP }; enum CredentialsState { CRED_NOT_PROV, CRED_PROV, CRED_NO_CHECK }; enum CredentialsProvStatus { NONE, SSID, BOTH }; enum NTPState { NTP_NOT_CONN, NTP_CONN }; enum SDState { SD_PROC_B, SD_NO_PROC_B, SD_PROC_FN }; enum SDCommand { SD_COMM_NONE, SD_COMM_WR, SD_COMM_RD, SD_COMM_AP, SD_COMM_CR, SD_COMM_LS, SD_COMM_RM };
FSM-ul pentru STM32 se ocupa de inițializare, iar apoi intra într-un loop Procesare Comanda ↔ Afișare Status.
pub enum State { StateGetSsid, StateGetSsidPwd, StateInit, StateLoadCmd, StateDoneLoadCmd }
Code snippet ca exemplu:
match processed_cmd.cmd_type { CommandType::CmdLs => { first_byte = '5' as u8; } CommandType::CmdAp => { first_byte = '2' as u8; } CommandType::CmdRd => { first_byte = '1' as u8; } CommandType::CmdRm => { first_byte = '6' as u8; } CommandType::CmdWr => { first_byte = '3' as u8; } CommandType::CmdCr => { first_byte = '4' as u8; } }
Ferris facandu-si prezenta simitita Ła power-up:
Cum arata circuitul final (utilizatorul logat default este root, se vede ca LCD-ul 16×2 este folosit pentru ca utilizatorul sa vadă ce tastează, iar display-ul OLED este folosit pentru informații despre sistemul de fișiere + afișarea orei):
Comanda ls care implicit afișează numele și dimensiunea fișierelor din /:
Mi-ar fi plăcut sa pot sa implementez alte funcționalități ( de ex. mutarea cursorului printr-o secvența de caractere apăsate etc.), dar am subestimat dificultatea proiectului.
Concluzie finala: o experienta placuta (mai ales cand am făcut câteva scurturi XD).