Add LIN_to_IR environment, correct SWM key mappings, and organize Arduino project files
This commit is contained in:
@@ -47,3 +47,7 @@ fp-info-cache
|
||||
.pio/
|
||||
.pioenvs/
|
||||
.piolibdeps/
|
||||
compile_commands.json
|
||||
Kicad/PCB_AndroidAuto-backups/
|
||||
*.lck
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#include <Arduino.h>
|
||||
|
||||
void setup() {
|
||||
pinMode(3, OUTPUT);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
digitalWrite(3, HIGH);
|
||||
delay(500);
|
||||
digitalWrite(3, LOW);
|
||||
delay(500);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#include <Arduino.h>
|
||||
#include <SoftwareSerial.h>
|
||||
#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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <Arduino.h>
|
||||
#include <SoftwareSerial.h>
|
||||
|
||||
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};
|
||||
@@ -36,3 +36,8 @@ build_src_filter = -<*> +<Arduino/Blink_Test/>
|
||||
[env:IR_remote_test]
|
||||
extends = env:attiny84_isp
|
||||
build_src_filter = -<*> +<Arduino/IR_remote_test/>
|
||||
|
||||
; Environment for decoding LIN bus events and converting to IR remote commands
|
||||
[env:LIN_to_IR]
|
||||
extends = env:attiny84_isp
|
||||
build_src_filter = -<*> +<Arduino/LIN_to_IR/>
|
||||
|
||||
Reference in New Issue
Block a user