Trice Compared with printf
Contents
- 1. Printf-like functions
- 2. Trice IDs
- 3. Trice values bit width
- 4. Many value parameters
- 5. Floating Point Values
- 6. Runtime Generated 0-terminated Strings Transfer with triceS
- 7. Runtime Generated counted Strings Transfer with triceN
- 8. Runtime Generated Buffer Transfer with triceB
- 9. Extended format specifier possibilities
- 10. Unsupported printf format features
- 11. UTF-8 Support
- 12. Switch the language without changing a bit inside the target code
- 13. Format tags prototype specifier examples
1. Printf-like functions
…have a lot of things to do: Copy format string from FLASH memory into a RAM buffer and parse it for format specifiers. Also parse the variadic parameter list and convert each parameter according to its format specifier into a character sequences, what includes several divisions - costly function calls. Concatenate the parts to a new string and deliver it to the output, what often means copying again. A full-featured printf library consumes plenty space and processing time and several open source projects try to make it better in this or that way. Never ever call a printf-like function in time critical code, like an interrupt - it would crash your target in most cases.
The trice calls are usable inside interrupts, because they only need a few MCU clocks for execution. Porting legacy code to use it with the Trice library, means mainly to replace Printf-like function calls with trice function calls. See also chapter Legacy User Code Option Print Buffer Wrapping and Framing.
2. Trice IDs
- Each Trice caries a 14-bit nuber ID as replacement for the format string.
- This ID is automatically generated (controllable) and in the source code it is the first parameter inside the Trice macro followed by the format string and optional values.
- The user can decide not to spoil the code by having the IDs permanently in its source code, by just inserting them as a pre-compile step with
trice insertand removing them as a post-compile step withtrice clean.- The Trice cache makes this invisible to the build system, allowing full translation speed.
- The format string is not compiled into the target code. It goes together with the ID into a project specific reference list file til.json (example).
3. Trice values bit width
- No need to explicit express the value bit width.
- The default parameter width for the Trice macro is 32 bit. It is changeable to 8, 16 or 64-bit:
- Adapt
TRICE_DEFAULT_PARAMETER_BIT_WIDTHinsidetriceConfig.h. It influences - Use
-defaultTRICEBitwidthswitch during logging when changing this value.
- Adapt
- The macros
trice8,trice16,trice32,trice64are usable too, to define the bit width explicit.- This leads for the smaller bit widths to less needed space and bandwidth. But when using the default package framing TCOBS, the influence is marginal because of the implicit compression.
- The fastest Trice macro execution is, when MCU bit width matches the macro bit width.
- The implicit TCOBS compression compacts the binary Trice data during the framing.
4. Many value parameters
- No need to explicit express the values count.
- Up to 12 values are supported directly. Example:
trice( "%p | %04x %04x %04x %04x %04x %04x %04x %04x %04x | %f\n", p, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9], aFloat(x));- The long count field can encode up to 32767 payload bytes, but the current target buffer macros impose a lower limit; see Binary Encoding. Supporting more than 12 values per Trice macro would require extending the target and host code.
- Each macro can be prolonged with the used parameter count, for example
TRICE8_3orTRICE_2to intense compile time checks.- This length code extension can be done automatically using
trice u -addParamCount. This is not needed anymore:
- This length code extension can be done automatically using
- The Trice tool compares the number of given format specifiers with the written parameters in a precimpile step to minimize the risk of runtime errors.
- There is no variadic values scanning during runtime. The C preprocessor does the work.
5. Floating Point Values
These types are mixable with integer types but need to be covered by converter function.
- float types use the
aFloat()function and need a minimal value bit width of 32, to secure correct data transfer.- Example:
float x = 7.2; trice( "%f", aFloat(x)); - double types use the
aDouble()function and need a value bit width of 64, to secure correct data transfer.- Example:
double y = 7.2; trice64( "float %f and double %f", aFloat(x), aDouble(y)); - Both functions are simple and fast:
// aFloat returns passed float value x as bit pattern in a uint32_t type.
static inline uint32_t aFloat( float x ){
union {
float f;
uint32_t u;
} t;
t.f = x;
return t.u;
}
// aDouble returns passed double value x as bit pattern in a uint64_t type.
static inline uint64_t aDouble( double x ){
union {
double d;
uint64_t u;
} t;
t.d = x;
return t.u;
}
6. Runtime Generated 0-terminated Strings Transfer with triceS
- The
%sformat specifier is supported by the Trice macro too but needs specific treatment. - Strings, known at compile time should be a part of a format string to reduce runtime overhead.
- Strings created at runtime need a special
TRICE_S(ortriceS,TriceS,TRiceS) macro, which accepts exactly one%sformat specifier. It transfers at mostTRICE_SINGLE_MAX_SIZE - 12string bytes, excluding the terminating zero byte: 92 bytes by default or at most 32752 bytes with the largest supported configuration. Longer strings are truncated.- Example:
char s[] = "Hello again!"; triceS("A runtime string %20s\n", s);
Trice was designed mainly for speed. An universal trice like printf would cost too much runtime and destroy this main Trice advantage.
If you need several %s, like in
char* n = "Ann";
char* f = "Fox";
uint8_t dd = 22;
uint8_t mm = 11;
uint16_t yyyy = 1988;
// ...
print( "Name: %12s, Family: %s, Birthday %2u-%02u-%4u\n", n, f, dd, mm, yyyy );
you can do:
// ...
triceS( "Name: %12s, ", n );
triceS( "Family: %s, ", f );
trice( "Birthday %2u-%02u-%4u\n", dd, mm, yyyy );
or also
// ...
triceS( "Name: %12s, ", n ); triceS( "Family: %s, ", f ); trice( "Birthday %2u-%02u-%4u\n", dd, mm, yyyy );
7. Runtime Generated counted Strings Transfer with triceN
- It is also possible to transfer a buffer with length n using the
TRICE_N(ortriceN,TriceN,TRiceN) macro. - This becomes handy for example, when a possibly not 0-terminated string in FLASH memory needs transmission:
triceN( "msg: FLASH string is %s", addr, 16 ); - There are also specific macros like
trice32Bortrice16F. Please look into triceCheck.c for usage or see the following.
8. Runtime Generated Buffer Transfer with triceB
- A buffer is transmittable with
TRICE_B(ortriceB,TriceB,TRiceB) and specifying just one format specifier, which is then repeated. Example:
s = "abcde 12345"; // assume this as runtime generated string
triceS( "msg:Show s with triceS: %s\n", s );
len = strlen(s);
triceN( "sig:Show s with triceN:%s\n", s, len );
triceB( "dbg: %02x\n", s, len ); // Show s as colored code sequence in hex code.
triceB( "msg: %4d\n", s, len ); // Show s as colored code sequence in decimal code.
This gives output similar to:
Channel specifier within the TRICE_B format string are supported in Trice versions >= v0.66.0.
If the buffer is not 8 but 16, 32 or 32 bits wide, the macros TRICE8_B, TRICE16_B, TRICE32_B and TRICE64_B, are usable in the same manner.
9. Extended format specifier possibilities
- Because the format string is interpreted by the Trice tool written in Go, the Go capabilities partial usable.
9.1. Trice format specifier
- The Trice macros are used in C code.
- The format strings are interpreted by the Trice tool, which is written in Go.
- The C and Go format specifier are not equal but similar.
- Therefore, a Trice adaptation is internally performed.
9.2. Length modifier support
- Trice now accepts the common C length modifiers
hh,h,l,ll,j,z,t, andLtogether with the corresponding supported conversion specifiers. - This is useful for ordinary Trice macros as well as for buffer macros such as
TRICE8_B,TRICE16_B,TRICE32_B, andTRICE64_B. - The original C format string stays unchanged in the lookup data. Internally, the Trice tool normalizes only a temporary working copy so that the host side can format the values with Go.
- Therefore, these examples are accepted and decoded as expected:
%4ldbehaves like%4d%zubehaves like%u%02zxbehaves like%02x%02llxbehaves like%02x%04Lfbehaves like%04f
- The normalization keeps flags, field width, and precision. Only the C length modifier itself is removed from the host side working copy.
- Invalid combinations are not legalized by Trice. For example,
%LXis not treated as a valid integer format becauseLbelongs to floating-point conversions such as%Lf, not to%X.
9.3. Overview Table
| Format Specifier Type | C | Go | T | (T =Trice) | remark |
|---|---|---|---|---|
| signed decimal integer | d | d | d | Supported. |
| unsigned decimal integer | u | - | u | The Trice tool changes %u into %d and treats value as unsigned. |
| signed decimal integer | i | d | i | The Trice tool changes %i into %d and treats value as signed. |
| signed octal integer | - | o | o | With trice log -unsigned=false value is treated as signed. |
| unsigned octal integer | o | - | o | With trice log value is treated as unsigned. |
| signed octal integer with 0o prefix | - | O | O | With trice log -unsigned=false value is treated as signed. |
| unsigned octal integer with 0o prefix | - | - | O | With trice log value is treated as unsigned. |
| signed hexadecimal integer lowercase | - | x | x | With trice log -unsigned=false value is treated as signed. |
| unsigned hexadecimal integer lowercase | x | - | x | With trice log value is treated as unsigned. |
| signed hexadecimal integer uppercase | - | X | X | With trice log -unsigned=false value is treated as signed. |
| unsigned hexadecimal integer uppercase | X | - | X | With trice log value is treated as unsigned. |
| signed binary integer | - | b | b | With trice log -unsigned=false value is treated as signed. |
| unsigned binary integer | - | - | b | With trice log value is treated as unsigned. |
| decimal floating point, lowercase | f | f | f | aFloat(value)|aDouble(value) |
| decimal floating point, uppercase | - | F | F | aFloat(value)|aDouble(value) |
| scientific notation (mantissa/exponent), lowercase | e | e | e | aFloat(value)|aDouble(value) |
| scientific notation (mantissa/exponent), uppercase | E | E | E | aFloat(value)|aDouble(value) |
| the shortest representation of %e or %f | g | g | g | aFloat(value)|aDouble(value) |
| the shortest representation of %E or %F | G | G | G | aFloat(value)|aDouble(value) |
| a character as byte | c | - | c | Value can contain ASCII character. |
| a character represented by the corresponding Unicode code point | c | c | c | Value can contain UTF-8 characters if the C-File is edited in UTF-8 format. |
| a quoted character | - | q | q | Supported. |
| the word true or false | - | t | t | Supported. |
| a string | s | s | s | Use triceS macro with one and only one runtime generated string. |
| pointer address | p | p | p | Supported. |
| a double %% prints a single % | % | % | % | Supported. |
| Unicode escape sequence | - | U | - | Not supported. |
| value in default format | - | v | - | Not supported. |
| Go-syntax representation of the value | - | #v | - | Not supported. |
| a Go-syntax representation of the type of the value | - | T | - | Not supported. |
| nothing printed | n | - | - | Not supported. |
- Long story short: Use the
-unsigned=falseswitch when you like to see hex numbers and the like as signed values. - Look in triceCheck.c for exampe code producing this:
Select the preview to open the full animation.
10. Unsupported printf format features
Trice supports the common printf-style format specifiers used for embedded logging. Some less common printf features are intentionally not supported yet, because they do not fit well into the current lightweight Trice argument handling model or because they introduce side effects that are unsuitable for logging.
This mainly concerns:
- dynamic field width with
*, for example%*d - dynamic precision with
*, for example%.*sor%*.*f - wide character and wide string formats such as
%lcand%ls - the
%nconversion specifier
10.1. Dynamic field width with *
In standard printf, a field width can either be fixed inside the format string or supplied dynamically.
Example with fixed width:
printf("%10d", value);
Example with dynamic width:
printf("%*d", width, value);
The * is not the value to be printed. It tells printf to consume an additional int argument from the argument list and to use that value as the field width. Therefore:
printf("%*d", 10, value);
behaves like:
printf("%10d", value);
A negative dynamic width has a special meaning and implies left-aligned output, similar to the - flag.
10.2. Dynamic precision with *
The same principle exists for precision.
Example with fixed precision:
printf("%.3f", value);
Example with dynamic precision:
printf("%.*f", precision, value);
Again, the * consumes an additional int argument. For strings this is often used to limit the maximum number of emitted characters:
printf("%.*s", maxLen, text);
10.3. Dynamic width and precision together
Both dynamic field width and dynamic precision can be used in the same conversion:
printf("%*.*f", width, precision, value);
This consumes three arguments:
int width;
int precision;
double value;
The important point is that each * consumes an additional int argument before the actual value argument. This means that %*.*f does not correspond to one runtime value only. It corresponds to int, int, double.
Trice is designed so that a format string usually maps to a compact and predictable sequence of transmitted values. Dynamic width and dynamic precision break that simple mapping because the * tokens are not visible output conversions, but they still consume additional arguments.
For that reason, Trice currently does not support these forms. Supporting them correctly would require the Trice format analysis to count and encode the hidden width and precision arguments in addition to the visible value arguments.
10.4. Wide character and wide string formats: %lc and %ls
The %lc and %ls conversions are valid C printf forms for wide character and wide string data.
They are not equivalent to %c and %s:
%cand%soperate on narrow character data%lcand%lsoperate on wide character data such aswchar_t
For Trice this matters because triceS and related string handling transport runtime buffers, but the Trice tool does not automatically know how a target represents wide characters internally.
Without additional target-specific metadata, the host side would not know:
- whether
wchar_ton the target is 16 bit or 32 bit - which byte order is used
- which encoding semantics should be assumed for the transported code units
Therefore, simply removing the l and treating %lc like %c or %ls like %s would not be correct. That would silently change the meaning of the original C format string and could decode the payload differently from what the target-side C code actually describes.
For that reason, Trice currently does not support %lc and %ls. Proper support would require extra target-side type or encoding information so that the host can decode the transported data in an unambiguous and portable way.
10.5. The special %n conversion specifier
The %n specifier is fundamentally different from ordinary printf conversions.
Most conversions produce output:
printf("%d", value);
printf("%s", text);
printf("%f", number);
The %n specifier prints nothing. Instead, it writes the number of characters printed so far into the object pointed to by the corresponding argument.
Example:
int count = 0;
printf("abc%nxyz", &count);
The visible output is:
abcxyz
After the call, count contains 3, because three characters were printed before %n was reached.
Depending on the length modifier, %n expects different pointer types:
printf("%n", &i); // int *
printf("%hn", &s); // short *
printf("%hhn", &c); // signed char *
printf("%ln", &l); // long *
printf("%lln", &ll); // long long *
This means %n is not a pure logging conversion. It has a side effect because it writes to memory.
10.6. Security implications of %n
The %n specifier is also relevant for format-string security.
This is unsafe when userInput is not trusted:
printf(userInput);
If userInput contains format specifiers, printf interprets them. If it contains %n, printf expects a pointer argument and writes through it. If no valid pointer was actually passed, this can cause undefined behavior, memory corruption, or a crash. In more serious cases, uncontrolled format strings can become a security vulnerability.
The correct way to print uncontrolled text is:
printf("%s", userInput);
Because %n writes to memory and is strongly associated with format-string vulnerabilities, many coding standards and safety-oriented code bases discourage or forbid it.
10.7. Why Trice does not support %n
Trice is a logging and tracing system. Its purpose is to transfer compact log information from the target to the host, where it is decoded into readable text.
The %n specifier does not fit this model for several reasons:
%nproduces no log output.%nwrites to target memory through a pointer argument.- The written value depends on the number of characters formatted so far.
- Trice intentionally avoids full target-side formatting in order to remain small and fast.
- Supporting
%nwould introduce a side effect into what should be a side-effect-free logging operation. %nhas known security implications when format strings are not fully controlled.
For these reasons, Trice currently does not support %n.
This is intentional. Trice log statements should describe data to be logged, not modify application memory as a side effect of formatting.
11. UTF-8 Support
This is gratis, if you edit your source files containing the format strings in UTF-8:
The target does not even “know” about that, because it gets only the Trice IDs.
12. Switch the language without changing a bit inside the target code
Once the til.json list is done the user can translate it in any language and exchanging the list switches to another language. This is nowadays a simple AI agent task.
13. Format tags prototype specifier examples
This syntax is supported: %[flags][width][.precision][length]
- Because the interpretation is done inside the Trice tool written in Go these all should work:
%-d%064b%+9.3f%+#012.12g%+'#012.12E%e%9.f
