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
CI / platformio (push) Failing after 5m56s
This commit was merged in pull request #22.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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++;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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="..."
|
||||
|
||||
Reference in New Issue
Block a user