Advertisement

How to enforce write-only access

April 13, 2004

Dan_Saks-April 13, 2004

Classes in C++ can impose semantic constraints on data, turning potential run-time errors into compile-time errors.

I'm a big fan of compile-time type checking. I don't enjoy debugging programs all that much, so I prefer to program in a style that transforms potential run-time errors into compile-time errors. Compile-time errors are easier to spot and fix than run-time errors. And while it's pretty easy to ship a program with run-time errors in it, it's impossible to ship a program that doesn't compile.

This month, I'll show you one simple, yet remarkably effective way you can use C++ to eliminate a particularly insidious class of run-time errors from your programs: accidentally reading from write-only device registers. The trick is to define a write-only data type.

Memory-mapped I/0
Most modern computers communicate with devices using memory-mapped device registers. To a C or C++ programmer, memory-mapped device registers look more or less like ordinary data objects. That is, storing into a memory-mapped device register sends commands or data to the device, and reading from a memory-mapped device register retrieves status or data from the device.

A typical hardware device often communicates through a small collection of device registers, rather than just one register. Some registers communicate control and status information, while others communicate data. A device may use separate registers for input and output.

For example, the ARM Evaluator-7T is a single-board computer built around a Samsung KS32C50100 processor (based on the ARM7 core) and a small assortment of peripheral devices, all of which use memory-mapped device addressing. Programs running on this processor communicate with devices by reading from and writing to selected locations in a 64KB bank of "memory" called the special registers.

The special registers start at address 0x03FF0000. The memory is byte-addressable, but each register is a four-byte word aligned to an address that's a multiple of four. Special registers are volatile—they may change state in ways that the compiler can't detect. Therefore, you should declare them as volatile objects. A typedef such as:

typedef unsigned volatile special_register;

is a good way to capture these properties of special registers.

The Evaluator-7T has two RS-232 serial connectors, each of which is attached to a UART (Universal Asynchronous Receiver/Transmitter) on the KS32C50100. UART 1 controls the serial port dedicated to the on-board debugger. UART 0 is available for user programming.

Each UART has six special registers:

  • ULCON: line control register
  • UCON: control register
  • USTAT: status register
  • UTXBUF: transmit buffer register
  • URXBUF: receive buffer register
  • UBRDIV: baud rate divisor register
You can access each of these registers as members of a struct defined as:

struct UART
    {
    special_register ULCON;
    special_register UCON;
    special_register USTAT;
    special_register UTXBUF;
    special_register URXBUF;
    special_register UBRDIV;
    };

In C++, you can declare this struct as a class, something that I'll do in a future column.

For UART 0, these registers reside at address 0x03FFD000. A program can access these registers via a pointer that points to that address. You can define that pointer as a macro, as in:

#define UART0 ((UART *)0x03FFD000)

or as a constant pointer, as in:

UART *const UART0 = (UART *)0x03FFD000;

In C++, you could also use a reference, another subject for a future column.

Many operations on the UART are simple to program. You can set the UART's transmit speed by storing a value into its UBRDIV register. For example:

UART0->UBRDIV = 0xA20;

sets the transmission rate to 9,600bps. You can transmit a character via the UART by storing the character's value into the UART's UTXBUF register, as in:

UART0->UTXBUF = 'A';

Read-only and write-only registers
Three of the UART registers—ULCON, UCON, and UBRDIV—are read-write registers. A program can copy values into and out of these registers. However, USTAT and URXBUF are read-only registers, and UTXBUF is write-only.

Writing to a read-only register produces undefined behavior. The program will misbehave in any of a number of ways. This bug can be hard to detect, so you're much better off trying to expose it at compile time. Fortunately, declaring data as read-only is an easy thing to do in both C and C++—just declare it as const, as in:

struct UART
    {
    special_register ULCON;
    special_register UCON;
    special_register const USTAT; // read-only
    special_register UTXBUF;
    special_register const URXBUF; // read-only
    special_register UBRDIV;
    };

Reading from a write-only register can also lead to subtle run-time errors. Unfortunately neither C nor C++ has a write-only type qualifier. You can fake it by defining an empty macro:

#define write_only

Then you can write:

special_register write_only UTXBUF;

but this offers no compile-time protection. It's just documentation.

A write-only register type
In C++, you can do much better—you can write a class called write_only_special_register, which defines a write-only register type. Using this class, you can define the UART struct as:

