Skip to content
VLSI Mentor

C Part II · Module 16 · Registers and Memory-Mapped I/O

The Hardware/Software Contract

A datasheet is a contract, and like the C standard it has four categories rather than two: things it guarantees, things it says the implementation decides, things it is simply silent about, and things it forbids. Software that cannot tell these apart encodes the behaviour of one particular chip on one particular day and calls it a driver. This chapter is Chapter 13.3's distinction applied to a hardware specification instead of a language standard — the same skill, a different document — with a test for each category and the measured cases from this module sorted into them.

Advanced21 min readCPart IIHardwareRegisters

Part II · Module 16 · Chapter 16.8 · The Hardware/Software Contract

1. Two Documents, One Skill

Chapter 13.3 established that the C standard does not divide behaviour into works and does not work. It has four categories — guaranteed, implementation-defined, unspecified, undefined — and the entire discipline of Part I rested on telling them apart, because what you may rely on differs in each.

A datasheet is the same kind of document and has the same structure. It guarantees some things outright, delegates others to the particular part you have, is silent on a great deal, and explicitly forbids a few. And the failure mode is identical: software that cannot tell the categories apart ends up encoding what this chip did on this day as though it were what the device promises.

That is the whole chapter. The categories are in §3, the tests for applying them in §4, and the reason it matters more here than it did in Part I is that a datasheet has no committee, no sanitizer, and no -Wpedantic.

2. What a Specification Is For

A specification exists so that two teams can work independently. The hardware designer needs freedom to change the implementation — a different FIFO depth, a faster state machine, a new silicon revision — without breaking your driver. You need to write a driver before the silicon exists and have it work on the part that eventually arrives.

What makes that possible is precisely the gap between what is promised and what is implemented. The promise is narrower than the behaviour, deliberately, and everything in the gap is the designer's room to move.

So when you write code that depends on something in the gap, you have not found a clever shortcut. You have taken the designer's freedom away without telling them, and the bill arrives at the next revision — typically after your code has shipped and the person who wrote it has moved on.

Chapter 13.2 made the same argument for undefined behaviour in C: the compiler accepting your program is a statement about its syntax, not about its meaning. Here it is one level out: the device behaving correctly is a statement about this part, not about the specification.

3. The Four Categories

Guaranteed. The document states it as a requirement. CTRL is at offset 0x04; it is read-write; bits 15:4 are the divisor. Rely on this freely — it is the reason the document exists.

Implementation-defined. The document says the implementation decides and documents its choice. Reset values are the common case: the specification for a device family may say "reset value is device-specific, see the part datasheet". Rely on this part's documentation, write down that you are doing so, and re-check it on a new part. This is exactly Chapter 13.3 §7's protocol: write it down, check it, confine it, revisit on a new target.

Unspecified. The document is silent. What a reserved bit reads as. How long an operation takes when the document gives no timing. What happens to the second byte in a FIFO when you read the first. You will observe a value, and the value is not a promise. Silence is not permission.

Forbidden. The document says do not do it. "Do not write to this register while the enable bit is set." "Access must be 32-bit." "Do not read this register in an interrupt context." Doing it anyway is the hardware analogue of undefined behaviour: the device may do what you expected, or something else, or something else next week.

The categories map so closely onto Chapter 13.3's that the mapping is worth stating outright:

C standard (13.3)DatasheetWhat you may do
guaranteed by ISO Cstated as a requirementrely on it
implementation-defined"device-specific, see part datasheet"rely on this part, write it down, re-check
unspecifiedthe document is silentobserve it; never depend on it
undefinedexplicitly forbiddendo not

4. Four Tests You Can Actually Apply

"Read the datasheet carefully" is not advice. These are the four questions that do the work.

Is this sentence in the document, or did I infer it? The most common failure is not misreading a sentence but supplying one. "STATUS bit 0 means data available" is in the document. "STATUS bit 0 clears when I read DATA" may not be — it may be something you noticed. Go and find the sentence; if there is none, you are in the unspecified category.

