Component Best Practice
Component Description
A component is defined by an XML file specification that describes a specified function that may be implemented using different languages or alternative source codes. An implementation of a component using one of the supported languages is called a worker. A component (specification) along with the workers that implement it, are usually contained in the same component library, although a worker can implement a component specification that lives in a different component library or project.
Having workers use component specifications found in other libraries or projects is unusual, but appropriate when that other library or project is considered read-only, or when the worker is part of a specialized library of workers for a particular type of optimization.
In OpenCPI, components should perform some step of communications, signal, or data processing (e.g. a filter, a mixer, a CRC encoder), and must specify generic processing functions and must not be application specific.
Components are considered finished when they have one or more implementations (workers), unit tests (which complete successfully) and documentation. Wherever it is sensible there must be an HDL (hardware description language) and RCC (resource-constrained C/C++) implementation for a component, tested using common unit tests.
Component scope
If the properties and ports of a component do not have a common theme, then consideration should be made to whether the component is best split into multiple components.
The functional size of a component can vary significantly, for example at the extremes it is considered acceptable to have:
On the smaller end of a viable component, a component which completes a single functional behavior (e.g. a gain component or other signal processing step).
On the larger end of a best practice component, a component which completes a processing step in a data chain (e.g. IQ stream to binary data).
However, a component library would be expected to have all components of a similar size based on the aim of the component library. Smaller components, which complete a single signal processing stage would be expected in generic function component repositories. Larger components, which specify the functionality of a composition of smaller components, would be expected in a component library that contains large parts of a signal processing chain and potentially part of repository focused on one application. When possible, workers should not exceed the footprint of a small FPGA. In addition, component design should take into consideration the likelihood of component reusability.
Common Worker and Component Functionality
When a component has multiple workers, for example an RCC worker and an HDL worker, all the worker’s functionality should be the same, with the same testing and documentation applicable to all workers.
Components that share similar names (e.g. have the same description and only differ in name because the components support different input and output types) should have the same functional behavior. If different input or output types require a difference in the functional behavior, such as an integer implementation requiring scaling while a floating-point component does not, then the components should be given different names to highlight the difference in function. For example, a floating-point FIR filter would be fir_filter_f while a short FIR filter which has a scaled output would be fir_filter_scaled_s.
Within a component library there are likely to be similar components, with overlapping functionality (e.g. a cascading integrator comb (CIC) filter which interpolates and a CIC filter which decimates). It is encouraged that components with overlapping functionality are in the same component library. HDL workers that have the same functionality should make use of a common primitive for the shared functionality.
Alternative, Equivalent Implementations
The primary worker implementation should be balanced for performance on all expected platforms and for all expected use cases. However, in some cases there will be optimizations for different attributes. For example in an HDL worker an optimization might be for minimizing resource usage while another might be for timing conditions.
Different implementations for the same functional output and the same authoring model, but with different performance characteristics, are encouraged.
In this case the selection of which implementation is to be used should be enabled by a parameter property defined by the component specification, and given a constant value in each worker. The type of such a “worker selection” property should be a scalar numeric value (a metric of implementations), or an enum property offering alternative modes.
Classic examples are tradeoffs between: performance and resource usage (i.e. fast vs. small) or speed and accuracy. If the attribute is naturally numeric (e.g. accuracy), then the parameter property could be a simple numeric scalar, enabling the application to express a preference or requirement for accuracy using selection expressions (see the OpenCPI Application Development Guide). If the tradeoff is binary, a boolean value might be appropriate. In some cases a discrete set of choices would suggest an enum value.
When such a parameter property is not made available for such alternative implementations, the application developer is required to explicitly select a worker or build configuration, which is less desirable since it requires more detailed knowledge of the available implementations.
Component name
Components have zero or more input ports and zero or more output ports. Components should be named in the following way:
DESCRIPTION_INPUTTYPE_OUTPUTTYPE
DESCRIPTION should be a short description of the function the component implements. The description should start with the key functional behavior and be appended with implementation variations (e.g. fir_filter and fir_filter_scaled). This description should be a noun of the implementation, rather than a verb (e.g. cic_decimator rather than cic_decimate). The description should only use widely recognized acronyms. Abbreviations or shortening of words should not be used (e.g. “num” should not be used in place of “number”), the exception to this is for the following mathematical terms:
“sin” in place of sine
“cos” in place of cosine
“tan” in place of tangent
“log” in place of logarithm
INPUTTYPE and OUTPUTTYPE should be the initials of primary data type of the primary input and primary output port, respectively. If these types are the same, only one type need be specified. These initials do not specify the types of non-primary input or output ports (i.e. when there are more than one input or output port, and they have different types). The initials of some the different data types expected are:
Boolean:
bChar (signed):
cUnsigned char:
ucShort:
sLong:
lFloat:
fComplex short:
xsComplex float:
xf
As with C++ integer literal suffixes, a leading u defines the type as unsigned vs. a default of signed, and a type of l and ll indicate long (32 bits) and longlong (64 bits). Beyond these from C++, the letters b, c, s, f, d, indicate boolean, char, short, float or double, respectively. A prefix of x indicates the type is complex, indicating a pair of real/imaginary (I/Q) values of the indicated type.
If a component has multiple input or output ports of different types the primary type should be used in the component name (the port named input, or output or input_1 or output_1).
The component name should be all lower case with words separated by an underscore.
If a worker has one or more slaves it is considered a proxy and its name should end with _proxy.
Ports
The main input data port name should be input; variations such as input_port or in should not be used. Appended number identifiers are allowed if there are multiple data inputs (e.g. input_1, input_2).
Other input ports, which are not the component’s primary data inputs, should be given short descriptive names. The name should be all lower case and words separated with underscores. For these port names only widely recognized acronyms should be used; other abbreviations and word shortenings should not be used.
An example of the input port names for a component with multiple inputs would be, for an oscillator mixer which has a data port and another port which takes a timing signal; the data port would be called input and the other port could be called timing_reference.
The main data output port name should be output; variations such as output_port or out should not be used. Appended number identifiers are allowed if there are multiple data outputs (e.g. output_1, output_2).
Other output ports, which are not the component’s primary data outputs (e.g. optional or event outputs), should be given short descriptive names. The name should be all lower case and words separated with underscores. For these port names only widely recognized acronyms should be used; other abbreviations and word shortenings should not be used.
Protocol Support
Protocols should use the smallest data type possible. For example, if a binary state is being conveyed, a boolean data type for the protocol operation argument should be used rather than a long. When it does not conflict with a logical ordering of arguments within an operation in the XML (e.g. sequential processing dependency of later arguments on earlier arguments), longer/larger arguments should precede shorter ones (e.g. a ulong should precede a bool). This improves message compactness due to alignment rules.
A component should accept all protocol operations specified by the protocols of its input ports. A protocol operation is defined by both its opcode and associated message. A component should not discard any input operations or generate default values at the output unless the component’s functional description specifies conditions under which it is expected or acceptable (i.e. a property might indicate some such behavior). Thus the default expected behavior should be to correctly process all operations/messages. These default expectations could also be in the protocol’s documentation.
For protocols that carry timed samples, operations which do not convey sample data (i.e. metadata operations) should, as far as possible, maintain their relative timing position within the sample data stream. This usually suggests that buffered sample data should be produced for output before any metadata is passed from input to output.
For the protocols used within OpenCPI, components should:
Support all input operations as defined in the protocol or component specification, which may specify optional behaviors for certain operations.
Output operations must be produced as stated in the component or protocol documentation, which may also specify optional behavior.
When the component specification allows data to be dropped due to back-pressure on output ports (an overrun condition), earlier data should be dropped in preference to later data.
As the specific implementation will always affect how components handle the protocol, the full non-default behavior should be documented for each worker.
For protocols that specify timestamp and sample intervals, the following requirements apply.
For sample data components that have a different output sample rate to their input sample rate, update any sample interval indications whenever they are received and adjust timestamps so they correctly correspond to the first sample following them.
Where sensible, send any data in internal buffers before forwarding on non-data messages, so that non-data messages maintain, as close as possible, their relative position within the data.
Component Specification XML File
In line with the OpenCPI documentation, the ComponentSpec top-level element of a component specification XML file should not have a name attribute. This means that the component name defaults to the component specification file name.
In the component specification XML file, the properties should be listed first, followed by the inputs and finally the outputs. Parameter properties should precede non-parameter properties. Settable properties should provide default values appropriate for typical operation, except when the property value is expected to be worker-specific (i.e. a parameter property without any value in the spec).
All names in component specifications should be in lower case and use underscores to separate words. Conciseness in naming should be maintained. Names (e.g. property names) should only use widely recognized acronyms. Abbreviations or shortening of words should not be used (e.g. num should not be used in place of number).
The type attribute of properties in component specifications should be the smallest possible type to support the need (i.e. if the values needed for a property are 0-400 then a short rather than long should be used). A preference for unsigned types should be maintained. Enumerations should be used rather than “magic numbers”.
Setting the producer attribute of ports is encouraged, even when false.
The description attribute or child element of properties should be included. The description value should document the purpose and function of the attribute sufficient for using the component without other documentation. XML comments (<!-- -->) for this purpose should not be used. When multiple lines are necessary, use description elements: it is encouraged to use leading whitespace and indentation for readability of the XML. E.g.
<property name='smoothing' type='bool' initial='true'>
<description>
Enable a smoothing calculation using the formula:
V_0 = (V_0 + V_1)/2
</description>
</property>
<property name='bypass' type='bool' initial='true'
description='enable data to bypass the computation'/>
As a description is expected, comments in the component specification XML file for this purpose are not encouraged.
Worker Build Files
Parameter properties are values that are used at build-time. The build file sets the parameter property values that the worker is built for; this is done before application development and so is a component development task.
The parameter values in the build file should cover all expected use cases, with the minimum possible number of parameter values. The build file should cover all the supported component parameter values; it is expected that all supported parameter values would be tested as part of component development. When the space of possible values is too large, the tested values should span the space and include boundary conditions.
OpenCPI build files should make use of the extension -build.xml.
Component Libraries
Component libraries should contain similar components. It is allowable to move components between libraries or split libraries to keep libraries as similar collections. However, it is encouraged that older components, with their existing package IDs, are retained and deprecated rather than being removed.
If a component can fit into the definition of multiple libraries, the component should be placed into the library the component most closely matches. Historical precedence can be used as the final decider for which library a component is placed in, if there is no other deciding factor available.
If historical precedence is becoming a major and frequent decider on which library a component is to be placed in, this should trigger consideration on whether the component libraries need to be reorganized.
“Miscellaneous” component libraries should not be used. Ideally component libraries will contain more than one component; however, it is acceptable to have a library of a single component particularly if this prevents the need for a miscellaneous library, but libraries should not be named or designed to have a single component.
Component Library Naming
Component libraries exist in projects, and thus are scoped within the name of the project. Thus component libraries exist in a two-level namespace which includes the project name and the library name. The project name provides a higher level category name for the component libraries it containers. Normally the project name connotes what might be called an “application domain”, and the libraries within the project are logical groupings of components within that broader project.
All library names should contain a short description of the library. This description should be a noun. This description should only use widely recognized acronyms. Abbreviations or shortening of words should not be used (e.g. num should not be used in place of number).
As accessing components for use is done by referencing the parent OpenCPI project and components/<library> folder of the project, the project-level information should not be duplicated in the component library name. The word “component” or variations of either should not be part of the component library name.
Component library names should be all lower case with words separated by an underscore.
An example component library name, for a digital signal processing (DSP) component library would be dsp.