struct UART
    {
    ...
    write_only_special_register UTXBUF;
    ...
    };

You can then assign values to a UART's UTXBUF, as in:

com0->UTXBUF = c;

However, the compiler will reject any attempt to read the UTXBUF's value, is in:

c = com0->UTXBUF;    // error

or:

if (com0->UTXBUF > 0)    // error
    ...

Neat, huh?

The class definition appears in Listing 1. The class has a single private data member of type special_register. Essentially just a very thin wrapper around that data member, the class permits only write access to that member.

Listing 1: A class for a write-only register type

class write_only_special_register
    {
public:
    write_only_special_register()
        {
        }
    write_only_special_register(special_register v)
        : m(v)
        {
        }
    write_only_special_register &
        operator=(special_register v)
        {
        m = v;
        return *this;
        }
private:
    special_register m;
    // Leave these functions undefined...
    write_only_special_register
        (write_only_special_register const &);
    write_only_special_register &
        operator=(write_only_special_register const &);
    };

The write_only_special_register class has just three member functions. The first is the default constructor:

write_only_special_register()
    {
    }

which lets you declare a write_only_special_register without specifying its initial value. This constructor would be called from a definition such as:

write_only_special_register w;

The second member function is also a constructor:

write_only_special_register(special_register v)
    : m(v)
    {
    }

This constructor initializes a write_only_special_register by copying the value of a special_register. The construct : m(v) on the second line of the constructor is a member initializer, which tells the constructor to initialize data member m by copying parameter v. This constructor would be called from a definition such as:

write_only_special_register w = 0;

(NOTE: See Dan Saks' July 2004 column for updated info)

The third and final member function is an assignment operator:

write_only_special_register &operator=(special_register v)
    {
    m = v;
    return *this;
    }

which assigns value v to the write_only_special_register's data member m.

By convention, assignment operators for any class T should have a return type of "reference to T" and a return expression of *this. In other words, an assignment operator should return a reference to its left operand. This class follows that convention, even though the class has such limited capability, you can't take much advantage of it.

The class declares two more functions as private members. The first:

write_only_special_register (write_only_special_register const &);

is the copy constructor. The second:

write_only_special_register & operator=(write_only_special_register const &);

is the copy assignment operator. These are two special member functions that the compiler generates on demand for almost any class that doesn't declare them explicitly. The problem with the compiler-generated versions is that they perform read accesses on a write_only_special_register object, which this class is trying to prevent.

Declaring the functions as private members is the next best thing to not having them there at all. Given a declaration such as:

write_only_special_register w1, w2;

then an assignment such as:

w1 = w2; // error: operator= is private

won't compile. This is good. It turns a would-be run-time error into a compile-time error.

All of the member functions of this class are defined within the class definition. This means they are declared inline implicitly. The function definitions are indeed very short, so compilers have no trouble actually inlining them. Consequently, using this class adds no run-time overhead to programs. It just improves compile-time type checking.

A write-only class template
The write_only_special_register class implements a write-only variant for just one specific data type, a special_register. Over a broad range of architectures and applications, you'll probably need write-only versions of other types. Rather than write many variations of a write_only class, you can write a single general-purpose write_only class template.

In C++, a class template is not an actual class. Rather, it's a general recipe for a class that the compiler uses to generate actual classes. In this particular case:

template <typename T>
class write_only;

declares a class template called write_only<T> that can generate a write-only variant for almost any type T. The complete class template definition appears in Listing 2.

Listing 2: A template for a write-only variant of any type T

template <typename T> class write_only
    {
public:
    write_only()
        {
        }
    write_only(T v):
        m(v)
        {
        }
    write_only &operator=(T v)
        {
        m = v;
        return *this;
        }
private:
     T m;
    // Leave these functions undefined...
    write_only(write_only const &w);
    write_only &operator=(write_only const &w);
    };

For example, the struct definition:

struct UART
    {
    ...
    write_only<special_register> UTXBUF;
    ...
    };

uses the class template to define a write-only variant of the special_register type. The type specification write_only<special_register> generates a class by substituting special_register for T everywhere in the class template definition. Elsewhere in the same or other programs, you can easily specify other write-only types, such as write_only<char> or write_only<uint16_t>. This is something you just can't do in C.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. You can write to him at dsaks@wittenberg.edu.

Loading comments...