X-Git-Url: https://sigrok.org/gitweb/?a=blobdiff_plain;f=src%2Foutput%2Fvcd.c;h=ec326b7eeae99d1184b9cd18f8e12607bd2923a2;hb=deb7615262ac4f9cc0750a08351afa7cbf9c34d5;hp=ba06c5ae69d885bdc43effe63f9708dd147da33f;hpb=2ea1fdf12113311cbe1a4316e9e2efe4d8ac40f0;p=libsigrok.git diff --git a/src/output/vcd.c b/src/output/vcd.c index ba06c5ae..ec326b7e 100644 --- a/src/output/vcd.c +++ b/src/output/vcd.c @@ -3,6 +3,7 @@ * * Copyright (C) 2010 Uwe Hermann * Copyright (C) 2013 Bert Vermeulen + * Copyright (C) 2017-2020 Gerhard Sittig * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,67 +19,321 @@ * along with this program; if not, see . */ +/* + * TODO + * - Check the mixed signal queue for completeness and correctness. + * - Tune the analog "immediate write" code path for throughput. + * - Remove excess diagnostics when the implementation is considered + * feature complete and reliable. + */ + #include + +#include +#include #include #include -#include + #include #include "libsigrok-internal.h" #define LOG_PREFIX "output/vcd" +static const int with_queue_stats = 0; +static const int with_pool_stats = 0; + +struct vcd_channel_desc { + size_t index; + GString *name; + enum sr_channeltype type; + struct { + uint8_t logic; + double real; + } last; + uint64_t last_rcvd_snum; +}; + +/** Queued values for a given sample number. */ +struct vcd_queue_item { + uint64_t samplenum; /**!< sample number, _not_ timestamp */ + GString *values; /**!< text of value changes */ +}; + struct context { - int num_enabled_channels; - GArray *channelindices; - uint8_t *prevsample; + size_t enabled_count; + size_t logic_count; + size_t analog_count; gboolean header_done; - int period; - int *channel_index; + uint64_t period; + struct vcd_channel_desc *channels; uint64_t samplerate; - uint64_t samplecount; + GSList *free_list, *used_list; + size_t alloced, freed, reused, pooled; + GList *vcd_queue_list; + GList *vcd_queue_last; + gboolean immediate_write; + uint8_t *last_logic; }; +/* + * Construct VCD signal identifiers from a sigrok channel index. The + * routine returns a GString which the caller is supposed to release. + * + * There are 94 printable ASCII characters. For larger channel index + * numbers multiple letters get concatenated (sticking with letters). + * + * The current implementation covers these ranges: + * - 94 single letter identifiers + * - 26 ^ 2 = 676, 94 + 676 = 770 for two letter identifiers + * - 26 ^ 3 = 17576, 770 + 17576 = 18346 for three letter identifiers + * + * This approach can get extended as needed when support for larger + * channel counts is desired. Any such extension remains transparent + * to call sites. + * + * TODO This implementation assumes that the software will run on a + * machine which uses the ASCII character set. Platforms that use other + * representations or non-contiguous character ranges for their alphabet + * cannot use a simple addition, instead need to execute table lookups. + */ + +#define VCD_IDENT_CHAR_MIN '!' +#define VCD_IDENT_CHAR_MAX '~' +#define VCD_IDENT_COUNT_1CHAR (VCD_IDENT_CHAR_MAX + 1 - VCD_IDENT_CHAR_MIN) +#define VCD_IDENT_ALPHA_MIN 'a' +#define VCD_IDENT_ALPHA_MAX 'z' +#define VCD_IDENT_COUNT_ALPHA (VCD_IDENT_ALPHA_MAX + 1 - VCD_IDENT_ALPHA_MIN) +#define VCD_IDENT_COUNT_2CHAR (VCD_IDENT_COUNT_ALPHA * VCD_IDENT_COUNT_ALPHA) +#define VCD_IDENT_COUNT_3CHAR (VCD_IDENT_COUNT_2CHAR * VCD_IDENT_COUNT_ALPHA) +#define VCD_IDENT_COUNT (VCD_IDENT_COUNT_1CHAR + VCD_IDENT_COUNT_2CHAR + VCD_IDENT_COUNT_3CHAR) + +static GString *vcd_identifier(size_t idx) +{ + GString *symbol; + char c1, c2, c3; + + symbol = g_string_sized_new(4); + + /* First 94 channels, one printable character. */ + if (idx < VCD_IDENT_COUNT_1CHAR) { + c1 = VCD_IDENT_CHAR_MIN + idx; + g_string_printf(symbol, "%c", c1); + return symbol; + } + idx -= VCD_IDENT_COUNT_1CHAR; + + /* Next 676 channels, two lower case characters. */ + if (idx < VCD_IDENT_COUNT_2CHAR) { + c2 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA); + idx /= VCD_IDENT_COUNT_ALPHA; + c1 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA); + idx /= VCD_IDENT_COUNT_ALPHA; + if (idx) + sr_dbg("VCD identifier creation BUG (two char)."); + g_string_printf(symbol, "%c%c", c1, c2); + return symbol; + } + idx -= VCD_IDENT_COUNT_2CHAR; + + /* Next 17576 channels, three lower case characters. */ + if (idx < VCD_IDENT_COUNT_3CHAR) { + c3 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA); + idx /= VCD_IDENT_COUNT_ALPHA; + c2 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA); + idx /= VCD_IDENT_COUNT_ALPHA; + c1 = VCD_IDENT_ALPHA_MIN + (idx % VCD_IDENT_COUNT_ALPHA); + idx /= VCD_IDENT_COUNT_ALPHA; + if (idx) + sr_dbg("VCD identifier creation BUG (three char)."); + g_string_printf(symbol, "%c%c%c", c1, c2, c3); + return symbol; + } + idx -= VCD_IDENT_COUNT_3CHAR; + + /* + * TODO + * Add combinations with more positions or larger character sets + * when support for more channels is required. + */ + sr_dbg("VCD identifier creation ENOTSUPP (need %zu more).", idx); + g_string_free(symbol, TRUE); + + return NULL; +} + +/* + * Notes on the VCD text output formatting routines: + * - Always start new text lines when timestamps get emitted. + * - Optionally terminate timestamp lines when the caller asked us to. + * - Prepend all values with whitespace, assume they follow a timestamp + * or a previously printed value. This works fine from the data point + * of view for the start of new lines, as well. + * - Put the mandatory whitespace between real (or vector) values and + * the following identifier. No whitespace for single bit values. + * - For real values callers need not specify "precision" nor the number + * of significant digits. The Verilog VCD spec specifically picked the + * "%.16g" format such that all bits of the internal presentation of + * the IEEE754 floating point value get communicated between the + * writer and the reader. + */ + +static void append_vcd_timestamp(GString *s, double ts, gboolean lf) +{ + + g_string_append_c(s, '\n'); + g_string_append_c(s, '#'); + g_string_append_printf(s, "%.0f", ts); + g_string_append_c(s, lf ? '\n' : ' '); +} + +static void format_vcd_value_bit(GString *s, uint8_t bit_value, GString *id) +{ + + g_string_append_c(s, bit_value ? '1' : '0'); + g_string_append(s, id->str); +} + +static void format_vcd_value_real(GString *s, double real_value, GString *id) +{ + + g_string_append_c(s, 'r'); + g_string_append_printf(s, "%.16g", real_value); + g_string_append_c(s, ' '); + g_string_append(s, id->str); +} + static int init(struct sr_output *o, GHashTable *options) { struct context *ctx; + size_t alloc_size; struct sr_channel *ch; GSList *l; - int num_enabled_channels, i; + size_t num_enabled, num_logic, num_analog, desc_idx; + struct vcd_channel_desc *desc; (void)options; - num_enabled_channels = 0; + /* Determine the number of involved channels. */ + num_enabled = 0; + num_logic = 0; + num_analog = 0; for (l = o->sdi->channels; l; l = l->next) { ch = l->data; - if (ch->type != SR_CHANNEL_LOGIC) - continue; if (!ch->enabled) continue; - num_enabled_channels++; + if (ch->type == SR_CHANNEL_LOGIC) { + num_logic++; + } else if (ch->type == SR_CHANNEL_ANALOG) { + num_analog++; + } else { + continue; + } + num_enabled++; } - if (num_enabled_channels > 94) { - sr_err("VCD only supports 94 channels."); + if (num_enabled > VCD_IDENT_COUNT) { + sr_err("Only up to %d VCD signals supported.", VCD_IDENT_COUNT); return SR_ERR; } - ctx = g_malloc0(sizeof(struct context)); + /* Allocate space for channel descriptions. */ + ctx = g_malloc0(sizeof(*ctx)); o->priv = ctx; - ctx->num_enabled_channels = num_enabled_channels; - ctx->channel_index = g_malloc(sizeof(int) * ctx->num_enabled_channels); + ctx->enabled_count = num_enabled; + ctx->logic_count = num_logic; + ctx->analog_count = num_analog; + alloc_size = sizeof(ctx->channels[0]) * ctx->enabled_count; + ctx->channels = g_malloc0(alloc_size); - /* Once more to map the enabled channels. */ - for (i = 0, l = o->sdi->channels; l; l = l->next) { + /* + * Reiterate input descriptions, to fill in output descriptions. + * Map channel indices, and assign symbols to VCD channels. + */ + desc_idx = 0; + for (l = o->sdi->channels; l; l = l->next) { ch = l->data; - if (ch->type != SR_CHANNEL_LOGIC) - continue; if (!ch->enabled) continue; - ctx->channel_index[i++] = ch->index; + desc = &ctx->channels[desc_idx]; + desc->index = ch->index; + desc->name = vcd_identifier(desc_idx); + desc->type = ch->type; + /* + * Make sure to _not_ match next time, to have initial + * values dumped when the first sample gets received. + */ + if (desc->type == SR_CHANNEL_LOGIC && num_logic) { + num_logic--; + desc->last.logic = ~0; + } else if (desc->type == SR_CHANNEL_ANALOG && num_analog) { + num_analog--; + /* "Construct" NaN, avoid a compile time error. */ + desc->last.real = 0.0; + desc->last.real = 0.0 / desc->last.real; + } else { + g_string_free(desc->name, TRUE); + memset(desc, 0, sizeof(*desc)); + continue; + } + desc_idx++; } + /* + * Keep channel counts at hand, and a flag which allows to tune + * for special cases' speedup in .receive(). + */ + ctx->immediate_write = FALSE; + if (ctx->analog_count == 0) + ctx->immediate_write = TRUE; + if (ctx->logic_count == 0 && ctx->analog_count == 1) + ctx->immediate_write = TRUE; + + /* + * Keep a copy of the last logic data bitmap around. To avoid + * iterating over individual bits when nothing in the set has + * changed. The overhead of two byte array compares should + * outweight the tenfold bit count compared to byte counts. + */ + alloc_size = (ctx->logic_count + 7) / 8; + ctx->last_logic = g_malloc0(alloc_size); + if (ctx->logic_count && !ctx->last_logic) + return SR_ERR_MALLOC; + return SR_OK; } +/* + * VCD can only handle 1/10/100 factors in the s to fs range. Find a + * suitable timescale which satisfies this resolution constraint, yet + * won't result in excessive overhead. + */ +static uint64_t get_timescale_freq(uint64_t samplerate) +{ + uint64_t timescale; + size_t max_up_scale; + + /* Go to the next full decade. */ + timescale = 1; + while (timescale < samplerate) { + timescale *= 10; + } + + /* + * Avoid loss of precision, go up a few more decades when needed. + * For example switch to 10GHz timescale when samplerate is 400MHz. + * Stop after at most factor 100 to not loop endlessly for odd + * samplerates, yet provide good enough accuracy. + */ + max_up_scale = 2; + while (max_up_scale--) { + if (timescale / samplerate * samplerate == timescale) + break; + timescale *= 10; + } + + return timescale; +} + +/* Emit a VCD file header. */ static GString *gen_header(const struct sr_output *o) { struct context *ctx; @@ -87,83 +342,664 @@ static GString *gen_header(const struct sr_output *o) GString *header; GSList *l; time_t t; - int num_channels, i; + size_t num_channels, i; char *samplerate_s, *frequency_s, *timestamp; + struct vcd_channel_desc *desc; + char *type_text, *size_text; + int ret; ctx = o->priv; - header = g_string_sized_new(512); - num_channels = g_slist_length(o->sdi->channels); - /* timestamp */ + /* Get channel count, and samplerate if not done yet. */ + num_channels = g_slist_length(o->sdi->channels); + if (!ctx->samplerate) { + ret = sr_config_get(o->sdi->driver, o->sdi, NULL, + SR_CONF_SAMPLERATE, &gvar); + if (ret == SR_OK) { + ctx->samplerate = g_variant_get_uint64(gvar); + g_variant_unref(gvar); + } + } + ctx->period = get_timescale_freq(ctx->samplerate); t = time(NULL); timestamp = g_strdup(ctime(&t)); - timestamp[strlen(timestamp)-1] = 0; + timestamp[strlen(timestamp) - 1] = '\0'; + samplerate_s = NULL; + if (ctx->samplerate) + samplerate_s = sr_samplerate_string(ctx->samplerate); + frequency_s = sr_period_string(1, ctx->period); + + /* Construct the VCD output file header. */ + header = g_string_sized_new(512); g_string_printf(header, "$date %s $end\n", timestamp); + g_string_append_printf(header, "$version %s %s $end\n", + PACKAGE_NAME, sr_package_version_string_get()); + g_string_append_printf(header, "$comment\n"); + g_string_append_printf(header, + " Acquisition with %zu/%zu channels%s%s\n", + ctx->enabled_count, num_channels, + samplerate_s ? " at " : "", samplerate_s ? : ""); + g_string_append_printf(header, "$end\n"); + g_string_append_printf(header, "$timescale %s $end\n", frequency_s); + + /* List generated VCD signals within a scope. */ + g_string_append_printf(header, "$scope module %s $end\n", PACKAGE_NAME); + i = 0; + for (l = o->sdi->channels; l; l = l->next) { + ch = l->data; + if (!ch->enabled) + continue; + desc = &ctx->channels[i++]; + if (desc->type == SR_CHANNEL_LOGIC) { + type_text = "wire"; + size_text = "1"; + } else if (desc->type == SR_CHANNEL_ANALOG) { + type_text = "real"; + size_text = "64"; + } else { + i--; + continue; + } + g_string_append_printf(header, "$var %s %s %s %s $end\n", + type_text, size_text, desc->name->str, ch->name); + } + g_string_append(header, "$upscope $end\n"); + + g_string_append(header, "$enddefinitions $end\n"); + g_free(timestamp); + g_free(samplerate_s); + g_free(frequency_s); - /* generator */ - g_string_append_printf(header, "$version %s %s $end\n", - PACKAGE_NAME, SR_PACKAGE_VERSION_STRING); - g_string_append_printf(header, "$comment\n Acquisition with " - "%d/%d channels", ctx->num_enabled_channels, num_channels); + return header; +} - if (ctx->samplerate == 0) { - if (sr_config_get(o->sdi->driver, o->sdi, NULL, SR_CONF_SAMPLERATE, - &gvar) == SR_OK) { - ctx->samplerate = g_variant_get_uint64(gvar); - g_variant_unref(gvar); +/* + * Gets called when a session feed packet was received. Either creates + * a VCD file header (once in the output module's lifetime), or an empty + * GString. Callers will append the text representation of sample data + * to that string as needed. + */ +static GString *chk_header(const struct sr_output *o) +{ + struct context *ctx; + GString *s; + + ctx = o->priv; + + if (!ctx->header_done) { + ctx->header_done = TRUE; + s = gen_header(o); + } else { + s = g_string_sized_new(512); + } + + return s; +} + +/* + * Helpers to "merge sort" sample data that we have received in chunks + * at different times in different code paths. Queue the data until we + * have seen samples from all involved channels for a given samplenumber. + * Data for a given sample number can only get emitted when we are sure + * no other channel's data can arrive any more. + */ + +static struct vcd_queue_item *queue_alloc_item(struct context *ctx, uint64_t snum) +{ + GSList *node; + struct vcd_queue_item *item; + + /* Get an item from the free list if available. */ + node = ctx->free_list; + if (node) { + ctx->reused++; + + /* Unlink GSList node from the free list. */ + ctx->free_list = node->next; + node->next = NULL; + item = node->data; + node->data = NULL; + + /* Setup content of the item. */ + item->samplenum = snum; + if (!item->values) + item->values = g_string_sized_new(32); + else + g_string_truncate(item->values, 0); + + /* Keep GSList node in the used list (avoid free/alloc). */ + node->next = ctx->used_list; + ctx->used_list = node; + + return item; + } + + /* Dynamic allocation of an item. */ + ctx->alloced++; + item = g_malloc0(sizeof(*item)); + if (!item) + return NULL; + item->samplenum = snum; + item->values = g_string_sized_new(32); + + /* Create a used list item, to later move to the free list. */ + ctx->used_list = g_slist_prepend(ctx->used_list, item); + + return item; +} + +static void queue_free_item(struct context *ctx, struct vcd_queue_item *item) +{ + GSList *node; + + /* + * Put item back into the free list. We can assume to find a + * used list node, it got allocated when the item was acquired. + */ + node = ctx->used_list; + if (node) { + ctx->pooled++; + + ctx->used_list = node->next; + node->next = NULL; + node->data = item; + + item->samplenum = 0; + g_string_truncate(item->values, 0); + + node->next = ctx->free_list; + ctx->free_list = node; + + return; + } + + /* + * Release dynamically allocated resources. Could also be used + * to release free list items when the use list is empty. + */ + ctx->freed++; + if (item->values) + g_string_free(item->values, TRUE); + g_free(item); +} + +static void queue_drain_pool_cb(gpointer data, gpointer cb_data) +{ + struct context *ctx; + struct vcd_queue_item *item; + + item = data; + ctx = cb_data; + queue_free_item(ctx, item); +} + +static void queue_drain_pool(struct context *ctx) +{ + GSList *list; + + /* + * Grab the list and "empty" the context member. Then + * iterate over the items, have dymamic memory released. + * Then free the GSList nodes (but not their data parts). + * Do this for the used and the free lists. + */ + list = ctx->used_list; + ctx->used_list = NULL; + g_slist_foreach(list, queue_drain_pool_cb, ctx); + g_slist_free(list); + + list = ctx->free_list; + ctx->free_list = NULL; + g_slist_foreach(list, queue_drain_pool_cb, ctx); + g_slist_free(list); +} + +static int cmp_snum(gconstpointer l, gconstpointer d) +{ + const struct vcd_queue_item *list_item; + const uint64_t *snum_ptr; + + list_item = l; + snum_ptr = d; + if (list_item->samplenum > *snum_ptr) + return +1; + if (list_item->samplenum < *snum_ptr) + return -1; + return 0; +} + +static int cmp_items(gconstpointer a, gconstpointer b) +{ + const struct vcd_queue_item *item_a, *item_b; + + item_a = a; + item_b = b; + if (item_a->samplenum > item_b->samplenum) + return +1; + if (item_a->samplenum < item_b->samplenum) + return -1; + return 0; +} + +/* + * Position the current pointer of the VCD value queue to a specific + * sample number. Create a new queue item when needed. The logic assumes + * a specific use pattern: Reception of striped sample data for channels + * and processing in strict order of sample numbers within a channel. + * Lower sample numbers near the start of the queue when channels change + * between session feed packets, before another linear sequence follows. + * + * Naive use of convenience glib routines would severely lose performance. + * That's why custom code is used, which is as complex as it needs to be, + * yet shall execute faster than a simpler implementation. For trivial + * cases (logic only, one analog channel only) this queue is bypassed. + */ +static int queue_samplenum(struct context *ctx, uint64_t snum) +{ + struct vcd_queue_item *item, *add_item; + GList *walk_list, *after_snum, *before_snum, *add_list; + GList *last; + gboolean add_after_last, do_search; + + /* Already at that position? */ + item = ctx->vcd_queue_last ? ctx->vcd_queue_last->data : NULL; + if (item && item->samplenum == snum) + return SR_OK; + + /* + * Search after the current position in the remaining queue. The + * custom code uses the queue's being sorted by sample number. + * Narrow down a later insert position as much as possible. This + * avoids linear search in huge spaces later on. + */ + last = NULL; + add_after_last = FALSE; + after_snum = NULL; + before_snum = NULL; + walk_list = ctx->vcd_queue_last; + while (walk_list) { + item = walk_list->data; + if (!item) + break; + if (item->samplenum == snum) { + ctx->vcd_queue_last = walk_list; + return SR_OK; + } + last = walk_list; + if (item->samplenum < snum) + before_snum = walk_list; + if (item->samplenum > snum) { + after_snum = walk_list; + break; } + if (!walk_list->next) + add_after_last = TRUE; + walk_list = walk_list->next; } - if (ctx->samplerate != 0) { - samplerate_s = sr_samplerate_string(ctx->samplerate); - g_string_append_printf(header, " at %s", samplerate_s); - g_free(samplerate_s); + + /* + * No exact match at or beyond the current position. Run another + * search from the start of the queue, again restrict the space + * which is searched, and narrow down the insert position when + * no match is found. + * + * If the searched sample number is larger than any we have seen + * before, or was in the above covered range but was not found, + * then we know that another queue item needs to get added, and + * where to put it. In that case we need not iterate the earlier + * list items. + */ + walk_list = ctx->vcd_queue_list; + do_search = TRUE; + if (add_after_last) + do_search = FALSE; + if (before_snum) + do_search = FALSE; + while (do_search && walk_list && walk_list != ctx->vcd_queue_last) { + item = walk_list->data; + if (!item) + break; + if (item->samplenum == snum) { + ctx->vcd_queue_last = walk_list; + return SR_OK; + } + if (item->samplenum < snum) + before_snum = walk_list; + if (item->samplenum > snum) { + after_snum = walk_list; + break; + } + walk_list = walk_list->next; } - g_string_append_printf(header, "\n$end\n"); - - /* timescale */ - /* VCD can only handle 1/10/100 (s - fs), so scale up first */ - if (ctx->samplerate > SR_MHZ(1)) - ctx->period = SR_GHZ(1); - else if (ctx->samplerate > SR_KHZ(1)) - ctx->period = SR_MHZ(1); - else - ctx->period = SR_KHZ(1); - frequency_s = sr_period_string(ctx->period); - g_string_append_printf(header, "$timescale %s $end\n", frequency_s); - g_free(frequency_s); - /* scope */ - g_string_append_printf(header, "$scope module %s $end\n", PACKAGE_NAME); + /* + * The complete existing queue was exhausted, no exact match was + * found. A new queue item must get inserted. Identify a good + * position where to start searching for the exact position to + * link the new item to the list. Assume that the combination of + * the glib routine's list traversal and the sample number check + * in the callback is expensive, reduce the amount of work done. + * + * If we have seen an item with a larger sample number than the + * wanted, check its immediate predecessor. If this has a smaller + * sample number, then we found a perfect location to insert the + * new item. If we know that the new item must be inserted after + * the last traversed queue item, start there. + */ + if (!before_snum) do { + if (add_after_last) + break; + if (!after_snum) + break; + walk_list = after_snum->prev; + if (!walk_list) + break; + item = walk_list->data; + if (!item) + break; + if (item->samplenum == snum) { + ctx->vcd_queue_last = walk_list; + return SR_OK; + } + if (item->samplenum < snum) + before_snum = walk_list; + } while (0); + add_list = add_after_last ? last : before_snum; + if (!add_list) { + walk_list = ctx->vcd_queue_list; + while (walk_list) { + item = walk_list->data; + if (!item) + break; + if (item->samplenum == snum) { + ctx->vcd_queue_last = walk_list; + return SR_OK; + } + if (item->samplenum > snum) { + after_snum = walk_list; + break; + } + add_list = walk_list; + walk_list = walk_list->next; + } + } + if (add_list && (item = add_list->data) && item->samplenum == snum) { + ctx->vcd_queue_last = add_list; + return SR_OK; + } - /* Wires / channels */ - for (i = 0, l = o->sdi->channels; l; l = l->next, i++) { - ch = l->data; - if (ch->type != SR_CHANNEL_LOGIC) + /* + * Create a new queue item for the so far untracked sample + * number. Immediately search for the inserted position (is + * unfortunately not returned from the insert call), and + * cache that position for subsequent lookups. + */ + if (with_queue_stats) + sr_dbg("%s(), queue nr %" PRIu64, __func__, snum); + add_item = queue_alloc_item(ctx, snum); + if (!add_item) + return SR_ERR_MALLOC; + if (!add_list) + add_list = ctx->vcd_queue_list; + if (add_list && add_list->prev) + add_list = add_list->prev; + walk_list = g_list_insert_sorted(add_list, add_item, cmp_items); + if (!walk_list->prev) + ctx->vcd_queue_list = walk_list; + walk_list = g_list_find_custom(walk_list, &snum, cmp_snum); + item = walk_list ? walk_list->data : NULL; + if (item && item->samplenum == snum) { + ctx->vcd_queue_last = walk_list; + } + return SR_OK; +} + +/* + * Prepare to append another text fragment for a value change to the + * queue item which corresponds to the current sample number. Return + * the GString which the caller then will append to. + */ +static GString *queue_value_text_prep(struct context *ctx) +{ + struct vcd_queue_item *item; + GString *buff; + + /* Cope with not-yet-positioned write pointers. */ + item = ctx->vcd_queue_last ? ctx->vcd_queue_last->data : NULL; + if (!item) + return NULL; + + /* Create a GString if not done already. */ + buff = item->values; + if (!buff) { + buff = g_string_sized_new(20); + item->values = buff; + } + + /* Separate items with spaces (if previous content is present). */ + if (buff->len) + g_string_append_c(buff, ' '); + + return buff; +} + +static double snum_to_ts(struct context *ctx, uint64_t snum) +{ + double ts; + + ts = (double)snum; + ts /= ctx->samplerate; + ts *= ctx->period; + + return ts; +} + +/* + * Unqueue one item of the VCD values queue which corresponds to one + * sample number. Append all of the text to the passed in GString. + */ +static int unqueue_item(struct context *ctx, + struct vcd_queue_item *item, GString *s) +{ + double ts; + GString *buff; + gboolean is_empty; + + /* + * Start the sample number's string with the timestamp. Append + * all value changes. Terminate lines for items which have a + * timestamp but no value changes, assuming this is the last + * entry which corresponds to SR_DF_END. + */ + ts = snum_to_ts(ctx, item->samplenum); + buff = item->values; + is_empty = !buff || !buff->len || !buff->str || !*buff->str; + append_vcd_timestamp(s, ts, is_empty); + if (!is_empty) + g_string_append(s, buff->str); + + return SR_OK; +} + +/* + * Get the last sample number which logic data was received for. This + * implementation assumes that all logic channels get received within + * exactly one packet of corresponding unitsize. + */ +static uint64_t get_last_snum_logic(struct context *ctx) +{ + size_t i; + struct vcd_channel_desc *desc; + + for (i = 0; i < ctx->enabled_count; i++) { + desc = &ctx->channels[i]; + if (desc->type != SR_CHANNEL_LOGIC) continue; - if (!ch->enabled) + return desc->last_rcvd_snum; + } + + return 0; +} + +/* + * Update the last sample number which logic data was received for. + */ +static void upd_last_snum_logic(struct context *ctx, uint64_t inc) +{ + size_t i; + struct vcd_channel_desc *desc; + + for (i = 0; i < ctx->enabled_count; i++) { + desc = &ctx->channels[i]; + if (desc->type != SR_CHANNEL_LOGIC) continue; - g_string_append_printf(header, "$var wire 1 %c %s $end\n", - (char)('!' + i), ch->name); + desc->last_rcvd_snum += inc; } +} + +/* + * Get and update the last sample number which analog data was received + * for on a specific channel (which the caller already has identified). + */ - g_string_append(header, "$upscope $end\n$enddefinitions $end\n"); +static uint64_t get_last_snum_analog(struct vcd_channel_desc *desc) +{ - return header; + return desc->last_rcvd_snum; } -static int receive(const struct sr_output *o, const struct sr_datafeed_packet *packet, - GString **out) +static void upd_last_snum_analog(struct vcd_channel_desc *desc, uint64_t inc) +{ + + if (!desc) + return; + desc->last_rcvd_snum += inc; +} + +/* + * Determine the maximum sample number which data from all involved + * channels was received for. + */ +static uint64_t get_max_snum_export(struct context *ctx) { + uint64_t snum; + size_t i; + struct vcd_channel_desc *desc; + + snum = ~UINT64_C(0); + for (i = 0; i < ctx->enabled_count; i++) { + desc = &ctx->channels[i]; + if (snum > desc->last_rcvd_snum) + snum = desc->last_rcvd_snum; + } + + return snum; +} + +/* + * Determine the maximum sample number of any channel we may have + * received data for. Then pretend we had seen that number of samples + * on all channels. Such that the next export can flush all previously + * queued data up to and including the final number, which serves as + * some kind of termination of the VCD output data. + */ +static uint64_t get_max_snum_flush(struct context *ctx) +{ + uint64_t snum; + size_t i; + struct vcd_channel_desc *desc; + + /* Determine the maximum sample number. */ + snum = 0; + for (i = 0; i < ctx->enabled_count; i++) { + desc = &ctx->channels[i]; + if (snum < desc->last_rcvd_snum) + snum = desc->last_rcvd_snum; + } + + /* Record that number as "seen" with all channels. */ + for (i = 0; i < ctx->enabled_count; i++) { + desc = &ctx->channels[i]; + desc->last_rcvd_snum = snum + 1; + } + + return snum; +} + +/* + * Pass all queued value changes when we are certain we have received + * data from all channels. + */ +static int write_completed_changes(struct context *ctx, GString *out) +{ + uint64_t upto_snum; + GList **listref, *node; + struct vcd_queue_item *item; + int rc; + size_t dumped; + + /* Determine the number which all data was received for so far. */ + upto_snum = get_max_snum_export(ctx); + if (with_queue_stats) + sr_spew("%s(), check up to %" PRIu64, __func__, upto_snum); + + /* + * Forward and consume those items from the head of the list + * which we completely have accumulated and are certain about. + */ + dumped = 0; + listref = &ctx->vcd_queue_list; + while (*listref) { + /* Find items before the targetted sample number. */ + node = *listref; + item = node->data; + if (!item) + break; + if (item->samplenum >= upto_snum) + break; + + /* + * Unlink the item from the list. Void cached positions. + * Append its timestamp and values to the caller's text. + */ + dumped++; + if (with_queue_stats) + sr_dbg("%s(), dump nr %" PRIu64, + __func__, item->samplenum); + if (ctx->vcd_queue_last == node) + ctx->vcd_queue_last = NULL; + *listref = g_list_remove_link(*listref, node); + rc = unqueue_item(ctx, item, out); + queue_free_item(ctx, item); + if (rc != SR_OK) + return rc; + } + + return SR_OK; +} + +/* Get packets from the session feed, generate output text. */ +static int receive(const struct sr_output *o, + const struct sr_datafeed_packet *packet, GString **out) +{ + struct context *ctx; const struct sr_datafeed_meta *meta; const struct sr_datafeed_logic *logic; + const struct sr_datafeed_analog *analog; const struct sr_config *src; GSList *l; - struct context *ctx; - unsigned int i; - int p, curbit, prevbit, index; - uint8_t *sample; - gboolean timestamp_written; + struct vcd_channel_desc *desc; + uint64_t snum_curr; + size_t count, index, p, unit_size; + gboolean changed; + GString *s_val; + uint8_t *sample, *last_logic, prevbit, curbit; + GSList *channels; + struct sr_channel *channel; + int rc; + float *floats, value; + double ts; *out = NULL; if (!o || !o->priv) @@ -181,62 +1017,158 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p } break; case SR_DF_LOGIC: - logic = packet->payload; + *out = chk_header(o); - if (!ctx->header_done) { - *out = gen_header(o); - ctx->header_done = TRUE; - } else { - *out = g_string_sized_new(512); - } - - if (!ctx->prevsample) { - /* Can't allocate this until we know the stream's unitsize. */ - ctx->prevsample = g_malloc0(logic->unitsize); - } + logic = packet->payload; + sample = logic->data; + unit_size = logic->unitsize; + count = logic->length / unit_size; + snum_curr = get_last_snum_logic(ctx); + upd_last_snum_logic(ctx, count); - for (i = 0; i <= logic->length - logic->unitsize; i += logic->unitsize) { - sample = logic->data + i; - timestamp_written = FALSE; + last_logic = ctx->last_logic; + while (count--) { + /* Check whether any logic value has changed. */ + changed = memcmp(last_logic, sample, unit_size) != 0; + changed |= snum_curr == 0; + if (changed) + memcpy(last_logic, sample, unit_size); - for (p = 0; p < ctx->num_enabled_channels; p++) { - index = ctx->channel_index[p]; + /* + * Start or continue tracking that sample number. + * Avoid string copies for logic-only setups. + */ + if (changed) { + if (ctx->immediate_write) { + ts = snum_to_ts(ctx, snum_curr); + append_vcd_timestamp(*out, ts, FALSE); + } else { + queue_samplenum(ctx, snum_curr); + } + } - curbit = ((unsigned)sample[index / 8] - >> (index % 8)) & 1; - prevbit = ((unsigned)ctx->prevsample[index / 8] - >> (index % 8)) & 1; + /* Iterate over individual logic channels. */ + for (p = 0; changed && p < ctx->enabled_count; p++) { + /* + * TODO Check whether the mapping from + * data image positions to channel numbers + * is required. Experiments suggest that + * the data image "is dense", and packs + * bits of enabled channels, and leaves no + * room for positions of disabled channels. + */ + desc = &ctx->channels[p]; + if (desc->type != SR_CHANNEL_LOGIC) + continue; + index = desc->index; + prevbit = desc->last.logic; - /* VCD only contains deltas/changes of signals. */ - if (prevbit == curbit && ctx->samplecount > 0) + /* Skip over unchanged values. */ + curbit = sample[index / 8]; + curbit = (curbit & (1 << (index % 8))) ? 1 : 0; + if (snum_curr != 0 && prevbit == curbit) continue; + desc->last.logic = curbit; - /* Output timestamp of subsequent signal changes. */ - if (!timestamp_written) - g_string_append_printf(*out, "#%.0f", - (double)ctx->samplecount / - ctx->samplerate * ctx->period); + /* + * Queue, or immediately emit the text for + * the observed value change. + */ + if (ctx->immediate_write) { + g_string_append_c(*out, ' '); + s_val = *out; + } else { + s_val = queue_value_text_prep(ctx); + if (!s_val) + break; + } + format_vcd_value_bit(s_val, curbit, desc->name); + } - /* Output which signal changed to which value. */ - g_string_append_c(*out, ' '); - g_string_append_c(*out, '0' + curbit); - g_string_append_c(*out, '!' + p); + /* Advance to next set of logic samples. */ + snum_curr++; + sample += unit_size; + } + write_completed_changes(ctx, *out); + break; + case SR_DF_ANALOG: + *out = chk_header(o); - timestamp_written = TRUE; - } + /* + * This implementation expects one analog packet per + * individual channel, with a number of samples each. + * Lookup the VCD output channel description. + */ + analog = packet->payload; + count = analog->num_samples; + channels = analog->meaning->channels; + if (g_slist_length(channels) != 1) { + sr_err("Analog packets must be single-channel."); + return SR_ERR_ARG; + } + channel = g_slist_nth_data(channels, 0); + desc = NULL; + for (index = 0; index < ctx->enabled_count; index++) { + desc = &ctx->channels[index]; + if ((int)desc->index == channel->index) + break; + } + if (!desc) + return SR_OK; + if (desc->type != SR_CHANNEL_ANALOG) + return SR_ERR; + snum_curr = get_last_snum_analog(desc); + upd_last_snum_analog(desc, count); - if (timestamp_written) - g_string_append_c(*out, '\n'); + /* + * Convert incoming data to an array of single precision + * floating point values. + */ + floats = g_try_malloc(sizeof(*floats) * analog->num_samples); + if (!floats) + return SR_ERR_MALLOC; + rc = sr_analog_to_float(analog, floats); + if (rc != SR_OK) { + g_free(floats); + return rc; + } + + /* + * Check for changes in the channel's values. Have the + * sample number's timestamp and new value printed when + * the value has changed. + */ + for (index = 0; index < count; index++) { + /* Check for changes in the channel's values. */ + value = floats[index]; + changed = value != desc->last.real; + changed |= snum_curr + index == 0; + if (!changed) + continue; + desc->last.real = value; - ctx->samplecount++; - memcpy(ctx->prevsample, sample, logic->unitsize); + /* Queue, or emit the timestamp and the new value. */ + if (ctx->immediate_write) { + ts = snum_to_ts(ctx, snum_curr + index); + append_vcd_timestamp(*out, ts, FALSE); + s_val = *out; + } else { + queue_samplenum(ctx, snum_curr + index); + s_val = queue_value_text_prep(ctx); + } + format_vcd_value_real(s_val, value, desc->name); } + + g_free(floats); + write_completed_changes(ctx, *out); break; case SR_DF_END: - /* Write final timestamp as length indicator. */ - *out = g_string_sized_new(512); - g_string_printf(*out, "#%.0f\n", - (double)ctx->samplecount / ctx->samplerate * ctx->period); + *out = chk_header(o); + /* Push the final timestamp as length indicator. */ + snum_curr = get_max_snum_flush(ctx); + queue_samplenum(ctx, snum_curr); + /* Flush previously queued value changes. */ + write_completed_changes(ctx, *out); break; } @@ -246,13 +1178,26 @@ static int receive(const struct sr_output *o, const struct sr_datafeed_packet *p static int cleanup(struct sr_output *o) { struct context *ctx; + struct vcd_channel_desc *desc; if (!o || !o->priv) return SR_ERR_ARG; ctx = o->priv; - g_free(ctx->prevsample); - g_free(ctx->channel_index); + + if (with_pool_stats) + sr_info("STATS: alloc/reuse %zu/%zu, pool/free %zu/%zu", + ctx->alloced, ctx->reused, ctx->pooled, ctx->freed); + queue_drain_pool(ctx); + if (with_pool_stats) + sr_info("STATS: alloc/reuse %zu/%zu, pool/free %zu/%zu", + ctx->alloced, ctx->reused, ctx->pooled, ctx->freed); + + while (ctx->enabled_count--) { + desc = &ctx->channels[ctx->enabled_count]; + g_string_free(desc->name, TRUE); + } + g_free(ctx->channels); g_free(ctx); return SR_OK; @@ -261,7 +1206,7 @@ static int cleanup(struct sr_output *o) struct sr_output_module output_vcd = { .id = "vcd", .name = "VCD", - .desc = "Value Change Dump", + .desc = "Value Change Dump data", .exts = (const char*[]){"vcd", NULL}, .flags = 0, .options = NULL,