Would the designer consider this a bug if it changed? This is the sharpest test, because it is the one the designer themselves would apply. If a future revision made reserved bit 3 read as zero instead of one, would that be an erratum or a legitimate change? If it would be legitimate, you may not depend on the current value — and Chapter 16.4's register comes out of reset with exactly that bit set.

Did I learn this from the document or from the device? Measured behaviour is evidence about one part. The document is evidence about the family. When they agree, cite the document. When the document is silent, you have an observation, and observations belong in comments, not in logic.

What happens on the next silicon revision? The practical version of the second test. Most of these bugs are found at a revision boundary, which is also the worst possible time.

5. What the Contract Covers, and What It Does Not

A register specification tells you what happens at each address. Three things it typically does not tell you, and each is a source of long-lived bugs.

Timing. Writing CTRL does not mean the device has reconfigured by the time the next instruction runs. Many devices need cycles, or require you to poll a status bit. If the document gives a figure, it is a guarantee and you must honour it; if it is silent, you may not assume immediate either. The cost of a wrong assumption here is an intermittent fault that gets worse when the code gets faster.

Ordering between accesses. You wrote the enable before the start command. That the processor issues them in that order — let alone that they arrive at the device in that order — is not something C's volatile promises beyond the compiler's own behaviour. Chapter 16.3 §7 flagged this and Chapter 17.6 is where the mechanisms live. For now: the order you wrote is a statement about your source, not about the bus.

What happens while a transaction is in flight. Reading a status register during a transfer may give you a snapshot, a stale value, or a legal-but-unhelpful intermediate state. Unless the document says the register is coherent at any time, assume it is not.

The practical defence for all three is the same, and it is cheap: write down the assumption where the code makes it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* datasheet rev C §7.3: CTRL takes effect within 3 PCLK cycles.
   Caller must not issue a transfer until then. */
static void uart_set_divisor(uintptr_t base, uint32_t divisor);

A comment naming the section is checkable by the next reader; a silent assumption is not.

6. The Misconception: It Works on My Board

"I tested it on the hardware and it works, so it is correct."

This is Chapter 13.2's thesis wearing different clothes, and it is more seductive here because the evidence feels stronger. A C program that works might still be undefined; a device that works has been physically observed doing the right thing, with an oscilloscope if you like.

But the observation has exactly the same scope as before. You have evidence about this part, this revision, this temperature, this timing, this compiler's instruction scheduling — and the driver will run on parts you have not seen.

Three concrete ways the gap shows up, all of which are in this module:

A reserved bit you preserved by accident. Chapter 16.4's register has bit 3 set at reset. Code that read-modify-writes preserves it without ever knowing it exists, and works. Code that rebuilds the word clears it, and also works — on a part where bit 3 happens to do nothing yet. The two are indistinguishable by testing until the revision that gives bit 3 a meaning.

An access width that happened to be accepted. Chapter 16.3 §2 measured an 8-bit write to a 32-bit register updating only the low byte. On that model it was accepted. On a part that requires 32-bit access it would fault or be ignored — and the code that did it would be unchanged.

A read you got away with. Reading a read-to-clear register in a debug print works fine until the day the FIFO is non-empty at that moment. Chapter 16.6 §3 measured the mechanism; the symptom is Chapter 16.9's "data goes missing under load", and it appears only under load.

The corrected version is one sentence:

Testing tells you the code is not obviously wrong on this part. The document tells you whether it is right.

Both are necessary. Neither substitutes for the other, and the one people skip is the cheap one.

7. Writing the Contract Down

Everything above reduces to a habit that costs minutes and is skipped almost universally: put the document's promise next to the code that relies on it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* --- UART0, datasheet rev C §12.4 ------------------------------------ */
#define UART0_BASE   UINT32_C(0x40021000)   /* §12.1 memory map          */
#define UART_CTRL    0x04u                  /* RW, 32-bit access ONLY    */
#define UART_IRQ     0x0Cu                  /* W1C — write only the bit  */
#define UART_DATA    0x10u                  /* RC — reading pops the FIFO */

