FAQs

If your testbench hangs up, the likely reasons are:

  1. A for all loop, with a control variable which has too many bits in it. If you are looping over all possible values of a 32-bit variable, for example, your computer is likely to fail before the loop terminates. Try to use these loops with only relatively small variables (possibly 16 bits, for example).
  2. Comparing a variable against a value that it cannot hold: if you check that a bit5 is less than 32, for example, then the loop will never terminate, because a bit5 has a maximum value of 31. You should normally use an int as the control variable when carrying out testing.
  3. Using an unsigned variable (a bit or a var) to control loop execution, when the termination condition expects a signed comparison. The simplest solution here is to again use an int as the control variable. See the example below.
This is an example of a case 3 infinite loop:

  bit16 j;
  for(j = 9; j >= 0; --j) {
    ... // infinite loop
  }

j is unsigned, and the >= carries out an unsigned comparison, so j >= 0 is always true, and the loop does not terminate. You should instead use one of these alternatives:

  int i;
  for(i = 9; i >= 0; --i) {
    // 'i' is a plain signed integer, so loop iterates 10 times
  }

  bit16 j;
  for(j = 9; j >=# 0; --j) {
    // j compared to 0 using a signed comparator, so loop iterates 10 times
  }

See also the 'Loop not executing' FAQ.

If your loop is not entered, and you think it should be, then you may have a problem with an unsigned comparison in your loop termination condition. Consider this code:

  bit16 j;
  for(j = -2; j < 2; j++)
    report("j is %x\n", j);

In this case, the loop index is an unsigned variable (the bit and var types are unsigned). If the default word size is unchanged at 32 bits, Maia will scan the (integer) constant '2' as 0x00000002. The unary minus is a two's complement negation operator; it is the programmer's way to tell Maia that 0xfffe should be assigned to j at the loop start. Since 0xfffe is already greater than 2, the loop body will never be executed.

This is therefore the same problem as the 'Infinite loop' FAQ. You should not, in general, use unsigned variables for loop control. For general housekeeping operations such as loop indexing, you should use the plain int datatype.

If you really have to use a bit or var in this case, rather than a plain int, then you must carry out a signed comparison. <# is the 2's complement comparator (it treats both operands as 2's complement integers). This is the corrected code:

