Part I - Adding a Component Code to FW¶
This lesson describes how to add a component code to the FW source tree, with a minimal Component API implementation and a simple copying function inside. It also demonstrates how to register the component driver in the FW infrastructure so that the FW can respond to the new component request sent by the driver and instantiate it.
For this lesson, the new component is called “the amplifier.” The amplifier is based on a processing component class such as effect.
Adding Basic Component Code¶
New Component Type¶
First, define a new component type in src/include/ipc/topology.h. It is a unique identifier used while declaring instances of the component as parts of the topology. More details on the required topology modifications will be provided in the next part of the tutorial; for now, our focus is on the FW source code.
Note
Simple component IDs currently used at the moment will be replaced by UUIDs in the future to avoid conflict resolutions while integrating independently developed components. The current implementation requires you to assign an unoccupied number. The UUIDs assigned to the components today are used for logging purposes only.
enum sof_comp_type {
/* ...
*/
SOF_COMP_AMP = 1000,
/* ...
*/
};
Identifier for Logging¶
Components use Universally Unique Identifiers (UUIDs) for logging. Refer to the UUID API documentation for basic information about UUIDs in FW and how to generate one for your component.
The example UUID generated for the amplifier component is 1d501197-da27-4697-80c8-4e694d3600a0.
Add the following declaration at the beginning of the source file:
DECLARE_SOF_RT_UUID("amp", amp_uuid, 0x1d501197, 0xda27, 0x4697,
0x80, 0xc8, 0x4e, 0x69, 0x4d, 0x36, 0x00, 0xa0);
DECLARE_TR_CTX(amp_tr, SOF_UUID(amp_uuid), LOG_LEVEL_INFO);
where “amp” is the component name that will be printed by the logger tool. Amplifier’s UUID value and its name is stored in the ldc file deployed on the target system and is used by the logger to resolve and print the name of the component. The only thing required to “teach” the logger the new component’s name is to update the ldc file along with the FW binary on the target system. No tool recompilation is required.
Every component has to define its trace context. It groups UUID to be inserted
into the traces produced by the component for identification purposes, as well
as run-time trace settings like the tracing level, initialized to
LOG_LEVEL_INFO
in this example. The trace context is declared using
DECLARE_TR_CTX()
macro as amp_tr
.
Basic Component API¶
Create a folder for your component source code in src/audio, such as src/audio/amp, and create a new amp.c file inside.
Declare the basic required part of the API for your component using struct
comp_driver
in amp.c. To learn more about component instances, or
devices, and their drivers, refer to Overview and
Component API.
#include <sof/audio/component.h>
/* ...
*/
struct comp_driver comp_amp = {
.type = SOF_COMP_AMP,
.uid = SOF_RT_UUID(amp_uuid),
.tctx = &_tr,
.ops = {
.create = amp_new,
.free = amp_free,
.params = NULL,
.cmd = NULL,
.trigger = amp_trigger,
.prepare = amp_prepare,
.reset = amp_reset,
.copy = amp_copy,
},
};
static SHARED_DATA struct comp_driver_info comp_amp_info = {
.drv = &comp_amp,
};
static void sys_comp_amp_init(void)
{
comp_register(platform_shared_get(&comp_amp_info,
sizeof(comp_amp_info)));
}
DECLARE_MODULE(sys_comp_amp_init);
Note that the type
used for the component driver is set to the
SOF_COMP_AMP
which is declared earlier. The uid
used for logging is
initialized by the SOF_RT_UUID(amp_uuid)
, where amp_uuid
is declared at
the beginning of the source file. The trace context amp_tr
is associated
with the driver object as well.
The API declaration is followed by a registration handler attached to the
initialization list by the DECLARE_MODULE()
macro. This is all the
infrastructure needs to know in order to find and create an instance of the
SOF_COMP_AMP
component.
The following operations are currently not implemented:
params
- the amplifier will do all the preparations and setup inside theprepare
handler; this one will not be used.cmd
- a handler to report and receive our custom run-time parameters will be implemented later in Part III - Adding Run-time Parameter Control.
Constructor amp_new()
¶
Add the following handler before your API declaration:
static struct comp_dev *amp_new(const struct comp_driver *drv,
struct sof_ipc_comp *comp)
{
struct comp_dev *dev;
struct sof_ipc_comp_process *amp;
struct sof_ipc_comp_process *ipc_amp
= (struct sof_ipc_comp_process *)comp;
struct amp_comp_data *cd;
int ret;
dev = comp_alloc(drv, COMP_SIZE(struct sof_ipc_comp_process));
if (!dev)
return NULL;
cd = rzalloc(SOF_MEM_ZONE_RUNTIME, 0, SOF_MEM_CAPS_RAM, sizeof(*cd));
if (!cd) {
rfree(dev);
return NULL;
}
amp = COMP_GET_IPC(dev, sof_ipc_comp_process);
ret = memcpy_s(amp, sizeof(*amp), ipc_amp,
sizeof(struct sof_ipc_comp_process)));
assert(!ret);
comp_set_drvdata(dev, cd);
dev->state = COMP_STATE_READY;
comp_dbg(dev, "amplifier created");
return dev;
}
The constructor:
Allocates the memory, usually in two steps. Both allocations are done from the SOF_MEM_ZONE_RUNTIME heap that should be used by the application layer which includes processing components.
First, a common context for the device is allocated including some extensions specific for a component class. In this example, the component device is based on the
struct sof_ipc_comp_process
, which is used for processing components. Component’s parameters received from the IPC request are copied to the allocated space.comp_alloc()
used for the first allocation guarantees that all important parts of thedev
are initialized as well.The second allocation acquires memory for the private data of the amplifier instance,
struct amp_comp_data
. This structure contains a placeholder at the moment. You will redefine it later to store run-time parameters of the instance. Note how the private data is attached to the device by callingcomp_set_drvdata()
. You will use symmetriccomp_get_drvdata()
to retrieve the private data object from the device object later while implementing other handlers.struct amp_comp_data { int placeholder; };
The device state is set to
COMP_STATE_READY
. To learn more about the component device state machine, refer to Overview.
Note the comp_dbg()
macro used to log the creation event where dev
is
the first argument that lets the logger resolve the name of the trace source
while processing the log entry. DEBUG level messages are not traced by
default; the trace subsystem has to be reconfigured. The trace system
outputs INFO, WARN, and ERROR messages by default.
Destructor amp_free()
¶
The destructor frees the memory allocated previously in the amp_new()
.
static void amp_free(struct comp_dev *dev)
{
struct comp_data *cd = comp_get_drvdata(dev);
rfree(cd);
rfree(dev);
}
State Transition Handler amp_trigger()
¶
The transition handler just invokes comp_set_state()
. No specific
actions are defined in this simple example.
static int amp_trigger(struct comp_dev *dev, int cmd)
{
comp_dbg(dev, "amplifier got trigger cmd %d", cmd);
return comp_set_state(dev, cmd);
}
Stream Parameters Handler amp_prepare()
¶
This is where your component can be reconfigured for the stream parameters.
This example assumes that only one source buffer and one sink buffer are
connected; therefore, only the first item from dev->bsink_list
is
verified.
Note that in the event that another “prepare” call was previously issued,
the handler returns PPL_STATUS_PATH_STOP
and exits to prevent
propagation of a likely configuration coming from another connected pipeline.
Add the following handler code before your API declaration:
static int amp_prepare(struct comp_dev *dev)
{
int ret;
struct comp_buffer *sink_buf;
struct sof_ipc_comp_config *config = dev_comp_config(dev);
uint32_t sink_per_bytes;
ret = comp_set_state(dev, COMP_TRIGGER_PREPARE);
if (ret < 0)
return ret;
if (ret == COMP_STATUS_STATE_ALREADY_SET)
return PPL_STATUS_PATH_STOP;
sink_buf = list_first_item(&dev->bsink_list,
struct comp_buffer, source_list);
sink_per_bytes = audio_stream_period_bytes(&sink_buf->stream,
dev->frames);
if (sink_buf->stream.size < config->periods_sink * sink_per_bytes) {
comp_err(dev, "amp_prepare(): sink buffer size is insufficient");
return -ENOMEM;
}
comp_dbg(dev, "amplifier prepared");
return 0;
}
Reset Handler amp_reset()
¶
The reset handler toggles the device state. It is a good place to add any instance reset code later.
static int amp_reset(struct comp_dev *dev)
{
return comp_set_state(dev, COMP_TRIGGER_RESET);
}
Signal Processing Function amp_copy
¶
This first version of the processing function simply copies input samples to output and shows how to:
Use
comp_copy_limits
andcomp_get_copy_limits_with_lock()
to retrieve information about the number of samples to be processed.Refresh the local data cache with
buffer_invalidate()
in case the input data is being provided to the source buffer by a component running on another core.Iterate over the frames, channels, and samples using the
comp_copy_limits
descriptor.Read/write from/to the circular buffers. This implementation assumes both input and output are signed 16-bit samples; therefore,
audio_stream_read_frag_s16()
andaudio_stream_write_frag_s16()
are used. You may prepare more alternatives and use the one suitable for the input/output format obtained from thesink_buf->stream.frame_fmt
in theamp_prepare()
handler.Update the shared memory containing produced samples with the local data cache using
buffer_writeback()
in the event that the output data is being consumed from the sink buffer by a component running on another core.Update the buffers’ pointers using
comp_update_buffer_consume()
andcomp_update_buffer_produce()
to indicate the data consumed and produced.
The *dst = *src
copy operation will be replaced later by amplification.
Add the following handler code before your API declaration:
static int amp_copy(struct comp_dev *dev)
{
struct comp_copy_limits cl;
struct comp_buffer *source;
struct comp_buffer *sink;
int frame;
int channel;
uint32_t buff_frag = 0;
int16_t *src;
int16_t *dst;
source = list_first_item(&dev->bsource_list, struct comp_buffer,
sink_list);
sink = list_first_item(&dev->bsink_list, struct comp_buffer,
source_list);
comp_get_copy_limits_with_lock(source, sink, &cl);
buffer_invalidate(source, cl.source_bytes);
for (frame = 0; frame < cl.frames; frame++) {
for (channel = 0; channel < sink->stream.channels; channel++) {
src = audio_stream_read_frag_s16(&source->stream,
buff_frag);
dst = audio_stream_write_frag_s16(&sink->stream,
buff_frag);
*dst = *src;
++buff_frag;
}
}
buffer_writeback(sink, cl.sink_bytes);
comp_update_buffer_produce(sink, cl.sink_bytes);
comp_update_buffer_consume(source, cl.source_bytes);
return 0;
}
Build Scripts¶
Add the following line to src/audio/CMakeLists.txt inside the block where other components’ subfolders are specified:
add_subdirectory(amp)
Create a new file src/audio/amp/CMakeLists.txt and add this line inside:
add_local_sources(sof amp.c)
Rebuild the firmware.