This shows you the differences between two versions of the page.
|
pm:prj2026:jan.vaduva:dragos.ciobanu1706 [2026/05/24 22:52] dragos.ciobanu1706 [Hardware Design] |
pm:prj2026:jan.vaduva:dragos.ciobanu1706 [2026/05/25 02:04] (current) dragos.ciobanu1706 [Code] |
||
|---|---|---|---|
| Line 17: | Line 17: | ||
| ===== Hardware Design ===== | ===== Hardware Design ===== | ||
| - | ^ Component ^ Count ^ PIN ^ Role ^ | + | ^ Component ^ Count ^ PIN ^ Role ^ |
| - | | ESP32 | 1 | N/A | Dart MCU: Wi-Fi data transmission to PC, FreeRTOS light sleep standby | | + | | ESP32 | 1 | N/A | Dart MCU: Wi-Fi data transmission to PC, FreeRTOS light sleep standby | |
| - | | MPU6050 | 1 | E-GPIO21(SDA)/E-GPIO22(SCL) |Measures dart acceleration & gyroscopic angles;triggers motion interrupt to wake ESP32 | | + | | MPU6050 | 1 | E-GPIO21(SDA)/E-GPIO22(SCL) |Measures dart acceleration & gyroscopic angles; triggers motion interrupt to wake ESP32| |
| - | | IR LED | 1 | A-GPIO13 | Dart-side sync signal for distance measurement protocol | | + | | IR LED | 1 | A-GPIO13 | Dart-side sync signal for distance measurement protocol | |
| - | | Ultrasound Sensor | 2 |E-GPIO16(Trig)/A-GPIO12(Echo)| Measures dart-to-board distance for trajectory calculation | | + | | Ultrasound Sensor | 2 | E-GPIO16(Trig)/A-GPIO12(Echo) | Measures dart-to-board distance for trajectory calculation | |
| - | | Arduino | 1 | N/A | Board MCU: receives processed data from PC via UART, controls board mechanisms | | + | | Arduino | 1 | N/A | Board MCU: receives processed data from PC via UART, controls board mechanisms | |
| - | | LEDs | 82 | A-GPIO2->A-GPIO11 | Board lighting: illuminate scored segments | | + | | LEDs | 82 | A-GPIO2->A-GPIO11 | Board lighting: illuminate scored segments | |
| - | | LED Screen | 1 | A-SDA/A-SCL | Displays score, game state, or throw feedback | | + | | LED Screen | 1 | A-SDA/A-SCL | Displays score, game state, or throw feedback | |
| ===== Software Design ===== | ===== Software Design ===== | ||
| - | The firmware for both the ESP32 and the Arduino is developed using PlatformIO. The ESP32 runs on top of FreeRTOS, spending most of its time in light sleep until the MPU6050 motion interrupt signals a throw. Current libraries in use include the ESP-IDF GPIO and sleep drivers, a custom I2C driver, and the FreeRTOS task scheduler. Additional libraries will be integrated as the design progresses. | + | |
| - | The Arduino communicates with the PC through UART, receiving processed throw data and translating it into board actions. | + | The firmware for both the ESP32 and the Arduino is developed using PlatformIO. The ESP32 runs on top of FreeRTOS, spending most of its time in light sleep until the MPU6050 motion interrupt signals a throw. The Arduino side uses bare-metal avr-libc, without the Arduino library, for direct register-level control of the charlieplexed LED matrix. |
| + | |||
| + | === Libraries and Frameworks === | ||
| + | |||
| + | == The Dart (ESP32 / ESP-IDF + FreeRTOS) == | ||
| + | * **ESP-IDF I2C Master Driver** (''driver/i2c_master.h'') — communicates with the MPU6050 over I2C (GPIO21 SDA, GPIO22 SCL, 100 kHz) | ||
| + | * **ESP-IDF GPIO Driver** (''driver/gpio.h'') — configures GPIO34 as an interrupt input for the MPU6050 motion detect signal | ||
| + | * **ESP-IDF Sleep API** (''esp_sleep.h'') — manages light sleep between throws to conserve power | ||
| + | * **FreeRTOS Semaphore** — an ISR-safe binary semaphore synchronizes the motion interrupt with the main game loop task | ||
| + | * **NimBLE (esp-nimble-cpp v2.5.0)** — BLE GATT server that exposes a single Notify characteristic for streaming throw data to the PC | ||
| + | |||
| + | == The Handler (Python / PC) == | ||
| + | * **Bleak** — asynchronous BLE client library, connects to the Dart controller and subscribes to throw notifications | ||
| + | |||
| + | == The Board (ATmega328P / Bare-metal AVR) == | ||
| + | * **avr-libc** — direct port/pin register manipulation for charlieplexing | ||
| + | * Custom **charlieplex_dartboard** driver — maps all 82 segments to rail pairs stored in PROGMEM, minimizing RAM usage on the 2 KB ATmega328P | ||
| + | |||
| + | === Firmware === | ||
| + | |||
| + | == The Dart == | ||
| + | |||
| + | **MPU6050 Initialization:** | ||
| + | The sensor is configured for ±8g accelerometer range, ±500°/s gyroscope range, with a digital low-pass filter at 94 Hz. The motion detection interrupt is set to trigger at 0.50g with a 1 ms duration threshold: | ||
| + | |||
| + | <code c> | ||
| + | void init_mpu() { | ||
| + | i2c_write(0x6B, 0x00); // Wake up the MPU-6050 | ||
| + | i2c_write(0x1A, 0x02); // Set DLPF to 94Hz | ||
| + | |||
| + | i2c_write(0x1B, 0x08); // Set gyro full scale to ±500°/s | ||
| + | i2c_write(0x1C, 0x18); // Set accelerometer full scale to ±8g | ||
| + | |||
| + | i2c_write(0x1F, 0x04); // Set motion threshold (~0.51g) | ||
| + | i2c_write(0x20, 0x01); // Set motion duration to 1ms | ||
| + | |||
| + | i2c_write(0x37, 0x20); // Latch mode until interrupt cleared | ||
| + | i2c_write(0x38, 0x40); // Enable Motion Interrupt | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | **Interrupt-driven throw detection:** | ||
| + | When the MPU6050 detects motion above the threshold, it asserts its INT pin. An ISR on GPIO34 gives a FreeRTOS binary semaphore, waking the main game task without polling: | ||
| + | |||
| + | <code c> | ||
| + | static SemaphoreHandle_t throwSemaphore; | ||
| + | |||
| + | static void IRAM_ATTR mpu_isr_handler(void* arg) { | ||
| + | BaseType_t xHigherPriorityTaskWoken = pdFALSE; | ||
| + | xSemaphoreGiveFromISR(throwSemaphore, &xHigherPriorityTaskWoken); | ||
| + | portYIELD_FROM_ISR(xHigherPriorityTaskWoken); | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | **Main game loop:** | ||
| + | The task blocks indefinitely on the semaphore. When a throw is detected, it reads 6 accelerometer and gyroscope values, packs them into a 24-byte float array, and sends it to the PC via BLE Notify: | ||
| + | |||
| + | <code c> | ||
| + | while(1) { | ||
| + | xSemaphoreTake(throwSemaphore, portMAX_DELAY); | ||
| + | |||
| + | i2c_read(0x3A, buff); // Clear interrupt flag | ||
| + | read_mpu_acc(&accX, &accY, &accZ); | ||
| + | read_mpu_gyro(&gyroX, &gyroY, &gyroZ); | ||
| + | |||
| + | float payload[6] = {accX, accY, accZ, gyroX, gyroY, gyroZ}; | ||
| + | pCharacteristic->setValue((uint8_t*)payload, sizeof(payload)); | ||
| + | pCharacteristic->notify(); | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | == The Handler == | ||
| + | The Python handler bridges the dart and the board. A background thread runs the Bleak BLE event loop, and each notification triggers a hitscan calculation. | ||
| + | |||
| + | **Hitscan and scoring:** | ||
| + | The accelerometer Y and Z values are scaled by a sensitivity constant to produce impact coordinates in mm. The scoring function uses polar coordinates to determine the ring (bullseye, single, triple, double, miss) and sector (standard 20-sector layout): | ||
| + | |||
| + | <code python> | ||
| + | HITSCAN_SENSITIVITY = 27.5 | ||
| + | |||
| + | def calculate_hitscan(acc_y, acc_z): | ||
| + | impact_x = acc_y * HITSCAN_SENSITIVITY | ||
| + | impact_y = acc_z * HITSCAN_SENSITIVITY | ||
| + | return impact_x, impact_y | ||
| + | |||
| + | def get_dartboard_score(x_mm, y_mm): | ||
| + | distance = math.sqrt(x_mm**2 + y_mm**2) | ||
| + | if distance <= 6.35: return "Inner Bullseye (50)" | ||
| + | if distance <= 15.9: return "Outer Bullseye (25)" | ||
| + | if distance > 170: return "Miss (0)" | ||
| + | |||
| + | angle = math.degrees(math.atan2(y_mm, x_mm)) | ||
| + | if angle < 0: angle += 360 | ||
| + | sector_angle = (angle + 9) % 360 | ||
| + | sector_index = int(sector_angle / 18) | ||
| + | |||
| + | sectors = [6, 10, 15, 2, 17, 3, 19, 7, 16, 8, | ||
| + | 11, 14, 9, 12, 5, 20, 1, 18, 4, 13] | ||
| + | base = sectors[sector_index] | ||
| + | |||
| + | if 99 <= distance <= 107: return f"Triple {base} ({base * 3})" | ||
| + | if 162 <= distance <= 170: return f"Double {base} ({base * 2})" | ||
| + | return f"Single {base} ({base})" | ||
| + | </code> | ||
| + | |||
| + | == The Board == | ||
| + | The board firmware uses a charlieplexing driver to control 82 LEDs with only 10 GPIO pins (Arduino D3–D12, mapped to AVR ports PD3–PD7 and PB0–PB4). | ||
| + | |||
| + | **Segment-to-rail mapping:** | ||
| + | A PROGMEM lookup table stores the (HIGH rail, LOW rail) pair for each of the 82 dartboard segments. The mapping was derived from the Eagle schematic, where each diode's anode and cathode pins were cross-referenced with the physical board layout: | ||
| + | |||
| + | <code c> | ||
| + | static const struct { | ||
| + | uint8_t hi; /* rail_t for anode (HIGH) */ | ||
| + | uint8_t lo; /* rail_t for cathode (LOW) */ | ||
| + | } seg_map[NUM_SEGMENTS] PROGMEM = { | ||
| + | [SEG_BEYE ] = { RAIL_R6 , RAIL_R8 }, /* bulseye */ | ||
| + | [SEG_CTR ] = { RAIL_R8 , RAIL_R6 }, /* bullseye ring */ | ||
| + | [SEG_DBL1 ] = { RAIL_R3 , RAIL_R1 }, /* double points foe segment 1 */ | ||
| + | [SEG_DBL2 ] = { RAIL_R2 , RAIL_R10 }, /* double points foe segment 2 */ | ||
| + | /* ... */ | ||
| + | }; | ||
| + | </code> | ||
| + | |||
| + | **LED control (''led_on''):** | ||
| + | To light a specific LED, the function reads the two rail descriptors from flash, sets exactly those two pins as OUTPUT (one HIGH, one LOW), and leaves all other pins in high-impedance. The write sequence is ordered to prevent glitches: | ||
| + | |||
| + | <code c> | ||
| + | void led_on(segment_t seg) { | ||
| + | if ((uint8_t)seg >= (uint8_t)NUM_SEGMENTS) { | ||
| + | leds_off(); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | uint8_t hi_rail = pgm_read_byte(&seg_map[seg].hi); | ||
| + | uint8_t lo_rail = pgm_read_byte(&seg_map[seg].lo); | ||
| + | |||
| + | rail_pin_t h = read_rail(hi_rail); | ||
| + | rail_pin_t l = read_rail(lo_rail); | ||
| + | |||
| + | uint8_t ddr_d = 0, ddr_b = 0; | ||
| + | uint8_t port_d = 0, port_b = 0; | ||
| + | |||
| + | /* Anode (HIGH) */ | ||
| + | if (h.port_id == PORT_ID_D) { ddr_d |= h.mask; port_d |= h.mask; } | ||
| + | else { ddr_b |= h.mask; port_b |= h.mask; } | ||
| + | |||
| + | /* Cathode (LOW): OUTPUT but PORT bit stays 0 */ | ||
| + | if (l.port_id == PORT_ID_D) { ddr_d |= l.mask; } | ||
| + | else { ddr_b |= l.mask; } | ||
| + | |||
| + | /* Glitch-free sequence: tri-state all → set PORT → enable OUTPUT */ | ||
| + | DDRD &= (uint8_t)~RAILS_MASK_D; | ||
| + | DDRB &= (uint8_t)~RAILS_MASK_B; | ||
| + | PORTD = (uint8_t)((PORTD & ~RAILS_MASK_D) | port_d); | ||
| + | PORTB = (uint8_t)((PORTB & ~RAILS_MASK_B) | port_b); | ||
| + | DDRD |= ddr_d; | ||
| + | DDRB |= ddr_b; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | When integrated with the Handler, the Arduino will receive a segment ID over UART (from the PC) and call ''led_on()'' with the corresponding segment to illuminate the hit zone. | ||
| ===== Results ===== | ===== Results ===== | ||
| In this moment, only the dart is finished. It works as intended, as you can see in the clip provided on moodle. The dartboard n screen is a proof of concept written in python. It simulates the real life throw from a set distance and height. When the project will be in its final state, the virtual dartboard will be switched with the real one and the script will only handle data computation and transmition. | In this moment, only the dart is finished. It works as intended, as you can see in the clip provided on moodle. The dartboard n screen is a proof of concept written in python. It simulates the real life throw from a set distance and height. When the project will be in its final state, the virtual dartboard will be switched with the real one and the script will only handle data computation and transmition. | ||
| Line 37: | Line 198: | ||
| {{ :pm:prj2026:jan.vaduva:whatsapp_image_2026-05-24_at_9.21.58_pm_1_.jpeg?1000 |}} | {{ :pm:prj2026:jan.vaduva:whatsapp_image_2026-05-24_at_9.21.58_pm_1_.jpeg?1000 |}} | ||
| ===== Code ===== | ===== Code ===== | ||
| - | The whole project code, including the Dart, the Handler and the Board can be found here. | + | <note tip>The whole project code, including the Dart, the Handler and the Board can be found [[https://github.com/dragos473/Darts_Game.git|Here]].</note> |
| ===== Journal ===== | ===== Journal ===== | ||
| + | <note tip>noting to see here for now</note> | ||
| ===== Bibliography/Resources ===== | ===== Bibliography/Resources ===== | ||
| - | Datasheets for ESP32, ATmega328, MPU6050 | ||
| - | PM labs | ||
| + | === Datasheets === | ||
| + | * [[https://www.espressif.com/sites/default/files/documentation/esp32-wroom-32_datasheet_en.pdf|ESP32-WROOM-32 Datasheet]] — Espressif Systems, Module datasheet (pinout, electrical specifications, schematics) | ||
| + | * [[https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf|ATmega328P Datasheet]] — Microchip/Atmel, Full datasheet (662 pages, register descriptions, electrical characteristics) | ||
| + | * [[https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf|MPU-6000/MPU-6050 Product Specification]] — InvenSense (TDK), Rev 3.4 (gyroscope/accelerometer specs, I2C timing, pin descriptions) | ||
| + | * [[https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf|MPU-6000/MPU-6050 Register Map and Descriptions]] — InvenSense (TDK), Rev 4.0 (register addresses, bit fields, configuration values) | ||
| + | * [[https://docs.espressif.com/projects/esp-idf/en/stable/esp32/|ESP-IDF Programming Guide]] — Espressif, Official development framework for ESP32 (I2C master driver, GPIO driver, sleep API, FreeRTOS integration) | ||
| + | |||
| + | === Libraries === | ||
| + | * [[https://github.com/h2zero/esp-nimble-cpp|esp-nimble-cpp]] — h2zero, C++ wrapper for the ESP32 NimBLE BLE stack (used for BLE GATT server on the dart) | ||
| + | * [[https://github.com/hbldh/bleak|Bleak]] — Henrik Blidh, Cross-platform Python BLE client library (used in the Handler for connecting to the dart) | ||
| + | === Course Materials === | ||
| + | * PM Labs — [[https://ocw.cs.pub.ro/courses/pm|Proiectarea cu Microprocesoare]], UPB course labs | ||