/* §12.4 note 2: bits 31:24 and bit 3 are reserved — PRESERVE the value read.
   Hence every update of CTRL is read-modify-write, never a rebuild.       */
#define UART_CTRL_RESET  UINT32_C(0x00000008)

_Static_assert((UART_CTRL_RESET & 0x8u) != 0u,
               "bit 3 is set at reset — see rev C §12.4 note 2");
_Static_assert(UART_CTRL % 4u == 0u,
               "CTRL requires 32-bit aligned access — §12.2");

Four properties make this worth the typing.

Every claim has a section number. The next reader can check it against the document in seconds rather than searching.

The access types are where the code is, not in a separate document nobody opens — which is Chapter 16.6 §6's rule.

The assumptions are assertions where they can be. A reset value and an alignment requirement are both checkable at build time; Chapter 13.3 §7 named this as the way to rely on something deliberately.

The consequence is stated, not just the fact. "Hence every update of CTRL is read-modify-write, never a rebuild" tells the next person what to do, which the bare fact does not.

8. What You Should Be Able to Say Now

  1. A datasheet has four categories, like the C standard: guaranteed, implementation-defined (device-specific), unspecified (silent), and forbidden. This is Chapter 13.3's distinction applied to a different document.
  2. The gap between promise and behaviour is deliberate — it is the designer's freedom to change the implementation. Depending on it removes that freedom silently.
  3. Silence is not permission. An observed value in an unspecified area is evidence about one part.
  4. Four tests: Is the sentence in the document, or did I supply it? Would the designer call a change a bug? Did I learn it from the document or the device? What happens at the next revision?
  5. Measured instance: 16.7's bit-field structure produced byte-for-byte the correct register word on this compiler, and bit-field allocation order is implementation-defined (10.10). Perfect behaviour, zero guarantee.
  6. Three things the contract usually omits: timing, ordering between accesses, and coherence during a transaction. Assume none of them.
  7. The order you wrote is a statement about your source, not about the bus (17.6).
  8. "It works on my board" is 13.2's thesis in hardware clothing. Testing says not obviously wrong on this part; the document says right.
  9. Write the promise next to the code, with a section number, the access type, an assertion where possible, and the consequence spelled out.

9. Exercises

  1. Take any register description you have access to and classify five statements about it into the four categories of §3. Note which ones required you to go and find a sentence.
  2. For the model's CTRL, list three things you know about it from this module and say, for each, whether it is guaranteed, device-specific, unspecified, or something you observed.
  3. Apply §4's second test — would the designer call this a bug if it changed? — to the claim "reserved bit 3 reads as 1". Justify your answer.
  4. Write the header block from §7 for a device of your choice, with at least three section references and two _Static_assert lines.
  5. A driver polls a status bit immediately after writing a control register, with no delay. List the assumptions it is making, and say which category each falls into.
  6. Chapter 16.7 measured the bit-field structure producing exactly the right word. Write the one-sentence comment you would put above it if you were forced to ship it, naming what it depends on.
  7. Find one assumption in code you have written that came from observation rather than documentation. Write the comment that would have told the next reader.

10. Summary

  • A datasheet has four categories, like the C standard: guaranteed · device-specific · silent · forbidden. Same skill as 13.3, different document.
  • The promise is deliberately narrower than the behaviour — that gap is the designer's freedom.
  • Silence is not permission.
  • Four tests: in the document or supplied by me? · would a change be a bug? · document or device? · what about the next revision?
  • Measured: 16.7's bit-field word was byte-for-byte correct and is implementation-defined across compilers. Working perfectly and being guaranteed are different properties.
  • Usually omitted: timing, ordering, in-flight coherence. Assume none.
  • "It works on my board" is 13.2 in hardware clothing. Testing finds obvious wrongness; the document decides correctness.
  • Write the promise next to the code — section number, access type, assertion, and the consequence.

The contract is now something you can read and cite. What remains is the other direction: starting from a symptom on a bench — a device that ignores a setting, an interrupt that goes missing, a character dropped under load — and working back to which clause was broken.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.