feat(plip): SLIC control — power-up (PD OD-HIGH), hook on SHK GPIO23, ring RM/FR

- board_config.h: add PLIP_SLIC_RM=18, FR=5, SHK=23, PD=19 (A1S KEY3-6 repurposed)
- slic.c/slic.h: new ESP-IDF module porting Ks0835SlicController:
    * slic_init(): RM/FR output LOW, SHK input+pullup, PD open-drain HIGH (power-up)
    * slic_is_offhook(): reads SHK GPIO23, HIGH = off-hook (active-high, matches A252ConfigStore default)
    * slic_ring_start/stop(): RM HIGH + FreeRTOS task toggles FR at 25 Hz (20 ms period)
- CMakeLists.txt: add slic.c to SRCS, esp_driver_gpio to PRIV_REQUIRES
- Kconfig: PLIP_HOOK_GPIO default 4→23, add PLIP_HOOK_ACTIVE_HIGH (default y)
- phone.c: hook reads SHK GPIO23 via HOOK_OFFHOOK_LEVEL/HOOK_PULSE_OPEN macros (active-HIGH);
    phone_ring_start/stop() now drives slic_ring_start/stop() for physical bell + audio tone
- main.c: slic_init() called early in boot_task before audio_init

Root cause fixed: SLIC was never powered (PD never released from reset state).
Hook was read on wrong GPIO (4) with wrong polarity. Ring drove only audio, not bell.
This commit is contained in:
2026-06-15 00:26:35 +02:00
parent 87075ba92b
commit f531bf3e55
7 changed files with 290 additions and 29 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"
+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 */
+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 -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++;
+142
View File
@@ -0,0 +1,142 @@
/*
* 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 — open-drain output, written HIGH = released = SLIC active.
* This mirrors the Arduino: pinMode(pin_pd, OUTPUT_OPEN_DRAIN); digitalWrite(pin_pd, HIGH);
* Open-drain HIGH → line floats → SLIC PD pin sees its own pull-up → power ON. */
ret = gpio_set_direction(PLIP_SLIC_PD, GPIO_MODE_OUTPUT_OD);
if (ret != ESP_OK) return ret;
ret = gpio_set_level(PLIP_SLIC_PD, 1); /* HIGH = release = SLIC powered */
if (ret != ESP_OK) return ret;
ESP_LOGI(TAG, "SLIC power-up: PD GPIO%d open-drain HIGH (SLIC active)", 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