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.
Type name |
Size |
Range |
|---|---|---|
|
8 bits |
0 to 1 |
|
8 bits |
0 to 255 |
|
8 bits |
-128 to 127 |
|
8 bits |
0 to 255 |
|
16 bits |
-32768 to 32767 |
|
16 bits |
-32768 to 32767 |
|
16 bits |
0 to 65535 |
|
16 bits |
-32768 to 32767 |
|
16 bits |
-32768 to 32767 |
|
16 bits |
0 to 65535 |
|
32 bits |
\(-2^{31}\) to \(2^{31}-1\) |
|
32 bits |
\(-2^{31}\) to \(2^{31}-1\) |
|
32 bits |
0 to \(2^{32}-1\) |
|
64 bits |
\(-2^{63}\) to \(2^{63}-1\) |
|
64 bits |
\(-2^{63}\) to \(2^{63}-1\) |
|
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.
Type name |
Size |
Approximate range (normal values) |
|---|---|---|
|
32 bits |
\(\pm1.18\times10^{-38}\) to \(\pm3.40\times10^{38}\) |
|
32/64 |
as |
|
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:
Name or keyword |
Size |
Index type |
Address range |
|---|---|---|---|
Zero page |
8 bits |
signed char |
|
Default |
16 bits |
signed int |
|
When the 45GS02 for the MEGA65 is enabled, the following additional data pointers are available:
Name or keyword |
Size |
Index type |
Address range |
|---|---|---|---|
|
32 bits |
signed int |
|
|
32 bits |
signed long |
|
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:
Code becomes more readable with meaningful type names. For instance,
speed_tis clearer thanlong. Should the type definition change, only one location requires modification, preventing the need to search and selectively updatelonginstances that represent speed.A
struct fishcan be concisely namedfish_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.