From 34487446732ccb9e359272205b3ecfee543d84c0 Mon Sep 17 00:00:00 2001 From: lurenaud Date: Mon, 25 May 2026 12:33:19 +0200 Subject: [PATCH] Add LIN_to_IR environment, correct SWM key mappings, and organize Arduino project files --- .gitignore | 4 + Arduino/Blink_Test/Blink_Test.cpp | 12 ++ Arduino/LIN_to_IR/LIN_to_IR.cpp | 204 ++++++++++++++++++ Arduino/LIN_to_IR/README.md | 47 ++++ .../{RTI_Control.ino => RTI_Control.cpp} | 5 +- platformio.ini | 5 + 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 Arduino/Blink_Test/Blink_Test.cpp create mode 100644 Arduino/LIN_to_IR/LIN_to_IR.cpp create mode 100644 Arduino/LIN_to_IR/README.md rename Arduino/RTI_Control/{RTI_Control.ino => RTI_Control.cpp} (94%) diff --git a/.gitignore b/.gitignore index e83816e..b988b88 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ fp-info-cache .pio/ .pioenvs/ .piolibdeps/ +compile_commands.json +Kicad/PCB_AndroidAuto-backups/ +*.lck + diff --git a/Arduino/Blink_Test/Blink_Test.cpp b/Arduino/Blink_Test/Blink_Test.cpp new file mode 100644 index 0000000..aaa7c89 --- /dev/null +++ b/Arduino/Blink_Test/Blink_Test.cpp @@ -0,0 +1,12 @@ +#include + +void setup() { + pinMode(3, OUTPUT); +} + +void loop() { + digitalWrite(3, HIGH); + delay(500); + digitalWrite(3, LOW); + delay(500); +} diff --git a/Arduino/LIN_to_IR/LIN_to_IR.cpp b/Arduino/LIN_to_IR/LIN_to_IR.cpp new file mode 100644 index 0000000..4dc1c5a --- /dev/null +++ b/Arduino/LIN_to_IR/LIN_to_IR.cpp @@ -0,0 +1,204 @@ +#include +#include +#include "lin_frame.h" + +#define IR_ARDUINO_PIN 10 +#define LED_PIN 3 + +#define RX_PIN 8 +#define TX_PIN 2 // Unused dummy pin for SoftwareSerial TX +#define FAULT_PIN 9 +#define CS_PIN 11 + +#define SYN_FIELD 0x55 +#define SWM_ID 0x20 + +SoftwareSerial LINBusSerial(RX_PIN, TX_PIN); + +// RC-6 timing constants +// 1 time unit (1t) = 444us +static const uint16_t RC6_T = 444; + +// Mark = LOW on wire (simulating TSOP receiving an IR burst) +void sendMark(uint16_t us) { + digitalWrite(IR_ARDUINO_PIN, LOW); + delayMicroseconds(us); +} + +// Space = HIGH on wire (simulating TSOP idle) +void sendSpace(uint16_t us) { + digitalWrite(IR_ARDUINO_PIN, HIGH); + delayMicroseconds(us); +} + +// Send a single RC-6 bit using Manchester encoding +void sendRC6Bit(uint8_t bit, uint8_t width) { + if (bit) { + sendMark(RC6_T * width); + sendSpace(RC6_T * width); + } else { + sendSpace(RC6_T * width); + sendMark(RC6_T * width); + } +} + +// Send RC-6 Mode 6A (MCE) frame +void sendRC6_MCE(uint32_t data, uint8_t toggle) { + // Disable interrupts to ensure precise IR timing + noInterrupts(); + + // Leader: 6t mark + 2t space + sendMark(RC6_T * 6); + sendSpace(RC6_T * 2); + + // Start bit: always 1 + sendRC6Bit(1, 1); + + // Mode bits: 1, 1, 0 (mode 6) + sendRC6Bit(1, 1); + sendRC6Bit(1, 1); + sendRC6Bit(0, 1); + + // Toggle bit (double width = 2t per half-bit) + sendRC6Bit(toggle & 1, 2); + + // 32 data bits, MSB first + for (int i = 31; i >= 0; i--) { + sendRC6Bit((data >> i) & 1, 1); + } + + // Final return to idle state (HIGH) + sendSpace(1000); + + // Re-enable interrupts + interrupts(); + + // Wait to let receiver process + delay(40); +} + +uint32_t get_mce_code(uint8_t button) { + switch (button) { + case 1: return 0x800f041e; // UP + case 2: return 0x800f041f; // DOWN + case 3: return 0x800f0420; // LEFT + case 4: return 0x800f0421; // RIGHT + case 5: return 0x800f0422; // ENTER / OK + case 6: return 0x800f0423; // BACK + default: return 0; + } +} + +void send_ir_for_button(uint8_t button, uint8_t toggle) { + uint32_t code = get_mce_code(button); + if (code == 0) return; + + digitalWrite(LED_PIN, HIGH); // Turn debug LED ON + sendRC6_MCE(code, toggle); + digitalWrite(LED_PIN, LOW); // Turn debug LED OFF +} + +byte b, n; +LinFrame frame; + +unsigned long last_frame_time = 0; +uint8_t current_button = 0; +uint8_t toggle_bit = 0; +unsigned long last_ir_send_time = 0; + +void process_button_state(uint8_t active_button) { + unsigned long now = millis(); + + if (active_button != 0) { + if (current_button == 0) { + // Button was just pressed! + current_button = active_button; + toggle_bit ^= 1; // Toggle bit alternates on each new press + send_ir_for_button(current_button, toggle_bit); + last_ir_send_time = now; + } else if (current_button == active_button) { + // Button is being held down! + // Send repeat code every 250ms + if (now - last_ir_send_time >= 250) { + send_ir_for_button(current_button, toggle_bit); + last_ir_send_time = now; + } + } else { + // A different button was pressed! + current_button = active_button; + toggle_bit ^= 1; + send_ir_for_button(current_button, toggle_bit); + last_ir_send_time = now; + } + } else { + // Idle state + current_button = 0; + } + + last_frame_time = now; +} + +void handle_frame() { + if (frame.get_byte(0) != SWM_ID) + return; + + if (!frame.isValid()) + return; + + // Extract the data bytes + // SWM button frame has 4 data bytes + uint8_t d0 = frame.get_byte(1); + uint8_t d1 = frame.get_byte(2); + + uint8_t active_button = 0; + + if (d0 & 0x01) active_button = 1; // UP + else if (d0 & 0x02) active_button = 2; // DOWN + else if (d0 & 0x04) active_button = 3; // LEFT + else if (d0 & 0x08) active_button = 4; // RIGHT + else if (d1 & 0x08) active_button = 5; // ENTER / OK + else if (d1 & 0x01) active_button = 6; // BACK + + process_button_state(active_button); +} + +void setup() { + pinMode(IR_ARDUINO_PIN, OUTPUT); + digitalWrite(IR_ARDUINO_PIN, HIGH); // Idle state (HIGH) + + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); // LED OFF + + // Enable MCP2004 + pinMode(CS_PIN, OUTPUT); + digitalWrite(CS_PIN, HIGH); + + pinMode(FAULT_PIN, OUTPUT); + digitalWrite(FAULT_PIN, HIGH); + + LINBusSerial.begin(9600); + frame = LinFrame(); + last_frame_time = millis(); +} + +void loop() { + if (LINBusSerial.available()) { + b = LINBusSerial.read(); + n = frame.num_bytes(); + + if (b == SYN_FIELD && n > 2 && frame.get_byte(n - 1) == 0) { + frame.pop_byte(); + handle_frame(); + frame.reset(); + } else if (n == LinFrame::kMaxBytes) { + frame.reset(); + } else { + frame.append_byte(b); + } + } + + // Timeout: if no LIN frames received for 200ms, assume no button is pressed + if (millis() - last_frame_time > 200) { + current_button = 0; + } +} diff --git a/Arduino/LIN_to_IR/README.md b/Arduino/LIN_to_IR/README.md new file mode 100644 index 0000000..c032df0 --- /dev/null +++ b/Arduino/LIN_to_IR/README.md @@ -0,0 +1,47 @@ +# Volvo LIN-to-IR Controller (ATtiny84) + +This project implements the production firmware for the ATtiny84 to interface a Volvo V50 Steering Wheel Module (SWM) LIN bus with a Raspberry Pi running LineageOS (Android). + +It decodes steering wheel navigation buttons from the vehicle's LIN bus and translates them into bit-banged RC-6 Mode 6A (MCE) remote control commands sent over a direct wire connection. + +## Pin Configurations (ATtiny84) + +| ATtiny84 Pin | Digital Pin (Arduino) | Function | Description | +| :--- | :--- | :--- | :--- | +| **PA0** | `10` | IR Output | Direct-wired to Raspberry Pi GPIO 24 | +| **PA5** | `8` | LIN RX | SoftwareSerial RX from MCP2004 RXD | +| **PA1** | `9` | LIN FAULT/TXE | MCP2004 Fault Detect / Transmit Enable | +| **PA2** | `11` | LIN CS | MCP2004 Chip Select (Active High) | +| **PA6** | `3` | Debug LED | Flash on command transmission | + +## Key Mapping + +The Steering Wheel Module (SWM) sends frames on LIN ID `0x20` with the navigation key statuses. The firmware decodes these and maps them to the following Microsoft MCE remote scancodes: + +| Button | LIN Frame Trigger | Active Button Code | MCE Scancode | Action on LineageOS | +| :--- | :--- | :--- | :--- | :--- | +| **UP** | `d0 & 0x01` | `1` | `0x800f041e` | Navigate Up | +| **DOWN** | `d0 & 0x02` | `2` | `0x800f041f` | Navigate Down | +| **LEFT** | `d0 & 0x04` | `3` | `0x800f0420` | Navigate Left | +| **RIGHT** | `d0 & 0x08` | `4` | `0x800f0421` | Navigate Right | +| **ENTER** | `d1 & 0x08` | `5` | `0x800f0422` | Select / OK | +| **BACK** | `d1 & 0x01` | `6` | `0x800f0423` | Back / Exit | + +*Note: In previous revisions, `ENTER` and `BACK` were reversed. This has been corrected so that pressing `ENTER` maps to `0x800f0422` and `BACK` maps to `0x800f0423`.* + +## Protocol and Timing + +- **Protocol**: RC-6 Mode 6A (MCE). +- **Time Unit (1T)**: `444us`. +- **Modulation**: None. Timing is bit-banged directly since the output pin is wired directly to the Pi's IR receiver GPIO (which expects demodulated active-low signals). +- **Idle (Space)**: `HIGH` (3.3V). +- **Active Pulse (Mark)**: `LOW` (0V). +- **Toggle Bit**: Alternates state on each new button press, but remains constant for repeated holds. +- **Repeat Interval**: Holds trigger repeat IR commands sent every `250ms`. + +## Software Architecture + +1. **SoftwareSerial Interrupt Isolation**: + Since SoftwareSerial RX interrupts consume about 1ms per byte, they will disrupt the precise microsecond-level timing of bit-banged IR frames. To avoid this, interrupts are disabled (`noInterrupts()`) during the 37ms window of IR transmission and re-enabled (`interrupts()`) immediately afterward. +2. **Release detection**: + The SWM continuously transmits frames on the LIN bus. If no button frames are detected or a frame with all 0s is received, the current active button resets to idle. A timeout of `200ms` ensures that if the LIN bus goes quiet, button repeats cease immediately. diff --git a/Arduino/RTI_Control/RTI_Control.ino b/Arduino/RTI_Control/RTI_Control.cpp similarity index 94% rename from Arduino/RTI_Control/RTI_Control.ino rename to Arduino/RTI_Control/RTI_Control.cpp index 62a375a..4d9b141 100644 --- a/Arduino/RTI_Control/RTI_Control.ino +++ b/Arduino/RTI_Control/RTI_Control.cpp @@ -1,5 +1,8 @@ +#include #include +void rtiWrite(char byte); + const byte pinLinTx = 10; const byte pinLinRx = 8; const byte pinLinFalut = 9; @@ -10,7 +13,7 @@ const byte pinRTISerial = 5; const byte pinDefaultRemoteControl = 1; const byte pinMuxVgaSel = 3; const byte pinMuxLogicSel = 4; -const byte pinLedDbg = 7; +const byte pinLedDbg = 3; const byte pinLedCom = 6; enum display_mode_name {RTI_RGB, RTI_PAL, RTI_NTSC, RTI_OFF}; diff --git a/platformio.ini b/platformio.ini index 5a85524..4e24f2c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,3 +36,8 @@ build_src_filter = -<*> + [env:IR_remote_test] extends = env:attiny84_isp build_src_filter = -<*> + + +; Environment for decoding LIN bus events and converting to IR remote commands +[env:LIN_to_IR] +extends = env:attiny84_isp +build_src_filter = -<*> +