[pcm5122] Add PCM5122 audio DAC component (#15709)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: kbx81 <kbx81x@gmail.com>
This commit is contained in:
Remco van Essen
2026-06-09 08:33:15 +02:00
committed by GitHub
parent ddd21ba442
commit cdc63f0fed
12 changed files with 450 additions and 0 deletions

View File

@@ -379,6 +379,7 @@ esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
esphome/components/pcm5122/* @remcom
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984

View File

@@ -0,0 +1 @@
CODEOWNERS = ["@remcom"]

View File

@@ -0,0 +1,98 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.audio_dac import AudioDac
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OUTPUT,
)
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["i2c"]
pcm5122_ns = cg.esphome_ns.namespace("pcm5122")
PCM5122 = pcm5122_ns.class_("PCM5122", AudioDac, cg.Component, i2c.I2CDevice)
CONF_PCM5122 = "pcm5122"
pcm5122_bits_per_sample = pcm5122_ns.enum("PCM5122BitsPerSample")
PCM5122_BITS_PER_SAMPLE_ENUM = {
16: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_16,
24: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_24,
32: pcm5122_bits_per_sample.PCM5122_BITS_PER_SAMPLE_32,
}
_validate_bits = cv.float_with_unit("bits", "bit")
PCM5122GPIOPin = pcm5122_ns.class_(
"PCM5122GPIOPin",
cg.GPIOPin,
cg.Parented.template(PCM5122),
)
CONFIG_SCHEMA = (
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)
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x4D))
)
def _validate_pin_mode(value):
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
raise cv.Invalid("Mode must be either input or output")
if value[CONF_INPUT] and value[CONF_OUTPUT]:
raise cv.Invalid("Mode must be either input or output, not both")
return value
def _validate_pin(value):
if value[CONF_MODE][CONF_INPUT] and value[CONF_NUMBER] == 6:
raise cv.Invalid("GPIO6 cannot be used as input on the PCM5122")
return value
PIN_SCHEMA = cv.All(
pins.gpio_base_schema(
PCM5122GPIOPin,
cv.int_range(min=3, max=6),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=_validate_pin_mode,
).extend(
{
cv.Required(CONF_PCM5122): cv.use_id(PCM5122),
}
),
_validate_pin,
)
@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCM5122, PIN_SCHEMA)
async def pcm5122_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_parented(var, config[CONF_PCM5122])
cg.add(var.set_pin(config[CONF_NUMBER]))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE]))

View File

@@ -0,0 +1,140 @@
#include "pcm5122.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome::pcm5122 {
static const char *const TAG = "pcm5122";
void PCM5122::setup() {
// Select page 0 and verify chip presence via I2C ACK
if (!this->select_page_(0)) {
ESP_LOGE(TAG, "Write failed");
this->status_set_error(LOG_STR("Write failed"));
this->mark_failed();
return;
}
// Reset audio modules
this->reg(PCM5122_REG_RESET) = PCM5122_RESET_MODULES;
delay(20);
this->reg(PCM5122_REG_RESET) = 0x00;
// Ignore clock halt detection; enable clock divider autoset
optional<uint8_t> err_detect = this->read_byte(PCM5122_REG_ERROR_DETECT);
if (!err_detect.has_value()) {
ESP_LOGE(TAG, "Failed to read ERROR_DETECT");
this->mark_failed();
return;
}
uint8_t err_detect_val = err_detect.value();
err_detect_val |= PCM5122_ERROR_DETECT_IGNORE_CLKHALT;
err_detect_val &= ~PCM5122_ERROR_DETECT_DISABLE_DIV_AUTOSET;
this->reg(PCM5122_REG_ERROR_DETECT) = err_detect_val;
// I2S format with the configured word length
uint8_t alen;
switch (this->bits_per_sample_) {
case PCM5122_BITS_PER_SAMPLE_16:
alen = PCM5122_AUDIO_FORMAT_ALEN_16BIT;
break;
case PCM5122_BITS_PER_SAMPLE_24:
alen = PCM5122_AUDIO_FORMAT_ALEN_24BIT;
break;
case PCM5122_BITS_PER_SAMPLE_32:
default:
alen = PCM5122_AUDIO_FORMAT_ALEN_32BIT;
break;
}
this->reg(PCM5122_REG_AUDIO_FORMAT) = PCM5122_AUDIO_FORMAT_I2S | alen;
// PLL reference clock: BCK
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");
this->mark_failed();
return;
}
uint8_t pll_ref_val = pll_ref.value();
pll_ref_val &= ~PCM5122_PLL_REF_MASK;
pll_ref_val |= PCM5122_PLL_REF_SOURCE_BCK;
this->reg(PCM5122_REG_PLL_REF) = pll_ref_val;
if (!this->set_mute_on() || !this->set_volume(this->volume_)) {
this->mark_failed();
return;
}
}
void PCM5122::dump_config() {
ESP_LOGCONFIG(TAG, "Audio DAC:");
LOG_I2C_DEVICE(this);
ESP_LOGCONFIG(TAG,
" Bits per sample: %u\n"
" Muted: %s",
this->bits_per_sample_, YESNO(this->is_muted_));
}
bool PCM5122::set_mute_off() {
this->is_muted_ = false;
return this->write_mute_();
}
bool PCM5122::set_mute_on() {
this->is_muted_ = true;
return this->write_mute_();
}
bool PCM5122::set_volume(float volume) {
this->volume_ = clamp<float>(volume, 0.0f, 1.0f);
return this->write_volume_();
}
bool PCM5122::is_muted() { return this->is_muted_; }
float PCM5122::volume() { return this->volume_; }
bool PCM5122::select_page_(uint8_t page) {
if (this->current_page_ == page)
return true;
if (!this->write_byte(PCM5122_REG_PAGE_SELECT, page)) {
this->current_page_ = -1;
return false;
}
this->current_page_ = page;
return true;
}
bool PCM5122::write_mute_() {
uint8_t mute_byte = this->is_muted() ? 0x11 : 0x00;
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_MUTE, mute_byte)) {
ESP_LOGE(TAG, "Writing mute failed");
return false;
}
return true;
}
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.
// 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 volume_byte =
dvol_max_volume + static_cast<uint8_t>(lroundf((1.0f - this->volume_) * (dvol_min_volume - dvol_max_volume)));
ESP_LOGV(TAG, "Setting volume to 0x%.2x", volume_byte);
if (!this->select_page_(0) || !this->write_byte(PCM5122_REG_DVOL_LEFT, volume_byte) ||
!this->write_byte(PCM5122_REG_DVOL_RIGHT, volume_byte)) {
ESP_LOGE(TAG, "Writing volume failed");
return false;
}
return true;
}
} // namespace esphome::pcm5122

View File

@@ -0,0 +1,72 @@
#pragma once
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
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_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_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
static const uint8_t PCM5122_GPIO_OUTPUT_SELECT_REGISTER = 0x02; // GPIO driven by GPIO_OUTPUT register (reg 0x56)
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;
// 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)
// ALEN (word length) occupies bits [1:0] of the audio format register
static const uint8_t PCM5122_AUDIO_FORMAT_ALEN_16BIT = 0x00;
static const uint8_t PCM5122_AUDIO_FORMAT_ALEN_24BIT = 0x02;
static const uint8_t PCM5122_AUDIO_FORMAT_ALEN_32BIT = 0x03;
static const uint8_t PCM5122_ERROR_DETECT_IGNORE_CLKHALT = (1 << 3);
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)
enum PCM5122BitsPerSample : uint8_t {
PCM5122_BITS_PER_SAMPLE_16 = 16,
PCM5122_BITS_PER_SAMPLE_24 = 24,
PCM5122_BITS_PER_SAMPLE_32 = 32,
};
class PCM5122 : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
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; }
bool set_mute_off() override;
bool set_mute_on() override;
bool set_volume(float volume) override;
bool is_muted() override;
float volume() override;
friend class PCM5122GPIOPin;
protected:
bool select_page_(uint8_t page);
bool write_mute_();
bool write_volume_();
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
bool is_muted_{false};
PCM5122BitsPerSample bits_per_sample_{PCM5122_BITS_PER_SAMPLE_16};
};
} // namespace esphome::pcm5122

View File

@@ -0,0 +1,69 @@
#include "pcm5122_gpio.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pcm5122 {
static const char *const TAG = "pcm5122.gpio";
void PCM5122GPIOPin::setup() { this->pin_mode(this->flags_); }
void PCM5122GPIOPin::pin_mode(gpio::Flags flags) {
this->flags_ = flags;
if (!this->parent_->select_page_(0)) {
ESP_LOGE(TAG, "Failed to select page 0");
return;
}
optional<uint8_t> curr = this->parent_->read_byte(PCM5122_REG_GPIO_ENABLE);
if (!curr.has_value()) {
ESP_LOGE(TAG, "Failed to read GPIO_ENABLE");
return;
}
if (flags & gpio::FLAG_INPUT) {
this->parent_->reg(PCM5122_REG_GPIO_ENABLE) = curr.value() & ~(1 << (this->pin_ - 1));
} else if (flags & gpio::FLAG_OUTPUT) {
this->parent_->reg(PCM5122_REG_GPIO_ENABLE) = curr.value() | (1 << (this->pin_ - 1));
this->parent_->reg(PCM5122_REG_GPIO_OUTPUT_SELECT + (this->pin_ - 1)) = PCM5122_GPIO_OUTPUT_SELECT_REGISTER;
optional<uint8_t> invert = this->parent_->read_byte(PCM5122_REG_GPIO_INVERT);
if (!invert.has_value()) {
ESP_LOGE(TAG, "Failed to read GPIO_INVERT");
return;
}
if (this->inverted_) {
this->parent_->reg(PCM5122_REG_GPIO_INVERT) = invert.value() | (1 << (this->pin_ - 1));
} else {
this->parent_->reg(PCM5122_REG_GPIO_INVERT) = invert.value() & ~(1 << (this->pin_ - 1));
}
}
}
void PCM5122GPIOPin::digital_write(bool value) {
if (!this->parent_->select_page_(0))
return;
optional<uint8_t> curr = this->parent_->read_byte(PCM5122_REG_GPIO_OUTPUT);
if (!curr.has_value())
return;
if (value) {
this->parent_->reg(PCM5122_REG_GPIO_OUTPUT) = curr.value() | (1 << (this->pin_ - 1));
} else {
this->parent_->reg(PCM5122_REG_GPIO_OUTPUT) = curr.value() & ~(1 << (this->pin_ - 1));
}
}
bool PCM5122GPIOPin::digital_read() {
if (!this->parent_->select_page_(0))
return this->value_;
optional<uint8_t> read = this->parent_->read_byte(PCM5122_REG_GPIO_INPUT);
if (read.has_value()) {
// GPIO input register has RSV at bit 0; GPIN_N is at bit N (unlike other GPIO registers)
this->value_ = !!(read.value() & (1 << this->pin_)) != this->inverted_;
}
return this->value_;
}
size_t PCM5122GPIOPin::dump_summary(char *buffer, size_t len) const {
return buf_append_printf(buffer, len, 0, "PCM5122 GPIO%u", this->pin_);
}
} // namespace esphome::pcm5122

View File

@@ -0,0 +1,29 @@
#pragma once
#include "esphome/core/gpio.h"
#include "pcm5122.h"
namespace esphome::pcm5122 {
class PCM5122GPIOPin : public GPIOPin, public Parented<PCM5122> {
public:
void setup() override;
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
size_t dump_summary(char *buffer, size_t len) const override;
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_inverted(bool inverted) { this->inverted_ = inverted; }
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
gpio::Flags get_flags() const override { return this->flags_; }
protected:
uint8_t pin_{0};
bool inverted_{false};
gpio::Flags flags_{gpio::FLAG_NONE};
bool value_{false};
};
} // namespace esphome::pcm5122

View File

@@ -0,0 +1,24 @@
audio_dac:
- platform: pcm5122
id: pcm5122_dac
i2c_id: i2c_bus
address: 0x4D
bits_per_sample: 32bit
output:
- platform: gpio
id: pcm5122_amp_enable
pin:
pcm5122: pcm5122_dac
number: 3
mode:
output: true
binary_sensor:
- platform: gpio
id: pcm5122_gpio_input
pin:
pcm5122: pcm5122_dac
number: 4
mode:
input: true

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
<<: !include common.yaml