Audio System + Drivers (#562)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(audio-codec-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel esp_codec_dev
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Audio codec module
|
||||
|
||||
Defines `AUDIO_CODEC_TYPE` and the codec-agnostic `AudioCodecApi` (open/close/
|
||||
read/write/volume/mute/native-rate/native-channels/capabilities) that every
|
||||
audio codec driver in this repo implements (ES8311, ES8388, ES7210, AW88298,
|
||||
the dummy I2S amps, the PDM mic). This is intentionally low-level: no
|
||||
resampling or mixing, one codec device per chip. Most app code should go
|
||||
through `audio_stream` (see `Drivers/audio-stream-module`) instead of talking
|
||||
to a codec device directly.
|
||||
|
||||
Also provides `audio_codec_adapters.h`: glue that lets a codec driver build an
|
||||
`esp_codec_dev` control/data/GPIO interface (`audio_codec_ctrl_if_t` /
|
||||
`audio_codec_data_if_t` / `audio_codec_gpio_if_t`) directly from Tactility's
|
||||
own I2C controller, I2S controller, and GPIO pin devices, so a codec driver
|
||||
only ever needs `device_get_parent()`/`device_find_by_name()` plus a handful
|
||||
of devicetree properties -- it never touches the platform I2C/I2S/GPIO APIs
|
||||
directly.
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <tactility/drivers/gpio.h>
|
||||
#include "audio_codec_data_if.h"
|
||||
#include "audio_codec_ctrl_if.h"
|
||||
#include "audio_codec_gpio_if.h"
|
||||
|
||||
struct Device;
|
||||
|
||||
/**
|
||||
* @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device.
|
||||
* @param[in] i2s_controller the I2S controller device to read/write audio frames from/to
|
||||
* @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if().
|
||||
*/
|
||||
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller);
|
||||
|
||||
/**
|
||||
* @brief Creates an esp_codec_dev data interface backed by a Tactility I2S controller device,
|
||||
* configured for PDM RX mode (e.g. for a PDM MEMS microphone such as the SPM1423).
|
||||
* @param[in] i2s_controller the I2S controller device to read PDM audio frames from. Must be
|
||||
* backed by I2S controller 0 -- PDM RX is hardware-restricted to that controller.
|
||||
* @return the data interface, or NULL on failure. Free with audio_codec_delete_data_if().
|
||||
*/
|
||||
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller);
|
||||
|
||||
/**
|
||||
* @brief Creates an esp_codec_dev control interface backed by a Tactility I2C controller device.
|
||||
* @param[in] i2c_controller the I2C controller device the codec chip is attached to
|
||||
* @param[in] address the 7-bit I2C address of the codec chip
|
||||
* @return the control interface, or NULL on failure. Free with audio_codec_delete_ctrl_if().
|
||||
*/
|
||||
const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address);
|
||||
|
||||
/**
|
||||
* @brief Creates an esp_codec_dev GPIO interface backed by Tactility GpioPinSpec descriptors.
|
||||
*
|
||||
* The codec chip driver references GPIOs by small integer index (0, 1, ...); this adapter
|
||||
* maps those indices to GpioPinSpec entries supplied at creation time (e.g. reset/PA-enable lines
|
||||
* declared in devicetree). Indices outside the supplied array are treated as no-ops.
|
||||
*
|
||||
* @param[in] pins array of pin specs, indexed by the small integer GPIO id used by the codec chip driver
|
||||
* @note The caller must keep `pins` valid for the lifetime of the returned interface (until
|
||||
* audio_codec_adapter_delete_gpio() is called); the adapter stores the pointer, not a copy.
|
||||
* @param[in] pin_count number of entries in `pins`
|
||||
* @note Only a single instance is supported at a time; a second call before deleting the
|
||||
* first returns NULL.
|
||||
* @return the GPIO interface, or NULL on failure. Free with audio_codec_adapter_delete_gpio()
|
||||
* (esp_codec_dev's generic audio_codec_delete_gpio_if() does not release our GPIO descriptors).
|
||||
*/
|
||||
const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count);
|
||||
|
||||
/**
|
||||
* @brief Frees a GPIO interface created by audio_codec_adapter_new_gpio(), releasing its GPIO descriptors.
|
||||
*/
|
||||
int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define I2C_TIMEOUT_TICKS pdMS_TO_TICKS(100)
|
||||
|
||||
struct I2cCtrlContext {
|
||||
audio_codec_ctrl_if_t base;
|
||||
struct Device* i2c_controller;
|
||||
uint8_t address;
|
||||
bool is_open;
|
||||
};
|
||||
|
||||
static bool ctrl_is_open(const audio_codec_ctrl_if_t* handle) {
|
||||
const struct I2cCtrlContext* context = (const struct I2cCtrlContext*) handle;
|
||||
return context->is_open;
|
||||
}
|
||||
|
||||
static int ctrl_open(const audio_codec_ctrl_if_t* handle, void* config, int config_size) {
|
||||
(void) config;
|
||||
(void) config_size;
|
||||
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
|
||||
context->is_open = true;
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
static int ctrl_read_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) {
|
||||
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
|
||||
if (!context->is_open || reg_len != 1) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
if (data_len < 0 || data_len > UINT16_MAX) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
error_t error = i2c_controller_read_register(context->i2c_controller, context->address, (uint8_t) reg, (uint8_t*) data, (size_t) data_len, I2C_TIMEOUT_TICKS);
|
||||
return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_READ_FAIL;
|
||||
}
|
||||
|
||||
static int ctrl_write_reg(const audio_codec_ctrl_if_t* handle, int reg, int reg_len, void* data, int data_len) {
|
||||
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
|
||||
if (!context->is_open || reg_len != 1) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
if (data_len < 0 || data_len > UINT16_MAX) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
error_t error = i2c_controller_write_register(context->i2c_controller, context->address, (uint8_t) reg, (const uint8_t*) data, (uint16_t) data_len, I2C_TIMEOUT_TICKS);
|
||||
return (error == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_WRITE_FAIL;
|
||||
}
|
||||
|
||||
static int ctrl_close(const audio_codec_ctrl_if_t* handle) {
|
||||
struct I2cCtrlContext* context = (struct I2cCtrlContext*) handle;
|
||||
context->is_open = false;
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
const audio_codec_ctrl_if_t* audio_codec_adapter_new_i2c_ctrl(struct Device* i2c_controller, uint8_t address) {
|
||||
if (i2c_controller == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct I2cCtrlContext* context = (struct I2cCtrlContext*) calloc(1, sizeof(struct I2cCtrlContext));
|
||||
if (context == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context->i2c_controller = i2c_controller;
|
||||
context->address = address;
|
||||
context->is_open = false;
|
||||
context->base.open = ctrl_open;
|
||||
context->base.is_open = ctrl_is_open;
|
||||
context->base.read_reg = ctrl_read_reg;
|
||||
context->base.write_reg = ctrl_write_reg;
|
||||
context->base.close = ctrl_close;
|
||||
|
||||
return &context->base;
|
||||
}
|
||||
|
||||
// Note: esp_codec_dev already provides audio_codec_delete_ctrl_if() (calls ->close then frees);
|
||||
// no adapter-specific cleanup is needed, so we don't redefine it here.
|
||||
@@ -0,0 +1,193 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
// esp_codec_dev_read/write may issue several of these chunked I2S transfers per call and
|
||||
// gives us no way to forward the caller's audio_stream timeout through. Keep this short so
|
||||
// a stalled/underrunning I2S link returns promptly -- callers retry on failure, and a long
|
||||
// per-chunk timeout here is what made AudioStreamTest's loopback task unkillable (it blocked
|
||||
// far longer than its read/write timeout and the 2s drain wait in stopLoopback()).
|
||||
#define I2S_TIMEOUT_TICKS pdMS_TO_TICKS(200)
|
||||
|
||||
struct I2sDataContext {
|
||||
audio_codec_data_if_t base;
|
||||
struct Device* i2s_controller;
|
||||
bool is_open;
|
||||
bool is_pdm; // true if created via audio_codec_adapter_new_i2s_pdm_data()
|
||||
};
|
||||
|
||||
static bool data_is_open(const audio_codec_data_if_t* handle) {
|
||||
const struct I2sDataContext* context = (const struct I2sDataContext*) handle;
|
||||
return context->is_open;
|
||||
}
|
||||
|
||||
static int data_open(const audio_codec_data_if_t* handle, void* data_cfg, int cfg_size) {
|
||||
(void) data_cfg;
|
||||
(void) cfg_size;
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
context->is_open = true;
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
static int data_enable(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, bool enable) {
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
|
||||
// esp_codec_dev_close() calls this with enable=false but never calls set_fmt() again
|
||||
// before the next open -- without releasing the channel here, a TDM/PDM RX channel
|
||||
// (e.g. ES7210, a PDM mic) or a TX channel stays enabled and holding its GPIOs/DMA
|
||||
// channel indefinitely after the app that opened it closes, starving other I2S/DMA
|
||||
// users system-wide. Only release the direction this codec actually owns -- a single
|
||||
// I2S controller can carry an independent TX user (e.g. a speaker amp) and RX user
|
||||
// (e.g. a PDM mic) at once, and tearing down the whole controller on one closing would
|
||||
// also kill the other's still-active stream.
|
||||
if (!enable) {
|
||||
bool isInput = (dev_type & ESP_CODEC_DEV_TYPE_IN) != 0;
|
||||
bool isOutput = (dev_type & ESP_CODEC_DEV_TYPE_OUT) != 0;
|
||||
if (isInput) {
|
||||
i2s_controller_disable_direction(context->i2s_controller, true);
|
||||
}
|
||||
if (isOutput) {
|
||||
i2s_controller_disable_direction(context->i2s_controller, false);
|
||||
}
|
||||
}
|
||||
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
static int data_set_fmt(const audio_codec_data_if_t* handle, esp_codec_dev_type_t dev_type, esp_codec_dev_sample_info_t* fs) {
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
if (!context->is_open || fs == NULL) {
|
||||
return ESP_CODEC_DEV_INVALID_ARG;
|
||||
}
|
||||
|
||||
// PDM RX is a distinct setup path chosen at adapter-construction time (see
|
||||
// audio_codec_adapter_new_i2s_pdm_data()), not inferred from fs here -- PDM mics are
|
||||
// mono/stereo just like standard mode, so there's no sample-info heuristic that could
|
||||
// tell them apart the way fs->channel > 2 does for TDM.
|
||||
if (context->is_pdm) {
|
||||
// ESP-IDF's PDM RX mode only supports 16-bit samples -- silently configuring the
|
||||
// controller anyway would leave the caller's chosen bit depth misapplied.
|
||||
if (fs->bits_per_sample != 16) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
// PDM RX is fixed at 2 slots (mono or stereo) -- anything else doesn't map onto
|
||||
// I2S_SLOT_MODE_MONO/STEREO and would silently get treated as stereo otherwise.
|
||||
if (fs->channel != 1 && fs->channel != 2) {
|
||||
return ESP_CODEC_DEV_NOT_SUPPORT;
|
||||
}
|
||||
struct I2sPdmRxConfig pdm_config = {
|
||||
.sample_rate_hz = fs->sample_rate,
|
||||
.stereo = fs->channel == 2,
|
||||
};
|
||||
error_t error = i2s_controller_set_rx_pdm_config(context->i2s_controller, &pdm_config);
|
||||
if (error != ERROR_NONE) {
|
||||
return ESP_CODEC_DEV_DRV_ERR;
|
||||
}
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
// Standard (stereo) mode and TDM mode are alternative setup paths for the same I2S
|
||||
// port, not sequential steps -- set_config() always allocates+enables a TX/RX channel
|
||||
// pair, and calling it before set_rx_tdm_config() leaves that pair's TX channel bound
|
||||
// to the shared BCLK/WS/MCLK pins, so the TDM path's fresh i2s_new_channel() can't
|
||||
// claim those pins ("occupied by i2s_driver" / "GPIO not usable" at runtime).
|
||||
if ((dev_type & ESP_CODEC_DEV_TYPE_IN) != 0 && fs->channel > 2) {
|
||||
struct I2sTdmRxConfig tdm_config = {
|
||||
.sample_rate_hz = fs->sample_rate,
|
||||
.mclk_multiple = (fs->mclk_multiple != 0) ? (uint32_t) fs->mclk_multiple : 256,
|
||||
.bclk_div = 8,
|
||||
.slot_count = fs->channel,
|
||||
.bits_per_sample = fs->bits_per_sample,
|
||||
.slot_bit_width = 0,
|
||||
};
|
||||
error_t error = i2s_controller_set_rx_tdm_config(context->i2s_controller, &tdm_config);
|
||||
if (error != ERROR_NONE) {
|
||||
return ESP_CODEC_DEV_DRV_ERR;
|
||||
}
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
struct I2sConfig config = {
|
||||
.communication_format = I2S_FORMAT_STAND_I2S,
|
||||
.sample_rate = fs->sample_rate,
|
||||
.bits_per_sample = fs->bits_per_sample,
|
||||
.channel_left = 0,
|
||||
.channel_right = (fs->channel > 1) ? 1 : I2S_CHANNEL_NONE,
|
||||
};
|
||||
|
||||
if (i2s_controller_set_config(context->i2s_controller, &config) != ERROR_NONE) {
|
||||
return ESP_CODEC_DEV_DRV_ERR;
|
||||
}
|
||||
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
static int data_read(const audio_codec_data_if_t* handle, uint8_t* data, int size) {
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
if (!context->is_open) {
|
||||
return ESP_CODEC_DEV_WRONG_STATE;
|
||||
}
|
||||
size_t bytes_read = 0;
|
||||
error_t error = i2s_controller_read(context->i2s_controller, data, (size_t) size, &bytes_read, I2S_TIMEOUT_TICKS);
|
||||
if (error != ERROR_NONE) {
|
||||
return ESP_CODEC_DEV_READ_FAIL;
|
||||
}
|
||||
return (int) bytes_read;
|
||||
}
|
||||
|
||||
static int data_write(const audio_codec_data_if_t* handle, uint8_t* data, int size) {
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
if (!context->is_open) {
|
||||
return ESP_CODEC_DEV_WRONG_STATE;
|
||||
}
|
||||
size_t bytes_written = 0;
|
||||
error_t error = i2s_controller_write(context->i2s_controller, data, (size_t) size, &bytes_written, I2S_TIMEOUT_TICKS);
|
||||
if (error != ERROR_NONE) {
|
||||
return ESP_CODEC_DEV_WRITE_FAIL;
|
||||
}
|
||||
return (int) bytes_written;
|
||||
}
|
||||
|
||||
static int data_close(const audio_codec_data_if_t* handle) {
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) handle;
|
||||
context->is_open = false;
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
|
||||
static const audio_codec_data_if_t* new_i2s_data(struct Device* i2s_controller, bool is_pdm) {
|
||||
if (i2s_controller == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct I2sDataContext* context = (struct I2sDataContext*) calloc(1, sizeof(struct I2sDataContext));
|
||||
if (context == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context->i2s_controller = i2s_controller;
|
||||
context->is_open = false;
|
||||
context->is_pdm = is_pdm;
|
||||
context->base.open = data_open;
|
||||
context->base.is_open = data_is_open;
|
||||
context->base.enable = data_enable;
|
||||
context->base.set_fmt = data_set_fmt;
|
||||
context->base.read = data_read;
|
||||
context->base.write = data_write;
|
||||
context->base.close = data_close;
|
||||
|
||||
return &context->base;
|
||||
}
|
||||
|
||||
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_data(struct Device* i2s_controller) {
|
||||
return new_i2s_data(i2s_controller, false);
|
||||
}
|
||||
|
||||
const audio_codec_data_if_t* audio_codec_adapter_new_i2s_pdm_data(struct Device* i2s_controller) {
|
||||
return new_i2s_data(i2s_controller, true);
|
||||
}
|
||||
|
||||
// Note: esp_codec_dev already provides audio_codec_delete_data_if() (calls ->close then frees);
|
||||
// no adapter-specific cleanup is needed, so we don't redefine it here.
|
||||
@@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/gpio_descriptor.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#define TAG "AudioCodecGpio"
|
||||
|
||||
struct GpioAdapterContext {
|
||||
audio_codec_gpio_if_t base;
|
||||
const struct GpioPinSpec* pins;
|
||||
size_t pin_count;
|
||||
struct GpioDescriptor** descriptors;
|
||||
};
|
||||
|
||||
static struct GpioAdapterContext* g_context = NULL;
|
||||
|
||||
static struct GpioDescriptor* acquire_descriptor(int16_t gpio) {
|
||||
if (g_context == NULL || gpio < 0 || (size_t) gpio >= g_context->pin_count) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (g_context->descriptors[gpio] != NULL) {
|
||||
return g_context->descriptors[gpio];
|
||||
}
|
||||
|
||||
const struct GpioPinSpec* spec = &g_context->pins[gpio];
|
||||
if (spec->gpio_controller == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct GpioDescriptor* descriptor = gpio_descriptor_acquire(spec->gpio_controller, spec->pin, GPIO_OWNER_GPIO);
|
||||
if (descriptor != NULL) {
|
||||
gpio_descriptor_set_flags(descriptor, spec->flags);
|
||||
g_context->descriptors[gpio] = descriptor;
|
||||
}
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
static int gpio_setup(int16_t gpio, audio_gpio_dir_t dir, audio_gpio_mode_t mode) {
|
||||
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
|
||||
if (descriptor == NULL) {
|
||||
return ESP_CODEC_DEV_NOT_FOUND;
|
||||
}
|
||||
|
||||
gpio_flags_t flags = GPIO_FLAG_NONE;
|
||||
gpio_descriptor_get_flags(descriptor, &flags);
|
||||
flags &= ~(GPIO_FLAG_DIRECTION_INPUT | GPIO_FLAG_DIRECTION_OUTPUT | GPIO_FLAG_PULL_UP | GPIO_FLAG_PULL_DOWN);
|
||||
|
||||
flags |= (dir == AUDIO_GPIO_DIR_OUT) ? GPIO_FLAG_DIRECTION_OUTPUT : GPIO_FLAG_DIRECTION_INPUT;
|
||||
if ((mode & AUDIO_GPIO_MODE_PULL_UP) != 0) {
|
||||
flags |= GPIO_FLAG_PULL_UP;
|
||||
}
|
||||
if ((mode & AUDIO_GPIO_MODE_PULL_DOWN) != 0) {
|
||||
flags |= GPIO_FLAG_PULL_DOWN;
|
||||
}
|
||||
|
||||
return (gpio_descriptor_set_flags(descriptor, flags) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR;
|
||||
}
|
||||
|
||||
static int gpio_set(int16_t gpio, bool high) {
|
||||
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
|
||||
if (descriptor == NULL) {
|
||||
return ESP_CODEC_DEV_NOT_FOUND;
|
||||
}
|
||||
return (gpio_descriptor_set_level(descriptor, high) == ERROR_NONE) ? ESP_CODEC_DEV_OK : ESP_CODEC_DEV_DRV_ERR;
|
||||
}
|
||||
|
||||
// esp_codec_dev's gpio_if_t::get returns plain bool with no error channel, so a failed
|
||||
// acquire and a legitimate low reading are indistinguishable to the caller; log so the
|
||||
// failure is at least visible.
|
||||
static bool gpio_get(int16_t gpio) {
|
||||
struct GpioDescriptor* descriptor = acquire_descriptor(gpio);
|
||||
if (descriptor == NULL) {
|
||||
LOG_E(TAG, "gpio_get: failed to acquire descriptor for pin %d", gpio);
|
||||
return false;
|
||||
}
|
||||
bool high = false;
|
||||
gpio_descriptor_get_level(descriptor, &high);
|
||||
return high;
|
||||
}
|
||||
|
||||
const audio_codec_gpio_if_t* audio_codec_adapter_new_gpio(const struct GpioPinSpec* pins, size_t pin_count) {
|
||||
if (pins == NULL || pin_count == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// esp_codec_dev's gpio_if_t has no per-call user context -- only a single global instance
|
||||
// is supported (see g_context usage in acquire_descriptor()/gpio_setup/etc). A second
|
||||
// call would silently replace it underneath whatever codec already holds the first
|
||||
// pointer, so refuse rather than corrupt that codec's GPIO access.
|
||||
if (g_context != NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct GpioAdapterContext* context = (struct GpioAdapterContext*) calloc(1, sizeof(struct GpioAdapterContext));
|
||||
if (context == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context->descriptors = (struct GpioDescriptor**) calloc(pin_count, sizeof(struct GpioDescriptor*));
|
||||
if (context->descriptors == NULL) {
|
||||
free(context);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context->pins = pins;
|
||||
context->pin_count = pin_count;
|
||||
context->base.setup = gpio_setup;
|
||||
context->base.set = gpio_set;
|
||||
context->base.get = gpio_get;
|
||||
|
||||
g_context = context;
|
||||
|
||||
return &context->base;
|
||||
}
|
||||
|
||||
// Note: esp_codec_dev's generic audio_codec_delete_gpio_if() only frees the struct — it doesn't
|
||||
// know about our descriptor array, so we provide our own cleanup under a non-colliding name.
|
||||
int audio_codec_adapter_delete_gpio(const audio_codec_gpio_if_t* gpio_if) {
|
||||
struct GpioAdapterContext* context = (struct GpioAdapterContext*) gpio_if;
|
||||
for (size_t i = 0; i < context->pin_count; i++) {
|
||||
if (context->descriptors[i] != NULL) {
|
||||
gpio_descriptor_release(context->descriptors[i]);
|
||||
}
|
||||
}
|
||||
free(context->descriptors);
|
||||
|
||||
if (g_context == context) {
|
||||
g_context = NULL;
|
||||
}
|
||||
free(context);
|
||||
return ESP_CODEC_DEV_OK;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(audio-stream-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Audio stream module
|
||||
|
||||
Defines `AUDIO_STREAM_TYPE` and `AudioStreamApi`: the high-level, full-duplex
|
||||
audio API that apps (including ELF side-loaded apps) and `AudioService` should
|
||||
use instead of talking to an `AUDIO_CODEC_TYPE` device directly. Binds to one
|
||||
input-capable and/or one output-capable codec device (found automatically at
|
||||
start; see `Drivers/audio-codec-module`) and adds on top of it:
|
||||
|
||||
- Resampling, so the same app code works at any requested sample rate
|
||||
regardless of what rate the bound codec natively runs at.
|
||||
- Shared volume/mute/enable state per direction, with a change callback
|
||||
(`AudioStreamChangeCallback`) that `AudioService` subscribes to.
|
||||
- A single always-present device (`audio-stream0`), constructed unconditionally
|
||||
at module-start time before any devicetree codec exists, and bound lazily on
|
||||
first use -- so boards with no audio hardware at all still get a harmless,
|
||||
inert device rather than nothing.
|
||||
|
||||
`open_input`/`open_output` return a stream handle; `read`/`write` on that
|
||||
handle are blocking and must be called from the caller's own task, never from
|
||||
the main/LVGL thread.
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,2 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module audio_stream_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,821 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_stream.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#define TAG "AudioStream"
|
||||
|
||||
namespace {
|
||||
|
||||
// Linear-interpolation resampler. Cheap and good enough for voice/UI audio; S3/P4 have
|
||||
// plenty of headroom for this at the rates this subsystem targets (16k/44.1k/48k).
|
||||
// Operates on interleaved 16-bit PCM, which is what esp_codec_dev / our codec drivers use.
|
||||
size_t resample_s16(const int16_t* in, size_t in_frames, uint8_t channels,
|
||||
uint32_t in_rate, uint32_t out_rate,
|
||||
int16_t* out, size_t out_frame_capacity) {
|
||||
if (in_rate == out_rate) {
|
||||
size_t frames = (in_frames < out_frame_capacity) ? in_frames : out_frame_capacity;
|
||||
std::memcpy(out, in, frames * channels * sizeof(int16_t));
|
||||
return frames;
|
||||
}
|
||||
|
||||
if (in_frames == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double ratio = (double) in_rate / (double) out_rate;
|
||||
size_t out_frames = 0;
|
||||
for (; out_frames < out_frame_capacity; out_frames++) {
|
||||
double src_pos = (double) out_frames * ratio;
|
||||
size_t src_index = (size_t) src_pos;
|
||||
if (src_index + 1 >= in_frames) {
|
||||
if (src_index >= in_frames) {
|
||||
break;
|
||||
}
|
||||
// Last frame: no next sample to interpolate with, repeat it.
|
||||
for (uint8_t channel = 0; channel < channels; channel++) {
|
||||
out[out_frames * channels + channel] = in[src_index * channels + channel];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
double frac = src_pos - (double) src_index;
|
||||
for (uint8_t channel = 0; channel < channels; channel++) {
|
||||
int16_t a = in[src_index * channels + channel];
|
||||
int16_t b = in[(src_index + 1) * channels + channel];
|
||||
out[out_frames * channels + channel] = (int16_t) ((double) a + ((double) b - (double) a) * frac);
|
||||
}
|
||||
}
|
||||
|
||||
return out_frames;
|
||||
}
|
||||
|
||||
// Converts between interleaved S16 PCM with different channel counts.
|
||||
// - channels_out < channels_in: downmix by averaging the first `channels_out` source channels
|
||||
// plus folding any extra source channels into them round-robin (e.g. 4 -> 1 averages all 4;
|
||||
// 4 -> 2 averages {0,2} into channel 0 and {1,3} into channel 1).
|
||||
// - channels_out > channels_in: upmix by repeating source channels round-robin (e.g. mono -> stereo
|
||||
// duplicates the single channel into both output channels).
|
||||
// - equal: copies through.
|
||||
void convert_channels_s16(const int16_t* in, size_t frames, uint8_t channels_in,
|
||||
int16_t* out, uint8_t channels_out) {
|
||||
if (channels_in == channels_out) {
|
||||
std::memcpy(out, in, frames * channels_in * sizeof(int16_t));
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t frame = 0; frame < frames; frame++) {
|
||||
const int16_t* in_frame = in + frame * channels_in;
|
||||
int16_t* out_frame = out + frame * channels_out;
|
||||
|
||||
if (channels_out < channels_in) {
|
||||
for (uint8_t out_ch = 0; out_ch < channels_out; out_ch++) {
|
||||
int32_t sum = 0;
|
||||
uint8_t count = 0;
|
||||
for (uint8_t in_ch = out_ch; in_ch < channels_in; in_ch += channels_out) {
|
||||
sum += in_frame[in_ch];
|
||||
count++;
|
||||
}
|
||||
out_frame[out_ch] = (int16_t) (sum / (int32_t) count);
|
||||
}
|
||||
} else {
|
||||
for (uint8_t out_ch = 0; out_ch < channels_out; out_ch++) {
|
||||
out_frame[out_ch] = in_frame[out_ch % channels_in];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioStreamHandleImpl : AudioStreamHandleData {
|
||||
AudioCodecDirection direction = AUDIO_CODEC_DIR_BOTH;
|
||||
struct AudioStreamConfig config = {};
|
||||
uint32_t codec_rate = 0;
|
||||
uint8_t codec_channels = 0;
|
||||
uint8_t bytes_per_frame = 0; // app-side frame size (config.channels)
|
||||
uint8_t codec_bytes_per_frame = 0; // codec-side frame size (codec_channels)
|
||||
float input_gain = 1.0f; // fixed digital gain multiplier, input direction only (see audio_codec_get_input_gain_multiplier)
|
||||
std::vector<uint8_t> codec_buffer; // raw codec-rate/codec-channel PCM, scratch
|
||||
std::vector<uint8_t> convert_buffer; // intermediate scratch for the second conversion stage
|
||||
|
||||
// Lifetime guard: close_stream() can be triggered from a different task than the one
|
||||
// doing read()/write() (e.g. the Settings UI disabling output while SfxEngine's audio
|
||||
// task is mid-write). `closing` keeps new I/O calls out, `busy_count` tracks I/O calls
|
||||
// currently in flight, and `drain_semaphore` lets close_stream() block until they finish
|
||||
// before freeing the handle. All three are only touched while `AudioStreamData::mutex`
|
||||
// is held, except for the give/take on drain_semaphore itself.
|
||||
bool closing = false;
|
||||
int busy_count = 0;
|
||||
SemaphoreHandle_t drain_semaphore = nullptr;
|
||||
};
|
||||
|
||||
struct AudioStreamData {
|
||||
Device* input_codec = nullptr;
|
||||
Device* output_codec = nullptr;
|
||||
bool input_enabled = true;
|
||||
bool output_enabled = true;
|
||||
|
||||
// Codecs reject volume/mute/gain calls until their chip is initialized, which only
|
||||
// happens when a stream of that direction is first opened (audio_codec_open ->
|
||||
// esp_codec_dev_open -> chip's open()/enable()). A Settings UI must be able to set
|
||||
// these regardless of whether anything is currently streaming, so we cache the desired
|
||||
// values here, apply them best-effort immediately, and replay them once the codec opens.
|
||||
float input_volume = 100.0f;
|
||||
float output_volume = 100.0f;
|
||||
bool input_muted = false;
|
||||
bool output_muted = false;
|
||||
AudioStreamHandleImpl* open_input = nullptr;
|
||||
AudioStreamHandleImpl* open_output = nullptr;
|
||||
// Guards open_input/open_output and the closing/busy_count fields of any handle reachable
|
||||
// through them, so close (possibly forced by set_enabled) can't race with read/write.
|
||||
SemaphoreHandle_t mutex = nullptr;
|
||||
|
||||
AudioStreamChangeCallback change_callback = nullptr;
|
||||
void* change_callback_user_data = nullptr;
|
||||
};
|
||||
|
||||
// Reads the registered change callback under the lock, then invokes it outside the lock
|
||||
// (same lock-then-release-then-act pattern as codec_for_direction).
|
||||
void notify_change(AudioStreamData* data, Device* device, AudioCodecDirection direction, AudioStreamChange change) {
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
AudioStreamChangeCallback callback = data->change_callback;
|
||||
void* user_data = data->change_callback_user_data;
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
if (callback != nullptr) {
|
||||
callback(device, direction, change, user_data);
|
||||
}
|
||||
}
|
||||
|
||||
#define GET_DATA(device) (static_cast<AudioStreamData*>(device_get_driver_data(device)))
|
||||
|
||||
struct CodecSearchContext {
|
||||
AudioCodecDirection wanted_direction;
|
||||
Device* exact_match = nullptr;
|
||||
Device* fallback_match = nullptr;
|
||||
};
|
||||
|
||||
bool find_codec_by_direction(Device* device, void* context_ptr) {
|
||||
auto* context = static_cast<CodecSearchContext*>(context_ptr);
|
||||
if (!device_is_ready(device)) {
|
||||
return true; // continue searching
|
||||
}
|
||||
|
||||
AudioCodecDirection capabilities = AUDIO_CODEC_DIR_BOTH;
|
||||
if (audio_codec_get_capabilities(device, &capabilities) != ERROR_NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (capabilities == context->wanted_direction) {
|
||||
// Dedicated codec for exactly this direction (e.g. an input-only mic ADC) --
|
||||
// prefer it over a wider-capability codec and stop looking.
|
||||
context->exact_match = device;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((capabilities & context->wanted_direction) == context->wanted_direction
|
||||
&& context->fallback_match == nullptr) {
|
||||
// Supports the wanted direction as part of a wider capability set (e.g. a
|
||||
// BOTH-capable codec asked for INPUT). Remember it but keep looking -- boards
|
||||
// commonly pair a BOTH-capable output codec with a separate dedicated input
|
||||
// ADC, and binding the wrong one makes both directions fight over the same
|
||||
// physical esp_codec_dev handle (reopen for input reconfigures/breaks output).
|
||||
context->fallback_match = device;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Device* find_first_codec_supporting(AudioCodecDirection direction) {
|
||||
CodecSearchContext context = { .wanted_direction = direction };
|
||||
device_for_each_of_type(&AUDIO_CODEC_TYPE, &context, find_codec_by_direction);
|
||||
return (context.exact_match != nullptr) ? context.exact_match : context.fallback_match;
|
||||
}
|
||||
|
||||
// The audio-stream device is constructed while modules start, which happens before the
|
||||
// device tree's codec devices (nested under i2c0) are started. So codecs can't be resolved
|
||||
// at start_device time — resolve (and cache) them lazily on first use instead, by which
|
||||
// point the device tree has finished starting.
|
||||
Device* codec_for_direction(AudioStreamData* data, AudioCodecDirection direction) {
|
||||
Device** slot = (direction == AUDIO_CODEC_DIR_INPUT) ? &data->input_codec : &data->output_codec;
|
||||
|
||||
// Fast path: already resolved (the common case after first use).
|
||||
Device* existing = *slot;
|
||||
if (existing != nullptr) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Slow path: resolve outside the lock (device_for_each_of_type can be costly), then
|
||||
// re-check under the mutex before committing -- avoids two threads racing to write
|
||||
// *slot (and double-logging "Bound ... codec").
|
||||
Device* found = find_first_codec_supporting(direction);
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (*slot == nullptr) {
|
||||
*slot = found;
|
||||
if (found != nullptr) {
|
||||
LOG_I(TAG, "Bound %s codec: %s", (direction == AUDIO_CODEC_DIR_INPUT) ? "input" : "output", found->name);
|
||||
}
|
||||
}
|
||||
Device* result = *slot;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Marks an I/O operation as in-flight on `handle`, preventing close_stream() from freeing it
|
||||
// underneath us. Returns false (and does nothing further) if the handle is closing/closed --
|
||||
// callers must bail out with an error in that case. Must be paired with io_end().
|
||||
bool io_begin(AudioStreamData* data, AudioStreamHandleImpl* handle) {
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
bool is_input = (handle->direction == AUDIO_CODEC_DIR_INPUT);
|
||||
AudioStreamHandleImpl* slot_value = is_input ? data->open_input : data->open_output;
|
||||
if (slot_value != handle || handle->closing) {
|
||||
xSemaphoreGive(data->mutex);
|
||||
return false;
|
||||
}
|
||||
handle->busy_count++;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
void io_end(AudioStreamData* data, AudioStreamHandleImpl* handle) {
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
handle->busy_count--;
|
||||
if (handle->closing && handle->busy_count == 0) {
|
||||
xSemaphoreGive(handle->drain_semaphore);
|
||||
}
|
||||
xSemaphoreGive(data->mutex);
|
||||
}
|
||||
|
||||
// region AudioStreamApi
|
||||
|
||||
error_t open_stream(Device* device, const struct AudioStreamConfig* config, AudioCodecDirection direction, AudioStreamHandle* out_handle) {
|
||||
if (config->bits_per_sample != 8 && config->bits_per_sample != 16
|
||||
&& config->bits_per_sample != 24 && config->bits_per_sample != 32) {
|
||||
// bytes_per_frame/codec_bytes_per_frame below assume a whole number of bytes per
|
||||
// sample; anything else corrupts every frame-size calculation in read/write_stream.
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (config->channels == 0) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
bool is_input = (direction == AUDIO_CODEC_DIR_INPUT);
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
|
||||
if ((is_input && !data->input_enabled) || (!is_input && !data->output_enabled)) {
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
||||
if (*slot != nullptr) {
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// Reserve the slot with a placeholder so concurrent opens can't race past the check
|
||||
// above while we do the (potentially slow) codec open below outside the lock.
|
||||
auto* reservation = reinterpret_cast<AudioStreamHandleImpl*>(1);
|
||||
*slot = reservation;
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
uint32_t codec_rate = 0;
|
||||
if (audio_codec_get_native_sample_rate(codec, direction, &codec_rate) != ERROR_NONE || codec_rate == 0) {
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (*slot == reservation) { *slot = nullptr; }
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// The codec must be opened with its native channel layout (e.g. 4 for a 4-slot TDM mic
|
||||
// ADC) -- opening it with the app's requested channel count can silently corrupt the
|
||||
// stream (ES7210 in TDM mode halves its configured bit depth for <= 2 channels). We
|
||||
// convert between the codec's layout and the app's requested channel count ourselves.
|
||||
uint8_t codec_channels = config->channels;
|
||||
if (audio_codec_get_native_channels(codec, direction, &codec_channels) != ERROR_NONE || codec_channels == 0) {
|
||||
codec_channels = config->channels;
|
||||
}
|
||||
|
||||
struct AudioCodecStreamConfig codec_config = {
|
||||
.sample_rate = codec_rate,
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channels = codec_channels,
|
||||
.direction = direction,
|
||||
};
|
||||
|
||||
if (audio_codec_open(codec, &codec_config) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to open codec for %s", is_input ? "input" : "output");
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (*slot == reservation) { *slot = nullptr; }
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// The chip is initialized now -- replay any volume/mute settings that were requested
|
||||
// before this stream existed (set_volume/set_mute cache them but the chip rejects them
|
||||
// until opened).
|
||||
if (is_input) {
|
||||
audio_codec_set_volume(codec, AUDIO_CODEC_DIR_INPUT, data->input_volume);
|
||||
audio_codec_set_mute(codec, AUDIO_CODEC_DIR_INPUT, data->input_muted);
|
||||
} else {
|
||||
audio_codec_set_volume(codec, AUDIO_CODEC_DIR_OUTPUT, data->output_volume);
|
||||
audio_codec_set_mute(codec, AUDIO_CODEC_DIR_OUTPUT, data->output_muted);
|
||||
}
|
||||
|
||||
auto* handle = new AudioStreamHandleImpl();
|
||||
handle->device = device;
|
||||
handle->direction = direction;
|
||||
handle->config = *config;
|
||||
handle->codec_rate = codec_rate;
|
||||
handle->codec_channels = codec_channels;
|
||||
handle->bytes_per_frame = (uint8_t) ((config->bits_per_sample / 8) * config->channels);
|
||||
handle->codec_bytes_per_frame = (uint8_t) ((config->bits_per_sample / 8) * codec_channels);
|
||||
if (is_input) {
|
||||
audio_codec_get_input_gain_multiplier(codec, &handle->input_gain);
|
||||
}
|
||||
handle->drain_semaphore = xSemaphoreCreateBinary();
|
||||
if (handle->drain_semaphore == nullptr) {
|
||||
LOG_E(TAG, "Failed to create drain semaphore");
|
||||
delete handle;
|
||||
audio_codec_close(codec);
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (*slot == reservation) { *slot = nullptr; }
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
*slot = handle;
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
*out_handle = handle;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t open_input(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle) {
|
||||
return open_stream(device, config, AUDIO_CODEC_DIR_INPUT, out_handle);
|
||||
}
|
||||
|
||||
error_t open_output(Device* device, const struct AudioStreamConfig* config, AudioStreamHandle* out_handle) {
|
||||
return open_stream(device, config, AUDIO_CODEC_DIR_OUTPUT, out_handle);
|
||||
}
|
||||
|
||||
error_t read_stream(AudioStreamHandle handle_base, void* out_data, size_t data_size, size_t* bytes_read, TickType_t timeout) {
|
||||
auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
|
||||
if (handle->direction != AUDIO_CODEC_DIR_INPUT || handle->bytes_per_frame == 0) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
auto* data = GET_DATA(handle->device);
|
||||
if (data == nullptr || data->input_codec == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
size_t requested_frames = data_size / handle->bytes_per_frame;
|
||||
if (requested_frames == 0) {
|
||||
if (bytes_read != nullptr) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (!io_begin(data, handle)) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
bool same_rate = (handle->codec_rate == handle->config.sample_rate);
|
||||
bool same_channels = (handle->codec_channels == handle->config.channels);
|
||||
|
||||
error_t result;
|
||||
if (same_rate && same_channels) {
|
||||
size_t codec_bytes_read = 0;
|
||||
result = audio_codec_read(data->input_codec, out_data, data_size, &codec_bytes_read, timeout);
|
||||
if (bytes_read != nullptr) {
|
||||
*bytes_read = codec_bytes_read;
|
||||
}
|
||||
} else {
|
||||
// Read enough codec-rate frames to produce the requested number of output-rate frames.
|
||||
size_t codec_frames = (size_t) ((double) requested_frames * ((double) handle->codec_rate / (double) handle->config.sample_rate)) + 2;
|
||||
size_t codec_bytes_needed = codec_frames * handle->codec_bytes_per_frame;
|
||||
if (handle->codec_buffer.size() < codec_bytes_needed) {
|
||||
handle->codec_buffer.resize(codec_bytes_needed);
|
||||
}
|
||||
|
||||
size_t codec_bytes_read = 0;
|
||||
result = audio_codec_read(data->input_codec, handle->codec_buffer.data(), codec_bytes_needed, &codec_bytes_read, timeout);
|
||||
if (result == ERROR_NONE) {
|
||||
size_t codec_frames_read = codec_bytes_read / handle->codec_bytes_per_frame;
|
||||
const int16_t* rate_input = reinterpret_cast<const int16_t*>(handle->codec_buffer.data());
|
||||
uint8_t rate_input_channels = handle->codec_channels;
|
||||
size_t rate_input_frames = codec_frames_read;
|
||||
|
||||
// Downmix first (while still at the codec's higher rate -- cheaper) if needed.
|
||||
if (!same_channels) {
|
||||
size_t convert_bytes_needed = codec_frames_read * handle->bytes_per_frame;
|
||||
if (handle->convert_buffer.size() < convert_bytes_needed) {
|
||||
handle->convert_buffer.resize(convert_bytes_needed);
|
||||
}
|
||||
convert_channels_s16(rate_input, codec_frames_read, handle->codec_channels,
|
||||
reinterpret_cast<int16_t*>(handle->convert_buffer.data()), handle->config.channels);
|
||||
rate_input = reinterpret_cast<const int16_t*>(handle->convert_buffer.data());
|
||||
rate_input_channels = handle->config.channels;
|
||||
}
|
||||
|
||||
size_t out_frames = resample_s16(
|
||||
rate_input, rate_input_frames, rate_input_channels,
|
||||
handle->codec_rate, handle->config.sample_rate,
|
||||
reinterpret_cast<int16_t*>(out_data), requested_frames);
|
||||
|
||||
if (bytes_read != nullptr) {
|
||||
*bytes_read = out_frames * handle->bytes_per_frame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result == ERROR_NONE && handle->input_gain != 1.0f && bytes_read != nullptr && *bytes_read > 0) {
|
||||
auto* samples = reinterpret_cast<int16_t*>(out_data);
|
||||
size_t sample_count = *bytes_read / sizeof(int16_t);
|
||||
for (size_t i = 0; i < sample_count; i++) {
|
||||
float boosted = (float) samples[i] * handle->input_gain;
|
||||
samples[i] = (int16_t) (boosted < -32768.0f ? -32768.0f : boosted > 32767.0f ? 32767.0f : boosted);
|
||||
}
|
||||
}
|
||||
|
||||
io_end(data, handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t write_stream(AudioStreamHandle handle_base, const void* in_data, size_t data_size, size_t* bytes_written, TickType_t timeout) {
|
||||
auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
|
||||
if (handle->direction != AUDIO_CODEC_DIR_OUTPUT || handle->bytes_per_frame == 0) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
auto* data = GET_DATA(handle->device);
|
||||
if (data == nullptr || data->output_codec == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
size_t in_frames = data_size / handle->bytes_per_frame;
|
||||
if (in_frames == 0) {
|
||||
if (bytes_written != nullptr) {
|
||||
*bytes_written = 0;
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (!io_begin(data, handle)) {
|
||||
return ERROR_INVALID_STATE;
|
||||
}
|
||||
|
||||
bool same_rate = (handle->codec_rate == handle->config.sample_rate);
|
||||
bool same_channels = (handle->codec_channels == handle->config.channels);
|
||||
|
||||
error_t result;
|
||||
if (same_rate && same_channels) {
|
||||
size_t codec_bytes_written = 0;
|
||||
result = audio_codec_write(data->output_codec, in_data, data_size, &codec_bytes_written, timeout);
|
||||
if (bytes_written != nullptr) {
|
||||
// Report in terms of input bytes consumed, matching codec_bytes_written 1:1 here.
|
||||
*bytes_written = codec_bytes_written;
|
||||
}
|
||||
} else {
|
||||
const int16_t* rate_input = reinterpret_cast<const int16_t*>(in_data);
|
||||
uint8_t rate_input_channels = handle->config.channels;
|
||||
size_t rate_input_frames = in_frames;
|
||||
|
||||
// Upmix/downmix first (while still at the app's rate -- cheaper if downmixing) if needed.
|
||||
if (!same_channels) {
|
||||
size_t convert_bytes_needed = in_frames * handle->codec_bytes_per_frame;
|
||||
if (handle->convert_buffer.size() < convert_bytes_needed) {
|
||||
handle->convert_buffer.resize(convert_bytes_needed);
|
||||
}
|
||||
convert_channels_s16(rate_input, in_frames, handle->config.channels,
|
||||
reinterpret_cast<int16_t*>(handle->convert_buffer.data()), handle->codec_channels);
|
||||
rate_input = reinterpret_cast<const int16_t*>(handle->convert_buffer.data());
|
||||
rate_input_channels = handle->codec_channels;
|
||||
}
|
||||
|
||||
size_t codec_frame_capacity = (size_t) ((double) rate_input_frames * ((double) handle->codec_rate / (double) handle->config.sample_rate)) + 2;
|
||||
size_t codec_bytes_capacity = codec_frame_capacity * handle->codec_bytes_per_frame;
|
||||
if (handle->codec_buffer.size() < codec_bytes_capacity) {
|
||||
handle->codec_buffer.resize(codec_bytes_capacity);
|
||||
}
|
||||
|
||||
size_t codec_frames;
|
||||
if (same_rate) {
|
||||
codec_frames = rate_input_frames;
|
||||
std::memcpy(handle->codec_buffer.data(), rate_input, rate_input_frames * handle->codec_bytes_per_frame);
|
||||
} else {
|
||||
codec_frames = resample_s16(
|
||||
rate_input, rate_input_frames, rate_input_channels,
|
||||
handle->config.sample_rate, handle->codec_rate,
|
||||
reinterpret_cast<int16_t*>(handle->codec_buffer.data()), codec_frame_capacity);
|
||||
}
|
||||
|
||||
size_t codec_bytes_to_write = codec_frames * handle->codec_bytes_per_frame;
|
||||
size_t codec_bytes_written = 0;
|
||||
result = audio_codec_write(data->output_codec, handle->codec_buffer.data(), codec_bytes_to_write, &codec_bytes_written, timeout);
|
||||
if (result == ERROR_NONE && bytes_written != nullptr) {
|
||||
// The caller provided `data_size` worth of input; we consumed all of it (resampled/converted).
|
||||
*bytes_written = data_size;
|
||||
}
|
||||
}
|
||||
|
||||
io_end(data, handle);
|
||||
return result;
|
||||
}
|
||||
|
||||
error_t close_stream(AudioStreamHandle handle_base) {
|
||||
auto* handle = static_cast<AudioStreamHandleImpl*>(handle_base);
|
||||
auto* data = GET_DATA(handle->device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
bool is_input = (handle->direction == AUDIO_CODEC_DIR_INPUT);
|
||||
Device* codec = is_input ? data->input_codec : data->output_codec;
|
||||
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (handle->closing) {
|
||||
// Already being closed by another caller (e.g. concurrent set_enabled + app close).
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
handle->closing = true;
|
||||
if (*slot == handle) {
|
||||
*slot = nullptr;
|
||||
}
|
||||
bool must_drain = (handle->busy_count > 0);
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
if (must_drain && handle->drain_semaphore != nullptr) {
|
||||
xSemaphoreTake(handle->drain_semaphore, portMAX_DELAY);
|
||||
}
|
||||
|
||||
if (codec != nullptr) {
|
||||
audio_codec_close(codec);
|
||||
}
|
||||
|
||||
if (handle->drain_semaphore != nullptr) {
|
||||
vSemaphoreDelete(handle->drain_semaphore);
|
||||
}
|
||||
|
||||
delete handle;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
bool is_input = (direction == AUDIO_CODEC_DIR_INPUT);
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (is_input) { data->input_volume = volume_percent; } else { data->output_volume = volume_percent; }
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
// The chip rejects this until it's been opened by a stream of this direction; that's
|
||||
// fine -- the cached value above gets replayed in open_stream() once it is.
|
||||
audio_codec_set_volume(codec, direction, volume_percent);
|
||||
notify_change(data, device, direction, AUDIO_STREAM_CHANGE_VOLUME);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (audio_codec_get_volume(codec, direction, volume_percent) == ERROR_NONE) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// Codec not open yet -- report the cached/desired value instead.
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
*volume_percent = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_volume : data->output_volume;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
bool is_input = (direction == AUDIO_CODEC_DIR_INPUT);
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (is_input) { data->input_muted = muted; } else { data->output_muted = muted; }
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
// As with volume, the chip may reject this until opened; cached value is replayed
|
||||
// in open_stream().
|
||||
audio_codec_set_mute(codec, direction, muted);
|
||||
notify_change(data, device, direction, AUDIO_STREAM_CHANGE_MUTE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (audio_codec_get_mute(codec, direction, muted) == ERROR_NONE) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
*muted = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_muted : data->output_muted;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_enabled(Device* device, AudioCodecDirection direction, bool enabled) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
bool is_input = (direction == AUDIO_CODEC_DIR_INPUT);
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
if (is_input) {
|
||||
data->input_enabled = enabled;
|
||||
} else {
|
||||
data->output_enabled = enabled;
|
||||
}
|
||||
|
||||
// Capture and clear the slot under the lock so we hand close_stream() a pointer that
|
||||
// can't simultaneously be torn down by a racing close from the owning app (close_stream
|
||||
// re-checks `*slot == handle` and no-ops if it's already been cleared/replaced).
|
||||
AudioStreamHandleImpl* to_close = nullptr;
|
||||
if (!enabled) {
|
||||
AudioStreamHandleImpl** slot = is_input ? &data->open_input : &data->open_output;
|
||||
to_close = *slot;
|
||||
}
|
||||
xSemaphoreGive(data->mutex);
|
||||
|
||||
if (to_close != nullptr) {
|
||||
close_stream(to_close);
|
||||
}
|
||||
|
||||
notify_change(data, device, direction, AUDIO_STREAM_CHANGE_ENABLED);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_enabled(Device* device, AudioCodecDirection direction, bool* enabled) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
Device* codec = codec_for_direction(data, direction);
|
||||
if (codec == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
*enabled = (direction == AUDIO_CODEC_DIR_INPUT) ? data->input_enabled : data->output_enabled;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t is_supported(Device* device, AudioCodecDirection direction, bool* supported) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*supported = codec_for_direction(data, direction) != nullptr;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_change_callback(Device* device, AudioStreamChangeCallback callback, void* user_data) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(data->mutex, portMAX_DELAY);
|
||||
data->change_callback = callback;
|
||||
data->change_callback_user_data = user_data;
|
||||
xSemaphoreGive(data->mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioStreamApi API = {
|
||||
.open_input = open_input,
|
||||
.open_output = open_output,
|
||||
.read = read_stream,
|
||||
.write = write_stream,
|
||||
.close = close_stream,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.set_enabled = set_enabled,
|
||||
.get_enabled = get_enabled,
|
||||
.is_supported = is_supported,
|
||||
.set_change_callback = set_change_callback,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
auto* data = new AudioStreamData();
|
||||
data->mutex = xSemaphoreCreateMutex();
|
||||
if (data->mutex == nullptr) {
|
||||
delete data;
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->open_input != nullptr) {
|
||||
close_stream(data->open_input);
|
||||
}
|
||||
if (data->open_output != nullptr) {
|
||||
close_stream(data->open_output);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
if (data->mutex != nullptr) {
|
||||
vSemaphoreDelete(data->mutex);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver audio_stream_driver = {
|
||||
.name = "audio-stream",
|
||||
.compatible = (const char*[]) { "audio-stream", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_STREAM_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
Device audio_stream_device = {
|
||||
.address = 0,
|
||||
.name = "audio-stream0",
|
||||
.config = nullptr,
|
||||
.parent = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver audio_stream_driver;
|
||||
extern Device audio_stream_device;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&audio_stream_driver) == ERROR_NONE);
|
||||
|
||||
/* The audio-stream device is a software aggregate with no devicetree backing — it binds
|
||||
* to whichever AUDIO_CODEC_TYPE devices are present at start_device time. Constructed
|
||||
* here (after codec drivers/devices are up) rather than from a .dts node. */
|
||||
if (device_construct_add_start(&audio_stream_device, "audio-stream") != ERROR_NONE) {
|
||||
LOG_W("AudioStream", "No audio codec available; audio-stream0 not started");
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
if (device_is_added(&audio_stream_device)) {
|
||||
device_stop(&audio_stream_device);
|
||||
device_remove(&audio_stream_device);
|
||||
device_destruct(&audio_stream_device);
|
||||
}
|
||||
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&audio_stream_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol audio_stream_module_symbols[];
|
||||
|
||||
Module audio_stream_module = {
|
||||
.name = "audio-stream",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = audio_stream_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol audio_stream_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(aw88298-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev esp_driver_i2s
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# AW88298 speaker amplifier
|
||||
|
||||
A driver for the `AW88298` smart speaker power amplifier by Awinic, wired as an
|
||||
output-only `AUDIO_CODEC_TYPE` device. The I2C bus is the device's parent; the I2S
|
||||
controller carrying audio data is referenced via a devicetree phandle.
|
||||
|
||||
Wraps Espressif's `esp_codec_dev` AW88298 implementation. If a hardware reset
|
||||
(`pin-reset`) is wired, this driver pulses it itself before opening the codec,
|
||||
rather than handing it to `esp_codec_dev`'s own reset handling -- the vendor
|
||||
driver treats a reset pin index of `0` as "no pin wired" and silently skips the
|
||||
reset, which doesn't match the `audio_codec_adapter_new_gpio()` convention used
|
||||
elsewhere in this codebase (a single-pin array is always referenced at index 0).
|
||||
|
||||
See https://www.awinic.com/en/productDetail/AW88298
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,15 @@
|
||||
description: Awinic AW88298 smart speaker power amplifier (output-only)
|
||||
|
||||
include: [ "i2c-device.yaml" ]
|
||||
|
||||
compatible: "awinic,aw88298"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: "Hardware reset (RSTN) pin. The AW88298's I2C interface does not respond until this pin is released, so if wired, this driver asserts/releases reset itself before opening the codec, ensuring correct ordering relative to other devicetree devices. Omit if reset is tied high in hardware."
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/aw88298.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(aw88298, struct Aw88298Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief AW88298 codec device configuration.
|
||||
*
|
||||
* The AW88298 is an output-only (speaker power amp) codec. The I2C bus is the
|
||||
* device's parent (per i2c-device.yaml), while the I2S controller carrying the
|
||||
* audio data is referenced via a devicetree phandle.
|
||||
*
|
||||
* The chip's I2C interface does not respond until its hardware RESET (RSTN)
|
||||
* line is released, so if that pin is wired (e.g. behind a GPIO expander on
|
||||
* M5Stack StackChan/CoreS3), this driver asserts and releases it itself before
|
||||
* opening the codec. This guarantees correct ordering: devicetree GPIO
|
||||
* controller devices (e.g. the expander driving reset_pin) start in
|
||||
* declaration order before this device, whereas board Configuration.cpp
|
||||
* initBoot() callbacks run after ALL devicetree devices have already started
|
||||
* and so cannot reliably gate a devicetree device's own bring-up.
|
||||
*/
|
||||
struct Aw88298Config {
|
||||
/** I2C address on the bus (typically 0x36) */
|
||||
uint8_t address;
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
/** Optional hardware reset (RSTN) pin. GPIO_PIN_SPEC_NONE if tied high in hardware. */
|
||||
struct GpioPinSpec reset_pin;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,364 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/aw88298.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <aw88298_dac.h>
|
||||
#include <esp_codec_dev.h>
|
||||
#include <esp_codec_dev_defaults.h>
|
||||
#include <driver/i2s_types.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#define TAG "AW88298"
|
||||
|
||||
namespace {
|
||||
|
||||
// AW88298 native output rate per M5Unified's _speaker_enabled_cb_cores3(); higher rates
|
||||
// (e.g. 44100Hz) additionally need mclk_multiple = I2S_MCLK_MULTIPLE_384 per aw88298_dac.h.
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 16000;
|
||||
|
||||
struct Aw88298Data {
|
||||
const audio_codec_ctrl_if_t* ctrl_if = nullptr;
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
bool is_open = false;
|
||||
esp_codec_dev_sample_info_t open_sample_info = {};
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Aw88298Config*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<Aw88298Data*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (config->direction == AUDIO_CODEC_DIR_INPUT || config->direction == AUDIO_CODEC_DIR_BOTH) {
|
||||
LOG_E(TAG, "AW88298 is output-only");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = config->channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = (config->sample_rate > 16000) ? I2S_MCLK_MULTIPLE_384 : 0,
|
||||
};
|
||||
|
||||
if (data->is_open) {
|
||||
bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||
&& data->open_sample_info.channel == sample_info.channel
|
||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->is_open = true;
|
||||
data->open_sample_info = sample_info;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) device;
|
||||
(void) buffer;
|
||||
(void) size;
|
||||
(void) bytes_read;
|
||||
(void) timeout;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length write.
|
||||
int result = esp_codec_dev_write(data->codec_device, const_cast<void*>(buffer), (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_written = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (volume_percent < 0.0f || volume_percent > 100.0f) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
int volume = (int) std::lround(volume_percent);
|
||||
return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
int volume = 0;
|
||||
if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (float) volume;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = 2;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_OUTPUT;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = nullptr,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent is not an I2C controller");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new Aw88298Data();
|
||||
|
||||
data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address);
|
||||
data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
|
||||
if (data->ctrl_if == nullptr || data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create adapters");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open control interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// If a reset pin is wired (e.g. behind a GPIO expander), the AW88298's I2C interface
|
||||
// won't respond until it's released. We pulse it ourselves here rather than handing it
|
||||
// to aw88298_codec_new() via gpio_if/reset_pin: the vendor driver's internal
|
||||
// aw88298_reset() treats reset_pin <= 0 as "no pin wired" and no-ops (aw88298.c:197),
|
||||
// but the audio_codec_adapter_new_gpio() convention (matched by dummy_i2s_amp's
|
||||
// pa_pin, see Drivers/dummy-i2s-amp-module) is to pass 0 as the array index of a
|
||||
// single-pin GPioPinSpec array -- so index 0 collides with the vendor's own "disabled"
|
||||
// sentinel and the reset never actually fires. This runs synchronously as part of this
|
||||
// device's own start_device(), so it's correctly ordered relative to devicetree bring-up
|
||||
// (unlike a board Configuration.cpp initBoot() callback, which only runs after ALL
|
||||
// devicetree devices, including this one, have already started).
|
||||
if (config->reset_pin.gpio_controller != nullptr) {
|
||||
auto* reset_descriptor = gpio_descriptor_acquire(config->reset_pin.gpio_controller, config->reset_pin.pin, GPIO_OWNER_GPIO);
|
||||
if (reset_descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire reset pin descriptor");
|
||||
goto cleanup;
|
||||
}
|
||||
gpio_descriptor_set_flags(reset_descriptor, GPIO_FLAG_DIRECTION_OUTPUT);
|
||||
gpio_descriptor_set_level(reset_descriptor, false);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_descriptor_set_level(reset_descriptor, true);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
gpio_descriptor_release(reset_descriptor);
|
||||
}
|
||||
|
||||
{
|
||||
aw88298_codec_cfg_t codec_config = {};
|
||||
codec_config.ctrl_if = data->ctrl_if;
|
||||
codec_config.gpio_if = nullptr;
|
||||
codec_config.reset_pin = -1;
|
||||
codec_config.hw_gain = {};
|
||||
|
||||
data->codec_if = aw88298_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create AW88298 codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver aw88298_driver = {
|
||||
.name = "aw88298",
|
||||
.compatible = (const char*[]) { "awinic,aw88298", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver aw88298_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&aw88298_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&aw88298_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol aw88298_module_symbols[];
|
||||
|
||||
Module aw88298_module = {
|
||||
.name = "aw88298",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = aw88298_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol aw88298_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(aw9523b-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# AW9523B I/O expander
|
||||
|
||||
A driver for the `AW9523B` 16-bit I2C-bus I/O expander with LED driver, by Awinic.
|
||||
Exposes both 8-bit ports (P0/P1) as 16 GPIO pins (0-15, port 0 = pins 0-7, port 1 =
|
||||
pins 8-15) via the `GPIO_CONTROLLER_TYPE` API. P0 is switched to push-pull mode on
|
||||
start (every known board wiring for this chip expects push-pull output, not the
|
||||
open-drain default).
|
||||
|
||||
It does not support pull-up/down resistors, high-impedance outputs, or interrupts;
|
||||
requesting those returns `ERROR_NOT_SUPPORTED`. LED driver functionality is not
|
||||
implemented.
|
||||
|
||||
See https://www.awinic.com/en/productDetail/AW9523B
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,5 @@
|
||||
description: Awinic AW9523B 16-bit I2C-bus I/O expander with LED driver
|
||||
|
||||
include: [ "i2c-device.yaml" ]
|
||||
|
||||
compatible: "awinic,aw9523b"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module aw9523b_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/aw9523b.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(aw9523b, struct Aw9523bConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief AW9523B GPIO expander device configuration.
|
||||
*
|
||||
* 16 pins total: P0_0..P0_7 map to GpioDescriptor pin numbers 0..7, P1_0..P1_7
|
||||
* map to pin numbers 8..15. P0 defaults to open-drain per hardware reset; this
|
||||
* driver switches P0 to push-pull mode on start (matches every known board's
|
||||
* usage of this chip, since open-drain P0 cannot drive an active-high output
|
||||
* without an external pull-up).
|
||||
*/
|
||||
struct Aw9523bConfig {
|
||||
/** I2C address on the bus (typically 0x58) */
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,183 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/aw9523b.h>
|
||||
#include <aw9523b_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/gpio_descriptor.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#define TAG "AW9523B"
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Aw9523bConfig*>((device)->config))
|
||||
|
||||
namespace {
|
||||
|
||||
// Port 0: pins 0..7, Port 1: pins 8..15.
|
||||
constexpr auto AW9523B_REG_INPUT_P0 = 0x00;
|
||||
constexpr auto AW9523B_REG_INPUT_P1 = 0x01;
|
||||
constexpr auto AW9523B_REG_OUTPUT_P0 = 0x02;
|
||||
constexpr auto AW9523B_REG_OUTPUT_P1 = 0x03;
|
||||
// Configuration register: 0 = output, 1 = input (inverted vs. most other expanders).
|
||||
constexpr auto AW9523B_REG_CONFIG_P0 = 0x04;
|
||||
constexpr auto AW9523B_REG_CONFIG_P1 = 0x05;
|
||||
// Global Control Register: bit 4 selects P0 drive mode (0 = open-drain default, 1 = push-pull).
|
||||
constexpr auto AW9523B_REG_GCR = 0x11;
|
||||
constexpr uint8_t AW9523B_GCR_P0_PUSH_PULL = 1U << 4U;
|
||||
|
||||
constexpr uint8_t portRegister(bool isPort1, uint8_t regPort0, uint8_t regPort1) {
|
||||
return isPort1 ? regPort1 : regPort0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
if (device_get_type(parent) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent device is not I2C");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
// P0 defaults to open-drain out of reset, which can't drive an active-high output
|
||||
// without an external pull-up. Every known board wiring for this chip expects
|
||||
// push-pull, so switch it unconditionally on start.
|
||||
if (i2c_controller_register8_set_bits(parent, address, AW9523B_REG_GCR, AW9523B_GCR_P0_PUSH_PULL, portMAX_DELAY) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to set P0 to push-pull mode");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Started AW9523B device %s", device->name);
|
||||
return gpio_controller_init_descriptors(device, 16, nullptr);
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
check(gpio_controller_deinit_descriptors(device) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
static error_t set_level(GpioDescriptor* descriptor, bool high) {
|
||||
auto* device = descriptor->controller;
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
bool isPort1 = descriptor->pin >= 8;
|
||||
uint8_t bit = 1U << (descriptor->pin % 8U);
|
||||
uint8_t reg = portRegister(isPort1, AW9523B_REG_OUTPUT_P0, AW9523B_REG_OUTPUT_P1);
|
||||
|
||||
if (high) {
|
||||
return i2c_controller_register8_set_bits(parent, address, reg, bit, portMAX_DELAY);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, reg, bit, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
static error_t get_level(GpioDescriptor* descriptor, bool* high) {
|
||||
auto* device = descriptor->controller;
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
bool isPort1 = descriptor->pin >= 8;
|
||||
uint8_t bit = 1U << (descriptor->pin % 8U);
|
||||
uint8_t reg = portRegister(isPort1, AW9523B_REG_INPUT_P0, AW9523B_REG_INPUT_P1);
|
||||
|
||||
uint8_t bits;
|
||||
error_t err = i2c_controller_register8_get(parent, address, reg, &bits, portMAX_DELAY);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
*high = (bits & bit) != 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t set_flags(GpioDescriptor* descriptor, gpio_flags_t flags) {
|
||||
auto* device = descriptor->controller;
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
bool isPort1 = descriptor->pin >= 8;
|
||||
uint8_t bit = 1U << (descriptor->pin % 8U);
|
||||
uint8_t reg = portRegister(isPort1, AW9523B_REG_CONFIG_P0, AW9523B_REG_CONFIG_P1);
|
||||
|
||||
// Config register is inverted: clear the bit for output, set it for input.
|
||||
if (flags & GPIO_FLAG_DIRECTION_OUTPUT) {
|
||||
return i2c_controller_register8_reset_bits(parent, address, reg, bit, portMAX_DELAY);
|
||||
} else {
|
||||
return i2c_controller_register8_set_bits(parent, address, reg, bit, portMAX_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
static error_t get_flags(GpioDescriptor* descriptor, gpio_flags_t* flags) {
|
||||
auto* device = descriptor->controller;
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
bool isPort1 = descriptor->pin >= 8;
|
||||
uint8_t bit = 1U << (descriptor->pin % 8U);
|
||||
uint8_t reg = portRegister(isPort1, AW9523B_REG_CONFIG_P0, AW9523B_REG_CONFIG_P1);
|
||||
|
||||
uint8_t val;
|
||||
error_t err = i2c_controller_register8_get(parent, address, reg, &val, portMAX_DELAY);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
// Config register is inverted: 0 = output, 1 = input.
|
||||
*flags = (val & bit) ? GPIO_FLAG_DIRECTION_INPUT : GPIO_FLAG_DIRECTION_OUTPUT;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t get_native_pin_number(GpioDescriptor* descriptor, void* pin_number) {
|
||||
(void) descriptor;
|
||||
(void) pin_number;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t add_callback(GpioDescriptor* descriptor, void (*callback)(void*), void* arg) {
|
||||
(void) descriptor;
|
||||
(void) callback;
|
||||
(void) arg;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t remove_callback(GpioDescriptor* descriptor) {
|
||||
(void) descriptor;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t enable_interrupt(GpioDescriptor* descriptor) {
|
||||
(void) descriptor;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t disable_interrupt(GpioDescriptor* descriptor) {
|
||||
(void) descriptor;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
const static GpioControllerApi aw9523b_gpio_api = {
|
||||
.set_level = set_level,
|
||||
.get_level = get_level,
|
||||
.set_flags = set_flags,
|
||||
.get_flags = get_flags,
|
||||
.get_native_pin_number = get_native_pin_number,
|
||||
.add_callback = add_callback,
|
||||
.remove_callback = remove_callback,
|
||||
.enable_interrupt = enable_interrupt,
|
||||
.disable_interrupt = disable_interrupt
|
||||
};
|
||||
|
||||
Driver aw9523b_driver = {
|
||||
.name = "aw9523b",
|
||||
.compatible = (const char*[]) { "awinic,aw9523b", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = static_cast<const void*>(&aw9523b_gpio_api),
|
||||
.device_type = &GPIO_CONTROLLER_TYPE,
|
||||
.owner = &aw9523b_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver aw9523b_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&aw9523b_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&aw9523b_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module aw9523b_module = {
|
||||
.name = "aw9523b",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(dummy-i2s-amp-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev
|
||||
)
|
||||
|
||||
# audio_codec_sw_vol.h lives at the esp_codec_dev component root, which its own
|
||||
# COMPONENT_ADD_INCLUDEDIRS (include interface device/include) does not expose.
|
||||
if (DEFINED ENV{ESP_IDF_VERSION})
|
||||
idf_component_get_property(esp_codec_dev_dir espressif__esp_codec_dev COMPONENT_DIR)
|
||||
target_include_directories(${COMPONENT_LIB} PRIVATE "${esp_codec_dev_dir}")
|
||||
endif()
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Dummy I2S class-D amplifiers (MAX98357A, NS4168)
|
||||
|
||||
A driver covering any I2S class-D speaker amplifier with no I2C/register
|
||||
interface -- just an I2S data line plus an optional GPIO enable (SD) pin --
|
||||
wired as an output-only `AUDIO_CODEC_TYPE` device. Has no I2C parent; the device
|
||||
sits standalone in the devicetree and only references the I2S controller by
|
||||
name. Covers the Maxim MAX98357A and NSIway NS4168.
|
||||
|
||||
Since these amps have no hardware volume/mute registers, volume and mute are
|
||||
applied in software on the PCM stream via `esp_codec_dev`'s software volume
|
||||
handler.
|
||||
|
||||
See https://www.analog.com/media/en/technical-documentation/data-sheets/MAX98357A-MAX98357B.pdf
|
||||
And https://datasheet.lcsc.com/lcsc/2110191830_NSIWAY-NS4168_C965904.pdf
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,13 @@
|
||||
description: Maxim MAX98357A class-D speaker amplifier (I2S input, optional GPIO enable)
|
||||
|
||||
compatible: "maxim,max98357a"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
pin-enable:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: "Amplifier enable (SD) pin. Omit if the amplifier is always enabled."
|
||||
@@ -0,0 +1,13 @@
|
||||
description: Nsiway NS4168 class-D speaker amplifier (I2S input, optional GPIO enable)
|
||||
|
||||
compatible: "nsiway,ns4168"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
pin-enable:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: "Amplifier enable (SD) pin. Omit if the amplifier is always enabled."
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/dummy_i2s_amp.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// One DEFINE_DEVICETREE per compatible string -- the devicetree compiler derives the
|
||||
// expected config typedef name from the compatible string's suffix (e.g. "maxim,max98357a"
|
||||
// -> max98357a_config_dt), not from a name we choose, so each supported chip needs its own
|
||||
// typedef even though they all share the same underlying config layout.
|
||||
DEFINE_DEVICETREE(max98357a, struct DummyI2sAmpConfig)
|
||||
DEFINE_DEVICETREE(ns4168, struct DummyI2sAmpConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Dummy class-D amplifier configuration.
|
||||
*
|
||||
* Covers any speaker amp with no I2C/register interface -- just I2S data in plus an
|
||||
* optional GPIO enable (SD) pin -- e.g. MAX98357A, NS4168. There is no I2C parent; the
|
||||
* device sits standalone in the devicetree and only references the I2S controller by name.
|
||||
*/
|
||||
struct DummyI2sAmpConfig {
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
/** Optional amplifier enable pin (SD pin). GPIO_PIN_SPEC_NONE if not wired. */
|
||||
struct GpioPinSpec enable_pin;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module dummy_i2s_amp_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,385 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/dummy_i2s_amp.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <dummy_codec.h>
|
||||
#include <esp_codec_dev.h>
|
||||
#include <audio_codec_sw_vol.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#define TAG "DummyI2sAmp"
|
||||
|
||||
namespace {
|
||||
|
||||
// A dumb I2S class-D amp has no native rate of its own -- it just clocks whatever I2S
|
||||
// frames arrive. Report a common default; audio_stream resamples to whatever rate it is
|
||||
// opened with.
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 44100;
|
||||
|
||||
struct DummyI2sAmpData {
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_gpio_if_t* gpio_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
const audio_codec_vol_if_t* vol_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
bool is_open = false;
|
||||
esp_codec_dev_sample_info_t open_sample_info = {};
|
||||
|
||||
// esp_codec_dev_set_out_mute() short-circuits on dummy_codec's mute callback (which
|
||||
// only toggles the PA enable GPIO, a no-op when there's no enable pin) and never
|
||||
// reaches its own software-volume mute fallback as a result. Track mute state
|
||||
// ourselves and implement it by zeroing/restoring volume through the working
|
||||
// esp_codec_dev_set_out_vol() path (confirmed working, unlike mute) instead.
|
||||
bool muted = false;
|
||||
int volume_percent = 100;
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const DummyI2sAmpConfig*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<DummyI2sAmpData*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (config->direction != AUDIO_CODEC_DIR_OUTPUT && config->direction != AUDIO_CODEC_DIR_BOTH) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = config->channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
if (data->is_open) {
|
||||
bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||
&& data->open_sample_info.channel == sample_info.channel
|
||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->is_open = true;
|
||||
data->open_sample_info = sample_info;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) device;
|
||||
(void) buffer;
|
||||
(void) size;
|
||||
(void) bytes_read;
|
||||
(void) timeout;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length write.
|
||||
int result = esp_codec_dev_write(data->codec_device, const_cast<void*>(buffer), (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_written = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (volume_percent < 0.0f || volume_percent > 100.0f) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->volume_percent = (int) std::lround(volume_percent);
|
||||
|
||||
// Don't push the new volume to hardware while muted -- that would audibly unmute.
|
||||
// It takes effect once set_mute(false) restores it.
|
||||
if (data->muted) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
return (esp_codec_dev_set_out_vol(data->codec_device, data->volume_percent) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
*volume_percent = (float) data->volume_percent;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (data->muted == muted) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_set_out_mute() is unusable here -- see DummyI2sAmpData::muted comment.
|
||||
// Implement mute as a volume override through the working esp_codec_dev_set_out_vol()
|
||||
// path instead: zero the hardware volume while muted, restore the tracked percentage
|
||||
// on unmute.
|
||||
int target = muted ? 0 : data->volume_percent;
|
||||
if (esp_codec_dev_set_out_vol(data->codec_device, target) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->muted = muted;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
*muted = data->muted;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = 2;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_OUTPUT;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new DummyI2sAmpData();
|
||||
|
||||
data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
|
||||
if (data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create I2S data adapter");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
int16_t pa_pin = -1;
|
||||
if (config->enable_pin.gpio_controller != nullptr) {
|
||||
data->gpio_if = audio_codec_adapter_new_gpio(&config->enable_pin, 1);
|
||||
if (data->gpio_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create GPIO adapter for enable pin");
|
||||
goto cleanup;
|
||||
}
|
||||
pa_pin = 0;
|
||||
}
|
||||
|
||||
dummy_codec_cfg_t codec_config = {
|
||||
.gpio_if = data->gpio_if,
|
||||
.pa_pin = pa_pin,
|
||||
.pa_reverted = false,
|
||||
};
|
||||
|
||||
data->codec_if = dummy_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create dummy codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_OUT,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
// Dumb I2S amps have no hardware volume/mute registers -- apply volume/mute in
|
||||
// software on the PCM stream instead.
|
||||
data->vol_if = audio_codec_new_sw_vol();
|
||||
if (data->vol_if == nullptr || esp_codec_dev_set_vol_handler(data->codec_device, data->vol_if) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to install software volume handler");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->vol_if != nullptr) {
|
||||
audio_codec_delete_vol_if(data->vol_if);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->vol_if != nullptr) {
|
||||
audio_codec_delete_vol_if(data->vol_if);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver dummy_i2s_amp_driver = {
|
||||
.name = "dummy_i2s_amp",
|
||||
.compatible = (const char*[]) { "maxim,max98357a", "nsiway,ns4168", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver dummy_i2s_amp_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&dummy_i2s_amp_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&dummy_i2s_amp_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol dummy_i2s_amp_module_symbols[];
|
||||
|
||||
Module dummy_i2s_amp_module = {
|
||||
.name = "dummy_i2s_amp",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = dummy_i2s_amp_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol dummy_i2s_amp_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(es7210-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# ES7210 microphone ADC
|
||||
|
||||
A driver for the `ES7210` 4-channel microphone ADC by Everest Semiconductor,
|
||||
wired as an input-only `AUDIO_CODEC_TYPE` device. The I2C bus is the device's
|
||||
parent; the I2S controller carrying the (4-slot TDM) audio data is referenced by
|
||||
name.
|
||||
|
||||
Always configures the hardware for the full 4-slot TDM frame regardless of how
|
||||
many mics are actually wired (`mic-mask`), then demuxes down to just the active
|
||||
mic slots in software -- `es7210_set_fs()` silently halves the configured bit
|
||||
depth when asked for 2 or fewer TDM channels, which corrupts audio if the mic
|
||||
count is passed straight through.
|
||||
|
||||
Wraps Espressif's `esp_codec_dev` ES7210 implementation.
|
||||
|
||||
See https://www.everest-semi.com/pdf/ES7210%20PB.pdf
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,20 @@
|
||||
description: Everest Semiconductor ES7210 4-channel microphone ADC
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "everest,es7210"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
mic-mask:
|
||||
type: int
|
||||
required: false
|
||||
default: 15
|
||||
description: "Bitmask of microphones to enable (bit0=MIC1 .. bit3=MIC4); defaults to all four"
|
||||
input-gain-percent:
|
||||
type: int
|
||||
default: 100
|
||||
description: "Extra digital gain multiplier applied by audio_stream on top of the ES7210's own 30dB hardware ADC gain, as an integer percentage (100 = 1.0x / no extra boost). devicetree has no float type, hence x100."
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/es7210.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(es7210, struct Es7210Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief ES7210 codec device configuration.
|
||||
*
|
||||
* The ES7210 is an input-only (microphone ADC) codec. The I2C bus is the
|
||||
* device's parent (per i2c-device.yaml), while the I2S controller carrying
|
||||
* the (typically 4-slot TDM) audio data is referenced via a devicetree phandle.
|
||||
*/
|
||||
struct Es7210Config {
|
||||
/** I2C address on the bus (typically 0x40) */
|
||||
uint8_t address;
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
/** Bitmask of microphones to enable, e.g. ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ... */
|
||||
uint8_t mic_selected_mask;
|
||||
/**
|
||||
* Extra fixed digital gain multiplier applied by audio_stream on top of the ES7210's
|
||||
* own hardware ADC gain (already at a hard-coded 30dB default out of a 0..37.5dB
|
||||
* range), as an integer percentage (100 = 1.0x / no extra boost). Small MEMS mic
|
||||
* capsules can still sound quiet even near max hardware gain; this is for boards where
|
||||
* 30dB hardware gain alone isn't enough. devicetree has no float property type, hence
|
||||
* the x100 integer encoding.
|
||||
*/
|
||||
uint16_t input_gain_percent;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module es7210_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,435 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/es7210.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <es7210_adc.h>
|
||||
#include <esp_codec_dev.h>
|
||||
#include <esp_codec_dev_defaults.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#define TAG "ES7210"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 16000;
|
||||
constexpr float MAX_INPUT_GAIN_DB = 37.5f; // ES7210 mic gain range is roughly 0..37.5 dB
|
||||
|
||||
struct Es7210Data {
|
||||
const audio_codec_ctrl_if_t* ctrl_if = nullptr;
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
bool is_open = false;
|
||||
uint8_t mic_count = 4;
|
||||
uint8_t mic_mask = 0x0F;
|
||||
uint8_t tdm_slot_count = 4;
|
||||
uint8_t open_bits_per_sample = 16;
|
||||
uint32_t open_sample_rate = 0;
|
||||
float input_gain = 1.0f;
|
||||
std::vector<uint8_t> raw_buffer;
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Es7210Config*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<Es7210Data*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (config->direction == AUDIO_CODEC_DIR_OUTPUT || config->direction == AUDIO_CODEC_DIR_BOTH) {
|
||||
LOG_E(TAG, "ES7210 is input-only");
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
bool same_config = config->bits_per_sample == data->open_bits_per_sample
|
||||
&& config->sample_rate == data->open_sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Open with tdm_slot_count channels, not the (possibly smaller) active mic count.
|
||||
// tdm_slot_count is set in start_device() to match whichever framing the vendor
|
||||
// driver will actually use: the full 4-slot TDM frame when TDM mode engages
|
||||
// (mic_count >= 3), or exactly mic_count when it doesn't (plain N-channel I2S --
|
||||
// see start_device()'s tdm_slot_count comment). Opening with the wrong channel
|
||||
// count here mismatches the I2S receive layout the codec actually produces,
|
||||
// corrupting every channel past the first. When TDM *is* engaged, using the full
|
||||
// slot count (rather than just the active mic count) also sidesteps a separate
|
||||
// vendor quirk: es7210_set_fs() silently halves the configured bit depth when asked
|
||||
// for <= 2 channels in TDM mode (channel_mask == 0). We demux down to the active
|
||||
// mic slots ourselves in read().
|
||||
uint8_t hw_channels = data->tdm_slot_count;
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = hw_channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->open_bits_per_sample = config->bits_per_sample;
|
||||
data->open_sample_rate = config->sample_rate;
|
||||
data->is_open = true;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// The hardware always runs at tdm_slot_count channels (see open() for why); demux down
|
||||
// to just the active mic slots here so callers see exactly mic_count channels of real
|
||||
// signal -- never diluted by silent/AEC-reference TDM slots.
|
||||
if (data->mic_count == data->tdm_slot_count) {
|
||||
int result = esp_codec_dev_read(data->codec_device, buffer, (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*bytes_read = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
uint8_t bytes_per_sample = (uint8_t) (data->open_bits_per_sample / 8);
|
||||
size_t out_frame_bytes = (size_t) data->mic_count * bytes_per_sample;
|
||||
size_t requested_frames = (out_frame_bytes != 0) ? (size / out_frame_bytes) : 0;
|
||||
if (requested_frames == 0) {
|
||||
*bytes_read = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
size_t raw_frame_bytes = (size_t) data->tdm_slot_count * bytes_per_sample;
|
||||
size_t raw_bytes_needed = requested_frames * raw_frame_bytes;
|
||||
if (data->raw_buffer.size() < raw_bytes_needed) {
|
||||
data->raw_buffer.resize(raw_bytes_needed);
|
||||
}
|
||||
|
||||
int result = esp_codec_dev_read(data->codec_device, data->raw_buffer.data(), (int) raw_bytes_needed);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
size_t raw_bytes_read = (size_t) result;
|
||||
size_t frames_read = raw_bytes_read / raw_frame_bytes;
|
||||
|
||||
// Demux: pick the bytes_per_sample-wide slot for each set bit in mic_mask, in ascending
|
||||
// slot order, e.g. mic_mask = MIC1|MIC3 -> slots 0 and 2.
|
||||
uint8_t slot_indices[8];
|
||||
uint8_t slot_count = 0;
|
||||
for (uint8_t slot = 0; slot < data->tdm_slot_count && slot_count < sizeof(slot_indices); slot++) {
|
||||
if ((data->mic_mask & (uint8_t) (1u << slot)) != 0) {
|
||||
slot_indices[slot_count++] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
auto* out = static_cast<uint8_t*>(buffer);
|
||||
const uint8_t* raw = data->raw_buffer.data();
|
||||
for (size_t frame = 0; frame < frames_read; frame++) {
|
||||
const uint8_t* raw_frame = raw + frame * raw_frame_bytes;
|
||||
uint8_t* out_frame = out + frame * out_frame_bytes;
|
||||
for (uint8_t ch = 0; ch < slot_count && ch < data->mic_count; ch++) {
|
||||
std::memcpy(out_frame + (size_t) ch * bytes_per_sample, raw_frame + (size_t) slot_indices[ch] * bytes_per_sample, bytes_per_sample);
|
||||
}
|
||||
}
|
||||
|
||||
*bytes_read = frames_read * out_frame_bytes;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) device;
|
||||
(void) buffer;
|
||||
(void) size;
|
||||
(void) bytes_written;
|
||||
(void) timeout;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// Map 0..100% linearly onto the gain range.
|
||||
float db = (volume_percent / 100.0f) * MAX_INPUT_GAIN_DB;
|
||||
return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
float db = 0.0f;
|
||||
if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (db / MAX_INPUT_GAIN_DB) * 100.0f;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr || direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = data->mic_count;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_INPUT;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_input_gain_multiplier(Device* device, float* gain) {
|
||||
auto* data = GET_DATA(device);
|
||||
*gain = data->input_gain;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = get_input_gain_multiplier,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
// devicetree's int property type has no min/max constraint support, so a bogus
|
||||
// negative literal would otherwise silently wrap to 65535 when narrowed to uint16_t.
|
||||
// Bound it here instead -- 2000 (20x) is already an extreme upper bound for
|
||||
// compensating a quiet mic capsule on top of the ES7210's own hardware gain.
|
||||
if (config->input_gain_percent > 2000) {
|
||||
LOG_E(TAG, "Invalid input_gain_percent %u (must be 0..2000)", config->input_gain_percent);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Same rationale as input_gain_percent: devicetree ints aren't bit-constrained, so
|
||||
// reject any bit outside the four supported mic slots before it inflates mic_count
|
||||
// beyond the fixed 4-slot TDM frame that read() demuxes against.
|
||||
constexpr uint8_t supported_mic_mask = (uint8_t) (ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4);
|
||||
if ((config->mic_selected_mask & ~supported_mic_mask) != 0) {
|
||||
LOG_E(TAG, "Invalid mic_selected_mask 0x%02X (must be subset of 0x%02X)", config->mic_selected_mask, supported_mic_mask);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent is not an I2C controller");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new Es7210Data();
|
||||
|
||||
uint8_t mic_mask = (config->mic_selected_mask != 0)
|
||||
? config->mic_selected_mask
|
||||
: (uint8_t) (ES7210_SEL_MIC1 | ES7210_SEL_MIC2 | ES7210_SEL_MIC3 | ES7210_SEL_MIC4);
|
||||
data->mic_mask = mic_mask;
|
||||
data->mic_count = (uint8_t) __builtin_popcount(mic_mask);
|
||||
// Mirrors the vendor driver's own TDM threshold (es7210_is_tdm_mode(): TDM only
|
||||
// engages when mic_num >= 3) -- below that it runs a plain N-channel I2S frame, not
|
||||
// a fixed 4-slot TDM frame. Forcing a 4-channel open (see open()'s hw_channels
|
||||
// comment) against a non-TDM 2-mic config mismatches the I2S receive layout the
|
||||
// codec is actually producing, corrupting every channel past the first.
|
||||
data->tdm_slot_count = (data->mic_count >= 3) ? 4 : data->mic_count;
|
||||
data->input_gain = (float) config->input_gain_percent / 100.0f;
|
||||
|
||||
data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address);
|
||||
data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
|
||||
if (data->ctrl_if == nullptr || data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create adapters");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open control interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
es7210_codec_cfg_t codec_config = {};
|
||||
codec_config.ctrl_if = data->ctrl_if;
|
||||
codec_config.master_mode = false;
|
||||
codec_config.mic_selected = mic_mask;
|
||||
codec_config.mclk_src = ES7210_MCLK_FROM_PAD;
|
||||
codec_config.mclk_div = 0;
|
||||
|
||||
data->codec_if = es7210_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create ES7210 codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver es7210_driver = {
|
||||
.name = "es7210",
|
||||
.compatible = (const char*[]) { "everest,es7210", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver es7210_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&es7210_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&es7210_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol es7210_module_symbols[];
|
||||
|
||||
Module es7210_module = {
|
||||
.name = "es7210",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = es7210_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol es7210_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(es8311-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# ES8311 audio codec
|
||||
|
||||
A driver for the `ES8311` audio codec by Everest Semiconductor, wired as a dual
|
||||
in/out `AUDIO_CODEC_TYPE` device (speaker output + microphone input on the same
|
||||
chip). The I2C bus is the device's parent; the I2S controller carrying audio data
|
||||
is referenced via a devicetree phandle.
|
||||
|
||||
Wraps Espressif's `esp_codec_dev` ES8311 implementation.
|
||||
|
||||
See https://www.everest-semi.com/pdf/ES8311%20PB.pdf
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
description: Everest Semiconductor ES8311 audio codec (speaker output + mic input, dual mode)
|
||||
|
||||
include: [ "i2c-device.yaml" ]
|
||||
|
||||
compatible: "everest,es8311"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/es8311.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(es8311, struct Es8311Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief ES8311 codec device configuration.
|
||||
*
|
||||
* The ES8311 is wired as both control (I2C) and audio (I2S) device: the I2C
|
||||
* bus is the device's parent (per i2c-device.yaml), while the I2S controller
|
||||
* is referenced via a devicetree phandle. It operates in dual mode
|
||||
* (speaker output + mic input) on boards such as M5Stack StickS3.
|
||||
*/
|
||||
struct Es8311Config {
|
||||
/** I2C address on the bus (typically 0x18) */
|
||||
uint8_t address;
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,400 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/es8311.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <es8311_codec.h>
|
||||
#include <esp_codec_dev.h>
|
||||
#include <esp_codec_dev_defaults.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#define TAG "ES8311"
|
||||
|
||||
namespace {
|
||||
|
||||
// Volume curve maps 1..100 to roughly -49.5dB..0dB; native ES8311 range is wide enough
|
||||
// that we expose a flat 0..100 percent scale to callers and let esp_codec_dev convert.
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 44100;
|
||||
|
||||
struct Es8311Data {
|
||||
const audio_codec_ctrl_if_t* ctrl_if = nullptr;
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_gpio_if_t* gpio_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
bool is_open = false;
|
||||
AudioCodecDirection open_direction = AUDIO_CODEC_DIR_BOTH;
|
||||
esp_codec_dev_sample_info_t open_sample_info = {};
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Es8311Config*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<Es8311Data*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = config->channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
if (data->is_open) {
|
||||
// open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the
|
||||
// same sample settings -- only an exact direction mismatch (e.g. requesting BOTH
|
||||
// while opened for INPUT only) needs a reopen.
|
||||
bool direction_compatible = data->open_direction == config->direction
|
||||
|| data->open_direction == AUDIO_CODEC_DIR_BOTH;
|
||||
bool same_config = direction_compatible
|
||||
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||
&& data->open_sample_info.channel == sample_info.channel
|
||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->is_open = true;
|
||||
data->open_direction = config->direction;
|
||||
data->open_sample_info = sample_info;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length read.
|
||||
int result = esp_codec_dev_read(data->codec_device, buffer, (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_read = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length write.
|
||||
int result = esp_codec_dev_write(data->codec_device, const_cast<void*>(buffer), (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_written = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (volume_percent < 0.0f || volume_percent > 100.0f) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
int volume = (int) std::lround(volume_percent);
|
||||
return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
// ES8311 ADC gain range is roughly 0..24 dB; map 0..100% linearly onto it.
|
||||
float db = (volume_percent / 100.0f) * 24.0f;
|
||||
return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
int volume = 0;
|
||||
if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (float) volume;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
float db = 0.0f;
|
||||
if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (db / 24.0f) * 100.0f;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT && direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = (direction == AUDIO_CODEC_DIR_INPUT) ? 1 : 2;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
(void) direction;
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_BOTH;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = nullptr,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent is not an I2C controller");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new Es8311Data();
|
||||
|
||||
data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address);
|
||||
data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
|
||||
if (data->ctrl_if == nullptr || data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create adapters");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open control interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
es8311_codec_cfg_t codec_config = {};
|
||||
codec_config.ctrl_if = data->ctrl_if;
|
||||
codec_config.gpio_if = nullptr;
|
||||
codec_config.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
|
||||
codec_config.pa_pin = -1;
|
||||
codec_config.pa_reverted = false;
|
||||
codec_config.master_mode = false;
|
||||
codec_config.use_mclk = true;
|
||||
codec_config.digital_mic = false;
|
||||
codec_config.invert_mclk = false;
|
||||
codec_config.invert_sclk = false;
|
||||
codec_config.no_dac_ref = false;
|
||||
codec_config.mclk_div = 0;
|
||||
|
||||
data->codec_if = es8311_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create ES8311 codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver es8311_driver = {
|
||||
.name = "es8311",
|
||||
.compatible = (const char*[]) { "everest,es8311", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver es8311_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&es8311_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&es8311_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol es8311_module_symbols[];
|
||||
|
||||
Module es8311_module = {
|
||||
.name = "es8311",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = es8311_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol es8311_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(es8388-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# ES8388 audio codec
|
||||
|
||||
A driver for the `ES8388` audio codec by Everest Semiconductor, wired as a dual
|
||||
in/out `AUDIO_CODEC_TYPE` device (speaker output + line/mic input). The I2C bus is
|
||||
the device's parent; the I2S controller carrying audio data is referenced via a devicetree phandle.
|
||||
|
||||
Wraps Espressif's `esp_codec_dev` ES8388 implementation.
|
||||
|
||||
See https://www.everest-semi.com/pdf/ES8388%20DS.pdf
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,11 @@
|
||||
description: Everest Semiconductor ES8388 audio codec (speaker output + line/mic input)
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "everest,es8388"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/es8388.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(es8388, struct Es8388Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief ES8388 codec device configuration.
|
||||
*
|
||||
* The ES8388 is wired as both control (I2C) and audio (I2S) device: the I2C
|
||||
* bus is the device's parent (per i2c-device.yaml), while the I2S controller
|
||||
* is referenced via a devicetree phandle.
|
||||
*/
|
||||
struct Es8388Config {
|
||||
/** I2C address on the bus (typically 0x10) */
|
||||
uint8_t address;
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module es8388_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,394 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/es8388.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <es8388_codec.h>
|
||||
#include <esp_codec_dev.h>
|
||||
#include <esp_codec_dev_defaults.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#define TAG "ES8388"
|
||||
|
||||
namespace {
|
||||
|
||||
// Volume curve maps 1..100 to roughly -49.5dB..0dB; native ES8388 range is wide enough
|
||||
// that we expose a flat 0..100 percent scale to callers and let esp_codec_dev convert.
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 44100;
|
||||
|
||||
struct Es8388Data {
|
||||
const audio_codec_ctrl_if_t* ctrl_if = nullptr;
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_gpio_if_t* gpio_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
bool is_open = false;
|
||||
AudioCodecDirection open_direction = AUDIO_CODEC_DIR_BOTH;
|
||||
esp_codec_dev_sample_info_t open_sample_info = {};
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Es8388Config*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<Es8388Data*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = config->channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
if (data->is_open) {
|
||||
// open_direction == BOTH already serves INPUT-only or OUTPUT-only requests on the
|
||||
// same sample settings -- only an exact direction mismatch (e.g. requesting BOTH
|
||||
// while opened for INPUT only) needs a reopen.
|
||||
bool direction_compatible = data->open_direction == config->direction
|
||||
|| data->open_direction == AUDIO_CODEC_DIR_BOTH;
|
||||
bool same_config = direction_compatible
|
||||
&& data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||
&& data->open_sample_info.channel == sample_info.channel
|
||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->is_open = true;
|
||||
data->open_direction = config->direction;
|
||||
data->open_sample_info = sample_info;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length read.
|
||||
int result = esp_codec_dev_read(data->codec_device, buffer, (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_read = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_write returns the number of bytes written (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length write.
|
||||
int result = esp_codec_dev_write(data->codec_device, const_cast<void*>(buffer), (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_written = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (volume_percent < 0.0f || volume_percent > 100.0f) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
int volume = (int) std::lround(volume_percent);
|
||||
return (esp_codec_dev_set_out_vol(data->codec_device, volume) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
// ES8388 ADC gain range is roughly 0..24 dB; map 0..100% linearly onto it.
|
||||
float db = (volume_percent / 100.0f) * 24.0f;
|
||||
return (esp_codec_dev_set_in_gain(data->codec_device, db) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
int volume = 0;
|
||||
if (esp_codec_dev_get_out_vol(data->codec_device, &volume) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (float) volume;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
float db = 0.0f;
|
||||
if (esp_codec_dev_get_in_gain(data->codec_device, &db) != ESP_CODEC_DEV_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
*volume_percent = (db / 24.0f) * 100.0f;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return (esp_codec_dev_set_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
return (esp_codec_dev_set_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return (esp_codec_dev_get_out_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (direction == AUDIO_CODEC_DIR_INPUT) {
|
||||
return (esp_codec_dev_get_in_mute(data->codec_device, muted) == ESP_CODEC_DEV_OK) ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT && direction != AUDIO_CODEC_DIR_OUTPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = 2;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
(void) direction;
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_BOTH;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = nullptr,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* i2c_controller = device_get_parent(device);
|
||||
if (i2c_controller == nullptr || device_get_type(i2c_controller) != &I2C_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "Parent is not an I2C controller");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new Es8388Data();
|
||||
|
||||
data->ctrl_if = audio_codec_adapter_new_i2c_ctrl(i2c_controller, config->address);
|
||||
data->data_if = audio_codec_adapter_new_i2s_data(i2s_controller);
|
||||
if (data->ctrl_if == nullptr || data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create adapters");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->ctrl_if->open(data->ctrl_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open control interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
es8388_codec_cfg_t codec_config = {};
|
||||
codec_config.ctrl_if = data->ctrl_if;
|
||||
codec_config.gpio_if = nullptr;
|
||||
codec_config.codec_mode = ESP_CODEC_DEV_WORK_MODE_BOTH;
|
||||
codec_config.master_mode = false;
|
||||
codec_config.pa_pin = -1;
|
||||
codec_config.pa_reverted = false;
|
||||
|
||||
data->codec_if = es8388_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create ES8388 codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN_OUT,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->gpio_if != nullptr) {
|
||||
audio_codec_adapter_delete_gpio(data->gpio_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
if (data->ctrl_if != nullptr) {
|
||||
audio_codec_delete_ctrl_if(data->ctrl_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver es8388_driver = {
|
||||
.name = "es8388",
|
||||
.compatible = (const char*[]) { "everest,es8388", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver es8388_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&es8388_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&es8388_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol es8388_module_symbols[];
|
||||
|
||||
Module es8388_module = {
|
||||
.name = "es8388",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = es8388_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol es8388_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(pdm-mic-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel audio-codec-module esp_codec_dev
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# SPM1423 PDM microphone
|
||||
|
||||
A driver for the `SPM1423` PDM MEMS microphone (and compatible PDM mics), wired
|
||||
as an input-only `AUDIO_CODEC_TYPE` device. Has no I2C/register interface -- just
|
||||
a PDM data line -- so there is no I2C parent; the device sits standalone in the
|
||||
devicetree and only references the I2S controller by name. PDM RX is hardware-
|
||||
restricted to I2S controller 0.
|
||||
|
||||
See https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/core/SPM1423HM4H-B_datasheet_en.pdf
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,17 @@
|
||||
description: Generic PDM MEMS microphone (e.g. SPM1423), I2S PDM RX input
|
||||
|
||||
compatible: "generic,spm1423"
|
||||
|
||||
properties:
|
||||
i2s:
|
||||
type: phandle
|
||||
required: true
|
||||
description: "I2S controller device that carries audio data"
|
||||
channels:
|
||||
type: int
|
||||
default: 1
|
||||
description: "Number of PDM mics wired (1 = mono, 2 = stereo)"
|
||||
input-gain-percent:
|
||||
type: int
|
||||
default: 400
|
||||
description: "Digital gain multiplier applied by audio_stream, as an integer percentage (100 = 1.0x, 400 = 4.0x). PDM mics have no hardware gain; default 400 matches M5Stack's own SPM1423 reference firmware (magnification=4)."
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/pdm_mic.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// One DEFINE_DEVICETREE per compatible string -- see dummy-i2s-amp-module's bindings
|
||||
// header for why this can't be unified into a single typedef name.
|
||||
DEFINE_DEVICETREE(spm1423, struct PdmMicConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief PDM MEMS microphone configuration.
|
||||
*
|
||||
* Covers any PDM mic with no I2C/register interface -- just I2S PDM RX -- e.g. SPM1423.
|
||||
* There is no I2C parent; the device sits standalone in the devicetree and only
|
||||
* references the I2S controller by name. PDM RX is hardware-restricted to I2S
|
||||
* controller 0 on ESP32 targets, so i2s_device must resolve to that controller.
|
||||
*/
|
||||
struct PdmMicConfig {
|
||||
/** I2S controller device that carries audio data */
|
||||
struct Device* i2s_device;
|
||||
/** Number of PDM mics wired (1 = mono, 2 = stereo) */
|
||||
uint8_t channels;
|
||||
/**
|
||||
* Fixed digital gain multiplier applied to this mic's samples by audio_stream, as an
|
||||
* integer percentage (100 = 1.0x / no boost, 400 = 4.0x). PDM mics have no hardware
|
||||
* gain control, so this is the only way to compensate for a capsule that's quiet at
|
||||
* the ADC's full-scale output. devicetree has no float property type, hence the
|
||||
* percent-scaled integer encoding. M5Stack's own SPM1423 reference firmware uses a
|
||||
* software multiplier of 4 (i.e. 400 here).
|
||||
*/
|
||||
uint16_t input_gain_percent;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module pdm_mic_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver pdm_mic_driver;
|
||||
|
||||
static error_t start() {
|
||||
check(driver_construct_add(&pdm_mic_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
check(driver_remove_destruct(&pdm_mic_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
extern const ModuleSymbol pdm_mic_module_symbols[];
|
||||
|
||||
Module pdm_mic_module = {
|
||||
.name = "pdm_mic",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = pdm_mic_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/pdm_mic.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/audio_codec.h>
|
||||
#include <tactility/drivers/audio_codec_adapters.h>
|
||||
#include <tactility/drivers/i2s_controller.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <dummy_codec.h>
|
||||
#include <esp_codec_dev.h>
|
||||
|
||||
#define TAG "PdmMic"
|
||||
|
||||
namespace {
|
||||
|
||||
// PDM mics have no fixed native rate of their own -- the PDM RX peripheral generates
|
||||
// whatever clock the configured sample rate implies. Report a common voice-capture
|
||||
// default; audio_stream resamples to whatever rate it is opened with.
|
||||
constexpr uint32_t NATIVE_SAMPLE_RATE = 16000;
|
||||
|
||||
struct PdmMicData {
|
||||
const audio_codec_data_if_t* data_if = nullptr;
|
||||
const audio_codec_if_t* codec_if = nullptr;
|
||||
esp_codec_dev_handle_t codec_device = nullptr;
|
||||
uint8_t native_channels = 1;
|
||||
float input_gain = 1.0f;
|
||||
bool is_open = false;
|
||||
esp_codec_dev_sample_info_t open_sample_info = {};
|
||||
};
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const PdmMicConfig*>((device)->config))
|
||||
#define GET_DATA(device) (static_cast<PdmMicData*>(device_get_driver_data(device)))
|
||||
|
||||
// region AudioCodecApi
|
||||
|
||||
error_t open(Device* device, const struct AudioCodecStreamConfig* config) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Input-only codec (see get_capabilities()/write()) -- BOTH would imply output support
|
||||
// that write() can't actually provide.
|
||||
if (config->direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
esp_codec_dev_sample_info_t sample_info = {
|
||||
.bits_per_sample = config->bits_per_sample,
|
||||
.channel = config->channels,
|
||||
.channel_mask = 0,
|
||||
.sample_rate = config->sample_rate,
|
||||
.mclk_multiple = 0,
|
||||
};
|
||||
|
||||
if (data->is_open) {
|
||||
bool same_config = data->open_sample_info.bits_per_sample == sample_info.bits_per_sample
|
||||
&& data->open_sample_info.channel == sample_info.channel
|
||||
&& data->open_sample_info.sample_rate == sample_info.sample_rate;
|
||||
return same_config ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (esp_codec_dev_open(data->codec_device, &sample_info) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open codec device");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
data->is_open = true;
|
||||
data->open_sample_info = sample_info;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t close(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data->codec_device == nullptr) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
data->is_open = false;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t read(Device* device, void* buffer, size_t size, size_t* bytes_read, TickType_t timeout) {
|
||||
(void) timeout;
|
||||
auto* data = GET_DATA(device);
|
||||
if (!data->is_open) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// esp_codec_dev_read returns the number of bytes read (>= 0) on success, or a negative
|
||||
// ESP_CODEC_DEV_* error code on failure -- it does NOT return ESP_CODEC_DEV_OK (0) for
|
||||
// a successful nonzero-length read.
|
||||
int result = esp_codec_dev_read(data->codec_device, buffer, (int) size);
|
||||
if (result < 0) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
*bytes_read = (size_t) result;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t write(Device* device, const void* buffer, size_t size, size_t* bytes_written, TickType_t timeout) {
|
||||
(void) device;
|
||||
(void) buffer;
|
||||
(void) size;
|
||||
(void) bytes_written;
|
||||
(void) timeout;
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// PDM mics have no hardware gain/mute registers, and unlike esp_codec_dev's output path
|
||||
// (esp_codec_dev_set_out_vol(), which falls back to a software volume handler when the
|
||||
// codec_if has no set_vol), the input path (esp_codec_dev_set_in_gain()/set_in_mute()) has
|
||||
// no such fallback -- it returns ESP_CODEC_DEV_NOT_SUPPORT outright when the codec_if's
|
||||
// set_mic_gain/mute_mic are NULL, which dummy_codec always leaves NULL. So input
|
||||
// volume/mute genuinely isn't supported through this codec for now; report that honestly
|
||||
// rather than masking a dead call path.
|
||||
|
||||
error_t set_volume(Device* device, AudioCodecDirection direction, float volume_percent) {
|
||||
(void) device;
|
||||
(void) volume_percent;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_volume(Device* device, AudioCodecDirection direction, float* volume_percent) {
|
||||
(void) device;
|
||||
(void) volume_percent;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t set_mute(Device* device, AudioCodecDirection direction, bool muted) {
|
||||
(void) device;
|
||||
(void) muted;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_mute(Device* device, AudioCodecDirection direction, bool* muted) {
|
||||
(void) device;
|
||||
(void) muted;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
error_t get_native_channels(Device* device, AudioCodecDirection direction, uint8_t* channels) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*channels = data->native_channels;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_native_sample_rate(Device* device, AudioCodecDirection direction, uint32_t* rate_hz) {
|
||||
(void) device;
|
||||
if (direction != AUDIO_CODEC_DIR_INPUT) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*rate_hz = NATIVE_SAMPLE_RATE;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_capabilities(Device* device, AudioCodecDirection* supported_directions) {
|
||||
(void) device;
|
||||
*supported_directions = AUDIO_CODEC_DIR_INPUT;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t get_input_gain_multiplier(Device* device, float* gain) {
|
||||
auto* data = GET_DATA(device);
|
||||
*gain = data->input_gain;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static const struct AudioCodecApi API = {
|
||||
.open = open,
|
||||
.close = close,
|
||||
.read = read,
|
||||
.write = write,
|
||||
.set_volume = set_volume,
|
||||
.get_volume = get_volume,
|
||||
.set_mute = set_mute,
|
||||
.get_mute = get_mute,
|
||||
.get_native_sample_rate = get_native_sample_rate,
|
||||
.get_native_channels = get_native_channels,
|
||||
.get_capabilities = get_capabilities,
|
||||
.get_input_gain_multiplier = get_input_gain_multiplier,
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
error_t start_device(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
if (config->channels == 0 || config->channels > 2) {
|
||||
LOG_E(TAG, "Invalid channel count %u (must be 1 or 2)", config->channels);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// devicetree's int property type has no min/max constraint support, so a bogus
|
||||
// negative literal (e.g. "input-gain-percent = <-1>;") would otherwise silently wrap to
|
||||
// 65535 when narrowed to uint16_t and produce a wildly wrong gain. Bound it here
|
||||
// instead -- 0 disables boost entirely, 2000 (20x) is already an extreme upper bound
|
||||
// for compensating a quiet mic capsule.
|
||||
if (config->input_gain_percent > 2000) {
|
||||
LOG_E(TAG, "Invalid input_gain_percent %u (must be 0..2000)", config->input_gain_percent);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* i2s_controller = config->i2s_device;
|
||||
if (i2s_controller == nullptr || device_get_type(i2s_controller) != &I2S_CONTROLLER_TYPE) {
|
||||
LOG_E(TAG, "I2S controller device is not valid");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* data = new PdmMicData();
|
||||
data->native_channels = config->channels;
|
||||
data->input_gain = (float) config->input_gain_percent / 100.0f;
|
||||
|
||||
data->data_if = audio_codec_adapter_new_i2s_pdm_data(i2s_controller);
|
||||
if (data->data_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create I2S PDM data adapter");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (data->data_if->open(data->data_if, nullptr, 0) != ESP_CODEC_DEV_OK) {
|
||||
LOG_E(TAG, "Failed to open data interface");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
{
|
||||
dummy_codec_cfg_t codec_config = {
|
||||
.gpio_if = nullptr,
|
||||
.pa_pin = -1,
|
||||
.pa_reverted = false,
|
||||
};
|
||||
|
||||
data->codec_if = dummy_codec_new(&codec_config);
|
||||
if (data->codec_if == nullptr) {
|
||||
LOG_E(TAG, "Failed to create dummy codec interface");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
esp_codec_dev_cfg_t dev_config = {
|
||||
.dev_type = ESP_CODEC_DEV_TYPE_IN,
|
||||
.codec_if = data->codec_if,
|
||||
.data_if = data->data_if,
|
||||
};
|
||||
|
||||
data->codec_device = esp_codec_dev_new(&dev_config);
|
||||
if (data->codec_device == nullptr) {
|
||||
LOG_E(TAG, "Failed to create codec device");
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
device_set_driver_data(device, data);
|
||||
return ERROR_NONE;
|
||||
|
||||
cleanup:
|
||||
// Mirrors stop_device's teardown order -- delete_*_if() routines close their interface
|
||||
// first, so we don't need separate ->close() calls here.
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
delete data;
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
error_t stop_device(Device* device) {
|
||||
auto* data = GET_DATA(device);
|
||||
if (data == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
if (data->is_open) {
|
||||
esp_codec_dev_close(data->codec_device);
|
||||
}
|
||||
|
||||
if (data->codec_device != nullptr) {
|
||||
esp_codec_dev_delete(data->codec_device);
|
||||
}
|
||||
if (data->codec_if != nullptr) {
|
||||
audio_codec_delete_codec_if(data->codec_if);
|
||||
}
|
||||
if (data->data_if != nullptr) {
|
||||
audio_codec_delete_data_if(data->data_if);
|
||||
}
|
||||
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete data;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
Driver pdm_mic_driver = {
|
||||
.name = "pdm_mic",
|
||||
.compatible = (const char*[]) { "generic,spm1423", nullptr },
|
||||
.start_device = start_device,
|
||||
.stop_device = stop_device,
|
||||
.api = &API,
|
||||
.device_type = &AUDIO_CODEC_TYPE,
|
||||
.owner = nullptr,
|
||||
.internal = nullptr,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/module.h>
|
||||
|
||||
const struct ModuleSymbol pdm_mic_module_symbols[] = {
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
Reference in New Issue
Block a user