11. Data representation

This chapter describes the available data types in detail.

11.1. Basic types

All standard integer types are supported. The following table lists the built-in integer types and their ranges.

Table 11.1 Integer types

Type name

Size

Range

bool

8 bits

0 to 1

char

8 bits

0 to 255

signed char

8 bits

-128 to 127

unsigned char

8 bits

0 to 255

short

16 bits

-32768 to 32767

signed short

16 bits

-32768 to 32767

unsigned short

16 bits

0 to 65535

int

16 bits

-32768 to 32767

signed int

16 bits

-32768 to 32767

unsigned int

16 bits

0 to 65535

long

32 bits

\(-2^{31}\) to \(2^{31}-1\)

signed long

32 bits

\(-2^{31}\) to \(2^{31}-1\)

unsigned long

32 bits

0 to \(2^{32}-1\)

signed long long

64 bits

\(-2^{63}\) to \(2^{63}-1\)

long long

64 bits

\(-2^{63}\) to \(2^{63}-1\)

unsigned long long

64 bits

0 to \(2^{64}-1\)

bool

To use the bool data type, include stdbool.h, which also defines true and false. The boolean data type is also available as _Bool without requiring stdbool.h.

char

The char type is unsigned by default. To enable signed char, compile with the --char-is-signed option. Note that the supplied C library uses unsigned char.

Note

The char type differs from short, int and long in that it defaults to being unsigned in this compiler. Standard C allows it to be either signed or unsigned. The rationale for making char unsigned is that it is meant to represent a character code and in encoding standards like ASCII, ISO-8859-1 and Unicode, character values are unsigned entities.

If you intend to use 8 bits data types in expressions, you should consider using int8_t or uint8_t instead. They are defined in stdint.h.

wchar_t

The wide character type wchar_t is defined if you include stddef.h.

Bit fields

Bit fields are supported based on any integer type. A bit field value has the same type (and signedness) as the integer base type it is defined in and are subject to the usual conversion rules when used in expressions.

A bit field is allocated starting from the least significant available bitposition in its container. If there are not enough bits available in the container to represent the bit field, a new container is allocated.

Consider the following declaration:

struct bf {
  uint16_t  a:7;
  int16_t   b:5;
  uint32_t  c:24;
  uint8_t   d:4;
  uint8_t   e:2;
  int8_t    f:3;
};

The two bit fields a and b are allocated in the same 16 bits container. Bit field c gets a 32 bits container of its own. Finally, d and e will share the same 8 bits container while f is allocated in a separate 8 bits container as there is not room to store it together with d and e.

Bit fields should be used with care. They can be a convenient way to pack several small values into some structure when data space is limited. However, accessing bit fields is in general more costly in terms of produced code, compared to using normal integer types.

Note

Sometimes it can be tempting to try and map bit fields to hardware registers. This can work, but it makes the code more sensitive to using a particular compiler. It may also require some thinking to get it right. The alternative way of accessing hardware registers in its intended access width and manually apply shift and mask operations is often more robust.

Floating-point types

Floating point values follows the IEEE 754 format and is supported in two different sizes.

Table 11.2 Floating-point types

Type name

Size

Approximate range (normal values)

float

32 bits

\(\pm1.18\times10^{-38}\) to \(\pm3.40\times10^{38}\)

double

32/64

as float or long double

long double

64 bits

\(\pm2.23\times10^{-308}\) to \(\pm1.80\times10^{308}\)

Floating point numbers are represented in binary floating point form. The size of double is 32 bits by default. This can be changed to 64 bits by using the command-line option --64bit-doubles. The runtime library comes both in variants compiled with double set to 32 bits as well as 64 bits.

Subnormal numbers, infinity and NaN (not a number) are supported. The ranges stated in the table are for normal floating point numbers. Subnormal floating-point numbers extends the range of the exponent further at the cost of gradual loss of precision in the mantissa.

Floating point exceptions and changing the rounding mode are not supported.

32 bits format

In the 32 bits format the exponent is 8 bits and the mantissa is 23 bits. The precision is between 6 and 7 decimal digits.

64 bits format

In the 64 bits format the exponent is 11 bits and the mantissa is 52 bits. The precision is between 15 and 16 decimal digits.

Function pointer types

For the 6502 architecture function pointers are always 16 bits.

Data pointer types

The following data pointers are available:

Table 11.3 Data pointers

Name or keyword

Size

Index type

Address range

Zero page __zpage

8 bits

signed char

0x00-0xff

Default

16 bits

signed int

0x0000-0xffff

When the 45GS02 for the MEGA65 is enabled, the following additional data pointers are available:

Table 11.4 Additional data pointers on the MEGA65

Name or keyword

Size

Index type

Address range

__far

32 bits

signed int

0x00000000-0xffffffff

__huge

32 bits

signed long

0x00000000-0xffffffff

Each data pointer has an associated index type, always a signed integer type. This type is used in address calculations, such as array access with an index or advancing a pointer by adding or subtracting an integer value.

Pointer conversions

Function and data pointers are treated as being unsigned values. In general, casting to a type that has fewer bits means a pointer value gets truncated. Casting to a wider type results in zero extension.

size_t

