Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.pio
__pycache__/
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
Expand Down
88 changes: 88 additions & 0 deletions boards/m5stack-cardputer/ADV_3IN1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Bruce 1.16.1 para Cardputer ADV y módulo RF 3 en 1

Entorno PlatformIO: `m5stack-cardputer-adv-3in1`. Parte de la release estable
1.16.1 de Bruce y conserva la licencia AGPL-3.0 del repositorio. Este entorno
fija el cableado del módulo CC1101 + nRF24L01 + SX1262. No requiere ajustar
los pines desde el menú después de arrancar.

## Cableado

| Señal | GPIO | Uso |
| --- | ---: | --- |
| SPI SCK | 40 | CC1101, nRF24, SX1262 y SD |
| SPI MISO | 39 | CC1101, nRF24, SX1262 y SD |
| SPI MOSI | 14 | CC1101, nRF24, SX1262 y SD |
| CC1101 CS | 15 | Selección de CC1101 |
| CC1101 GDO0 | 13 | Interrupción/datos CC1101 |
| nRF24 CE | 8 | Compartido con teclado SDA |
| nRF24 CS | 9 | Compartido con teclado SCL |
| SX1262 CS | 5 | Selección de SX1262 |
| SX1262 RST | 3 | Reset SX1262 |
| SX1262 DIO1 | 4 | Interrupción SX1262 |
| SX1262 BUSY | 6 | Estado SX1262 |
| Teclado TCA8418 SDA | 8 | Compartido con nRF24 CE |
| Teclado TCA8418 SCL | 9 | Compartido con nRF24 CS |
| Teclado TCA8418 INT | 11 | Interrupción de teclado |
| Tarjeta SD CS | 12 | Selección de SD existente |

## Transición del teclado al nRF24

En reposo, GPIO8/9 pertenecen a `Wire1` para el teclado. Al iniciar una función
SPI de nRF24, el firmware bloquea el lector de teclas, desconecta la interrupción
TCA8418, termina `Wire1`, establece CE bajo y CS alto y configura ambos GPIO
como salidas. Luego usa `acquireSPIBus()`, el gestor SPI de Bruce, para compartir
el bus de la SD. El botón lateral GPIO0 sirve para salir de la operación activa
cuando el teclado I2C está suspendido. Al terminar, se apaga el nRF24, se dejan
CE bajo y CS alto, se liberan GPIO8/9, se reinicia `Wire1` y se reinicializan la
matriz y la interrupción del teclado. Un fallo de reinicialización se reintenta.

Las operaciones de CC1101, nRF24 y LoRa preparan sus CS antes de inicializar la
radio. El perfil usa el soporte SX1262 ya incluido en Bruce, con BUSY en GPIO6.
La variante SX1262 se guarda en `/lora_settings.json`. Los pines fijos del
perfil se reimponen al leer `/brucePins.conf`, incluyendo después de reinicio o
salida de deep sleep; un fichero antiguo con otro pinout se corrige y guarda.
Una asignación GPS antigua en GPIO15/13 se mueve a Grove GPIO1/2 para no
activar accidentalmente CS o GDO0 de CC1101.

## Límite eléctrico del montaje indicado

GPIO8 y GPIO9 están conectados **directamente** tanto al TCA8418 como al
nRF24L01. Durante una transacción I2C, SDA cambia el nivel de CE y SCL lleva
CS a bajo. El firmware no puede mantener CS alto ni CE bajo en el nRF24 y, a la
vez, usar esas mismas líneas para el teclado. El nRF24 podría ver tráfico SPI
ajeno mientras CS está bajo, incluso cuando el software haya apagado la radio.
Por tanto, la exclusión eléctrica de CS y el funcionamiento RF fiable **no están
garantizados** con este cableado. Hace falta aislar físicamente las dos líneas
del nRF24 (con un multiplexor, interruptor o cableado alternativo) para cumplir
ese requisito. Ese cambio de hardware necesitaría especificar su pin de control
antes de adaptar el firmware.

Mientras se usa nRF24, el teclado I2C no proporciona navegación; el botón
lateral permite terminar la función activa. Las funciones nRF24 que requieren
teclas simultáneas para cambiar opciones conservan el código original, pero su
control completo necesita aislamiento externo. Esto también afecta a cualquier
uso simultáneo de `Wire1` por otro periférico en los mismos GPIO.

## Compilación y comprobaciones

```
pio run -e m5stack-cardputer-adv-3in1
python -m unittest discover -s tests -p 'test_cardputer_adv_3in1.py'
```

