CAB202 Lecture Notes
CAB202 Lecture Notes
Semester 2, 2022
Dr Mark Broadmeadow
Tarang Janawalkar
Contents
Contents 1
I Foundations of Microcontrollers 7
1 Computer Systems Architecture 7
1.1 Introduction to Computers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Microprocessors & Microcontrollers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3 The AVR ATtiny1626 Microcontroller . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.3.1 Flash Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.2 SRAM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.3 EEPROM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.4 The AVR Core . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.5 Status Register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4 Computer Programming Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.5 Program Execution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.5.1 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5.2 Memory and Peripherals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1
Microprocessors and Digital Systems CONTENTS
2.6.6 Shifts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.6.7 Rotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.7 Arithmetic Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.7.1 Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.7.2 Overflows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.7.3 Subtraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7.4 Multiplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.7.5 Division . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
II Microcontroller Fundamentals 22
3 Microcontroller Interfacing 23
3.1 Logic Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.1 Discretisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.2 Logic Levels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.1.3 Hysteresis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
3.2 Electrical Quantities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.1 Voltage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.2 Current . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.3 Power . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.2.4 Resistance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
3.3 Common Electrical Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3.1 Resistors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3.2 Switches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3.3 Diodes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
3.3.4 Integrated Circuit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.4 Digital Outputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.4.1 Push-Pull Outputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.4.2 High-Impedance Outputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.4.3 Pull-up and Pull-down Resistors . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.4.4 Open-Drain Outputs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.5 Microcontroller Pins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.5.1 Configuring an Output . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.5.2 Configuring an Input . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.5.3 Peripheral Multiplexing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.6 Interfacing to Simple I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.6.1 Interfacing to LEDs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.6.2 Interfacing to Switches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
3.6.3 Interfacing to Integrated Circuits . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.7 Programming a Microcontroller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2
Microprocessors and Digital Systems CONTENTS
IV C Programming 47
6 Introduction to C Programming 47
6.1 Basic Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.1.1 The Main Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.1.2 Statements and Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.2 Variables, Literals, and Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.2.1 Declaring and Initialising Variables . . . . . . . . . . . . . . . . . . . . . . . . 49
6.2.2 Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.3 Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.3.1 Integer Prefixes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.3.2 Integer Suffixes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.3.3 Floating Point Suffixes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.4 Flow Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.4.1 If Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.4.2 While Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
6.4.3 For Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.4.4 Break and Continue Statements . . . . . . . . . . . . . . . . . . . . . . . . . . 54
6.5 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
6.5.1 Operation Precedence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.5.2 Arithmetic Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.5.3 Operator Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
6.5.4 Bitwise Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.5.5 Relational Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.5.6 Logical Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
3
Microprocessors and Digital Systems CONTENTS
8 Advanced C Programming 63
8.1 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.1 Referencing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.2 Dereferencing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.3 Using Qualifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.1.4 Pointers to Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
8.1.5 Pointer Arithmetic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.6 Void Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.1.7 Size-of . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2 Array Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.1 Indexing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.2 Array Decay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.2.3 Array Length . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.2.4 Copying Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.2.5 Multidimensional Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.3 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8.3.1 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8.3.2 Return Values . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.3.3 Function Prototypes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
8.3.4 Passing by Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.3.5 Call Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.4 Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.4.1 Global Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
8.4.2 Local Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.4.3 Block Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.4.4 Static Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.5 Advanced Type Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.5.1 Volatile Qualifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
8.5.2 Type Casting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
4
Microprocessors and Digital Systems CONTENTS
9 Objects 75
9.1 Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
9.1.1 Memory Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.1.2 Anonymous Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.1.3 Structures Inside Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.1.4 Structures and Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9.1.5 Typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
9.2 Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
9.3 Bitfields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
9.3.1 Properties of Bitfields . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
9.4 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
10 Interrupts 81
10.1 Interrupts and the AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
10.1.1 Interrupt Vectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
10.1.2 Interrupt Service Routine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
10.1.3 Interrupt Flags . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
10.1.4 Peripheral Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
10.1.5 Port Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
10.1.6 Interrupts and Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . 83
11 Hardware Peripherals 83
11.1 Configuring Hardware Peripherals . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
11.2 Timers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
11.2.1 Timer Implementations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
11.2.2 Timer Counters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
11.2.3 Timer Periods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
11.2.4 Timer Counter B Example Configuration . . . . . . . . . . . . . . . . . . . . 84
11.3 Pulse Width Modulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
11.3.1 PWM Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
11.3.2 PWM Brightness Control Example . . . . . . . . . . . . . . . . . . . . . . . . 85
11.4 Analog to Digital Conversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
11.4.1 Quantisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
11.4.2 Sampling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
11.4.3 ADC Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.4.4 ADC Potentiometer Example . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
11.5 Serial Communication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
11.5.1 Serial Communication Terminology . . . . . . . . . . . . . . . . . . . . . . . . 88
11.5.2 UART . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
11.5.3 UART Frame Format . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.5.4 USART0 on the ATtiny1626 . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
11.5.5 USART0 Example Configuration . . . . . . . . . . . . . . . . . . . . . . . . . 90
11.5.6 Serial Peripheral Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
11.5.7 SPI0 Example Configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
11.5.8 Other Serial Protocols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
11.5.9 Polled vs Interrupt Driven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5
Microprocessors and Digital Systems CONTENTS
12 State Machines 96
12.1 State Machine Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
12.2 Enumerated Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
12.3 Switch Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
13 Serial Protocols 99
13.1 Serial Protocol Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
13.1.1 Requirements for a Serial Protocol . . . . . . . . . . . . . . . . . . . . . . . . 99
13.1.2 Symbols . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
13.1.3 Messages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
13.1.4 Encoding . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
13.1.5 Message Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
13.1.6 Start Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
13.1.7 Multi-Symbol Start Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . 100
13.1.8 Sub-Symbol Start Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
13.1.9 Message Identifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
13.1.10 Payloads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
13.1.11 Payload Length . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
13.1.12 Variable Length Payloads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
13.1.13 Escape Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
13.1.14 Handshakes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
13.1.15 Message Verification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
13.1.16 Flow Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
13.2 Serial Protocol Parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
6
Microprocessors and Digital Systems 1 COMPUTER SYSTEMS ARCHITECTURE
Part I
Foundations of Microcontrollers
1 Computer Systems Architecture
1.1 Introduction to Computers
Definition 1.1 (Computer). A computer is a digital electronic machine that can be programmed
to carry out sequences of arithmetic or logical operations (computations) automatically.
Definition 1.2 (Central Processing Unit). The central processing unit (CPU) is a system of
components that processes instructions, performs calculations, and manages the flow of data through
a computer. It consists of several components such as the control unit, arithmetic logic unit, and
processing core(s).
Definition 1.3 (Control unit). The control unit is a component of the CPU responsible for
managing the flow of instructions and data within the processor. It interprets program instructions,
coordinates the activities of other CPU components, and ensures that operations are carried out in
the correct sequence.
Definition 1.4 (Arithmetic logic unit). The arithmetic logic unit (ALU) is a subsystem of the
CPU that performs mathematical operations, such as addition and subtraction, as well as logical
comparisons, such as equality or inequality checks. It is a critical component for executing calculations
and decision-making tasks within a computer.
Definition 1.5 (Processing core). A core is an independent processing unit within the CPU, capable
of executing its own set of instructions. Modern CPUs may contain multiple cores, enabling parallel
execution of tasks to improve efficiency and performance. Each core operates as a self-contained
processor within the larger CPU system.
7
Microprocessors and Digital Systems 1 COMPUTER SYSTEMS ARCHITECTURE
– EEPROM (256 B)
• Peripherals: Implemented in hardware (part of the chip) in order to offload complexity
1.3.2 SRAM
• Volatile — memory is lost when power is removed
• Expensive
• Faster than flash memory and is used to store variables and temporary data
• Can access individual bytes (large chunk erases are not required)
1.3.3 EEPROM
• Older technology
• Expensive
• Non-volatile
• Can erase individual bytes
8
Microprocessors and Digital Systems 1 COMPUTER SYSTEMS ARCHITECTURE
Z Zero Flag
N Negative Flag
V Two’s Complement Overflow Flag
S Sign Flag
9
Microprocessors and Digital Systems 1 COMPUTER SYSTEMS ARCHITECTURE
4. Store result
5. Update PC: increment once if the instruction is one word, otherwise increment twice. Control
flow instructions may move the program to another location and, as a result, set the PC to a
specific address.
Instruction
Decode
Status
Register ALU
1.5.1 Instructions
• The CPU understands and can execute a limited set of instructions — ~88 unique instructions
for the ATtiny1626
• Instructions are encoded in program memory as opcodes. Most instructions are two bytes
long, but some instructions are four bytes long
10
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
• The AVR Instruction Set Manual describes all the available instructions, and how they are
translated into opcodes
• Instructions fall into five categories:
– Arithmetic and logic — arithmetic and logical operations performed by the ALU
– Change of flow — jumping to specific locations of the program unconditionally, or
conditionally, by testing bits in the status register
– Data transfer — moving data in/out of registers, into the data space, or into RAM
– Bit and bit-test — inspecting data in registers (specifically for bit-level operations)
– Control — Special microcontroller instructions
A sequence of eight bits is known as a byte, and it is the most common representation of data in
digital systems. A sequence of four bits is known as a nibble. A sequence of 𝑛 bits can represent
up to 2𝑛 states.
1 The term bit comes from binary digit.
11
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
The subscript 2 indicates that the number is represented using a base-2 system. As with the familiar
decimal system, left-padded zeros do not change the value of a number, but are included here for
formatting purposes.
2.2.2 Octal
The octal system is a base-8 system. It is most notably used as a shorthand for representing
file permissions on UNIX systems, where three bits are used to represent read, write, execute
permissions for the owner of a file, the groups the owner is part of, and other users.
08 = 0002 48 = 1002
18 = 0012 58 = 1012
28 = 0102 68 = 1102
38 = 0112 78 = 1112
As each octal digit maps to three bits, it is not very convenient for systems with byte-sized data.
Despite this, it is still available in many programming languages for historical reasons.
2.2.3 Hexadecimal
The hexadecimal system (hex) is a base-16 system. As we need more than 10 digits in this
system, we use the letters A-F to represent digits 10 to 15. Hex is a convenient notation when
working with digital systems as each hexadecimal digit maps to a nibble.
12
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
In the binary system (base-2) the unsigned integers are encoded using a sequence of binary digits
(0–1) in the same manner. For example,
101012 = 1 × 24 + 0 × 23 + 1 × 22 + 0 × 21 + 1 × 20
= 1 × 16 +0×8 +1×4 +0×2 +1×1
= 16 +0 +4 +0 +1
= 2110
The range of values an 𝑛-bit binary number can hold when encoding an unsigned integer is 0 to
2𝑛 − 1.
13
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
2.4.1 Sign-Magnitude
In sign-magnitude representation, the most significant bit encodes the sign of the integer. In an
8-bit sequence, the remaining 7-bits are used to encode the value of the bit.
• If the sign bit is 0, the remaining bits represent a positive value,
• If the sign bit is 1, the remaining bits represent a negative value.
As the sign bit consumes one bit from the sequence, the range of values that can be represented by
an 𝑛-bit sign-magnitude encoded bit sequence is:
− (2𝑛−1 − 1) to 2𝑛−1 − 1.
For 8-bit sequences, this range is: −127 to 127. However, this presents several issues:
1. There are two ways to represent zero: 0b10000000 = 0, or 0b00000000 = -0.
2. Arithmetic and comparison requires inspecting the sign bit
3. The range is reduced by 1 (due to the redundant zero representation)
14
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
2.5.2 Negation
NOT is a unary operator used to invert a bit.
𝑎 NOT 𝑎
0 1
1 0
2.5.3 Conjunction
AND is a binary operator whose output is true if both inputs are true.
𝑎 𝑏 𝑎 AND 𝑏
0 0 0
0 1 0
1 0 0
1 1 1
2.5.4 Disjunction
OR is a binary operator whose output is true if either input is true.
𝑎 𝑏 𝑎 OR 𝑏
0 0 0
0 1 1
1 0 1
1 1 1
15
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
𝑎 𝑏 𝑎 XOR 𝑏
0 0 0
0 1 1
1 0 1
1 1 0
byte: 1
1 0 0 1 0 0 0
∨ ∨ ∨ ∨
bitmask: 0 1 0 1 0 0 1 1
result: 1 1 0 1 1 0 1 1
Figure 2: Setting bits using the logical or.
16
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
byte: 0
0 0 1 1 1 1 0
∧ ∧ ∧
bitmask: 1 0 1 1 1 0 0 1
result: 0 0 0 1 1 0 0 0
Figure 3: Clearing bits using the logical and.
byte: 1 1 1 1 0 1 0 0
⊕ ⊕ ⊕ ⊕
bitmask: 1 1 0 0 0 0 1 1
result: 0 0 1 1 0 1 1 1
Figure 4: Toggling bits using the logical exclusive or.
• Shifts
– Logical
– Arithmetic (for signed integers)
• Rotations
17
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
2.6.6 Shifts
Shifts are used to move bits within a byte. In many programming languages this is represented by
two greater than >> or two less than << characters.
𝑎≫𝑠
shifts the bits in 𝑎 by 𝑠 places to the right while adding 0’s to the MSB.
1 1 0 0 0 1 0 0 C
0 1 1 0 0 0 1 0
Figure 5: Right shift using lsr in AVR Assembly.
Similarly,
𝑎≪𝑠
shifts the bits in 𝑎 by 𝑠 places to the left while adding 0’s to the LSB.
C 1 1 0 0 0 1 0 0
1 0 0 0 1 0 0 0
Figure 6: Left shift using lsl in AVR Assembly.
When using signed integers, the arithmetic shift is used to preserve the value of the sign bit when
shifting.
1 1 0 0 0 1 0 0 C
1 1 1 0 0 0 1 0
Figure 7: Arithmetic right shift using asr in AVR Assembly.
18
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
Left shifts are used to multiply numbers by 2, whereas right shifts are used to divide numbers by
2 (with truncation).
2.6.7 Rotations
Rotations are used to shift bits with a carry from the previous instruction. To understand why,
calculate the decimal value of the resulting byte after a shift.
C 1 1 0 0 0 1 0 0
1 0 0 0 1 0 0 C C
Figure 8: Rotate left using rol in AVR Assembly.
1 1 0 0 0 1 0 0 C
C C 1 1 0 0 0 1 0
Figure 9: Rotate right using ror in AVR Assembly.
Here the blue bit is carried from the previous instruction, and the carry bit is updated to the value
of the bit that was shifted out. Rotations are used to perform multibyte shifts and arithmetic
operations.
; Accumulator
ldi r16, 0
19
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
; First number
ldi r17, 29
add r16, r17 ; R16 <- R16 + R17 = 0 + 29 = 29
; Second number
ldi r17, 118
add r16, r17 ; R16 <- R16 + R17 = 29 + 118 = 147
R16: 0 0 0 1 1 1 0 1
add
R17: 0 1 1 1 0 1 1 0
R16: 1 0 0 1 0 0 1 1
2.7.2 Overflows
When the sum of two 8-bit numbers is greater than 8-bit (255), an overflow occurs. Here we must
utilise a second register to store the high byte so that the result is represented as a 16-bit number.
To avoid loss of information, a carry bit is used to indicate when an overflow has occurred. This
carry bit can be added to the high byte in the event that an overflow occurs. The following example
shows how to use the adc instruction to carry the carry bit when an overflow occurs.
; Low byte
ldi r30, 0
; High byte
ldi r31, 0
; First number
ldi r16, 0b11111111
; Add to low byte
add r30, r16 ; R30 <- R30 + R16 = 0 + 255 = 255, C <- 0
; Add to high byte
adc r31, r29 ; R31 <- R31 + R29 + C = 0 + 0 + 0 = 0
; Second number
ldi r16, 0b00000001
; Add to low byte
20
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
Therefore, the final result is: R31:R30 = 0b00000001:0b00000001 = 256. Below is a graphical
representation of the above code.
R30: 1 1 1 1 1 1 1 1
add
R16: 0 0 0 0 0 0 0 1
R30: 0 0 0 0 0 0 0 0 1
R31: 0 0 0 0 0 0 0 0
adc
R29: 0 0 0 0 0 0 0 0
R31: 0 0 0 0 0 0 0 1 1
2.7.3 Subtraction
Subtraction is performed using the same process as binary addition, with the subtrahend in two’s
complement form. In the case of overflows, the carry bit is discarded.
2.7.4 Multiplication
Multiplication is understood as the sum of a set of partial products, similar to the process used in
decimal multiplication. Here each digit of the multiplier is multiplied to the multiplicand and each
partial product is added to the result.
21
Microprocessors and Digital Systems 2 DIGITAL REPRESENTATIONS AND OPERATIONS
Given an 𝑚-bit and an 𝑛-bit number, the product is at most (𝑚 + 𝑛)-bits wide.
13 × 43 = 000011012 × 001010112
= 000011012 × 12
+ 000011012 × 102
+ 000011012 × 10002
+ 000011012 × 1000002
= 000011012
+ 000110102
+ 011010002
+ 1101000002
= 1000101111
Using AVR assembly, we can use the mul instruction to perform multiplication.
; First number
ldi r16, 13
; Second number
ldi r17, 43
; Multiply
mul r16, r17 ; R1:R0 <- 0b00000010:0b00101111 = 559
2.7.5 Division
Division, square roots and many other functions are very expensive to implement in hardware, and
thus are typically not found in conventional ALUs, but rather implemented in software. However,
there are other techniques that can be used to implement division in hardware. By representing
the divisor in reciprocal form, we can try to represent the number as the sum of powers of 2. For
example, the divisor 6.4 can be represented as:
1 10
= = 10 × 2−6
6.4 64
so that dividing an integer 𝑛 by 6.4 is approximately equivalent to:
𝑛
≈ (𝑛 × 10) ≫ 6
6.4
When the divisor is not exactly representable as a power of 2 we can use fractional exponents to
represent the divisor, however this requires a floating point system implementation which is not
provided on the AVR.
22
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
Part II
Microcontroller Fundamentals
3 Microcontroller Interfacing
3.1 Logic Levels
3.1.1 Discretisation
The process of discretisation translates a continuous signal into a discrete signal (bits). As an
example, we can translate voltage levels on microcontroller pins into digital logic levels.
3.1.3 Hysteresis
Hysteresis refers to the property of a system whose state is dependent on its history. In electronic
circuits, this avoids ambiguity in determining the state of an input as it switches between voltage
levels.
𝑡𝐻
Voltage
𝑡𝐿
𝐻
State
23
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
Given a transition:
• If an input is currently in the low state, it has not transitioned to the high state until the
voltage crosses the high input voltage threshold.
• If an input is currently in the high state, it has not transitioned to the low state until the
voltage crosses the low input voltage threshold.
It is therefore always preferable to drive a digital input to an unambiguous voltage level.
3.2.2 Current
Current 𝑖 is the rate of flow of electrical charge through a circuit, measured in Amperes (A).
• Current is measured through a circuit element.
3.2.3 Power
Power 𝑝 is the rate of energy transferred per unit time, measured in Watts (W). Power can be
determined through the equation
𝑝 = 𝑣𝑖.
3.2.4 Resistance
Resistance 𝑅 is a property of a material to resist the flow of current, measured in Ohms (𝛺).
Ohm’s law states that the voltage across a component is proportional to the current that flows
through it:
𝑣 = 𝑖𝑅.
Note that not all circuit elements are resistive (or Ohmic), as they do not follow Ohm’s law; this
can be seen in diodes.
24
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
𝑖𝑠
0 0
𝑖
𝑖
0 𝑣𝑠 0 𝑣𝑓
𝑣 𝑣
Although the wires used to connect a circuit are resistive, we usually assume that they are ideal,
that is, they have zero resistance.
𝑅
𝑖
+ 𝑣 −
Figure 14: Resistor circuit symbol.
3.3.2 Switches
A switch is used to connect and disconnect different elements in a circuit. It can be open or
closed.
• In the open state, the switch will not conduct2 current
25
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
(a) Single pole single throw switch. (b) Single pole double throw switch.
(c) Double pole single throw switch. (d) Double pole double throw switch.
3.3.3 Diodes
A diode is a semiconductor device that conducts current in only one direction: from the anode to
the cathode.
Anode 𝑖 Cathode
𝐴 𝐾
+ 𝑣 −
Figure 16: Diode symbol.
• When reverse biased, a diode does not conduct current, and the cathode-anode voltage is
equal to the applied voltage.
26
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
+ +
𝑣𝐴𝐾 𝑣𝐾𝐴
− 𝑖 𝑖 −
A diode is only forward biased when the applied anode-cathode voltage exceeds the forward voltage
𝑣𝑓 . A typical forward voltage 𝑣𝑓 for a silicon diode is in the range 0.6 V to 0.7 V, whereas for Light
Emitting Diodes (LEDs), 𝑣𝑓 ranges between 2 V to 3 V.
• Input pins are typically high-impedance, and they appear as an open circuit.
• Output pins are typically low-impedance, and will actively drive the voltage on a pin and
any connected circuitry to a high or low state. They can also drive connected loads.
27
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
• LOW by connecting the output net to the ground voltage GND (0 V).
+V
𝐴 𝑌
GND
Hence, the output state 𝑌 is determined by the logic level of the output driver 𝐴.
𝑌 = 𝐴.
𝐴 𝑌
LOW LOW
HIGH HIGH
The push-pull output 𝑌 can both source and sink current from the connected net.
OE
𝐴 𝑌
• When the OE signal is HIGH, the output state 𝑌 is determined by the output driver 𝐴.
• When the OE signal is LOW, the output state 𝑌 is in a high-impedance state.
28
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
𝐴 OE 𝑌
LOW LOW HiZ
HIGH LOW HiZ
LOW HIGH LOW
HIGH HIGH HIGH
+V OE
OE 𝐴 𝑌
pull-up pull-down
𝐴 𝑌
GND
• When no circuitry is actively driving the net, the resistor will passively pull the voltage to
either the voltage supply, or ground.
• When another device actively drives the net, the active device defines the voltage of the
net. Hence, the current from the resistor is simply sourced or sunk by the active device.
The resistors used as pull-up and pull-down resistors are typically in the k𝛺 range.
29
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
+V
pull-up
𝐴 𝑌
GND
30
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
Pull-up Enable
DIRn
D Q
Peripheral Override
R
OUTn
D Q
Pxn
Peripheral Override
R
Invert Enable
Synchronizer
INn
Synchronous
Q D Q D
Input
R R
Sense Configuration
Interrupt
Interrupt
Generator
Asynchronous
Input/Event
Input Disable
Peripheral Override
Analog
Input/Output
31
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
For example, assume an active HIGH device is connected to pin 5 on port B. To configure the pin
as an output and set the output state to HIGH:
; Enable output
sts PORTB_DIRSET, r16 ; Enable output on PB5
#include <avr/io.h>
32
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
• Peripheral functions can be mapped to different sets of pins to provide flexibility and to
avoid clashes when multiple peripherals are used in an application.
• When enabled, peripheral functions override standard port functions.
• The Port Multiplexer (PORTMUX) is used to select which pin set should be used by a
peripheral.
• Certain peripherals can have their inputs/outputs mapped to different sets of pins through
the PORTMUX peripheral’s configuration registers.
Note that we cannot re-map a single peripheral function to another pin, and must consider the
entire set.
Both of these configurations have their benefits, and the best configuration depends entirely on the
context.
33
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
𝑖 +V
1
0
GND 𝑖
Figure 24: Active high configuration. Figure 25: Active low configuration.
On the QUTy, the LED display is driven in the active low configuration. This has a number of
advantages:
• If the internal pull-up resistors are mistakenly enabled, no current will flow into the LEDs.
• The microcontroller pins can sink higher currents than they can source, allowing us to drive
the display to a higher brightness.
• The display used on the QUTy has a common anode configuration, hence we must use an
active low configuration to drive the display segments independently.
An LED is an example of a simple digital output, as we can map logical states to LED states
(lit or unlit) for a digital output.
+V +V
pin pin
GND GND
Figure 26: Active high configuration. Figure 27: Active low configuration.
34
Microprocessors and Digital Systems 3 MICROCONTROLLER INTERFACING
• When the switch is open, the pull-up/pull-down resistor is used to define the state of the
switch.
• When the switch is closed, the state of the pin is defined by the voltage connected to the
switch.
As with LEDs, we can interface switches to microcontroller pins in two different configurations:
• active high; in which case the pin is HIGH when the switch is closed.
• active low; in which case the pin is LOW when the switch is closed.
An active low configuration is usually preferred as:
• It allows for the utilisation of an internal pull-up resistor that is commonly implemented
in microcontrollers.
• It eliminates the risk of unsafe voltages being applied to the pin from the power supply in an
active high configuration.
• It is easier to access a ground reference on a circuit board.
35
Microprocessors and Digital Systems 4 INTRODUCTION TO AVR ASSEMBLY
Part III
AVR Assembly Programming
4 Introduction to AVR Assembly
Definition 4.1 (Word). A word refers to a value that is two bytes in size (16-bit).
Definition 4.2 (Registers). A register refers to a memory location that is 1 byte in size (8-bit).
The ATtiny1626 has 32 registers of which r16 to r31 can be loaded with an immediate value (0 to
255) using ldi.
Values are commonly loaded into registers as many other operations can be performed on them.
Instructions in Assembly language are mnemonics that represent a specific operation that the
microcontroller can perform. These instructions can take a number of operands that specify
the parameters of the operation. The particular syntax for an instruction and its operands is
dependent on the instruction, but generally, take the following form:
For many instructions, the first operand often corresponds to the destination of an operation.
4.1.1 Labels
Most change of flow instructions take an address in program memory as a parameter. Hence,
to make this process easier, we can use labels to refer to locations in program memory (and also
RAM).
new_location: ; Label
push r16
36
Microprocessors and Digital Systems 4 INTRODUCTION TO AVR ASSEMBLY
When a label appears in source code, the assembler replaces references to it with the address of the
directive/instruction immediately following that label. Labels work for both absolute and relative
addresses and the assembler will automatically adjust the address to the correct type. Labels can
also be used as parameters to other immediate instructions if we store the high and low bytes in
registers and wish to reference the location in an indirect jumping instruction.
• Skip instructions
37
Microprocessors and Digital Systems 4 INTRODUCTION TO AVR ASSEMBLY
ldi r16, 0
ldi r19, 10
cp r16, r19 ; Compare values in registers r16 and r19
brge new_location ; Branch if r16 greater than or equal to r19
new_location:
Note that many instructions are able to set the Z flag, which is used to indicate if the result of the
operation is zero. In these cases, the compare instruction may be redundant.
cp r16, r17
breq new_location ; Skips to new_location if r16 == r17
inc r16 ; This is skipped
Note that the number of cycles for a skip instruction depends on the size of the instruction being
skipped. The sbrc and sbrs instructions are used to skip the next instruction if the specified bit
a register is cleared/set.
38
Microprocessors and Digital Systems 4 INTRODUCTION TO AVR ASSEMBLY
The sbis and sbic instructions are used to skip the next instruction if the specified bit an I/O
register is set/cleared. For example, if we wish to toggle the decimal point LED (DISP DP) on the
QUTy (PORT B pin 5) when the first button (BUTTON0) was pressed (PORT A pin 4),
new_location:
4.3 Loops
By jumping to an earlier address, we can loop over a block of instructions.
infinite_loop:
; Code to repeat
rjmp infinite_loop
Loops can also be finite, in which case the loop will terminate when a counter reaches zero.
39
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
Loops can also be used to repeat until some external event occurs.
main_loop:
in r17, VPORTA_IN ; Read the input register of PORT A
andi r17, 0b00010000 ; Isolate pin 4
button_pressed:
; Execute instructions
rjmp main_loop ; Return to main loop
• The clock speed — frequency of the clocks oscillations (default: 20 MHz — configurable in
CLKCTLR_MCLKCTRLA)
• The prescaler — reduces the frequency of the CPU clock through division by a specific amount;
12 different settings from 1x to 64x (default: 6 — configurable in CLKCTLR_MCLKCTRLA)
The clock oscillates at its effective clock speed:
1
effective clock speed = clock speed ×
prescaler
As the default prescaler is 6, the default effective clock speed is 3.333 MHz. The effective clock
speed can therefore range between:
• Effective maximum clock frequency: 20 MHz (20 MHz clock & prescaler 1)4
• Effective minimum clock frequency: 512 Hz (32.768 kHz clock & prescaler 64)
Therefore to create a delay, we must first determine the required number of CPU cycles in the body
of the loop and iterate until the number of CPU cycles reaches the required amount. The following
examples utilise counters of various sizes to create delays. Note that 𝑛 represents the number of
iterations.
3 Note that this type of loop is not recommended for time-sharing systems, such as a personal computer, as the
lost CPU cycles cannot be used by other programs. In these cases, clock interrupts are preferred. However, on a
device such as the ATtiny1626, delay loops can be utilised to precisely insert delays in a program.
4 As the QUTy is supplied with 3.3 V, it is not safe to go above 10 MHz.
40
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
delay_1:
ldi r16, x ; 1 CPU cycle
ldi r17, 1 ; 1 CPU cycle ; Incrementor
loop:
add r16, r17 ; 1 CPU cycle
brcc loop ; 2 CPU cycles (1 CPU cycle when condition is false)
𝑥 = (28 − 1) − 𝑛 ⟺ 𝑛 = (28 − 1) − 𝑥
with
total cycles = 1 + 1 + (𝑛 + 1) + 2𝑛 + 1
= 3𝑛 + 4
for a maximum delay of 230.7 µs ((3 × (28 − 1) + 4) 𝑇 )5 . To create larger delays, we can use multiple
registers:
delay_2:
ldi r24, x ; 1 CPU cycle
ldi r25, y ; 1 CPU cycle
loop:
adiw r24, 1 ; 2 CPU cycles
brcc loop ; 2 CPU cycles (1 CPU cycle when condition is false)
(𝑦 ∶ 𝑥) = (216 − 1) − 𝑛 ⟺ 𝑛 = (216 − 1) − (𝑦 ∶ 𝑥)
with
total cycles = 1 + 1 + 2 (𝑛 + 1) + 2𝑛 + 1
= 4𝑛 + 5
delay_3:
ldi r24, x ; 1 CPU cycle
ldi r25, y ; 1 CPU cycle
ldi r26, z ; 1 CPU cycle
5𝑇 1
is the period of one CPU cycle (using the default clock configuration): 𝑇 = 20 MHz/6 = 300 ns.
41
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
loop:
adiw r24, 1 ; 2 CPU cycles
adc r26, r0 ; 1 CPU cycle (r0 represents a register with value 0)
brcc loop ; 2 CPU cycles (1 CPU cycle when condition is false)
SIGROW 0x1100-0x11FF
FUSE 0x1280-0x1289
LOCKBIT 0x128A-0x12FF
USERROW 0x1300-0x137F
EEPROM 0x1400-0x14FF
256 Bytes
Flash Code
(Reserved)
32 KB
SRAM 0x3400-0x3FFF
3 KB
(Reserved) 0x4000-0x7FFF
In-System Reprogrammable
Flash 0x8000-0xFFFF
32 KB
42
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
The following instructions may be used to access memory from the data space:
• lds (load direct from data space to register)
• sts (store direct from register to data space)
ldi r17, 24
st X, r17 ; Store byte from r16 to X
; The byte in X (and hence at RAMSTART) is now 24
43
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
This operation can be used to copy bytes from one location to another:
5.3 Stack
The stack is a last-in first-out (LIFO) data structure in SRAM. It is accessed through a register
called the stack pointer (SP), which is not part of the register file like SREG.
44
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
Upon reset, SP is set to the last available address in SRAM (0x3FFF), and can be modified through
push/pop and other methods that are generally not recommended.
• push stores a register to SP then decrements SP (SP ← SP − 1)
• pop increments SP (SP ← SP + 1) then loads to a register from SP
If a particular register is required without modifying other code, we can temporarily store the value
of that register on the stack, and pop it back when we are done:
5.4 Procedures
Procedures allow us to write modular, reusable code which makes them powerful when working on
complex projects. Although they are usually associated with high level languages as methods, or
functions, they are also available in assembly.
Procedures begin with a label, and end with the ret keyword. They must be called using the
call/rcall instructions.
procedure:
; Procedure body
ret ; Return to caller
rjmp main_loop
procedure:
push r16 ; Save r16 on the stack
; Code that possibly modifies r16
pop r16 ; Restore r16 from the stack
ret
main_loop:
ldi r16, 10
rcall procedure ; Call procedure
push r16 ; r16 should still be 10
45
Microprocessors and Digital Systems 5 WORKING WITH AVR ASSEMBLY
rjmp main_loop
; Calculate average
add r16, r17
ror r16
main_loop:
; Arguments
ldi r16, 100
ldi r17, 200
rcall average
rjmp main_loop
46
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
; Calculate average
add r16, r17
ror r16
main_loop:
; Arguments
ldi r16, 100
push r16
ldi r16, 200
push r16
rcall average
Part IV
C Programming
6 Introduction to C Programming
C is a programming language developed in the early 1970s by Dennis Richie. C is a compiled
language, meaning that a separate program is used to efficiently translate the source code into
assembly. Its compilers are capable of targeting a wide variety of microprocessor architectures
and hence it is used to implement all major operating system kernels. Compared to many other
languages, C is a very efficient programming language as its constructs map directly onto machine
instructions.
47
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
int main()
{
// Function body
return 0;
}
The purpose of returning a zero at the end of the main function is to signify the exit status code
of the process. An exit status of 0 is traditionally used to indicate success, while all non-zero values
indicate failure.
int main()
{
int x = 3;
{
int y = 4;
x = x + y;
}
// x is now 7
// y is no longer in scope
return 0;
}
C supports two styles of comments. The first of these are known as “C-style comments”, which
allow multi-line/block comments. Multi-line comments use the /* */ syntax.
/*
This is a multi-line comment.
It can span multiple lines.
*/
The second style is known as “C++-style comments”, which allow single-line comments. These
comments are denoted by the // syntax.
48
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
int x;
x = 4;
To optionally assign a value during declaration, we can apply the assignment operator after the
declaration. This is known as a variable initialisation, as we are assigning an initial value to the
variable.
int x = 4;
Note that using uninitialised variables results in unspecified behaviour in C, meaning that
the value of such variables is unpredictable. If we want to assign values to multiple variables of the
same type, we can use the comma (,) operator.
int x = 1, y = 2, z = 3;
We can also use the assignment (=) operator to assign the same value to multiple variables of the
same type.
int x, y, z;
x = y = z = 5;
Compound assignment operators perform the operation specified by the additional operator, then
assign the result to the left operand.
char x = 0b11001010;
x |= 0b00000001; // x = 0b11001010 | 0b00000001 = 0b11001011
49
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
int y = 25;
y += 5; // y = 25 + 5 = 30
char z = 0b10000010;
z <<= 1; // z = 0b10000010 << 1 = 0b00000100
6.2.2 Types
While AVR assembly uses 8-bit registers, C supports larger data types by treating them as a
sequence of bytes. We can also create compound data types with struct and union.
Type Specifiers
Type specifiers in declarations define the type of the variable. The signed char, signed int,
and signed short int, signed long int types, together with their unsigned variants and enum,
are all known as integral types. float, double, and long double are known as floating or
floating-point types. The following table summarises various numeric types in C:
Note that the size of these types is not necessarily the same across platforms, hence it is discouraged
to use these keywords for platform specific tasks. See the section on Exact Width Types for more
information.
Type Qualifiers
Types can be qualified with additional keywords to modify the properties of the identifier. Three
common qualifiers are const, static, and volatile.
50
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
• volatile — indicates that the variable can be modified or accessed by other programs or
hardware.
Portable Types
C has a set of standard types that are defined in the language specification, however the type
specifiers shown above may have different storage sizes depending on the platform. Although this
may be insignificant for most platforms, microcontrollers use specific sizes for registers, meaning it
is important to refer to the correct type specifiers when declaring a variable.
The standard integer (stdint.h) library provides exact-width type definitions that are specific
to the development platform. This ensures that variables can be initialised with the correct size on
any platform.
#include <stdint.h>
int8_t i8;
int16_t i16;
int32_t i32;
int64_t i64;
uint8_t ui8;
uint16_t ui16;
uint32_t ui32;
uint64_t ui64;
Floating-Point Types
The float and double types can store floating-point value types in C. Their implementation
allows for variable levels of precision, i.e., extremely large and small values. These types are very
useful on systems with a floating point unit (FPU) or equivalent. In most computer systems,
floating point types are represented as 32-bit IEEE 754 single precision floating point numbers.
The float type is a 32-bit floating point number and the double type is a 64-bit floating point
number.
A single precision floating point number has a 1-bit sign, 8-bit exponent, and 23-bit mantissa. As
such, the range of a single precision floating point number is −2127 … 2127 . The value of a floating
point number 𝑓 is defined as
𝑠
𝑓 = (−1) (1 + 2−23 𝑚) 2𝑒−127
where 𝑠 is the sign bit, 𝑚 is the mantissa, and 𝑒 is the exponent. Note that values are not equally
spaced and there are several special values that can be represented by floating point numbers.
• 𝑒 = 255 ⟹ 2128 :
– 𝑚 = 0 (all 0s): INFINITY if 𝑠 = 0, -INFINITY if 𝑠 = 1
51
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
6.3 Literals
6.3.1 Integer Prefixes
Integer literals are assumed to be base 10 unless a prefix is specified. C supports the following
prefixes:
• Binary (base 2) — 0b
• Octal (base 8) — 0 (note that C does not use the 0o prefix)
• Decimal (base 10) — no prefix
• Hexadecimal (base 16) — 0x
#include <stdio.h>
52
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
if (condition)
{
// Code to execute if condition is true
}
This structure can be nested and also supports else and else if statements.
if (x > 1)
{
// Code to execute if x is greater than 1
if (x < 10)
{
// Code to execute if x is greater than 1 and less than 10
}
} else if (x < -1)
{
// Code to execute if x is less than -1
if (x > -10)
{
// Code to execute if x is less than -1 and greater than -10
}
} else
{
// Code to execute if x is not greater than 1 and not less than -1
}
53
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
while (condition)
{
// Code to execute while condition is true
}
A do while loop is similar to a while loop, but the loop will execute at least once.
do
{
// Code to execute at least once
} while (condition);
int i = 0; // Iterator
i++; // Increment i by 1
}
Note the initialisation and increment statements are optional, and while the condition statement
is also optional, we must ensure that the loop can terminate from within the structure (see next
section).
54
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
if (i == 5)
{
break; // Terminate loop early
}
printf("%d\n", i);
}
If the loop is nested within another loop, the break and continue statements will only terminate
the innermost loop.
6.5 Expressions
C provides a number of operators which can be used to perform arithmetic/logical operations on
values. C follows the same precedence rules as mathematics, however caution should be used when
comparing precedence of certain logical and bitwise operations.
55
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
56
Microprocessors and Digital Systems 6 INTRODUCTION TO C PROGRAMMING
char x = 0b11001010;
unsigned char y = 0b01100001;
int x = 5;
int y = 10;
int z = 15;
if (x < y)
{
printf("x is less than y\n");
}
if (x != 15)
{
printf("x is not equal to 15\n");
}
int x = 5;
int y = 10;
int z = 15;
57
Microprocessors and Digital Systems 7 COMPILING AND LINKING C PROGRAMS
{
printf("x is less than y and x is not equal to 15\n");
}
int x = 5;
x++; // x = 6
The increment and decrement operators can be used as either prefix or postfix operators.
int x = 5;
int y = x++; // y = 5, x = 6
int z = ++x; // z = 7, x = 7
Prefix operators are evaluated before the statement is executed, while postfix operators are evaluated
after the statement is executed.
• #include "filename" — include programmer-defined header files that are typically in the
same directory as the file containing the directive.
When this directive is used, it is equivalent to copying the contents of the file into the current file,
at the location of the directive. The included file is also preprocessed and may contain other include
directives.
58
Microprocessors and Digital Systems 7 COMPILING AND LINKING C PROGRAMS
#include <stdio.h>
#define PI 3.14159265358979
int main()
{
printf("%f\n", 2 * PI);
return 0;
}
Aside from constant values, macros can also be used to create small compile-time “functions”, that
expand to code:
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
int main()
{
int x = 5;
int y = 10;
return 0;
}
Note that the semicolon is omitted at the end of the macro definition, as it would also be substituted
into the program. Only a single preprocessor directive can appear on a line, and the directives must
occupy a single line (note that a backslash (\) can be used to break long lines).
7.2 Compilation
After it has been preprocessed, a translation unit can be translated into machine code by a
compiler, similar to how assembly code is translated into machine code by an assembler. Note
that compilers may also emit assembly code as an intermediate step, which is then passed to an
assembler to produce machine code.
59
Microprocessors and Digital Systems 7 COMPILING AND LINKING C PROGRAMS
7.2.1 Compilers
A compiler is a program that translates a translation unit written in a high-level language such as
C. Compilers can be configured to produce executable code in various ways. Compiling without
optimisation will produce code that closely resembles source code, making the program easier to
debug. This often also results in faster compilation times. When releasing a program, a compiler
can also be configured to optimise code for minimum code size or maximum speed (or a combination
of both). Compilers offer some advantages over assembly code:
• Portability — high-level languages are more portable than assembly code, as they are not
tied to a specific instruction set architecture. This means that the same source code can be
used on another platform, granted that a compiler for that platform is available.
• Efficiency — compilers allow us to design efficient program through the use of compiler
optimisations, which can be difficult to achieve manually in assembly code.
7.2.2 Assemblers
Program code that is run directly on a CPU is not designed to be written by humans. The assembler
prioritises performance and size efficiency, and accounts for simplified chip design. An assembler
allows us to write programs in plain text without needing to memorise opcodes or manually keep
track of memory locations. Assemblers prioritise performance and size efficiency, and account for
simplified chip design in their operation. Modern assemblers also provide features such as macros
that allow us to write reusable code. While the scope of an assembler is limited, the programmer can
be confident that the code produced is efficient and optimal, as instructions are directly translated
into machine code. Some advantages of assemblers are described below:
• Efficiency — assemblers allow for precise control over the hardware, which may not be
available in a high-level language. In such cases, inline assembly can be used to write assembly
code within a high-level language.
• Precision — precise timing of code is possible due to the ability to predict how the assembler
generates code. Compilers may introduce additional instructions that can make it difficult to
predict the exact timing of code.
7.4 Linking
During the linking stage, the linker combines multiple object files and links them together to
produce a single executable file. The linker resolves all external references and updates addresses
where required. This step is necessary when source code is split into multiple files, and is extremely
fast as only addresses need to be updated. The linker can also perform some optimisations such
60
Microprocessors and Digital Systems 7 COMPILING AND LINKING C PROGRAMS
as dead code elimination, which removes unused code. In assembly, the .global directive can be
used to make labels available to the linker.
// function.S
.global function
function:
ret
// main.S
rcall function
In this example, both files can be compiled into object files even though the function label is not
defined in main.S. The linker will resolve the reference to function and produce a single executable
file. In C, top-level symbols are public by default, but can be made private to the current translation
unit by using the static keyword.
// main.c
static int a = 0;
// file1.c
a = 1; // Error: a is not visible
To make a symbol visible to other translation units, the extern keyword can be used.
// main.c
extern int a;
printf("%d\n", a); // Prints 5
// file1.c
int a = 5;
Any non-static symbols are implicitly global, and can be accessed from any translation unit.
7.5 Debugging
While most microcontrollers are equipped with debugging tools, we are often presented with no
debugging tools at all. Therefore, it is important to develop strategies to be able to systematically
debug embedded programs with access to basic I/O. Some simple methods include toggling pins on
the microcontroller to indicate the state of the program and sending formatted strings through the
serial port to a terminal. To route stdin and stdout to/from any serial communications interface
that can read and write characters (e.g., UART, SPI, I2 C, etc.), we can use the stdio.h library:
1. Declare function prototypes for the read/write functions (function names are arbitrary):
61
Microprocessors and Digital Systems 7 COMPILING AND LINKING C PROGRAMS
3. Implement the prototyped functions that read from the serial interface (i.e., via UART):
void stdio_init(void)
{
// Assumes serial interface is initialised elsewhere
stdout = &stdio;
stdin = &stdio;
}
Here we may use the following blocking functions to read and write characters to the UART
interface:
uint8_t uart_getc(void) {
while (!(USART0.STATUS & USART_RXCIF_bm)); // Wait for data
return USART0.RXDATAL;
}
void uart_putc(uint8_t c) {
while (!(USART0.STATUS & USART_DREIF_bm)); // Wait for TX.DATA empty
USART0.TXDATAL = c;
}
Assembly listings are also useful for debugging in extreme cases, as they allow us to see exactly
what instructions are being executed. This can be achieved via the avr-objdump tool.
62
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
8 Advanced C Programming
8.1 Pointers
When a variable is declared, the compiler automatically allocates a block of memory to store
that variable. We can access this block of memory directly using the identifier for that variable,
or indirectly, using a pointer. Pointers are variables that store the memory address of another
variable and are declared using the following syntax:
This code declares a variable ptr that “points to” a uint8_t. Internally, as this pointer is simply
an address, it only occupies the size of a memory address, which is 16 bits on the ATtiny1626.
8.1.1 Referencing
We can reference another variable using the following syntax:
uint8_t x = 5;
uint8_t *ptr = &x; // Address of x
The ampersand (&) operator is used here to return the address of the variable x. Notice that the
type of the pointer ptr must match the type of x to ensure that the pointer is correctly dereferenced.
If we know the address of a location in advance, we can also declare a pointer with a specific address,
ensuring that we cast this address to a pointer type:
8.1.2 Dereferencing
Once we have defined a pointer, we can indirectly access the value it references using the unary
dereference operator (*). This is also known as indirection.
uint8_t x = 5;
uint8_t *ptr = &x; // Address of x
63
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
• Non-constant Data — We can reference non-constant data without any qualifiers, as shown
in the examples above.
uint8_t a = 100;
uint8_t b = 200;
We can enforce constancy on the referenced variable by applying the const qualifier to the
data type in the pointer’s declaration, even if the data itself is not constant.
uint8_t a = 100;
uint8_t b = 200;
• Constant Data — We can reference constant data by using the const qualifier on the
referenced data type.
If we wish to modify the referenced variable, we can reference the variable without the const
qualifier:
64
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
uint8_t a = 100;
uint8_t b = 200;
• Constant Pointers to Constant Data — We can enforce both the pointer and the
referenced data to be constant by applying the const qualifier to both the pointer and the
referenced data type.
uint8_t a = 100;
uint8_t b = 200;
uint8_t a = 100;
uint8_t a = 100;
uint8_t b = 200;
These examples demonstrate that pointers are simply variables that store memory addresses. We
may increase the level of indirection by including more asterisks in a pointer declaration, but this
65
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
is generally not very common, nor is it particularly useful in most cases. In the following example,
we apply qualifiers to pointers referencing other pointers:
uint8_t a = 100;
uint8_t *ptr = &a; // Points to `a`
const uint8_t **ptr1 = &ptr; // Pointer to constant uint8_t
uint8_t * const *ptr2 = &ptr; // Pointer to constant pointer to uint8_t
uint8_t ** const ptr3 = &ptr; // Constant pointer to pointer to uint8_t
uint8_t a = 100;
uint8_t *ptr = &a; // Points to `a`
void *ptr;
Void pointers have no type, and as such, cannot be dereferenced without first being cast to a specific
type. On the other hand, a void pointer can be assigned to any other pointer type without a cast.
uint8_t a = 100;
void *ptr = &a; // Void pointer
66
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
8.1.7 Size-of
The sizeof function can be used to determine the size of a variable in bytes.
uint8_t a = 100;
uint16_t b = 200;
sizeof(a); // Returns 1
sizeof(b); // Returns 2
The brace ({ }) syntax can only be used to initialise an array and if the length of the array which
is being assigned is less than the length of the array being assigned to, the remaining values will be
set to 0.
8.2.1 Indexing
Array elements can be accessed with the array index operator ([ ]). In C, array indices start at 0.
It is undefined behaviour to access an array element which is out of bounds. However, it is possible
to have a pointer to an element one past the end of an array as long as the pointer is not dereferenced:
uint8_t a[10];
for (uint8_t i = 0; i < 10; i++) {
a[i] = i;
}
67
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
uint8_t a[10];
sizeof(a); // Returns 10
In the declaration of ptr, the array decays into a pointer. This property is especially useful when
accessing arrays through function parameters, as arrays themselves cannot be passed by value.
Instead, we can pass a reference to the array by taking advantage of array decay, ensuring that we
also pass a second parameter for the size of the array, as this information is lost.
Note that the parameter syntax uint8_t *arr is equivalent to uint8_t arr[], however the former
is generally preferred. The syntax arr[i] is equivalent to *(arr + i). This is possible because
arrays are stored contiguously in memory. Note that it is not possible to change an array’s address:
uint8_t a[10];
a++; // Error: Cannot change the address of an array
uint8_t a[10];
uint16_t b[5];
68
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
We divide by the size of the first element of the array because the type of the array may be larger
than 1 byte.
uint8_t a[10];
uint8_t b[10];
for (uint8_t i = 0; i < sizeof(a) / sizeof(a[0]); i++) {
b[i] = a[i];
}
The second approach uses the memcpy function from the string.h library, which performs the same
operation.
uint8_t a[10];
uint8_t b[10];
memcpy(b, a, sizeof(a) / sizeof(a[0]));
uint8_t a[][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
To declare a multidimensional array, all dimensions but the first need to be specified. The rows of
the array must be specified within additional braces ({ }). Elements can be accessed by specifying
the index of each dimension.
a[0][0]; // Returns 1
a[1][2]; // Returns 6
These arrays are also stored contiguously in memory, in row-major order, and hence pointer
arithmetic is performed differently.
69
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
uint8_t a[][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
uint8_t rows = 3;
uint8_t cols = 3;
// Single indexing
printf("%d ", a[i * cols + j]);
// Pointer arithmetic
printf("%d ", *(*(a + i) + j));
// Equivalent to: printf("%d ", *(a[i] + j));
// Each row is a pointer to the first element of that row
}
}
8.3 Functions
Procedures are called functions in C. Functions can return values and take arguments. The main
function is the entry point of a program.
int main(void) {
return 0;
}
Functions in C must be declared in the top-level of a C program, and thus cannot be declared inside
other functions. Functions are declared with the following syntax:
8.3.1 Parameters
The parameters of a function are local variables scoped to that function.
70
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
int main(void) {
uint8_t a = 10;
uint8_t b = 20;
When a function does not take any arguments, we can use the void keyword to prevent parameters
from being passed in accidentally.
void func(void) {
}
int main(void) {
uint8_t a = 10;
uint8_t b = 20;
The compiler uses the function prototype to generate the code required to call the function without
having to know the entire body of the function. The linker will then resolve all function calls
71
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
to the appropriate function definitions. Note that parameter names are not required in function
prototypes.
int main(void) {
uint8_t a = 10;
uint8_t b = 20;
swap(&a, &b);
}
8.4 Scope
All variables and other identifiers in C are scoped. Scope affects the visibility and lifecycle of
variables. Scope is hierarchical, meaning that variables declared in a parent scope are visible to
all child scopes. Variables declared in a child scope can also hide variables declared in a parent
scope declared with the same name.
int main(void) {
72
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
Global variables are allocated a fixed location in SRAM and do not exist on the stack.
The volatile keyword is important here because this data is outside the control of the program.
The compiler will therefore not optimise accesses to this variable. The avr/io.h header file includes
macros and type definitions for accessing various registers on the AVR microcontroller with similar
declarations.
#include <avr/io.h>
73
Microprocessors and Digital Systems 8 ADVANCED C PROGRAMMING
Applying this type cast does not make this code more portable, but rather it tells the compiler that
the programmer is aware of the conversion being made, and that it is intentional. Some common
type casts are performed between:
• Integer types: Conversions between integer types (signed or unsigned) will expand or narrow
the type, resulting in a truncation or zero extension.
(uint8_t)-3 // 253
• Floating point types: Conversions from floating point types to integer types results in
truncation of the fractional part.
(int16_t)-3.45 // -3
• Pointer types: Conversions on pointer types change the pointer’s type but do not affect the
referenced data. As platforms may store data differently, these conversions are not portable.
uint16_t a = 12345;
uint8_t *b = (uint8_t *)&a; // likely 57, but may vary on different platforms
Casting an integer to a pointer will cause the resulting value to be treated as an address.
Likewise, casting a pointer to an integer will cause the pointer to be treated as an integer,
effectively giving us the value of the pointer.
These conversions are not portable, but are often necessary when accessing memory mapped
IO.
74
Microprocessors and Digital Systems 9 OBJECTS
Type casting can also be used to add/remove qualifiers such as const and volatile, although this
can lead to undefined behaviour if not used correctly. For example, some platforms store const
variables in read-only memory, and attempting to modify these variables is undefined behaviour.
Casting can also be used to avoid truncation errors when performing arithmetic.
uint16_t a = 25000;
uint16_t b = 10000;
9 Objects
9.1 Structures
Structures are used to group related data together.
struct Point {
uint8_t x;
uint8_t y;
};
struct Point p;
p.x = 30;
p.y = 40;
Unlike arrays, structures need not be accessed via pointers and can be passed between functions,
and copied normally.
75
Microprocessors and Digital Systems 9 OBJECTS
Due to this, structs can contain arrays which can be passed and copied by placing them in structs.
Along with this, functions can also return structs.
struct {
uint8_t x;
uint8_t y;
} p;
struct Point {
uint8_t x;
uint8_t y;
};
76
Microprocessors and Digital Systems 9 OBJECTS
struct Rectangle {
struct Point p1;
struct Point p2;
};
When accessing members of structs through pointers, the arrow operator (->) can be used. Structures
can also contain pointers.
9.1.5 Typedef
Typedefs can be used to give a type an alias so that the variables type is determined by the typedef
instead of the actual type. If we want to use a structure multiple times, we can use a typedef to
give it a (new) name.
The type of this variable is Point. The struct also need not be defined inside of the typedef.
struct PointStruct {
uint8_t x;
uint8_t y;
};
Point p = { 30, 40 };
77
Microprocessors and Digital Systems 9 OBJECTS
This can be useful when the struct is defined in a header file and the typedef is defined in a source
file. In both cases, it is still possible to use the struct name to declare variables.
struct PointStruct p;
typedef struct {
uint8_t x;
uint8_t y;
} Point;
In this case, the struct name cannot be used to declare variables as it is anonymous. Typedefs can
also be used with qualifiers to reduce unnecessary code.
9.2 Unions
Unions have similar syntax to structures, but all the members of a union share the same overlapping
memory. While structs have capacity to store multiple members, unions only have the capacity to
store their largest member.
union Character {
char character;
uint8_t integer;
};
This allows us to interpret the same memory location as multiple types without needing to perform
a cast. When used with structs (or other aggregates), the order of members in those structs is also
maintained.
struct a {
uint8_t i;
float f;
};
struct b {
uint8_t i;
78
Microprocessors and Digital Systems 9 OBJECTS
char c[4];
};
union u {
struct a a;
struct b b;
};
union u u;
u.a.i = 10;
u.a.f = 3.14;
u.b.c[0] = 'A';
u.b.c[1] = 'B';
u.b.c[2] = 'C';
u.b.c[3] = 'D';
9.3 Bitfields
Bitfields can be used within structures or unions to specify types of specific bit sizes.
struct {
uint8_t x : 4;
uint8_t y : 4;
} bits;
bits.x = 13;
bits.y = 7;
In this example, the members x and y each occupy 4 bits. Note that the base type of each member
must be able to store the specified number of bits.
79
Microprocessors and Digital Systems 9 OBJECTS
struct {
uint8_t x : 4;
uint8_t y : 4;
} bits;
struct {
uint8_t x : 4;
uint8_t y[4] : 4;
} bits; // Error
struct {
uint8_t x : 4;
uint8_t : 4;
} bits;
A zero-width bitfield can be used to align the next member to the next word boundary.
struct {
uint8_t x : 4;
uint8_t : 0;
uint8_t y;
} bits;
9.4 Strings
In C, strings are represented as arrays of characters, terminated by a character of value 0. Strings
that are declared using double quotes ("") are automatically terminated by this “null character”.
80
Microprocessors and Digital Systems 10 INTERRUPTS
In the example above, the compiler automatically allocates a block of memory to store the string,
which in this case is 12 bytes long (11 characters + null terminator). The pointer str points to the
first character in the string.
When using the printf function, the null terminator is required to indicate the end of the string.
Strings can therefore be indexed and passed to functions like arrays.
10 Interrupts
An interrupt is a signal sent to the processor to indicate that it should interrupt the current code
that is being executed to execute a function called an interrupt service routine (or interrupt
handler). Rather than polling for individual events (such as button presses), interrupts allow the
processor to be notified when an event occurs.
81
Microprocessors and Digital Systems 10 INTERRUPTS
It is also important to restore the state of the CPU or registers before the ISR returns. This is
because another interrupt can be triggered while the ISR is executing. To tackle this, we can use
the ISR macro from the avr/interrupt.h header file which will automatically save and restore the
state of the CPU and registers.
#include <avr/interrupt.h>
ISR(TCB0_INT_vect) {
// Interrupt service routine for TCB0
}
This header file also sets aside program memory for the interrupt vector table.
82
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
cli();
// Enable pull-up resistor and interrupt on falling edge
PORTA.PIN4CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
sei();
11 Hardware Peripherals
Microcontrollers typically include a variety of hardware peripherals that remove the burden of
having to write software for common functionality such as timers, serial communication, and
analogue to digital conversion. They can provide very precise timing and very fast (nanosecond)
response times. Hardware peripherals can run independent of the CPU (in parallel) so that:
All control of, and communication with peripherals is done through peripheral registers which
the CPU can access via the memory map. Peripherals also typically have direct access to hardware
resources such as pins.
83
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
11.2 Timers
Timers provide precise measurements of elapsed clock cycles in hardware, independent of software
and the CPU. Timers are used to generate periodic events (via an interrupt), measure time between
two events, or generate periodic signals on a pin.
1
𝑇clk = ,
𝑓main /prescaler
where 𝑓main is the frequency of the main clock and prescaler is the prescaler applied to the timer
peripheral. Here we assume the main clock frequency to be 20 MHz/6, where the main clock has
its own default prescaler of 6. If we want to generate an event every 𝑇 seconds, we need to set the
period register to be the number of counts of the timer clock required to reach the period 𝑇 :
𝑇
𝑛=
𝑇clk
Here, 𝑛 is the value that should be written to the period register, PER. The value of the prescaler
influences the timer period and timer resolution, as an increase in the prescaler leads to both an
increase in the period duration and a decrease in the timer resolution (time elapsed between each
count). Therefore, the smallest prescaler that allows the desired period should be chosen.
#include <avr/io.h>
#include <avr/interrupt.h>
void tcb_init()
84
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
{
TCB0.CTRLB = TCB_CNTMODE_INT_gc; // Configure TCB0 in periodic interrupt mode
TCB0.CCMP = 3333; // Set interval for 1 ms (3333 clocks @
↪ 3.333 MHz)
TCB0.INTCTRL = TCB_CAPT_bm; // Invoke the CAPT ISR when the counter
↪ reaches CCMP
TCB0.CTRLA = TCB_ENABLE_bm; // Enable TCB0
}
int main(void)
{
cli();
tcb_init();
sei();
while (1);
}
#include <avr/io.h>
#include <avr/interrupt.h>
85
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
void tca_init()
{
// DISP EN
PORTB.DIRSET = PIN1_bm;
int main(void)
{
cli();
tca_init();
sei();
while (1);
}
As the duty cycle of this PWM signal is set to 100%, the display will be at full brightness. To
dynamically change the brightness of the display, the compare value can be changed using the
buffered register TCA0.CMP1BUF. The same approach should be used to update the period register.
This is done to ensure that a new value is updated only when the counter is at the bottom of the
waveform.
11.4.1 Quantisation
Discretisation in amplitude is referred to as quantisation. Each amplitude is assigned a digital
code. The code width determines the amplitude resolution and introduces quantisation
error.
11.4.2 Sampling
Discretisation in time is referred to as sampling. The sampling rate determines the time
resolution and introduces aliasing error. This rate is typically the period of the CPU clock.
86
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
void adc_init()
{
// Select AIN2 (potentiometer R1)
ADC0.MUXPOS = ADC_MUXPOS_AIN2_gc;
// Need 4 CLK_PER cycles @ 3.333 MHz for 1us, select VDD as ref
ADC0.CTRLC = (4 << ADC_TIMEBASE_gp) | ADC_REFSEL_VDD_gc;
// Sample duration of 64
ADC0.CTRLE = 64;
// Free running
ADC0.CTRLF = ADC_FREERUN_bm;
// Select 8-bit resolution, single-ended
ADC0.COMMAND = ADC_MODE_SINGLE_8BIT_gc | ADC_START_IMMEDIATE_gc;
// Enable ADC
ADC0.CTRLA = ADC_ENABLE_bm;
}
int main(void)
{
cli();
adc_init();
sei();
while (1)
{
printf("%u\n", ADC0.RESULT0);
}
}
87
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
• Simplex: unidirectional communication. Requires one wire as data only flows in one direction.
• Half-duplex: bidirectional communication, occurring in one direction at a time. Requires
one wire as data flows in one direction at a time.
• Full-duplex: bidirectional communication, occurring simultaneously. Requires two wires as
data flows in both directions simultaneously.
• Synchronous: communication relying on a shared clock. Requires one wire for the clock
signal.
• Asynchronous: communication that does not rely on a shared clock.
There are many serial interfaces that can be used in embedded systems for communication. Some
common interfaces include:
• UART: Universal asynchronous receiver/transmitter.
• I2 S: Inter-IC sound.
11.5.2 UART
UART is a simple and cost effective serial communication protocol. As it is asynchronous, its clock
is not shared between the two communicating devices. Instead, the sender and receiver must agree
on a baud rate (the number of bits transmitted per second). This is typically in the range of
9600 baud to 115 200 baud (with a 2 M baud maximum). UART is a frame based protocol, where
each frame is signalled by a start bit (always LOW), and is fixed in length and format. UART can
be used in both full-duplex or half-duplex, depending on the hardware implementation, where the
transmitter and receiver are fully independent. This means either a 1- or 2-wire mode is possible
(plus 1 for GND).
88
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
2. When the TX shift register is empty, the data will immediately be copied into the shift
register.
3. Data is shifted out from the TX shift register, one bit at a time, according to the baud rate.
4. The transmitter is double-buffered so that:
• If a second byte is loaded into the TXDATA register before the first byte has finished
transmitting, this byte will be transferred into the TX buffer and transmitted after the
first byte.
• Additionally, writing a third byte will cause it to remain in the TXDATA register, until
the previous two bytes have been transmitted.
89
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
• If a second byte is shifted out of the shift register before the first byte is read, it will be
stored in the RX buffer.
• Additionally, a third byte will remain in the RX shift register until the RX buffer is
empty.
For more information about the USART0 peripheral, see the datasheet.
void uart_init(void)
{
PORTB.DIRSET = PIN2_bm; // Output enable TX pin
USART0.BAUD = 1389; // 9600 baud
USART0.CTRLA = USART_RXCIE_bm; // Enable RX interrupt
USART0.CTRLB = USART_RXEN_bm | USART_TXEN_bm; // Enable RX and TX
}
void spi_init(void)
{
PORTC.DIRSET = PIN0_bm | PIN2_bm; // Output enable SPI CLK and SPI MOSI
PORTMUX.SPIROUTEA = PORTMUX_SPI0_ALT1_gc;
90
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
91
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
// 5ms interrupt
ISR(TCB0_INT_vect)
{
static uint8_t current_display_side = 0;
if (current_display_side)
{
SPI0.DATA = left_byte;
}
else
{
SPI0.DATA = right_byte;
}
// Toggle side
current_display_side ^= 1;
92
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
ISR(SPI0_INT_vect)
{
// Latch the byte
PORTA.OUTCLR = PIN1_bm; // Prepare for rising edge
PORTA.OUTSET = PIN1_bm; // Generate rising edge
• when a pushbutton is pressed, the corresponding net is pulled LOW, and the pins value is 0.
• when the pushbutton is released, the corresponding net is pulled HIGH, and the pins value 1.
The state of individual pushbuttons can be read using bitwise AND operations with the PORTA.IN
register.
#include <avr/io.h>
PORTA.PIN4CTRL = PORT_PULLUPEN_bm;
while (1)
{
if (PORTA.IN & PIN4_bm)
{
// Pushbutton is released
}
else
{
// Pushbutton is pressed
}
}
In this loop structure, the state of the pushbutton will be in the pressed state until the pushbutton
is released. This may not be desirable if we want to perform a single action when the pushbutton
is pressed. To solve this, we must respond to a change in state.
• A falling edge is created when the pushbutton is pressed (transition from 1 to 0).
• A rising edge is created when the pushbutton is released (transition from 0 to 1).
93
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
This is known as edge detection. To implement this in C, we can use the XOR operator (^) to
detect a change in the signal. To identify the direction of the edge, we can additionally condition
the result with one of the states.
while (1)
{
pb_previous_state = pb_current_state; // save previous state
pb_current_state = PORTA.IN; // update with latest measurement
6 What is acceptably small depends on what magnitude of latency is perceptible and is specific to the application.
94
Microprocessors and Digital Systems 11 HARDWARE PERIPHERALS
if (pb_edge)
{
if (counter-- == 0)
{
// Update debounced state
pb_debounced_state = pb_sample;
// Reset counter
counter = 3;
}
}
else
{
// Reset counter
counter = 3;
}
The above implementation can only debounce a single pushbutton, as the counter corresponds
to a single pushbutton, S1. To debounce multiple pushbuttons, we can declare counters for each
pushbutton, however this can lead to a large amount of duplicate code. Instead, we can utilise
vertical counters.
// We can also use PIN4_bm | PIN5_bm | PIN6_bm | PIN7_bm, as we do not care about
↪ the other bits
volatile uint8_t pb_debounced_state = 0xFF;
95
Microprocessors and Digital Systems 12 STATE MACHINES
// Update counters
// If the state of the pushbutton has changed, increment the counter
counter1 = (counter1 ^ counter0) & pb_edge;
counter0 = ~counter0 & pb_edge;
This code allows us to debounce up to 8 pushbuttons using a single 8-bit variable. The debounced
state of the pushbuttons is stored in pb_debounced_state and can be used to perform edge
detection similar to the code in the previous section. This variable is updated if the state of
the pushbutton is consistent over 3 samples, or if a falling edge is detected. Note that if both falling
and rising edges need to be detected, it is not recommended to immediately update the debounced
state on a falling edge, as this leads to inconsistent latency between rising and falling edges.
12 State Machines
A state machine or finite state machine (FSM) is a mathematical model of computation in which a
machine can only exist in one of a finite number of states. The machine transitions between states
in response to inputs, and performs actions during transitions. A state machine is fully defined by
its list of states, initial state, and the conditions for transitioning between states.
96
Microprocessors and Digital Systems 12 STATE MACHINES
typedef enum
{
START,
STATE1,
STATE2
} state_t;
// Initial state
state_t state = START;
while (1)
{
// State machine
switch (state)
{
case START:
if (condition1)
{
// Configure outputs for STATE1 state
97
Microprocessors and Digital Systems 12 STATE MACHINES
state = START;
}
break;
default: // Invalid state (program should never reach this state)
// Configure outputs for START state
// Go back to start
state = START;
break;
}
}
typedef enum
{
FALSE,
TRUE
} boolean_t;
While they can be compared to integers, it is recommended to use the enumerators in comparisons.
Enumerated types can also be defined with explicit values, and can be used to represent bitmasks.
typedef enum
{
MONDAY = 0b00000001,
TUESDAY = 0b00000010,
WEDNESDAY = 0b00000100,
THURSDAY = 0b00001000,
FRIDAY = 0b00010000,
SATURDAY = 0b00100000,
SUNDAY = 0b01000000,
WEEKEND = SATURDAY | SUNDAY,
WEEKDAY = MONDAY | TUESDAY | WEDNESDAY | THURSDAY | FRIDAY
} DAYS;
98
Microprocessors and Digital Systems 13 SERIAL PROTOCOLS
13 Serial Protocols
A serial protocol is an agreed-upon standard by which two devices can communicate with each
other, enabling them to exchange data. UART and SPI are standards for transmitting data, but
do not ascribe any meaning to the data.
13.1.2 Symbols
A symbol is the fundamental data type used in serial communication protocols, which can be
comprised of several bits. The number of bits is usually set by the underlying medium and depends
on the baud rate. Smaller symbols are more flexible and allow for more symbols to be transmitted,
whereas larger symbols are more efficient and allow for more data to be transmitted.
13.1.3 Messages
If the information to be exchanged can be entirely encoded within a single symbol, there is no need
for a message structure. However, more complex protocols require a message structure for large
quantities of data or information of variable length. This is done by dividing the communication
into discrete messages.
99
Microprocessors and Digital Systems 13 SERIAL PROTOCOLS
13.1.4 Encoding
The choice of encoding may also be of concern, depending on the communication medium, symbol
length, and other factors such as human readability. For example using the entire ASCII character
set may not be desirable as it is not human-readable. Human-readable encoding schemes usually
limit the number of symbols to a small subset of the ASCII character set:
• ASCII 32-126 (0x20-0x7E) which uses 8-bit symbols
• Base64 (0-9, A-Z, a-z, +, /) which encodes 6 bits into an 8-bit symbol
• Hexadecimal (0-9, A-F) which encodes 4-bits per symbol
100
Microprocessors and Digital Systems 13 SERIAL PROTOCOLS
13.1.10 Payloads
A payload is used when a message identifier alone is insufficient to convey the information required.
Payloads should be as small as possible to reduce the overhead of the protocol, as longer payloads
increase the risk of transmission errors, so it may be preferable to split a large payload into multiple
messages.
• For fixed length payloads, the message type itself may define the payload length and hence
will know when to expect the end of the payload.
• For variable length payloads, the payload length is encoded in the message itself, by either
specifying the length within the payload, or by using a delimiter to indicate the end of the
payload.
101
Microprocessors and Digital Systems 13 SERIAL PROTOCOLS
13.1.14 Handshakes
A protocol where the sender purely transmits data does not know whether the same information
has been received and handled by the receiver. As such, it is common for protocols where only side
is sending information and the other is receiving, to have the receiving side acknowledge what it has
received (if the serial communications medium is half-duplex or full-duplex). The most common
form of handshakes are ACK and NACK messages (for acknowledged and not acknowledged).
• ACK indicates that the message was received, and that its contents were understood.
• NACK indicates that the message was received, but that there was an error in the message.
For example if the message was malformed, failed its checksum, or was unable to be processed.
In these situations, the sender may retransmit the message, or send a different message.
102
Microprocessors and Digital Systems 13 SERIAL PROTOCOLS
103