[pcm5122] Add analog gain, channel mixing, volume range, standby/powerdown switch, and XSMT enable pin support (#17313)

This commit is contained in:
Remco van Essen
2026-07-07 07:15:37 +02:00
committed by GitHub
parent 9aed1d2700
commit f823a23ea4
7 changed files with 274 additions and 9 deletions
+49 -2
View File
@@ -5,6 +5,7 @@ from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
@@ -16,6 +17,11 @@ from esphome.const import (
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
CONF_ANALOG_GAIN = "analog_gain"
CONF_CHANNEL_MIX = "channel_mix"
CONF_VOLUME_MIN_DB = "volume_min_db"
CONF_VOLUME_MAX_DB = "volume_max_db"
pcm5122_ns = cg.esphome_ns.namespace("pcm5122")
PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice)
CONF_PCM5122 = "pcm5122"
@@ -27,26 +33,60 @@ PCM5122_BITS_PER_SAMPLE_ENUM = {
32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32,
}
pcm5122_analog_gain = pcm5122_ns.enum("PCM5122AnalogGain")
PCM5122_ANALOG_GAIN_ENUM = {
"0db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_0DB,
"-6db": pcm5122_analog_gain.PCM5122_ANALOG_GAIN_MINUS_6DB,
}
pcm5122_channel_mix = pcm5122_ns.enum("PCM5122ChannelMix")
PCM5122_CHANNEL_MIX_ENUM = {
"stereo": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_STEREO,
"left": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_LEFT_ONLY,
"right": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_RIGHT_ONLY,
"swapped": pcm5122_channel_mix.PCM5122_CHANNEL_MIX_SWAPPED,
}
_validate_bits = cv.float_with_unit("bits", "bit")
def _validate_volume_range(config):
if config[CONF_VOLUME_MIN_DB] >= config[CONF_VOLUME_MAX_DB]:
raise cv.Invalid(f"{CONF_VOLUME_MIN_DB} must be less than {CONF_VOLUME_MAX_DB}")
return config
PCM5122GPIOPin = pcm5122_ns.class_(
"PCM5122GPIOPin",
cg.GPIOPin,
cg.Parented.template(PCM5122),
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(PCM5122),
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
_validate_bits, cv.enum(PCM5122_BITS_PER_SAMPLE_ENUM)
),
cv.Optional(CONF_ANALOG_GAIN, default="0db"): cv.enum(
PCM5122_ANALOG_GAIN_ENUM, lower=True
),
cv.Optional(CONF_CHANNEL_MIX, default="stereo"): cv.enum(
PCM5122_CHANNEL_MIX_ENUM, lower=True
),
cv.Optional(CONF_VOLUME_MIN_DB, default="-52.5dB"): cv.All(
cv.decibel, cv.float_range(min=-103.0, max=24.0)
),
cv.Optional(CONF_VOLUME_MAX_DB, default="0dB"): cv.All(
cv.decibel, cv.float_range(min=-103.0, max=24.0)
),
cv.Optional(CONF_ENABLE_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x4D))
.extend(i2c.i2c_device_schema(0x4D)),
_validate_volume_range,
)
@@ -96,3 +136,10 @@ async def to_code(config):
await i2c.register_i2c_device(var, config)
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))
cg.add(var.set_analog_gain(config[CONF_ANALOG_GAIN]))
cg.add(var.set_channel_mix(config[CONF_CHANNEL_MIX]))
cg.add(var.set_volume_min_db(config[CONF_VOLUME_MIN_DB]))
cg.add(var.set_volume_max_db(config[CONF_VOLUME_MAX_DB]))
if enable_pin_config := config.get(CONF_ENABLE_PIN):
enable_pin = await cg.gpio_pin_expression(enable_pin_config)
cg.add(var.set_enable_pin(enable_pin))
+99 -5
View File
@@ -10,6 +10,12 @@ namespace esphome::pcm5122 {
static const char *const TAG = "pcm5122";
void PCM5122::setup() {
// Hold XSMT low (soft mute asserted) until init completes
if (this->enable_pin_ != nullptr) {
this->enable_pin_->setup();
this->enable_pin_->digital_write(false);
}
// Select page 0 and verify chip presence via I2C ACK
if (!this->select_page_(0)) {
ESP_LOGE(TAG, "Write failed");
@@ -51,7 +57,22 @@ void PCM5122::setup() {
}
this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen;
if (!this->write_channel_mix_()) {
this->mark_failed();
return;
}
if (!this->write_analog_gain_()) {
this->mark_failed();
return;
}
// PLL reference clock: BCK
if (!this->select_page_(0)) {
ESP_LOGE(TAG, "Write failed");
this->mark_failed();
return;
}
optional<uint8_t> pll_ref = this->read_byte(PCM5122_REG_PLL_REF);
if (!pll_ref.has_value()) {
ESP_LOGE(TAG, "Failed to read PLL_REF");
@@ -67,15 +88,40 @@ void PCM5122::setup() {
this->mark_failed();
return;
}
// Release XSMT (soft un-mute) now that init has completed
if (this->enable_pin_ != nullptr) {
this->enable_pin_->digital_write(true);
}
}
void PCM5122::dump_config() {
const char *channel_mix_str;
switch (this->channel_mix_) {
case PCM5122_CHANNEL_MIX_LEFT_ONLY:
channel_mix_str = "left only";
break;
case PCM5122_CHANNEL_MIX_RIGHT_ONLY:
channel_mix_str = "right only";
break;
case PCM5122_CHANNEL_MIX_SWAPPED:
channel_mix_str = "swapped";
break;
default:
channel_mix_str = "stereo";
break;
}
ESP_LOGCONFIG(TAG, "Audio DAC:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG,
" Bits per sample: %u\n"
" Analog gain: %s\n"
" Channel mix: %s\n"
" Volume range: %.1f dB to %.1f dB\n"
" Muted: %s",
this->bits_per_sample_, YESNO(this->is_muted_));
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
LOG_PIN(" Enable Pin: ", this->enable_pin_);
}
bool PCM5122::set_mute_off() {
@@ -118,11 +164,11 @@ bool PCM5122::write_mute_() {
}
bool PCM5122::write_volume_() {
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFF = mute (-0.5 dB/step).
// Note: volume=0.0 maps to -52.5 dB (still audible), not true silence.
// DVOL register: 0x00 = +24 dB, 0x30 = 0 dB, 0xFE = -103 dB, 0xFF = mute (-0.5 dB/step).
// Note: volume=0.0 maps to volume_min_db_, which is not true silence unless set to -103 dB.
// Use set_mute_on() for silence.
const uint8_t dvol_max_volume = 0x30; // 0 dB at full scale
const uint8_t dvol_min_volume = 0x99; // -52.5 dB at minimum
const uint8_t dvol_max_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_max_db_ * 2.0f));
const uint8_t dvol_min_volume = static_cast<uint8_t>(lroundf(0x30 - this->volume_min_db_ * 2.0f));
const uint8_t volume_byte =
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
@@ -137,4 +183,52 @@ bool PCM5122::write_volume_() {
return true;
}
bool PCM5122::write_analog_gain_() {
uint8_t gain_byte = this->analog_gain_;
if (!this->select_page_(1) || !this->write_byte(PCM5122_REG_ANALOG_GAIN, gain_byte)) {
ESP_LOGE(TAG, "Writing analog gain failed");
return false;
}
return true;
}
bool PCM5122::write_channel_mix_() {
uint8_t channel_mix_byte = this->channel_mix_;
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DAC_DATA_PATH, channel_mix_byte)) {
ESP_LOGE(TAG, "Writing channel mix failed");
return false;
}
return true;
}
bool PCM5122::set_standby(bool enable) {
bool prev_standby = this->standby_;
this->standby_ = enable;
if (!this->write_power_control_()) {
this->standby_ = prev_standby;
return false;
}
return true;
}
bool PCM5122::set_powerdown(bool enable) {
bool prev_powerdown = this->powerdown_;
this->powerdown_ = enable;
if (!this->write_power_control_()) {
this->powerdown_ = prev_powerdown;
return false;
}
return true;
}
bool PCM5122::write_power_control_() {
uint8_t power_byte =
(this->standby_ ? PCM5122_POWER_CONTROL_RQST : 0) | (this->powerdown_ ? PCM5122_POWER_CONTROL_RQPD : 0);
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_POWER_CONTROL, power_byte)) {
ESP_LOGE(TAG, "Writing power control failed");
return false;
}
return true;
}
} // namespace esphome::pcm5122
+47 -2
View File
@@ -3,6 +3,7 @@
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
namespace esphome::pcm5122 {
@@ -10,11 +11,13 @@ namespace esphome::pcm5122 {
// Page 0 register addresses
static const uint8_t PCM5122_REG_PAGE_SELECT = 0x00;
static const uint8_t PCM5122_REG_RESET = 0x01;
static const uint8_t PCM5122_REG_POWER_CONTROL = 0x02;
static const uint8_t PCM5122_REG_MUTE = 0x03;
static const uint8_t PCM5122_REG_GPIO_ENABLE = 0x08;
static const uint8_t PCM5122_REG_PLL_REF = 0x0D;
static const uint8_t PCM5122_REG_ERROR_DETECT = 0x25;
static const uint8_t PCM5122_REG_AUDIO_FORMAT = 0x28;
static const uint8_t PCM5122_REG_DAC_DATA_PATH = 0x2A;
static const uint8_t PCM5122_REG_DVOL_LEFT = 0x3D;
static const uint8_t PCM5122_REG_DVOL_RIGHT = 0x3E;
static const uint8_t PCM5122_REG_GPIO_OUTPUT_SELECT = 0x50; // Base address; GPIO n uses offset n-1
@@ -23,6 +26,9 @@ static const uint8_t PCM5122_REG_GPIO_OUTPUT = 0x56;
static const uint8_t PCM5122_REG_GPIO_INVERT = 0x57;
static const uint8_t PCM5122_REG_GPIO_INPUT = 0x77;
// Page 1 register addresses
static const uint8_t PCM5122_REG_ANALOG_GAIN = 0x02;
// Register values for init sequence
static const uint8_t PCM5122_RESET_MODULES = 0x10; // RSTM: reset audio modules
static const uint8_t PCM5122_AUDIO_FORMAT_I2S = 0x00; // AFMT = I2S (bits [5:4] = 00)
@@ -35,12 +41,33 @@ static const uint8_t PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET = (1 << 1);
static const uint8_t PCM5122_PLL_REF_MASK = (7 << 4); // SREF bits [6:4]
static const uint8_t PCM5122_PLL_REF_SOURCE_BCK = (1 << 4); // SREF = 001 (BCK)
// Page 0, Register 2 (Power Control): RQST = standby request, RQPD = powerdown request (§10.5.3)
static const uint8_t PCM5122_POWER_CONTROL_RQST = (1 << 4);
static const uint8_t PCM5122_POWER_CONTROL_RQPD = (1 << 0);
// Page 1, Register 2 (Analog Gain Control): LAGN/RAGN select 0 dB or -6 dB analog gain (§8.3.5.5)
static const uint8_t PCM5122_ANALOG_GAIN_LAGN = (1 << 4);
static const uint8_t PCM5122_ANALOG_GAIN_RAGN = (1 << 0);
enum PCM5122BitsPerSample : uint8_t {
PCM5122_BITS_PER_SAMPLE_16 = 16,
PCM5122_BITS_PER_SAMPLE_24 = 24,
PCM5122_BITS_PER_SAMPLE_32 = 32,
};
enum PCM5122AnalogGain : uint8_t {
PCM5122_ANALOG_GAIN_0DB = 0x00,
PCM5122_ANALOG_GAIN_MINUS_6DB = PCM5122_ANALOG_GAIN_LAGN | PCM5122_ANALOG_GAIN_RAGN,
};
// Page 0, Register 0x2A (DAC Data Path): AUPL/AUPR select which channel's data feeds each output (§7.4.2.42)
enum PCM5122ChannelMix : uint8_t {
PCM5122_CHANNEL_MIX_STEREO = 0x11, // Left data -> left out, right data -> right out
PCM5122_CHANNEL_MIX_LEFT_ONLY = 0x12, // Left data -> both outputs
PCM5122_CHANNEL_MIX_RIGHT_ONLY = 0x21, // Right data -> both outputs
PCM5122_CHANNEL_MIX_SWAPPED = 0x22, // Left/right outputs swapped
};
class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
@@ -48,6 +75,11 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
float get_setup_priority() const override { return setup_priority::IO; }
void set_bits_per_sample(PCM5122BitsPerSample bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_analog_gain(PCM5122AnalogGain analog_gain) { this->analog_gain_ = analog_gain; }
void set_channel_mix(PCM5122ChannelMix channel_mix) { this->channel_mix_ = channel_mix; }
void set_volume_min_db(float volume_min_db) { this->volume_min_db_ = volume_min_db; }
void set_volume_max_db(float volume_max_db) { this->volume_max_db_ = volume_max_db; }
void set_enable_pin(GPIOPin *enable_pin) { this->enable_pin_ = enable_pin; }
bool set_mute_off() override;
bool set_mute_on() override;
@@ -56,17 +88,30 @@ class PCM5122 final : public audio_dac::AudioDac, public Component, public i2c::
bool is_muted() override;
float volume() override;
bool set_standby(bool enable);
bool set_powerdown(bool enable);
friend class PCM5122GPIOPin;
protected:
bool select_page_(uint8_t page);
bool write_mute_();
bool write_volume_();
bool write_analog_gain_();
bool write_channel_mix_();
bool write_power_control_();
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
GPIOPin *enable_pin_{nullptr};
float volume_{1.0f}; // Matches chip post-reset DVOL default (0x30 = 0 dB)
float volume_min_db_{-52.5f}; // Matches the previous hardcoded minimum (0x99)
float volume_max_db_{0.0f}; // Matches the previous hardcoded maximum (0x30)
int16_t current_page_{-1}; // -1 = unknown; cached to skip redundant page-select writes
bool is_muted_{false};
bool standby_{false};
bool powerdown_{false};
PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16};
PCM5122AnalogGain analog_gain_{PCM5122_ANALOG_GAIN_0DB};
PCM5122ChannelMix channel_mix_{PCM5122_CHANNEL_MIX_STEREO};
};
} // namespace esphome::pcm5122
@@ -0,0 +1,32 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_POWER_MODE, ENTITY_CATEGORY_CONFIG
from ..audio_dac import CONF_PCM5122, PCM5122, pcm5122_ns
PCM5122PowerSwitch = pcm5122_ns.class_("PCM5122PowerSwitch", switch.Switch)
pcm5122_power_switch_mode = pcm5122_ns.enum("PCM5122PowerSwitchMode")
PCM5122_POWER_SWITCH_MODE_ENUM = {
"standby": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_STANDBY,
"powerdown": pcm5122_power_switch_mode.PCM5122_POWER_SWITCH_MODE_POWERDOWN,
}
CONFIG_SCHEMA = switch.switch_schema(
PCM5122PowerSwitch,
entity_category=ENTITY_CATEGORY_CONFIG,
).extend(
{
cv.GenerateID(CONF_PCM5122): cv.use_id(PCM5122),
cv.Optional(CONF_POWER_MODE, default="powerdown"): cv.enum(
PCM5122_POWER_SWITCH_MODE_ENUM, lower=True
),
}
)
async def to_code(config):
var = await switch.new_switch(config)
await cg.register_parented(var, config[CONF_PCM5122])
cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
@@ -0,0 +1,12 @@
#include "power_switch.h"
namespace esphome::pcm5122 {
void PCM5122PowerSwitch::write_state(bool state) {
bool ok = (this->mode_ == PCM5122_POWER_SWITCH_MODE_STANDBY) ? this->parent_->set_standby(state)
: this->parent_->set_powerdown(state);
if (ok)
this->publish_state(state);
}
} // namespace esphome::pcm5122
@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../pcm5122.h"
namespace esphome::pcm5122 {
enum PCM5122PowerSwitchMode : uint8_t {
PCM5122_POWER_SWITCH_MODE_STANDBY,
PCM5122_POWER_SWITCH_MODE_POWERDOWN,
};
class PCM5122PowerSwitch final : public switch_::Switch, public Parented<PCM5122> {
public:
void set_power_mode(PCM5122PowerSwitchMode mode) { this->mode_ = mode; }
protected:
void write_state(bool state) override;
PCM5122PowerSwitchMode mode_{PCM5122_POWER_SWITCH_MODE_POWERDOWN};
};
} // namespace esphome::pcm5122
+11
View File
@@ -4,6 +4,11 @@ audio_dac:
i2c_id: i2c_bus
address: 0x4D
bits_per_sample: 32bit
analog_gain: -6db
channel_mix: swapped
volume_min_db: -60dB
volume_max_db: -3dB
enable_pin: GPIO12
output:
- platform: gpio
@@ -22,3 +27,9 @@ binary_sensor:
number: 4
mode:
input: true
switch:
- platform: pcm5122
pcm5122: pcm5122_dac
name: PCM5122 Power Down
power_mode: powerdown