The T-Display P4 keyboard expansion (TCA8418 + its own XL9555, ported from camillia-mt) now works. It is looked for every 1.5 s, so it can be clipped on or off while running, and is routed like the CardKB and Bluetooth keyboards: it types into the focused field and keeps the on-screen keyboard down. The external-keyboard routing was gated on Bluetooth, which the P4 lacks, so it now follows a new CAP_EXT_KEYBOARD; the Bluetooth-only parts stay on CAP_BLE_KEYBOARD. - Keys: Shift/Sym/Alt one-shot, Caps Lock lights the expansion LEDs, arrows and Backspace repeat, Alt+Backspace backs out, F1-F5 open the tabs left to right, and F11 steps the keyboard light through off/low/medium/high (saved across reboots). - Wire1 is shared with the ES8311 codec on other pins; P4I2c1 switches it under a lock, and the codec's lazy bring-up now goes through it. - With the keyboard attached the tab bar labels F1-F5 beside each icon, and Enter on a chat bubble opens its action menu. Also, on any keyboard build, Enter runs the command in the terminal and the repeater admin console (the admin console's Enter hook sat on its Send button, where nothing raised it). And on the P4: the status-bar name shrinks to fit the bar, the Discover buttons are spaced out, contact names clear their icon in landscape, and leaving an app returns the focus to its tile in the app drawer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
8.2 KiB
C++
209 lines
8.2 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
#if defined(HAS_TDISPLAY_P4)
|
||
// ES8311 register code adapted from Espressif's esp-bsp components/es8311 (Apache-2.0,
|
||
// SPDX-FileCopyrightText: 2015-2026 Espressif Systems) — trimmed to the playback path this
|
||
// board needs and re-based onto Arduino Wire1, because the original talks the LEGACY
|
||
// driver/i2c API which aborts at boot when linked next to arduino-esp32 3.x's new-driver
|
||
// Wire. Clocking is fixed at MCLK = 256×fs from the MCLK pin, so one static divider row
|
||
// ({4096000, 16000}: pre_div 1, mult 1x, adc/dac_div 1, bclk_div 4, osr 0x10) covers us.
|
||
#include "P4Audio.h"
|
||
#include "P4I2c1.h"
|
||
#include <Arduino.h>
|
||
#include <Wire.h>
|
||
#include <math.h>
|
||
#include "driver/i2s_std.h"
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/semphr.h"
|
||
|
||
// t_display_p4_config.h (LilyGo): ES8311 on IIC_2 (pins 20/21, in P4I2c1.cpp), I2S pins below.
|
||
#define P4A_ADDR 0x18
|
||
#define P4A_MCLK 13
|
||
#define P4A_BCLK 12
|
||
#define P4A_WS 9
|
||
#define P4A_DOUT 10
|
||
|
||
#define P4A_RATE 16000
|
||
#define P4A_MCLK_HZ (P4A_RATE * 256)
|
||
|
||
// ---- minimal ES8311 register layer (names == hex addresses in the datasheet map) ----
|
||
static bool es8311Write(uint8_t reg, uint8_t val) {
|
||
Wire1.beginTransmission(P4A_ADDR);
|
||
Wire1.write(reg);
|
||
Wire1.write(val);
|
||
return Wire1.endTransmission() == 0;
|
||
}
|
||
static bool es8311Read(uint8_t reg, uint8_t* val) {
|
||
Wire1.beginTransmission(P4A_ADDR);
|
||
Wire1.write(reg);
|
||
if (Wire1.endTransmission(false) != 0) return false;
|
||
if (Wire1.requestFrom((uint8_t)P4A_ADDR, (uint8_t)1) != 1) return false;
|
||
*val = Wire1.read();
|
||
return true;
|
||
}
|
||
|
||
// es8311_init() for clk{mclk_from_mclk_pin, 4.096 MHz, 16 kHz} + 16-bit I2S slave + the
|
||
// "NOT default" analog power-ups, then DAC volume + unmute. Register values follow the
|
||
// esp-bsp driver verbatim.
|
||
static bool es8311Init() {
|
||
uint8_t v;
|
||
if (!es8311Write(0x00, 0x1F)) return false; // reset
|
||
vTaskDelay(pdMS_TO_TICKS(20));
|
||
es8311Write(0x00, 0x00);
|
||
es8311Write(0x00, 0x80); // power-on, CSM on
|
||
|
||
es8311Write(0x01, 0x3F); // all clocks on, MCLK from MCLK pin
|
||
if (es8311Read(0x06, &v)) es8311Write(0x06, v & ~0x20); // SCLK not inverted
|
||
|
||
// divider row {mclk=4096000, rate=16000}: pre_div=1 mult=1x adc/dac_div=1 fs_mode=0
|
||
// lrck 0x00/0xFF bclk_div=4 osr 0x10/0x10 (es8311_sample_frequency_config)
|
||
if (es8311Read(0x02, &v)) es8311Write(0x02, (uint8_t)((v & 0x07) | ((1 - 1) << 5) | (0 << 3)));
|
||
es8311Write(0x03, (uint8_t)((0 << 6) | 0x10)); // fs_mode | adc_osr
|
||
es8311Write(0x04, 0x10); // dac_osr
|
||
es8311Write(0x05, (uint8_t)(((1 - 1) << 4) | (1 - 1))); // adc_div | dac_div
|
||
if (es8311Read(0x06, &v)) es8311Write(0x06, (uint8_t)((v & 0xE0) | (4 - 1))); // bclk_div
|
||
if (es8311Read(0x07, &v)) es8311Write(0x07, (uint8_t)((v & 0xC0) | 0x00)); // lrck_h
|
||
es8311Write(0x08, 0xFF); // lrck_l
|
||
|
||
if (es8311Read(0x00, &v)) es8311Write(0x00, v & 0xBF); // slave serial port
|
||
es8311Write(0x09, (uint8_t)(3 << 2)); // SDP-in 16-bit
|
||
es8311Write(0x0A, (uint8_t)(3 << 2)); // SDP-out 16-bit
|
||
|
||
es8311Write(0x0D, 0x01); // power up analog circuitry
|
||
es8311Write(0x0E, 0x02); // enable analog PGA / ADC modulator
|
||
es8311Write(0x12, 0x00); // power up DAC
|
||
es8311Write(0x13, 0x10); // enable output to HP drive
|
||
es8311Write(0x1C, 0x6A); // ADC EQ bypass, cancel DC offset
|
||
es8311Write(0x37, 0x08); // bypass DAC equalizer
|
||
|
||
es8311Write(0x32, (uint8_t)(85 * 256 / 100 - 1)); // DAC volume ~85% (loudness = amplitude)
|
||
if (es8311Read(0x31, &v)) es8311Write(0x31, v & ~0x60); // unmute
|
||
return true;
|
||
}
|
||
|
||
static i2s_chan_handle_t s_tx = nullptr;
|
||
static SemaphoreHandle_t s_mtx = nullptr; // one player at a time
|
||
static bool s_ready = false, s_failed = false;
|
||
|
||
bool p4AudioReady() {
|
||
if (s_ready) return true;
|
||
if (s_failed) return false;
|
||
if (!s_mtx) s_mtx = xSemaphoreCreateMutex();
|
||
if (!s_mtx) return false;
|
||
xSemaphoreTake(s_mtx, portMAX_DELAY);
|
||
if (s_ready || s_failed) { xSemaphoreGive(s_mtx); return s_ready; }
|
||
|
||
// Wire1 is shared with the keyboard expansion on other pins (P4I2c1.h): take it
|
||
// and point it at the codec for the register writes, then hand it back. The
|
||
// codec is not touched over I2C again once it is up.
|
||
bool present = false, inited = false;
|
||
if (p4I2c1Acquire(P4_I2C1_AUDIO, UINT32_MAX)) {
|
||
Wire1.beginTransmission(P4A_ADDR);
|
||
present = (Wire1.endTransmission() == 0);
|
||
inited = present && es8311Init();
|
||
p4I2c1Release();
|
||
}
|
||
if (!inited) {
|
||
printf("[P4AUDIO] ES8311 %s\n", present ? "init failed" : "not found @0x18");
|
||
s_failed = true;
|
||
xSemaphoreGive(s_mtx);
|
||
return false;
|
||
}
|
||
|
||
i2s_chan_config_t ch = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
|
||
ch.auto_clear = true; // underrun sends silence, not a looping last buffer
|
||
if (i2s_new_channel(&ch, &s_tx, nullptr) != ESP_OK) {
|
||
s_failed = true;
|
||
xSemaphoreGive(s_mtx);
|
||
return false;
|
||
}
|
||
i2s_std_config_t std_cfg = {
|
||
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(P4A_RATE),
|
||
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
|
||
.gpio_cfg = {
|
||
.mclk = (gpio_num_t)P4A_MCLK,
|
||
.bclk = (gpio_num_t)P4A_BCLK,
|
||
.ws = (gpio_num_t)P4A_WS,
|
||
.dout = (gpio_num_t)P4A_DOUT,
|
||
.din = GPIO_NUM_NC,
|
||
.invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false },
|
||
},
|
||
};
|
||
std_cfg.clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_256; // codec dividers assume 256×fs
|
||
if (i2s_channel_init_std_mode(s_tx, &std_cfg) != ESP_OK ||
|
||
i2s_channel_enable(s_tx) != ESP_OK) {
|
||
printf("[P4AUDIO] I2S init failed\n");
|
||
i2s_channel_disable(s_tx);
|
||
i2s_del_channel(s_tx);
|
||
s_tx = nullptr;
|
||
s_failed = true;
|
||
xSemaphoreGive(s_mtx);
|
||
return false;
|
||
}
|
||
printf("[P4AUDIO] ES8311 up (16 kHz, MCLK 256x)\n");
|
||
s_ready = true;
|
||
xSemaphoreGive(s_mtx);
|
||
return true;
|
||
}
|
||
|
||
void p4AudioTone(int freq_hz, int duration_ms, int amplitude) {
|
||
if (amplitude <= 0 || freq_hz <= 0 || duration_ms <= 0) return;
|
||
if (!p4AudioReady()) return;
|
||
if (amplitude > 30000) amplitude = 30000;
|
||
if (xSemaphoreTake(s_mtx, 0) != pdTRUE) return;
|
||
const int total = P4A_RATE * duration_ms / 1000;
|
||
const int fade = P4A_RATE * 4 / 1000; // 4 ms in/out fade kills the click
|
||
static int16_t buf[256 * 2]; // stereo frames, chunked
|
||
int done = 0;
|
||
float phase = 0.0f;
|
||
const float step = 2.0f * (float)M_PI * (float)freq_hz / (float)P4A_RATE;
|
||
while (done < total) {
|
||
int n = 0;
|
||
for (; n < 256 && done < total; ++n, ++done) {
|
||
float env = 1.0f;
|
||
if (done < fade) env = (float)done / fade;
|
||
else if (done > total - fade) env = (float)(total - done) / fade;
|
||
int16_t s = (int16_t)((float)amplitude * env * sinf(phase));
|
||
phase += step;
|
||
if (phase > 2.0f * (float)M_PI) phase -= 2.0f * (float)M_PI;
|
||
buf[2 * n] = buf[2 * n + 1] = s;
|
||
}
|
||
size_t wr = 0;
|
||
i2s_channel_write(s_tx, buf, (size_t)n * 2 * sizeof(int16_t), &wr, pdMS_TO_TICKS(300));
|
||
}
|
||
xSemaphoreGive(s_mtx);
|
||
}
|
||
|
||
uint32_t p4AudioStreamRate() { return P4A_RATE; }
|
||
|
||
bool p4AudioStreamBegin() {
|
||
if (!p4AudioReady()) return false;
|
||
return xSemaphoreTake(s_mtx, portMAX_DELAY) == pdTRUE;
|
||
}
|
||
|
||
bool p4AudioStreamWrite(const int16_t* samples, size_t frames) {
|
||
if (!samples || !frames || !s_tx) return false;
|
||
static int16_t stereo[256 * 2];
|
||
while (frames) {
|
||
const size_t count = frames > 256 ? 256 : frames;
|
||
for (size_t i = 0; i < count; ++i)
|
||
stereo[2 * i] = stereo[2 * i + 1] = samples[i];
|
||
size_t written = 0;
|
||
const size_t bytes = count * 2 * sizeof(int16_t);
|
||
if (i2s_channel_write(s_tx, stereo, bytes, &written, pdMS_TO_TICKS(300)) != ESP_OK ||
|
||
written != bytes) return false;
|
||
samples += count;
|
||
frames -= count;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void p4AudioStreamEnd() {
|
||
if (!s_mtx) return;
|
||
static const int16_t silence[256 * 2] = {};
|
||
size_t written = 0;
|
||
i2s_channel_write(s_tx, silence, sizeof silence, &written, pdMS_TO_TICKS(300));
|
||
xSemaphoreGive(s_mtx);
|
||
}
|
||
|
||
#endif // HAS_TDISPLAY_P4
|