CS 185 C Class Notes
Chapter 1 Introduction
Motivation
This class has several purposes.
The class emphasizes the details of using C language in embedded programs. The optional text, Programming Embedded Systems, gives a broad and complete overview of embedded programming in C.
Class Prerequisites
This class assumes you know the basic architecture of computers, CPU, stored program in memory, integer variables, etc.; and you know how to program, either in C or C++, or have a basic understanding of C and the ability to program in another procedural language like Java or Pascal.
Texts for Course: These Notes
and K&R
The lectures will not follow a textbook. These notes are provided to students for the purpose of reviewing what is taught in the lectures.
These notes review only selected special topics regarding the C language. Depending on the need of the students, more elements of the C language will be covered in class. Please use the required text, C Programming Language (2nd Edition), by Kernighan & Ritchie, as a reference.
No matter how well you know C, it will be useful in this class for you to know it better. If you know C well, please read K&R at the beginning of the course regardless. If not, read it at the beginning and go back over it until you understand it. Questions about C, no matter how basic, are welcome in class.
You should carefully read chapters 1 through 6 and appendix A (except A13). Appendix B is less important. Chapters 7 and 8 are not necessary for this class.
Pay particular attention to the following advanced topics: Declarations (Appendix A4, A6 and A8), Pointers (Chapter 5), and Structures (Chapter 6).
Optional Text
Programming Embedded Systems, Second Edition, by Barr and Massa, covers the subject of the class exactly. This class goes through specific examples and details which are not in the book, so it cannot be used as a text for this class.
The book is an excellent text for the subject of embedded programming in C. Anyone who intends to continue learning embedded programming should read it. At the general level, it covers the subject much better than the class notes. It also covers embedded operating systems which we will not do in class.
Chapter 4 explains in detail, the GNU tool-chain, which we will use in class.
Format of These Notes
Each chapter of the class notes has sections for the discussion of hardware, programming and examples. The hardware sections discuss the operation of the processor, its built-in peripheral devices or external hardware devices included on the Development Board you will use in this class. The programming sections discuss the C language, details of how the machine code produced from C language code controls the processor, how to use C language to control specific features of the processor, algorithms specific to low level programming and tips, tricks and techniques for short effective and maintainable code.
Hardware section:
Embedded system
An embedded system is computer system with a fixed purpose, unlike a personal computer. Examples range from a lawn sprinkler controller or a microwave oven with only timer and on-off switching functions to network routers which typically run a full Linux operating system.
This limitation of purpose allows the included software to be fixed, never changing except for possible updates controlled by the manufacturer. Most embedded computer devices do not include disk drives. The software is stored in read-only memory or, in flash memory, which can be modified, but only slowly and a limited number of times.
The fixed nature of embedded software has led to the term "firmware" - more difficult to change than software but easier to change than hardware.
Writing firmware for full sized embedded operating systems (like Linux or Unix-like commercial operating systems) resembles writing software for personal computers more that it resembles firmware for small embedded systems. Large embedded systems combine a few elements of small embedded systems, like the lack of keyboard, mouse and display, and the need interact directly with the processor or custom hardware, with main stream software issues like network protocols, high level operating system services and programming in JAVA and scripting languages.
Small embedded system
firmware
Small embedded systems often have no operating system. In this case, the only operations the processor executes are those specified the C code you write (except for a small block of assembly language code which runs before the C code to initialize global variables and set up the stack pointer).
We will study a small embedded system, learning the issues specific to embedded systems without the distracting complexity of larger systems. For the largest embedded systems, this knowledge needs to be combined with knowledge of high level software issues and techniques, and details of the services of full sized operating systems.
Small embedded system
hardware
Small embedded systems can be built using nothing more than a single microcontroller integrated circuit. A practical product would include a robust power supply, hardware for user interface like buttons, an LCD display and maybe sound, and probably electronics to convert the low power digital inputs and outputs of the microcontroller to whatever is being controlled.
Microcontroller
The microcontroller we will use contains flash memory that holds the firmware, RAM memory used to hold operating variables, the required clock and reset generation functionality as well as a complete processor. Once programmed this chip can be connected to DC power (3 to 5 volts) and will begin running the programmed firmware.
Connecting to one or more of its I/O pins allows it to do real work.
Development Board for this
class
We will use a development board which connects to a USB port on a Windows PC. This connection provides power and allows the PC to download programs to the microcontroller as well as emulate a terminal connected to its serial port. The board has two buttons and four LEDs connected to the microcontroller I/O pins and two peripheral chips connected to the microcontroller's SPI bus. It has a loudspeaker to output sound from either a digital output of the microcontroller or the DAC (digital to analog converter) peripheral.
Processor Architecture
Section:
Here, in the introduction, we will review computer processor architecture adding comments related to small microcontrollers. Later, we will learn the specifics of the core architecture of the particular microcontroller that we will be using as well as that of its peripheral parts.
Basics of a processor
Most readers will already know these general facts about processors in general:
· The processor fetches and then executes instructions from memory.
· The order of execution is sequential except when specifically modified by an executing instruction.
· Instructions can read data from and write data into specific memory locations.
· Instructions can also read and write data in a small number of registers.
· Instructions can do operations (arithmetic and bit manipulation) on data.
Microcontrollers in
particular
Microcontrollers differ from ordinary processors in that they have the memory the processor uses on the same chip. In most cases this is very much less memory that a normal PC.
Separate memory areas for
program and data
The type of memory for instructions is physically different than that used for data because the instruction memory must be preserved when power is removed and later restored while data memory must be writable as easily as it is read. In this case, it is usual to number the memory locations in each type of memory separately. In other words, the memory at any address, 0x0004 for example, in program memory is not related to the memory at address 0x0004 in data memory. It is said that the program and data address spaces are separate. (This is called Harvard architecture. The single address space of larger processors where the contents at an address can be treated as an instruction or as data is called Von Neumann architecture.)
Microcontrollers with separate address spaces have a special instruction to read data from program memory. Although it is not useful for a program to be able to read its own instructions, it is very useful to store constant data in program memory and then allow an executing program to read them as needed.
Nonvolatile memory
Where flash memory is used as program memory, there is usually a way for an executing program to write to it. Almost always, this is done only to install a new firmware version rather than as a part of normal operation because it delays program execution longer than is usually acceptable and because the number of possible rewrites of flash memory is limited.
Many microcontrollers have EEPROM (writable non-volatile) storage that is separate from both data and program address spaces. It is not treated as data memory because its timing is different, and writing is so slow that software must continue to execute after a write operation is initiated and return later when it is complete.
Lack of features for speed
and security
Microcontrollers are designed to favor simplicity over speed (with rare exceptions). They do not use cache and so operate at relatively low speed, ten to forty, rarely up to one hundred, million instructions per second, and instructions that use data memory are often slower. Memory mapping (MMU) and hardware data transfer (DMA) are absent. Hardware implementation that can be done more slowly in software, such as division and floating point data, often even multiply, are absent. Protection features like privileged mode, memory protection and error exceptions are absent.
Extra of features for use in
electronic devices and circuits
Unlike large processors, microcontrollers are intended to reduce the size and complexity of a product, so they include as much peripheral hardware functionality as feasible, such as multiple counters and timers, pulse generators, various serial port types and analog converters, as well as functionality needed to support the processor such as clock and reset generation. Many microcontrollers are designed for battery powered applications with minimized power consumption and special sleep modes.
Complex peripherals like Ethernet and USB have recently become available integrated in microcontrollers but at extra cost.
Many embedded products use
more powerful processors
These comments apply to microcontrollers which contain their own memory and are complete systems in themselves. Many embedded products contain more powerful processors with large amounts of separate memory chips and resemble full computers more than they resemble the microcontrollers we will study.
Upcoming
The next chapter will examine in detail the particular microcontroller we will use.
C Language Section:
High level features without
blocking access to low level details
The C language, and its successor C++, are unique in providing abstraction and independence from low level machine details without making these details inaccessible.
Most high level languages present a closed, self-sufficient programming environment in which the programmer needs to know nothing the underlying hardware details. People who write code that owns the whole system: embedded systems and operating systems, need more, they need to be able to control every aspect of the processor, not just move data and do computations.
Assembly language = machine
instructions
Every processor has an assembly language in which the programmer codes the native instructions that the processor executes. Assembly language therefore allows the programmer to control every operation the processor is designed to do. Assembly language programming is tedious, difficult and differs radically between each of the many processor types. Many lines of assembly code are needed for what can be done each line of high level code, making it much more prone to errors and difficult to maintain.
High level features automate
rote tasks
High level languages hide the differences and automate tasks such as arithmetic on variables larger than the size supported by the processor, choosing locations in memory to store variables and converting complex algebraic expressions into a linear sequence of machine instructions.
C Language is intended for
both abstraction and machine level control
C was originally designed as a high level language with which to rewrite an early version of UNIX previously written in assembly language. It intentionally combines the advantages of high level abstraction with the ability to override these abstractions.
Pointers are necessary
Support for the pointer data type is avoided by some languages because its use makes programs harder to verify and more prone to coding errors. Pointers in C parallel the manipulation of addresses at the machine instruction level. At the C level, pointers allow operations that, although unnecessary for computation and potentially dangerous, can be done just as in assembly language, but concisely, clearly and conveniently.
Programs have meaning to
humans as well as the machine
Well written programs can be read at two levels - as instructions to be executed precisely according to the specifications of the computer language - and as a human readable description of how the program operates. Similarly, a single piece of C code can be read both as a high level program hiding unnecessary details and, where needed, as a tool to manipulate data at the level of bytes and words stored at specific memory locations.
You need to know details of
the machine and how C uses them
Obviously, to be able to use C for hardware level manipulation of data, it is necessary to understand how to manipulate data at that level. Therefore, we will need to study the architecture of the processor, especially how memory is used.
An important related subject is the details of operation of the machine instruction (assembly language) version of a C program which is produced by the C compiler to execute on the processor. Particularly, knowledge of its usage of the stack is needed in the embedded case where the memory is size is limited. (The entire memory of the microcontroller we will use is a fraction of the amount a Windows program reserved for the stack of a single thread.) This knowledge is necessary also to interface sections of assembly language code with C and very useful when debugging a program.
Summary -> Understand C
thoroughly to use it for embedded programming
In summary, the use of C (or C++) is almost mandatory in small embedded systems, understanding the C language completely and thoroughly is greatly useful and it is necessary to understand assembly language programming but preferable to avoid using it wherever possible.
Upcoming
The upcoming chapters will review the C language emphasizing techniques used for embedded programming along with general tips for organization and maintainable code. Features of the GNU C compiler and related parts like the linker will be presented.
Program Example Section:
The upcoming chapters have program examples that run on the development board and demonstrate features of the microcontroller and programming that have been presented. Each chapter has a Program Example Section which discusses presents and discusses the examples related to that chapter.
Chapter 2 Memory
This chapter goes over the memory included on the microcontroller chip.
Although the first chapter described microcontrollers in general, starting now, we will be discussing the particular microcontroller that we are going to use, the Atmel AVR ATmega168. Almost everything will apply to all microcontrollers in the Atmel AVR family. All the principles we will learn apply to all microcontrollers.
Program and data address
spaces
The most important memories are program memory and data memory. As already mentioned, in this microcontroller, addresses that refer to memory locations are understood to be separate for program and data memory. Address 0x0004 might refer to an instruction stored in program memory, or a separate location in data memory, depending on the context in which it is used. This is different from the large processors which may be familiar to you, which have only one addressing space for both instructions and data.
Hardware section:
The ATmega168 has 1 Kbytes of data memory and 16 Kbytes of program memory. The data memory is SRAM which can be read and written very simply. The program memory is flash memory for which reading is simple, but writing is slow and cumbersome. This is appropriate because the program is written only once, when the microcontroller is programmed. The flash is nonvolatile – the program remains when power is turned off.
Program memory
Program memory addresses go from 0x0000 to 0x1FFF. This range is 8 K addresses. Each instruction takes a multiple of two bytes – most are 16 bits (2 bytes), a few are 32 bits (4 bytes). Unlike single address space processors, where every byte has an address, program space addresses refer to 16 bit memory locations. (Note that the GNU linker, which is designed mainly for byte addresses, uses addresses that count bytes. Therefore all address numbers it prints out are twice the number seen and used in the program and by the processor.)
Data memory
Data memory addresses go from 0x0100 to 0x04FF. Each byte is addressed normally.
Addresses in the range 0x0000 to 0x00FF represent registers. These include the 32 general purpose registers (which are normally used in instructions without referencing their addresses, a few other registers used by the processor like the stack pointer, and hardware peripheral control registers which the program writes and reads to control the on-chip peripherals. Not all locations in the range 0x0000 to 0x00FF are used.
There are two other memories in the microcontroller: EEPROM and Fuse Bits.
EEPROM
The EEPROM is physically similar to the flash: Both are nonvolatile. Reading happens at the normal speed of program execution, writing takes several milliseconds – 10,000 times slower. Both must be erased before writing. The EEPROM is intended to be used for storing data. In the EEPROM, individual bytes can be erased and rewritten; in the flash, large sections must be erased at one time. The EEPROM can be rewritten 100,000 times; the flash only 10,000 times. Most importantly EEPROM can be rewritten while the program is running; the flash can be rewritten during program execution only in a limited way. The addresses in the EEPROM are not in either address space. It is not known to the C Compiler. It is accessed by reading and writing several
Fuse bits
The Fuse Bits consist of three bytes which control aspects of the operation that never change – mostly electrical characteristics. They cannot be changed by the program.
Initial programming
The program memory, the fuse bits and optionally the EEPROM need to be programmed when the product is manufactured. The EEPROM can be changed at any time under control of the program. The program memory can be changed under program control with limitations, normally only to update the firmware after the embedded system is already in use.
Processor Architecture
Section:
In this section, we examine the data memory area from the program point of view.
Data Memory Area
Memory locations 0x0100 to 0x04FF are ordinary SRAM. They just retain whatever value is stored. Locations 0x0020 to 0x00FF are control registers for the processor and for on-chip peripheral devices. For these, reading does not necessarily give what was previously written.
Peripheral Control Registers
– not exactly memory
Some of these locations are read-only, reading gives information that may change from time to time, writing has no effect. For example, the Port C Input register, named PINC, gives the digital state of the voltage levels on the “Port C” pins – 6 of the 28 pins on the microcontroller.
If you want to force the voltage on the pins to digital one or zero, you write the desired bit pattern to a different register, the Port C Output register, named PORTC. If you write 0x00 to PORTC, to pull all the pins to low voltage, but a stronger external device forces it high, reading PINC will show the actual voltage level.
Most writable registers can also be read and show the same value that was previously written. In this way, they are the same as ordinary memory. The difference is that there is the side effect – the voltages on the pins change in this case. The purpose of allowing such registers to be read as well as written is to allow the program to see what value is in the register. The program could keep a copy in normal memory and update it each time the control register were changed. Allowing the register to be read makes that unnecessary.
In the example above where PORTC is set to zero, but a pin forced high, reading PORTC will show it low and reading PINC will show it high. (Also, the microcontroller will overheat.)
Port C can also be used in input mode where the pins can be driven freely by external devices without conflict. The Port C Data Direction register determines whether
There are a few cases of read/write registers where the value written and read are less related or not related at all. There are interrupt flag bits in several registers which show as 1 if the hardware is requesting an interrupt, 0 if not. The program can clear that flag by writing 1. Writing 1 to that bit, when it reads as 1, causes it to read as 0 afterwards.
Writing to the UART data register, UDR0, causes 8 bits to be transmitted serially on a microcontroller pin. Reading the same pin gets 8 bits previously received on a different pin.
Despite these exceptions, most writable control registers act like memory – reading gives the value most recently written there.
Use Register Names
PINC is read as location 0x0026. You will not need to know the actual locations except perhaps when you are debugging a program. Programs always use a symbolic name for such locations, PINC, in this case. In C language, there is a header file where PINC is defined as (*(volatile uint8_t *)(0x26) – it is a number that is specified to be a memory location. Related but different microcontrollers, in the same family, also have a PINC register, but it might be a different location in the memory area. Using the name, PINC, in a program reduces the number of changes needed to make the program run on a different processor. It also, obviously, makes the program easier to understand
Implications for C
The point of the preceding discussion is that the C compiler does not differentiate control registers from ordinary data memory. Accessing control registers in C will be discussed later in the C language section below.
C Language Section:
Pointers are variables which contain the address (location in memory) of variables. The type of the pointer specifies how the contents of the memory are to be interpreted. Dereferencing such a pointer in a C program causes the compiler to produce machine instructions to fetch from (or write to) the location in data memory given by the value of the pointer. C also supports pointers to functions. In that case, the address is understood to be in program memory. To dereference a pointer to function, the compiler produces a machine instruction that calls a function. The location called is similarly given by the value of the pointer variable, but it is in program memory.
Casting Pointers
Casting pointers is a feature of C that should never be used in high level programs because it can make programs non portable by exposing features of the hardware that change from system to system. For programs that interact directly with the hardware, this exposure is crucial.
Here is a brief summary of casting for C programmers unclear on it. First, understand that the sense of the word, cast, is not the meaning, to throw, rather it is the meaning, to melt a substance and pour it into a container in order to give it a particular shape. So, casting a variable means changing its “shape” without changing its “substance” – specifically changing its type without changing its value.
You seldom need to cast variables because C automatically translates types. For example
long x;
float f;
f = 1.;
x = (long)f;
In the last line the float value 1 (internal representation 0x3f800000) gets converted to the integer type before it is stored in x. In this case, its new internal representation (now 0x00000001) is obtained by a rather complicated set of steps, but its value is still the number 1. It is not necessary to use the cast in this case because C knows that this is a normal and useful type of conversion.
When you cast a pointer, the value never changes (the
address remains the same address), but the meaning changes. When the new pointer is dereferenced
(the value is read from memory as specified by the address in the pointer), the
same stored data (at the same address) is interpreted differently. In the following example, the first pointer
points to memory containing data representing a variable of type float. The program creates a second pointer whose
data type is long,
and copies the address in the first pointer into the second using a cast. The data in memory has not changed, but when
dereferencing reads it into a variable of type long, the data has a different meaning.
long x;
float f;
long * px;
float * pf;
f = 1.;
pf = &f;
px
= (long *)pf;
x = *px;
In the line, pf = &f, pf is assigned the address the memory location (say 0x04dc for example) which holds the first of the four bytes of the value of f. Casting pf to type (long *) in the line, px = (long *)pf, leaves its value (0x04dc) the same but makes it a pointer to long and stores it in px. In the line, x = *px, the value of the long at that location, is copied to x. The value of x is now 1065353216. Why? In the last line x gets the four byte value at location 0x04dc, which is 0x3f800000. That binary bit pattern, in a long, is 1065353216 in decimal, 0x3f800000 in hexadecimal.
The explicit cast in the second to last line is necessary. Without it, the statement, px = pf, is an error. The compiler knows that converting a pointer-to-float to a pointer-to-long leads to a result that is normally not desired or correct. Therefore, it does not make the conversion automatically. The explicit cast is a direct instruction to the compiler to make the conversion. Since the programmer clearly intends this conversion, the compiler does not treat it as an error. (In C++, casting is separated into four types. The type we are using here is called the reinterpret_cast<>, in C++.)
This is an extreme example. Even hardware aware programs do not normally read float data as an integer. Mostly, we will cast integers which represent memory addresses into pointers. Sometimes we will cast a pointer to a long integer into a pointer to a single byte. Then we could, for instance, change the upper byte without changing the rest. (We could do the same thing using shift and mask operations. Sometimes, casting produces more efficient code.)
Casting an Integer Address to
a Pointer – to Read a Specific Memory Location
Now consider the code which reads the Port C pins and puts the bits in x.
unsigned char x;
x = ( (*(unsigned
char volatile *)(0x26) );
This is a shorter way of saying
short addr;
unsigned char volatile
* px;
unsigned char x;
addr
= 0x26;
px
= ((*(unsigned char volatile *)addr;
x = *px;
The 16 bit variable addr is assigned the value 0x0026. Then the pointer px is given that value. The variable addr must be cast explicitly because it is unsafe to convert an integer to a pointer. The last line reads the value of the memory location at address 0x0026, which is actually the Port C Input Register rather than memory, but that does not matter to the compiler.
The short version of the program uses internal temporary variables instead of addr and px in the same way that
x = (a + b) * (c + d);
is an abbreviation of
t1 = a + b;
t2 = c + d;
x = t1*t2;
Once you understand casting to pointers, you can do many operations related to talking to hardware registers in the data address space.
Volatile Keyword
The meaning of the keyword, volatile, in ( (*(unsigned char volatile *)(0x26) ) is somewhat subtle, but very important in embedded programming. Syntactically, it is called a variable modification, like the keyword const. The keyword, const, applied to a variable, tells the compiler that it should not allow the program to modify that variable. The keyword, volatile, tells the compiler that the variable could unexpectedly be changed.
You can always write a working program without using the const keyword – it just helps the compiler to find errors and makes the program easier to understand. There are cases where the program will run incorrectly if the volatile keyword is lacking.
In C, the variables normally are kept in specific locations in memory. The machine code produced by the compiler often moves the value of a variable into a register before it can use it. If the value is used several times, the compiler produces code that reuses the value in the register rather than reading it out of memory a second time.
This makes the code smaller and faster. It normally works because the compiler normally knows whether it has produced code that could have changed the value of the variable. The volatile keyword forces the compiler to read the variable from memory every time it is used.
In the case above where the variable is located at the address of a register, the value does change spontaneously, every time the inputs to port C change. Without the volatile keyword, the compiler would likely assume the value never changes and produce code that would read it only once, save that value and reuse it.
Location of Modifier Keyword in a Pointer Declaration
The location of a modifier in the declaration of a pointer is important. The following declarations are both syntactically correct, but they cause different operations in programs.
unsigned char volatile
* p1;
unsigned char * volatile
p2;
p1 is a pointer to a volatile
byte. If the value of p1 is 0x0026, then the
compiler must reread memory location 0x0026 each time is *p1 evaluated,
in case the content of that location has changed. The volatile keyword is next to unsigned char; it
refers to the unsigned
char to which the pointer points.
p2 is a volatile pointer to a byte. If the value of p2 was 0x0026, then the compiler must reread the value of p2 in case the value is no longer 0x0026. The volatile keyword is next to p2; it refers to p2, the pointer itself.
Usually it is the first case, the referred location is
volatile, not the value of the pointer.
Program Example Section:
Here is our first program. It blinks an LED on the development board.
The program first sets the Port C Data Direction register to 0x0F. This makes the four lowest port C pins (PC0, PC1, PC2, PC3) into outputs. The other two remain inputs.
The variable val will control the LEDs it will alternate between 0x00 and 0x01. Decrementing the variable count is used to delay between the two states of the LED. Otherwise the blink would be too fast to see.
A Program to Blink an LED
#include
<inttypes.h>
#include
<avr/io.h>
void main()
{
int32_t volatile
count;
uint8_t val;
DDRC = 0x0F;
val
= 0x00;
for(;;)
{
for(
count=0x100000; count>=0; --count ) continue;
val
= val ^ 0x01;
PORTC = val;
}
}
The symbols, DDRC and PORTC, are macros defined in <avr/io.h>
as something like
( (*(unsigned char volatile *)(0x26) ). The symbols, int32_t and uint8_t, are typedefs
defined in <inttypes.h> as long and unsigned char.
Volatile Keyword Again
Note a different use of the volatile keyword in the declaration of count. Without it, the compiler would (correctly) decide that the line
for( count=0x100000; count>=0; --count ) continue;
is irrelevant to every calculation in the program, and eliminate it in the interest of execution speed and code size. That would eliminate the delay that we intended. Declaring count to be volatile forces the compiler to decrement and test it regardless (as well as forcing it to use the real variable in memory rather than a copy in a register).
The Program to Blink an LED,
in Assembly Language
Before we get into C tools, we will use a version of this program written entirely in assembly language. All subsequent programs will be written entirely or almost entirely C. In this case only, we do not need to use the C compiler.
Instead of C variables in memory, we use several of the many general purpose registers in this processor. R16 is used as val above. R20, r21 and r22 are used as count. (Even though count is four bytes long, we only use three here because the top byte is always zero. If we wanted very long delays, we would use a fourth.) R17 is used to hold the constant 1 that is used in the line
val
= val ^ 0x01;
Here is the same functionality in assembly language.
.equ DDRC =
0x07
.equ PORTC = 0x08
.org 0 ; program starts at location 0 of
program memory
ldi
r16, 0x0F ; put value 0x0F in r16
out DDRC, r16 ; port C bit 0-3 are output
ldi r16, 0x00 ; value to output on port C
out DDRC, r16 ;
output it
ldi
r16, 0x01 ; put value 0x01 in r16
loop2:
ldi
r20, 0x00
ldi
r21, 0x00
ldi
r22, 0x10 ; load 24 bit counter with
0x100000
loop1:
subi
r20, 1
sbci
r21, 0
sbci
r22, 0 ; subtract 0x000001 from
counter
brcc
loop1 ; loop if not negative
eor
r17, r16 ; change bit 0 of value to
output
out PORTC,
r17 ; output value to port C pin
rjmp
loop2
Readers who are familiar with Intel x86 assembly language will be misled by the out instructions. There is no separate I/O address space. The out instruction causes a value to be stored in memory, but it is optimized to address locations 0x0020 to 0x003F. The instruction,
sts
PORTC, r17
would also work. This instruction can address all data memory locations 0x0000 to 0xFFFF, but the instruction occupies 32 bits, whereas the out instruction occupies only 16 bits.
Chapter 3 Data Representation
This chapter describes how the data that represents the value of variables is stored in memory.
Hardware Section:
The hardware components involved in data storage are the processor and the data memory. These are included on the microcontroller chip.
The processor contains a computation unit (ALU) and 32 temporary storage registers which hold eight bits each.
The data memory has 1024 storage locations which hold eight bits each.
Processor Architecture
Section:
Data Width is 8 Bits
Data memory is eight bits wide. This means that each of the memory locations having a unique address, holds 8 bits. In all modern processors, each unique address holds 8 bits, even though many read and write a larger number of bits in each operation. These 8 bits, when considered as a unit, is called a byte.
The processor we use is an eight bit processor. Most of the operations it does act on groups of 8 bits, one byte. In particular, all memory operations always read or write single bytes and almost all computation operations use single byte inputs and give a single byte output.
The size of data used by a processor is called the machine word size. Other processors use machine word size of two, four or eight bytes.