Las pruebas automatizadas verifican pinout, reimposición tras cargar configuración,
secuencia de transición, preparación de CS, BUSY/IRQ SX1262 y presencia en los
menús. Son comprobaciones de código; no sustituyen la validación sobre placa.

## Pruebas físicas pendientes

1. Medir con osciloscopio o analizador lógico GPIO8/9, CE y CS durante escritura
I2C y tráfico SD/SPI; comprobar que no se selecciona nRF24 indebidamente.
2. Arrancar en frío, reiniciar y despertar de deep sleep; leer el teclado y
confirmar los pines efectivos de las tres radios en cada arranque.
3. Entrar y salir repetidamente de Spectrum, Jammer y MouseJack; comprobar
restauración del teclado, incluso tras fallo de detección nRF24.
4. Confirmar CC1101 RX/TX con un equipo de prueba legal y SX1262 TX/RX con
otra radio configurada a la misma frecuencia y región.
5. Validar nRF24 RX/TX con un segundo nRF24, después de incorporar el aislamiento
eléctrico; repetir las pruebas de teclado durante actividad RF.
10 changes: 10 additions & 0 deletions boards/m5stack-cardputer/adv_3in1.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

#ifdef CARDPUTER_ADV_3IN1
bool cardputerAdvEnterNrf();
bool cardputerAdvLeaveNrf();
bool cardputerAdvNrfActive();
bool cardputerAdvKeyboardRecoveryPending();
void cardputerAdvLockInput();
void cardputerAdvUnlockInput();
#endif
142 changes: 142 additions & 0 deletions boards/m5stack-cardputer/interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,101 @@
#include <Keyboard.h>
#include <Wire.h>
#include <interface.h>
#ifdef CARDPUTER_ADV_3IN1
#include "adv_3in1.h"
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#endif

// Cardputer and 1.1 keyboard
Keyboard_Class Keyboard;
// TCA8418 keyboard controller for ADV variant
Adafruit_TCA8418 tca;
bool UseTCA8418 = false; // Set to true to use TCA8418 (Cardputer ADV)
#ifdef CARDPUTER_ADV_3IN1
static std::atomic_bool nrfOwnsKeyboardPins{false};
static std::atomic_bool keyboardRecoveryPending{false};
extern Adafruit_TCA8418 tca;
extern volatile bool kb_interrupt;
void gpio_isr_handler(void *arg);
static SemaphoreHandle_t keyboardPinMutex() {
static SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
return mutex;
}

void cardputerAdvLockInput() { xSemaphoreTake(keyboardPinMutex(), portMAX_DELAY); }
void cardputerAdvUnlockInput() { xSemaphoreGive(keyboardPinMutex()); }
bool cardputerAdvNrfActive() { return nrfOwnsKeyboardPins; }
bool cardputerAdvKeyboardRecoveryPending() { return keyboardRecoveryPending; }

bool cardputerAdvEnterNrf() {
cardputerAdvLockInput();
if (nrfOwnsKeyboardPins) {
bool usable = !keyboardRecoveryPending;
cardputerAdvUnlockInput();
return usable;
}
if (!UseTCA8418) {
cardputerAdvUnlockInput();
Serial.println("ADV 3IN1: TCA8418 keyboard unavailable; refusing NRF pin takeover");
return false;
}
detachInterrupt(digitalPinToInterrupt(TCA8418_INT_PIN));
if (!Wire1.end()) {
Wire1.begin(TCA8418_SDA_PIN, TCA8418_SCL_PIN);
attachInterruptArg(digitalPinToInterrupt(TCA8418_INT_PIN), gpio_isr_handler, nullptr, CHANGE);
cardputerAdvUnlockInput();
Serial.println("ADV 3IN1: could not release keyboard I2C");
return false;
}
// Set the inactive logic levels before enabling the output drivers.
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
pinMode(8, OUTPUT);
pinMode(9, OUTPUT);
nrfOwnsKeyboardPins = true;
keyboardRecoveryPending = false;
cardputerAdvUnlockInput();
return true;
}

