Merge pull request 'feat(plip): SLIC integration + I2S RX mic fix + /debug/ring,slic' (#22) from feat/plip-slic-integration into main
CI / platformio (push) Failing after 5m56s

This commit was merged in pull request #22.
This commit is contained in:
2026-06-14 23:01:39 +00:00
11 changed files with 384 additions and 66 deletions
+2
View File
@@ -11,9 +11,11 @@ idf_component_register(
"dialer.c"
"conversation.c"
"turn_client.c"
"slic.c"
INCLUDE_DIRS "."
PRIV_REQUIRES
driver
esp_driver_gpio
esp_event
esp_http_client
esp_http_server
+12 -3
View File
@@ -38,10 +38,19 @@ menu "PLIP Voice Configuration"
config PLIP_HOOK_GPIO
int "Off-hook GPIO number"
default 4
default 23
help
GPIO that signals handset off-hook (active LOW via INPUT_PULLUP).
Dev kit uses BOOT button (GPIO4 / KEY1). PCB uses Si3210 INT.
GPIO that signals handset off-hook.
SLIC SHK line is GPIO23 (A1S board KEY4, re-assigned to SLIC).
Legacy dev kit stub used GPIO4 (BOOT button, active-LOW).
config PLIP_HOOK_ACTIVE_HIGH
bool "Hook GPIO active-HIGH means off-hook"
default y
help
When enabled, a HIGH level on PLIP_HOOK_GPIO means the handset is
off-hook (SLIC SHK polarity). When disabled, LOW means off-hook
(original dev-kit pull-up + BOOT button polarity).
config PLIP_DIAL_PULSE
bool "Enable rotary dial pulse decoding on SHK GPIO"
+9 -25
View File
@@ -428,7 +428,13 @@ esp_err_t audio_init(void)
return ret;
}
/* 3a. TX (speaker): Philips 16-bit stereo @ 16 kHz. */
/* 3a+3b. Full-duplex I2S config: SAME gpio_cfg for TX and RX.
* IDF5 i2s_std constitutes full-duplex ONLY when TX and RX configs are
* identical (memcmp). If they differ, on ESP32 HW v1 it tries to move
* RX to I2S_NUM_1, breaking clock routing and yielding permanent zeros.
* Fix: set both dout=PLIP_I2S_DOUT and din=PLIP_I2S_DIN in the same
* config and apply it to both handles. The IDF GPIO driver handles
* direction internally (dout→output, din→input). */
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
@@ -438,7 +444,7 @@ esp_err_t audio_init(void)
.bclk = PLIP_I2S_BCLK,
.ws = PLIP_I2S_WS,
.dout = PLIP_I2S_DOUT,
.din = I2S_GPIO_UNUSED,
.din = PLIP_I2S_DIN,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
@@ -451,29 +457,7 @@ esp_err_t audio_init(void)
ESP_LOGE(TAG, "i2s_channel_init_std_mode (TX): %s", esp_err_to_name(ret));
return ret;
}
/* 3b. RX (mic): same GPIO config as TX except DIN=GPIO35, DOUT=UNUSED.
* In full-duplex std mode, IDF requires the same BCLK/WS GPIOs to be
* specified in both TX and RX configs — it deduplicates GPIO matrix
* routing internally. Using UNUSED for BCLK/WS causes zero-data RX. */
i2s_std_config_t rx_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = PLIP_I2S_MCLK,
.bclk = PLIP_I2S_BCLK,
.ws = PLIP_I2S_WS,
.dout = I2S_GPIO_UNUSED,
.din = PLIP_I2S_DIN,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ret = i2s_channel_init_std_mode(s_mic_handle, &rx_cfg);
ret = i2s_channel_init_std_mode(s_mic_handle, &std_cfg);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "i2s_channel_init_std_mode (RX): %s", esp_err_to_name(ret));
return ret;
+7
View File
@@ -40,3 +40,10 @@
/* ---------- Off-hook GPIO (dev kit uses BOOT/KEY1 GPIO4 as stand-in) ---------- */
/* Actual value comes from CONFIG_PLIP_HOOK_GPIO (Kconfig) */
/* ---------- SLIC K50835F / AG1171-class front-end (A1S board wiring) ---------- */
/* KEY3=GPIO19, KEY4=GPIO23, KEY5=GPIO18, KEY6=GPIO5 share these pins — reassigned to SLIC */
#define PLIP_SLIC_RM 18 /* Ring Mode output — HIGH = ring burst active */
#define PLIP_SLIC_FR 5 /* Forward/Reverse output — toggled at 25 Hz for bell */
#define PLIP_SLIC_SHK 23 /* Switch Hook input — HIGH = off-hook (active-high) */
#define PLIP_SLIC_PD 19 /* Power Down (open-drain) — HIGH = SLIC active */
+18 -10
View File
@@ -152,10 +152,14 @@ esp_err_t es8388_init(void)
* - ADCCONTROL5 (0x0D) = 0x02: ADCFsMode SINGLE SPEED RATIO=256 (16kHz@MCLK 4.096MHz)
* - ADCCONTROL8/9 (0x10/0x11) = 0x00: ADC digital volume 0dB */
if (i2c_write_reg(ES8388_ADC_CTL1, 0xBB) != ESP_OK) return ESP_FAIL; /* PGA +24dB L+R */
if (i2c_write_reg(ES8388_ADC_CTL2, 0x00) != ESP_OK) return ESP_FAIL; /* LIN1/RIN1 differential (onboard mic) */
/* ADCCONTROL2 (0x0A): LINSEL/RINSEL = 0x00 → LINPUT1/RINPUT1 (LINE IN header on A1S kit)
* 0x00 = LIN1/RIN1 differential (standard LINE IN jack where AG1171 SLIC audio is wired)
* 0x50 = LIN2/RIN2 single-ended (onboard mic path — no SLIC signal here) */
if (i2c_write_reg(ES8388_ADC_CTL2, 0x00) != ESP_OK) return ESP_FAIL; /* LIN1/RIN1 differential (LINE IN / AG1171 SLIC) */
if (i2c_write_reg(ES8388_ADC_CTL3, 0x02) != ESP_OK) return ESP_FAIL; /* DS filter sel */
if (i2c_write_reg(ES8388_ADC_CTL4, 0x0C) != ESP_OK) return ESP_FAIL; /* I2S 16-bit */
if (i2c_write_reg(ES8388_ADC_CTL5, 0x02) != ESP_OK) return ESP_FAIL; /* RATIO=256 */
if (i2c_write_reg(0x0E, 0x00) != ESP_OK) return ESP_FAIL; /* ADCCONTROL6: clear ADCSMUTE bit5 (reset default 0x30 = ADC output muted) */
if (i2c_write_reg(ES8388_ADC_CTL8, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol L 0dB */
if (i2c_write_reg(ES8388_ADC_CTL9, 0x00) != ESP_OK) return ESP_FAIL; /* ADC vol R 0dB */
@@ -173,11 +177,12 @@ esp_err_t es8388_init(void)
if (i2c_write_reg(ES8388_DAC_CTL17, 0x90) != ESP_OK) return ESP_FAIL;
if (i2c_write_reg(ES8388_DAC_CTL20, 0x90) != ESP_OK) return ESP_FAIL;
/* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync = 0xC0 (both ADC and DAC share LRCK).
* Espressif reference: 0xC0 when both ADC and DAC active (record+playback mode).
* 0x80 when DAC only.
* This register is a clock routing register, NOT a volume register. */
if (i2c_write_reg(ES8388_DAC_CTL21, 0xC0) != ESP_OK) return ESP_FAIL;
/* 11. DACCONTROL21 (0x2B): ADC+DAC LRCK sync.
* Espressif reference es8388_start():
* 0xC0 = LINE mode (analog bypass, not ADC digital path)
* 0x80 = DAC+ADC digital record+playback mode (bit7 only)
* We want digital ADC recording + DAC playback → use 0x80. */
if (i2c_write_reg(ES8388_DAC_CTL21, 0x80) != ESP_OK) return ESP_FAIL;
/* 11b. Restart internal state machine (required after DACCONTROL21 change).
* Without this pulse, the ADC clock domain may not synchronise properly.
@@ -198,9 +203,12 @@ esp_err_t es8388_init(void)
/* 13. DAC power: power up DAC L+R. */
if (i2c_write_reg(ES8388_DAC_POWER, 0x3C) != ESP_OK) return ESP_FAIL;
/* 14. ADC power: power up ADC (0x09 per Espressif reference).
* Must come AFTER DACCONTROL21 state machine restart and all ADC register writes. */
if (i2c_write_reg(ES8388_ADC_POWER, 0x09) != ESP_OK) return ESP_FAIL;
/* 14. ADC power: full power-up (all Pdn bits cleared = 0x00).
* 0x09 is the intermediate state (Espressif es8388_open end-state), but
* the full ADC + analog input power-up requires 0x00 (Espressif es8388_start(ADC)).
* Must come AFTER DACCONTROL21 state machine restart and ADCCONTROL6 unmute. */
if (i2c_write_reg(ES8388_ADC_POWER, 0x00) != ESP_OK) return ESP_FAIL;
vTaskDelay(pdMS_TO_TICKS(10)); /* let ADC analog settle after full power-up */
/* 15. Enable PA (power amplifier for speaker). */
gpio_set_direction(PLIP_PA_ENABLE, GPIO_MODE_OUTPUT);
@@ -216,7 +224,7 @@ esp_err_t es8388_init(void)
es8388_read_reg(ES8388_CHIP_POWER, &chippower);
ESP_LOGI(TAG, "ES8388 regs: CTL1=0x%02X ADCPWR=0x%02X ADCINSEL=0x%02X ADCCTL3=0x%02X DACCTL21=0x%02X CHIPPOWER=0x%02X",
ctl1, adcpwr, adcinsel, adcctl3, dacctl21, chippower);
ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB, input=LIN1, DACCTL21=0xC0");
ESP_LOGI(TAG, "ES8388 init OK — PA enabled, DAC @ 0dB, ADC PGA +24dB, input=LIN1/RIN1 (LINE IN), DACCTL21=0x80");
return ESP_OK;
}
+13 -1
View File
@@ -24,11 +24,13 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "board_config.h"
#include "audio.h"
#include "net.h"
#include "cmd_exec.h"
#include "hook_client.h"
#include "phone.h"
#include "slic.h"
#include "scenario_mesh.h"
#include "dialer.h"
#include "conversation.h"
@@ -60,8 +62,18 @@ static void boot_task(void *arg)
{
(void)arg;
/* ── 1b. SLIC power-up (before audio — PD must be released early) ── */
esp_err_t ret = slic_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "slic_init FAILED: %s — SLIC remains in power-down (no audio path, no hook sense)",
esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "SLIC powered up: PD=GPIO%d OD-HIGH, SHK=GPIO%d active-HIGH, RM=GPIO%d, FR=GPIO%d",
PLIP_SLIC_PD, PLIP_SLIC_SHK, PLIP_SLIC_RM, PLIP_SLIC_FR);
}
/* ── 2. Audio init (Phase A) ── */
esp_err_t ret = audio_init();
ret = audio_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "audio_init FAILED: %s", esp_err_to_name(ret));
ESP_LOGE(TAG, "PHASE A BLOCKED — check ES8388 I2C wiring (SDA=33, SCL=32)");
+55 -2
View File
@@ -26,7 +26,11 @@
#include "audio.h"
#include "cmd_exec.h"
#include "phone.h"
#include "es8388.h"
#include "slic.h"
#include "board_config.h"
#include "driver/gpio.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_http_server.h"
@@ -429,6 +433,40 @@ static esp_err_t handle_cmd_post(httpd_req_t *req)
return send_json(req, "200 OK", "{\"ok\":true}");
}
/* ── GET /debug/ring (trigger ring tone over HTTP) ──────────────────────── */
static esp_err_t handle_debug_ring(httpd_req_t *req)
{
phone_ring_start();
ESP_LOGI(TAG, "debug/ring: ring started");
return send_json(req, "200 OK", "{\"ok\":true,\"ringing\":true}");
}
/* ── GET /debug/ringstop (stop ring tone over HTTP) ─────────────────────── */
static esp_err_t handle_debug_ringstop(httpd_req_t *req)
{
phone_ring_stop();
ESP_LOGI(TAG, "debug/ringstop: ring stopped");
return send_json(req, "200 OK", "{\"ok\":true,\"ringing\":false}");
}
/* ── GET /debug/slic (read GPIO levels of all 4 SLIC pins) ──────────────── */
static esp_err_t handle_debug_slic(httpd_req_t *req)
{
char buf[192];
/* gpio_get_level reads the actual pad level even for output pins. */
snprintf(buf, sizeof(buf),
"{\"rm\":%d,\"fr\":%d,\"shk\":%d,\"pd\":%d,\"offhook\":%s,\"ringing\":%s}",
gpio_get_level(PLIP_SLIC_RM), gpio_get_level(PLIP_SLIC_FR),
gpio_get_level(PLIP_SLIC_SHK), gpio_get_level(PLIP_SLIC_PD),
slic_is_offhook() ? "true" : "false",
slic_is_ringing() ? "true" : "false");
ESP_LOGI(TAG, "debug/slic: %s", buf);
return send_json(req, "200 OK", buf);
}
/* ── GET /debug/dial?number=NNNN (push digits into the dialer) ───────────── */
static esp_err_t handle_debug_dial(httpd_req_t *req)
@@ -527,7 +565,7 @@ esp_err_t net_init(void)
/* Start HTTP server. */
httpd_config_t hcfg = HTTPD_DEFAULT_CONFIG();
hcfg.server_port = 80;
hcfg.max_uri_handlers = 14;
hcfg.max_uri_handlers = 17;
hcfg.stack_size = 8192;
esp_err_t ret = httpd_start(&s_httpd, &hcfg);
@@ -564,6 +602,18 @@ esp_err_t net_init(void)
.uri = "/debug/dial", .method = HTTP_GET,
.handler = handle_debug_dial, .user_ctx = NULL,
};
static const httpd_uri_t uri_debug_ring = {
.uri = "/debug/ring", .method = HTTP_GET,
.handler = handle_debug_ring, .user_ctx = NULL,
};
static const httpd_uri_t uri_debug_ringstop = {
.uri = "/debug/ringstop", .method = HTTP_GET,
.handler = handle_debug_ringstop, .user_ctx = NULL,
};
static const httpd_uri_t uri_debug_slic = {
.uri = "/debug/slic", .method = HTTP_GET,
.handler = handle_debug_slic, .user_ctx = NULL,
};
httpd_register_uri_handler(s_httpd, &uri_status);
httpd_register_uri_handler(s_httpd, &uri_scenario);
httpd_register_uri_handler(s_httpd, &uri_file);
@@ -571,7 +621,10 @@ esp_err_t net_init(void)
httpd_register_uri_handler(s_httpd, &uri_capture);
httpd_register_uri_handler(s_httpd, &uri_debug_regs);
httpd_register_uri_handler(s_httpd, &uri_debug_dial);
httpd_register_uri_handler(s_httpd, &uri_debug_ring);
httpd_register_uri_handler(s_httpd, &uri_debug_ringstop);
httpd_register_uri_handler(s_httpd, &uri_debug_slic);
ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, GET /debug/dial, POST /game/scenario, POST /game/file, POST /game/cmd, POST /voice/capture)");
ESP_LOGI(TAG, "httpd up on :80 (GET /status, GET /debug/regs, GET /debug/dial, GET /debug/ring, GET /debug/ringstop, GET /debug/slic, POST /game/scenario, POST /game/file, POST /game/cmd, POST /voice/capture)");
return ESP_OK;
}
+55 -25
View File
@@ -19,6 +19,7 @@
#include "audio.h"
#include "hook_client.h"
#include "conversation.h"
#include "slic.h"
#if CONFIG_PLIP_DIAL_PULSE
#include "dialer.h"
@@ -52,7 +53,10 @@ void phone_ring_start(void)
{
if (s_ringing) return;
s_ringing = true;
ESP_LOGI(TAG, "ring start");
ESP_LOGI(TAG, "ring start: SLIC RM/FR + audio tone");
/* Physical bell via SLIC RM/FR (main ringer) */
slic_ring_start();
/* Optional in-earpiece audio tone (audible feedback when handset is up) */
audio_ring_start();
}
@@ -60,7 +64,8 @@ void phone_ring_stop(void)
{
if (!s_ringing) return;
s_ringing = false;
ESP_LOGI(TAG, "ring stop");
ESP_LOGI(TAG, "ring stop: SLIC RM/FR off + audio stop");
slic_ring_stop();
audio_stop();
}
@@ -70,6 +75,26 @@ static void report_offhook(bool offhook)
hook_client_report(offhook ? "off" : "on", offhook ? "pickup" : "hangup");
}
/*
* Hook polarity:
* CONFIG_PLIP_HOOK_ACTIVE_HIGH=y (default, SLIC SHK GPIO23): HIGH = off-hook
* CONFIG_PLIP_HOOK_ACTIVE_HIGH=n (legacy BOOT button GPIO4): LOW = off-hook
*
* Rotary pulse notes (SLIC SHK, active-HIGH):
* Off-hook base: SHK HIGH. A rotary pulse briefly opens the loop → SHK drops LOW
* for ~60-100 ms, then returns HIGH. Hangup: SHK stays LOW for >500 ms.
* So "open" (pulse event) = level drops LOW when active-HIGH polarity.
*/
#if CONFIG_PLIP_HOOK_ACTIVE_HIGH
#define HOOK_OFFHOOK_LEVEL 1 /* HIGH = off-hook */
#define HOOK_PULSE_OPEN 0 /* LOW = rotary pulse open (loop broken) */
#define HOOK_PULSE_CLOSED 1 /* HIGH = rotary pulse closed (loop restored) */
#else
#define HOOK_OFFHOOK_LEVEL 0 /* LOW = off-hook (legacy pull-up + BOOT button) */
#define HOOK_PULSE_OPEN 1 /* HIGH = rotary pulse open */
#define HOOK_PULSE_CLOSED 0 /* LOW = rotary pulse closed */
#endif
static void phone_task(void *arg)
{
(void)arg;
@@ -88,9 +113,11 @@ static void phone_task(void *arg)
/* Read and report initial level so master state machine is in sync. */
int last_level = gpio_get_level(hook_gpio);
s_offhook = (last_level == 0);
ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d (%s)",
hook_gpio, last_level, s_offhook ? "off-hook" : "on-hook");
s_offhook = (last_level == HOOK_OFFHOOK_LEVEL);
ESP_LOGI(TAG, "phone task ready, hook GPIO=%d level=%d active_%s (%s)",
hook_gpio, last_level,
(HOOK_OFFHOOK_LEVEL == 1) ? "high" : "low",
s_offhook ? "off-hook" : "on-hook");
if (!s_offhook) {
ESP_LOGI(TAG, "boot: on-hook -> PA off (mute)");
@@ -104,11 +131,12 @@ static void phone_task(void *arg)
conversation_on_hook_change(s_offhook);
#if CONFIG_PLIP_DIAL_PULSE
/* Rotary pulse state */
/* Rotary pulse state.
* With SLIC SHK (active-HIGH): off-hook = HIGH, pulse = brief LOW dip. */
int pulse_count = 0;
bool in_pulse = false; /* GPIO is currently high (open) */
int64_t pulse_open_us = 0; /* timestamp when GPIO went high */
int64_t last_close_us = 0; /* timestamp when GPIO last went low (closed) */
bool in_pulse = false; /* true while SHK is in pulse-open state */
int64_t pulse_open_us = 0; /* timestamp when pulse open started */
int64_t last_close_us = 0; /* timestamp when pulse closed (SHK back to OFFHOOK) */
#endif
for (;;) {
@@ -119,19 +147,19 @@ static void phone_task(void *arg)
/* While off-hook, use fast polling instead of 30ms debounce
* so rotary pulses (~60-100ms) are not missed. */
if (s_offhook) {
/* The ISR fired — re-read immediately to catch rising edge. */
/* The ISR fired — re-read immediately to catch the edge. */
vTaskDelay(pdMS_TO_TICKS(5)); /* minimal settle */
int level = gpio_get_level(hook_gpio);
if (level == 1 && last_level == 0) {
/* Rising edge: GPIO went open */
last_level = 1;
if (level == HOOK_PULSE_OPEN && last_level == HOOK_PULSE_CLOSED) {
/* Pulse open: loop broken (SHK dropped from off-hook level) */
last_level = HOOK_PULSE_OPEN;
in_pulse = true;
pulse_open_us = esp_timer_get_time();
ESP_LOGD(TAG, "pulse: open");
} else if (level == 0 && last_level == 1) {
/* Falling edge: GPIO closed again */
last_level = 0;
} else if (level == HOOK_PULSE_CLOSED && last_level == HOOK_PULSE_OPEN) {
/* Pulse closed: loop restored */
last_level = HOOK_PULSE_CLOSED;
last_close_us = esp_timer_get_time();
int64_t open_dur_ms = (last_close_us - pulse_open_us) / 1000;
@@ -155,12 +183,12 @@ static void phone_task(void *arg)
goto poll_sleep;
}
#endif
/* Standard 30 ms debounce for on-hook state transitions */
/* Standard 30 ms debounce for on-hook/off-hook transitions */
vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
int level = gpio_get_level(hook_gpio);
if (level != last_level) {
last_level = level;
if (level == 0) {
if (level == HOOK_OFFHOOK_LEVEL) {
/* Off-hook: handset picked up. */
s_offhook = true;
#if CONFIG_PLIP_DIAL_PULSE
@@ -168,7 +196,8 @@ static void phone_task(void *arg)
in_pulse = false;
last_close_us = esp_timer_get_time();
#endif
ESP_LOGI(TAG, "off-hook (pickup) detected — PA on");
ESP_LOGI(TAG, "off-hook (pickup) detected — PA on, SLIC offhook=%d",
slic_is_offhook());
audio_pa_set(true);
phone_ring_stop();
report_offhook(true);
@@ -185,7 +214,7 @@ static void phone_task(void *arg)
#if CONFIG_PLIP_DIAL_PULSE
/* Check for inter-digit gap: if we accumulated pulses and the line
* has been closed (quiet) for > MAX_GAP_MS, emit the digit. */
* has been at closed (off-hook) state for > MAX_GAP_MS, emit the digit. */
if (s_offhook && pulse_count > 0 && !in_pulse && last_close_us > 0) {
int64_t gap_ms = (esp_timer_get_time() - last_close_us) / 1000;
if (gap_ms > CONFIG_PLIP_DIAL_PULSE_MAX_GAP_MS) {
@@ -201,14 +230,15 @@ static void phone_task(void *arg)
}
}
/* Also detect prolonged open (hangup) even if no more edges arrive */
/* Also detect prolonged open (hangup) even if no more edges arrive.
* Active-HIGH: hangup = level stays at HOOK_PULSE_OPEN (LOW) > 500 ms. */
if (s_offhook && in_pulse && pulse_open_us > 0) {
int64_t open_ms = (esp_timer_get_time() - pulse_open_us) / 1000;
if (open_ms >= HANGUP_THRESHOLD_MS) {
int level_now = gpio_get_level(hook_gpio);
if (level_now == 1) {
if (level_now == HOOK_PULSE_OPEN) {
ESP_LOGI(TAG, "on-hook (prolonged open %"PRId64"ms) detected", open_ms);
last_level = 1;
last_level = HOOK_PULSE_OPEN;
pulse_count = 0;
in_pulse = false;
s_offhook = false;
@@ -216,8 +246,8 @@ static void phone_task(void *arg)
audio_pa_set(false);
report_offhook(false);
} else {
/* GPIO closed already but ISR missed it */
last_level = 0;
/* GPIO returned to off-hook level but ISR missed it */
last_level = HOOK_PULSE_CLOSED;
last_close_us = esp_timer_get_time();
if ((esp_timer_get_time() - pulse_open_us) / 1000 < HANGUP_THRESHOLD_MS) {
pulse_count++;
+150
View File
@@ -0,0 +1,150 @@
/*
* slic.c — K50835F / AG1171-class SLIC control (ESP-IDF port).
*
* Ported from hardware/projects/slic-phone/src/slic/Ks0835SlicController.cpp
* (Arduino) to bare ESP-IDF 5.x GPIO driver.
*
* Key differences from Arduino original:
* - Open-drain on PD achieved via GPIO_MODE_OUTPUT_OD + gpio_set_level(PD, 1)
* - Ring FR toggle runs as a FreeRTOS task instead of a cooperative tick()
* - hook_active_high is hard-coded TRUE (matches A252ConfigStore default)
*/
#include "slic.h"
#include "board_config.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "slic"
/* Active-high hook polarity: HIGH on SHK = off-hook.
* Source: hardware/projects/slic-phone/src/config/A252ConfigStore.cpp:262
* cfg.hook_active_high = true; */
#define SLIC_SHK_OFFHOOK_LEVEL 1
static volatile bool s_ringing = false;
static TaskHandle_t s_ring_task = NULL;
/* ── FR toggle task (ring drive at ~25 Hz) ────────────────────────────────── */
static void slic_ring_task(void *arg)
{
(void)arg;
bool fr_state = false;
for (;;) {
if (s_ringing) {
fr_state = !fr_state;
gpio_set_level(PLIP_SLIC_FR, fr_state ? 1 : 0);
ESP_LOGV(TAG, "FR=%d", fr_state ? 1 : 0);
} else {
/* Suspended — keep FR low while idle */
gpio_set_level(PLIP_SLIC_FR, 0);
fr_state = false;
}
vTaskDelay(pdMS_TO_TICKS(20)); /* 20 ms → ~25 Hz */
}
}
/* ── Public API ──────────────────────────────────────────────────────────── */
esp_err_t slic_init(void)
{
/* RM: Ring Mode output, init LOW */
gpio_config_t rm_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_RM),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t ret = gpio_config(&rm_cfg);
if (ret != ESP_OK) return ret;
gpio_set_level(PLIP_SLIC_RM, 0);
/* FR: Forward/Reverse output, init LOW */
gpio_config_t fr_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_FR),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ret = gpio_config(&fr_cfg);
if (ret != ESP_OK) return ret;
gpio_set_level(PLIP_SLIC_FR, 0);
/* SHK: Switch Hook input with pull-up (physical line has no external pull) */
gpio_config_t shk_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_SHK),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ret = gpio_config(&shk_cfg);
if (ret != ESP_OK) return ret;
/* PD: Power Down — push-pull output HIGH = SLIC active (definite power-up).
* Previous config (open-drain HIGH) left the line floating when no pull-up
* is present on the bench, keeping the SLIC in power-down.
* Push-pull HIGH drives the PD pin firmly high regardless of external resistors. */
gpio_config_t pd_cfg = {
.pin_bit_mask = (1ULL << PLIP_SLIC_PD),
.mode = GPIO_MODE_OUTPUT, /* push-pull (pas OD) */
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ret = gpio_config(&pd_cfg);
if (ret != ESP_OK) return ret;
ret = gpio_set_level(PLIP_SLIC_PD, 1); /* HIGH franc = SLIC actif */
if (ret != ESP_OK) return ret;
ESP_LOGI(TAG, "slic: PD GPIO%d push-pull HIGH (SLIC power-up, franc)", PLIP_SLIC_PD);
ESP_LOGI(TAG, "slic: pins RM=%d FR=%d SHK=%d PD=%d hook_active_high=1",
PLIP_SLIC_RM, PLIP_SLIC_FR, PLIP_SLIC_SHK, PLIP_SLIC_PD);
/* Spawn the FR-toggle task (runs indefinitely, toggles only when s_ringing=true) */
BaseType_t ok = xTaskCreatePinnedToCore(slic_ring_task, "slic_ring",
2048, NULL, 4, &s_ring_task, 1);
if (ok != pdPASS) {
ESP_LOGE(TAG, "Failed to create slic_ring task");
return ESP_ERR_NO_MEM;
}
int shk = gpio_get_level(PLIP_SLIC_SHK);
ESP_LOGI(TAG, "SLIC init OK — SHK level=%d (%s)",
shk, (shk == SLIC_SHK_OFFHOOK_LEVEL) ? "off-hook" : "on-hook");
return ESP_OK;
}
bool slic_is_offhook(void)
{
return gpio_get_level(PLIP_SLIC_SHK) == SLIC_SHK_OFFHOOK_LEVEL;
}
void slic_ring_start(void)
{
if (s_ringing) return;
ESP_LOGI(TAG, "ring start: RM=HIGH, FR toggling at 25 Hz");
gpio_set_level(PLIP_SLIC_RM, 1);
s_ringing = true;
}
void slic_ring_stop(void)
{
if (!s_ringing) return;
ESP_LOGI(TAG, "ring stop: RM=LOW, FR=LOW");
s_ringing = false;
gpio_set_level(PLIP_SLIC_RM, 0);
gpio_set_level(PLIP_SLIC_FR, 0);
}
bool slic_is_ringing(void)
{
return s_ringing;
}
+59
View File
@@ -0,0 +1,59 @@
/*
* slic.h — K50835F / AG1171-class SLIC control (ESP-IDF port of Ks0835SlicController).
*
* Pins (A1S board, from board_config.h):
* RM GPIO18 Ring Mode output — HIGH = ring burst active
* FR GPIO5 Forward/Reverse out — toggled at ~25 Hz during ring to drive bell
* SHK GPIO23 Switch Hook input — HIGH = off-hook (active-high)
* PD GPIO19 Power Down (OD) — HIGH = SLIC powered (released open-drain)
*
* Power-up: PD is open-drain, written HIGH → high-impedance → SLIC active.
* Power-down: PD driven LOW → SLIC off (no audio, no hook sense).
*
* Ring: slic_ring_start() sets RM=1 and spawns/enables a 20 ms FR-toggle task.
* slic_ring_stop() clears RM=0, FR=0, pauses the toggle task.
*
* Hook: slic_is_offhook() reads SHK; HIGH = off-hook (matches A252ConfigStore default).
*/
#pragma once
#include <stdbool.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Initialise SLIC GPIO and power up the SLIC.
* Must be called early in boot, before audio init.
* Returns ESP_OK on success.
*/
esp_err_t slic_init(void);
/**
* Returns true when the handset is off-hook (SHK GPIO23 HIGH).
*/
bool slic_is_offhook(void);
/**
* Activate ringing: RM=HIGH + FR toggles at ~25 Hz (20 ms period).
* No-op if already ringing.
*/
void slic_ring_start(void);
/**
* Stop ringing: RM=LOW, FR=LOW, FR toggle suspended.
* No-op if not ringing.
*/
void slic_ring_stop(void);
/**
* Returns true if slic_ring_start() was called and not yet stopped.
*/
bool slic_is_ringing(void);
#ifdef __cplusplus
}
#endif
+4
View File
@@ -35,6 +35,10 @@ CONFIG_ESP_TASK_WDT_TIMEOUT_S=15
# when I2S/WiFi interrupts spill into idle stack on original ESP32.
CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=2048
# SLIC hook GPIO — SHK on GPIO23 (A1S KEY4, active-HIGH: HIGH = off-hook)
CONFIG_PLIP_HOOK_GPIO=23
CONFIG_PLIP_HOOK_ACTIVE_HIGH=y
# WiFi credentials — set via `idf.py menuconfig` (PLIP Voice Configuration)
# or override locally in sdkconfig (NOT committed).
# CONFIG_PLIP_WIFI_SSID="..."