This unsigned integer type holds the maximum size of an object. On the 6502 the size of size_t is 16 bits.

ptrdiff_t

This signed integer type represents a distance within the largest possible object. On the 6502 the size of ptrdiff_t is 16 bits.

Subtracting two data pointers (within the same object) yields a ptrdiff_t value, representing the number of elements between the pointers, not the number of bytes.

Note

Standard C allows referring to an element one beyond the actual object. Subtracting the end address from the start address may result in a negative result if the value exceeds the ranfe of ptrdiff_t.

11.2. Structure types

Structure types are fully supported and can be nested. Structure members are stored sequentially in the order they appear in the declaration.

11.3. Union types

Union types are fully supported. The compiler also supports a useful extension that allows anonymous unions within a structure. Consider the following code:

struct Scope {
  union {
    char alpha;
    int num;
  } u;
  int b;
};

int main() {
  struct Scope x;
  x.u.num = 65;
  x.u.alpha = 'A';
  return 0;
}

The use of a declarator on the union within the structure requires an extra step to access its members. This was relaxed in the C11 standard, allowing you to write:

struct Scope {
  // Anonymous union
  union {
    char alpha;
    int num;
  };
  int b;
};

int main() {
  struct Scope x;
  x.num = 65;
  x.alpha = 'A';
  return 0;
}

This C11 extension, enabled by default, allows anonymous unions to be compiled without diagnostic messages, even though the compiler actually supports C99.

You can enable warnings for such extensions using the -Wc11-extensions or -Wpedantic command-line options.

You can also disable such extensions with the command-line option --pedantic-errors. In that case you will get an error instead.

11.4. Enumeration types

Enumeration types are represented as int. To use a smaller storage representation, choose a smaller integral type. For a better name, use a typedef:

enum fruit { apple, orange, banana };

typedef char fruit_t;

fruit_t active;

void citrus(void)
{
  active = orange;
}

11.5. Type qualifiers

Standard C provides two type qualifiers: volatile and const.

Volatile objects

C has the concept of volatile objects, typically used for hardware access. Both writing and reading volatile objects are considered side effects that will occur.

Related is the concept of sequence points in a program. A volatile access between two sequence points occurs between those points and cannot be moved past a sequence point. To ensure ordered memory accesses, make them volatile and separate them with a sequence point. The semicolon after a statement is an example of a sequence point:

uint8_t volatile * mem1;
uint8_t volatile * mem2;

void foo () {
   *mem1 = 2;
   *mem2;
}

In this case the write to mem1 is guaranteed to be performed before the read of mem2. The read of mem2 will also occur even if the result of the read is not used.

If multiple volatile accesses are done between two sequence points the order they happen in is undefined:

uint8_t volatile * mem1;
uint8_t volatile * mem2;

int foo () {

   return *mem1 + *mem2;
}

In this case both mem1 and mem2 are read, but the order in which they are read is undefined.

Access size

Accessing a volatile object wider than the natural register size on the target results in access performed in several steps, as dictated by the natural register size.

Reading and using only a portion of a scalar volatile object still results in the entire object being read:

volatile uint64_t wide;

uint16_t foo () {
   return wide;
}

Here the volatile object is 64 bits, but we are only interested in the lower 16 bits. In this case all 64 bits are read, the upper 48 bits are then discarded and the function returns the lower 16 bits.

If wide was not volatile, the compiler may instead choose to only read the lower 16 bits of the 64-bit wide variable.

Assignment results

Assignments in C has an expression value. Consider:

volatile int var;

int foo () {
   return var = 4;
}

The assignment writes 4 to the volatile variable, but the function return value (either 4 or the value read from var after the assignment) is implementation-defined.

Avoid such constructs in your programs. Be more explicit about your intent. To force a read after the assignment:

volatile int var;

int foo () {
   var = 4;
   return var;
}

If you want to be sure the function returns 4:

volatile int var;

int foo () {
   var = 4;
   return 4;
}

This clarifies the intent and ensures consistent behavior across C compilers.

Bit fields

Accessing volatile bit fields has undefined behavior. A bit field describes a subset of bits within its storage unit. Adjacent bits may be accessed depending on layout. Using volatile bit fields is discouraged.

Note

Rather than using bit fields, define normal scalar values so that they cover hardware registers, following the defined or intended size of hardware register access. Then use expressions to extract or manipulate the part of the register you want.

Const objects

The const type qualifier indicates a read-only object. It can be applied to data objects and pointers.

When a static object is defined as const, the compiler attempts to place it in a read-only memory section.

A pointer to a const object can point to both read-only and writable objects. Such a pointer signifies that its user should not attempt to alter memory. This can aid the optimizer and is considered good practice, as it prevents unintended data alteration by parts of the application.

11.6. Type definitions

Using type definitions (the typedef keyword) is highly recommended, offering several benefits:

  1. Code becomes more readable with meaningful type names. For instance, speed_t is clearer than long. Should the type definition change, only one location requires modification, preventing the need to search and selectively update long instances that represent speed.

  2. A struct fish can be concisely named fish_t, which also hides its structure, if desired.

typedef incurs no cost; the generated code remains identical.

11.7. Alignment

The 6502 imposes no data alignment. Data objects can start at any address, and no padding is introduced between structure elements.