Files
wadamesh/variants/tdisplay_p4/P4Audio.cpp
T
Michael A. CojocariandClaude Opus 5.5 ad91b1cb3f feat(tdisplay-p4): clip-on keyboard expansion, plus keyboard and layout fixes
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>
2026-09-30 20:10:12 -04:00

209 lines
8.2 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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