#include "dac_njw1195.h" #include "driver/spi_master.h" #include "driver/gpio.h" #include "nvs_flash.h" #include "nvs.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "rom/ets_sys.h" #include "esp_timer.h" static const char TAG[] = "NJW1195AV"; // GPIO Pin Definitions #define NJW_MOSI_GPIO 23 #define NJW_CLK_GPIO 18 #define NJW_LATCH_GPIO 33 // NJW1195 寄存器地址 D3~D0 (ADR1=GND, ADR0=GND -> 芯片地址 0x0) // 16-bit 帧结构: [D15~D8 = DATA] [D7~D4 = 芯片地址 0x0] [D3~D0 = 寄存器地址] // ESP32 发送: Byte[0] = DATA(高字节), Byte[1] = (ChipAddr<<4)|RegAddr(低字节) #define NJW_REG_VOL_1A 0x0 // D3~D0=0000 主音箱左声道音量 (1A) #define NJW_REG_VOL_1B 0x1 // D3~D0=0001 主音箱右声道音量 (1B) #define NJW_REG_VOL_2A 0x2 // D3~D0=0010 低音炮左声道音量 (2A) #define NJW_REG_VOL_2B 0x3 // D3~D0=0011 低音炮右声道音量 (2B) #define NJW_REG_SEL_A 0x4 // D3~D0=0100 输入选择器 A (所有左声道) #define NJW_REG_SEL_B 0x5 // D3~D0=0101 输入选择器 B (所有右声道) // 芯片地址 D7~D4: ADR1=GND, ADR0=GND -> 0x0 #define NJW1195_ADDR 0x0 // 音量衰减数据 (D15~D8): // 0x40 = 0dB (最大安全音量) // 0xFF = 静音 (-95dB) #define NJW_VOL_MUTE 0xFF // 静音 (最大衰减) #define NJW_VOL_0DB 0x40 // 0dB 最大安全音量 #define NJW_VOL_MAX_ATT 0xFF // -95dB 最大衰减值 (同静音) // 输入选择器数据 (D15~D8): // D15~D13: 1A 通道选择 (001=Input1/BT, 010=Input2/AUX) // D12~D10: 2A 通道选择 (001=Input1/BT, 010=Input2/AUX) // D9~D8: 固定为 0 // BT(Input1): 001 001 00 -> 0x24 // AUX(Input2): 010 010 00 -> 0x48 // GND(Input3): 011 011 00 -> 0x6C #define NJW_SEL_BT 0x24 // 选通 Input 1 (蓝牙) #define NJW_SEL_AUX 0x48 // 选通 Input 2 (AUX) #define NJW_SEL_GND 0x6C // 选通 Input 3 (物理强制静音) // Volume range in dB for AirPlay #define VOLUME_MIN_DB (-30.0f) #define VOLUME_MAX_DB 0.0f static spi_device_handle_t s_spi_handle = NULL; static SemaphoreHandle_t s_njw_mutex = NULL; static uint8_t s_vol_main = 100; static uint8_t s_vol_bass = 100; static uint8_t s_prompt_vol = 50; static njw_source_t s_source = NJW_SOURCE_BT; static bool s_muted = false; bool s_in_panel_update = false; extern void panel_uart_report(uint8_t item, uint8_t value); static void njw1195_write_reg(uint8_t reg, uint8_t data) { if (s_spi_handle == NULL) { ESP_LOGE(TAG, "错误:SPI 未初始化"); return; } // 16-bit 帧组包 (MSB First, 高字节先发): // Byte[0] = DATA (D15~D8): 音量衰减值 或 选通控制位 // Byte[1] = (寄存器地址 D7~D4) | (芯片地址 D3~D0) uint8_t low_byte = (uint8_t)((reg & 0x0F) << 4) | (NJW1195_ADDR & 0x0F); uint8_t tx_data[2]; tx_data[0] = data; // 高字节: DATA tx_data[1] = low_byte; // 低字节: 地址 ESP_LOGI(TAG, "SPI TX: %02X %02X (DATA=%02X, REG=0x%X)", tx_data[0], tx_data[1], data, reg); spi_transaction_t t = { .flags = 0, .length = 16, .tx_buffer = tx_data, .rx_buffer = NULL, }; // 时序: 空闲时 LATCH=HIGH // 拉低 LATCH -> 发送 16 bits -> 拉高 LATCH (芯片锁存数据) -> 维持高电平空闲 gpio_set_level(NJW_LATCH_GPIO, 0); // 拉低:芯片进入接收状态 ets_delay_us(2); // 建立时间,让芯片稳定 esp_err_t err = spi_device_transmit(s_spi_handle, &t); if (err != ESP_OK) { ESP_LOGE(TAG, "SPI 发送失败: %s", esp_err_to_name(err)); gpio_set_level(NJW_LATCH_GPIO, 1); // 恢复高电平空闲态 return; } ets_delay_us(5); // 建立时间: 数据手册要求最小 4μs,此处给 5μs 余量 gpio_set_level(NJW_LATCH_GPIO, 1); // 拉高:芯片锁存数据到寄存器 ets_delay_us(5); // LATCH 保持脉宽 >= 1.6μs,此处给 5μs 余量 // LATCH 保持高电平作为空闲态,等待下一次操作 } static void save_settings_nvs(void) { nvs_handle_t nvs; if (nvs_open("njw", NVS_READWRITE, &nvs) == ESP_OK) { nvs_set_u8(nvs, "vol_main", s_vol_main); nvs_set_u8(nvs, "vol_bass", s_vol_bass); nvs_set_u8(nvs, "source", (uint8_t)s_source); nvs_commit(nvs); nvs_close(nvs); } } static esp_timer_handle_t s_nvs_timer = NULL; static void nvs_timer_callback(void *arg) { ESP_LOGI(TAG, "音量已保存到 NVS"); save_settings_nvs(); } static void trigger_save_settings_deferred(void) { if (s_nvs_timer == NULL) { esp_timer_create_args_t timer_args = { .callback = nvs_timer_callback, .arg = NULL, .name = "njw_nvs_save" }; esp_timer_create(&timer_args, &s_nvs_timer); } if (s_nvs_timer != NULL) { esp_timer_stop(s_nvs_timer); // 延迟 3 秒保存 (3,000,000 微秒),以降低 Flash 磨损 esp_timer_start_once(s_nvs_timer, 3000000); } } static void load_settings_nvs(void) { nvs_handle_t nvs; if (nvs_open("njw", NVS_READONLY, &nvs) == ESP_OK) { nvs_get_u8(nvs, "vol_main", &s_vol_main); nvs_get_u8(nvs, "vol_bass", &s_vol_bass); nvs_get_u8(nvs, "prompt_vol", &s_prompt_vol); uint8_t src_val = 0; if (nvs_get_u8(nvs, "source", &src_val) == ESP_OK) { s_source = (njw_source_t)src_val; } nvs_close(nvs); } ESP_LOGI(TAG, "从 NVS 加载设置成功 - 主音量: %d%%, 低音音量: %d%%, 输入源: %s", s_vol_main, s_vol_bass, (s_source == NJW_SOURCE_BT) ? "蓝牙" : "AUX"); } // 将 0~100% 百分比转换为 NJW1195 音量衰减代码 (D15~D8) // 数据手册: 0x00 = +31.5dB (危险!), 0x40 = 0dB (最大安全值), 0xFF = 静音(-95dB) // 衰减值越大(数字越大),音量越小!方向与直觉相反! // 0% -> 0xFF (静音) // 100% -> 0x40 (0dB, 最大安全音量) static uint8_t pct_to_code(uint8_t pct) { if (pct == 0) { return NJW_VOL_MUTE; // 0xFF 静音 } if (pct > 100) { pct = 100; } // 使用更柔和的符合人耳听觉的二次方曲线 (Audio Taper) // 最大衰减限制为 120 级 (约 -60dB)。 // 使得 50% 音量对应约 -15dB,解决前 50% 太小、后 50% 太大的问题。 uint32_t x = 100 - pct; uint32_t offset = (120 * x * x) / 10000; uint8_t code = 0x40 + (uint8_t)offset; return code; } // 主声道重映射 0~100% 到实际的物理输出比例 static uint8_t pct_to_code_main(uint8_t pct) { if (pct == 0) return NJW_VOL_MUTE; if (pct > 100) pct = 100; // 限制最大音量,例如 (pct * 50) / 100; uint8_t physical_pct = (pct * 68) / 100; if (physical_pct == 0) physical_pct = 1; // 避免计算为0变静音 return pct_to_code(physical_pct); } // 低音重映射 0~100% 到实际的物理输出比例 static uint8_t pct_to_code_bass(uint8_t pct) { if (pct == 0) return NJW_VOL_MUTE; if (pct > 100) pct = 100; uint8_t physical_pct = (pct * 38) / 100; if (physical_pct == 0) physical_pct = 1; // 避免计算为0变静音 return pct_to_code(physical_pct); } // ========================================== // 音量消抖 (Debounce) 定时器机制 // ========================================== static esp_timer_handle_t s_vol_debounce_timer = NULL; static uint8_t s_target_vol_main = 0; static bool s_vol_update_pending = false; static bool s_target_vol_from_panel = false; static void vol_debounce_timer_cb(void *arg) { if (!s_vol_update_pending) return; s_vol_update_pending = false; uint8_t pct = s_target_vol_main; bool from_panel = s_target_vol_from_panel; if (s_vol_main == pct) { return; // 防抖阶段已经被其他机制覆盖,无变化则跳过 } if (s_njw_mutex != NULL) { xSemaphoreTake(s_njw_mutex, portMAX_DELAY); } s_vol_main = pct; if (!s_muted) { uint8_t code = pct_to_code_main(s_vol_main); njw1195_write_reg(NJW_REG_VOL_2A, code); ets_delay_us(500); // 连续写入保护延时 njw1195_write_reg(NJW_REG_VOL_2B, code); ESP_LOGI(TAG, "Vol: %d%% (Mapped to %d%%, Reg: %02X)", s_vol_main, (s_vol_main * 100) / 100, code); } trigger_save_settings_deferred(); if (!from_panel) { panel_uart_report(0x0C, s_vol_main); } if (s_njw_mutex != NULL) { xSemaphoreGive(s_njw_mutex); } } void dac_njw1195_set_vol_main(uint8_t pct) { if (s_vol_main == pct && !s_vol_update_pending) { return; // Prevent flooding from rapid identical volume commands } if (s_vol_debounce_timer == NULL) { esp_timer_create_args_t args = { .callback = vol_debounce_timer_cb, .arg = NULL, .name = "vol_deb" }; esp_timer_create(&args, &s_vol_debounce_timer); } s_target_vol_main = pct; s_vol_update_pending = true; s_target_vol_from_panel = s_in_panel_update; // 延迟 40ms 执行,如果在 40ms 内有新指令则重置计时器 (过滤快速滑动) esp_timer_stop(s_vol_debounce_timer); esp_timer_start_once(s_vol_debounce_timer, 40000); } void dac_njw1195_set_vol_bass(uint8_t pct) { if (s_njw_mutex == NULL) { s_njw_mutex = xSemaphoreCreateMutex(); } if (s_njw_mutex != NULL) { xSemaphoreTake(s_njw_mutex, portMAX_DELAY); } s_vol_bass = pct; if (!s_muted) { uint8_t code = pct_to_code_bass(s_vol_bass); njw1195_write_reg(NJW_REG_VOL_1A, code); ets_delay_us(500); // 连续写入保护延时 njw1195_write_reg(NJW_REG_VOL_1B, code); ESP_LOGI(TAG, "Bass: %d%% (Mapped to %d%%, Reg: %02X)", s_vol_bass, (s_vol_bass * 90) / 100, code); } trigger_save_settings_deferred(); // 通知面板更新低音音量 (0x0D) if (!s_in_panel_update) { panel_uart_report(0x0D, s_vol_bass); } if (s_njw_mutex != NULL) { xSemaphoreGive(s_njw_mutex); } } void dac_njw1195_set_prompt_volume(uint8_t pct) { if (pct > 100) pct = 100; if (s_prompt_vol == pct) return; s_prompt_vol = pct; // Save directly to NVS nvs_handle_t nvs; if (nvs_open("njw", NVS_READWRITE, &nvs) == ESP_OK) { nvs_set_u8(nvs, "prompt_vol", s_prompt_vol); nvs_commit(nvs); nvs_close(nvs); } ESP_LOGI(TAG, "Prompt Volume set to: %d%%", s_prompt_vol); } uint8_t dac_njw1195_get_prompt_volume(void) { return s_prompt_vol; } void dac_njw1195_set_prompt_mode(bool active) { if (s_njw_mutex != NULL) { xSemaphoreTake(s_njw_mutex, portMAX_DELAY); } if (!s_muted) { if (active && s_source == NJW_SOURCE_AUX) { // Force source to BT (I2S) for prompt tone njw1195_write_reg(NJW_REG_SEL_A, NJW_SEL_BT); ets_delay_us(500); njw1195_write_reg(NJW_REG_SEL_B, NJW_SEL_BT); } uint8_t code = pct_to_code_main(active ? s_prompt_vol : s_vol_main); njw1195_write_reg(NJW_REG_VOL_2A, code); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_2B, code); if (!active && s_source == NJW_SOURCE_AUX) { // Restore source to AUX after prompt tone njw1195_write_reg(NJW_REG_SEL_A, NJW_SEL_AUX); ets_delay_us(500); njw1195_write_reg(NJW_REG_SEL_B, NJW_SEL_AUX); } ESP_LOGI(TAG, "Prompt Mode %s, Vol: %d%% (Reg: %02X)", active ? "ON" : "OFF", active ? s_prompt_vol : s_vol_main, code); } if (s_njw_mutex != NULL) { xSemaphoreGive(s_njw_mutex); } } void dac_njw1195_set_source(njw_source_t src) { if (s_njw_mutex == NULL) { s_njw_mutex = xSemaphoreCreateMutex(); } if (s_njw_mutex != NULL) { xSemaphoreTake(s_njw_mutex, portMAX_DELAY); } s_source = src; uint8_t code = (s_source == NJW_SOURCE_BT) ? NJW_SEL_BT : NJW_SEL_AUX; njw1195_write_reg(NJW_REG_SEL_A, code); ets_delay_us(500); // 连续写入保护延时 njw1195_write_reg(NJW_REG_SEL_B, code); ESP_LOGI(TAG, "设置输入源: %s (寄存器代码: %02X)", (s_source == NJW_SOURCE_BT) ? "蓝牙" : "AUX", code); trigger_save_settings_deferred(); // 通知面板更新输入源 (ITEM_SOURCE = 0x01, PROTO_VAL_ON = 0x01/BT, PROTO_VAL_OFF = 0x00/AUX) if (!s_in_panel_update) { panel_uart_report(0x01, (s_source == NJW_SOURCE_BT) ? 0x01 : 0x00); } if (s_njw_mutex != NULL) { xSemaphoreGive(s_njw_mutex); } } void dac_njw1195_set_mute(bool mute) { if (s_njw_mutex == NULL) { s_njw_mutex = xSemaphoreCreateMutex(); } if (s_njw_mutex != NULL) { xSemaphoreTake(s_njw_mutex, portMAX_DELAY); } s_muted = mute; if (s_muted) { njw1195_write_reg(NJW_REG_VOL_1A, NJW_VOL_MUTE); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_1B, NJW_VOL_MUTE); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_2A, NJW_VOL_MUTE); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_2B, NJW_VOL_MUTE); ESP_LOGI(TAG, "NJW1195AV 静音"); } else { uint8_t main_code = pct_to_code_main(s_vol_main); uint8_t bass_code = pct_to_code_bass(s_vol_bass); // 主音量现已调换到通道 2A/2B njw1195_write_reg(NJW_REG_VOL_2A, main_code); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_2B, main_code); ets_delay_us(500); // 低音音量现已调换到通道 1A/1B njw1195_write_reg(NJW_REG_VOL_1A, bass_code); ets_delay_us(500); njw1195_write_reg(NJW_REG_VOL_1B, bass_code); ESP_LOGI(TAG, "NJW Init (Vol: %d%%, Bass: %d%%)", s_vol_main, s_vol_bass); } if (s_njw_mutex != NULL) { xSemaphoreGive(s_njw_mutex); } } bool dac_njw1195_get_mute(void) { return s_muted; } uint8_t dac_njw1195_get_vol_main(void) { return s_vol_main; } uint8_t dac_njw1195_get_vol_bass(void) { return s_vol_bass; } njw_source_t dac_njw1195_get_source(void) { return s_source; } // ============================================================================ // dac_ops_t Implementations // ============================================================================ static esp_err_t njw1195_init(void *i2c_bus) { (void)i2c_bus; // NJW1195 uses SPI, not I2C if (s_njw_mutex == NULL) { s_njw_mutex = xSemaphoreCreateMutex(); if (s_njw_mutex == NULL) { ESP_LOGE(TAG, "Failed to create mutex"); return ESP_ERR_NO_MEM; } } // Configure LATCH Pin as GPIO Output gpio_config_t latch_conf = { .pin_bit_mask = (1ULL << NJW_LATCH_GPIO), .mode = GPIO_MODE_OUTPUT, .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; ESP_ERROR_CHECK(gpio_config(&latch_conf)); gpio_set_level(NJW_LATCH_GPIO, 1); // LATCH 空闲态为高电平 // Initialize SPI Bus spi_bus_config_t buscfg = { .mosi_io_num = NJW_MOSI_GPIO, .miso_io_num = -1, .sclk_io_num = NJW_CLK_GPIO, .quadwp_io_num = -1, .quadhd_io_num = -1, .max_transfer_sz = 32, }; // SPI2_HOST is commonly used esp_err_t err = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err)); return err; } // Add device to SPI Bus // NJW1195A 时序: CLOCK 空闲高电平 (CPOL=1),数据在下降沿被采样 (CPHA=0) // 对应 SPI Mode 2 spi_device_interface_config_t devcfg = { .clock_speed_hz = 200000, // 200kHz 时钟速度(保守,确保稳定) .mode = 2, // SPI Mode 2: CPOL=1, CPHA=0 .spics_io_num = -1, // 软件控制 LATCH(不使用硬件 CS) .queue_size = 1, }; err = spi_bus_add_device(SPI2_HOST, &devcfg, &s_spi_handle); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to add SPI device: %s", esp_err_to_name(err)); return err; } // Load settings from NVS load_settings_nvs(); // Initial sequence: Mute first, then restore values dac_njw1195_set_mute(true); // Apply source selection uint8_t src_code = (s_source == NJW_SOURCE_BT) ? NJW_SEL_BT : NJW_SEL_AUX; njw1195_write_reg(NJW_REG_SEL_A, src_code); njw1195_write_reg(NJW_REG_SEL_B, src_code); // Unmute with saved volume levels dac_njw1195_set_mute(false); ESP_LOGI(TAG, "NJW1195AV 初始化成功"); // ================= 强制测试代码 ================= /* ESP_LOGW(TAG, "执行强制暴力写入测试 (所有通道 0dB, Input 1)"); // 主音箱 (1A, 1B) njw1195_write_reg(0x00, 0x40); // 1A 设为 0dB (0x40) ets_delay_us(500); njw1195_write_reg(0x01, 0x40); // 1B 设为 0dB (0x40) ets_delay_us(500); // 低音炮 (2A, 2B) njw1195_write_reg(0x02, 0x40); // 2A 设为 0dB (0x40) ets_delay_us(500); njw1195_write_reg(0x03, 0x40); // 2B 设为 0dB (0x40) ets_delay_us(500); // 强制选通蓝牙 (Input 1) -> 高中低位合并为 0x24 njw1195_write_reg(0x04, 0x24); // 强制选通左声道 Input 1 ets_delay_us(500); njw1195_write_reg(0x05, 0x24); // 强制选通右声道 Input 1 */ // =============================================== return ESP_OK; } static esp_err_t njw1195_deinit(void) { if (s_spi_handle) { spi_bus_remove_device(s_spi_handle); s_spi_handle = NULL; } if (s_njw_mutex != NULL) { vSemaphoreDelete(s_njw_mutex); s_njw_mutex = NULL; } return ESP_OK; } static void njw1195_set_volume(float volume_db) { // AirPlay volume is in dB scale (-30.0f to 0.0f) if (volume_db > VOLUME_MAX_DB) { volume_db = VOLUME_MAX_DB; } if (volume_db < VOLUME_MIN_DB) { volume_db = VOLUME_MIN_DB; } // Map -30..0 dB to 0..100% float pct_f = ((volume_db - VOLUME_MIN_DB) / (VOLUME_MAX_DB - VOLUME_MIN_DB)) * 100.0f; uint8_t pct = (uint8_t)(pct_f + 0.5f); ESP_LOGI(TAG, "AirPlay set volume DB: %.1f -> Pct: %d%%", volume_db, pct); dac_njw1195_set_vol_main(pct); } static void njw1195_set_power_mode(dac_power_mode_t mode) { switch (mode) { case DAC_POWER_ON: dac_njw1195_set_mute(false); break; case DAC_POWER_STANDBY: case DAC_POWER_OFF: dac_njw1195_set_mute(true); break; default: break; } } static void njw1195_on_i2s_started(void) { // No clock reconfiguration required for NJW1195 as it's not a codec dac_njw1195_set_mute(false); } static void njw1195_enable_speaker(bool enable) { dac_njw1195_set_mute(!enable); } static void njw1195_enable_line_out(bool enable) { (void)enable; // Main output is Out1, already controlled by speaker enable } const dac_ops_t dac_njw1195_ops = { .init = njw1195_init, .deinit = njw1195_deinit, .set_volume = njw1195_set_volume, .set_power_mode = njw1195_set_power_mode, .on_i2s_started = njw1195_on_i2s_started, .enable_speaker = njw1195_enable_speaker, .enable_line_out = njw1195_enable_line_out, };