bool cardputerAdvLeaveNrf() {
cardputerAdvLockInput();
if (!nrfOwnsKeyboardPins) {
cardputerAdvUnlockInput();
return true;
}
digitalWrite(8, LOW);
digitalWrite(9, HIGH);
pinMode(8, INPUT);
pinMode(9, INPUT);
bool ready = false;
for (int attempt = 0; attempt < 3 && !ready; ++attempt) {
if (Wire1.begin(TCA8418_SDA_PIN, TCA8418_SCL_PIN)) {
ready = tca.begin(TCA8418_I2C_ADDR, &Wire1) && tca.matrix(7, 8);
}
if (!ready) {
Wire1.end();
delay(10);
}
}
if (ready) {
tca.flush();
pinMode(TCA8418_INT_PIN, INPUT);
attachInterruptArg(digitalPinToInterrupt(TCA8418_INT_PIN), gpio_isr_handler, nullptr, CHANGE);
tca.enableInterrupts();
kb_interrupt = true;
nrfOwnsKeyboardPins = false;
keyboardRecoveryPending = false;
} else {
Serial.println("ADV 3IN1: keyboard I2C recovery failed");
keyboardRecoveryPending = true;
}
cardputerAdvUnlockInput();
return ready;
}
#endif

// Keyboard state variables
bool fn_key_pressed = false;
Expand Down Expand Up @@ -84,9 +173,16 @@ inline void mapRawKeyToPhysical(uint8_t keyvalue, uint8_t &row, uint8_t &col) {
void _setup_gpio() {
// Keyboard.begin();
pinMode(0, INPUT);
#ifdef CARDPUTER_ADV_3IN1
digitalWrite(5, HIGH);
pinMode(5, OUTPUT);
digitalWrite(15, HIGH);
pinMode(15, OUTPUT);
#else
pinMode(5, OUTPUT);
// Set GPIO5 HIGH for SD card compatibility (thx for the tip @bmorcelli & 7h30th3r0n3)
digitalWrite(5, HIGH);
#endif
}
volatile bool kb_interrupt = false;
void IRAM_ATTR gpio_isr_handler(void *arg) {
Expand Down Expand Up @@ -129,10 +225,13 @@ void _post_setup_gpio() {
bruceConfigPins.sys_i2c.sda = (gpio_num_t)8;
bruceConfigPins.sys_i2c.scl = (gpio_num_t)9;

#ifndef CARDPUTER_ADV_3IN1
bruceConfigPins.gps_bus.rx = (gpio_num_t)15;
bruceConfigPins.gps_bus.tx = (gpio_num_t)13;
#endif
bruceConfigPins.gpsBaudrate = 115200;

#ifndef CARDPUTER_ADV_3IN1
bruceConfigPins.CC1101_bus.sck = (gpio_num_t)40;
bruceConfigPins.CC1101_bus.miso = (gpio_num_t)39;
bruceConfigPins.CC1101_bus.mosi = (gpio_num_t)14;
Expand All @@ -151,6 +250,15 @@ void _post_setup_gpio() {
digitalWrite(bruceConfigPins.NRF24_bus.cs, HIGH);
digitalWrite(bruceConfigPins.CC1101_bus.cs, HIGH);
digitalWrite(bruceConfigPins.LoRa_bus.cs, HIGH);
#else
// The dedicated build profile overrides pin files loaded from flash/SD.
bruceConfigPins.applyBoardProfile();
pinMode(bruceConfigPins.CC1101_bus.cs, OUTPUT);
digitalWrite(bruceConfigPins.CC1101_bus.cs, HIGH);
pinMode(bruceConfigPins.LoRa_bus.cs, OUTPUT);
digitalWrite(bruceConfigPins.LoRa_bus.cs, HIGH);
// GPIO8/9 remain owned by Wire1 until a NRF function starts.
#endif

tca.matrix(7, 8);
tca.flush();
Expand Down Expand Up @@ -205,10 +313,24 @@ void InputHandler(void) {
tm = millis();
if (!wakeUpScreen()) yield();
else return;
#ifdef CARDPUTER_ADV_3IN1
if (cardputerAdvNrfActive()) EscPress = true;
else SelPress = true;
#else
SelPress = true;
#endif
AnyKeyPress = true;
}

#ifdef CARDPUTER_ADV_3IN1
// The TCA8418 is disconnected here. Do not fall through to the legacy
// Cardputer keyboard driver, which uses unrelated GPIOs.
if (cardputerAdvNrfActive()) {
KeyStroke.Clear();
return;
}
#endif

if (UseTCA8418) {
bool keyEventHandled = false;
bool nextPulse = false;
Expand Down Expand Up @@ -503,7 +625,17 @@ void _setup_codec_speaker(bool enable) {
};
static constexpr const uint8_t disabled_bulk_data[] = {0};

#ifdef CARDPUTER_ADV_3IN1
cardputerAdvLockInput();
if (cardputerAdvNrfActive()) {
cardputerAdvUnlockInput();
return;
}
#endif
i2c_bulk_write(&Wire1, ES8311_ADDR, enable ? enabled_bulk_data : disabled_bulk_data);
#ifdef CARDPUTER_ADV_3IN1
cardputerAdvUnlockInput();
#endif
}

/*********************************************************************
Expand Down Expand Up @@ -540,5 +672,15 @@ void _setup_codec_mic(bool enable) {
0
};

#ifdef CARDPUTER_ADV_3IN1
cardputerAdvLockInput();
if (cardputerAdvNrfActive()) {
cardputerAdvUnlockInput();
return;
}
#endif
i2c_bulk_write(&Wire1, ES8311_ADDR, enable ? enabled_bulk_data : disabled_bulk_data);
#ifdef CARDPUTER_ADV_3IN1
cardputerAdvUnlockInput();
#endif
}
8 changes: 8 additions & 0 deletions boards/m5stack-cardputer/m5stack-cardputer-adv-3in1.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[env:m5stack-cardputer-adv-3in1]
extends = env:m5stack-cardputer
board = m5stack-cardputer-adv
build_flags =
${env:m5stack-cardputer.build_flags}
-DCARDPUTER_ADV_3IN1=1
-DLORA_BUSY=6
-DLORA_IRQ=4
12 changes: 8 additions & 4 deletions patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@
)

if mcu=="esp32c5" or mcu=="esp32c6" :
env.Execute(
"pio pkg exec -p toolchain-riscv32-esp -- riscv32-esp-elf-objcopy --weaken-symbol=ieee80211_raw_frame_sanity_check %s %s"
patch_result = env.Execute(
"pio pkg exec -p toolchain-riscv32-esp -- riscv32-esp-elf-objcopy --weaken-symbol=ieee80211_raw_frame_sanity_check \"%s\" \"%s\""
% (original_file, patched_file)
)
elif mcu=="esp32p4":
"""Do nothing"""
patch_result = 0
else:
env.Execute(
"pio pkg exec -p toolchain-xtensa-%s -- xtensa-%s-elf-objcopy --weaken-symbol=ieee80211_raw_frame_sanity_check %s %s"
patch_result = env.Execute(
"pio pkg exec -p toolchain-xtensa-%s -- xtensa-%s-elf-objcopy --weaken-symbol=ieee80211_raw_frame_sanity_check \"%s\" \"%s\""
% (mcu, mcu, original_file, patched_file)
)

if patch_result != 0 or (mcu != "esp32p4" and not isfile(patched_file)):
raise RuntimeError("Failed to patch libnet80211.a; original archive was preserved")

if isfile("%s.old" % (original_file)):
remove("%s.old" % (original_file))

Expand Down
2 changes: 1 addition & 1 deletion platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ lib_deps =
bitbank2/AnimatedGIF
bitbank2/PNGdec @ ^1.1.2
ESP32Async/ESPAsyncWebServer
fastled/FastLED @^3.10.3
fastled/FastLED @3.10.3
jgromes/RadioLib @ ^7.4.0
; Pinned to fixed commits so upstream changes never break our patches
; (applied by patch_library_conflicts.py): IRQ-less I2C polling for
Expand Down
22 changes: 22 additions & 0 deletions src/core/bus_HAL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
#include "core/configPins.h"
#include "globals.h"
#include "soc/soc_caps.h"
#ifdef CARDPUTER_ADV_3IN1
#include "adv_3in1.h"
// The NRF driver owns the keyboard pin transition; stop it before another radio.
void nrf_stop();
#endif
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>

Expand Down Expand Up @@ -299,6 +304,23 @@ bool checkAndRecoverSysI2CBus() {

// ---------------- SPI bus arbitration ----------------

void prepareRadioSPI(RadioSPISelection selected) {
#ifdef CARDPUTER_ADV_3IN1
if (selected != RadioSPISelection::NRF24 && cardputerAdvNrfActive()) nrf_stop();
auto deselect = [](gpio_num_t pin) {
if (pin == GPIO_NUM_NC) return;
digitalWrite(pin, HIGH);
pinMode(pin, OUTPUT);
};
if (selected != RadioSPISelection::CC1101) deselect(bruceConfigPins.CC1101_bus.cs);
if (selected != RadioSPISelection::LoRa) deselect(bruceConfigPins.LoRa_bus.cs);
// GPIO9 belongs to Wire1 in keyboard mode. Its directly connected NRF CS
// cannot be driven HIGH here; electrical exclusivity needs hardware isolation.
#else
(void)selected;
#endif
}

// Pins currently configured on the shared auxiliary bus (AUX_SPI), so repeated acquisitions
// with the same pins skip a redundant end()/begin() cycle that could disturb whoever else is
// mid-transaction on it.
Expand Down
Loading