bit16 j;
for(j = -2; j <# 2; j++)
   report("j is %x\n", j);

In this example, the loop counter is declared as a 3-bit var, to make it obvious how the value wraps around:

var3 i;
for(i = -2; i <# 2; i++)
   report("i is %x (%d)\n", i, i);

The output of this code is:

  i is 6 (-2)
  i is 7 (-1)
  i is 0 (0)
  i is 1 (1)

The "outputs" which are tested are any DUT ports or internal signals which appear on the right-hand-side of a drive statement; these may be outputs or I/Os. The tests are defined according to a stability window, which is set by the tOH and tOD specifications in the DUT section. tOH defaults to 0 if it not specified. Note that:

  • tOH is the output hold specification for an output, measured from a controlling input (normally a clock edge). The output must remain unchanged for at least the time given by tOH after the controlling input changes. If tOH is 1.50 ns, for example, and the simulator time step is 10 ps, then the output is out of specification if it changes at 1.49 ns, but is within specification if it changes at 1.51 ns. The output may change at exactly 1.50 ns and still be within specification (you should think in terms of an analog oscilloscope display when looking at tSU, tIH, tOH, and tOD specifications). Note that the Maia-generated testbench must be timing-aware in order to verify the output; it cannot simply sample at 1.49, 1.50, and 1.51 ns, because there is a race between the DUT and the testbench at the sample points.
  • tOD is the output delay specification for an output, measured from a controlling input (normally a clock edge). To be within specification, the output must change by at most tOD after the controlling input changes. If tOD is 2.00 ns, for example, then the output is within specification if it changes at 1.99 ns, but is out of specification if it changes at 2.01 ns. The output is within specification if it changes at exactly 2.00 ns. The TB must again be timing-aware to verify the output, and cannot simply sample it at 1.99, 2.00, and 2.01 ns.

If both tOH and tOD are non-zero, then the output (or I/O) is only expected to change at or after tOH has expired, and either at or before tOD. The output must be stable at all other times, and it must also have the expected value at all other times. Any violation of these requirements is reported as an error.

If tOH is zero, then the output may change at the same time as the controlling input, without an error being reported. This is the normal case for delta-delay simulations (although both tOH and tOD should be zero to qualify as 'delta delay').

Note that these tests are independent of whether a tested output is combinatorial or sequential.

You may have inadvertently used an integer constant or operation in your calculation. Consider this test program, which attempts to multiply 1.5 by 2.0 to get 3.0:

1 #pragma _DefaultWordSize 64 // required for compilation: see below
2 
3 void main() {
4    real2 y;
5    real2 x = 1.5;    report("x: %f (0x%x)\n", x, x);
6    y = 2 * x;        report("y: %f (0x%x)\n", y, y);  // wrong
7    y = 2 .F* x;      report("y: %f (0x%x)\n", y, y);  // wrong
8    y = 2.0 .F* x;    report("y: %f (0x%x)\n", y, y);  // right
9 }

The output from this program is:

  x: 1.500000 (0x3ff8000000000000)
  y: inf      (0x7ff0000000000000)
  y: 0.000000 (0x3)
  y: 3.000000 (0x4008000000000000)

In short, you will get the correct result only if all constants and operators are floating-point. Note that this particular program will not compile if the default word size is 32 bits, since line 7 attempts to combine a 32-bit and a 64-bit quantity using a 64-bit floating-point operator. Adding the pragma ensures that the constant '2' is treated as 64 bits, and allows the program to compile and run.

The number of vectors or drive statements executed is always correctly reported. However, each vector has zero or more outputs, and each output is tested separately. If the expected value is a don't care (in other words, it was omitted completely, or specified as '-'), then it does not affect the pass and fail counters.

Maia will otherwise carry out a number of separate tests on the output. If all of these tests pass for an output, then the pass counter will be incremented by one. Otherwise, the fail counter will be incremented by the number of failing tests.

Not yet. However, Maia looks a lot like C code, so a quick fix is just to turn on C or C++ mode in your editor. If you're using emacs, you can add this to the bottom of your source files:

// Local Variables:
// mode:C
// End:

A better solution is to edit your initialisation file to associate .tv files with C mode. This file will be ~/.emacs, ~/.emacs.d/init.el, or something similar. Add this to the end of your initialisation file:

;;; Maia
(setq auto-mode-alist (cons '("\\.tv\\'" . c-mode) auto-mode-alist))

There are 3 different sets of code which might need debugging:

  1. Your Maia source files. There isn't a debugger for Maia source code; if your problem is in the source, you'll have to use report and assert to track down any problems. Note that you don't have to run a full simulation with HDL source files when running Maia code: you can just compile and run the Maia source, or parts of it, by itself, if this helps.
  2. The Verilog testbench produced by mtv. However, you shouldn't need to debug the testbench code (which looks like compiler output). It's not deliberately obfuscated, but it's certainly difficult to read.
  3. Your RTL code.

rtv runs batch-mode simulations, and so is of no use if you need to run up a GUI to debug your own RTL code. In this case, you should create the testbench code by running mtv explicitly. If your source code is in testbench.tv, you can create testbench.v and testbench.vcd as follows:

$ mtv –vcd testbench.vcd testbench.tv testbench.v

Now simulate testbench.v together with your own RTL code; for example:

  $ vlog testbench.v mycode.v
  $ vsim work.top

The vcd file produced by mtv can be viewed with a vcd viewer, but it's clearly preferable to use a simulator with a GUI, if you have one. The testbench output will give you the simulation times at which your problem(s) occurred, together with the actual and expected values of your DUT outputs; this should be enough to track down any issues.

I use gtkwave on Linux occasionally, and it's generally sufficient for debug. Another alternative is dinotrace, although I haven't tried it myself.

You should also note that it's not difficult to get free (restricted) versions of commercial simulators. If your design exceeds the size limit on the free version of the simulator, you may still be able to use the waveform viewer. If you have ModelSim, for example, you can convert your .vcd file to a .wlf file using vcd2wlf, and load the wlf file into the waveform viewer.

There is only one enable signal for the bidirectional data to and from your DUT, and that is the enable signal that you defined in the DUT section. You can't turn off both data sources with a single enable: either the DUT is driving the bidirectional data, or Maia is (Maia knows that it is allowed to drive the data when your enable is turned off; that's what the create_enable statement is for).

If you want to confirm that the DUT output tristates, then you will need to have the bus as both an input and an output in a drive declaration, and you will need to explicitly drive the bus with Z. If Maia is 'driving' Z, and the DUT is not driving the data, then you will be able to read back Z's:

DUT {
   /* the DUT drives D when DEN is 1, and tristates D when DEN is 0. However,
    * 'create_enable' creates a *testbench* enable signal: in other words, the
    * TB drives D to the DUT when DEN is 0, and tristates D when DEN is 1 */
   create_enable D(!DEN)

   [DEN] -> [D]    // active-high enable for D
   [DEN,D] -> [D]  // the same, but drive D as well
}

[0] -> [.Z]        // fails: Maia drives X, D is reported as all X's
[0, .Z] -> [.Z]    // passes

See tutorial Exercise #8 for the use of the create_enable statement.

Some of these terms aren't well-defined, and others will disagree with these definitions.

'Unit delay' normally appears to refer to the historical practice of adding a #1 intra-assignment delay when coding a Verilog register:

always @(posedge clk)
  q = #1 d;

You should only find this in legacy code; it's a hangover from the days when Verilog didn't have non-blocking assignments, and this mechanism was used to avoid race conditions in synchronous designs. It can be rationalised as a mechanism to clean up waveform displays, but that doesn't change the fact that it's a Very Bad Thing.

Historically, however, 'unit delay mode' probably refers to Verilog-XL's +delay_mode_unit option. This replaced all explicit (non-behavioural) timing delays with a delay of 1 simulator time unit (the minimum timing precision).

'Zero delay', in common usage, probably means 'delta-delay'. Historically, it probably referred to XL's +delay_mode_zero option. This differed from +delay_mode_unit in that gate delays were set to zero, rather than 1. In both modes, behavioural delays were unaffected. If you had a methodology in which registers were assigned to behaviourally with a non-zero delay, and you enabled zero-delay mode, you therefore got a compiler-assisted mechanism to avoid race conditions in synchronous designs, which was essentially an alternative to the #1 methodology.

If you're using an event-driven simulator, rather than a cycle-based simulator, then all your simulations are delta-delay simulations. 'Delta delays' are the mechanism which the simulator uses to ensure correct ordering of the events that you request in your source code; it's not a methodology.

Untimed simulation is basically a misnomer, since there must be explicit times coded somewhere in a simulation. If there really were no explicit delays, then simulation would never advance from time 0. In common usage, however, 'untimed' probably refers to a simulation in which the RTL code has no explicit time delays, and the testbench introduces any required delays (the time between clock edges, for example). This is the normal way to carry out RTL simulations. The simulation works as expected because of the delta-delay mechanism, so this may also be referred to as a delta-delay simulation.

To start with: Maia doesn't care what's in the DUT. It could be 'normal' untimed RTL, or it could be a back-annotated netlist with timing information; or it could be something else. It makes no difference to Maia.

All Maia has to do is to drive the DUT inputs at specific times, and record the values of the DUT outputs, and the times at which they change; everything else looks after itself. If your DUT section specifies setup and hold times on inputs, or if it specifies hold times and output delays on outputs, then Maia will adjust its drive and sample times accordingly. The drive and sample times are set, if possible, to the worst-case values which will ensure that the DUT is within specification.

If you don't specify these times, then Maia will use defaults. The defaults use appropriate delays so that your waveform display, if you use one, will show something logical and easily understandable.

In other words:

  1. If your DUT contains timing information, then your simulator will carry out a 'timing simulation', whether or not you explicitly specify any timing information to Maia
  2. If your DUT doesn't contain timing information, then your simulator will do something else, whether or not you explicitly specify any timing information to Maia. If I was pressed, I might call this 'untimed', to distinguish it from (1).

If your Maia DUT specification contains timing data, I personally call that specification 'timed'. If it does not, I personally call it 'untimed'. This is simply because I haven't thought of better terms. These two terms have nothing to do with whether or not a 'timing' simulation is carried out.

Here are some good reasons for carrying out a timing simulation:

  • An STA run doesn't tell you that your chip will work. All it tells you is that your chip should work if, and only if, your constraints are correct. If there's an error in your constraints, then an STA run tells you nothing.
  • The only way to find out if the vendor believes that your chip will work is to carry out a timing simulation. This is never a guarantee, for lots of reasons, but it's the closest you'll get to a guarantee. You don't get this guarantee with STA, because STA relies on the correctness of your timing constraints.
  • You can't check asynchronous inputs with STA; you just instruct your analyser to ignore these inputs. In principle, you can check async inputs with a timing simulation (but, in practice, it's difficult).
  • If you run STA, then you need to check the timing reports, even if the summary tells you that all your constraints passed. For a real chip, there can be dozens or hundreds of these. If this is an ASIC and you're not doing the back-end work, then you may have trouble getting someone to actually generate all the reports you need. If you're running a timing simulation, with a good testbench, then the simulation just tells you itself whether or not it has passed.
  • Sometimes a timing sim will tell you something that STA won't tell you. I had a case a few years ago (in a structured ASIC) where a library PLL was "guaranteed by design", including the digital parts. The digital circuitry was false path'ed, and so missed STA. However, there was timing data in the sdf, and the timing sim failed. After a couple of days, the vendor admitted that the digital feedback divider didn't work, despite being "guaranteed by design".

On the other hand, timing simulations have their own problems, and may be unnecessary:

  • If you've (a) got an FPGA, and (b) it's completely synchronous, and (c) it has one input clock, and (d) you haven't added any additional clock buffers to, for example, drive off-chip RAMs, and (e) your STA tool lets you constrain input setup and hold times, and (f) you're confident that you can write the constraints without error, then carrying out a timing simulation is probably a waste of time. Note that FPGA timing analysers may not allow you to constrain input hold times.
  • A timing simulation is useless if your testbench can't provide worst-case stimulus to your chip. This is normally much harder than it sounds. It's not just clock frequency: you have to drive all inputs with the minimum allowed setup and hold times, and you have to sample all outputs immediately that they're guaranteed valid (or you have to correctly define a stability window and sample within the window). For a complex device, it can be next to impossible to do this manually (Maia automates this process). It may also be difficult to create specific test cases that result in worst-case timing. This is all automatic and trivial with (correct) STA.
  • sdf data may or may not be accurate. Vendors may specify a significant derating in their timing analyser (15% is not uncommon) to cover path-based uncertainty, OCV, and so on, and this may or may not be included in the sdf data.

To start with: you can always find out why mtv has reported an error by running up a waveform viewer, and finding out what's going on at the reported time. mtv reports the time of the error, and the expected and actual values of the offending output or inout. If you think the output is actually correct, then there's probably some confusion about the precise time at which mtv should sample the output. You will need to adjust the sample time by changing your waveform or timing declarations; this is covered in detail in the LRM.

If you set the MTV_LOGENABLE environment variable to 1 before running mtv, then your logfile (mtv.log, by default) will contain an event timing list for each drive declaration in your design (MTV_LOGENABLE must be either 0 or 1 if it is set; it defaults to 0). The input events are probably not of interest, since you can see these on the waveform display anyway; they're the times at which your DUT input signals are driven. The output events show the times at which the outputs are sampled. These are relative times, within your defined clock waveform, in units of the minimum timing precision.

Is your test vector trying to test both combinatorial and sequential signals in a single statement?

An example would be a synchronous counter with an asynchronous reset, or a RAM which is written synchronously and read asynchronously. If you're doing this, then you'll almost certainly get errors reported. You need one drive declaration for the asynchronous path, and another one for the synchronous path; see Exercise #7, and section 8.3.5 the LRM.

Is there a mismatch between the reported error time and the actual test vector that you consider to be in error?

Clocked test operations are pipelined, so that the clock can be kept running continuously; see the LRM for the details. The total elapsed period defined by a single test vector is actually longer than a single clock cycle. If the checker detects an error, it will correctly report the source line number of the vector which gave rise to the error, and the time of the error, but the time may appear to correspond to the next vector in the source file.

Do you have a glitch reported on an output on the vector before that output is preloaded?

A 'preload' operation is a mechanism which sets DUT state by forcing internal nodes with user-supplied data. Internal nodes are declared with a signal declaration; the DUT ports are declared with a module declaration. If you have a test vector which drives an internal node, then it is actually forcing the value on that node. See Exercise #9 for a preload example.

mtv can't tell what will happen within your DUT when you force an internal node. The change may immediately be visible to the testbench, or it may only happen at some later time (after a clock edge, for example). If a force in vector N does immediately change an output node, and that node is in the process of being tested by vector N-1, then you will have a problem. Remember that test operations are pipelined, and that you may therefore be forcing the output to change before the previous test has completed. If you have this problem, you should add a dummy vector which does not test the affected output:

[.C, -] -> [0]     // clock Q to 0
[.C, -] -> [-]     // don't test; the next vector glitches Q
[.C, 10] -> [10]   // force/preload Q to 10

The round2 function in this program rounds a time in picoseconds to the nearest tenth of a nanosecond (or a time in ns to the nearest tenth of a us, and so on). This example sets a timescale of nanoseconds, so the times are reported in nanoseconds and microseconds.

The timescale defaults to ns, so the entire DUT section can be omitted in this example (a DUT section is not required for compilation). However, the DUT section will be required if you want to change the default timescale.

Maia maintains a timescale internally by counting the number of decimal places appearing after any explicit times in the code (in this case, there is one decimal place, in '1499.9'). This value is therefore maintained internally as 14999, in a 64-bit variable, and is interpreted according to context. When it is required as an integer, it is rounded to the nearest integer in the timescale units (in this case, 1500). The value '1500' is therefore passed to 'round1' and 'round2'. When the time is printed as float in the example below (with %f or %T), it is instead displayed as 1499.9.

DUT {
   timescale ns;
}

void main() {
   wait 1499.9;
   report("time 1: %d\n",       _timeNow);                // prints 'time 1: 1500'
   report("time 2: %t\n",       _timeNow);                // prints 'time 2: 1500 ns'
   report("time 3: %6.1f\n",    _timeNow);                // prints 'time 3: 1499.9'
   report("time 4: %T\n",       _timeNow);                // prints 'time 4: 1499.9 ns'
   report("time 5: %d us\n",    round1(_timeNow));        // prints 'time 5: 2 us'
   report("time 6: %3.1f us\n", round2(_timeNow));        // prints 'time 6: 1.5 us'
}

bit64 round1(bit64 time) {                                // time passed in as 1500
   result = time + 500;
   result /= 1000;                                        // returns 2
}

real2 round2(bit64 time) {                                // time passed in as 1500
   bit64 t = (time + 50) / 100;                           // integer division
   return (real2)t .F/ 10.0;                              // real division; return 1.5
}

The mtv compiler carries out static type checking during analysis, to determine whether or not the program is valid. However, Maia is unusual, in that the level of type checking required is set by the programmer, with the _StrictChecking and _Implicits pragmas. _StrictChecking can be set to 0, 1, or 2, and defaults to Level 1; higher levels correspond to stricter checking. A program which compiles at level n is guaranteed to compile at any lower level. The checking level is set by a pragma, and not as a compiler switch, to ensure that a given program always compiles, irrespective of how it is compiled.

The levels are:

  • 0: this carries out a level of weak checking which would normally be associated with scripting languages. Variables do not have to be declared in advance, for example, and are created when they are first assigned to.
  • 1: this is essentially equivalent to C and similar languages. However, an exception is made to the extension and truncation rules when writing to, or reading from, DUT ports and signals. In these cases, the variable which is written to the DUT, or which is assigned to on a read, must be of the correct size. You can't write a 32-bit variable to 16-bit port, for example. FAQ #19 covers Level 1 in greater detail.
  • 2: this is stricter than level 1: bool, for example, is a real type, with stricter checking, rather than just a 1-bit 2-state integer (a bit1, which is the case for levels 0 and 1).

Section 3.1 of the LRM covers the differences in more detail.

So which level should you use? Some of the tutorials examples set _StrictChecking to 0, to demonstrate the use of implicit variables. None of them set it to 2, and most of them leave it at the default level of 1. My own view is that you should always use the default: it's too easy to make mistakes at Level 0, and unnecessarily complex at Level 2. Level 1 is intended to be a compromise which is similar to that used in real-world software, and which avoids the issues with both Verilog and VHDL.

Earlier versions of Maia had an additional level (3), with stricter checking which was essentially VHDL-like. However, this was of little use in practice, and was dropped. Stricter checking may (or may not, depending on your point of view) have benefits when describing hardware, but Maia is not an HDL, and is used simply to drive and check HDL models.

As an aside, you may be thinking that Verilog is "C-like", and so level 1 should be similar to Verilog. Verilog, in fact, bears no non-trivial resemblance whatever to C. "C-like" Verilog is simply a myth (along with "ANSI" ports). The best-selling Verilog book for many years even stated that Verilog "is similar in syntax to the C programming language", and that behavioral modelling is "very similar to C programming". It isn't, and Verilog is, for all intents and purposes, essentially untyped in the sense used by mainstream languages.

'Level 1' is the default type checking level. This is what you get if you do not explicitly change the checker level by adding a #pragma _StrictChecking statement to your code. Level 1 forms a set of rules for static type checking, and is fairly conventional: it is stricter than Verilog, is about the same as C and similar languages, and is less strict than VHDL.

In a typed language, the type rules determine what you can store in an object, and what the properties of that object are. From the point of view of hardware development, the most obvious feature of the type rules is how an object is extended or truncated when it is assigned to, or compared against, another object. This differs fundamentally between Verilog and VHDL. Verilog allows extension and truncation, but has a complex set of rules to determine exactly how this happens (basically, size and signedness flow up from the leaves of an expression, to the destination object, and back down to the leaves, potentially changing along the way). VHDL does not allow this; objects have to be correctly sized to start with. The rest of this FAQ covers the Maia rules for extension and truncation, for Level 1.

In general, the source expression will be extended or truncated as required, and the left-hand side (LHS) and the right-hand side (RHS) of an expression do not have to have the same size. However, there is an exception when accessing DUT ports and signals. Note also that the size of the destination (the LHS) is only used to determine by how much the source (the RHS) should be extended or truncated. This is conventional: Verilog is unusual (and perhaps unique) in that the destination size is also used to determine operator sizes.

This exception requires expressions to be correctly sized when accessing the DUT. This is intended to address a particular issue with Verilog, which is that there is no port size checking (Verilog does not check port directions either; Maia does). This program compiles with the errors shown:

DUT {
   module adder(input[15:0] A, B, output[15:0] C);
   [A,B] -> [C];
}

void main() {
   bit32 in1;
   bit16 in2;
   int   x;                   // defaults to 32 bits

   in2 = in1;                 // Ok: can assign  32-bit object to 16-bit object
   in1 = in2;                 // Ok: can assign  16-bit object to 32-bit object
   assert(in2 == in1);        // Ok: can compare 32-bit object to 16-bit object

   // drive DUT ports
   A = 0;                     // error: can't assign 32-bit object to 16 bit signal
   A = 32'habcd;              // error: can't assign 32-bit object to 16 bit signal
   A = 16'habcd;              // Ok

   [x,   in2] -> [];          // error: can't assign 32-bit object to 16 bit signal
   [in1, in2] -> [];          // error: can't assign 32-bit object to 16 bit signal
   [in2, in2] -> [];          // Ok

   // test DUT ports
   assert(C == x);            // error: can't compare 32-bit object (x) to 16-bit signal (C)
   [-, -] -> [x];             // error: can't compare 32-bit object (x) to 16-bit signal (C)
}

Occasionally, it can get to be rather tedious to ensure that all the port sizes are correct. There are therefore two additional rules which relax the port size checking requirements ('exceptions to the exception'). These apply only when using drive statements; they do not apply when driving or reading the ports directly. The exceptions are:

  1. It's Ok to use an unsized constant on either the LHS or the RHS of a drive statement. This means that you can use the literal 0, for example, rather than 84'h0. You can use a plain integer, or a Verilog-style unsized constant ('d10, for example).
  2. You can use an object slice on the LHS or the RHS, as long as the compiler can determine what the indexes are during compilation (the indexes are static, and do not vary at runtime), and the slice size is correct for the port. This may not appear to be an exception, and may seem obvious, but the issue here is that a slice of an object has the size of that object, and not the size of the slice. A 4-bit slice of a 64-bit object is itself a 64-bit object, for example, with the required 4 bits at the bottom of the 64-bit object. However, this slice may be used to drive a 4-bit input port, and can be compared against a 4-bit output port.

Either.

Integers may be specified in C-like ('Cinteger'), or Verilog-like ('Vinteger'), form. Vintegers have a size prefix, which may be either an apostrophe (U+0027), or a grave accent (U+0060):

  bit16 a = 16'habcd;                     // apostrophe
  bit16 b = 16`habcd;                     // grave accent/back-tick

The apostrophe character is supported for Verilog compatibility. However, this character will cause problems for tools which expect C-like code (editors, for example), and the back-tick can be used as an alternative.

Neither.

Objects of these types just hold aribtrary data patterns, in the same way that a register, or a memory location, simply holds an arbitrary data pattern. The data is not signed, or unsigned, or integer, or floating-point, or anything else. It is just data. However, in common usage, this generally means 'unsigned', and bit and var are normally informally referred to as 'unsigned'.

Complexity is provided by operators in Maia, and not types. Consider this program:

void main() {
   bit8 a = 8`hfc;
   bit8 b = 8`h04;

   report("a greater than b, unsigned: %l\n", a >$8  b);  // %l displays bools
   report("a greater than b, signed:   %l\n", a >#$8 b);
}

This produces:

a greater than b, unsigned: true
a greater than b, signed:   false

> is the 'greater than' operator. In this case, it is explicitly sized to 8 bits, by appending $8. If the # character is present, the operator considers its inputs to be two's complement; it truncates or sign-extends the inputs to 8 bits (if necessary), and then carries out a two's complement comparison (in which case, -4 is not greater than +4). If the # character is not present, the operator considers its inputs to be unsigned. They are truncated or zero-extended to 8 bits (if necessary), and an unsigned comparison is carried out (in which case, 252 is greater than 4).