1.Overview
TCML (Timing Chart Markup Language) is a small text language for describing timing charts. With a simple ASCII-style notation it can express signal Low / High / bus / don't-care levels, transitions, arrows between signals, automatic clock expansion, and more.
This implementation builds on the prior art listed below. The author owes much to these works:
For polished, publication-quality figures use WaveDrom. Charts produced by this tool are not intended for datasheets.
That said, WaveDrom's notation is a bit involved and not the easiest thing to write while you are still thinking. The TCML-style notation pioneered by Prof. Kumagai (Tohoku Gakuin University) and Prof. Takeuchi (University of Tsukuba) is much friendlier in the thinking-and-sketching phase.
This tool was a long-weekend project to scratch the author's own itch from a few years ago, when designing hardware that could have used a notation like this. The author no longer does that kind of design, so honestly it isn't even for the author anymore.
2.Getting started
Each timing line has a signal name on the left and a level string on the right, separated by whitespace.
_ is Low, ~ is High, = is Bus, and ? is don't-care. Feed the source below to the tchart CLI and you get the SVG shown to the right.
@step 8
@slant 2
@bgcolor0 #f8f8ff
@bgcolor1 #f0f0f0
Clock _~_~_~_~_~_~
"Data
-Data" =<D0>====X<D1>====X<D2>====
Enable ____~~~~____
Output _______~~~~~____
3.Line types
| Kind | Leading char | Description |
|---|---|---|
| Comment | # | Ignored. |
| Parameter / directive | @ | Either a parameter setting, a row directive (@title / @skip / @clock / @->), or a per-signal attribute (@signal). |
| Overlay text | % | Places a text label at the given chart coordinates. |
| Timing line | Anything else | A signal name followed by a level string. |
4.Signal names
Any valid UTF-8 string. Control characters are not permitted (with \n as the only exception inside quoted names). Empty names are rejected.
Multi-line names
Surround a name with " to embed newlines:
"Data
Bus" =<D0>====X<D1>====
- The opening
"must sit at the start of the line. - The closing
"is followed by whitespace and then the level string.
Escapes (only inside "...")
| Sequence | Meaning |
|---|---|
\" | Literal " |
\n | Newline |
\\ | Literal \ |
5.Level symbols
| Symbol | Meaning | Shape |
|---|---|---|
_ | Low | Single line at the bottom |
~ | High | Single line at the top |
- | HiZ | Dashed line in the middle |
= | Bus | Two parallel rails |
? | Don't care | Filled hatch with a line at the previous level's position |
Example
@fontsize 14
@step 12
@slant 2
Low ________
High ~~~~~~~~
HiZ --------
Bus =<A>====
DontCare ___?????
Repeated identical symbols (__, ~~, ??, …) are merged into one segment by the parser.
6.Auxiliary symbols
| Symbol | Meaning | x advance |
|---|---|---|
: | Gap (one unit of whitespace, breaks signal continuity) | step |
X | Bus value change (BusCross) | step (cross part slant + body part step - slant; or body only of width step when the cross is omitted at the start of a signal) |
? | Don't-care marker (paints surrounding bus segment as don't-care) | 0 |
| | Vertical guide line | 0 |
[ / ] | Highlight start / end | 0 |
@{name} / @N | Anchor | 0 |
X is composed of two parts: a cross transition followed by a body (one bus unit at the new value). It occupies the width of one level char (= step); the cross gets slant, the body gets step - slant. When there is no preceding bus signal (e.g. at the start of a signal line) the cross is omitted and the body alone takes the full step.
? is a zero-width marker. It paints the surrounding contiguous level segment as a don't-care region.
Example
@fontsize 14
@step 12
@slant 2
Gap ____:____
BusX =<A>====X<B>====
Guide _~__|__~_
Highlight __[~~~~]__
7.Don't care ?
The line position drawn inside a don't-care region is determined by the preceding level (resolved at parse time).
| Preceding anchor | Line drawn inside the region |
|---|---|
_ Low | Bottom |
~ High | Top |
- HiZ | Middle |
= Bus | Bus envelope (top + bottom) |
X | Bus envelope |
Don't care fill shapes inside a bus
? in a bus context (e.g. =?=) is filled with a polygon shaped to fit the surrounding waveform boundaries. It never extends past the full signal_box height.
@fontsize 14
@step 12
@slant 3
@title "DontCareAlongBus shapes"
# `@dontcare_color` can be flipped between rows to change the hatch color.
# (Cycle through #bbb / #c00 / #06c / #080 below.)
# Default color (#bbb)
=?= ====?====
@dontcare_color #c00
# Both sides Low: /=\ shape (red)
_=?=_ ____====?====____
# Both sides High: \=/ shape (red)
~=?=~ ~~~~====?====~~~~
@dontcare_color #06c
# Low only on the left (left slant, right vertical) (blue)
_=?= ____====?====
# Low only on the right (left vertical, right slant) (blue)
=?=_ ====?====____
@dontcare_color #080
# Mixed High / Low on either side (green)
~=?=_ ~~~~====?====____
# HiZ on the left, bus continues on the right (green)
-=?= ----====?====
# Bus + label (green)
=?=L ==<A>==?====
# Bus (green)
=?=X ==X==?==X==
Error conditions for ?
- If a signal line begins with
?, or only zero-width elements (:,|,[,],@{...},@N) precede it, the parser raisesParseError::DontCareWithoutAnchor. - For example:
foo ?==,bar ???,baz ?_~,qux :?_~,quux @{a}?_~are all errors.
X / X? patterns
| Pattern | Interpretation |
|---|---|
=X= | Bus(1) + X (cross + body, new value) + Bus(1) |
=X? | Bus(1) + X (cross + body) + ? (X body becomes don't-care) |
=X?= | Don't-care region = X body + the trailing = (2 units total) |
=?X= | Don't-care region = the leading = (1 unit); the X body and trailing = are bus at the new value |
=X?X= | Don't-care region = X1 body of 1 unit; the polygon spans X1 cross-midpoint to X2 cross-midpoint (hexagonal) |
~X_ | High + BusOpen + Bus(1, X body) + BusClose + Low (X is valid even when the neighbours are non-bus) |
XXXX | A run of X's at the start of a signal. The first X drops its cross; subsequent X's behave as ordinary BusCross. |
?X= | Error (leading ?) |
8.Labels in level strings <...>
Attach a text label to a level segment or a transition.
@step 10
@slant 2
BusLabel =<A>====X<B>====X<C>====
LowLabel ____<L>____
HighLabel ~~~~<H>~~~~
HiZLabel ----<Z>----
- Labels cannot contain control characters (other than
\n). - Escape with
\<for<,\>for>, and\\for\.
9.Anchors @{name} and arrows @->
An anchor is a zero-width marker that records a particular point on a waveform. An @-> line connects two anchors as an arrow. Anchors alone draw nothing; a line is rendered only when an arrow references them as an endpoint.
Minimal example
@fontsize 14
@step 10
@slant 2
Req ___@{s}~~~~~~
Ack ________@{a}~~~
@-> (@{s}, @{a}, red) request
Anchor rules
- Use
@{name}or@N(a positive integer). Anchors are zero-width and do not advance x. - Named and numbered anchors live in separate namespaces (
@{1}and@1are different). - Defining the same id twice yields
ParseError::DuplicateAnchor. - The character class for
AnchorNameis[A-Za-z_][A-Za-z0-9_-]*. - Anchors are not transparent for the
?lookup: an anchor by itself does not become the level used to determine a following?.
Arrow syntax
@-> (<from>, <to> [, <attribute>, ...]) [<text>]
| Category | Examples | Disambiguation |
|---|---|---|
| Color | red, #f0f, #ff8800 | Anything Color::parse accepts |
| Width | 2, 2px, 1.5px | Numeric (the px suffix is optional) |
| Style | solid, dashed, dotted | Keyword |
| Arrowhead | head=end, head=both, head=none | head= prefix |
Defaults: color = signal_color, width = 1px, style = solid, arrowhead = end. Attributes can appear in any order; specifying two attributes from the same category raises ParseError::DuplicateArrowAttribute.
Combining color, width, style, and forward references
@step 10
@slant 2
@bgcolor0 #f8f8ff
@bgcolor1 #f0f0f0
Req ___@{s}~~~~@{e}___
Ack _______@{a}~~~~
@-> (@{s}, @{a}) request
@-> (@{e}, @{a}, dashed) ack
Bus =<A>====@1X<B>====@2X<C>====@3
Flag ____@4~~~~@5___
@-> (@1, @4, red, 2px) A
@-> (@2, @5, blue, head=both) B
@-> (@3, @4, green, dotted) forward ref
Data __@{d1}~~~~@{d2}___
Out ___@{o1}~~~~@{o2}__
@-> (@{d1}, @{o1}, #ff8800, solid) start
@-> (@{d2}, @{o2}, dashed, head=none) end
- Forward references are allowed; an
@->line may appear anywhere in the file. - Style values such as
fontorsignal_colorare taken from the local settings in effect at the position of the@->line. - Referring to an undefined anchor produces
ParseError::UndefinedAnchor. - The renderer does not auto-route arrows to avoid overlap (that's the author's responsibility).
10.Parameters
Syntax: @<parameter-name> <value>. Names are case-insensitive and treat - and _ as equivalent (@fontsize, @font-size, and @FONT_SIZE are all the same).
Global parameters (cannot be changed mid-file)
| Name | Default | Description |
|---|---|---|
fontsize | 14 | Font size (px). The layout reference unit. |
lineheight | 1.2 | Multiplier for waveform row height (= fontsize × lineheight). |
capwidth | 0 | Width of the signal-name column (px). Auto-computed when 0. |
namepad | 8 | Gap between the right edge of the name and the left edge of the waveform (px). |
scale | 1.0 | Overall SVG scale factor. |
page-margin | 10 | Fixed margin around the chart (px). |
bgcolor0 | none | Background colour for even rows. |
bgcolor1 | none | Background colour for odd rows. |
Local parameters (may be changed mid-file; new value applies from that point onward)
| Name | Default | Description |
|---|---|---|
step | 10 | X advance per level char (px). When there is a preceding transition, that transition is rendered as the leading slant portion of this step. step <= slant is a parse error. |
slant | 2 | Transition width (px). Set to 0 for vertical edges. Applies to SingleEdge / BusOpen / BusClose / BusCross alike. |
h_space | 4.0 | Total vertical padding for a signal row (px). The legacy name signal_gap is also accepted. |
font | sans-serif | Font family. Quote with " if it contains spaces; comma-separated lists are honoured as fallback chains. |
signal_color | black | Signal line colour. |
signal_width | 1 | Signal line width (px). |
guide_color | red | Vertical guide line colour. |
guide_width | 0.6 | Vertical guide line width (px). |
bg | none | Background colour for the next row only (local override). |
highlight_style | fill="#ff8" stroke="none" | Highlight rectangle style. |
dontcare_color | #bbb | Hatch colour for ?. A single colour value such as @dontcare_color #c00 applies from that point onward; redeclare to switch again. |
titlealign | center | Horizontal alignment for @title (center / left / right). |
clockmark_position | 0.5 | Position of the clock triangle marker's apex along the edge (0.0..=1.0). |
clockmark_height | 5 | Clock marker height (px). |
clockmark_width | 4 | Clock marker base width (px). |
clockmark_color | Inherits signal_color | Clock marker fill colour. Inherits the current signal_color when unset. |
overline_gap | 2 | Gap between the overline and the cap-top of the signal name (px). |
overline_thickness | 1 | Overline thickness (px). |
11.Background colour
The global @bgcolor0 / @bgcolor1 alternate row colours, while the local @bg overrides only the next row.
@fontsize 14
@step 10
@bgcolor0 #eef6ff
@bgcolor1 #fff4ee
A _~_~_~_~
B ~_~_~_~_
@bg #ffe4cc
Local _~_~_~_~
After _~_~_~_~
@bgis consumed by the next row (Signal / Skip / Title) and then resets.@bg nonediscards the pending value explicitly.- Rows that have a pending
@bgdo not also paintbgcolor0/1. SkipandTitlerows are excluded from the even/odd counter forbgcolor0/1.
@highlight_style / @dontcare_color
@highlight_style fill="#8f8" stroke="green" stroke-width="1"
@dontcare_color #c00
@highlight_style takes whitespace-separated key="value" SVG attributes. @dontcare_color takes a single colour value (same notation as @bgcolor0 &c.) and switches the hatch colour from then on.
12.@skip — blank rows
@step 10
@slant 2
@bgcolor0 #f8f8ff
@bgcolor1 #f0f0f0
Clock _~_~_~_~_~_~
@skip(1)
Data =<A>====X<B>====
@skip(2)
Control ____~~~~____
@skip(0.5)
Flag ~~____~~____
@skip(20px)
Out _~~~~___~~~~
- A bare number is interpreted as
lh(line-height units). - Append
pxto specify pixels instead. - Negative or unparseable values raise
ParseError::InvalidSkipAmount. - Zero is allowed, but no
SkipRowis emitted.
13.@title — title rows
@step 10
@slant 2
@bgcolor0 #f8f8ff
@bgcolor1 #f0f0f0
@title "Default Center Title"
A _~_~_~_~
@titlealign left
@title "Left Aligned"
B _~_~_~_~
@titlealign right
@title "Right Aligned"
C _~_~_~_~
@titlealign center
@title "Back to Center"
D _~_~_~_~
- The argument string is rendered as a title row. Multi-line titles are quoted with
"..."following the same escape rules as signal names. @titlemay appear multiple times in a single file.- Title rows are excluded from the even/odd counter for
bgcolor0/1. @titlealigntakescenter/left/rightand applies to every@titleemitted after it.
14.@clock — automatic clock expansion
Treats the next signal row as a clock. If the body is empty or partial it is padded out from the last state, and triangle markers are placed automatically on the rising / falling edges.
@fontsize 14
@step 10
@slant 2
@clock(pos)
ClkPos _~_~_~
@clock(neg)
ClkNeg _~_~_~
@clock(both)
ClkBoth _~_~_~
@clock(pos, _=2, ~=1)
ClkWide
Syntax: @clock(<edge> [, _=<n>] [, ~=<n>] [, start=<low|high>] [, mark_position=<f32>] [, mark_height=<px>] [, mark_width=<px>] [, mark_color=<color>])
| Attribute | Value | Description |
|---|---|---|
edge | pos / neg / both / none | Where to place triangle markers (required). |
_=<n> | Positive integer | Time units in Low (default 1). |
~=<n> | Positive integer | Time units in High (default 1). |
start | low / high | Initial phase (default low). |
mark_position | 0.0..=1.0 | Position of the marker's apex. |
mark_height | Positive value | Marker height. |
mark_width | Positive value | Marker base width. |
mark_color | Colour | Fill colour (inherits signal_color when unset). |
- Attributes may appear in any order. Keys are case-insensitive, and
-/_are equivalent. - Clock markers are completely independent of
@->arrows; clock-derived markers never leak into Arrow output.
15.@signal — per-signal attributes
Applies an attribute to the next signal row only. The attribute resets immediately after that row. Currently only overline is provided.
@step 10
@slant 2
@bgcolor0 #f8f8ff
@bgcolor1 #f0f0f0
@signal(overline)
nReset ~~~~__~~~~
@signal(overline)
nWrite ~~__~~__~~
Enable ____~~~~____
@signal(overline)
"nChip
Enable" ~~~~____~~~~
Out __~~~~____~~
overline: draws an overline above the signal name (active-low convention). For multi-line names, only the topmost line gets the overline, with width fixed to the longest line.- Position and thickness are controlled by
@overline_gap/@overline_thickness. The SVG output is an explicit<line>element rather than atext-decorationattribute.
16.% — overlay text rows
% <x> <y> <text>
Places a text overlay at the given pixel coordinates. The origin is the chart's top-left corner.
17.key=value rules
Every = attribute in TCML (the options to @clock(...), head= for @->, @highlight_style, …) follows the same rules.
- Whitespace around
=is optional:key=value,key =value,key= value, andkey = valueare equivalent. - Both key and value have leading/trailing whitespace stripped before evaluation.
- Use
"..."when the value needs to contain spaces.
18.Error reference
Every error is reported with a line and column.
| Error | Cause |
|---|---|
| ParseError::DontCareWithoutAnchor | No level anchor precedes ?. |
| ParseError::DanglingBusTransition | X is not surrounded by Bus / ?, or appears alone at the start of a row. |
| ParseError::InvalidSkipAmount | @skip argument is negative or unparseable. |
| ParseError::DuplicateAnchor | The same AnchorId is defined more than once. |
| ParseError::UndefinedAnchor | @-> references an undefined anchor. |
| ParseError::DuplicateArrowAttribute | @-> has multiple attributes from the same category. |
| ParseError::UnknownArrowAttribute | @-> contains an attribute token that cannot be classified. |
| ParseError::UnknownParameter | Unknown @<name> parameter. |
| ParseError::InvalidColor | Color::parse failed. |
| ParseError::InvalidSignalName | Signal name contains a control character or is empty. |
| ParseError::InvalidLevelChar | Unknown level character. |
| ParseError::UnclosedHighlight | [ is not closed by ]. |
| ParseError::UnopenedHighlightEnd | ] has no matching [. |
| ParseError::UnclosedQuote | "..." is not closed. |
| ParseError::UnclosedLabel | <...> is not closed. |
| ParseError::ClockBodyConflict | The body of the row following @clock contradicts the clock expansion. |
19.Handing off to WaveDrom
tchart is a small tool for jotting down timing diagrams alongside your thoughts while editing. When the diagrams need to ship — datasheets, slides, papers — please switch to WaveDrom, a real timing-chart tool. tchart is not a true companion for the final stage of your work.
The work you have already done is not wasted. The tchart wavedrom subcommand converts a .tc file to WaveJSON. The mapping is approximate, not exhaustive: only constructs WaveDrom can render (signal levels, bus data, clocks, anchors / arrows, …) are translated, and styling that has no WaveDrom equivalent (background colour, fonts, overlines, highlights, …) is silently dropped. Carry your TCML thinking into WaveDrom and continue the journey there.
tchart wavedrom chart.tc # → chart.json
tchart wavedrom chart.tc -o out.json
Three example cases are shown below. Each lists the TCML input, the tchart svg rendering, the tchart wavedrom WaveJSON output, and the wavedrom-cli rendering of that JSON, side by side. The WaveJSON shown is generated by tchart-core/examples/wavedrom_help_demo.rs.
Case 1: breaking continuity with Gap (:)
TCML's : Gap is a one-unit blank that breaks signal continuity. It maps to WaveDrom's |, which extends the previous level by one unit while drawing a visual break. The pictures are not identical, but the “continuity is broken here” semantic is preserved.
TCML input
@title 連続性の断絶
sig1 ~_~_:~_~_
sig2 ====:====
TCML rendering (tchart svg)
WaveJSON output (tchart wavedrom)
{
"head": { "text": "連続性の断絶" },
"signal": [
{ "name": "sig1", "wave": "1010|1010" },
{ "name": "sig2", "wave": "=...|=..." }
]
}
WaveDrom rendering (wavedrom-cli)
Case 2: changing bus values (X = BusCross)
An X splits the bus segment, so each side of X becomes its own = segment in the WaveDrom output, and a corresponding data entry is generated for each. This expresses the typical “bus value changes here” pattern.
TCML input
@title バス値の切替
clk ~_~_~_~_
data ==A=X=B=X=C
TCML rendering (tchart svg)
WaveJSON output (tchart wavedrom)
{
"head": { "text": "バス値の切替" },
"signal": [
{ "name": "clk", "wave": "10101010" },
{ "name": "data", "wave": "=..=..=.",
"data": ["A", "B", "C"] }
]
}
WaveDrom rendering (wavedrom-cli)
Case 3: anchors @{name} and inter-signal arrows @->
Embed zero-width anchors (e.g. @{request}) in the waveform and connect any two with @->. Spread anchors across multiple signals to express dependencies that span signals (e.g. request → ack → complete). In the WaveDrom output, anchors map to per-signal node strings (same length as the wave; an anchor character at the anchor position, otherwise .) and a top-level edge array.
Some information is lost in the conversion. WaveDrom node identifiers are single characters (a–z, A–Z, at most 52 of them), so any descriptive anchor name on the TCML side (such as @{request}) is replaced by a single character assigned in order of appearance (e.g. a) and the original name cannot be recovered. If there are more than 52 anchors, the surplus arrows are dropped and a warning is printed to stderr. Arrow colour, width, and style nuances are also dropped (WaveDrom has no equivalent); only the existence of the arrow, its endpoints, and its label survive.
TCML input
@title 信号間にまたがる矢印
clk ~_~_~_~_
req _@{request}~~~~~~_
ack ___@{ack_received}~~~~_
done ______@{complete}~_
@-> (@{request}, @{ack_received}) ack
@-> (@{ack_received}, @{complete}) done
TCML rendering (tchart svg)
WaveJSON output (tchart wavedrom)
{
"head": { "text": "信号間にまたがる矢印" },
"signal": [
{ "name": "clk", "wave": "10101010" },
{ "name": "req", "wave": "01.....0", "node": ".a......" },
{ "name": "ack", "wave": "0..1...0", "node": "...b...." },
{ "name": "done", "wave": "0.....10", "node": "......c." }
],
"edge": ["a->b ack", "b->c done"]
}