Warwick A. Smith Warwick A. Smith lives in South Africa and works as an Electronics Engineer and Embedded System Programmer. He is a bestselling author of the books C Programming for Embedded Microcontrollers, ARM Microcontroller Interfacing and Open Source Electronics on Linux. ISBN 978-1-907920-46-2 Arduino is the hardware platform used to teach the C programming language as Arduino boards are available worldwide and contain the popular AVR microcontrollers from Atmel. Atmel Studio is used as the development environment for writing C programs for AVR microcontrollers. It is a full-featured integrated development environment (IDE) that uses the GCC C software tools for AVR microcontrollers and is free to download. • Start learning to program from the very first chapter • No programming experience is necessary • Learn by doing - type and run the example programs • A fun way to learn the C programming language • Ideal for electronic hobbyists, students and engineers wanting to learn the C programming language in an embedded environment on AVR microcontrollers • Use the free full-featured Atmel Studio IDE software for Windows • Write C programs for 8-bit AVR microcontrollers as found on the Arduino Uno and MEGA boards • Example code runs on Arduino Uno and Arduino MEGA 2560 boards and can be adapted to run on other AVR microcontrollers or boards • Use the AVR Dragon programmer / debugger in conjunction with Atmel Studio to debug C programs DESIGN www.elektor.com Technology is constantly changing. New microcontrollers become available every year. The one thing that has stayed the same is the C programming language used to program these microcontrollers. If you would like to learn this standard language to program microcontrollers, then this book is for you! AVR MICROCONTROLLERS AND ATMEL STUDIO FOR C PROGRAMMING WITH ARDUINO LEARN Elektor International Media BV C PROGRAMMING WITH ARDUINO C PROGRAMMING WITH ARDUINO ● WARWICK A. SMITH AVR MICROCONTROLLERS AND ATMEL STUDIO FOR Warwick A. Smith LEARN DESIGN SHARE SHARE LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● S ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE GN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE C Programming with Arduino ● Warwick Smith an Elektor Publication LEARN DESIGN SHARE ● This is an Elektor Publication. Elektor is the media brand of Elektor International Media B.V. 78 York Street London W1H 1DP, UK Phone: (+44) (0)20 7692 8344 © Elektor International Media BV 2016 First published in the United Kingdom 2016 159020-1/EN ● All rights reserved. No part of this book may be reproduced in any material form, including photocopying, or storing in any medium by electronic means and whether or not transiently or incidentally to some other use of this publication, without the written permission of the copyright holder except in accordance with the provisions of the Copyright, Designs and Patents Act 1988 or under the terms of a licence issued by the Copyright Licensing Agency Ltd, 90 Tottenham Court Road, London, England W1P 9HE. Applications for the copyright holder’s written permission to reproduce any part of this publication should be addressed to the publishers. The publishers have used their best efforts in ensuring the correctness of the information contained in this book. They do not assume, and hereby disclaim, any liability to any party for any loss or damage caused by errors or omissions in this book, whether such errors or omissions result from negligence, accident or any other cause. ● British Library Cataloguing in Publication Data Catalogue record for this book is available from the British Library 978-1-907920-46-2 ● ISBN EISBN 978-3-89576-352-6 EPUB 978-3-89576-353-3 Prepress production: DMC ¦ daverid.com Printed in the Netherlands by Wilco Elektor is part of EIM, the world’s leading source of essential technical information and electronics products for pro engineers, electronics designers, and the companies seeking to engage them. Each day, our international team develops and delivers high-quality content - via a variety of media channels (e.g., magazines, video, digital media, and social media) in several languages - relating to electronics design and DIY electronics. www.elektor.com LEARN DESIGN SHARE Table of Contents Table of Contents Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Learning C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Target Audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 AVR Microcontrollers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 The AVR Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 Embedded Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Choosing an Embedded System to Learn C On . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 Why use the C Programming Language? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 Hardware Requirements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Arduino Board . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 Programmer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 The Approach Taken in this Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Some Good Advice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Accompanying Files and Support Website . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 Chapter 1 Your First C Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1.1 Download and Install Atmel Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1.1.1 Downloading Atmel Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25 1.1.2 Installing Atmel Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.2 Download and Install a Terminal Emulator . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.3 Install Template Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 1.4 Connecting the Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.5 How C Programs are Created . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.6 Start Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 1.6.1 Creating a New Project from the Template Files . . . . . . . . . . . . . . . . . . . . . 27 1.6.2 Building the Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 1.6.3 Build Problems and Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 1.6.4 Loading and Running the Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 1.6.5 Program Loading and Running – Problems and Solutions . . . . . . . . . . . . . . 33 1.7 About Your First C Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 ●5 C Programming with Arduino 1.8 Analysing the Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 1.8.1 Characters and Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 1.8.2 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 1.8.3 Program Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 1.8.4 Preprocessor Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 1.8.5 Whitespace Characters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 1.8.6 Ending the Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 1.9 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 1.9.1 Three Text Lines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 1.9.2 Find the Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 1.10 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 1.10.1 Solution to 1.9.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 1.10.2 Solution to 1.9.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 1.11 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 Chapter 2 C Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.1 Input, Output and Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.1.1 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 2.1.2 Getting Input from the User Using the scanf() Function . . . . . . . . . . . . . . . 44 2.2 Variable Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 2.2.1 Floating Point Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 2.2.2 Character Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 2.3 Arithmetic Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 2.4 Field Width Specifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 2.5 Compiling and Linking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 2.6 Errors and Warnings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 2.6.1 Compile Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 2.6.2 Link Errors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 2.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 2.7.1 Ohm’s Law Calculations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 2.7.2 Variable Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 2.8 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 2.8.1 Solution to 2.7.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 ●6 Table of Contents 2.8.2 Solution to 2.7.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 2.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 Chapter 3 Comparative Operators and Decisions . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.1 Comparative Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.1.1 True and False . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 3.1.2 C Comparative Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 3.2 Decisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 3.2.1 Using if to Make a Decision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.2.2 Using else in Conjunction with if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 3.2.3 The = Operator and the == Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 3.2.4 Using else – if in Conjunction with if . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 3.3 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.3.1 Variable Compare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.3.2 Variable Compare 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.4 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.4.1 Solution to 3.3.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 3.4.2 Solution to 3.3.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 3.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72 Chapter 4 The while Loop and Commenting Code . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.1 The while Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 4.2 Using if Inside the while Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.3 The Guess My Number Game . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 4.4 Back to the Temperature Controller Example . . . . . . . . . . . . . . . . . . . . . . . . . . 78 4.5 Commenting Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 4.6 Programming Style . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4.6.1 Tab Settings in Atmel Studio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4.6.2 Choosing Tab Width . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 4.6.3 Code Indenting Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 4.6.4 Other Aspects of Programming Style . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4.7.1 Subtract . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 4.7.2 Program Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 ●7 C Programming with Arduino 4.8 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.8.1 Solution to 4.7.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 4.8.2 Solution to 4.7.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 4.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 Chapter 5 Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 5.1 Your Second Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 5.2 Passing Data to a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 5.3 Passing More Than One Value to a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 5.4 Passing a Variable to a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 5.5 Getting a Value Back from a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 5.6 Passing Values to a Function and Receiving a Value Back . . . . . . . . . . . . . . . . . . 96 5.7 Flashing LED Simulation Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 5.7.1 The while(1) Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 5.7.2 The long Data Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 5.8 Preprocessor Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99 5.9 Functions Calling Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 5.10 Using Multiple Source Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 5.10.1 Adding a New C Source File to an Atmel Studio Project . . . . . . . . . . . . . . 104 5.10.2 Project with Two C Source Code Files . . . . . . . . . . . . . . . . . . . . . . . . . . 104 5.11 Header Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 5.12 How Functions Relate to Linking and Library Files . . . . . . . . . . . . . . . . . . . . . 107 5.13 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 5.13.1 Parallel Resistor Calculation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 5.13.2 Resistor Formula C File . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107 5.14 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.14.1 Solution to 5.13.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.14.2 Solution to 5.13.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 5.15 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Chapter 6 Number Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 6.1 Binary Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111 6.2 The Need for Binary Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 6.3 Numbering Systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 ●8 Table of Contents 6.3.1 A Quick Look at Decimal Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 6.3.2 Binary Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113 6.3.3 Hexadecimal Numbers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116 6.4 Working with Hexadecimal Numbers in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121 6.5 The ASCII Alphanumeric Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 6.6 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.6.1 Hexadecimal to Port . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.6.2 Print Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.7 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.7.1 Solution to 6.6.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6.7.2 Solution to 6.6.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 6.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 Chapter 7 Memory and Microcontrollers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.1 Memory Basics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.1.1 ROM (Read Only Memory) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.1.2 RAM (Random Access Memory) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.1.3 Data Stored in Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 7.2 A Look at a Memory Chip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 7.3 How Microprocessors Access Memory and Peripherals . . . . . . . . . . . . . . . . . . . 133 7.4 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 7.5 More on C Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138 7.6 Microcontroller Memory Sizes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 7.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 Chapter 8 Accessing AVR Ports in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 8.1 AVR Pins and Ports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145 8.2 Switching the On-board LED On and Off . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 8.3 Controlling LEDs interfaced to a Port . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 8.3.1 Connecting the Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152 8.3.2 Starting a New Atmel Studio Project from Scratch . . . . . . . . . . . . . . . . . . 154 8.3.3 LED Port Control Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 8.4 Increment and Decrement Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 8.4.1 Pre-increment and Pre-decrement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 ●9 C Programming with Arduino 8.4.2 Post-increment and Post-decrement . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159 8.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 Chapter 9 I/O and Memory Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 9.1 Input Pins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 9.1.1 Connecting a Switch to a Port Pin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 9.1.2 Reading the State of a Switch in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 9.2 Input and Output Pins on the Same Port . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 9.2.1 Connecting a Switch and LED . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 9.2.2 Programming a Switch and LED . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 9.2.3 The AND Bitwise Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 9.3 AVR Memory Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 9.3.1 Memory Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 9.3.2 Flash Program Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 9.3.3 Data Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 9.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170 Chapter 10 Previous C Topics Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 10.1 Format Specifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 10.2 Field Width Specifiers Revisited . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 10.3 Escape Sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 10.4 Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 10.4.1 A while Loop that uses break and continue . . . . . . . . . . . . . . . . . . . . . . 178 10.4.2 The do – while Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 10.4.3 The for Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181 10.5 The Problem with printf() in Embedded Systems . . . . . . . . . . . . . . . . . . . . . . 182 10.5.1 Programming the Serial Port . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182 10.5.2 Writing Serial Port Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 10.6 Nested Loops and Decisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 10.6.1 Nested Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 10.6.2 Nesting Decisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189 10.7 Decision Making with the switch Statement . . . . . . . . . . . . . . . . . . . . . . . . . . 190 10.8 The Conditional Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 10.9 Functions and Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 ● 10 Table of Contents 10.9.1 Passing an Address to a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194 10.9.2 Returning More Than One Value from a Function . . . . . . . . . . . . . . . . . . 195 10.10 Variables and Scope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 10.10.1 Local Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196 10.10.2 Global Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197 10.11 Static Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198 10.12 Floating Point Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 10.13 Casts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200 10.14 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 10.14.1 Count Down for Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 10.14.2 LED Control using a switch Construct . . . . . . . . . . . . . . . . . . . . . . . . . 201 10.15 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 10.15.1 Solution to 10.14.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 10.15.2 Solution to 10.14.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 10.16 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 Chapter 11 Arrays and Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 11.1 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 11.2 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 11.2.1 Writing to a String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 11.2.2 Initialising a String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 11.2.3 C Library String Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 11.3 Arrays and Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 11.3.1 Arrays and Array Element Addresses . . . . . . . . . . . . . . . . . . . . . . . . . . 214 11.3.2 Passing an Array to a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 11.4 Strings as Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218 11.5 Writing to a String using sprintf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 219 11.6 Multidimensional Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 11.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 11.7.1 Button Message . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226 11.7.2 Button Wait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 11.8 Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 11.8.1 Solution to 11.7.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 ● 11 C Programming with Arduino 11.8.2 Solution to 11.7.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227 11.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 Chapter 12 Bit Manipulation and Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . 229 12.1 Bit Manipulation with Bitwise Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229 12.1.1 Why Do We Need Bitwise Operators? . . . . . . . . . . . . . . . . . . . . . . . . . . 231 12.1.2 Using the Bitwise AND and OR Operators . . . . . . . . . . . . . . . . . . . . . . . 232 12.1.3 Using the Bitwise NOT Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 234 12.1.4 Using the Bitwise XOR Operator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235 12.1.5 The Left and Right Shift Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236 12.1.6 The C Assignment Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 12.2 Logical Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238 12.3 Operator Precedence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241 12.4 Using Pre-defined Bit Names with Shift Operators . . . . . . . . . . . . . . . . . . . . . 243 12.4.1 Pre-defined Bit Names in C Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 12.4.2 The AVR _BV() Macro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245 12.5 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 12.5.1 Print Binary Number . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 12.6 Solution . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 12.6.1 Solution to 12.5.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 12.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248 Chapter 13 Programming Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 13.1 Timer Counter Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251 13.1.1 Polled Timer Time Delay . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252 13.1.2 Hardware Timer Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255 13.2 The Analogue to Digital Converter (ADC) . . . . . . . . . . . . . . . . . . . . . . . . . . . 256 13.3 The Watchdog Timer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260 13.4 I/O Ports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 13.4.1 I/O Port Register Functionality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 13.4.2 Register Pin Toggle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 13.4.3 Enabling Pull-up Resistors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 13.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265 Chapter 14 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 ● 12 Table of Contents 14.1 Configuring Arduino Boards for Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . 267 14.2 Debugging Example Program . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 14.3 Switching Compiler Optimisation Off . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 14.4 Enabling debugWIRE on the AVR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 14.5 Using the Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 14.5.1 Starting the Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 14.5.2 Single Stepping and Adding Watches . . . . . . . . . . . . . . . . . . . . . . . . . . 271 14.5.3 Viewing Hardware Registers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 14.5.4 Viewing and Modifying Variable and Register Values . . . . . . . . . . . . . . . . 273 14.5.5 Viewing Variables in Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 14.5.6 Stopping Debugging and Setting Breakpoints . . . . . . . . . . . . . . . . . . . . 274 14.5.7 Viewing Pin State Changes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275 14.5.8 Viewing Variable Value Changes & Setting Conditional Breakpoints . . . . . 276 14.5.9 Stepping into a Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 14.6 Enabling ISP on the AVR / Disabling debugWIRE . . . . . . . . . . . . . . . . . . . . . . 277 14.7 Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277 14.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278 Chapter 15 Interrupts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 15.1 Handling Interrupts in C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 15.2 Using the Timer Interrupt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 279 15.3 One Millisecond Tick Interrupt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281 15.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 Chapter 16 Wrapping Up the C Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 16.1 Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 16.1.1 Using Structures in C Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285 16.1.2 Initialising Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 287 16.1.3 Pointers to Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 16.2 Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 16.3 Enumerated Type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 291 16.4 The typedef Declarator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293 16.5 Storage Class Specifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294 16.6 Type Qualifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295 ● 13 C Programming with Arduino 16.7 The goto Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295 16.8 A List of All C Keywords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 16.9 More Preprocessor Directives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296 16.10 C99 Variable Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298 16.11 Alternative Continuous Loop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 298 16.12 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 Chapter 17 C Projects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 17.1 Serial Port Driver Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 17.1.1 Serial Port Driver Design Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 17.1.2 Serial Port Driver Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302 17.2 Converting Numbers to Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306 17.2.1 Using AVR C Library Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 306 17.2.2 Writing Number to String Conversion Functions . . . . . . . . . . . . . . . . . . . 309 17.3 Voltmeter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 17.3.1 Voltmeter Project Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 17.3.2 Voltmeter Project Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314 17.3.3 Enabling the printf Family of Functions to Work . . . . . . . . . . . . . . . . . . . 317 17.3.4 Principle of Operation of the Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . 317 17.3.5 Examining the Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318 17.3.6 Code Timing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 17.4 Stopwatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320 17.4.1 Stopwatch Project Hardware and Code . . . . . . . . . . . . . . . . . . . . . . . . . 320 17.4.2 Stopwatch Project Design . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 17.4.3 Stopwatch Project Code Explanation . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 17.5 Where to from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 Appendix A The ASCII Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 329 Appendix B Arduino – AVR Port Pin Mapping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 B.1 Arduino Uno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 B.2 Arduino MEGA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332 Appendix C Standard Arduino I/O Circuit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 C.1 Arduino Uno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335 C.2 Arduino MEGA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336 ● 14 Table of Contents Appendix D References and Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 D.1 Book Website and Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 D.2 Datasheets and Application Notes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 D.3 AVR Libc Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 D.4 AVR Forum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 D.5 Additional Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339 ● 15 C Programming with Arduino ● 16 Introduction Introduction This book will teach you the C programming language using AVR microcontroller based Arduino boards, such as the Uno and MEGA, as a teaching platform. Atmel Studio for Windows operating systems is the IDE and software tool used in this book for writing, compiling and loading C programs to the Arduino boards. Atmel Studio and the standard C language are used as an alternative to the Arduino IDE which was designed to help those with less technical knowledge of electronics and microcontrollers to get started with microcontroller projects. Standard C is taught by Universities as part of electronic engineering courses and is used throughout industry for programming microcontrollers. C programming can be used on any microcontroller for which C programming tools are available and is not restricted to Arduino. Arduino is used with this book for teaching C as it is very convenient to use and readily available. Learning C Learning a new programming language and learning how to program embedded systems covers a lot of ground as there are many things to learn: the C programming language itself, some knowledge of microcontroller architecture, some electronics, microcontroller peripherals, number systems and logic. There is also the software programming toolchain and IDE to learn, which in the case of this book is the Atmel Studio IDE that uses the GNU toolchain for AVR microcontrollers. It may seem to be a daunting task to learn about all these aspects of an embedded system and programming language, but taking it one step at a time as this book does makes it easy, enjoyable and a lot of fun. Technical Terms IDE – Integrated Development Environment. An IDE is software that provides a complete development environment for writing and editing programs, compiling, loading programs to the target board and debugging. Toolchain – the C toolchain is a set of software programs that are used to convert a C program into a file that can be loaded to a microcontroller. Atmel Studio – an IDE that runs on Windows operating systems and is used to program Atmel AVR and other Atmel microcontrollers. Atmel Studio can be downloaded for free from the Atmel website. Target Audience This book has been written for the hobbyist, student and engineer wanting to learn the C programming language in an embedded environment using microcontrollers. AVR Microcontrollers AVR microcontrollers, the microcontrollers found on Arduino boards such as the Uno and MEGA are used in this book. The C language applies equally to other microcontrollers, but when learning to program, a particular microcontroller architecture must be chosen as hardware differences between microcontrollers will change how they are programmed. The AVR Architecture AVR microcontrollers from Atmel (www.atmel.com) are RISC microcontrollers available ● 17 C Programming with Arduino in 8-bit and 32-bit series. These microcontrollers contain a “AVR core” that is interfaced to memory and peripheral devices and packaged in a single chip. Only 8-bit AVR microcontrollers are presented in this text. Technical Terms RISC – RISC stands for Reduced Instruction Set Computer and is a processor that is designed with a simplified instruction set in order to produce higher performance. The AVR architecture has been designed this way. Atmel AVR Microcontrollers Embedded Systems An embedded system is an electronic circuit board that contains a microprocessor or microcontroller and other electronic parts, running a software program and used for a specific purpose rather than a general purpose as a Personal Computer (PC) would be used. Technical Terms Microprocessor – A microprocessor is a silicon chip, also known as an Integrated Circuit or I.C., containing a Central Processing Unit (CPU) that will fetch program instructions from memory and execute or run them. The memory required by the microprocessor is external to the microprocessor chip – it must be put on the circuit board by the hardware engineer designing the system, whether it is to be used as an embedded system or general purpose computer. There are a number of microprocessor architectures such as Intel’s x86 architecture, ARM architecture, AVR from Atmel and many others. Microcontroller – A microcontroller contains a microprocessor and a number of peripheral devices such as timers, serial ports, general purpose input/output (I/O) pins, counters, analogue inputs, etc. all inside a single silicon chip. A microcontroller will usually have memory on the chip as well. Some microcontrollers will be able to support external memory and others have enough memory on-chip and do not have the ability to support external memory. Microcontrollers are designed to be used in embedded systems as their on-chip peripherals and memory enable an embedded system designer to save circuit board space by not having to add these items as external devices on the circuit board. ● 18 Introduction An example of an embedded system is a calculator. It is specifically made to perform mathematical calculations input by a user on the keypad – it is a dedicated device. A PC on the other hand is a general purpose computer that can be used for any number of purposes such as word processing, playing games, a file server, web server, etc. (i.e. not an embedded system). Other examples of embedded systems are cell phones and computer keyboards as they contain embedded microcontrollers that give these devices their functionality. Smartphones, however, have become more like PCs than dedicated embedded systems, even running their own operating systems. A washing machine may contain an embedded system that responds to button presses on its front panel and controls washing cycles. Although Arduino boards are general purpose boards that can be used for many different tasks, they are still embedded systems as their end use is for a specific task, such as a temperature monitoring system, animated robot, chocolate dispensing machine, or any other project that you can think of. Abbreviations Microprocessor – the word microprocessor is often abbreviated to μP. Microcontroller – the word microcontroller is often abbreviated to μC or MCU. μ – is the symbol for “micro” and is also used in electronic or other engineering measurements such as microfarad μF for capacitor values. Choosing an Embedded System to Learn C On Arduino has been chosen as the embedded system for this book, and specifically Arduino boards that have 8-bit AVR microcontrollers on-board and can be fitted with the standard add-on boards called shields. Arduino has been chosen because it is so widely available, is an open source project and has many second-source compatible boards available. Before Arduino became popular, a specific proprietary board would have to be recommended for any embedded programming book. With Arduino we now have standard off-the-shelf hardware that is open source, allowing anyone to view and modify the circuit diagram and board. Here are some of the reasons that Arduino was chosen: • Powered by USB – no external power supply needed • Easy to connect hardware using an electronic breadboard and jumper wires • USB port on-board that is configured as a virtual COM port on the PC for serial communications • Widely available with alternative sources and clone boards / derivative boards • Has ICSP header for programming and debugging In the pages that follow, you will learn to write programs that run on an Arduino board or embedded system and switch LEDs on and off, read switches to see if they are being pressed or released, send messages out of the serial port to be displayed on a PC and more. ● 19 C Programming with Arduino Arduino MEGA 2560 (left) and Arduino Uno (right) Why use the C Programming Language? If I think of all the technology changes that have taken place in the world of microelectronics since the start of my career as an embedded programmer, the one thing that has stayed the same is the C language. Processor architectures and systems have come and gone, but the common thing between all of them is that they were programmed in C. The reason for the popularity and use of C is that C is a standard language and ideally suited to embedded programming where access to the hardware of a system is required. C programming tools produce small and fast programs – a requirement when you have limited amounts of memory available on an embedded system. Whenever a new processor architecture comes on the market, you can be pretty sure that there will be C programming tools available for it. C programming is necessary to move beyond Arduino in order to program other embedded systems that are not supported by the Arduino IDE, as well as gain more control over Arduino boards. You may have heard that C is cryptic and difficult to learn, but this is nonsense. C is a simple language with very few keywords and is easy to learn. As with any new language, you need to become familiar with the syntax of the language. This book will explain everything to you, starting with simple and easy programs. When I leaned to program in C, I thoroughly enjoyed it and I hope that you will too. Prerequisites In order to use this book you will need to be able to operate a computer running Windows and know how to download and install software on a Windows system. Any knowledge of electronics will help, for example you should know what an LED is, a switch, I.C. and a resistor. I will refer to basic electronic components such as these in the text. If you don’t know what these items are, there are plenty of books available that teach basic electronics. A good online resource for basic electronics, breadboard circuits and an introduction to Arduino is: startingelectronics.org/beginners/start-electronics-now/ Any knowledge of programming in any language will help, but is not required. Basic mathematics is also assumed. i.e. addition, subtraction, multiplication, division, counting and a little algebra. If you are not good at mathematics, that is not a problem as the mathematics is very basic and the programs that you will write will actually help you to understand mathematics better. ● 20 Introduction You will need to have an Internet connection in order to download software programs and example programs used with this book (all software used is free). Hardware Requirements A standard USB cable will be needed for powering the Arduino board and communicating between the Arduino and PC. An external power supply can be used to power the Arduino board, but a USB cable will still be needed for serial communications with the PC. Electronic components such as LEDs, resistors and switches will be needed and are listed in each section of the book when they are required. When using the Atmel Studio IDE, a programming device is needed to load programs to the microcontroller on the Arduino board. The programmer is connected to the PC using a USB cable and to the 6-pin header on the Arduino board labelled ICSP. More details of the Arduino board and programmer required can be found below. Arduino Board An Arduino Uno with an Atmega328P microcontroller or Arduino MEGA 2560 will be needed to follow the example programs in this book. An Arduino Uno R3 and Arduino MEGA 2560 R3 were used when developing the code examples. If you have not yet bought an Arduino board, I would recommend getting the Arduino MEGA 2560 as it has more memory and input / output pins than the Uno. Atmel AVRISP mkII Programmer and Arduino Uno ● 21 C Programming with Arduino Programmer An AVRISP mkII programmer (part number ATAVRISP2) or AVR Dragon programmer / debugger (part number ATAVRDRAGON) is required for loading programs to the Arduino board. Both of these programmers are Atmel devices and are fully compatible with Atmel Studio. I would recommend getting the AVR Dragon as it has debugging capabilities as well as the ability to do HV programming that can be used to unbrick a DIP packaged AVR microcontroller. The AVRISP mkII can only program AVR microcontrollers and has no debugging capabilities. The AVR Dragon does not come with any cables, you will need a standard USB cable as well as a 6-way ribbon cable with female 6-way (2 by 3) headers on each end. There are other AVR programmers on the market made by various manufacturers that may be cheaper than the programmers from Atmel, but these programmers will need external software in order to use them. They will also require extra set-up steps in order to use them in Atmel Studio and will most likely not have any debugging abilities. Atmel AVR Dragon Programmer / Debugger and Arduino Uno Technical Terms Debugging – Debugging refers to finding and fixing program errors. An embedded programmer or software engineer will write a program and then test it to see if it works as intended. If there is a problem with the program, e.g. “Why does the LED not switch on when it is supposed to?”, the embedded programmer will debug the program. AVR microcontrollers on the Arduino boards used in this book can be debugged using the AVR Dragon attached to the ICSP header on the board. When a software program called a debugger is run on the PC from within Atmel Studio, it accesses the embedded system through the ICSP header via the AVR Dragon and enables the programmer to step through the program and examine the contents of the microcontroller’s memory. This helps the programmer to find the bug (program error) and fix it. ● 22 Introduction The Approach Taken in this Book This book uses free or open source software only. You will start learning to program right from chapter 1 after installing and setting up programming tools. The first 7 chapters of the book teach the basics of the C language, concentrating on the language by sending output from C programs running on an Arduino to the PC for display in a terminal window, and receiving data from the terminal window typed in by a user. Chapter 8 and beyond builds on the C basics already learned in the first chapters and starts to look at programs that use the microcontroller’s peripherals for more practical embedded applications. Some Good Advice Take your time to learn and understand how the programs work before moving to the next chapter. Experiment with the code by changing it to see what happens and try to write your own programs, or modified versions of the example programs to aid in learning as you progress through the book. If you can’t type, learn to type while learning C. This is what I did when learning to program and have never regretted it. I always entered the example programs by typing them in rather than just running the downloaded example programs from disk. Learning C and embedded systems is great fun and a big adventure, so it is my wish that you will thoroughly enjoy using this book and learning new things. Accompanying Files and Support Website An accompanying zipped file is available for download from Elektor that contains all of the C source code for the example programs in this book. Download the accompanying files from the Elektor website at www.elektor.com by browsing for the book from the books menu or using the search box to find the book’s page. An accompanying website can be found at wspublishing.net/avr-c that contains extra articles and information related to this book and will be updated with any errata found. ● 23 C Programming with Arduino ● 24 Chapter 1 • Your First C Program Chapter 1 • Your First C Program What you will do in this chapter: • Download and install Atmel Studio • Download and install a terminal emulator program • Install template files to make creating new projects easier • Connect the Arduino board and programmer to a PC • Write your first C program • Compile, load and run your first program 1.1 • Download and Install Atmel Studio Downloading and installing Atmel Studio is straightforward and should not pose any problems. Brief download and installation instructions are presented below. More detailed instructions with screen captures can be found on the supporting website at: wspublishing.net/avr-c. 1.1.1 • Downloading Atmel Studio Atmel Studio can be downloaded by visiting the Atmel Studio web page and scrolling down the page to find a link to the Atmel Studio installer. Atmel Studio web page: www.atmel.com/tools/atmelstudio.aspx Two downloads of Atmel Studio are available – a web installer version and an offline installer version. I would suggest downloading the offline installer version which will allow Atmel Studio to be installed to any PC whether it is connected to the Internet or not. Figure 1-1 Download Atmel Studio Click the image of the disk to the left of the chosen version of Atmel Studio (Figure 1-1) and you will be taken to a new web page. You can now either create an Atmel account, sign in with an existing Atmel account, or download the file as a guest. To download Atmel Studio as a guest, enter your details and a valid email address. A link to the file to download will be emailed to you, which you can click to open a page that contains a link to the Atmel Studio installation file. Click the link to start the download. The advantage of creating an Atmel account is that after Atmel Studio is installed, updates and addons for Atmel Studio can be installed which require signing into an Atmel ● 25 C Programming with Arduino account. If you do the download as a guest, you can always create an Atmel account later. When the download has finished, a file with a name such as as-installer-7.0.582-full.exe will be found in the download folder. The exact file name may be different, depending on whether the downloaded file is a newer version or not. 1.1.2 • Installing Atmel Studio To install Atmel Studio, simply double-click the downloaded file to run it, which will start the installation. You will need to agree with the license to be able to continue with the installation. Do a standard full installation and leave the installation folder names at their defaults. 1.2 • Download and Install a Terminal Emulator A terminal emulator program called Tera Term is used for program input / output between a PC and Arduino board using a USB cable. Tera Term can be downloaded from: en.osdn.jp/projects/ttssh2/releases/ Download the zipped Tera Term file, which was at version 4.88 at the time of writing, and named teraterm-4.88.zip containing a folder called teraterm-4.88. It is not necessary to install Tera Term, simply copy the teraterm-4.88 folder from the zipped file to any convenient location on the PC, e.g. to the desktop. Double-click the file called ttermpro.exe in the teraterm-4.88 folder to run Tera Term. It is best to enable Windows to show file extensions in the file manager to more easily identify file types. Enabling file extensions to be visible will show the .exe extension of ttermpro.exe as well as the .c extension of C program files. Displaying File Name Extensions in Windows 7 File Manager To display file name extensions in file manager, first open file manager and then click Organize → Folder and search options to pop up the Folder Options dialog box. In the dialog box, click the View tab. In the Advanced settings box, uncheck the Hide extensions for known file types box. Click the OK button in the dialog box to save the changes. 1.3 • Install Template Files Included with the accompanying download from Elektor are two template files used to start new projects in Atmel Studio called template_mega_dragon.zip and template_ uno_dragon.zip found in the Templates folder of the download file. To install these template files, open your Documents folder and then Atmel Studio → 7.0 → Templates → ProjectTemplates and copy the files into the ProjectTemplates sub-folder. If you are using a newer version of Atmel Studio, substitute the new version number in place of 7.0 in the above path. The full path to the ProjectTemplates folder is: C:\Users\<your user name>\Documents\Atmel Studio\7.0\Templates\ ProjectTemplates ● 26 Chapter 1 • Your First C Program 1.4 • Connecting the Hardware Use a standard USB cable to connect the AVRISP programmer or AVR Dragon programmer / debugger to the PC that has Atmel Studio loaded on it. Drivers for the programming device will automatically be installed. Connect the 6-way ribbon cable from the programming device (AVRISP or Dragon) to the 6-pin header labelled ICSP on the Arduino board. Don't connect the programmer to the 6-pin header that is near the USB connector on the Arduino. On the AVR Dragon, the 6-way ribbon cable is connected to the 6-pin header labelled ISP. Pin 1 of the ISP header on the Dragon is marked with a 1. Pin 1 of the ICSP header on the Arduino board is marked with a small dot. Connect the Arduino board to a second USB port on the PC using a second standard USB cable. On a Windows 7 system the Arduino drivers will need to be installed if the Arduino has never been programmed using the Arduino IDE software on the PC. The drivers will enable the Arduino to be seen as a virtual COM port on the PC. A Windows 10 system should automatically configure the Arduino as a virtual COM port. For more detailed hardware connection instructions, see the supporting website at: wspublishing.net/avr-c 1.5 • How C Programs are Created C programs are written using a text editor and saved to a file with a .c file extension e.g. myprog.c. When the C compiler is run it reads the C text file, known as the C source file, and creates an executable file that can be loaded to and run on the embedded system. The text editor in Atmel Studio has "syntax highlighting" which will highlight C language keywords making the program a lot easier to read. 1.6 • Start Programming With the software tools installed and hardware connected, you are now ready to write your first C program. Start by creating a new sub-folder in which to store the C programs that you will write when following the examples in this book. The sub-folder can be created in Documents → Atmel Studio or Documents → Atmel Studio → 7.0 assuming that the version of Atmel Studio that you are using is version 7.0. A sub-folder called c_ book in Documents → Atmel Studio → 7.0 will be used in the text when describing how to create new Atmel Studio projects. You may want to install the code examples for the book that are included with the accompanying files downloaded from Elektor, so that you can check your code against the examples. Copy the Arduino_C folder from the accompanying zipped file to Documents → Atmel Studio. 1.6.1 • Creating a New Project from the Template Files When writing a new C program using Atmel Studio, a new project must be created that will contain the C code as well as other files that store the project settings, such as the part number of the target microcontroller, which programming device is being used and various settings for the C toolchain. Start Atmel Studio, which can easily be found by clicking the Windows start button and typing "Atmel", or by double-clicking the Atmel Studio icon on the desktop. A new project can be created by clicking the New Project... item on the Start Page in Atmel Studio. If the Start Page is not visible, use the top menu to open it by clicking ● 27 C Programming with Arduino View → Start Page. Alternatively click the New Project icon on the top left toolbar or use File → New → Project... from the top menu. In the New Project dialog box that appears (Figure 1-2), select template_mega_dragon if you are using the Arduino MEGA or template_uno_dragon if you are using the Arduino Uno. If the template files do not appear in the dialog box, then refer to section 1.3 on page 26 which explains how to install the templates. Figure 1-2 The New Project Dialog Box The name of the first project is hello, so remove the text in the Name: field of the dialog box and type hello in its place. Use the Browse... button to navigate to the sub-folder that you created earlier to store your new projects in. Figure 1-3 shows the changes made to the bottom of the dialog box for the first project. Figure 1-3 Naming the Project and Selecting the Destination Folder Click the OK button when the project name and destination have been changed. A new project will be created in its own folder inside the selected destination folder. In the right panel of Atmel Studio, click the tab called Solution Explorer to display the ● 28 Chapter 1 • Your First C Program project's files as shown in Figure 1-4. Figure 1-4 Select the Solution Explorer Tab Atmel Studio should now have the main.c file open with some skeleton code displayed as shown in Figure 1-5. If the main.c file is not open, double-click it in the Solution Explorer pane to open it for editing. Figure 1-5 The hello Project Created from the Template File ● 29 C Programming with Arduino Edit the file by typing in the code from the hello project main.c listing below. After editing the file, save it by pressing Ctrl + S on the keyboard or by clicking the Save icon on the top toolbar. When editing the file, be careful not to leave anything out when typing the program and be sure to type everything exactly as it is shown in the code listing. Starting the project using the template file makes it necessary to add only one line of code to the C source file, the line starting with printf. Project name: hello Project location: CH1\hello main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("Hello, world!\r\n"); } while(1) { } Program Output Hello, world! The line starting with printf, and the line of code above and below it are moved to the right in the editor, which is known as indenting. Lines of code are indented by pressing the Tab key which moves the code to the right, although this should not be necessary when starting the project with the template. Once a line of code has been indented using the Tab key, lines of code following are automatically indented by the Atmel Studio editor. Figure 1-6 on page 31 shows the C file in Atmel Studio after editing. About the Code Listings Each code listing in this book, like the one above, shows the project name, project location and C file name above it. Project name – In Atmel Studio a new project must always be created before a C program can be written. Every C program is created in a project and this is the project name displayed above the code listing. Project location – The project location above the code listing is the location of the finished project found in the Arduino_C folder of the accompanying files. As an example, the above project can be found in Arduino_C\CH1\hello with the accompanying files. C file name – Each C file has a unique name and is usually named main.c in this book, which is the default name given to the initial C file in Atmel Studio 7, although it could be given a different name as it does not have to be named main.c. Some projects will contain more than one C file, each with its own name. There will always be a file that contains the function main(), explained later in this chapter, and it makes sense to name the file that contains it main.c so that it can be easily found in a project that contains many files. ● 30 Chapter 1 • Your First C Program You will have noticed when typing the program into the Atmel Studio editor that certain words change colour. This is known as syntax highlighting and makes your program easier to read by highlighting keywords, data types and other elements of the C language. Figure 1-6 The hello Project's main.c File After Editing 1.6.2 • Building the Project Now that the C source code file hello.c has been edited, the project can be built. The build process converts the code from the hello.c file into a file that can be loaded to the microcontroller on the Arduino board or other embedded system. To build the project, click the Build icon on the toolbar as shown in Figure 1-6. It may be easier at first to click Build → Build hello on the top menu where you will be able to get a better view of what the Build icon looks like. Clicking the Build Solution icon next to the Build icon will also build the project. Each Atmel Studio project is created in a solution, and a solution can contain more than one project, so building the solution will build all projects in the solution. In most cases only one project will be created per solution. 1.6.3 • Build Problems and Solutions Not much can go wrong with the build when using the template file. If an error message appears at the bottom of the Atmel Studio window, it will probably be because the line of code that was added is missing something like the semicolon at the end of the line or the closing quotation marks. 1.6.4 • Loading and Running the Program The template used for creating the project has been set up with the AVR Dragon as the programming tool and appears at the right side of the second toolbar as shown in ● 31 C Programming with Arduino Figure 1-7. If you are using the AVRISP mkII or the AVR Dragon does not appear on the toolbar, click the tool on the toolbar or the text "No tool" to change it to the AVRISP or to add the AVR Dragon if it is missing. Make sure that the AVRISP or Dragon is plugged into the PC before clicking the tool icon. In the dialog box that pops up (Figure 1-8, select the AVRISP or Dragon from the drop-down box and ISP for the interface. After selecting the correct programming tool, save the settings by clicking the Save icon or with Ctrl + S from the keyboard. Figure 1-7 Microcontroller and Programmer Selection Figure 1-8 Selecting a Programmer in Atmel Studio As soon as the program is loaded to the Arduino it will start running and the text message that the hello.c program prints will be sent out of the microcontroller serial port to the PC via USB. In order to see the message, the terminal emulator program must be started and connected to the Arduino USB port before loading the program to the Arduino. Find the folder containing Tera Term that you copied to your PC in section 1.2 on page 26 of this chapter and double-click the ttermpro.exe file to run Tera Term. In the Tera Term new connection dialog box, click the Serial radio button and select the COM port that your Arduino appears as, as shown in Figure 1-9 on page 33,and then click the OK button. The default communications settings of Tera Term should be correct for communicating with the Arduino, but can be checked by selecting Setup → Serial port... from the Tera Term menu. In the serial port setup dialog box, the serial communications settings should be as follows: ● 32 Chapter 1 • Your First C Program Port – the COM port that your Arduino appears as Baud rate – 9600 Data – 8 bit Parity – none Stop – 1 bit Flow control – none Figure 1-9 Selecting the Arduino in Tera Term With the programmer and Tera Term connected to the Arduino, we are ready to load the program. Click the Start Without Debugging icon on the top toolbar to load the program to the Arduino as shown in figure 1.8. This is the green triangle arrow icon on the top toolbar and not the green triangle arrow on the second toolbar. The icon can also be found on the top menu by clicking Debug → Start Without Debugging – be sure not to select Continue from this menu. If the program loaded successfully, the text "Hello, world!" will appear in the Tera Term window. 1.6.5 • Program Loading and Running – Problems and Solutions If anything went wrong when trying to load the program to the Arduino, then here are a few things to check. The programmer (AVRISP or Dragon) must be connected to the PC via a USB cable and must be shown on the toolbar as seen in Figure 1-7 on page 32. If the programmer name appears on the toolbar, but the programmer does not work, try selecting it again by clicking the programmer icon on the toolbar and using the drop-down list in the page that appears (Figure 1-8 on page 32). Be sure to save after changing the programmer on the page. The Arduino board must be powered in order to program it. It can be powered from the ● 33 C Programming with Arduino USB cable that is also used to communicate with the terminal emulator program. Tera Term must be connected to the correct COM port that the Arduino appears on. The Arduino port will only appear in Tera Term if it is plugged into the PC's USB port and the driver for it is installed. The correct microcontroller part number must be selected which should be the case if the project was created from the correct template file. The microcontroller that appears on the toolbar (Figure 1-7 on page 32 shows the ATmega328P) should be as follows: • ATmega328P for all modern Arduino Uno boards • ATmega2560 for the Arduino MEGA 2560 1.7 • About Your First C Program The program that you have written is a classical first program for any computer language – printing "Hello, world!". The first thing that you should know about C is that it is case sensitive. If you change the case of any of the keywords or function names in your program and try to compile it, you will get an error message. In the hello.c program printf is a function name and while is a key word. If you typed Printf or While or WHILE for example, the compiler will display an error message and will not be able to compile the program (the compiler is run as part of the build process). Try changing the case of the keyword or function name, then try to build the program again if you would like to experiment. Don't forget to save your source code file before building. Text that is printed to the screen between the quotation marks ("") can be changed as this is just a text message and not part of the C language. Type any message between the quotation marks that you would like and then save, compile and load the program to the Arduino to run it. The \r (carriage return) and \n (linefeed or newline) that forms part of the text between quotation marks moves the cursor to the beginning of a new line in the terminal window. It tells the compiler to insert a carriage return and newline character at the end of the text message. To see what the carriage return and newline characters do, print a second line of text after the first line as shown in the newline project code listing. Without the carriage return and newline character at the end of the first line of text, the second line of text would appear next to the first line of text and not below it. Create a new project called newline following the same steps used to create the first project in section 1.6.1 on page 27. Select the same working folder that you created for your projects and the new project will be saved in its own folder inside the working folder. ● 34 Chapter 1 • Your First C Program Project name: newline Project location: CH1\newline main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("Hello, world!\r\n"); printf("Newline"); } while(1) { } Program Output Hello, world! Newline Try removing the \r\n from the first line of text, save, build and load the program to see what it does. Now add some more \r\n's to the text, e.g. printf("Hello, world!\r\n\r\n\r\ n\r\n"); will move the cursor down four times in the terminal window so that when the program is run, there will be a vertical gap between the first line of text and the second line of text. You can also try putting a carriage return and newline character in front of the text or in the middle of the text and see what happens, just make sure that it is between the quotation marks. Hint Clearing the terminal window – To clear the terminal window between running different programs, click Edit → Clear screen on the top Tera Term menu bar. Re-running a program – To re-run a program without reloading it to the Arduino, press the RESET button on the Arduino. 1.8 • Analysing the Program We will now examine each element of the hello project code. Refer to Figure 1-10 on page 37 when reading this section. 1.8.1 • Characters and Strings A character is any keyboard character that you can type such as letters of the alphabet, numbers and special characters like $, %, !, etc. A string is a sequence of characters – written sequentially in a program and stored sequentially in the microcontroller's memory. In C, strings are written between quotation marks as in "Hello, world!\r\n". When you press the Enter key on the keyboard, you are typing a newline character. The newline character moves the cursor to a new line as we saw in our program. If you want to insert a newline character into a text string when editing your C source file, and you try to do so by pressing the Enter key, the cursor in your text editor program will ● 35 C Programming with Arduino move to the next line in the source file. This is because the text editor is interpreting the newline character that you type. To insert a newline character into your text string for your program to use, we use an "escape sequence" – this is the \n in the hello project code. Windows systems actually inset a carriage return (\r escape sequence) and newline character when pressing Enter in a text file. There are other escape sequences besides the newline and carriage return, they all start with a backslash (\) when written in C programs. \r\n or \n By convention, Windows systems use a carriage return and newline character at the end of a line of text in a text file to indicate that the next line of text must be displayed on a new line. These characters are represented by the escape sequences \r and \n in C program text strings. Unix systems use only a newline character in text files to indicate the end of a line of text and that the next line of text is to appear on a new line – represented by \n in a C program string. These conventions usually occur in other programs such as terminal emulators, which is the case with Tera Term that needs to be fed both \r and \n to move the cursor to the beginning of a new line of text. It does not necessarily have to be the case that a terminal emulator created for Windows follows the text file implementation and you may find a terminal emulator for Windows that uses the single newline character to move the cursor to the beginning of a new line in the terminal emulator window. In this case, the line of code containing \r\n would not need the \r and could be changed to: printf("Hello, world!\n"); 1.8.2 • Functions A function in C is a piece of code that can be called, or run, from within a C program to perform some task (or function). In the text a function is written with the function name first and then two parentheses following it, e.g. myfunc(). Functions are called subroutines in some programming languages. Every C program has to have a function called main(). This is the entry point of the C program – where it will start running. The main() function has an opening and closing brace ({ and }) around the code that it contains. This lets the compiler know that the code within the braces belongs to the main() function. Ignore the int and void keywords at the main() function for now. The printf() function is an external function that is called by the main() function in the hello program. What this means is that by typing the function name printf() in your C source code, a function called printf() will be run when the hello program is run. The printf() function exists somewhere externally and gets added to the program when the C toolchain programs are run during the build process. All you need to know for now is that the printf() function will print text to the terminal window when a line of text is passed to it between quotation marks. UartInit() is another function and is necessary to set up the serial port of the Arduino so that calls to printf() will send their text out of the serial port for display in the terminal window. This is all that you need to know about functions for now, they will be covered in more detail later in the book. ● 36 Chapter 1 • Your First C Program Figure 1-10 Elements of a C Program 1.8.3 • Program Statements The lines of code between the braces of the main() function are each examples of C statements. Note that every statement in C must be terminated with a semicolon (;). If the semicolon is left out, the compiler will display an error message and will not compile the source code. 1.8.4 • Preprocessor Directives Line 1 of the hello.c program contains a preprocessor directive: #include <stdio.h>. This tells the compiler to include an external file named stdio.h. For now all you have to know about this line is that it must be present at the top of your source code file if you want to use the printf() function. In the same way, the second line of the program contains a preprocessor directive #include "stdio_setup.h" which is necessary when using the UartInit() function. The reason why one include statement contains angle brackets and the other contains quotation marks will be explained later. 1.8.5 • Whitespace Characters The C compiler will ignore all whitespace characters in the source code. Whitespace characters are spaces, tabs and the newline character. So you could separate each line of the hello program vertically by hundreds of newline characters. Of course this would make the program difficult to read, but the compiler would still compile the program. ● 37 C Programming with Arduino 1.8.6 • Ending the Program A C program written for a PC would typically run, and when finished, simply exit and return to the operating system. A C program running on an embedded system without an operating system does not have anywhere to return to when it is finished, resulting in the microcontroller fetching and executing the next program instruction from memory whether it is valid or not. This can cause unknown and undesired behaviour of the microcontroller or embedded system. To prevent the microcontroller from trying to run invalid instructions from memory beyond the end of the C program, an empty while(1) loop is placed at the end of the C program. Loops will be explained in more detail later in the book. 1.9 • Exercises The best way of learning to program is to write programs. The following exercises are very simple, but be sure to complete them in order to reaffirm what you have learned in this chapter. The solutions to the exercises follow immediately after, but don't look at them until you have attempted to do the exercises yourself. I suggest that you make a subdirectory in your programming directory called exercises in which to save your programming exercises. 1.9.1 • Three Text Lines Write a program that uses a single printf() function to write three lines of text to the terminal emulator program on the PC. The output should look like this: The C language is great! 1.9.2 • Find the Errors The following program contains three errors, see if you can spot them. If you can't, then create the program and try to compile it. The error messages from the compiler should help you to find and correct the errors. Error messages appear in the pane at the bottom of Atmel Studio. Double-clicking an error message will take you to the line of code in the editor where the error was found which may indicate that the error actually occurred before this line of code. #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit() printf("I can use two printf()\r\n") printf("statements in my program.") } ● 38 while(1) { } Chapter 1 • Your First C Program 1.10 • Solutions 1.10.1 • Solution to 1.9.1 Project name: CH1_1_three_lines Project location: Exercises\CH1_1_three_lines main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("The C\r\nlanguage\r\nis great!\r\n"); } while(1) { } 1.10.2 • Solution to 1.9.2 The terminating semicolon has been left off the end of each of the statements in the program. Project name: CH1_2_errors Project location: Exercises\CH1_2_errors main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("I can use two printf()\r\n"); printf("statements in my program."); } while(1) { } 1.11 • Summary • C programs are written with a text editor and saved in a text file that ends with a .c file extension. This file is known as a source file and contains source code. • In Atmel Studio, a new project must be created before writing a C program for a microcontroller. The project stores the C source file as well as various settings for the project. • When the C compiler is run as part of the build process, it produces an executable file that can be run on a microcontroller (The tools that run actually compile and then link – more about this later). ● 39 C Programming with Arduino • C is case sensitive. Functions and keywords must be written in the correct case. C uses lowercase for keywords and standard library function names (e.g. for the printf() function). Non-standard library functions and functions you write yourself are not restricted to lowercase only. • Every C program must start with a main() function. This is where the program starts running. • The main() function and every other function that you write must have its code placed between braces {}. • All function names end with parentheses (). • Each statement in C must be terminated with a semicolon. • Common typing errors when learning the C programming language are: leaving off the semicolon at the end of a statement, leaving off one or both of the enclosing braces of a function and leaving off the parentheses of a function. Congratulations, you have come a long way in this chapter! We will move on to some more programming in the next chapter and cover the basics of the C programming language. If you do not understand everything in this chapter, do not worry, things will become clearer as you progress through the book. ● 40 Chapter 2 • C Basics Chapter 2 • C Basics What you will do in this chapter: • Write some more simple C programs • Learn about input, output and variables • Do some simple mathematical calculations • Learn how to find and fix program errors There are a number of example programs in this chapter and the chapters that follow. It is advisable to enter and run all the example programs in this book. Feel free to change the programs and experiment with them once you understand how they work. This will help you to learn C. 2.1 • Input, Output and Variables All programs work with input and output. The hello program worked with output only by displaying a message in the terminal emulator window. The message was sent from the Arduino making it output from the Arduino. If the terminal program is used to send a character to the Arduino, this is know as input because it is sent into the Arduino. Most programs will receive some input, do processing on the input and then produce an output that is useful. An embedded system may read the temperature (input), decide whether the temperature is too high or too low and then switch off or on a heating element (output) as shown in Figure 2-1. Figure 2-1 Example of Input and Output in an Embedded System If we are to get an external measurement or value into the Arduino to do some processing on, we need to store the value in the AVR microcontroller memory so that we can then use it in our program. The value that is read is stored in a variable. A variable is a piece of memory set aside to store a variable value as opposed to a fixed or constant value. A temperature is an example of a variable value. When it is read by the Arduino it is not known by the Arduino and could be any value within a range. If I say that the Arduino reads an external temperature, what is happening is that the Arduino is running a program and the program tells the Arduino to read a value. The act of reading the value is the act of getting the value and storing it in memory as a variable. ● 41 C Programming with Arduino To demonstrate input, output and variables on the Arduino in a useful way, we will get a number from the user of the program (typed in by the user on the keyboard), perform addition with the number and then output the result to the terminal program. Before we can do this, we need to be able to: 1. get a value that the user types in on the keyboard 2. store the value on the Arduino so that we can add a number to it 3. be able to display the result of the addition to the user in the terminal program. Let’s start by looking at how to print a number to the terminal program. We’ll use the printf() function again. Create a new C project from the template file called number and add the following lines of code to the C file. Project name: number Project location: CH2\number main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("Printing a number: 19\r\n"); printf("Printing a number: %d", 19); } while(1) { } Program Output Printing a number: 19 Printing a number: 19 Save, build and load the program. The program shows that you can print a number as part of the text as with the first printf() function; or as an external number that gets substituted into the text. In the second printf() function, the number 19 gets substituted in place of the %d format specifier. We will look at format specifiers in more detail in later programs. We will need the second method of printing a number that is variable (in this program the number is a constant and we are substituting a constant into the string – it is hard-coded into the program and can’t be changed after compiling). The first way of printing the number will not allow a stored value entered by a user of the program to be displayed on the screen because it is fixed as part of the text string (it is known as a string constant). We will substitute a stored variable value into the string just like the number 19 is substituted into the second text message. The number program might not make much sense right now, as the two printf() statements produce exactly the same output although they do so in a different way. ● 42 Chapter 2 • C Basics 2.1.1 • Variables As already explained, a variables in C can store or hold a value that is input by the user. We can also store the result of a calculation in a variable. If you were to add two numbers together in a program, the result would need to be stored somewhere so that it can be displayed to the user – it is stored in a variable. Variables can store different kinds of data and each kind of data must be specified when declaring a variable as the next program illustrates. Create the following project called var. Project name: var Project location: CH2\var main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int myvar; UartInit(); myvar = 8; printf("The value of variable myvar is: %d", myvar); } while(1) { } Program Output The value of variable myvar is: 8 In this program, an integer variable is defined: int myvar; This tells the microcontroller that it must make space in memory for a variable of type integer (int). The variable is given a name (myvar) so that we can identify it and use it in our program. An integer is a whole number (not a fraction) e.g. 5, 1, 94, 0, -3, 783, -102. The name of the integer can be anything that you choose. It may not begin with a number (0 to 9), may not contain any spaces (use the underscore character _ instead if you choose) and may not contain any special characters like !, +, *, #, ? or dots as most of these have special meaning in C. You may not use any of the C language keywords as a variable name either as these words are reserved and have special meaning in C. Or put another way, a variable name can consist of any alpha-numeric characters and the underscore character, but may not start with a numeric character or consist of a C keyword. It is also not advisable to start the variable name with the underscore character as this is used by the compiler for internal variable names. Syntax highlighting, as seen in the Atmel Studio editor, is helpful in identifying keywords ● 43 C Programming with Arduino in C. If a word that you choose to use as a variable name changes to the same colour as a keyword after typing it in, you will know that it is a keyword and not to use it. The variable is assigned a value of eight in the program: myvar = 8; This means that myvar, now contains the value eight – it is a location in memory and it is storing the value eight. The name of the variable is only used in the C source code. When the program is compiled and run, the variable will be allocated a space in the AVR’s memory and will be referenced by its address in memory. In the printf() statement, the value of the variable myvar, i.e. 8, is substituted into the text string in place of the format specifier %d by putting the name myvar after the comma. In the next program we will define a variable for storing the result of an addition calculation. Create the following project called add. Project name: add Project location: CH2\add main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int result; UartInit(); result = 3 + 4; printf("The result of 3 + 4 is: %d", result); } while(1) { } Program Output The result of 3 + 4 is: 7 Here we see the addition operator (+) being used to add two numbers together. The result of adding 3 and 4 is stored in an integer variable named result. The value that this variable is holding is then printed to the terminal program window. We now have the ability to store a value as a variable, perform addition and print the variable to the screen. We need one last thing before we can write our input/output program – the ability to get a number from the user of the program. 2.1.2 • Getting Input from the User Using the scanf() Function The scanf() function is used to get input from the user. Like the printf() function, the scanf() function uses format specifiers to specify what type of data it is using. We have only seen one format specifier so far (%d) which is used with decimal integer numbers and ● 44 Chapter 2 • C Basics is used again with scanf() in the next project. To see the scanf() function in action, create the following project called input. Project name: input Project location: CH2\input main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int input; UartInit(); printf("Enter an integer number: "); scanf("%d", &input); printf("\r\n\r\nYou entered %d", input); } while(1) { } Example Program Output Enter an integer number: 23 You entered 23 When you run this program, it will ask you for an integer number. Type an integer number into the Tera Term terminal window a shown in Figure 2-2 on page 46. When finished typing the number, press Enter. The program will then display the number that you typed. The scanf() function waits for the user to enter a number and then stores the number in the variable input. After the number is stored in the variable, it can be printed to the screen as done in the previous program. scanf() requires that the address of the variable is passed to it, and this is done with the address operator (&). Remember that when the variable is defined, it is allocated a space in memory. The memory location has an address and by using the ampersand symbol (&) in front of the variable name, we are supplying the scanf() function with the address of the variable. This is a requirement of the scanf() function. If you used the variable name without the ampersand, scanf() would then be getting the value that the variable holds instead of the address of the variable. If you do not fully understand this, do not worry, it will become clear later. If you want to use the scanf() function in your programs, you need to have the line #include <stdio.h> at the top of your source file – the same as for the printf() function. ● 45 C Programming with Arduino Figure 2-2 Entering Data into Tera Term to Send to the Arduino Although this program is called input, it actually uses input and output functions, but does nothing to change the input value. In the next program we will achieve our earlier objective of receiving input, processing it and then producing output. Create the next project in Atmel Studio called inout. Project name: inout Project location: CH2\inout main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int input, result; UartInit(); printf("Enter a number to add 5 to: "); scanf("%d", &input); result = input + 5; printf("\r\n%d + 5 = %d", input, result); } while(1) { } Example Program Output Enter a number to add 5 to: 31 31 + 5 = 36 ● 46 Chapter 2 • C Basics Two integer variables are used in this program – input and result. They are both defined on the same line and separated by a comma. They could also be defined on two separate lines as follows: int input; int result; The program asks the user to enter a number. The number obtained from the user by the scanf() function is stored in the input variable, 5 is then added to the value stored in input and the result of this calculation is stored in the variable called result. The values contained in both input and result are passed to the printf() function. Note that we have two format specifiers (%d). The variable input is written first in the printf() function, so it gets substituted into the first format specifier. The variable result is written second and gets substituted into the second format specifier as illustrated in Figure 2-3. Figure 2-3 Substitution of Variable Values into Format Specifiers 2.2 • Variable Types So far we have used only integer variables, but there are other types of variables. The next project called intdiv will illustrate the necessity for a different type of variable. Project name: intdiv Project location: CH2\intdiv main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int result; UartInit(); result = 3 / 4; printf("3 divided by 4 equals %d", result); } while(1) { } Program Output 3 divided by 4 equals 0 This program displays the result of a division calculation using the division operator (/), but the result is not as expected. We would expect the result of 3 ÷ 4 to be 0.75, but the result shown is 0. ● 47 C Programming with Arduino The reason for this result is that we are doing the division with integers and storing the result of the calculation in an integer variable. Integers are whole numbers and in the microcontroller’s memory can’t hold fractions like the result of our calculation, so the fractional part of the result of the calculation is discarded. If we divided 7 by 4, the result would be 1 instead of the expected 1.75, again this is because the fraction is discarded. To overcome this problem, we use a different variable type called a floating point variable. 2.2.1 • Floating Point Variables Floating point variables are able to store fractional numbers as the next project, fltdiv, shows. Project name: fltdiv Project location: CH2\fltdiv main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { float result; UartInit(); result = 3.0 / 4.0; printf("3.0 divided by 4.0 equals %f", result); } while(1) { } Program Output 3.0 divided by 4.0 equals 0.750000 A variable of type float (floating point variable) named result is defined in the program. This variable is able to hold a floating point number. When we want to specify a constant floating point number in C it must contain a decimal point such as 3.0 and 4.0 in this program. To print out the floating point result using printf(), the format specifier for floating point numbers is used - %f, as can be seen in the program. 2.2.2 • Character Variables Character variables are used to store individual characters as the next project charvar shows. ● 48 Chapter 2 • C Basics Project name: charvar Project location: CH2\charvar main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char ch; UartInit(); printf("Type a character: "); scanf("%c", &ch); printf("\r\nYou typed %c", ch); } while(1) { } Example Program Output Type a character: b You typed b A character variable named ch is defined in order to store a character typed in by the user. The scanf() and printf() functions both use the %c format specifier in order to work with the character data type. To initialise a character variable with a value in a C program, the character is written between single quotes (apostrophes). For example: char ch; ch = 't'; The first line defines a character variable with the name ch. The character t is then stored in the variable ch. There is more to be said about variables, but we will use our knowledge gained so far to write some useful programs that do mathematical calculations. 2.3 • Arithmetic Operators Ohm’s law can be used to calculate the current flowing through a resistor if the voltage across the resistor and resistance of the resistor are known. Figure 2-4 on page 50 shows the circuit used to demonstrate this. The resistor could represent a light bulb or relay coil for example. If you were to add an LED to your embedded system, you would need to be able to use Ohm’s law to calculate the value of the current limiting resistor needed by the LED. To calculate the current flowing through the resistor, simply divide the voltage across the resistor by the resistance of the resistor. The formula is represented mathematically by the following equation: ● 49 C Programming with Arduino I=V÷R Where: I is current in Amps V is voltage in Volts R is resistance in Ohms For example, if the voltage of the battery is 10V and the resistance of the resistor is 3Ω (3 ohms), then the current flowing in the circuit is 10 ÷ 3 = 3.333 Amps. Figure 2-4 Circuit for Demonstrating Ohm’s Law The next program called ohm.c asks the user for the voltage and resistance and then calculates the current using the above equation. Project name: ohm Project location: CH2\ohm main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { float V, R, I; UartInit(); printf("Current calculation using Ohm's Law.\r\n"); printf("\r\nEnter the voltage in Volts: "); scanf("%f", &V); printf("\r\nEnter the resistance in Ohms: "); scanf("%f", &R); I = V / R; printf("\r\n\r\nCurrent in Amps is %f A", I); } ● 50 while(1) { } Chapter 2 • C Basics Example Program Output Current calculation using Ohm's Law Enter the voltage in Volts: 5 Enter the resistance in Ohms: 2 Current in Amps is 2.500000 A The equals sign (=) is known as the assignment operator. The variable to the left of the assignment operator is assigned the value of whatever is to the right of it. In the above example, the variable I is assigned the result of the division of two variable values. You have already seen the + (addition) and / (division) arithmetic operators. Table 2-1 shows all of the arithmetic operators used in C. Table 2-1 Arithmetic Operators Operator Mathematical Symbol + - Meaning Example Result + Addition 10 + 34 44 - Subtraction 16 - 7 9 * × Multiplication 4*3 12 / ÷ Division 18.5 / 10.0 1.85 % rem. Remainder (modulo) 17 % 5 2 The left column shows each operator as it is typed into the C source file using the keyboard. Everything should be straight forward except for the modulo operator (%). The modulo operator gives the remainder left over from a division operation. In the example in the table, 5 will divide into 17 three times leaving a remainder of 2. 2.4 • Field Width Specifiers The output of the fltdiv program and the ohm program produce floating point numbers with 6 digits following the decimal point which is unnecessary. It is possible to change the number of digits displayed when using the printf() function by using a field width specifier as the next program, fwidth.c, shows. As can be seen in the program, by inserting .2 between the % and the f in the format specifier, the number of digits displayed after the decimal point is reduced to two, and inserting .3 reduces the number of digits to three. ● 51 C Programming with Arduino Project name: fwidth Project location: CH2\fwidth main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("Float with 2 trailing digits: %.2f", 43.140000); printf("\r\nFloat with 3 trailing digits: %.3f", 43.140000); } while(1) { } Program Output Float with 2 trailing digits: 43.14 Float with 3 trailing digits: 43.140 2.5 • Compiling and Linking When using Atmel Studio to create an executable file that can be loaded to the Arduino, there are two main steps involved. The first is called compiling and the second is called linking. Atmel Studio uses the GCC toolchain for AVR microcontrollers to compile and link a project when the build button is clicked to build the project. It is important to know this because programming errors can come from the compile step and the linking step. Being aware of this will help to find the source of the errors. Figure 2-5 on page 53 shows a diagram of the compile and link process. The compiler compiles the main.c file and the stdio.h file to produce an object file called main.o. The compiler knows that it must look for the stdio.h file because of the line at the top of the hello.c source file - #include <stdio.h>. This line is simply telling the tools to include this file in the compile step. The file stdio.h comes with the compiler and was installed when you installed Atmel Studio. Figure 2-5 on page 53 has been simplified to show only the compiling and linking stages and also excludes the stdio_setup.h file. In reality there is also a preprocessor stage to the build process. All the projects that have been built so far have functions for setting up the serial port of the Arduino for using the printf() and scanf() functions found in an additional C source file that is not shown in the figure. The printf() function that you have been using exists externally as mentioned before. It actually exists in what is known as a library file. In the figure, library files and the object file are linked together. Linking is the process of adding the external functions used in the program to the executable file. The linker "resolves externals" – that means that it sees, for example, that the external function printf() was used in the program, it then looks for this function in object or library files and adds it when found. If it can’t find the function, it will give an "unresolved externals" error and will not be able to produce the executable file. The linker does not link the entire library file with the object file. It only links the functions from the library file that are used in the program. The library files were installed ● 52 Chapter 2 • C Basics with the C toolchain when Atmel Studio was installed. The linker produces an intermediate file in ELF format which is then converted to a HEX file (a file in hexadecimal format) using a utility program that is part of the toolchain. The HEX file is the executable file that Atmel Studio uses in conjunction with a programming device (AVRISP or Dragon) to load the compiled and linked program to the target embedded system. Figure 2-5 Compile and Link Process 2.6 • Errors and Warnings Errors will prevent the compiler from compiling and producing an object file if the error is a compile error. If an error occurs when linking, the linker will not be able to produce an executable file. Warnings will alert the programmer that something may be wrong in the program. The program will still compile and link, but may not work correctly. The compiler can be set up to treat warnings as errors and not produce any output if any warnings occur. In Atmel Studio this setting and other compiler and linker settings can be found by using the top menu and selecting Project → <project name> Settings... which opens the properties page for the currently open project. Clicking the Toolchain tab on the left of the properties page will allow various toolchain settings to be changed as shown in Figure 2-6 on page 54. Figure 2-6 on page 54 shows where to set the compiler to treat warnings as ● 53 C Programming with Arduino errors if desired. 2.6.1 • Compile Errors Errors that occur during compiling are usually syntax errors such as a keyword being spelled wrong or a terminating semicolon that is left off. Another example that will produce a compile error is if two variables are defined that have the same name. In C each variable defined must have a unique name. Figure 2-6 Toolchain Settings Include Settings for the Compiler and Linker As an exercise open the ohm project that you created previously, and we will purposely put some errors in the code to see what the compiler and linker will do. Remove the semicolon from the end of the variable definition to invoke the first error. The variable definition should now look as follows. float V, R, I Save the main.c file and then build the project. The compiler errors that are displayed are shown in Figure 2-7 on page 55. ● 54 Chapter 2 • C Basics Figure 2-7 Errors Generated in the ohm Project The first error shows that something was expected before the function UartInit() and the error message includes some suggestions on what could be missing. Double-clicking the error message will move the cursor to the line in the code where the error was first discovered. Because the compiler ignores whitespace characters, it does not matter where the terminating semicolon for the variable definition occurs. The compiler only knows that the terminating semicolon is missing when it reaches the function call in the next statement. This is the reason why the error is marked as occurring on a line of code that has no errors. The missing semicolon also generates a second error because the variable I now appears not to exist. The error message shows that the error was first discovered on line 15, several lines of code down from where the actual error exists. The reason for this is that the variable I is used on line 15 for the first time in the code and this is where the compiler discovered that it is missing. From the above exercise we can see that a single error can produce more than one error message and that the line number that the error message reports is not necessarily the line that the error is actually on. Fix the error by replacing the semicolon at the end of the variable definition. Change the name of variable R in line 14 of the code to Res as shown below. Save the file and build the project. scanf("%f", &Res); This time the error message shows ‘Res’ undeclared (first use in this function) and that it occurs on line 14. The compiler does not recognise the identifier Res because it has not been defined to be a variable or anything else in the program and has not been declared to be an external identifier. In this case the line number that the error occurred at is the same line number that the compiler error message reports. Fix the error, we will look at link errors next. ● 55 C Programming with Arduino 2.6.2 • Link Errors To produce a link error, change the following line: printf("Current calculation using Ohm's Law.\n"); to prinf("Current calculation using Ohm's Law.\n"); i.e. spell printf incorrectly. Save the file and build the project. This will produce a linker error because, by spelling the name of the function incorrectly, we are referring to a function that does not exist. The compiler will actually warn you first that there is a function referred to that it does not know about. Because the linker looks for the unknown function in order to link it into the program, it will produce the error message, rather than the compiler. The linker will try to find the function in one of the library files, but will not find it. The warning message is from the compiler, but it still compiles correctly and produces an object file. The error message is from the linker and it fails to complete the linking process because of this error. The linker displays the error message undefined reference to ‘prinf’ indicating that it does not know what "prinf" is. A second error message says that ld (the name of the linker program) returned 1. If a program returns 0, to the operating system or program that ran it, it means that the program succeeded in what it was asked to do. If the program returns anything else it means that the program failed. In this case the linker program returned 1, indicating that it failed to do the linking that it was asked to do. If you can’t tell if the error is from the compiler or linker, try compiling the C source file on its own. If the compile does not produce any errors, then the error must be from the linker. To compile a file without building the whole project, first clean the project by selecting Build → Clean Solution or Build → Clean <project name> from the top menu. This will delete any object files from the project and ensure that the file that you compile will actually be compiled again. Right-click the name of the file in the Solution Explorer pane at the right of Atmel Studio and then click Compile on the pop-up menu. 2.7 • Exercises 2.7.1 • Ohm’s Law Calculations Write two program that use Ohm’s law to calculate: a. The resistance of a resistor if the voltage across the resistor and the current flowing through the resistor is known. b. The voltage across the resistor if the current flowing through the resistor and the resistance of the resistor is known. The program must ask the user to enter the two known quantities each time and then calculate the unknown quantity. The result must be displayed with two digits after the decimal point, use floating point variables throughout. Use the following mathematical formulas to calculate the resistance and voltage. They have been derived by manipulating the formula that you saw earlier in the chapter: ● 56 Chapter 2 • C Basics To calculate resistance: R = V ÷ I To calculate voltage: V = I × R 2.7.2 • Variable Names Which of the following are valid variable names? number8 8th_number particle void big number small_number If you don’t know the answer, try using them in a program and see what the compiler says. 2.8 • Solutions 2.8.1 • Solution to 2.7.1 a. Calculate resistance Project name: CH2_1a_resistance Project location: Exercises\CH2_1a_resistance main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { float R, V, I; UartInit(); printf("Calculate resistor value:\r\n"); printf("Enter voltage: "); scanf("%f", &V); printf("\r\nEnter current: "); scanf("%f", &I); R = V / I; printf("\r\nResistance is: %.2f Ohms", R); } while(1) { } ● 57 C Programming with Arduino b. Calculate Voltage Project name: CH2_1b_voltage Project location: Exercises\CH2_1b_voltage main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { float R, V, I; UartInit(); printf("Calculate voltage:\r\n"); printf("Enter current: "); scanf("%f", &I); printf("\r\nEnter resistance: "); scanf("%f", &R); V = I * R; printf("\r\nVoltage is : %.2f volts", V); } while(1) { } 2.8.2 • Solution to 2.7.2 Valid variable names: number8 particle small_number Invalid variable names: void – it is a C keyword big number – spaces are not allowed in variable names 8th_number – variable names may not start with a number 2.9 • Summary ● 58 • C program statements are run in order from top to bottom. • Variables are used to store data in a program. The value that a variable is holding can be changed when the program is running. • When defining a variable in C, the type of data that the variable will hold must be specified. (e.g. int, float, char) • Constants are numbers that are "hard coded" into a program and can’t be changed after compiling. • A variable can be assigned the value of a constant, but the value of the variable can be changed in a running program. e.g.: Chapter 2 • C Basics int var; var = 25; 25 is a constant. var is a variable. var can be assigned a different value later in the program. • Most programs will get input, perform a process on the input and produce an output. • Arithmetic operators are used to perform basic mathematics on variables and constants. • C source code is first compiled to produce an object file. The object file is then linked with functions from a library file in order to produce a working program. • Errors and warnings from the C tools can occur during the compile and link stages. This chapter has taken you through some of the basics of the C language. Almost every topic can be expanded on, but has been kept brief for now to increase your knowledge of C, but not overwhelm you with extra details that are unnecessary at this stage. In the next chapter we will look at how a C program can be written to make decisions. ● 59 C Programming with Arduino ● 60 Chapter 3 • Comparative Operators and Decisions Chapter 3 • Comparative Operators and Decisions What you will do in this chapter: • Learn how to compare integer values • Write programs that make decisions 3.1 • Comparative Operators Comparative operators are used to compare values, for example – is the value of variable X bigger than the value of variable Y?. These values can be stored as constants or variables. Comparative operators are also known as relational operators. 3.1.1 • True and False When an expression containing a comparative operator is evaluated, it will evaluate to either true or false. Logic systems and computers represent true by the value 1 and false by the value 0. The next example program called compare will show what this means. Project name: compare Project location: CH3\compare main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int temper = 20; UartInit(); printf("Is the temperature less than 23? %d\r\n", temper < 23); printf("Is the temperature more than 23? %d\r\n", temper > 23); printf("\nTemperature is now increasing...\r\n\r\n"); temper = 50; printf("Is the temperature less than 23? %d\r\n", temper < 23); printf("Is the temperature more than 23? %d\r\n", temper > 23); } while(1) { } Program Output Is the temperature less than 23? 1 Is the temperature more than 23? 0 Temperature is now increasing... Is the temperature less than 23? 0 Is the temperature more than 23? 1 ● 61 C Programming with Arduino In this example, a variable called temper is defined in order to store a temperature. The initial value assigned to the variable is 20, representing 20 degrees Celsius. As can be seen in the example, a variable can be defined and initialised with a value in one line; the examples in the previous chapter used two lines to do this. In the first printf() statement, the value stored in temper is compared to the constant value 23 using the mathematical "less than" sign: temper < 23. In other words we are saying "Is the value stored in the variable temper less than 23?" When this expression is evaluated in C, it will evaluate to either true (1) or false (0). The result is then substituted in place of the format specifier. As can be seen when the program is run, the result of asking whether 20 (stored in temper) is less than 23 evaluates to 1 (true). Logically this is because 20 is less than 23. See Figure 3-1. Figure 3-1 A Comparative Operator in Action In the second printf() statement, we ask whether 20 is greater than (>) 23 and get a result of 0 (false). When the value that temper is holding is changed to 50, and we ask the same questions, we get a result of false for our question "Is 50 less than 23?" and true for "Is 50 greater than 23?" as we would expect. 3.1.2 • C Comparative Operators Table 3-1 shows the comparative operators available in C. You have already seen the less than and greater than operators in use. Table 3-1 C Comparative Operators Operator Meaning Example Result (where 1 = true and 0 = false) < Less than A<B 1 if A is less than B , else 0 > Greater than A>B 1 if A is greater than B , else 0 <= Less than or equal to A <= B 1 if A is less than or equal to B , else 0 >= Greater than or equal to A >= B 1 if A is greater than or equal to B , else 0 == Equal to A == B 1 if A is equal to B , else 0 != Not equal to A != B 1 if A is not equal to B , else 0 3.2 • Decisions Comparative operators are used to make decisions in a C program. So far you have seen programs that execute or run each statement in the program line for line from top to bottom. Decisions can be used to jump into or over a particular part of a program. Getting back to our temperature controller example from Chapter 2 (Figure 2-1 on page 41), a decision must be made to switch the element of the system on or off. The ● 62 Chapter 3 • Comparative Operators and Decisions system needs to know if the temperature is too low, and if it is, switch the heater element on. When the temperature is too high, it must make a decision to switch the element off. 3.2.1 • Using if to Make a Decision The if keyword is used in a statement to make a decision as the next program whatif shows. Project name: whatif Project location: CH3\whatif main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int temper; UartInit(); printf("Enter a temperature (0 to 100): "); scanf("%d", &temper); if (temper >= 30) { printf("\r\n\r\nSwitch the heater OFF"); } } while(1) { } Example Program Output Enter a temperature (0 to 100): 47 Switch the heater OFF This program asks the user to enter a temperature between 0 and 100. The if statement is used to determine if the temperature that was entered is greater than or equal to 30. If it is, a message is printed. If not the program skips the block of code between the braces under the if statement. In an if statement, the expression between the parentheses must evaluate to true (1) in order for the block of code between the braces to be run. If the expression evaluates to false (0), the entire block is skipped. When you run this program and enter a value that is below 30, it will appear that the program does nothing. This is because there is no other code that prints any output after the if statement is evaluated. Try modifying the program by adding a line above while(1) that prints a message such as: "End of program.". 3.2.2 • Using else in Conjunction with if The problem with using the whatif program for controlling a temperature is that it will not do anything to switch the heater on if the temperature drops below 30. The else statement is used to run a block of code when the if statement evaluates to false. The ● 63 C Programming with Arduino ifelse program demonstrates how this is done. Project name: ifelse Project location: CH3\ifelse main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int temper; UartInit(); printf("Enter a temperature (0 to 100): "); scanf("%d", &temper); if (temper >= 30) { printf("\r\n\r\nSwitch the heater OFF"); } else { printf("\r\n\r\nSwitch the heater ON"); } } while(1) { } Example Program Output 1 Enter a temperature (0 to 100): 38 Switch the heater OFF Example Program Output 2 Enter a temperature (0 to 100): 19 Switch the heater ON Now if the temperature is greater than or equal to 30, a message is displayed to switch the heater off, otherwise a message to switch the heater on is displayed. This program works the same as the previous one, except that should the if statement evaluate to false, the code in the else block will run, skipping the code in the if block. This is known as branching as the program will branch to the else statement. Should the if statement be true, the code in the block below the if statement will be run and the block of code under the else statement will be skipped over. Figure 3-2 on page 65 shows the program flow of the ifelse.c program. ● 64 Chapter 3 • Comparative Operators and Decisions Figure 3-2 Program Flow in an if – else Construct In an embedded system that has the hardware in place to measure temperature and switch a heater element, the on and off messages can be replaced with the code that will actually switch the heater element on or off. The temperature of the quantity being heated would be obtained by reading a temperature sensor attached to the embedded system. The value obtained from the thermometer would be put into the temper variable. In the body or block of both the if and else statements, there can be multiple statements. e.g.: if (x < y) { statement1; statement2; } else { statement3; statement4; } When the if evaluates to true, statement 1 and statement 2 will run. When the if evaluates to false, statements 3 and 4 will run. If the if or else blocks contain only one statement each, then the body of the if or else is not required to have braces. The if – else construct used in ifelse could have been written like this: if (temper >= 30) printf("\r\nSwitch the heater OFF"); else printf("\r\nSwitch the heater ON"); I find that a program is easier to read when the enclosing braces of a single line statement are in place, so I prefer to always put them in. This is a matter of personal preference and programming style. Code examples in this book will contain a mix of both methods when dealing with single line statements in the body of if and else blocks. 3.2.3 • The = Operator and the == Operator It is important to know the difference between the assignment operator (=) and the comparative operator (==) (also known as the equality operator). The assignment operator copies the value or result of an expression that is to the right of it into the variable to the left of it. e.g. var = 3; The comparative operator compares the values to the left and right of it, and if they are ● 65 C Programming with Arduino equal to each other, evaluates to true (1). If the values are not equal to each other, the comparison evaluates to false (0). e.g. if (var == 3) In an if statement, any non-zero value will evaluate to true (1). For example: if (24) { printf("This code will always run!"); } When this if statement is evaluated, it will always evaluate to true because the number between the parentheses is non-zero. It is also possible to use the assignment operator within the parentheses of the if statement as follows: if (var = 3) { printf("This code will always run!"); } The variable var is assigned the value of 3 – var is therefore non-zero and will evaluate to true, resulting in the code within the if statement block always being run. This example code will compile and run and is perfectly legal in C, but may not be what the programmer intended. Accidentally using the assignment operator instead of the comparative operator is a common error, where the programmer intends to compare the two values to see if they are equal, but ends up assigning a value to a variable instead. The comparison of two constants by accidentally using the = assignment operator will cause the compiler to generate an error. If the value to the left of the = sign is a constant, the compiler will always generate an error. Not possible to assign a constant to a constant if (3 = 4) { } Legal to compare two constants (although the result of the comparison will always be the same) if (3 == 4) { } Not possible to assign a variable to a constant if (3 = var) { } Legal to compare a constant with a variable if (3 == var) { } Legal to assign a constant to a variable if (var = 3){ } Legal to compare a variable with a constant if (var == 3) { } The next program, fussy, shows how the equality operator works. ● 66 Chapter 3 • Comparative Operators and Decisions Project name: fussy Project location: CH3\fussy main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int temper; UartInit(); printf("Enter a temperature (0 to 100): "); scanf("%d", &temper); if (temper == 30) { printf("\r\n\r\nThe temperature is just right!"); } else { printf("\r\n\r\nThe temperature is too hot or too cold."); } } while(1) { } Example Program Output 1 Enter a temperature (0 to 100): 78 The temperature is too hot or too cold. Example Program Output 2 Enter a temperature (0 to 100): 30 The temperature is just right! When run, the program asks the user for a temperature between 0 and 100. The == operator is used to check for an exact temperature of 30. Only if the user enters 30 will the block of code under the if statement be run. If any other value is entered, the code in the else block is run. 3.2.4 • Using else – if in Conjunction with if If you want to write a program that checks for one of several possible values, you could use multiple if statements to do the checking as the multif program shows. In this program the user must enter either 1 or 2 to start a pump or mixer (represented by displaying a message in the terminal window). If the user enters 1, the "pump started" message will be displayed. The program will then check to see if the user entered 2, which is unnecessary because the value of input in this case is 1. If the variable input has the value of 1, it can’t have the value of 2 as well. If we wanted to add an else statement to take care of an incorrect number entered by the user, this would be difficult to check for. Thankfully it is possible to use the else if construct as shown in Figure 3-3 on page 68. ● 67 C Programming with Arduino Project name: multif Project location: CH3\multif main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int input; UartInit(); printf("Enter 1 to start pump\r\n"); printf("Enter 2 to start mixer\r\n"); scanf("%d", &input); if (input == 1) { printf("\r\nPump started.\r\n"); } if (input == 2) { printf("\r\nMixer started.\r\n"); } } while(1) { } Example Program Output 1 Enter 1 to start pump Enter 2 to start mixer 1 Pump started. Example Program Output 2 Enter 1 to start pump Enter 2 to start mixer 2 Mixer started. Figure 3-3 The else if Construct In the else if construct, the comparison at the else if statement will be skipped, should the result of the first if statement be true. Should the first if statement be false, the comparison at the else if statement will be evaluated. ● 68 Chapter 3 • Comparative Operators and Decisions Multiple else if statements can be placed after an initial if statement. A single else statement can be used at the end to run code if all of the previous evaluations were false. The pumpctrl program clarifies this. Project name: pumpctrl Project location: CH3\pumpctrl main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int input; UartInit(); printf("Enter 1 to start pump\r\n"); printf("Enter 2 to start mixer\r\n"); printf("Enter 3 to stop pump\r\n"); printf("Enter 4 to stop mixer\r\n"); scanf("%d", &input); if (input == 1) { printf("\r\nPump started.\r\n"); } else if (input == 2) { printf("\r\nMixer started.\r\n"); } else if (input == 3) { printf("\r\nPump stopped.\r\n"); } else if (input == 4) { printf("\r\nMixer stopped.\r\n"); } else { printf("\r\nInvalid option\r\n"); } } while(1) { } In this program, the value of the variable input is checked for four different values. If none of the values are found, the code in the else block is run. If one of the values is found, the code in the block under the statement that evaluates to true will be run. After that, the program will continue to run at while(1) – all other statements in the else – if construct are skipped. Don’t forget that you can run the program multiple times by pressing the reset button on the Arduino. Example Program Output 1 Enter 1 to start pump Enter 2 to start mixer Enter 3 to stop pump Enter 4 to stop mixer 1 Pump started. ● 69 C Programming with Arduino Example Program Output 2 Enter 1 to start pump Enter 2 to start mixer Enter 3 to stop pump Enter 4 to stop mixer 3 Pump stopped. Example Program Output 3 Enter 1 to start pump Enter 2 to start mixer Enter 3 to stop pump Enter 4 to stop mixer 9 Invalid option True, 1, High and False, 0, Low True, 1, High – In programming and logic systems true, 1 and high mean the same thing. True has the logic value of 1 and is often referred to as high or logic high because it is represented by the high voltage level in a logic system such as 5V. False, 0, Low – The opposite of true, 1 and high is false, 0 and low. False, 0 and low all refer to the same logic state with 0 being the logic value of false. Low or logic low refers to the low voltage, or 0V (zero volts) of a logic system. These three ways of referring to each of the logic states are often used interchangeably when talking about logic systems and programming. 3.3 • Exercises 3.3.1 • Variable Compare Write a program that compares two integer numbers of different values entered by the user and then tells the user which one is bigger. Hint: We have been comparing variable values with constants, but two variable values can also be compared. 3.3.2 • Variable Compare 2 Modify the previous exercise to test if the first number entered by the user is greater than, less than or equal to the second number entered by the user. 3.4 • Solutions 3.4.1 • Solution to 3.3.1 This solution does not take into consideration the case where the two numbers are equal, resulting in the code in the else block being run when the numbers are equal. The next solution shows how to fix this problem. ● 70 Chapter 3 • Comparative Operators and Decisions Project name: CH3_1_compare Project location: Exercises\CH3_1_compare main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int num1, num2; UartInit(); printf("Enter a number: "); scanf("%d", &num1); printf("\r\nEnter a different number: "); scanf("%d", &num2); if (num1 > num2) { printf("\r\n%d is bigger than %d", num1, num2); } else { printf("\r\n%d is bigger than %d", num2, num1); } } while(1) { } 3.4.2 • Solution to 3.3.2 Project name: CH3_2_compare2 Project location: Exercises\CH3_2_compare2 main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int num1, num2; UartInit(); printf("Enter a number: "); scanf("%d", &num1); printf("\r\nEnter another number: "); scanf("%d", &num2); if (num1 > num2) { printf("\r\n%d is bigger than %d", num1, num2); } else if (num1 < num2) { printf("\r\n%d is smaller than %d", num1, num2); } else { printf("\r\n%d is equal to %d", num1, num2); } ● 71 C Programming with Arduino } while(1) { } You could also check for num1 == num2 by using an else if block in place of the else block above and leave the else block off altogether. This is, however, unnecessary as if a number is neither greater than nor less than another number, it must be equal to the other number. Alternatively you could firstly check if the two numbers are equal to each other, then if the first is greater than the second. The else block would then be run if the first number was less than the second. (Do any two of the checks in the if and else if statements and the else statement will take care of the third condition). 3.5 • Summary • When two values are compared by a comparative operator in C, they will evaluate to either true or false. • In a computer or microcontroller, true is represented by 1 and false is represented by 0. • Decisions can be made in C by using the if statement to evaluate a comparative expression. • When the comparative expression inside an if statement evaluates to true, the block of code between braces under the if statement is run. Should the expression evaluate to false, the block of code under the if statement will be skipped over. The block of code is delimited by braces {}. • The else statement can be used to run a block of code when the if statement evaluates to false. Should the if statement evaluate to true, the block of code under the else statement will be skipped over (also known as branching or jumping – this also occurs when a single if statement is false; program flow branches over the if block). • Any non-zero value between the parentheses of an if statement will evaluate to true. • A common error in C is to accidentally use the = assignment operator instead of the == comparative operator when comparing two values to see if they are equal. • if and else are both C keywords. • The else if construct can be used to test for one of several conditions or values. There are more ways to make decisions in C programs than shown in this chapter. We will look at these in more detail in later chapters. ● 72 Chapter 4 • The while Loop and Commenting Code Chapter 4 • The while Loop and Commenting Code What you will do in this chapter: • Learn how to use the while loop • Improve the temperature controller example • Learn about commenting a program and programming style In the previous chapter we saw that a program can be made to branch when making a decision. Branching causes the regular flow of a program, from top to bottom through a list of statements, to change. Loops also change the flow of a program as explained in this chapter. 4.1 • The while Loop The while loop will cause a block of program code to run repeatedly from top to bottom while a certain condition is true. Running the code repeatedly is called looping. The water program demonstrates the while loop. Project name: water Project location: CH4\water main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int water_level = 0; UartInit(); printf("Pumping 10L of water...\r\n\r\n"); while (water_level < 10) { water_level = water_level + 1; printf("Progress: %d\r\n", water_level); } printf("\r\nFinished."); } while(1) { } Program Output Pumping 10L of water... Progress: Progress: Progress: Progress: 1 2 3 4 ● 73 C Programming with Arduino Progress: Progress: Progress: Progress: Progress: Progress: 5 6 7 8 9 10 Finished. The while loop evaluates a comparative expression to see if it is true or false in the same way as the if statement did in the previous chapter. The block of code between the braces under the while loop will run continually until the condition that it is evaluating becomes false. When the loop is run for the first time, the expression water_level < 10 is evaluated. Because water_level was initialised to the value of 0, the result of the evaluation is true (1). The code under the loop will now run because of this. If the evaluation had been false, the program flow would have ignored or "jumped over" the entire block of code inside the loop and carried on running any statements that occurred after this. The first line of code that runs in the loop adds 1 to the value of the variable water_level and then stores this result back in the water_level variable (0 + 1 = 1). This result is printed to the terminal window. Program flow has now reached the bottom of the loop, so the water_level < 10 expression is evaluated again. The value of water_level is 1, and 1 is less than 10, so the evaluation result is true again and the first statement within the loop block is run again. Running of this first statement again results in the water_level variable now containing the value 2 (1 + 1 = 2). As before, the result is printed to the terminal window and the loop expression is evaluated again. This process will continue looping and will increment water_level by one each time through the loop and print the result to the terminal window. Figure 4-1 The while Loop When the value of water_level reaches 9, the loop expression will still evaluate to true. After the incrementing of this variable on the next pass through the loop, it will be 10. The value of 10 is printed out to the terminal window. The loop expression gets evaluated again and evaluates to false because 10 is not less than 10. The code in the loop will now not be run again. Program flow continues at the printf() statement under the body of the while loop. Figure 4-1 shows how the while loop works. Upon running the two statements in this loop, the expression will be evaluated again. ● 74 Chapter 4 • The while Loop and Commenting Code 4.2 • Using if Inside the while Loop A while loop can be used to continually monitor a variable value as the following program, monit, shows. Project name: monit Project location: CH4\monit main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char key = 0; int num = 50; UartInit(); printf("Press u to increment number\r\n"); printf("Press d to decrement number\r\n"); printf("Press x to quit\r\n\r\n"); while (key != 'x') { printf("Number is %d\r\n", num); scanf("%c", &key); printf("\r\n"); if (key num } else if num } == 'u') { = num + 1; (key == 'd') { = num - 1; } printf("End of program."); } while(1) { } Example Program Output Press u to increment number Press d to decrement number Press x to quit Number is 50 u Number is 51 u Number is 52 u Number is 53 d Number is 52 d Number is 51 x End of program. ● 75 C Programming with Arduino When this program is run, the user can press the ‘u’ key on the keyboard to add 1 to a number (increment the number by one) or the ‘d’ key to subtract 1 from a number (decrement the number by one). The value of the number is initially set to 50 and the new value of the number is displayed on the screen each time that it is changed. The ‘x’ key can be pressed to quit the program which just breaks program flow out of the first while loop, runs the printf() statement below it and then drops into the continuous while(1) loop. Note that the program will not work if your Caps Lock key is on because the program is looking for lowercase characters only. In this program, two variables are defined and initialised with values – one is used to store the character of the key pressed by the user on the keyboard, the other is used to store the count value used in the program. The three printf() statements at the top of the program simply give the user instructions on how to use the program. The code in the first while loop will keep running so long as the variable key does not contain the character ‘x’. The character variable key is initialised to the value 0 at the start of the program to ensure that it does not contain ‘x’, which could happen if this variable is not initialised. An uninitialised variable can contain any random value that happens to be in the memory that is assigned to it and there is a chance that the value could contain ‘x’. The comparative expression of the while loop is evaluated and checked to see if key is not equal to (!=) ‘x’. When no key has been pressed by the user, the variable key is not equal to ‘x’, it is equal to 0, so the answer to the question "Is key not equal to ‘x’?" is true and the while loop is entered. Remember from Chapter 2 section 2.2.2 on page 48 that an individual character is written between single quotes in C. Upon entering the loop, the value of the variable num is printed to the screen. This variable was initialised to 50, so 50 is printed to the screen. The scanf() function is run next and will stop the program while it waits for the user to press a key. When the user does press a key, the value of the key is stored in the variable key and the next line of code is run. The if statement checks if the user pressed the ‘u’ key and if so, adds 1 to the variable num. If the key pressed is not ‘u’, the program flow jumps to the else if statement. The else if statement checks if the value of the key pressed is ‘d’. If it is, 1 is subtracted from the variable num. Regardless of whether one of the previous statements evaluated to true or false, program flow will continue at the top of the while loop where the loop expression is evaluated again. If the ‘x’ key was pressed, then the variable key would hold the character ‘x’. When the loop expression is evaluated again, it would evaluate to false and the loop would be exited. By exiting the loop, the program will run the printf() statement below the loop and program flow would drop into the while(1) loop. If none of the keys ‘u’, ‘d’ or ‘x’ were pressed, but some other key was pressed instead, the variable key would now contain that character value, but the program would just continue running without exiting the loop. 4.3 • The Guess My Number Game The next program, called guess, uses the if – else construct inside a while loop. Project name: guess Project location: CH4\guess main.c ● 76 Chapter 4 • The while Loop and Commenting Code #include <stdio.h> #include "stdio_setup.h" int main(void) { int guess; UartInit(); printf("Try to guess my number:\r\n\r\n"); printf("Enter a number between 1 and 100: "); scanf("%d", &guess); while (guess != 74) { if (guess > 74) { printf("\r\nNumber is Lower.\r\n"); } else { printf("\r\nNumber is Higher.\r\n"); } printf("Enter a number between 1 and 100: "); scanf("%d", &guess); } printf("\r\nYou guessed the number!\r\n"); } while(1) { } Example Program Output Try to guess my number: Enter a number between 1 Number is Higher. Enter a number between 1 Number is Higher. Enter a number between 1 Number is Lower. Enter a number between 1 Number is Lower. Enter a number between 1 You guessed the number! and 100: 5 and 100: 50 and 100: 80 and 100: 75 and 100: 74 The object of this program is for the user to guess a number. When run, the program prompts the user to enter a number between 1 and 100. The number is then compared to a constant value inside the program. If the user’s number is higher or lower than the programs value, a message is displayed to tell the user if his number is higher or lower. If the number entered by the user matches the program’s number, a message is displayed to say that the number guessed was correct. To really make this program into a proper game, the program should generate a random number for the user to guess, rather than have a fixed value programmed into it. We will skip this step to keep the program simple. An initial number is obtained from the user before evaluating the loop expression. If the user guessed the correct number the first time, i.e. 74, the loop expression would evaluate to false and the entire loop would be skipped over. ● 77 C Programming with Arduino If the first number that the user entered was not correct, the while loop would be entered into. The if – else construct is used to tell the user if the number was too high or too low. The user is then asked to enter a new number at the bottom of the loop. If the number is correct, the loop would be exited. If not the loop code will carry on running until the user enters the correct number. 4.4 • Back to the Temperature Controller Example In our temperature controller example from Chapters 2 on page 41 and Chapter 3 on page 61, the temperature is read by the program and a message is displayed to either turn on or off a heating element. The problem with the program is that the temperature is only read once. We can now use a loop to continually monitor the temperature and display the on or off message as the temperature changes. Our next program, called montemp, will allow the user to increase or decrease the temperature using the ‘u’ and ‘d’ keys on the keyboard. The program will keep monitoring the temperature and if it increases above a threshold value will display a message to switch the heater off. If below a threshold value the message to switch the heater on will be displayed. Project name: montemp Project location: CH4\montemp main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int temper = 30; char key = 0; UartInit(); printf("Press u to increase the temperature\r\n"); printf("Press d to decrease the temperature\r\n"); printf("Press x to quit the program\r\n"); printf("Temperature is %d deg. C ", temper); while (key != 'x') { scanf("%c", &key); if (key == 'u') { temper = temper + 1; } else if (key == 'd') { temper = temper - 1; } printf("\r\nTemperature is if (temper > 35) { printf("Switch element } else if (temper < 30) { printf("Switch element } else { printf("Temperature is } } printf("\r\nEnd of program."); ● 78 %d deg. C ", temper); OFF. "); ON. "); correct. "); Chapter 4 • The while Loop and Commenting Code } while(1) { } Example Program Output Press u to increase the temperature Press d to decrease the temperature Press x to quit the program Temperature is 30 deg. C u Temperature is 31 deg. C Temperature is correct. u Temperature is 32 deg. C Temperature is correct. u Temperature is 33 deg. C Temperature is correct. u Temperature is 34 deg. C Temperature is correct. u Temperature is 35 deg. C Temperature is correct. u Temperature is 36 deg. C Switch element OFF. d Temperature is 35 deg. C Temperature is correct. d Temperature is 34 deg. C Temperature is correct. d Temperature is 33 deg. C Temperature is correct. d Temperature is 32 deg. C Temperature is correct. d Temperature is 31 deg. C Temperature is correct. d Temperature is 30 deg. C Temperature is correct. d Temperature is 29 deg. C Switch element ON. x Temperature is 29 deg. C Switch element ON. End of program. There is nothing new to you in the montemp program, just more statements in the while loop. Because the Arduino is not interfaced to the thermometer or temperature sensor and heater element, the user must play the role of the heating system that the Arduino is controlling. The user must control the heater element by changing the temperature using the u and d keys and must respond to the on and off messages as a heater controlled by an embedded system would. The program is designed to keep a temperature between 30 and 35 degrees Celsius. If the user (heater) increases the temperature above 35, the message to switch the element off is displayed (the embedded systems switches the heater off). Of course it is up to the user to play the part of the heater and start decreasing the temperature when the heater is switched off by pressing the ‘d’ key a number of times to simulate the temperature decrease of the system. When the temperature is decreased below 30, a message is displayed to switch the heater element on. It is up to the user to now increase the temperature as the heater would and thus keep the temperature between the upper and lower limits (35°C and 30ºC). When controlling the heater element with the embedded system connected to a heater and temperature sensor, the program would be written to read the sensor continually and if the temperature was too high, switch the element off. If the temperature was too low, it would switch the element on. There would be no waiting in the loop as our example program does – waiting for a key press from the user. 4.5 • Commenting Programs Comments can be written into source code files by a programmer to explain what a program does, as well as to add further helpful information to the code. These comments ● 79 C Programming with Arduino are useful when the programmer wants to modify the program at a later date and may have forgotten exactly what the program code does. If another programmer is going to use or modify a program, comments in the source code will help him to understand the code and the intentions of the original author of the code. Note that comments should be used to explain what the program does, rather than explain how the programming language works. The next program called comment shows how comments are used. Project name: comment Project location: CH4\comment main.c #include <stdio.h> #include "stdio_setup.h" /* C style comment */ /* Multi line C style comment */ // C++ style comment // Let's see how to comment a program: /*-----------------------------------------------Project name: comment File name: main.c This program converts kilometres to miles ------------------------------------------------*/ int main(void) { float kilos; float miles; /* stores distance in kilometres */ /* stores distance in miles */ UartInit(); /* prompt the user for distance in km's */ printf("\r\nEnter distance in kilometres: "); scanf("%f", &kilos); /* convert kilometres to miles */ miles = kilos * 0.62137; /* show the user the result of the calculation */ printf("\r\nDistance is %.3f in miles.\r\n", miles); } while(1) { } Example Program Output Enter distance in kilometres: 25 Distance is 15.534 in miles. Comments are written between a forward slash and asterisk (/*) and an asterisk and forward slash (*/) e.g.: /* This is a comment */ ● 80 Chapter 4 • The while Loop and Commenting Code Comments included in the source code file and are ignored by the compiler. Comments can be written across multiple lines in a source code file e.g.: /* This comment occurs on multiple lines */ Most, if not all, modern C compilers allow the use of C++ comments. C++ comments can only be used on a single line and start with a double forward slash (//). There is nothing to terminate this style of comment, so everything to the right of the double forward slash is part of the comment. It is important to comment programs, particularly long ones. Commenting programs can save you hours of frustration when you come back to a program months after writing it and you can’t remember what it does or how some part of the program works. Comments should include such things as which units variables are stored in. In the above example, the comments clearly state what the variables are storing (distance) and in which units they are being stored (miles and kilometres). Other information can also be added to a file in the form of comments such as who the author of the file is, the date it was written, when it was modified, who modified it, details of the hardware that the program was written to run on, etc. The example programs in this book will contain few comments to save space in the book and to help keep the code uncluttered. Some of the later more advanced programs will show good use of comments. 4.6 • Programming Style Programming style refers to how a program is written in terms of where brackets and braces are placed, how the program is indented, how many spaces are used for indenting and other details. As an example, the programs that you have seen in this book have been indented by four spaces. Indenting refers to moving of statements or lines of code to the right inside a block of code. The Tab key is used to indent code, and depending on the code editor program, can be set to either insert a Tab character or insert space characters. 4.6.1 • Tab Settings in Atmel Studio In Atmel Studio, the editor is set to insert Tab characters by default and display them with a width of 4 spaces. Settings for the number of spaces to use for indenting and whether to use Tab characters or spaces can be set in Atmel Studio from the main menu under Tools → Options... and then expanding items from the left pane of the dialog box that pops up. Expand Text Editor → GCC and then click Tabs as shown in Figure 4-2 on page 82. 4.6.2 • Choosing Tab Width The default tab width of four spaces is a good choice which makes the code easy to read as the indenting is clearly visible without being too deep. Traditionally the tab character used to always be eight characters wide and many old command line text editors use this convention. It is commonly accepted for a tab to be set to a width of between two to eight characters in most programming languages. ● 81 C Programming with Arduino Figure 4-2 Tab Settings in Atmel Studio 4.6.3 • Code Indenting Examples The following program, called style1, has no indenting, but will compile and run without any problems. Project name: style1 Project location: CH4\style1 main.c /* style1.c */ /* a program without any indenting */ /* The program displays a count to the screen. Counts from 0 to 15, skipping 10 */ #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); int count = 0; printf("Counting to 15, skipping 10:\r\n"); while (count <= 15) { /* if the number is 10, skip over it */ if (count != 10) { printf("Count is: %d\r\n", count); } count = count + 1; } printf("Finished.\r\n"); while(1) { } } ● 82 Chapter 4 • The while Loop and Commenting Code The above program becomes easier to read when it is indented as shown in style2. Project name: style2 Project location: CH4\style2 main.c /* style2.c */ /* A program with indenting */ /* The program displays a count to the screen. Counts from 0 to 15, skipping 10 */ #include <stdio.h> #include "stdio_setup.h" int main(void) { int count = 0; UartInit(); printf("Counting to 15, skipping 10:\r\n"); while (count <= 15) { /* if the number is 10, skip over it */ if (count != 10) { printf("Count is: %d\r\n", count); } count = count + 1; } printf("Finished.\r\n"); } while(1) { } Program Output of style1.c, style2.c and style3.c Counting Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Count is: Finished. to 15, skipping 10: 0 1 2 3 4 5 6 7 8 9 11 12 13 14 15 This program can even be written on a single line and will still work. See style3 included with the example programs from the accompanying files and found in the project folder CH4\style3. The file will compile and run just as the two previous examples did. (Scroll to ● 83 C Programming with Arduino the right in the C source file of style3 to see the entire program.) 4.6.4 • Other Aspects of Programming Style Other aspects of style refer to where you place braces, as the next examples show around an if statement. As you have already seen: if (count != 10) { printf("Count is: %d\n", count); } Some programmers prefer this style: if (count != 10) { printf("Count is: %d\n", count); } The next example is only legal if there is a single statement under the if. Some programmers prefer this and will then choose one of the above styles if there is to be more than one statement under the if. if (count != 10) printf("Count is: %d\n", count); This is also legal for a single statement: if (count != 10) printf("Count is: %d\n", count); And this: if (count != 10) {printf("Count is: %d\n", count);} Some parts of coding style are just personal preferences, others are required to make a program clear, easy to read and understand. Bear this in mind when developing your own programming style. Consistency is very important after choosing a programming style. Make sure that all of your code uses the same style and formatting throughout each source code file and project. 4.7 • Exercises 4.7.1 • Subtract Write a program called subtract that asks the user for two numbers. The second number must be subtracted from the first and the result displayed. Use a loop to ask the user to either quit the program or do another calculation. ● 84 Chapter 4 • The while Loop and Commenting Code 4.7.2 • Program Loop The example program pumpctrl from the previous chapter has a problem in that the program exits after a selection is made by the user. Modify this program to run inside a loop and continually ask the user for an option. Add an extra menu option that the user can use to exit the program. 4.8 • Solutions 4.8.1 • Solution to 4.7.1 Project name: CH4_1_subtract Project location: Exercises\CH4_1_subtract main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int num1, num2; char key = 0; UartInit(); printf("Subtraction program\r\n"); while (key != 'x') { printf("\r\nEnter a number: "); scanf("%d", &num1); printf("\r\nEnter a number to subtract: "); scanf("%d", &num2); printf("\r\n%d - %d = %d\n", num1, num2, num1 - num2); printf("\r\nPress any key to subtract again\r\n"); printf("or x to quit\r\n"); scanf("%c", &key); // read extra character scanf("%c", &key); // read the actual key press } printf("\r\nEnd of program.\r\n"); } while(1) { } The above solution to the exercise is one possible solution. You could have used floating point variables if you chose to and used a different key press to quit the program. Note that the subtraction can be done in the printf() statement. The program creates a variable internally to hold the result of the subtraction in this case. You could of course create a variable to hold the result of the calculation. An extra character is received by the program from the terminal when Enter is pressed after the second number is typed in. This causes the last scanf() function in the loop to receive the character and go back to the top of the loop to start another subtraction. A second scanf() function is needed above the last scanf() to absorb the extra character. ● 85 C Programming with Arduino 4.8.2 • Solution to 4.7.2 Project name: CH4_2_pumpctrl_loop Project location: Exercises\CH4_2_pumpctrl_loop main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int input = 0; UartInit(); while (input != 5) { printf("Enter 1 to start pump\r\n"); printf("Enter 2 to start mixer\r\n"); printf("Enter 3 to stop pump\r\n"); printf("Enter 4 to stop mixer\r\n"); printf("Enter 5 to exit\r\n"); scanf("%d", &input); if (input == 1) { printf("Pump started.\r\n"); } else if (input == 2) { printf("Mixer started.\r\n"); } else if (input == 3) { printf("Pump stopped.\r\n"); } else if (input == 4) { printf("Mixer stopped.\r\n"); } else if (input == 5) { printf("Exiting."); } else { printf("Invalid option\r\n"); } } printf("\r\nEnd of program.\r\n"); } while(1) { } 4.9 • Summary ● 86 • A loop can be used to alter the flow of a program and to run program statements more than once. • The while loop will loop through the lines of code contained between its braces as long as the loop expression remains true. • After the last line of code in the loop has been executed, the loop expression will be evaluated again. • A loop can be used to continually monitor an external parameter, such as temperature, in an embedded system. Chapter 4 • The while Loop and Commenting Code • Comments can be added to source code to explain what a program does as well as to include other information such as who the author of the program is, when it was created, what units a variable is in, etc. • Programming style is a matter of personal choice – use it wisely. • The main objective when developing your personal style of programming should be to make programs simple, clear, and easy to read and understand. • If you work for a software company, they may have a styles guideline document that will outline the preferred style that they would have you use when programming for them. This chapter covered the while loop only. We will see that other kinds of loops are available in C later in the book. The next chapter looks at functions, which are an important part of C programming. ● 87 C Programming with Arduino ● 88 Chapter 5 • Functions Chapter 5 • Functions What you will do in this chapter: • Learn how to write functions • Use preprocessor directives • Learn about header files • See how functions relate to linking and library files You have already used the printf() and scanf() functions. You have also written a function each time that you have written a C program – the function main(). The printf() function was supplied with the GCC toolchain that is installed with Atmel Studio. The actual C code that makes up this function is rather large and complex as the function has a lot of text formatting capabilities. If you had to write this code each time you wanted to print text to the screen, programs would be very large, hence the advantage of writing functions – the code for the function is written once and the function is used many times over. Another advantage of using functions is that your program can be split over multiple files. If a large program is stored in a single file, it would become very difficult to read and maintain. By breaking a program up into functions it can be split across two or more files. When splitting up a program into files, similar functions can be grouped together. You may decide to save all of the functions to do with serial communications into a file called serial.c and the functions to do with timers to timer.c. Code can also be easily reused when it is divided up into separate files. For a particular piece of hardware, or algorithm you only need to write the functions once and then include the file that contains the functions in each program that you wish to write that uses them. For example we used printf() in all of our programs without ever writing the printf() function. If printf() had been a function that you had written, you would only have to create it once and then use it whenever you wanted to. Technical Terms Algorithm – An algorithm is a method or set of tasks for solving a problem. An example of an algorithm is code used to recognise a fingerprint scanned in by a fingerprint scanner. We would refer to the method used in this code that processes, compares and recognises a fingerprint as a fingerprint recognition algorithm. 5.1 • Your Second Function This will be the second function that you are going to write as you have already written the function main() each time that you have written a C program. The program countfunc shows how to write a function. ● 89 C Programming with Arduino Project name: countfunc Project location: CH5\countfunc main.c #include <stdio.h> #include "stdio_setup.h" void Count(void); /* function prototype */ int main(void) { UartInit(); printf("Counting to 5\r\n"); Count(); /* call the function */ printf("Counting to 5 again\r\n"); Count(); /* call the function */ } while(1) { } /* the function */ void Count(void) { int count = 1; } while (count <= 5) { printf("%d\r\n", count); count = count + 1; } /* count from 1 */ /* count to 5 */ /* display the count */ /* increment the count */ Program Output Counting to 5 1 2 3 4 5 Counting to 5 again 1 2 3 4 5 In this example, a function called Count() is created and then called twice in the main program as shown in Figure 5-1 on page 91. The function simply prints out a count of 1 to 5. Near the top of the source file is the "function prototype" terminated with a semicolon i.e. void Count(void);. The function prototype tells the compiler that this function will be used in the program, similar to defining a variable that will be used in a program. The main() function is a special function that does not require a function prototype. The actual function is written below the closing brace of the main() function. The body of this function is contained between opening and closing braces. The use of the function in ● 90 Chapter 5 • Functions this example saves the programmer from having to write the same code twice to print out the count sequence. The function is written once and can be called any number of times in the program. The first use of the C keyword void in front of the function name tells the compiler that the function does not return a value. The second use of the keyword void between the parentheses of the function tells the compiler that the function takes no arguments. We will see what this means in the examples that follow. Figure 5-1 How a Function is Called 5.2 • Passing Data to a Function Data can be sent to a function by using arguments. The function is then able to operate on the data as the next program passcount shows. Project name: passcount Project location: CH5\passcount main.c #include <stdio.h> #include "stdio_setup.h" void Count(int num); int main(void) { UartInit(); printf("Counting to 2\r\n"); Count(2); printf("Counting to 10\r\n"); Count(10); /* count to 2 */ /* count to 10 */ ● 91 C Programming with Arduino } while(1) { } void Count(int num) { int count = 1; } while ( count <= num) { printf("%d\r\n", count); count = count + 1; } /* count from 1 */ /* count to num */ /* display the count */ /* increment the count */ Program Output Counting to 2 1 2 Counting to 10 1 2 3 4 5 6 7 8 9 10 This program is very similar to the previous one except that the value that the function is to count to is passed to it as an argument. When the Count() function is called for the first time, a value of 2 is passed to it (the function takes an argument). The value of 2 is stored in the variable num in this function. When this variable is used in the function, the count is printed to the screen and the while loop exited when a count greater than 2 is reached. When Count() is called the second time, 10 is passed to it and the while loop inside this function counts to the new value of num which is now 10. The variable between parentheses of the function takes on the value that is passed to it – in this example the variable num which is of type int. 5.3 • Passing More Than One Value to a Function In the previous example, a single value was passed to a function (the function took a single argument). More than one value can be passed to a function as the twoargs program shows, which contains a function that takes two arguments. ● 92 Chapter 5 • Functions Project name: twoargs Project location: CH5\twoargs main.c #include <stdio.h> #include "stdio_setup.h" void Count(int num, int skip); int main(void) { UartInit(); printf("Counting to 5, skipping 3\r\n"); Count(5, 3); /* count to 5, skip 3 */ printf("Counting to 12, skipping 10\r\n"); Count(12, 10); /* count to 12, skip 10 */ } while(1) { } void Count(int num, int skip) { int count = 1; } while ( count <= num) { if (count != skip) { printf("%d\r\n", count); } count = count + 1; } /* count from 1 */ /* count to num */ /* display the count */ /* increment the count */ Program Output Counting to 5, skipping 3 1 2 4 5 Counting to 12, skipping 10 1 2 3 4 5 6 7 8 9 11 12 In this program, two values are passed to the Count() function. The first value is passed to it in the variable num and the second in the variable skip. The variable num is used by the function as the value to count up to and the variable skip contains the number that the function will skip over when printing out the count sequence. When calling the function it is important to put the two numbers that are to be passed to the function in ● 93 C Programming with Arduino the correct order. If the first call to the function had been Count(3, 5); then the num variable would have been assigned the value 3 and the skip variable the value 5. In this case the function would just count to 3 as there is not a value of 5 in this sequence to skip over. As you have seen in the previous two examples, values are passed to a function by defining variables between the functions parentheses. If more than one value is to be passed to the function, the variables are separated by commas. A function that is to receive four values (take four arguments), for example, will be written like this: void MyFunc(int val1, int val2, float val3, char val4) { // the integers val1 and val2 can be used by the function // the floating point variable val3 can be used by the function // the character variable val4 can be used by the function } As this example shows, the type of data to be passed to the function is specified when declaring the function arguments. Each of these different data types are passed to the function and can be used by the function. Each new variable to be passed to the function between parentheses is separated by a comma. When calling this function from within a program, values passed to it must be in the correct order (two ints first, one float and then one char): MyFunc(100, 20, 43.45, 'z'); 5.4 • Passing a Variable to a Function In the preceding examples, we passed constant values to the functions. A variable value can also be passed to a function as vararg shows. Project name: vararg Project location: CH5\vararg main.c #include <stdio.h> #include "stdio_setup.h" void Message(char ch); int main(void) { char key = 0; UartInit(); printf("Press a key\r\n"); printf("Press x to exit\r\n"); while (key != 'x') { /* exit if x key pressed */ scanf("%c", &key); /* get key press from user */ Message(key); /* display message */ } printf("End of program."); ● 94 Chapter 5 • Functions } while(1) { } void Message(char ch) { if (ch == 'x') printf("\r\nBye!\r\n"); else printf("\r\nYou pressed %c\r\n", ch); } Example Program Output Press a key Press x to exit a You pressed a b You pressed b t You pressed t y You pressed y x Bye! End of program. A character variable key is passed to the Message() function in this example. The value of key is copied into the function’s variable ch when the function is called. 5.5 • Getting a Value Back from a Function Functions can return a value, meaning we can get a value back from a function. The type of value being returned must be specified (e.g. int, float, char) and can be returned by the function by using the return keyword as retval demonstrates. Project name: retval Project location: CH5\retval main.c #include <stdio.h> #include "stdio_setup.h" int prompt(void); int main(void) { int user_num; UartInit(); user_num = prompt(); printf("You entered %d\r\n", user_num); while(1) { } ● 95 C Programming with Arduino } int prompt(void) { int num; } printf("Please enter an integer number:\r\n"); scanf("%d", &num); return num; Example Program Output Please enter an integer number: 100 You entered 100 When the prompt() function is called, it asks the user of the program to enter an integer number. The number is then stored in the local variable num (local to the function, it can’t be "seen" by the main() function). The number is then returned to the main() function by using the return keyword, where it is assigned to the variable user_num using the assignment (=) operator. The return type of the function is specified by placing the variable type keyword in front of the function name, in this case, int. 5.6 • Passing Values to a Function and Receiving a Value Back The next example program addfunc receives two values passed to it and returns a value. Project name: addfunc Project location: CH5\addfunc main.c #include <stdio.h> #include "stdio_setup.h" float AddNum(float num1, float num2); int main(void) { float add_result; UartInit(); printf("Adding two numbers:\r\n"); add_result = AddNum(10.34, 100.56); printf("Result is %.2f", add_result); } while(1) { } float AddNum(float num1, float num2) { float result; } ● 96 result = num1 + num2; return result; Chapter 5 • Functions Program Output Adding two numbers: Result is 110.90 A function called AddNum() is used to add two numbers together and return the result of the addition. Floating point numbers are used throughout. The return type of the function is specified as floating point and the two numbers passed to the function are also specified as floating point. In this example you could, of course, have more easily just added the numbers together in main() without creating a function. More than one value can be returned from a function, but you will have to wait until we see how pointers work in a later chapter. 5.7 • Flashing LED Simulation Program The flashled program simulates a flashing LED by displaying an on or off message instead of switching an LED on or off. When using a real LED, the flow of the program will be exactly the same; the only difference being the code that displays the messages will be replaced by code that switches the LED on and off. A function is called to use up some time while the LED is either on or off. If this function was not present on a system using a real LED, the LED would flash so fast that it would look like it was permanently on, although it might look a bit dimmer. Project name: flashled Project location: CH5\flashled main.c #include <stdio.h> #include "stdio_setup.h" void Delay(void); int main(void) { UartInit(); } while (1) { /* printf("ON\r\n"); /* Delay(); /* printf("OFF\r\n"); /* Delay(); /* } continuous loop */ switch LED on */ leave LED on for a while */ switch LED off */ leave LED off for a while */ void Delay(void) { volatile long count = 1000000; } /* waste time */ while (count) { count = count - 1; } ● 97 C Programming with Arduino Program Output Sample ON OFF ON OFF ON The Delay() function works by counting to a very big value which uses up processor time. This leaves the message displayed on the screen for the duration of the delay before a new message is displayed. Changing the value assigned to count in the Delay() function will change the waiting time between messages sent to the terminal emulator. The volatile keyword is used when defining the count variable to stop the default optimisation setting of the compiler from optimising the delay loop away. Compiler optimisation sees that the variable count is not used in any program output and so removes it and the loop that it is used in. Optimisation and the volatile keyword is still to be explained in more detail. 5.7.1 • The while(1) Loop As already mentioned, when an embedded system program has finished running, unlike a PC, it has no operating system to return to. You will typically want to have the program running continually and never exit. If you were using a calculator for example, you would not want the calculator program to finish and then stop. You would want it to be ready for you to enter a new calculation each time. The simplest way of stopping a program from terminating is to write the body of the program in a while(1) loop. This is just a while loop that always evaluates to true because it has 1 as its loop expression. The typical simple embedded program will start by initialising any hardware used by the program upon entry into the C code. It will then enter the while(1) loop to run the bulk of the program continually. This is what the flashled program does by having the main program code inside the while(1) loop. For those who are familiar with the Arduino IDE, the code above the while(1) loop is the same as code put in the Arduino setup() function – this code only runs once when the Arduino is powered on or a new program is loaded. Code placed inside the while(1) loop is the same as code inserted in the Arduino loop() function – the code runs continually in a loop. 5.7.2 • The long Data Type A new data type called long is used in the Delay() function of the flashled program. This data type is an integer, but can hold a bigger number than the int data type. It can also be written as long int in the code as follows. long int count = 1000000; In the flashled program, an int was not big enough to hold the number 1000000, making it necessary to use the long data type. These two data types vary in size, depending on the microcontroller data bus width e.g. 8-bit or 32-bit. If this was a 32-bit microcontroller that the code was running on, an int would work and a long would not be necessary for the count variable. Data types and sizes will be explained in more detail after looking at binary numbers. ● 98 Chapter 5 • Functions 5.8 • Preprocessor Directives You have already seen the #include preprocessor directive in each program that you have written. The #include preprocessor directive is used to include a header file (a text file with a .h file extension). These files contain the function prototypes for functions such as printf() and scanf(). As you have learned, each function must have a function prototype. By including the header files, you include the function prototypes for the standard library functions that are part of the C language standard. Using the #include preprocessor directive to include a header file is like copying the contents of the header file and pasting them into the top of your C source file. The C standard library containing the standard library functions has a number of header files associated with it. Functions from the library file are linked with your object file during the linking process. The header files contain the function prototypes of the library functions that you use from the library file. Function prototypes for printf() and scanf() can be found in the stdio.h header file. The name stdio stands for standard I/O or standard input/output. Both printf() and scanf() are standard I/O functions. These are not the only functions in the library or function prototypes in the header files, there are a number of other functions available. While on the subject of names, the functions that you have been using from the C standard library are shortened names that describe the function: printf() = print formatted (there are lots of formatting options that can be used with this function like printing floats or ints, etc.). scanf() = scan formatted Back to preprocessor directives – there are additional preprocessor directives other than #include. Each preprocessor directive starts with the hash (#) symbol. The #define preprocessor directive can be used to name a constant value used in a C program. For example the line: #define MY_NUM 100 When this preprocessor directive is placed at the top of a C source file, it will enable you to use the text MY_NUM everywhere that you wish to use the value 100. If the value ever needs to be changed, it only needs to be changed in one place – where it is defined, and the new number will be updated in the code wherever the name MY_NUM is used. The name defined by the #define directive is usually written in upper-case letters by most C programmers by convention, although it is possible to use any combination of upper and lowercase letters. The rules that apply to variable names when defining variables also apply to names defined with #define – the name must start with an alphabet or underscore character, etc. In the defdel program, the previous program has been modified to define the delay count value using a preprocessor directive as shown in the code listing below. Project name: defdel Project location: CH5\defdel main.c #include <stdio.h> #include "stdio_setup.h" #define DEL_CNT 100000 ● 99 C Programming with Arduino void Delay(volatile long count); int main(void) { UartInit(); printf("\x1B[2J"); /* clear screen */ printf("\x1B[?25l"); /* hide cursor */ } while(1) { printf("ON \r"); Delay(DEL_CNT); printf("OFF\r"); Delay(DEL_CNT); } /* /* /* /* switch LED on */ leave LED on for a while */ switch LED off */ leave LED off for a while */ void Delay(volatile long count) { /* waste time */ while (count) { count = count - 1; } } Program Output The program sends commands to the terminal window to clear the screen and hide the cursor. ON and OFF text is displayed in the top left of the terminal window instead of printing on a new line. Besides using the #define preprocessor directive, the program has also been modified to send commands to the terminal to tell it to clear the window and hide the cursor. When the ON and OFF text is printed, it is followed by a carriage return character only which causes the now invisible cursor to move to the beginning of the current line so that new text printed overwrites the old text on the line. The Delay() function has been modified to take the delay count value as an argument. The #define directive has been used to define DEL_CNT to the value of 100000. 100000 will now be substituted into the program everywhere that DEL_CNT is used before the file is compiled. This is done by the preprocessor. The preprocessor is either a separate program that is run before the compiler or a process that is part of the compiler but is run before compiling. The preprocessor will substitute header files into the C source file when it finds #include directives and will substitute the constant value in place of the name that it was defined as using the #define directive wherever it finds the name in the source code. The preprocessor will change the code below: int main(void) { UartInit(); } while(1) { printf("ON \r"); /* Delay(DEL_CNT); /* printf("OFF\r"); /* Delay(DEL_CNT); /* } ● 100 switch LED on */ leave LED on for a while */ switch LED off */ leave LED off for a while */ Chapter 5 • Functions So that the compiler sees: int main(void) { UartInit(); } while(1) { printf("ON \r"); Delay(100000); printf("OFF\r"); Delay(100000); } /* /* /* /* switch LED on */ leave LED on for a while */ switch LED off */ leave LED off for a while */ The #define directive used in the above program enables the programmer to change the value passed to the Delay() function at the top of the source file, no matter where or how many times it is used in the program. This saves the programmer from having to search for each occurrence of the constant value in the program in order to change it. It can be changed once at the top of the file. Terminal Control Codes Control codes, in the form of escape sequences, can be sent to the terminal program in order to change its behaviour and settings as the previous C program demonstrates by clearing the screen and hiding the cursor. Control codes are recognised by the terminal program as they all start with the escape code hexadecimal number 1B, which can be sent as part of a string in C using the hexadecimal escape sequence \x1B as the printf() statements do in the previous program. The escape code is followed by a terminal command, for example: Clear Screen Hide Cursor Show Cursor – [2J – [?25l – [?25h Hexadecimal numbers are explained in the next chapter on number systems and escape sequences are still to be looked at in more detail. Another use of the #define directive is to define a mathematical constant. An example is the constant PI (π) used in mathematics that has a value of 3.14159 when rounded to five decimal places. A program using this value becomes a lot more self explanatory when using the letters PI in place of the number 3.14159. The mathematical formula for calculating the area of a circle is: A = πr² where: A = area π = 3.14159 r = radius of the circle (distance from the centre of the circle to the edge) r² means r × r This formula would be written in C as: area = 3.14159 * radius * radius; ● 101 C Programming with Arduino Predefined Constants PI is actually already defined in the header file math.h and will be available in any C source file that includes math.h using #include <math.h> at the top of the C file. PI is defined as M_PI in math.h and is shown below with some other predefined constants from math.h. #define M_PI 3.14159265358979323846 /* pi */ #define M_PI_2 1.57079632679489661923 /* pi/2 */ #define M_E 2.7182818284590452354 /* the function e */ #define M_SQRT2 1.41421356237309504880 /* the square root of 2 */ circle shows how to define PI to make a program more readable. Project name: circle Project location: CH5\circle main.c #include <stdio.h> #include "stdio_setup.h" #define PI 3.14159 float CircArea(float radius); int main(void) { float radius; UartInit(); printf("Enter radius of circle: "); scanf("%f", &radius); printf("\r\nArea of circle is %.3f", CircArea(radius)); } while(1) { } float CircArea(float radius) { return PI * radius * radius; } Program Output Enter radius of circle: 23.5 Area of circle is 1734.943 PI is defined using the #define directive as previously described and used in the CircArea()function to calculate the area of a circle when given the radius of the circle. Previously you always defined a variable when doing a calculation to hold the result of the ● 102 Chapter 5 • Functions calculation. If you look at the CircArea() function, no variable has been defined in which to store the result of the area calculation before it is returned by this function. This will still work in C. A place to store the calculation result before returning it is still needed, but when a program is written like this, a temporary variable is created in the program and is not seen by the programmer. Another new thing in this program is calling the CircArea() function directly in the printf() function. This is also perfectly legal in C and a temporary variable will again be created for you and passed to the printf() function when you write a program in this way. Notice also that a floating point variable radius is defined in main() and a floating point variable named radius is also defined as an argument of the function CircArea(). In C you can not have two variables of the same name in a program, but because the two variables are defined in two separate functions, it is not a problem that they have the same name. The code running in main() and using the variable radius does not know about the variable with the same name in the CircArea() function. This is called the scope of the variable. The variables defined in main() can only be "seen" and used in main(). The variables defined in any other function can only be "seen" and used in that function. When the CircArea() function is called in the above program, the variable radius in main() is copied into the CircArea() function’s variable radius as would occur if the variables had different names. 5.9 • Functions Calling Functions You can write functions that call other functions, which is useful when breaking up a very big program into smaller parts to make it easier to read and manage. The main() function has already been used to call library functions as well as other functions that you have written. The program that follows will clarify. You don’t have to type this program in, it is only for illustrative purposes and does nothing. void a(void); void b(void); int main(void) { a(); b(); } void a(void) { b(); } // prototype for function a // prototype for function b // main() calls function a() // main() calls function b() // function a() calls function b() void b(void) { } 5.10 • Using Multiple Source Files A C program can be split up into multiple source code files by using functions. Functions used in a program do not have to exist in the same source file as main(). One reason for splitting up a program into more than one file is to make a large program modular by grouping similar functions together with each group in its own file. A large program becomes more maintainable this way than putting all of the source code in a single file. Another reason is to easily reuse source code by copying the files that contain the desired ● 103 C Programming with Arduino functions into another project. The splitprog project below consists of two C source code files: main.c containing the function main() and splitfunc.c containing a function used by main(). To create the splitprog project, start a new project from the template file as you have been doing with the previous projects. Name the project splitprog, and type the code from the first code listing below into main.c. Save the file. 5.10.1 • Adding a New C Source File to an Atmel Studio Project To add a new source code file to an Atmel Studio project, right-click the project name in Solution Explorer then select Add → New Item... from the pop-up menu as shown in Figure 5-2 on page 105. In the Add New Item dialog box, click C File and then give the new C file a name in the Name: field at the bottom of the dialog box. For our splitprog project, the name of the file is splitfunc.c as shown in Figure 5-3 on page 106. Click the Add button when done. 5.10.2 • Project with Two C Source Code Files Project name: splitprog Project location: CH5\splitprog main.c /* splitprog project main.c file */ #include <stdio.h> #include "stdio_setup.h" /* prototype for function in splitfunc.c */ int FindBiggest(int num1, int num2); int main(void) { int first, second; UartInit(); printf("Program determines which number is bigger.\r\n"); printf("Enter an integer number: "); scanf("%d", &first); printf("\r\nEnter a 2nd integer number: "); scanf("%d", &second); printf("\r\n%d is bigger", FindBiggest(first, second)); } while(1) { } The second file: ● 104 Chapter 5 • Functions splitfunc.c /* splitfunc.c */ /* function returns the bigger of two numbers */ int FindBiggest(int num1, int num2) { if (num1 > num2) { return num1; } else { return num2; } } Example Program Output Program determines which number is bigger. Enter an integer number: 99 Enter a 2nd integer number: 81 99 is bigger Figure 5-2 Adding a New Item to an Atmel Studio Project The new C source file will be opened for editing in Atmel Studio and will appear in the Solution Explorer pane as part of the project. Type the code from the second listing into the new source file. Build and load the program to the Arduino as done with previous projects. ● 105 C Programming with Arduino During the build process of the splitprog project, two object files will be produced – one for each C source file. The linker will link the two object files together with the necessary library files to produce the executable output file. Figure 5-3 Adding a New C File to an Atmel Studio Project 5.11 • Header Files In the previous example, you could put the function prototype of the FindBiggest() function into a header file. The header file would then be included in the file containing main() by using the #include directive. The function prototype would then not have to be written at the top of the file containing main(). A header file called splitfunc.h would be added to the project in the same way that the C file was added, but by choosing Include File from the Add New Item dialog box from Figure 5-3 on page 106. splitfunc.h would contain the function prototype: int FindBiggest(int num1, int num2); The first few lines of main.c of the splitprog project would now contain: // main.c #include <stdio.h> #include "stdio_setup.h" #include "splitfunc.h" int main(void) ... ... Note that when including your own header file that is part of a project, the header file name is placed between inverted commas (" "). When using standard library header files, the header file name is placed between angle brackets (<>). These angle brackets are the same characters used as less than and greater than signs in C code. Using a header file to contain a single function prototype as above is not very useful. When a C source file contains many functions, it makes much more sense to use a header file for the function prototypes. For a source file containing ten functions, you will create the header file and then include it in every source file that uses one or more of the functions. ● 106 Chapter 5 • Functions 5.12 • How Functions Relate to Linking and Library Files After seeing how a program can be broken up into functions, you should now be able to understand the big picture of how the C tools work. Library files that are linked with your programs were originally written in C and then compiled. The object files that were produced by compiling the library C source code files are all grouped together into library files using a utility program that comes with the GCC toolchain. When the C tools are installed on your system during the Atmel Studio installation, these library files are installed along with header files that contain the function prototypes of the library functions. To use a function from one of these library files, you include the header file that contains the function prototype in your C source file and use the linker settings to link the corresponding library into your program when it is built. Creating a new Atmel Studio project from scratch includes some libraries by default. The template file for this book used to create a new project also contains linker settings to include specific libraries making in unnecessary to add these libraries to the linker settings for the projects that have been built so far. 5.13 • Exercises 5.13.1 • Parallel Resistor Calculation Write a function that calculates the resistance of two resistors in parallel using the following formula: Write a program that uses the function in order to test it and demonstrate that it works. 5.13.2 • Resistor Formula C File Create a project in Atmel Studio that has the usual main.c file and a separate file called resistance.c that contains the function from the previous exercise that calculates the resistance of two resistors in parallel. Add a function to the resistance.c file that calculates the resistance of up to three resistors in parallel where the third parameter passed to the function can be zero when calculating the value of two resistors in series. Create a file called resistance.h in the project that contains the function prototypes for the two resistance calculations. Write code in main() that uses the two resistance calculation functions to demonstrate that they work. The resistance of resistors in series is calculated by adding the resistances together, for example: ● 107 C Programming with Arduino 5.14 • Solutions 5.14.1 • Solution to 5.13.1 Project name: CH5_1_parallel_resistors Project location: Exercises\CH5_1_parallel_resistors main.c #include <stdio.h> #include "stdio_setup.h" float ParResistance(float R1, float R2); int main(void) { UartInit(); printf("470 ohms in parallel with 1k is: %.2f ohms\r\n", ParResistance(470.0, 1000.0)); printf("2k2 in parallel with 2k2 is: %.2f ohms\r\n", ParResistance(2200.0, 2200.0)); printf("4k7 in parallel with 10k is: %.2f ohms\r\n", ParResistance(4700.0, 10000.0)); } while(1) { } float ParResistance(float R1, float { float res_mult; // result float res_add; // result float res_div; // result R2) of resistor multiplication of resistor addition of resistor division res_mult = R1 * R2; res_add = R1 + R2; res_div = res_mult / res_add; } return res_div; Program Output 470 ohms in parallel with 1k is: 319.73 ohms 2k2 in parallel with 2k2 is: 1100.00 ohms 4k7 in parallel with 10k is: 3197.28 ohms In the above solution, the ParResistance() function is written out in a way that stores each intermediate part of the calculation in its own variable. This is probably the longest way to write this function which could be written in a much shorter form as shown below. float ParResistance(float R1, float R2) { return R1 * R2 / (R1 + R2); } In the short version of the function, the addition part must be placed in parentheses to force the result of the multiplication to be divided by the result of the addition, otherwise the result of the multiplication will be divided by the value of R1 and then the value of R2 added to it afterwards. ● 108 Chapter 5 • Functions This has to do with operator precedence which is explained in detail in section 12.3 of Chapter 12 on page 241. In most cases the same C program can be written in several different ways to achieve the same result. Aim to create C programs that are easy to read and understand as well as efficient in their use of memory and processing time. 5.14.2 • Solution to 5.13.2 Project name: CH5_1_resistor_calculations Project location: Exercises\CH5_1_resistor_calculations main.c #include <stdio.h> #include "stdio_setup.h" #include "resistance.h" int main(void) { UartInit(); printf("100 ohms and 470 ohms in parallel: %.2f ohms\r\n", ParResistance(100.0, 470.0)); printf("100 ohms, 2k2 and 4k7 in series: %.2f\r\n", SerResistance(100.0, 2200.0, 4700.0)); printf("220 ohms and 6k8 in series: %.2f\r\n", SerResistance(220.0, 6800.0, 0.0)); } while(1) { } resistance.c float ParResistance(float R1, float R2) { return R1 * R2 / (R1 + R2); } float SerResistance(float R1, float R2, float R3) { return R1 + R2 + R3; } resistance.h #ifndef RESISTANCE_H_ #define RESISTANCE_H_ float ParResistance(float R1, float R2); float SerResistance(float R1, float R2, float R3); #endif /* RESISTANCE_H_ */ Program Output 100 ohms and 470 ohms in parallel: 82.46 ohms 100 ohms, 2k2 and 4k7 in series: 7000.00 220 ohms and 6k8 in series: 7020.00 ● 109 C Programming with Arduino In main.c the printf() statements have been split after the comma so that part of the statement is on the next line in order to fit them on the printed page. The part of each statement that appears on the next line is tabbed to the right for better looking formatting. In the resistance.h file Atmel Studio automatically puts some preprocessor directives in the file when it is created in the project. These preprocessor directives prevent the header file from being included in a C file more than once. At this stage of the book and your programming knowledge, you can either delete these preprocessor directives from the header file or make sure that all code added to the header file is placed between them as shown in the solution above. More details on these preprocessor directives are presented in chapter 16, section 16.9 on page 296. 5.15 • Summary • Functions can be used to break a large program into smaller units. • Functions can reduce the size of a program by reusing code that is called often. • Every function must have a function prototype, except for main(). • Data can be passed to a function and received back from a function. • The type of data sent to or received from a function must be specified. • Preprocessor directives are read by and applied by the preprocessor before compilation. • The #include preprocessor directive is used to include a header file in a C source file. The header file can contain function prototypes and other information. • The #define preprocessor directive can be used to label a constant value. The constant value will be substituted in place of the label throughout the source file by the preprocessor. • A C program can consist of more than one source file. This is made possible by splitting the program into functions. • A multiple source file program is created by adding new source files in Atmel Studio. • Functions can be supplied to a programmer in a library file with corresponding header files. The programmer does not necessarily have to have access to the source files of the functions when supplied in this way. This is the case with the C standard library supplied with the C compiler and containing functions such as printf() and scanf(). The main objective in this chapter was to teach you how to write your own functions which is a big part of learning the C language. In the next chapter we look at number systems, including the binary numbering which is an essential part of programming embedded systems. ● 110 Chapter 6 • Number Systems Chapter 6 • Number Systems What you will do in this chapter: • Learn why we use binary numbers • See how the binary numbering system works • Learn to read and use hexadecimal numbers • Use hexadecimal numbers in C programs • The ASCII Alphanumeric code Everything in a computer or microcontroller is stored as a binary number, whether it is text, a program, a photograph or video. A computer only works with binary numbers. It is therefore important to know how number systems work and this chapter will explain what you need to know about numbers as an embedded C programmer. 6.1 • Binary Basics Here is an example of a binary number: 11001000 This number is said to be an 8 bit number as it consists of 8 digits or bits. Each binary digit is either a zero (0) or a one (1). The digit to the very right of the number is known as the least significant bit (LSB) or least significant digit (LSD). The digit to the very left of the number is known as the most significant bit (MSB) or most significant digit (MSD). This is always the case, no matter how many bits the number consists of. The word "bit" comes from the words binary digit. When memory is referred to in relation to computers, it is usually specified in bytes e.g. 16 kilo bytes. A byte consists of 8 bits. A nibble is half a byte or 4 bits. The length of a word is dependent on the system architecture to which it applies and may be 8, 16 or 32 bits for example. A word with a length of 32 bits is referred to as a 32-bit word. 1 1 1 1 bit = nibble = byte = word = a single binary digit and can have a value of 0 or 1 a sequence of 4 bits a sequence of 8 bits depends on data bus width of processor When a binary number is written in text, it is usually terminated with the letter ‘B’ to distinguish it from a decimal number or a number from a different numbering system. Here are some examples: 1 1 1 1 bit nibble byte 16 bit word 1B 1010B 00101111B 1000100100001101B Some texts may use a lower case b at the end of a binary number e.g. 1001b. Others ● 111 C Programming with Arduino may use a small 2 e.g. 111001012 where the 2 represents the two digit values (0 and 1) of the binary system. 6.2 • The Need for Binary Numbers Numbers represented on a computer consist of voltage levels. Only two voltage levels are used: zero volts (0V) and a positive voltage e.g. five volts (5V). Binary numbers consist of only two digits zero (0) and one (1). A binary digit 0 is stored as a voltage level of 0 volts and a binary digit 1 is stored as a positive voltage, 5V for example. If you could open up a memory chip that is attached to a computer system and could use a voltmeter (or multi-meter set to voltage) to measure each binary digit in the chip, you could write out what was stored in the chip. Every time you measured 5V you would write down a 1 and every time that you measured 0V you would write down a 0. So a series of measurements as follows: 5V, 0V, 0V, 5V, 0V, 0V, 0V, 5V would represent the binary number: 10010001 The actual voltage used to represent a binary digit 1 is dependent on the system that it is used in. In the past, digital systems always used 5V to represent the binary digit 1. Many modern microprocessors run on lower voltages like 1.8V. On these systems a binary 1 would be represented by 1.8V. Memory interfaced to one of these systems would then have to consist of 1.8V memory chips. AVR microcontrollers on the Arduino Uno and MEGA boards are capable of running at voltages below 5V, but the power supply on the these Arduino boards is set to 5V. On these Arduinos a binary digit 1 would be represented by 5V. 6.3 • Numbering Systems 6.3.1 • A Quick Look at Decimal Numbers To help understand how binary numbers work, we will have a quick review of the decimal numbering system. The decimal number system should be familiar to you. Decimal numbers consist of ten digits 0 to 9 (1 to 9 = 9 digits, 0 to 9 = 10 digits). The decimal system is therefore said to have a base of ten. The use of only ten digits does not prevent us from counting higher than nine. When counting to a value larger than 9, we use two or more digits. The position of each digit in the number tells us what size the digit is. This is known as a weighted numbering system, for example the number 44 contains the same two digits (symbols), but their position in the number determines the value of each digit. The weight of each digit is a power of 10 increasing from right to left, with the first digit on the right being 1. For the example of the number 44, the first digit on the right has a weight of 1 and the second digit from the right has a weight of 10: 4 × 10 + 4×1 = 44 (4 10s and 4 1s) The "power of ten" is a mathematical term and is written mathematically as: 10° = 1 (ten to the power of zero equals one) 10¹ = 10 (ten to the power of one equals ten) 10² = 100 (ten to the power of two equals one hundred) ● 112 Chapter 6 • Number Systems 10³ = 1000 (ten to the power of three equals one thousand) And so on... In mathematics any number raised to the power of zero equals one, that is why 10° = 1. Any number raised to the power of one is equal to itself: 10¹ = 10. The number that ten is raised to in the above examples is the number of times that ten is multiplied by itself: 10° = 1 (any number raised to the power of 0 equals 1) 10¹ = 10 (any number raised to the power of one equals itself) 102 = 10 × 10 = 100 103 = 10 × 10 × 10 = 1000 The number 932 consists of 9 100s (10²), 3 10s (10¹) and 2 1s (10°): 9 × 100 + 3 × 10 + 2×1 = 932 The number 8157 can be viewed as follows: Decimal numbers are written without anything appended to them to show which numbering system they are from e.g. 100 and 95 are decimal numbers whereas 100B represents a binary number. Decimal numbers are sometimes written with their base number included e.g. 19110 (one hundred and ninety one base ten) where 10 represents the ten digits that make up the decimal number system. 6.3.2 • Binary Numbers The binary number system uses only two digits as already explained (base two) and is also a weighted number system. When counting in decimal starting from 0, we move through the ten digits of the decimal system. After 9 is reached, the place holder to the left of the 9 is increased from 0 to 1 and the number becomes 10. Counting continues at 11 and when 19 is reached, the left most digit is incremented again so that the number becomes 20. This continues until 99 is reached and then the place holder to the left of this number is increased to 1 making the number 100. Binary works in the same way, but there are only two digits to count through until the place holder to the left of the number must be increased by 1. We start with a count of 0 and count to 1. We are now out of digits so need to increment the place holder to the left of the number by 1. So the binary number becomes 10 (one zero). The digit to the right is now incremented and the number becomes 11 (one one). This would be equivalent of running out of digits when 99 is reached in decimal. In decimal the count changes to 100 and in binary the same happens. The number now becomes 100, but the actual value is not read as "one hundred" when using binary it is read as "one zero zero" which has a count value of 4. After 100 (one zero zero), the count continues to 101, 110 and 111. This may seem strange at first as it is a new concept, it is not difficult to understand but will require some getting used to. You can use the Windows Calculator program to experiment with binary numbers as shown in Figure 6-1 on page 114. To start Windows Calculator click the Windows start ● 113 C Programming with Arduino button, type calculator and hit Enter. The first thing that you will need to do before using the calculator to work with binary numbers is to change it to programmer mode. In the calculator program click the View menu and then Programmer. Now click the Bin radio button, which is displayed in the calculator window when it is in programmer mode, to put the calculator into binary mode. We will use the calculator to count in binary, starting at 1 and adding 1 each time. Click the "1" button and then the "+" button. Now click the "=" button to start counting. Just keep clicking it to count up by 1 each time. Figure 6-1 Windows Calculator Used for Counting in Binary Any time that you would like to see what the equivalent decimal number is of your current binary number, just click the Dec radio button. Click the Bin radio button to change back to binary, you can then continue to click the "=" button to carry on counting. We have seen how decimal numbers are weighted. Binary numbers are weighted as follows: 20 = 1 21 = 2 22 = 4 (2 × 2) 23 = 8 (2 × 2 × 2) etc. The number 1110 when read from right to left consists of: 0×1+ 1×2+ 1×4+ 1×8 =8+4+2+0 = 14 Another example, the number 10100011 ● 114 Chapter 6 • Number Systems 1×1+ 1×2+ 0×4+ 0×8+ 0 × 16 + 1 × 32 + 0 × 64 + 1 × 128 = 128 + 0 + 32 + 0 + 0 + 0 + 2 + 1 = 163 You can use Windows Calculator to convert these binary numbers to decimal. First select binary mode using the Bin radio button, type the number into the calculator and then change to decimal mode using the Dec radio button. The number will be converted when changing modes. Table 6-1 shows a binary count of 0 to 15. Table 6-1 Decimal Numbers with Binary Equivalents Decimal Binary 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 10 1010 11 1011 12 1100 13 1101 14 1110 15 1111 An easy way to learn how to write a sequence of binary numbers as seen in the table is to start with the right most bit (LSB) and write down a sequence of alternating numbers vertically starting from 0 i.e. 01010101... The next number to the left of this will consist of a sequence of numbers also starting from 0, but changing every two numbers i.e. 001100110011… To the left of this number, sequences of four numbers starting from 0 i.e. 0000111100001111… To the left of this number, sequences of eight numbers starting from 0 i.e. 0000000011111111… Figure 6-2 on page 116 will make this clearer. ● 115 C Programming with Arduino Figure 6-2 An easy way to count in Binary Why we need to know about binary numbers Embedded system programming consists of communicating with hardware devices attached to the microcontroller as well as hardware devices that are found on the microcontroller chip itself. These hardware devices are set up and operated by writing numbers to them and reading numbers back from them. Take the example of a microcontroller interfaced to four LEDs as shown in Figure 6-3 on page 117. To switch on LEDs D1 and D4 we write the binary number 1001 to the LEDs. Because binary numbers are stored as voltage levels, we are actually connecting 5V to the anode of D1, 0V to the anode of D2, 0V to the anode of D3 and 5V to the anode of D4. When we write 1001 to the LEDs D1 and D4 will therefore switch on and LEDs D2 and D3 will switch off. We will see more examples of writing binary numbers to switch things on and off in the practical embedded programming chapters of this book. In C programs we actually use hexadecimal numbers instead of binary numbers, but hexadecimal numbers are a direct representation of binary numbers. You could also use decimal numbers in C as sending the number 9 to the LEDs will send the correct binary number of 1001, however it is easier to see what is being switched on or off when looking at the binary number. One can immediately see from the binary number that the first bit is switching something on, the middle two bits are switching something off and the last bit is switching something on. 6.3.3 • Hexadecimal Numbers The hexadecimal number system is a shorter way to represent binary numbers. We can use hexadecimal numbers directly in our C programs. Hexadecimal numbers make binary numbers easier to read. We have seen the base 10 decimal system and the base 2 binary system. The hexadecimal system is base 16 (hex = 6, decimal = 10, hexadecimal = 6 + 10 = 16 digits). When counting in decimal, we use ten digits 0 to 9. In the hexadecimal system ● 116 Chapter 6 • Number Systems we have an extra 6 numbers to count to and use the letters A to F to represent them. So counting up in hexadecimal using a single digit will look like this: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F Figure 6-3 LEDs Interfaced to a Microcontroller If we want to count higher than F, we are now out of digits and have to increment the place holder to the left of the single digit number, so the counting sequence continues as follows: 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, 1D, 1E, 1F, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 2A, 2B ... Hexadecimal numbers are terminated with an H when written in text or with a 16 for example: 19A34C0H or 19A34C016 In C source code hexadecimal numbers are written starting with 0x (zero ex) as follows: 0x19A3C0 Each hexadecimal digit directly represents a four bit binary number. If you want to know how high you can count to using a binary number of x length, use the formula: max_count = 2x - 1 (maximum count = two to the power of x minus one) where x is the number of bits in the binary number. If we want to know how high it is possible to count with just 2 bits, we calculate it as follows: max_count= = = = = 2x – 1 22 – 1 2 × 2 - 1 4 – 1 3 This means that it is possible to count to 3 using two binary digits. Let’s check this: ● 117 C Programming with Arduino 00B = 0 01B = 1 10B = 2 11B = 3 With Windows Calculator in decimal mode, you can do this calculation more easily. Start the calculator and put it into scientific mode by clicking View → Scientific. Now Press 2, then press the xy button, press 2 again and then =. You have now calculated 22. Now subtract 1: press - and then 1 and finally =. An 8 bit number can be used to count up to: max_count = = = = = 2x – 1 28 – 1 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 - 1 256 – 1 255 It is possible to count up to 255 with an 8 bit number. To do this calculation in Windows Calculator in scientific mode, press 2, xy, 8, -, 1, = Now let’s look at a 4 bit number: max_count = = = = = 2x – 1 24 – 1 2 × 2 × 2 × 2 - 1 16 – 1 15 In Windows Calculator, press: 2, xy, 4, -, 1, = 15 is the same number that a single hexadecimal number can count up to (F hex = 15 dec). Note that there are 16 hexadecimal digits, but the first digit is zero. Counting from 1 to F in hexadecimal is the same as counting from 1 to 15 in decimal or 1 to 1111 in binary (Table 6-2). You can use Windows Calculator in programmer mode to convert between numbers from different number systems. Switch the calculator into whichever number system you want to convert from by using one of the radio buttons: Hex for hexadecimal, Dec for decimal or Bin for binary. Type the number in that you want to convert and then using the radio buttons again, select the number system that you would like to convert to. To count up in hexadecimal, put the calculator into hexadecimal mode. Type "1" and then "+" and then continually press the "=" key to count up. Table 6-2 Counting in Decimal, Hexadecimal and Binary Decimal Hex Binary 0 0 0000 1 1 0001 2 2 0010 3 3 0011 4 4 0100 5 5 0101 6 6 0110 7 7 0111 8 8 1000 ● 118 Chapter 6 • Number Systems Decimal Hex Binary 9 9 1001 10 A 1010 11 B 1011 12 C 1100 13 D 1101 14 E 1110 15 F 1111 Why we need the hexadecimal number system in C programming Hexadecimal numbers are needed so that we can switch external hardware devices on and off just like with the binary example. You can not type a binary number directly into your C source code to use it in your program. You could write a function to do this, but an easier way is to use a hexadecimal number. Hexadecimal numbers are also shorter than binary when typed. For example the 32 bit binary number: 10000101111100001111000010101100 is written in hexadecimal as: 0x85F0F0AC How to use hexadecimal numbers in C If you had a microcontroller with a 32 bit port connected to 32 LEDs, you could write the above number to the port to switch the LEDs on and off. Let’s take a simpler example of an 8-bit port with 8 LEDs interfaced to it. Let’s switch the first two LEDs on, the next two off, the next 3 off and the last one on (2 on, 2 off, 3 off, 1 on). Notice firstly that I am visualising the LEDs in groups of four (groups of 1 hexadecimal digit each) that is why I have not written 2 on, 5 off, 1 on). The binary number to switch this sequence will be: 1100 0001 Now convert this binary number to hexadecimal to use it in a C program. You can just look up the number in table 6.3 or type it into Windows Calculator and convert it. When you have worked with binary and hexadecimal numbers for a long time, you will be able to convert them in your head. When using the table to look up the hexadecimal number, group the binary number into four bits each starting from the right of the number and look up each four bit number in the table. 0001B is 1H 1100B is CH The number will therefore be: C1H or 0xC1 when written in a C program. How to convert from binary to hexadecimal in your head You might want to come back to this method for converting binary numbers to hexadecimal in your head later after you have worked with binary and hexadecimal numbers for a while. ● 119 C Programming with Arduino Always work in groups of four digits, starting from the right of the binary number. Each bit is weighted as follows: Note that when numbering bits, they are numbered from right to left starting at 0. Now apply the weighting to each group of four bits by multiplying the weight by the binary digit under it. Add the results of this operation together, and then count in hex to convert. A few examples will clarify. To convert the 8-bit binary number 1100 0001 to hexadecimal: Taking the lower nibble (first four bits from the right: 0001) line it up under the weight values. Now multiply each bit by its weight and then add the results: 8 × 0 + 4 × 0 + 2 × 0 + 1 × 1 = 1 Now the upper nibble (left four bits of the byte): 8 × 1 + 4 × 1 + 2 × 0 + 1 × 0 = 12 The number 12 is in decimal, so we need to count through this number to convert it to hexadecimal. Start counting 1, 2, 3, 4, 5, 6, 7, 8, 9, we can’t count ten we must count A, B, C that’s 12 counts so 12 dec = C hex. Now put the two numbers together in the correct order and you get: 0xC1 Another example using the 32 bit number shown earlier: 10000101111100001111000010101100 First break it up into groups of 4: 1000 0101 1111 0000 1111 0000 1010 1100 Now convert each group of four. You may convert the groups from left to right, so long as you have broken them up from right to left. This is because if you have say, a 7 bit number like 1010111, it is broken up as 101 0111 and not 1010 111. Each group will be converted from left to right for this example. 1000 = 1 × 8 + 0 × 4 + 0 × 2 + 0 × 1 = 8 No need to count because 8 dec = 8 hex 0101 = 0 × 8 + 1 × 4 + 0 × 2 + 1 × 1 = 5 5 dec = 5 hex Start assembling the number: 0x85 1111 = 1 × 8 + 1 × 4 + 1 × 2 + 1 × 1 = 15 Start counting to 15 by counting through the hex number sequence. After 9, count A, B, C, D, E, F. You will quickly learn to recognise that when all 4 bits of the number are 1s, the biggest hex digit is used i.e. F. The number is now 0x85F ● 120 Chapter 6 • Number Systems 0000 = 0 × 8 + 0 × 4 + 0 × 2 + 0 × 1 = 0 0 again The number is now 0x85F0, we are half way there… 1111 = 1 × 8 + 1 × 4 + 1 × 2 + 1 × 1 = 15 F again. The number is now 0x85F0F 0000 = 0 × 8 + 0 × 4 + 0 × 2 +0 × 1 = 0 0 again. The number is now 0x85F0F0 1010 = 1 × 8 + 0 × 4 +1 × 2 + 0 × 1 = 10 Start counting to ten in hex. After 9 comes ten, but in hex it is A. The number is now 0x85F0F0A. Last number coming up… 1100 = 1 × 8 + 1 × 4 + 0 × 2 + 0 × 1 = 12 Count to 12 in hex 1…9, A, B, C. The number is 0x85F0F0AC. Let’s look at one final example, but this time simplifying things a bit. We will line up our 8421 weighting above each 4 bit section of the number in our minds and whenever there is a 1 in the number, we will write the weight down. Whenever there is a 0 in the number, we will write 0 down. Our example number is: 11011110 Split it up into fours: 1101 1110 Now do the conversion: 1101 = 8 + 4 + 0 + 1 = 13 Count up to 13 in hex: 1…9, A, B, C, D. 1110 = 8 + 4 + 2 + 0 = 14 Count up to 14 in hex: 1…9, A, B, C, D, E. The number is therefore: 0xDE 6.4 • Working with Hexadecimal Numbers in C Now that we have seen how hexadecimal and binary numbers work, let’s see how to use them in some C programs. You won’t be able to use a binary number directly in your C program, but will use the hex equivalent instead. The printhex program demonstrates how to print a hexadecimal number: Project name: printhex Project location: CH6\printhex main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { ● 121 C Programming with Arduino UartInit(); printf("%x", 0xA914); printf("\r\n0x%x", 0xA914); printf("\r\n0x%X", 0xA914); } // print a hex number // print same number with 0x // hex number in capitals while(1) { } Program Output a914 0xa914 0xA914 The example program shows that to write a hexadecimal number in C, it is always started with 0x. When printing a number in hexadecimal using printf(), the %x format specifier is used. The second time that the number is printed out, 0x is included in the text string so that the hexadecimal number is displayed as 0xa914 rather than a914. The third time that the hexadecimal number is printed out, the format specifier uses a capital x (%X) which prints the hexadecimal number using capital letters for the numbers A to F: 0xA914 instead of 0xa914. Numbers in a computer or microcontroller are always stored as binary numbers. When we print out a number in decimal or use a decimal number in our program, we are only displaying the number on the screen as a decimal number. We can view the same number as a hexadecimal number. When typing a decimal or hexadecimal number into a source code file, we are using a number that is easy for people to read. After compiling the source code and then loading the program to memory to run it, all of the numbers from the source code are converted to and stored as binary numbers. The next program called printnum demonstrates that we can view the same number stored in a variable as either decimal or hexadecimal. Project name: printnum Project location: CH6\printnum main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int the_num; UartInit(); the_num = 498; printf("The number is %d in dec\r\n", the_num); printf("The number is %X in hex\r\n", the_num); // change the number the_num = 0x3A9C; printf("\r\nThe number is %d in dec\r\n", the_num); printf("The number is %X in hex\r\n", the_num); } while(1) { } ● 122 Chapter 6 • Number Systems Program Output The number is 498 in dec The number is 1F2 in hex The number is 15004 in dec The number is 3A9C in hex When the variable the_num in this program is assigned a value of 498, it is actually being assigned the binary equivalent of the decimal number 498 which is 111110010B (check this on your Windows Calculator). In reality, a location in memory is set aside for this variable and after assigning it this number, it contains the voltage levels: 5V, 5V, 5V, 5V, 5V, 0V, 0V, 5V, 0V Where 5V represents whatever voltage level is used on the system for the binary value 1. Whether we choose to display the number as hexadecimal or decimal in our source code or screen makes no difference, the number is always stored as a binary number on the microcontroller, consisting of voltage levels. And now for a program that converts hexadecimal to decimal and vice versa, convnum. Project name: convnum Project location: CH6\convnum main.c #include <stdio.h> #include "stdio_setup.h" void menu(void); int main(void) { char menu_item = 0; int num; UartInit(); } while(1) { menu(); // display the menu scanf("%c", &menu_item); // get key press if (menu_item == 'h') { printf("\r\nEnter hex num: "); scanf("%x", &num); printf("\r\nNumber in dec is %d\r\n", num); scanf("%c", &menu_item); // remove incoming carriage return } else if (menu_item == 'd') { printf("\r\nEnter dec num: "); scanf("%d", &num); printf("\r\nNumber in hex is %X\r\n", num); scanf("%c", &menu_item); } else { printf("\r\nInvalid menu selection...\r\n"); } } void menu(void) { ● 123 C Programming with Arduino } printf("\r\nPress h to convert hex to dec."); printf("\r\nPress d to convert dec to hex. "); Example Program Output Press h to covert hex to dec. Press d to convert dec to hex. h Enter hex number: 2f Number in dec is 47 Press h to covert hex to dec. Press d to convert dec to hex. d Enter dec num: 478 Number in hex is 1DE A function is used for displaying a menu to the user in this program. The program waits for the user to select one of the menu items and then, depending on the user’s selection, will either prompt the user to convert a number from hexadecimal to decimal or decimal to hexadecimal. When converting the hexadecimal number to decimal, the number can be entered in upper or lowercase, with or without a preceding 0x e.g. 1de, 1DE, 0x1DE. As in the previous program, the number stored in a variable is printed in either hexadecimal or decimal to the screen. The size of the number that can be entered is limited by the size of an integer (of type int) on the system that the program is being run on. In the AVR 8-bit GCC compiler, an integer is 16 bits wide, limiting the decimal number that can be entered to 65535 (216 - 1 = 65535) which will convert to 0xFFFF. Any number greater than 65535 will have its upper digits truncated. When converting from hexadecimal to decimal, numbers higher than 0x7FFF will display as negative numbers. This is because an int is a signed number, meaning that it can store a positive or negative number. The number will become negative when the most significant digit of the int is set to 1. More on this later. 6.5 • The ASCII Alphanumeric Code When working with character data types (char) in C programs, you are working with ASCII characters. What does this mean? As already mentioned, computers and microcontrollers only work with numbers. When sending text from one microcontroller to another or writing and saving text on a computer, only numbers can be used. When a text file is opened, the computer knows that the file is a text file as it has a .txt file extension or other file extension that lets the computer know that the file contains text. When the computer reads the contents of the text file and displays it on the screen, it will read a byte at a time and will interpret each byte. If it were to read the number 0x41 (01000001B or 65), it will interpret this and display the letter ‘A’ on the screen. The computer uses the ASCII code to look up this number and display the correct letter or symbol. ASCII stands for "American Standard Code for Information Interchange". The ASCII code consists of letters from the alphabet, numbers 0 to 9 and other characters like brackets and punctuation marks. When the computer reads ASCII text, it will look up the numbers that the text represents in the ASCII code and display the corresponding letter or symbol. The original ASCII table consisted of 7-bit numbers (0 to 127), but it has been extended to 8 bit numbers (0 to 255) to support languages other than English as well as add some graphical symbols. See Appendix A on page 329 for the extended ASCII table. ● 124 Chapter 6 • Number Systems The next program called asciishow displays a character as ASCII text, hexadecimal and decimal. Refer to Appendix A on page 329 to see the characters used in this program and their numeric values. Project name: asciishow Project location: CH6\asciishow main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char chr; UartInit(); chr = 'T'; printf("Character:%c Hex: 0x%x Dec: %d\r\n", chr, chr, chr); chr = 'i'; printf("Character:%c Hex: 0x%x Dec: %d\r\n", chr, chr, chr); chr = '8'; printf("Character:%c Hex: 0x%x Dec: %d\r\n", chr, chr, chr); } while(1) { } Program Output Character: T Hex: 0x54 Dec: 84 Character: i Hex: 0x69 Dec: 105 Character: 8 Hex: 0x38 Dec: 56 The program prints out the value that the variable chr is holding three times in each printf() statement by specifying chr as the variable to print three times. The variable is first printed out as a character, then as a hexadecimal number and finally as a decimal number. The chr variable is first initialised with the character ‘T’. ‘T’ can be found in the ASCII table and has a value of 0x54 in hexadecimal or 84 in decimal. In the printf() statements, the value that chr is holding is printed as a character using the %c format specifier, then as a hexadecimal number using the %x format specifier and finally as a decimal number using the %d format specifier. The contents of variable chr remains the same throughout each printing. The reason that it is displayed differently is because it is first interpreted as an ASCII character, then as a number in two different number formats. When the variable chr is assigned a value of ‘i’, it prints out as 0x69 or 105 as a number. Again, these numbers can be found in the ASCII table as the number representing the ASCII character ‘i’. Finally chr is assigned the value ‘8’ which is the ASCII character ‘8’ and not the number 8. Numbers are also encoded in the ASCII table, so looking up the ASCII character ‘8’ shows that it has the numerical value of 0x38 or 56. ● 125 C Programming with Arduino From this chapter I hope that you understand that: 1. Numbers are stored as voltage levels on a computer: 0V, 5V, 0V, 0V, 0V, 0V, 5V, 5V Note that when copying data from magnetic or optical media to memory, data is changed from being stored as magnetic fields or pits and lands to voltage levels. A microcontroller can only work with voltage levels, so all data that is to be processed, no matter where it is stored, is first changed to voltage levels and later if saved to other media, changed back to magnetic fields or however it is stored on the particular media. 2. These voltage levels are known as "logic levels" and represented as binary numbers: 0100 0011 3. These same logic levels or binary numbers can be viewed by people or written in C programs as decimal numbers: 67 Or they can be viewed as hexadecimal numbers: 0x43 Or they can be viewed as characters: C So whether you type ‘C’, 67 or 0x43 into your C source file, you are essentially typing the same binary number of 01000011 and can choose in which number system to display it on the screen when your program prints it out. Note that this is not specific to the C language, but to computers and digital systems in general. 6.6 • Exercises 6.6.1 • Hexadecimal to Port A microcontroller is interfaced to an 8 bit port that is driving 8 LEDs. What hexadecimal number would you write to this port to switch the first two LEDs on (left most LEDs), the second two LEDs off, the next 3 LEDs on and the last (right most) LED off? 6.6.2 • Print Number Write a program to print the hexadecimal number 0x5f as a character, as a decimal number and as a hexadecimal number. 6.7 • Solutions 6.7.1 • Solution to 6.6.1 The LEDs will be On, On, Off, Off, On, On, On, Off. This equals: 1100 1110B The hexadecimal equivalent will be: ● 126 Chapter 6 • Number Systems 8 + 4 for the first hex digit on the left (left four bits) = a count of 12 = count from 10 to 12 in letters starting from A =C 8 + 4 + 2 for the hex digit on the right (right four bits) = a count of 14 = count from 10 to 14 in letters starting from A =E Put the two together and the answer is: 0xCE 6.7.2 • Solution to 6.6.2 printf("%c\t%d\t%X", 0x5F, 0x5F, 0x5F); Project name: CH6_1_print_number Project location: Exercises\CH6_1_print_number main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("%c\t%d\t%X", 0x5F, 0x5F, 0x5F); while(1) { } } Program Output _ 95 5F In this solution the Tab escape sequence \t is used to separate each value printed to the terminal window. The character printed is the underscore and can be found in the ASCII table in appendix A. 6.8 • Summary • Numbers are stored as voltage levels in a computer or microcontroller’s memory. • These voltage levels are represented in binary as zeros (0s) and ones (1s). • Writing a number to an external port on a microcontroller can switch devices that are attached to the port on or off. • Reading a number from an external port will get the state of external devices that are attached to the port. The state of these devices will be reflected in the ● 127 C Programming with Arduino number read from the port. ● 128 • Binary numbers can be represented conveniently and in a more compact format by hexadecimal numbers. • In the C programming language, hexadecimal numbers can be used directly to represent binary numbers and are written starting with 0x. • The ASCII alphanumeric code is used to interpret numbers as characters. • Any number in a computer or microcontroller’s memory can be represented as a binary number, a hexadecimal number and a decimal number. If the number is read as an 8-bit number it can be represented as an ASCII character. Chapter 7 • Memory and Microcontrollers Chapter 7 • Memory and Microcontrollers What you will do in this chapter: • Find out how memory works • Learn how a microprocessor accesses memory and peripherals • See how to access memory by using the address of the memory • Learn about other data types in C 7.1 • Memory Basics Two main types of memory are present on microcontrollers and embedded systems, namely ROM and RAM. These memory types are used for storing program instructions from a compiled C program and data variables that are defined in a C program. 7.1.1 • ROM (Read Only Memory) This type of memory is used to store the program that is written and compiled. It is nonvolatile memory meaning that it will retain its contents even if the power is switched off. ROM on a modern microcontroller is usually present as Flash memory as it is on the Arduino AVR microcontrollers. The contents of the Flash memory are normally not altered when a user application program is running, although some microcontrollers’ Flash memory is in-application programmable. The program stored in ROM memory will be the program that is run when power is supplied to the microcontroller. 7.1.2 • RAM (Random Access Memory) RAM is used to store variables and data from our program that can be changed while the program is running. This type of memory is volatile meaning the contents of the chip will be lost when power is removed. Microcontrollers usually contain some SRAM (Static RAM) as the AVR microcontrollers do. Figure 7-1 Memory Showing How Data is Stored 7.1.3 • Data Stored in Memory Data is stored in memory as an array of bytes. Each byte has a unique address and can be individually addressed. Data is stored to memory by a write operation and retrieved from memory by a read operation. Each bit in a byte of a memory chip is called a cell and can hold either a 1 or a 0 logic level as shown in Figure 7-1. The capacity of the memory ● 129 C Programming with Arduino chip is determined by how many addressable bytes it contains. 7.2 • A Look at a Memory Chip Let’s take a look at how a SRAM memory chip works in order to better understand memory. Figure 7-2 shows the top view of a 2kB (two kilo byte) memory chip. Figure 7-3 on page 131 shows a block diagram of the same chip. The size of the memory is determined by how many bytes it can address. This is determined by the number of address lines on the chip. The chip has 11 address lines and so can address 211 bytes = 2048 bytes (or 2 kilo bytes because 1 kilo byte = 1024 bytes or 210). Figure 7-2 An 8-Bit SRAM Memory Chip A bus is just a group of wires on a computer system. An address bus is a group of wires that are used for addressing memory and peripheral devices and is 11 bits wide on the memory chip shown. A data bus is the group of wires used for sending and receiving data to and from memory and peripheral devices. The data bus on the memory chip in the figure is 8 bits wide. A control bus is the group of wires used to control a memory chip or peripheral device. There are 3 control lines on the memory chip. A peripheral device is any piece of hardware interfaced to the microcontroller, whether it is an internal device or external to the microcontroller. ● 130 Chapter 7 • Memory and Microcontrollers Figure 7-3 Block Diagram of an SRAM Memory Chip The procedure for writing data to or reading data from a memory chip is described below. If you plugged this memory chip into an electronic breadboard and used wires to connect the chip as described, you could write data to the chip and read data from the chip by plugging the correct sequences of wires to the correct power supply terminals and reading the data by measuring voltage levels with a voltmeter. In order to operate the memory chip of Figure 7-2 on page 130, power must first be supplied to the chip on the VDD and GND pins – the positive voltage specified in the chip’s datasheet is supplied to VDD from a power supply and the 0V of the power supply is connected to the GND pin. The three control lines of the control bus, CS, OE and WE must be "pulled high" i.e. they must have a logic 1 applied to them – connected to the positive of the power supply. This is because they are "active low" meaning that they are active when logic 0 is applied to them. We know this because of the line above these names as shown in the figure. If there was no line, they would be "active high" – activated by a logic 1 level. The chip will now be in an inactive state and the voltage levels measured on the address and data bus will be "high impedance" meaning that they are neither at logic 0 nor at logic 1, but are offering a high resistance – more like an insulator than a conductor. From this inactive state, we can write something to the chip. First we decide at which address we would like to save the data byte that we are going to write. If we choose the first address in the chip, address 0, then we must supply the address 000 0000 0000B to the address bus. In other words we must connect the 11 address lines of the address bus (A0 to A10) to 0V of the chip’s power supply. Remember that we are counting from 0 so A0 to A10 are 11 lines, not 10. Next we must decide what data byte to write. Let’s say 120 decimal. This is the equivalent of 78 in hexadecimal which is 0111 1000 in binary. We must therefore connect D7 of the data bus to 0V of the power supply, D6 to D3 to the positive of the power supply and D2 to D0 to the 0V of the power supply. The address is now addressing the first data byte in the memory chip, the data bus has the data on it that we would like to save to the chip, we must now operate the control signals to move the data into the chip. The chip must first be selected by putting logic 0 on the CS pin i.e. connect it to the 0V of the power supply. Next we put logic 0 on the WE pin which moves our data byte into the chip and saves it at address 0. The CS and WE pins can now be moved back to logic 1 to disable the chip again. The chip will now keep our data byte stored at address 0 until we change it by writing a byte to the same address or the power is switched off. This whole operation is known as a write operation. To read our data byte back from the chip we must first apply the address to the chip’s address bus again. No voltage levels must be applied to the data bus or they may be shorted out when the read is performed. Next, the chip is enabled with the CS pin pulled to logic 0. The output enable (OE) pin is now pulled to logic 0 and the byte that we saved at address 0 will now appear as binary voltage levels on the data bus. You would be able ● 131 C Programming with Arduino to measure them with a voltmeter. This same process can be applied to any data byte in the chip by changing the address on the address bus. Want to read or write byte number 1045? Apply the address 1045 to the address bus which is 0x415 or 100 0001 0101B and then follow the read or write procedure. Table 7-1 shows the write and Table 7-2 shows the read of the memory chip graphically. Read each table from top to bottom where the first line shows the address and data bus in high impedance indicated by a Z at each bit, and all control signals are in their inactive state with logic 1 levels applied. Table 7-1 Writing 0x78 to a Memory Chip at Address 0 Address Bus Data Bus A 7 A 6 A 5 A 4 A 3 Control Bus A 10 A 9 A 8 A 7 A 6 A 5 A 4 A 3 A 2 A 1 A 0 A 2 A 1 A 0 C S W E O E Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z 1 1 1 0 0 0 0 0 0 0 0 0 0 0 Z Z Z Z Z Z Z Z 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 1 Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z 1 1 1 Table 7-1 rows are read from top to bottom as follows: Steps 1. Chip is inactive (Z = high impedance or disconnected) 2. Address is applied to address bus 3. Data to be written is applied to the data bus 4. Chip select CS is enabled 5. Activating the WE control line moves the data into the chip at the selected address 6. Chip is put back into the inactive state Table 7-2 Reading Data from a Memory Chip at Address 0 Address Bus Data Bus A 7 A 6 A 5 A 4 A 3 Control Bus A 10 A 9 A 8 A 7 A 6 A 5 A 4 A 3 A 2 A 1 A 0 A 2 A 1 A 0 C S W E O E Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z 1 1 1 0 0 0 0 0 0 0 0 0 0 0 Z Z Z Z Z Z Z Z 1 1 1 0 0 0 0 0 0 0 0 0 0 0 Z Z Z Z Z Z Z Z 0 1 1 0 0 0 0 0 0 0 0 0 0 0 Z Z Z Z Z Z Z Z 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 1 0 Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z Z 1 1 1 ● 132 Chapter 7 • Memory and Microcontrollers Table 7-2 on page 132 rows are read from top to bottom as follows: Steps 1. Chip is inactive (Z = high impedance or disconnected) 2. Address is applied to address bus 3. Chip is selected by activating CS 4. Output enable OE control line is activated 5. Data being read now appears on the data bus 6. Chip is put back into the inactive state 7.3 • How Microprocessors Access Memory and Peripherals A microprocessor has an address bus, a data bus and a control bus. A memory chip is interfaced to the microprocessor by connecting its buses to the microprocessors buses. On a microcontroller with on-chip memory, this is already done inside the chip. The microprocessor contains registers – these are similar to memory locations in the microprocessor as they store a number. One of these registers is called the program counter (PC) and the value in this register is placed on the microprocessor’s address bus. When power is applied to the microprocessor, the program counter is reset to a value of 0, so at reset an address of 0 appears on the microprocessors address bus (all address lines are at logic 0). When a non-volatile memory chip is wired to the microprocessor at this address and it contains a program, the microprocessor will automatically fetch the first instruction of the program from the memory chip at address 0 and execute it (an instruction is just a binary number). The program counter will then count up by one and fetch the instruction from the memory at address 1 and execute it. The program counter will keep counting up and fetching each successive byte of the program from the memory chip and execute it. If a jump instruction occurs, the address in the program to jump to is loaded into the program counter and execution of instructions continues from the new address with the program counter incrementing the address after each instruction is fetched. A microprocessor or microcontroller will "boot up" – by starting to run a program at a known address. The start-up address does not have to be 0, but usually is. Some microprocessor manufacturers will design their chips to start running at a different address. A non-volatile memory chip containing the program to be run must then be wired at this address by the hardware engineer who builds the system or by the microcontroller manufacturer when a microcontroller is designed. A peripheral device, like a bank of 8 LEDs, can be interfaced to the microprocessor by using a buffer chip that is like a single memory location. The chip will hold the voltage levels written to it and will be able to deliver enough current to drive the LEDs to switch them on. The voltage levels that the chip is holding are available externally on the pins of the chip and this is where the LEDs are connected. Writing to a single memory location in a memory chip and writing to a bank of LEDs is the same operation in a microprocessor. Writing to the memory location just saves a number, writing to the LEDs displays the number on the LEDs in binary where an LED that is on represents binary 1 and an LED that is off represents a binary 0. Figure 7-4 on page 134 shows a block diagram of a microprocessor interfaced to Flash memory, SRAM memory and peripheral devices. In C, we can write to or read from a peripheral that is at a specific address. This will cause the microprocessor to load this address on the address bus and fetch data from or send data to the device on the data bus. Addresses in C can be accessed by pointers which we will look at next. ● 133 C Programming with Arduino Figure 7-4 Microprocessor Interfaced to Memory and Peripherals 7.4 • Pointers Pointers in C point to locations in memory or can be used to point to control registers of a peripheral device. Pointers contain the address of the device or memory location that they are addressing or pointing to. Before we start to use pointers, let’s write a program that displays the address of a variable in memory. We know that a variable is a piece of memory set aside by the compiler for use in a program. Because the variable exists in memory, it must have an address. The address program displays the address of an integer variable. In this program, the value that the variable var is holding is printed first. The address of this variable is then printed out in decimal and hexadecimal. To print the address of the variable, we use the ampersand character (&). This should be familiar to you from using the scanf() function where we passed the address of the variable in which we wanted to save data entered by the user to scanf(). When using the name of a variable in C we are referring to the value that the variable (or memory location) is storing. When preceding the variable name with an ampersand, we are referring to the address of the variable in memory as Figure 7-5 on page 135 shows. Project name: address Project location: CH7\address main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int var = 100; UartInit(); printf("The variable var contains the value %d\r\n", var); printf("The address of var is %d = 0x%X", &var, &var); } while(1) { } ● 134 Chapter 7 • Memory and Microcontrollers Example Program Output for UNO The variable var contains the value 100 The address of var is 2298 = 0x8FA Example Program Output for MEGA The variable var contains the value 100 The address of var is 8697 = 0x21F9 Figure 7-5 Variable Address and Variable Data in C The actual address that a variable is located at is assigned during the build process in Atmel Studio. A number of factors can affect the address of a variable, such as the size of the program, optimisation settings of the compiler and the actual microcontroller selected. In a C program it does not matter where a variable is located because we refer to the variable by its name. A valid address will automatically be assigned to a variable during the build process depending on the above factors. This is the reason that the program output listed above is different for the Uno and Mega boards. These boards each have different microcontrollers which each have different memory sizes. For our next example, we create a pointer and make it point to a variable. If the variable was a port with LEDs attached to it, we would then be able to control the LEDs by writing to them. Project name: led_port Project location: CH7\led_port main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int LEDs = 0x88; int *ptr; /* a variable representing LEDs */ /* a pointer */ UartInit(); ptr = &LEDs; /* get the address of the LEDs */ printf("LEDS are set to %x\r\n", *ptr); *ptr = 0x44; printf("LEDS are set to %x", *ptr); } while(1) { } ● 135 C Programming with Arduino Program Output LEDS are set to 88 LEDS are set to 44 Here we are just simulating a port connected to LEDs by using a variable, but normally in an embedded system the LED port will be at a specified address. The hardware engineer who designed the system would have allocated it to the address, and he would have to specify that the port is at, for example, address 0x0500. In the above code we don’t have any control of the exact location in memory that the build process puts the variable, so the address of the variable is obtained when the program is running. Let’s see how it works. A pointer variable is defined near the top of the body of main(): int *ptr; This tells the compiler that we want to store the address of an integer in this pointer variable. The next line assigns the address of the variable LEDs to the pointer variable. We can now use the pointer to access the contents of the variable LEDs. Putting an asterisk (*) in front of the pointer variable’s name dereferences the pointer, in other words it accesses the data that the pointer is pointing to. This example may not look very impressive as you could just as easily use the variable LEDs to access the contents of this variable, but in a practical embedded system, the first few lines of the program: int LEDs = 0x88; int *ptr; ptr = &LEDs; would be replaced by assigning a specific address to the pointer e.g. 0xF430. You would now use *ptr to access the LEDs that you would otherwise have no way of accessing. A further example should help to clarify the use of pointers in C. Project name: pointer Project location: CH7\pointer main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int data = 123; /* an integer variable */ int *ptr; /* a pointer variable (points to an integer) */ UartInit(); ptr = &data; /* get the address of data */ printf("data contains %d\r\n", data); printf("data contains %d\r\n", *ptr); printf("Address of data is %d\r\n", &data); printf("Address of data is %d\r\n", ptr); printf("Address of ptr is %d\r\n", &ptr); } while(1) { } ● 136 Chapter 7 • Memory and Microcontrollers Example Program Output for UNO data contains 123 data contains 123 Address of data is 2296 Address of data is 2296 Address of ptr is 2298 In the pointer program the pointer ptr is made to point to the variable data. Contents of the variable data are then printed out in the manner that you are used to. The second printf() statement in the program also prints out the value of data, but this time by using the pointer. The address of the variable data was saved to the pointer variable ptr and we are dereferencing the pointer when we use *ptr in the printf() statement. *ptr means "Get the data that the address stored in ptr is pointing to". The third printf() statement prints out the address of the variable data. After this the contents of the variable ptr is printed which is also the address of the variable data as we would expect because we assigned it this value at the beginning of the program. In other words ptr is holding the address of the variable data. Finally the address of the pointer variable ptr is printed out. ptr is also a variable so it must be located in memory. If it is located in memory, it must have an address. This pointer variable just contains the address of an integer. See Figure 7-6 for a graphical explanation of the pointer program. Note in Figure 7-6 that the memory locations that are shown are 16 bits wide – in other words 2 bytes wide, as memory is always referred to in bytes. The pointer variable ptr and the integer variable data are shown consecutively in memory. This will not necessarily always be the case as the build process will decide where to locate these variables in memory. Because these variables are placed consecutively in memory in this example and are each 16 bits wide, the addresses are separated by 2 bytes (or 2 addresses), this is why the second address is 2298 (2296 + 2) and not 2297 (2296 + 1). Getting back to the program counter in a microprocessor, in an 8-bit processor the address in this register will be incremented by 1 each time to fetch each 8-bit instruction from memory. A 32 bit processor will fetch 32-bit wide instructions, but addressing is still in bytes so the program counter will be incremented by 4 each time to get the next 32-bit instruction (4 bytes). Figure 7-6 The pointer Program’s Pointer and Integer Variables in Memory Hardware on an embedded system consists of registers from a programmer’s point of view. A register is just like a single memory location on a processor. These registers are accessible in a C program by using pointers. Hardware is at a fixed address and we therefore can’t use a variable name to access the hardware, but must use a pointer that contains the address of the hardware and thereby points to it. This will become clearer in the chapters that follow. ● 137 C Programming with Arduino 7.5 • More on C Data Types So far we have seen four data types: int, long, float and char. Each of these data types have a fixed width (in bytes) and can also be used in conjunction with other keywords to change whether they can be positive and negative (signed) or positive only numbers (unsigned). Their size determines how many bytes of memory they will occupy and their sign determines which range of values they can hold. The size of an integer is not fixed in the C standard, but depends on how the compiler defines it. The integer size is usually the same size as the maximum data size of the processor that it runs on, except for 8-bit devices that usually have a 16-bit integer. A 16-bit microcontroller will usually have a 16-bit integer size; a 32-bit microcontroller will usually have a 32-bit integer size. An integer is by default a signed integer i.e. it can store positive and negative numbers. Memory on a 32-bit microcontroller is 32 bits wide which means that four bytes can be accessed with one read or write operation. An 8-bit microcontroller, like 8-bit AVRs, can only read or write 8 bits at a time (one byte). A signed 16-bit integer can store numbers in the range of -32,768 to +32,767. (-(216 ÷ 2) to (216 ÷ 2) – 1). An unsigned 16-bit integer can hold numbers from 0 to 65,535 (216 – 1). This is because a single bit in the integer is used as the sign bit and is not needed when the number is unsigned. Even a char data type holds an integer value as we have already seen. The integer value of a char can be displayed as a character if desired and character values can be assigned to it, but they are all ultimately integer numbers on the microcontroller. Table 7-3 shows the integer data types in C. The sizes in the table are the actual sizes of these variables in C programs compiled with the GNU C compiler for 8-bit AVR microcontrollers in Atmel Studio. Table 7-3 C Integer Data Types Type Size Minimum Value Maximum Value signed char 1 byte -128 127 unsigned char or char 1 byte 0 255 short 2 bytes -32,768 32,767 unsigned short 2 bytes 0 65,535 int 2 bytes -32,768 32,767 unsigned int 2 bytes 0 65,535 long 4 bytes -2,147,483,648 2,147,483,647 unsigned long 4 bytes 0 4,294,967,295 C compilers can choose whether a char is signed or unsigned by default. Most compilers treat a char as signed by default, but this is not the case with Atmel Studio which treats a char as an unsigned number by default, meaning that it can’t store a signed number, only positive numbers from 0 to 255. The default setting can be changed in Atmel Studio from the top menu by clicking Project → <project name> Properties... and then clicking the Toolchain tab on the left of the page that opens, and finally clicking General under AVR/GNU C Compiler as shown in Figure 7-7 on page 139. ● 138 Chapter 7 • Memory and Microcontrollers Figure 7-7 The Default Sign of a char can be Changed if Needed If a signed char is needed in a program and the compiler default is not changed to unsigned, the char data type must then be written as unsigned char. The fact that an int data type and a short data type are the same length may seem odd, but this is dependent on the compiler and the processor architecture on which the program is run, some compilers may use a 16-bit short and 32-bit int. The keyword signed can be used with each of the integer data types, but the compiler treats each data type as signed unless unsigned is specified, except for char. The varsize program uses the sizeof keyword to obtain the size of each data type in bytes. You can use this keyword if you are not sure what size the data types are on the compiler that you are using. Project name: varsize Project location: CH7\varsize main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); printf("On this microcontroller,\r\n"); printf("A char is %d byte, or %d bits\r\n", sizeof(char), sizeof(char) * 8); printf("A short is %d bytes or %d bits\r\n", sizeof(short), sizeof(short) * 8); printf("A int is %d bytes or %d bits\r\n", sizeof(int), sizeof(int) * 8); printf("A long is %d bytes or %d bits\r\n", sizeof(long), sizeof(long) * 8); } while(1) { } ● 139 C Programming with Arduino Program Output On this microcontroller, A char is 1 byte, or 8 bits A short is 2 bytes or 16 bits A int is 2 bytes or 16 bits A long is 4 bytes or 32 bits This program prints out the size of each integer data type in bytes and in bits. Because a byte consists of eight bits, the number of bits that a data type consists of can be calculated by multiplying the number of bytes returned by sizeof by eight. The vardef program shows how to use each of the integer types from Table 7-3 on page 138 to define a variable and display the number that the variable is initialised to. Project name: vardef Project location: CH7\vardef main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { signed char unsigned char short unsigned short int unsigned int long unsigned long ch = -100; uch = 0xff; shrt = 20; ushrt = 0xffff; in = -10000; uin = 0xffff; lng = 100000001; ulng = 0xffffffff; UartInit(); printf("signed char printf("unsigned char printf("short printf("unsigned short printf("int printf("unsigned int printf("long printf("unsigned long } = = = = = = = = %hd\r\n", ch); %hu\r\n", uch); %hd\r\n", shrt); %hu\r\n", ushrt); %d\r\n", in); %u\r\n", uin); %ld\r\n", lng); %lu\r\n", ulng); while(1) { } Program Output signed char unsigned char short unsigned short int unsigned int long unsigned long ● 140 = = = = = = = = -100 255 20 65535 -10000 65535 100000001 4294967295 Chapter 7 • Memory and Microcontrollers In the vardef program the unsigned variables are initialised with the maximum number of binary 1s that each type can hold by using the equivalent hexadecimal number. When the value is printed out, it can be seen that it corresponds to the maximum value shown in Table 7-3 on page 138. The format specifier %u is used for all unsigned numbers, %ld (long decimal) is used to print out the long data type and the %lu (long unsigned) format specifier is used to print out the unsigned long data type. Format specifiers for the char and short data types are preceded by the letter h. The l (lower case L) and h in these format specifiers are known as modifiers as they modify the decimal, unsigned and char format specifiers for printing the correct data types with the printf() function. Format specifiers for integers are shown in Table 7-4. Each format specifier in the table, except for %c, can be preceded with the letter ‘l’ after the % when the data type is long. These same format specifiers can be preceded with the letter ‘h’ when printing short or char data types. Table 7-4 Integer Format Specifiers Format Specificier Data Type Print Format %d or %i int Decimal number %u unsigned int Unsigned decimal number %x unsigned int Lowercase hexadecimal %X unsigned int Uppercase hexadecimal %c char or int Character Table 7-5 shows what the data types from the above program look like in memory for the Arduino Uno / ATmega328P microcontroller. Each of the cells in the diagram represents one byte, so an int will take up two of these cells to make up its 16 bits. Bytes that make up a number that is larger than one byte will always be successive bytes in memory, but the individual variables will not necessarily be placed consecutively in memory, e.g. if the first byte of a 32-bit number occurs at address 2292, the other 3 bytes will occur at the addresses 2293, 2294 and 2295 to make up all 32 bits or 4 bytes of the number. Table 7-5 Integer Data Types in Memory Address Variable Name Data in Memory Data Type ch -100 signed char 0x08eb (2283) uch 0xff unsigned char 0x08ec (2284) shrt 20 short ushrt 0xffff unsigned short in -10000 int uin 0xffff unsigned int 0x08ed (2285) 0x08ee (2286) 0x08ef (2287) 0x08f0 (2288) 0x08f1 (2289) 0x08f2 (2290) 0x08f3 (2291) ● 141 C Programming with Arduino Address Variable Name Data in Memory Data Type 0x08f4 (2292) lng 100000001 long ulng 0xffffffff unsigned long 0x08f5 (2293) 0x08F6 (2294) 0x08f7 (2295) 0x08f8 (2296) 0x08f9 (2297) 0x08fa (2298) 0x08fb (2299) 7.6 • Microcontroller Memory Sizes Information on a microcontroller such as the size of memory and number of different memory types can be found in the microcontroller’s datasheet. Datasheets for microcontrollers tend to be rather large, but contain information about the internal microprocessor and all of the microcontroller’s peripheral devices. Datasheets for Atmel microcontrollers can be found by visiting the main Atmel website at www.atmel.com and then finding the microcontroller family that the device belongs to and then the individual page for the specific microcontroller part number which contains a link to the datasheet. For the microcontrollers on the Uno and MEGA Arduino boards the microcontroller family is chosen from the Atmel home page by selecting Products → AVR 8- and 32-bit MCUs → megaAVR MCUs which then lists all the megaAVR microcontrollers on the web page. The individual page for each microcontroller can then be found by scrolling down the page to find the correct part number or by using the web browser’s search (Ctrl + F). A new Arduino Uno board contains an ATmega328P and the Arduino MEGA 2560 contains an ATmega2560 part. Datasheets are usually shared between microcontrollers with similar features that often only differ in their memory sizes and physical packages, so several different part numbers will be displayed at the top of the datasheet when it is opened. Package sizes of microcontrollers usually influence the number of pins that the microcontroller has, which in turn influences the number of peripheral devices that a microcontroller has. A microcontroller that is housed in a package with fewer pins will usually have some peripheral devices missing when compared to the same microcontroller in a package that has more pins. These microcontrollers will typically have different part numbers, but share the same datasheet and be compared in a table in the datasheet. Table 7-6 compares the memory sizes of the microcontroller found on the Arduino Uno and MEGA 2560, although theses devices do not share a datasheet, but each have an individual datasheet. Table 7-6 Comparison of Memory Types on Uno and MEGA 2560 Arduino µC Part Number Flash (ROM) SRAM (RAM) EEPROM Uno ATmega328P 32k Bytes 2k Bytes 1k Bytes MEGA 2560 ATmega2560 256k Bytes 8k Bytes 4k Bytes From the table it can be seen that the MEGA has a lot more ROM memory in the form of Flash than the Uno. RAM in the form of SRAM (Static Random Access Memory) which is used for storing variables is also more plentiful on the MEGA. EEPROM is a type of nonvolatile memory that can be updated by a C program at runtime and is used to store data ● 142 Chapter 7 • Memory and Microcontrollers that must be preserved between power cycles – in other words between switching power to the board off and on. 7.7 • Summary • Two basic memory types are used on embedded systems: ROM and RAM. • ROM is non-volatile memory and keeps its contents even after the power to it has been switched off. • RAM is volatile memory and looses its contents when the power to it is switched off. • Memory is arranged as consecutive bytes in a computer or embedded system. • Each byte in memory has its own address and can be accessed by putting this address on its address bus. • Data is written to or read from memory using the data bus. • The control bus contains signals to move data into or out of the memory chip. • Pointers are used in C to access memory using the address of the memory. • Each variable in C stores data and has an address in memory. • C integer data types can be used to create and access variables of different bit widths. • The width of each integer variable in bits determines the size range of the number that it can hold. ● 143 C Programming with Arduino ● 144 Chapter 8 • Accessing AVR Ports in C Chapter 8 • Accessing AVR Ports in C What you will do in this chapter: • Write to AVR hardware registers using pointers • Set an AVR microcontroller port as an output port • Switch LEDs on and off using an AVR register • Learn about the increment and decrement operator • Start a new Atmel Studio project from scratch without using a template 8.1 • AVR Pins and Ports AVR microcontrollers have a number of pins that are known as general purpose input/ output (I/O) pins. These pins can be individually configured as either output pins to drive a load – e.g. to drive or switch an LED on and off; or as input pins used to read a logic level placed on the pin – e.g. to test whether an attached switch is open or closed. Additionally, these pins have an alternative function in that they can be configured to connect to an internal hardware device, such as a UART used for serial communications or ADC to read analogue values. In this chapter, we will look at configuring pins as outputs to drive LEDs. AVR 8-bit microcontrollers have 8-bit ports which are groups of I/O pins that are named with letters – e.g. port A, port B, port C, etc. These ports are each controlled by their own set of registers that allow the pins to be configured as inputs or outputs. Registers are just like memory locations and can therefore be written to using pointers in C. The actual names and functions of the registers can be found in the microcontroller’s datasheet. Figure 8-1 ATmega328P Pin Numbering and Ports Figure 8-1 shows a top view of the physical package of an ATmega328P microcontroller found on the Arduino Uno showing the pin numbering (pin 1 to pin 28) and individual numbering of pins that make up the ports, namely PB0 to PB7, PC0 to PC6 and PD0 to PD7. At the right of the figure, the ports are shown in a block diagram with port B and port D being 8 bits wide and port C being 7 bits wide. Figure 8-2 on page 146 shows a block diagram of the pins and ports of an ATmega2560 microcontroller found on the Arduino MEGA 2560. ● 145 C Programming with Arduino Figure 8-2 ATmega2560 Ports The above figures show unused ports of the microcontrollers, however, when the microcontrollers are placed on a board, such as the Arduino, some of the port pins are dedicated to certain functions such as the serial port. The figures that follow show which port pins are used for dedicated functions on the Arduino boards and which pins are free for general purpose use. Figure 8-3 ATmega328P on the Arduino Uno with Used Pins (L) and Free Pins (R) Figure 8-3 shows port pins of the ATmega328P that are used by the Arduino Uno board on the left of the figure. Pins that are available for general purpose use are shown on the right. PB3, PB4 and PB5 are used by the ICSP header along with the reset pin PC6. PC6, or reset, is connected to both the ICSP header and the reset button on the board. The ICSP header is the header that a programming device such as the AVRISP or AVR Dragon is connected to for programming. PB5 of the ICSP header is also connected to the onboard LED labelled L on the board which maps to Arduino pin 13. ● 146 Chapter 8 • Accessing AVR Ports in C PB6 and PB7 are connected to an external 16MHz ceramic resonator used to generate the main clock source for the AVR. PD0 and PD1 are dedicated to the on-chip UART which is connected to the USB port of the Arduino through a second AVR chip on the board. These pins are used for sending and receiving data between the Arduino and the PC and have been used to connect to the terminal program in the example programs in this book. Figure 8-4 ATmega2560 on the Arduino MEGA 2560 with Unused 8-bit Ports on the Right Figure 8-4 shows the ATmega2560 found on the Arduino MEGA 2560 with full 8-bit ports on the right and used ports on the left. In the case of the MEGA board, there are more I/O pins on the ATmega2560 chip than there are connection points on the board, which means that some of the pins of the chip are not connected to anything. The ATmega2560 chip has ten 8-bit ports and one 6-bit port which are reduced to five 8-bit ports and six ports with less than 8 pins when the chip is placed on the Arduino MEGA. Three pins of port B are used by the ICSP header along with a dedicated reset pin which can’t be configured as an I/O pin and is not shown in the figure. A fourth pin from port B (pin PB7) is used for the on-board L LED of the Arduino and maps to pin 13 of the Arduino’s connectors. Port D has three pins that are not connected, reducing it to a 5-bit port. Port E has three pins that are not connected and two pins that are dedicated to the UART for USB / serial port communications with the PC, leaving three spare pins. Port G was originally only 6 bits wide, but has two unconnected pins reducing it to a 4-bit port. Port H has two unconnected pins and port J has six unconnected pins. ● 147 C Programming with Arduino When connecting hardware such as LEDs, switches or other devices to the connectors on the Arduino, it is necessary to know which port pins map to which Arduino connector pins. The reason for this is that a C program references the ports by port name and not Arduino pin number. AVR port to Arduino pin mappings are shown as they are used in the C programs that follow. Full pin mappings of AVR port pins to Arduino header / connector pin numbers can be found in Appendix B on page 331 for both the Arduino Uno and Mega 2560. When the Arduino IDE is used to program Arduino boards, software is used to hide the AVR microcontroller port pin numbering and to give Arduino pins the same numbers across different Arduino boards even if they occur on different AVR port pins. An example of using software to give Arduino pins their Arduino pin numbers is pin 13 on the Uno and MEGA that is connected to the on-board L LED which occurs on AVR port pin PB5 on the Uno AVR, but AVR port pin PB7 on the MEGA AVR. 8.2 • Switching the On-board LED On and Off To switch an LED on and off that is attached to a pin of an AVR port, we need to write values to two AVR registers. Firstly we must write to the DDR (Data Direction Register) to set the pin as an output pin – by default when the microcontroller is powered up, all pins are set as inputs. Secondly, to switch the LED on and off we need to write values to the PORT data register. Each port has its own set of registers for controlling I/O port functions. To access the DDR of port A, the DDRA (Data Direction Register A) is used, to access the DDR of port B, DDRB is used, etc. The same occurs for the PORT register, e.g. PORTA, PORTB. These register names have already been defined and can be used just like variables. When the file avr/io.h is included in a C file, the AVR registers are available in that file. Figure 8-5 on page 149 shows the registers needed to control the on-board LED of the Arduino Uno. Arduino Uno pin number 13, which the L LED is attached to, maps to AVR pin PB5. On the MEGA the L LED is also attached to pin 13, but Arduino pin 13 maps to PB7 of the AVR and is shown in Figure 8-6 on page 150. For the Uno, pin PB5 maps to bit number 5 in each of the registers. Writing a logic 1 to bit 5 of DDRB sets up PB5 as an output pin. On the MEGA, the LED is attached to pin PB7, making it necessary to set bit number 7 in DDRB to configure the pin as an output pin. The hexadecimal number to write to the register is therefore 0x20 for the Uno and 0x80 for the MEGA. To switch the LED on, the same corresponding bit must be set to a logic 1, but this time in the PORTB register. Writing 0x20 to PORTB on the Uno switches the LED on. As Figure 8-5 on page 149 shows, the LED can be thought of as being connected to a single logic cell in the register. Writing a logic 1 to the register is the same as putting 5V onto the LED, switching it on. Writing 0x80 to the PORTB register on the MEGA will switch its LED on. The binary numbers in the registers which are 00100000B for the Uno and 10000000B for the MEGA can be converted to their hexadecimal equivalents using the Windows calculator. To switch the LED off, a 0 logic level is written to the corresponding bit in the register by writing 0x00 to the register. AVR Ports in the Datasheet A full description of the I/O ports and their functionality can be found in the I/O-Ports chapter of the AVR datasheet. I/O port registers and the name of each bit in these registers can be found at the end of the I/O-Ports chapter of the datasheet in the Register Description section. ● 148 Chapter 8 • Accessing AVR Ports in C The example C programs that follow will make the above explanation clearer. Because the Uno and MEGA have their on-board LEDs connected to different port pins, two different programs have been included – the first is for the Uno and the second for the MEGA. Figure 8-7 on page 151 shows the position of the L LED on the Arduino Uno and MEGA boards. Figure 8-5 ATmega328P Registers for Controlling a LED on an Arduino Uno Project name: port_write_uno Project location: CH8\port_write_uno main.c #include <avr/io.h> void Delay(void); int main(void) { DDRB = 0x20; // set bit 5 of DDR register which makes PB5 an output } while(1) { PORTB = 0x20; // switch LED on Delay(); PORTB = 0x00; // switch LED off Delay(); } void Delay(void) { volatile unsigned long count = 100000; } while (count--); Program Output for Both Programs The on-board L LED flashes on and off or 'blinks'. ● 149 C Programming with Arduino Figure 8-6 ATmega2560 Registers for Controlling a LED on an Arduino MEGA 2560 Project name: port_write_mega Project location: CH8\port_write_mega main.c #include <avr/io.h> void Delay(void); int main(void) { DDRB = 0x80; // set bit 7 of DDR register which makes PB7 an output } while(1) { PORTB = 0x80; // switch LED on Delay(); PORTB = 0x00; // switch LED off Delay(); } void Delay(void) { volatile unsigned long count = 100000; } while (count--); The two programs for controlling the LEDs should be self explanatory, except for the decrement operator used in the delay function that will be explained shortly. The only difference between the two programs is the number written to the PORTB register and DDRB. As can be seen, the two registers discussed earlier are accessed just as if they were variable names, although what they boil down to is accessing registers using pointers. The io.h file included at the beginning of the program makes these register names available and convenient to access. ● 150 Chapter 8 • Accessing AVR Ports in C Figure 8-7 Position of the L LED on the Arduino Uno (left) and MEGA 2560 (right) Writing a logic 1 to the bit position that corresponds to the desired port pin number in DDRB sets that pin as an output pin. In the while(1) loop, the PORTB register is written to in order to switch the LED on by setting the corresponding bit in the register to logic 1 and then written to again to switch the LED off by clearing the corresponding bit to logic 0. Calls to the Delay() function between switching the LED on and off ensure that the LED remains in the on and then off state long enough to be visible to the eye. In the Delay() function, the decrement operator (--) is used to decrease the value that the variable count is holding by one and has been included in the loop expression of the while loop. Written on its own, the decrement operator looks as follows when operating on a variable: count--; The above statement is a shorthand way of writing: count = count - 1; When inserted into the loop as in the program: while (count--); This statement will evaluate to true as long as count contains a value greater than zero. Each time through the loop, count is evaluated and then decremented. The value that count holds will eventually become zero, and when the loop expression is evaluated again will be false, breaking program flow out of the loop. The above statement could also be written as follows: while (count) { count--; } Or when written without the decrement operator: while (count) { count = count - 1; } The time taken to decrement the number from a big value to zero is what generates the time delay that leaves the LED on for a period of time and then off for a period of time, in the same way as the delay function from the flashing LED simulation program from Chapter 5 (on page 97) did. ● 151 C Programming with Arduino The volatile keyword has been used again in front of the variable definition in the Delay() function as follows: volatile unsigned long count = 100000; As a reminder, volatile is used so that the compiler optimisation process does not optimise this variable away. Compilers have optimisation settings that can be used to reduce the code size of a compiled program and/or increase program execution speed. The compiler may see that the loop that count is used in actually does nothing and may try to optimise the loop by removing it. Not using the volatile keyword when required can be the source of a seemingly mysterious error in embedded programming where a program that has no errors does not work as expected. If volatile is left out of the previous programs, the LED will appear to remain in the on state, although it is switching on and off too rapidly for the eye to perceive. An alternative to using the volatile keyword is to turn compiler optimisation off as is shown in the chapter on debugging, although it is better to use the volatile keyword no matter what the compiler settings are because anyone can change the optimisation settings and recompile the program. It is important to note that the above programs can be written more effectively using bitwise operators to switch the LED on and off. Bitwise operators are still to be covered. One of the problems with writing to all the bits in a register when only one bit needs to be written to is that the state of all the other bits is also changed. It would be better to leave the other bits unchanged. When writing 0x20 to a register, bit 5 is set to logic 1, but all other bits are cleared to logic 0 – we only wanted to change the value of one bit but ended up changing the value of all bits. The same happens when bit 5 is cleared, all bits are cleared as well. This is not a problem if we are writing to a full 8- bit port and want to write to all 8 bits, and did not matter in the previous programs because the other pins on the port were unused. In the case of microcontrollers it is often desirable to use only some of the pins of a port as outputs and use other pins from the same port as inputs or alternative functions – bitwise operators can then be used to change the value of a single bit of a port register without changing the values of other bits in the port register. 8.3 • Controlling LEDs interfaced to a Port In the next example program, LEDs are connected to port C of the Arduino. On the MEGA, port C is a full 8-bit port, but on the Uno only 6 pins are available from port C. The upper two bits of port C on the Arduino Uno are ignored and only six LEDs are interfaced to the port. On the MEGA, all 8 pins of port C are interfaced to LEDs. 8.3.1 • Connecting the Hardware LED connections to the Arduino Uno and Arduino MEGA are shown in the figures below. A current limiting 470Ω resistor is placed in series with each LED connected to the port. Figure 8-8 on page 153 shows six LEDs connected to port C of an Arduino Uno using an electronic breadboard, jumper wires and resistors and Figure 8-9 on page 153 shows eight LEDs connected to port C on an Arduino MEGA in the same way. ● 152 Chapter 8 • Accessing AVR Ports in C Figure 8-8 Connecting 6 LEDS to Port C of the Arduino Uno AVR Figure 8-9 Connecting 8 LEDs to Port C of the Arduino MEGA AVR Circuit diagrams for the Arduino Uno and Arduino MEGA are shown in Figure 8-10 on page 154 and Figure 8-11 on page 154 respectively. The circuit diagrams also show how the AVR port pins map to the Arduino connector pins. All of the available port C pins on the Arduino Uno map to analogue pins A0 to A5. These pins are configurable as I/O pins and have an alternative function of being analogue pins as well, so it is perfectly acceptable to used them as output pins. Connect the LEDs as shown to your Arduino in order to run the C program that follows. It does not matter whether the resistor or LED is connected to the Arduino first, as long as they are in series. Don’t forget to connect the Arduino board common GND pin to each of the resistors or LEDS, depending on in which order you wired these components to the Arduino. Using Port C Port C has been chosen to use with most of the example programs that connect to LEDs and switches because this port is available on both the Arduino Uno and Arduino MEGA, although not all of the port pins of port C are available on the Uno. Using the same port on the Uno and MEGA means that the same C program can be used ● 153 C Programming with Arduino on both boards, otherwise two versions of every program would need to be written – one for the port used on the Uno and one for the port used on the MEGA. Figure 8-10 Arduino Uno Circuit and Pin Mapping Figure 8-11 Arduino MEGA 2560 Circuit and Pin Mapping 8.3.2 • Starting a New Atmel Studio Project from Scratch Up until now, all the Atmel Studio projects have been created using the templates that were set up to enable the printf() and scanf() functions to work on the Arduino. These functions are used for teaching purposes and won’t be used on most practical embedded system application. Most new Atmel Studio projects will be created from scratch as this section shows. There is not much difference between creating a project from a template and creating a project from scratch as shown in the steps below. ● 154 Chapter 8 • Accessing AVR Ports in C 1. Start Atmel Studio and create a new project using the toolbar icon or File → New → Project... from the top menu bar. 2. In the New Project dialog box, click GCC Executable Project as shown in Figure 8-12 and give the new project a name as usual. Click OK when done. 3. Choose the correct microcontroller for the board being used. Typing the part number in the search box of the Device Selection dialog box is easiest, as shown in Figure 8-13 on page 156. Type atmega328p for the Uno or atmega2560 for the MEGA. 4. After searching for the correct part in the Device Selection dialog box, click the part in the left pane and then click the OK button (figure 8.13). 5. The project will be created with some skeleton code in the main C file which is automatically opened for editing. 6. Use the No Tool button on the second top toolbar at the right to select the tool (e.g. AVR Dragon or AVRISP) to program the board with (Figure 8-14 on page 156 and Figure 8-15 on page 157). The tool must be plugged into the USB port of the PC before it can be selected. 7. Click the Save All icon to save the new project, or use File → Save All from the top menu. The project is now ready to add the code shown in the listing below. Figure 8-12 Creating a New Atmel Studio Project from Scratch ● 155 C Programming with Arduino Figure 8-13 Selecting the AVR Device for the Arduino Board in Use Figure 8-14 Selecting the Programming Tool from the Toolbar in Atmel Studio Clicking "No Tool" on the toolbar next to the hammer icon will open the page shown in Figure 8-15 on page 157 which allows a programming tool to be selected from the drop-down list. Select either the AVR ISP tool or the AVR Dragon and ISP for the interface setting. After the selection has been made, the selected tool name will appear in place of the "No Tool" text on the toolbar shown in Figure 8-14. ● 156 Chapter 8 • Accessing AVR Ports in C Figure 8-15 Selecting the Programming Tool 8.3.3 • LED Port Control Program After creating a new project as described above, add the following code to the main C file, compile and load to the Arduino. This program displays a binary count value on the LEDs that were connected to port C of the AVR in the previous section. Project name: LED_port Project location: CH8\LED_port main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { // value to write to port unsigned char port_val = 0; // all port C pins set as outputs DDRC = 0xFF; } while(1) { PORTC = port_val; port_val++; _delay_ms(1000); } // write value to port // increment value // 1 second delay Program Output Binary count is displayed on the LEDs. In the LED_port program, avr/io.h is included in the C source file so that we have access to the AVR I/O registers. A second header file, util/delay.h, is also included in order to provide a delay function called _delay_ms() which creates a delay in milliseconds, with ● 157 C Programming with Arduino the number of milliseconds to delay passed to this function. This delay library function saves us from having to write our own delay function as we did in the previous program. In order for the delay function to accurately create the delay time, it needs to know at what speed the main system clock is running. In the case of both the Uno and MEGA, the main clock runs at 16MHz or 16,000,000Hz. Defining a value for F_CPU at the top of the C source file provides the clock value to the delay function. Because the clock runs at 16MHz, F_CPU is defined with a value of 16 million or 16,000,000 as follows. #define F_CPU 16000000UL The above line of code defines F_CPU with a value of 16000000 unsigned long (UL). The object of this program is to display a binary count value on the LEDs that are interfaced to an AVR port. The width of the port is 8 bits, so an unsigned char variable called port_val is defined to hold the count. An unsigned char was chosen because the number will never be negative and a char is 8 bits wide. After defining the variable, all bits in DDRC are set to logic level 1 by writing 0xFF to this register which is 11111111B. This value written to DDRC configures all of the pins of port C as output pins. Inside the continuous while(1) loop, the value in the variable port_val, which is initially zero, is placed on port C using the following line of code. PORTC = port_val; The first time through the loop all of the LEDs are switched off because the port_val variable contains zero. The increment operator is used to increase the value of port_val by one. The increment operator is similar to the decrement operator shown in the previous program, but instead of decreasing the value of a variable by one, it increases the value by one. port_val++; The increment operator ++ used on the variable shown above is the equivalent of the following line of code. port_val = port_val + 1; After the variable has been incremented by one, the delay library function _delay_ms() is called and passed a value of 1000. This 1000 means that the delay function will delay for 1000ms (one thousand milliseconds) which creates a 1 second delay (one second contains one thousand milliseconds). When program execution starts at the top of the loop again, port_val contains the value 1 which is written to the LEDs. This switches the least significant bit of port C on and the LED attached to port pin PC0. Each time through the loop, the variable is incremented by one and the new value is displayed on the LEDs. In this way the binary value in the variable is displayed on the LEDs where an on LED represents binary 1 and an off LED represents binary 0. When the value in variable port_val is incremented enough times and becomes 255 or 0xFF, all of the bits in this variable are logic 1s. The next time that the variable is incremented, it overflows and all the bits become zero – 0 or 0x00. In this way, the binary count automatically restarts from zero each time the maximum count value that an 8-bit number can hold is reached. ● 158 Chapter 8 • Accessing AVR Ports in C On the Arduino Uno with only 6 LEDs on the port, the binary count appears to work properly, but only counts up to 0x3F, 111111b or 63 (26 – 1). What is actually happening is that the top two, or two most significant bits of the number are not displayed on LEDs. The full count from 0 to 255 is still happening in the port_val variable, but because the binary count of the lower 6 bits always follows the same pattern, the missing two bits are not noticed. When looking at the binary value on the LEDs, make sure that the board and LEDs are not upside down in which case the LSB LED would be on the left and the binary count sequence would appear to be incorrect. 8.4 • Increment and Decrement Operators Both the increment and decrement operators have been shown and explained in the previous two example programs. What has not been shown is that the increment operator ++ and the decrement operator -- can be placed either before the variable being operated on or after the variable being operated on as the following code shows for the increment operator. ++port_val; port_val++; When used in the above manner it makes no difference whether the increment operator is placed before or after the variable. When it does matter is when the increment or decrement operator is evaluated, for example in an if statement or while loop. 8.4.1 • Pre-increment and Pre-decrement When an increment operator is placed before a variable (known as pre-increment) and appears in a statement that is evaluated, the value that the variable holds will first be incremented by one and then evaluated. The same applies to the decrement operator placed before a variable (pre-decrement) the variable will first be decremented by one and then evaluated. 8.4.2 • Post-increment and Post-decrement When an increment operator is placed after a variable (know as post-increment) and appears in a statement that is evaluated, the value that the variable holds will first be evaluated and then incremented by one. When a decrement operator is placed after a variable (post-decrement) the variable is first evaluated and then decremented by one. The next program called increment shows how the post-increment and pre-increment operators work. Create the project from the template file and use the serial port terminal program to view the output from the program. ● 159 C Programming with Arduino Project name: increment Project location: CH8\increment main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int num = 0; UartInit(); printf("num is %d\r\n", num); // num is 0 printf("Post-increment: %d\r\n", num++); // num is 0 printf("After increment: %d\r\n", num); // num is 1 printf("Pre-increment: %d\r\n", ++num); // num is 2 printf("After increment: %d\r\n", num); // num is 2 } while(1) { } Program Output num is 0 Post-increment: 0 After increment: 1 Pre-increment: 2 After increment: 2 In this program, the value of variable num is first printed out and has a value of 0 that it was initialised with. In the second printf() statement, num is post-incremented which means that num is first used in the statement and then incremented so that it will only have the new incremented value in the next statement. For this reason this statement, shown below, prints out 0 when the variable is incremented in the statement. printf("Post-increment: %d\r\n", num++); The new incremented value of num only takes effect in the next statement, shown below, which simply prints out the value of num without incrementing it. The value printed is 1. printf("After increment: %d\r\n", num); When the pre-increment statement is used, as shown below, num is incremented before it is used in the statement in which it occurs. The statement therefore increments the value of num from 1 to 2 and prints out 2. printf("Pre-increment: %d\r\n", ++num); The final printf() statement prints the value of num again which is still 2 because the increment operation took place in the previous statement. ● 160 Chapter 8 • Accessing AVR Ports in C 8.5 • Summary • To switch an LED on and off that is connected to a microcontroller’s port pin requires knowledge of the microcontroller port’s hardware. • Hardware information, such as port configuration, port registers and bit numbering, is found in a microcontroller’s datasheet. • Hardware differs between different microcontroller architectures and manufacturers. Setting up microcontroller pins as outputs on an AVR microcontroller differs from setting up microcontroller pins as outputs on an ARM microcontroller. • When connecting LEDs to microcontroller pins, always use a current limiting series resistor. • LEDs connected to a port of an AVR microcontroller can be switched on and off by first using a register to configure the pins as outputs and then using a different register to write to the LEDs to switch them on or off. • The increment and decrement operators are a shorthand way of increasing or decreasing the value that a variable contains by 1. • Placing the increment or decrement operator before a variable name increases or decreases the value of the variable by one before it is evaluated – this is know as pre-incrementing or pre-decrementing the variable. • Placing the increment or decrement operator after a variable name increases or decreases the value of the variable by one after it is evaluated – known as post-incrementing or post-decrementing the variable. ● 161 C Programming with Arduino ● 162 Chapter 9 • I/O and Memory Maps Chapter 9 • I/O and Memory Maps What you will do in this chapter: • Read the state of a switch connected to a microcontroller input pin • Work with input and output pins • See how hardware and memory are mapped in AVR microcontrollers 9.1 • Input Pins In the previous chapter we worked with output only by sending a value out of the microcontroller to switch LEDs on and off that were connected to pins on the AVR’s port. The port pins were set up as output pins and used to drive LEDs. Port pins can also be set up as inputs and the logic state of these pins can be read by the microcontroller. A switch connected to a microcontroller pin set up as an input can be read to see if the switch is open or closed. 9.1.1 • Connecting a Switch to a Port Pin In order to read the state of a switch in a C program, the switch must first be connected to the desired port pin of the AVR microcontroller on the Arduino. The switch used for this example can be almost any kind of switch that only has to close a circuit and then open a circuit. A momentary push-button switch, toggle switch, key switch or even a wire can be used to close and open a circuit. Figure 9-1 Connecting a Switch to Pin PC0 on the Uno (left) and MEGA (right) Figure 9-1 shows a switch interfaced to port pin PC0 on an Arduino Uno and MEGA, with the only difference between the two circuits being the Arduino pin number that the switch is connected to. The common GND at one end of the resistor is connected to the GND connection on the Arduino. ● 163 C Programming with Arduino When the switch is open, the 10k pull-down resistor connects the PC0 pin to GND or 0V of the power supply putting a logic level 0 onto the pin. When the switch is closed, the pin is shorted to 5V which places a logic level 1 onto the pin. The 10k resistor prevents the 5V from being shorted to GND. 9.1.2 • Reading the State of a Switch in C Create a new Atmel Studio project called switch_val using the template file and connect a switch to the Arduino using a breadboard as shown in the circuit from Figure 9-1 on page 163. Add the following code to the project. Project name: switch_val Project location: CH9\switch_val main.c #define #include #include #include #include F_CPU 16000000UL <avr/io.h> <util/delay.h> <stdio.h> "stdio_setup.h" int main(void) { DDRC = 0x00; UartInit(); } // set port C pins as outputs while(1) { printf("PORT C: 0x%02x\r\n", PINC); // print the port state _delay_ms(200); } Example Program Output PORT PORT PORT PORT PORT PORT ... C: C: C: C: C: C: 0x00 0x01 0x01 0x01 0x00 0x00 When the switch is open, the value of port C displayed in the terminal window is 0x00. When the switch is closed, the value displayed is 0x01. At the beginning of the program, DDRC is assigned a value of 0x00 to configure all of the port pins of port C as inputs. This is actually not necessary because the port pins are configured as inputs by default, but it does show the intention of the programmer that the entire port will be used as an input port, although we are only using one pin of the input port in the example program. In the while(1) loop, the value of the PINC register is printed to the terminal program running on the PC. The value in this register is used directly in the printf() statement without first reading it into an intermediate variable. The printf() statement is followed by a 200ms delay that prevents the terminal program from being swamped with too many consecutive messages. The PINC register is another register that is part of a set of registers used for configuring, reading and writing to AVR I/O ports. PINC is the register used for reading the value of ● 164 Chapter 9 • I/O and Memory Maps port pins on port C. The logic state of pins on port A can be read using the PINA register, pins on port B are read using the PINB register, etc. PINx (where x is the port letter A, B, C, etc.) registers operate in the same way as PORTx registers in that one bit in the register corresponds to one pin on the port. Register PORTC was used to write to LEDs connected to port C when the port pins were set up as outputs. PINC is now used to read the state of switches when the port pins are set up as inputs. Figure 9-2 shows how the state of the switch changes the value in the PINC register. Figure 9-2 Reading the State of a Switch using the PINC Register Using the same C program and hardware, disconnect the switch from pin PC0 and connect it in turn to PC1, then PC2, etc. When the switch is pressed, it will show the hexadecimal number 0x01 when connected to PC0, 0x02 when connected to PC1, 0x04 when connected to PC2 and 0x08 when connected to PC3. What is happening is that the weight of each bit position in the PINC register changes the number displayed. The value in the PINC register is just a binary number that is being changed by the switch connected to it. The number changes as a single bit is set to logic high or 1, while all other bits remain logic low or 0 as follows. Bit 0, PC0 20 = 1 or 0x01 (bit 0 high all other bits low) Bit 1, PC1 21 = 2 or 0x02 (bit 1 high all other bits low) Bit 2, PC2 22 = 4 or 0x04 (bit 2 high all other bits low) Bit 3, PC3 23 = 8 or 0x08 (bit 3 high all other bits low) This 1248 weighting continues in the upper nibble of the byte as the switch is connected to PC4 to PC7. Moving the switch from PC4 through to PC7 will change the number to 0x10, 0x20, 0x40 and 0x80. ● 165 C Programming with Arduino 9.2 • Input and Output Pins on the Same Port So far we have set up an entire port as either an output port or an input port. We will now set up a single pin on a port as an output and a single pin as an input to control a LED and read a switch in the same program. 9.2.1 • Connecting a Switch and LED The circuit in Figure 9-3 below is the same as the circuit used in the previous program, but adds a single LED and series resistor to one of the port pins. Build the circuit on breadboard which will be controlled by the C program that follows. Figure 9-3 Connecting a Switch and LED to Arduino Uno (left) and MEGA (right) 9.2.2 • Programming a Switch and LED Start a new Atmel Studio project from scratch as explained in Chapter 8 on page 154 and add the following code to it. Project name: switch_LED Project location: CH9\switch_LED main.c #include <avr/io.h> int main(void) { DDRC = 0x20; while(1) { if (PINC & 0x01) { PORTC = 0x20; } else { PORTC = 0x00; ● 166 // PC5 output, other pins inputs // is the switch on PC0 closed? // switch the LED on // switch the LED off Chapter 9 • I/O and Memory Maps } } } Program Output Closing the switch switches the LED on. Opening the switch switches the LED off. In this program, DDRC is used to set pin PC5 up as an output by setting bit number 5 in this register, all other bits in the register are cleared to 0 making the corresponding pins inputs. In the while(1) loop, the PINC register is checked in the if statement to see if bit 0 contains a logic 1 which would mean that the switch is closed. If the switch is closed, the LED connected to PC5 is switched on by setting bit 5 in the PORTC register. If the switch is open then the code in the else block is run which switches the LED off. A bitwise operator is used to check whether the switch is closed in the if statement. 9.2.3 • The AND Bitwise Operator The ampersand (&) represents the bitwise AND operator in C. This has nothing to do with the address operator used to get the address of a variable as you have previously seen. In this context the & operator is used to perform a bitwise logical operation. The AND logical operation can be thought of in terms of an electrical circuit as shown in Figure 9-4. The truth table for the circuit shows that only when switch 1 AND switch 2 are both on or closed, the light bulb will switch on. All other combinations of switch 1 and switch 2 states result in the light bulb staying off. This is exactly what the C bitwise AND operator does. In the switch_LED program, the PINC register containing the state of the switch is ANDed with a number in order to isolate the state of the switch. In the program, we want to test whether the switch attached to pin PC0 is open or closed so we use a ‘mask’ value of 00000001B or 0x01 hexadecimal and bitwise AND it with the value in the PINC register as shown in the following line of code. if (PINC & 0x01) { Figure 9-4 AND Circuit and Truth Table ● 167 C Programming with Arduino The bitwise AND operation performs the AND truth table operation on all 8 bits of the PINC register with the 8-bit mask number in parallel at the same time. Each bit in the PINC register is ANDed with the corresponding bit in the mask number. In other words, bit 0 of PINC is ANDed with bit 0 of the mask number, bit 1 of PINC is ANDed with bit 1 of the mask number, etc. as shown below. The table shows that bit 0 of PINC is ANDed with bit 0 of the mask number below it, in other words, a bit with a value of 0 is ANDed with a bit with a value of 1. From the truth table, 0 AND 1 results in 0. The same happens with all of the other bits in the byte as each bit from PINC is ANDed with the bit from the mask number below it and the result displayed below the two bits. All other bit results after the bitwise AND operation are 0, which is derived from the truth table where 0 AND 0 is 0. In this case the switch was open, so PINC had a value of 00000000B. In the C program, this bitwise AND operation in the if statement evaluates to 0 or false, causing the else statement to run and switch the LED off. The next table shows what happens when the switch is closed. In this case, the switch is closed which results in PINC having a value of 00000001B or 0x01 hexadecimal. Now when bit 0 of PINC and bit 0 of the mask number are ANDed together we have 1 AND 1 which results in 1 as can be seen in the truth table. All other bits ANDed together result in 0, so the final result of ANDing all 8 bits is 00000001B or 0x01 hexadecimal. The if statement in the program now evaluates to true and the LED is switched on. The advantage of using the bitwise AND operation is that it does not matter what the value of the other bits in the byte are. The bitwise AND operation isolates a single bit which will either evaluate to true if the bit is a 1 or false if the bit is a 0. If any random data is read in the upper bits of the PINC register, it does not matter because the mask number together with the AND bitwise operation will always give a zero result. Only the bit that we are interested in will be evaluated. The table below shows that other bits set in the PINC register, which could be other switches connected to the corresponding pins, still result in 0 when ANDed with the mask number, effectively masking them out and isolating a single bit for evaluation. If the switch were to be attached to PC2, we would simply change our mask number so that bit 2 would be isolated when using the bitwise AND operation as shown in the next table. ● 168 Chapter 9 • I/O and Memory Maps To isolate bit 2, the mask number is changed to 00000100B or 0x04 hexadecimal. When the switch connected to PC2 is open, the bitwise AND operation results in 0 or false. When the switch is closed, the bitwise operation results in 0x04 which is a non-zero number so evaluates to true. The C code to test the state of a switch on PC2, or the state of bit 2 of PINC, is shown in the following line of code. if (PINC & 0x04) { This has been an introduction to one of the bitwise operators in C, we will look at other bitwise operators in due course. 9.3 • AVR Memory Map A memory map is a graphical depiction of the memory of an embedded system or microcontroller and helps with visualising the memory of a system. A memory map shows the types and sizes of memory as well as the addresses of memory. Figure 9-5 shows the memory map of an ATmega328P as found on an Arduino Uno board and Figure 9-6 on page 170 shows the memory map of an ATmega2560 as found on the Arduino MEGA 2560. The main difference between the ATmega328P and the ATmega2560 memory maps is that the ATmega2560 has more memory than the ATmega328P. The ATmega2560 also has more extended I/O registers. Figure 9-5 ATmega328P Memory Map ● 169 C Programming with Arduino Figure 9-6 ATmega2560 Memory Map 9.3.1 • Memory Architecture AVRs have a Harvard architecture which means that they have separate buses for program memory and data memory. For this reason the Flash memory and SRAM memory of an AVR can both start at address zero or 0x0000. EEPROM memory is organised as a separate data space and is accessed through a set of registers which also allows its addressing to start at zero. 9.3.2 • Flash Program Memory Flash memory, which stores the program instructions of a compiled C program, has been arranged as 16-bit wide words because most AVR instructions have a 16-bit wide word format. This means that a 16-bit word is found at each address location in Flash memory instead of an 8-bit byte. The 32k bytes of Flash on an ATmega328P is arranged as 16k by 16-bit words and the 256k bytes of Flash on the ATmega2560 is arranged as 128k by 16bit words. 9.3.3 • Data Memory Various AVR registers have been mapped into the start of the AVR data memory address space. For this reason SRAM memory starts at 0x100 on the ATmega328P and at 0x200 on the ATmega2560. SRAM is used to store variables from a C program. I/O registers and extended I/O registers shown in the memory maps are the registers used for accessing various hardware devices on the AVR. 9.4 • Summary ● 170 • The state of a switch can be found by looking at the logic level of an individual bit in the PINx register of an AVR. A bit in the PINx register that corresponds to the pin that the switch is connected to indicates whether the switch is open or closed. • The bitwise AND operator can be used to evaluate the state of a single bit in a register without the other bits in the register affecting the evaluation. • An AVR has separate program and data buses which is called a Harvard architecture. Chapter 9 • I/O and Memory Maps • Flash memory in the program area of the memory map stores the compiled C program and is non-volatile which allows the program to start running when power is applied to the AVR. • Various registers are mapped at the beginning of AVR data memory space. • SRAM memory in the data area of the memory map is used to store variables from a C program allowing them to be changed while the program is running. ● 171 C Programming with Arduino ● 172 Chapter 10 • Previous C Topics Revisited Chapter 10 • Previous C Topics Revisited What you will do in this chapter: • Look at previous C topics in more detail • More on format specifiers, field width specifiers and escape sequences • Additional loops – the do-while and for loops are introduced • Programming the AVR USART / serial port • Nested loops and decisions • Decision making with the switch construct and conditional operator • Functions and pointers • Scope of variables and static variables • More on floating point variables • Casts in C 10.1 • Format Specifiers Table 10-1 on page 174 shows the format specifiers in C, you have already used some of them. The formats program makes use of some of the new format specifiers. Create this project using the template file and view the output in the terminal program on the PC. Project name: formats Project location: CH10\formats main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); /* characters and strings */ printf("%s %c\r\n", "This is the letter", 't'); /* integers */ printf("Ints:%d, %i, %u\r\n", -1000, -123, 958); printf("Oct:%o, hex:%x, HEX:%X\r\n", 191, 191, 191); /* floating point numbers */ printf("%f, %e, %E\r\n", 45.345, 45.345, 45.345); } while(1) { } Program Output This is the letter t Ints:-1000, -123, 958 Oct:277, hex:bf, HEX:BF 45.345001, 4.534500e+01, 4.53400E+01 ● 173 C Programming with Arduino %s and %c are used to print a string and character respectively. You have already seen %d for printing integer decimal numbers, %i does the same thing and %u works with unsigned integers. %x and %X are for printing hexadecimal numbers. %o prints out a number in octal format. Octal is another number system like binary or hexadecimal, but uses base 8. %f is familiar to you and is used to print a floating point number. %e and %E print a number in exponential notation, which is a notation used by scientists and engineers. Table 10-1 C Format Specifiers Format Specifier Data Type Print Format %d or %i int Decimal number %u unsigned int Unsigned decimal number %x unsigned int Lowercase hexadecimal %X unsigned int Uppercase hexadecimal %o unsigned int Octal unsigned integer %c char or int Character %s string A string of characters %f float Floating point number %e float Lowercase exponential notation %E float Uppercase exponential notation 10.2 • Field Width Specifiers Revisited We have already seen field width specifiers used with floating point numbers to set the number of digits to print after the decimal point. The number of digits can also be set before the decimal point, or in the case of decimal and hexadecimal numbers, set the number of place holders to the left of the number. As an embedded programmer, you may wish to use this to show the width of a number when using hexadecimal numbers, or to align numbers. For example, the number 0x5 is a 3 bit number (1012), but in a microcontroller or computer, memory is always displayed in multiples of bytes, so we may wish to display this number with padding zeros to represent all 8 bits – 0x05 (0000 01012). The next program, padding, shows how to pad different numbers with zeros or spaces. Create the project using the template file. Project name: padding Project location: CH10\padding main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); // a 3 bit hex number padded to 8 bits printf("0x%X\n", 0x5); // unpadded printf("0x%2X\n", 0x5); // pad with spaces printf("0x%02X\n\n", 0x5); // pad with 0 // a 3 bit hex number padded to 16 bits printf("0x%4X\n", 0x5); // pad with spaces printf("0x%04X\n\n", 0x5); // pad with 0 ● 174 Chapter 10 • Previous C Topics Revisited // decimal number padded printf("%d\n", 123); // unpadded printf("%5d\n", 123); // padded with spaces printf("%05d\n\n", 123); // padded with 0 // float padded printf("%f\n", 34.218); // printf("%10f\n", 34.218); // printf("%10.4f\n", 34.218); // printf("%010.4f\n\n", 34.218); // } unpadded padded with spaces pad = space, 4 nums after . pad = 0, 4 nums after . while(1) { } Program Output 0x5 0x 5 0x05 0x 5 0x0005 123 123 00123 34.217999 34.217999 34.2180 00034.2180 Run the program and note the following: 1. A number placed in the middle of the format specifier (directly after the %) is the field width specifier (which is now part of the format specifier). 2. A field width specifier will pad a number with spaces unless it is preceded with 0, in which case it will pad the number with 0s. 3. The value of the field width specifier is the total width of the number printed. So if the number is 23 and the field width specifier is 1 or 2, you will not see any padding. As soon as a width of 3 or more is used, the padding will be seen as the number is forced to a length of 3 digits instead of 2 in this example. 4. When using field width specifiers with floating point numbers, the number in front of the dot of the field width specifier specifies the entire length of the number printed including the decimal point and all numbers in front of and behind it. The number after the dot in the field width specifier specifies the number of digits after the decimal point in the printed number. Figure 10-1 on page 176 shows the above points diagrammatically. Something to note about floating point numbers is that they do not always print out exactly as they are written in the C program. From the previous examples we can see that 45.345 may be stored as 45.345001 (from the formats program) and 34.218 may be stored as 34.217999. This has to do with how floating point numbers are implemented and stored in memory in C. For this reason, floating point numbers should never be ● 175 C Programming with Arduino compared for exact matches using an equality operator. Figure 10-1 Field Width Specifiers in Decimal and Floating Point Numbers 10.3 • Escape Sequences You have already seen the newline and carriage return escape sequences. Table 10-2 shows the other possible escape sequences in C. Table 10-2 C Escape Sequences Escape Sequence Meaning \a alert (bell character) \b backspace \f formfeed \n newline \r carriage return \t horizontal tab \v vertical tab \\ backslash \? question mark \' single quote \" double quote \ooo octal number \xhh hexadecimal number An escape sequence always begins with a backslash. This gives the backslash character special meaning in C. To print the backslash character, we therefore need to use an escape sequence. This is the double backslash (\\) shown in the table. The same applies to single and double quotes. These characters have special meaning in C, so to print one of these characters out, we need an escape sequence. For example the following statement: printf("I said, "Hello""); would cause a compile error because the compiler sees "I said, " as a string. Hello is ● 176 Chapter 10 • Previous C Topics Revisited not part of the string, but falls outside of it. We need to use escape sequences to be able to print this text: printf("I said, \"Hello\""); Characters can also be inserted directly into a string by using the hexadecimal number escape sequence. This will insert the equivalent ASCII character from the ASCII table in place of the hexadecimal number. The hexadecimal number escape sequence can be used to print characters that you would not be able to type into your C source code, such as the graphical characters from the ASCII table. The escape project demonstrates the use of some of the new escape sequences. The program shows two ways of inserting quotes into a string, how to insert characters into a string from the ASCII table and how to insert graphical characters into a string from the ASCII table. Before running this program, set up the Tera Term font as shown in Figure 10-2 on page 178 in order to show the graphical characters properly. To open the Font selection dialog box, click Setup → Font... on the top menu. Even though the Terminal font appears to be selected, click a different font and then click the Terminal font again to select it. Click OK when done. Changes can be saved by using Setup → Save Setup... from the top menu of Tera Term so that the font does not have to be manually selected again. Project name: escape Project location: CH10\escape main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { UartInit(); /* printing quotes in a string */ printf("I said, %cHello%c\r\n", '"', '"'); printf("I said, \"Hello\"\r\n"); /* using hex number escape sequences */ printf("%c%c%c\r\n", 'A', 'B', 'C'); printf("\x41\x42\x43\r\n"); /* using hex numbers to print graphical characters */ printf("\xC9\xCD\xCD\xCD\xCD\xCD\xBB\r\n"); printf("\xBA AVR \xBA\r\n"); printf("\xC8\xCD\xCD\xCD\xCD\xCD\xBC\r\n"); } while(1) { } Program Output See Figure 10-2 The program first prints out quotes using format specifiers and character constants, the character constants being the double quotes (") placed between single quotes. The same string is then printed out using escape sequences which directly inserts the quotes into the string, no need for format specifiers. An example is then given of printing out the characters A, B and C first using character constants and then using the hexadecimal number escape sequence. The hexadecimal number is used to print out the same characters by using the correct number from the ● 177 C Programming with Arduino ASCII table. Finally the program prints out some graphical characters from the ASCII table that you would not be able to type into your C source code. The output of the program is shown in Figure 10-2. Refer to Appendix A for the ASCII table. The correct font set must be selected in the terminal program used with the above program in order to show the graphical characters correctly that display a box around the letters AVR. Many font sets in Windows do not contain the graphical characters from the extended part of the ASCII table and will print out incorrect characters. Figure 10-2 Setting up the Font in Tera Term 10.4 • Loops We have only seen one type of loop in C so far, the while loop. We will look at two more C loops, the do – while loop and the for loop, but let’s first look at the break and continue statements used in a while loop. These statements can be used in the other types of loops as well. 10.4.1 • A while Loop that uses break and continue The brk_cont project demonstrates how the break and continue statements work. Project name: brk_cont Project location: CH10\brk_cont main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int count = 0; UartInit(); while (count < 10) { count++; ● 178 Chapter 10 • Previous C Topics Revisited if (count == 5) break; printf("Count 1 = %d\r\n", count); } count = 0; while (count < 10) { count++; if (count == 5) continue; printf("Count 2 = %d\r\n", count); } } while (1); Program Output Count Count Count Count Count Count Count Count Count Count Count Count Count 1 1 1 1 2 2 2 2 2 2 2 2 2 = = = = = = = = = = = = = 1 2 3 4 1 2 3 4 6 7 8 9 10 The break Statement In this program, the break statement is demonstrated first. In the first while loop, a count is incremented at each pass through the loop and the result is printed to the serial terminal. When the value of the count reaches 5, the break statement is run. This statement breaks out of the loop altogether and will no longer run any of the code in the loop or evaluate the loop expression again. Program flow continues below this while loop. The continue Statement The continue statement is used in the second while loop. In this loop, the continue statement is run when the count reaches a value of 5. The continue statement causes program flow to continue at the top of the loop and skip all of the statements below it. In this program, printing out of the count value to the serial terminal is skipped when the count value is equal to 5. This is because the statement below the continue statement is not run at all, instead the loop expression is evaluated and then the count incremented at the top of the loop. This program also shows that an empty while(1) loop can be written without the opening and closing braces and is simply terminated with a semicolon. The following two lines of code do exactly the same thing by creating an empty infinite loop that stops program execution continuing in uninitialised memory that contains no program instructions to run. while(1) { } while(1); ● 179 C Programming with Arduino 10.4.2 • The do – while Loop The do – while loop is similar to the while loop, except that it is always run once, no matter what the loop expression evaluates to. The reason that the loop runs at least once is because the loop expression is evaluated after each pass through the loop. You can use this kind of loop every time that you want the code in the loop to be run at least once, no matter what the loop expression result is. The do_while program shows how a do – while loop works. Project name: do_while Project location: CH10\do_while main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int count = 0; UartInit(); do { count++; printf("Count: %d\r\n", count); } while (count <= 10); } while (1); Program Output Count: Count: Count: Count: Count: Count: Count: Count: Count: Count: Count: 1 2 3 4 5 6 7 8 9 10 11 The loop consists of two keywords, do and while. Note also that the while statement in this loop is terminated with a semicolon, unlike in the normal while loop. The loop will always be entered into and the statements run at least once. The loop expression is then evaluated and if the result is true, the loop will be run again. Figure 10-3 on page 181 shows how the do – while loop works. When the do_while program is run, the count that is displayed is from 1 to 11 and not 1 to 10. The reason is that when a count of 10 is reached, the loop expression still evaluates to true. The loop is therefore run another time, incrementing the count to 11. When the loop expression is evaluated with a count value of 11, the result is false and the loop is exited. ● 180 Chapter 10 • Previous C Topics Revisited Figure 10-3 The do – while Loop 10.4.3 • The for Loop The for loop is most often used in C programs for counting or doing something a certain number of times. The for_loop program that follows is similar to the water program of chapter four, except that it uses a for loop instead of a while loop. Project name: for_loop Project location: CH10\for_loop main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int level; UartInit(); for (level = 0; level <= 10; level++) { printf("Level = %d litres\r\n", level); } printf("Finished"); } while(1) { } Program Output Level = 0 litres Level = 1 litres Level = 2 litres Level = 3 litres Level = 4 litres Level = 5 litres Level = 6 litres Level = 7 litres Level = 8 litres Level = 9 litres Level = 10 litres Finished ● 181 C Programming with Arduino Figure 10-4 illustrates how the for loop works. Figure 10-4 The for Loop The for loop contains three expressions at the top of the loop body, namely, the initialisation expression, the control expression and the adjustment expression. The initialisation expression is only evaluated once, when the for loop is first run in order to perform the necessary initialisation of the variable that will be used in the loop. The control expression is evaluated every time that the loop is run, much like the loop expression in the while loop. If this expression evaluates to true, the statements in the body of the loop will be run. If it evaluates to false, the loop will be exited. The adjustment expression is executed after each pass through the loop and before the control expression is evaluated again. This expression is used to change the value of the variable that will result in the loop being exited. Here is how the for loop operates in the for_loop program: 1. Initialise the level variable to zero (level = 0) 2. Evaluate the control expression (level <= 10) 3. Run the statements in the loop body 4. Increment the level variable by one (level++) 5. Repeat steps 2 to 4 until step 2 evaluates to false, then exit the loop 10.5 • The Problem with printf() in Embedded Systems The printf() function is very convenient to use when learning the C language and allows text to be easily printed to a terminal program on a PC. In practical embedded systems, printf() is seldom if ever used because it uses a lot of memory which is a limited resource on microcontrollers, especially on smaller microcontrollers. One solution to this problem is to write functions that use the serial port directly to send and receive characters or strings of text. Although custom serial port functions will have less formatting options for text, they will use much less memory. On an embedded system printf() needs to be configured to tell it where to print to. There is no standard output device on an embedded system and the available device could be a serial port, LCD, or other screen, or there may be no standard output at all. Part of what the template file used with this book does is set up the serial / USB port on the Arduino as the output for the printf() function. In this section we look at programming the serial port using the set of registers available for the serial port and write some basic functions for using the serial port. 10.5.1 • Programming the Serial Port The hardware device inside an AVR microcontroller used to control a serial port is called a USART (Universal Synchronous and Asynchronous serial Receiver and Transmitter). A USART converts a byte into a serial stream of bits for transmitting over a serial link ● 182 Chapter 10 • Previous C Topics Revisited and converts serial data received from a serial link to bytes that can be read by the microcontroller. On both the Uno and MEGA, USART0 is wired to a second AVR chip in a small square surface mount package on the Arduino board near the USB connector as shown in Figure 10-5. The second AVR microcontroller is configured as a virtual COM port when plugged into a PC. Serial port data sent and received from the main AVR microcontroller is routed from the on-chip USART to the second AVR microcontroller and USB port. In this way sending and receiving serial data from USART0 is just like sending and receiving data to and from a PC using one of the older RS-232 serial ports that used a 9-pin D-type connector. There is one USART on the ATmega328P, USART0, and four USARTs on the ATmega2560, USART0, USART1, USART2 and USART3. Because USART0 is present on both chips, the same code for programming the serial port can be used on the Uno and MEGA. Before using the USART as a serial port, it must be configured. Basic configuring of USART0 as a serial port requires data to be written to two registers to set up the baud rate of the serial port, which is the speed at which the serial port communicates. The transmit and receive pins of the USART must be set up as such to configure these pins with their alternate function instead of as general purpose input or output pins. Finally the communication parameters for the serial port must be configured, such as number of data bits, number of stop bits and parity bits. In the program that follows, USART0 will be configured as follows: • Baud rate 9600 • 8-bit data width • 1 stop bit • No parity When a program such as Tera Term is connected to the Arduino virtual COM port on a PC, it must be set up with matching communication parameters. Figure 10-5 A Second AVR Adds a USB Port to the Arduino After the USART is initialised, further registers are used to check whether the USART is ready to transmit or if a byte has been received by the USART. Other registers are then used to send a byte or to read an incoming byte. The following registers are used for setting up USART0 for transmitting and receiving: • UBRR0H and UBRR0L – sets the baud rate • UCSR0B – enable USART0 transmit and receive pins • UCSR0C – USART mode and communication parameters ● 183 C Programming with Arduino • UCSR0A – used to check if a byte was received and if the USART is ready to transmit • UDR0 – writing to this register sends a byte, reading from this register gets a received byte 10.5.2 • Writing Serial Port Functions Three serial port functions can be found in the serial_port project code listing below. Functions for initialising USART0 and then transmitting and receiving data using USART0 can be found in the code. The project was created without the use of the template file. If the template file is used to create this project it will not be able to be built properly and Atmel Studio will display an error. Output from the program is displayed in the terminal program on a PC. The program sends two bytes out of the serial port and then echoes any characters typed into the terminal program back to the terminal program. Project name: serial_port Project location: CH10\serial_port main.c #define F_CPU 16000000UL #define BAUD 9600 #define BAUD_TOL 2 #include <avr/io.h> #include <util/setbaud.h> void UartInit(void); void UartTxByte(char data); int UartRxByte(void); int main(void) { int rx_data; UartInit(); UartTxByte('H'); UartTxByte('i'); } while(1) { rx_data = UartRxByte(); if (rx_data > -1) { UartTxByte(rx_data); } } void UartInit(void) { // set UART0 baud rate UBRR0H = UBRRH_VALUE; UBRR0L = UBRRL_VALUE; // dedicate pins to USART0 for transmit and receive UCSR0B = 0x18; // 8-bit, 1 stop bit, no parity, asynchronous UART UCSR0C = 0x06; } ● 184 Chapter 10 • Previous C Topics Revisited void UartTxByte(char data) { while(!(UCSR0A & 0x20)); UDR0 = data; } // wait until ready to tx // transmit byte int UartRxByte(void) { int data; if (UCSR0A & 0x80) { data = UDR0; } else { data = -1; } } // check if byte received // get received byte // byte not received return data; Example Program Output Hi abcd 123 Hello The example program output shows that the program initially sends the letters "Hi" out of the serial port. The Enter key was then pressed followed by Ctrl + Enter to move the cursor to the next line in the terminal window by sending a carriage return and newline character from the keyboard. Various characters were then typed into the terminal program that were echoed back to the terminal program by the Arduino. Each line was followed by Enter and Ctrl + Enter. In the body of main(), three functions are used to control the serial port: • UartInit() – initialises the serial port to the desired configuration • UartTxByte() – waits until the USART is ready to transmit and then sends a byte • UartRxByte() – checks if a byte was received and returns the received byte if available In the while(1) loop of the program, UartRxByte() is continually called and the value that it returns is placed in the variable rx_data. If no new byte was received by the USART, the function will return -1 and the if statement will evaluate to false. When a byte is received by the USART, the function will return the received byte which will be a value that is greater than -1 and the if statement will evaluate to true. Inside the body of the if statement, UartTxByte() is called to transmit the received byte back out of the serial port, resulting in data typed into Tera Term being received back in the Tera Term window. ● 185 C Programming with Arduino Figure 10-6 Registers Used to Control USART0 Setting the Baud Rate UBRR0H = UBRRH_VALUE; UBRR0L = UBRRL_VALUE; In UartInit(), registers UBRR0H and UBRR0L are used to set the baud rate of the USART. The AVR library provides a convenient way of setting the baud rate without having to calculate the values to assign to these two registers. By defining values for F_CPU, BAUD and BAUD_TOL at the top of the C file, the values to pass to the two baud registers are automatically calculated and made available in UBRRH_VALUE and UBRRL_VALUE which need only be assigned to the two baud registers to set the desired baud rate. The desired baud rate is set by the value assigned to BAUD which is set to 9600 in this program. Configuring the Transmit (Tx) and Receive (Rx) Pins UCSR0B = 0x18; On both the Uno and MEGA, Arduino pin number 0 is the serial port receive (Rx) pin and Arduino pin number 1 is the serial port transmit (Tx) pin. On the Arduino Uno the serial port Rx pin maps to port pin PD0 and the Tx pin maps to port pin PD1. On the MEGA the Rx pin maps to port pin PE0 and Tx maps to port pin PE1. These pins are configured as USART0 Tx and Rx pins in UCSR0B by setting the RXEN0 and TXEN0 bits as shown in Figure 10-6 at the top. Setting these two bits to logic 1 values and the rest of the bits in the register to logic 0 values is done by writing the value 0x18 to this register as shown in the figure. Setting Serial Communication Parameters UCSR0C = 0x06; UCSR0C (middle register in Figure 10-6) is used to set up the remaining serial communication parameters. The upper two bits of this register, bits UMSEL01 and UMSEL00, are cleared to logic 0 values to set the USART up in asynchronous USART mode, which is required for the type of serial port communication that we are using it for. Bits UPM01 and UPM00 are both cleared which sets parity to none. USBS0 is cleared which makes the number of stop bits 1 bit. Clearing bit UCSZ02 in the UCSR0B register (top register in the figure) and setting bits UCSZ01 and UCSZ00 in the UCSR0C register (middle register in the figure) configures the data width of the USART to 8 bits. The least significant bit in UCSR0C is not used in asynchronous mode and is set to 0. ● 186 Chapter 10 • Previous C Topics Revisited Transmitting a Byte void UartTxByte(char data) { while(!(UCSR0A & 0x20)); UDR0 = data; } Before a byte can be transmitted, the UDRE0 bit in UCSR0A (bottom register in Figure 10-6 on page 186) is checked to see if it is set. If this bit is set, it means that the USART is ready to transmit and a byte can be written to UDR0 which will then be sent serially on the Tx pin of the USART. The UDRE0 bit is checked in a while loop that will wait until the bit is set before the line of code below it is run which transmits the data. The bitwise AND operator is used on the register to test if the bit is set with the expression (UCSR0A & 0x20). When the bit is set, the expression (UCSR0A & 0x20) will evaluate to true, which will cause program execution to be stuck in the loop. If the USART is not ready for transmitting, this expression will evaluate to false and the loop will be exited. This is exactly the opposite of what we want to happen. The NOT operator (!) is used to invert the logic in order to get the program to behave as we would like. The NOT operator applied to the result of any logic evaluation will invert the result. In other words if an expression evaluates to true and the NOT operator is applied to it, the result will change to false. If an expression evaluates to false, the NOT operator will invert it to be true. Applied to the above code, when the UDRE0 bit is 0, then the USART is not available to transmit and we want to wait in the while loop. The expression (UCSR0A & 0x20) evaluates to false, but after the NOT operation is applied, it evaluates to true and we stay in the loop as desired. As soon as the USART is available to transmit, it sets the UDRE0 bit and (UCSR0A & 0x20) now evaluates to true. This is when we want to exit the loop and send the byte. The NOT operator inverts the evaluation of true to false, which allows program flow to exit the loop and send the byte. Receiving a Byte int UartRxByte(void) { int data; if (UCSR0A & 0x80) { data = UDR0; } else { data = -1; } } return data; UartRxByte() reads a single byte from the USART if a byte is available i.e. if a byte has been received over the serial link. The return type of this function is int which is bigger than the 8-bit byte width of UDR0, the register that contains a byte that has been received. Because the return type of this function is bigger (16 bits wide) than the data being received, it can return -1 to indicate that no new data was received. Any data byte received will be a positive number from 0 to 255. The if statement in UartRxByte() uses the bitwise AND operator to test whether the ● 187 C Programming with Arduino RXC0 bit is set in UCSR0A. If this bit is set it means that a new byte was received over the serial link and the received byte can be read from UDR0. If the RXC0 bit is not set, it means that a new byte was not received since the last time that this function was run. In this case, the variable data is assigned a value of -1 which is returned by the function. 10.6 • Nested Loops and Decisions Nesting in programming refers to putting one thing inside another. In the case of loops, we can put one loop inside another loop. With decisions, we can have a decision within a decision. 10.6.1 • Nested Loops The nest_loop program below shows one for loop inside another for loop. Create this project using the template file. The second for loop is said to be nested in the first for loop. The first loop counts from 1 to 3. Every time that the loop executes the statements in the loop’s body, it runs the nested loop as well. The nested loop counts through the ASCII characters from A to D. Because the letters in the ASCII table are sequential, we can move from letter to letter by incrementing each character by one. The nested loop could also use hexadecimal numbers or decimal numbers to achieve the same result. Each of the following for loops produce exactly the same results in this program: for (ch = 'A'; ch <= 'E'; ch++) for (ch = 0x41; ch <= 0x45; ch++) for (ch = 65; ch <= 69; ch++) Every time that the main loop is run, the nested loop is run to completion. For every increment of 1 in the main loop, the nested loop counts through the characters A to D. After the nested loop is run, four "-" signs are printed out when the statement below the nested loop is run. Any kind of loop can be nested in any other kind of loop. You can nest a for loop in a while loop, a while loop in a for loop, etc. Project name: nest_loop Project location: CH10\nest_loop main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int count; char ch; UartInit(); for (count = 1; count <= 3; count++) { for (ch = 'A'; ch < 'E'; ch++) { printf("%d, %c\r\n", count, ch); } ● 188 Chapter 10 • Previous C Topics Revisited } } printf("----\r\n"); while (1); Program Output 1, A 1, B 1, C 1, D ---2, A 2, B 2, C 2, D ---3, A 3, B 3, C 3, D ---- 10.6.2 • Nesting Decisions Decisions can also be nested as the code in the nest_if project below shows. This project must be created using the template file. Use Tera Term to send keyboard characters to this program over the USB / serial port. Sending ‘1’ or ‘2’ will display valid menu items in the terminal window. Any other character sent to the program will result in a message displaying the invalid character. Project name: nest_if Project location: CH10\nest_if main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { char rx_data; UartInit(); while (1) { if (UCSR0A & 0x80) { // serial data byte received rx_data = UDR0; // get the serial data byte if (rx_data == '1') { printf("Menu item 1 selected\r\n"); } else if (rx_data == '2') { printf("Menu item 2 selected\r\n"); } else { printf("%c invalid selection\r\n", rx_data); ● 189 C Programming with Arduino } } } } Example Program Output Menu item 1 selected Menu item 2 selected 3 invalid selection This program uses the serial port USART registers as done in the serial_port project, but this time it is not necessary to include a separate function to initialise the USART because this program is created from the template files and UartInit() is included with the template and called at the beginning of the program which initialises the USART. Figure 10-7 Nested Decisions Inside the while(1) loop, the first if statement continually checks if a character has been received on the serial port by checking the RXC0 bit in UCSR0A. When a character is received, it is read from UDR0 and stored in the rx_data variable. A nested if statement is used to check if the received character is a valid menu option. The use of indenting helps to show which else belongs to which if statement as shown in Figure 10-7. 10.7 • Decision Making with the switch Statement The menu program demonstrates the use of the switch statement which is similar to using the if – else if construct in C. Create this project using the template file. Project name: menu Project location: CH10\menu main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { char rx_data; ● 190 Chapter 10 • Previous C Topics Revisited UartInit(); while (1) { if (UCSR0A & 0x80) { rx_data = UDR0; switch (rx_data) { case '1': printf("Menu item 1.\r\n"); break; case '2': printf("Menu item 2.\r\n"); break; case '3': printf("Menu item 3.\r\n"); break; case '4': printf("Menu item 4.\r\n"); break; } } } } default: printf("Invalid selection.\r\n"); break; Example Program Output Menu item 1. Menu item 2. Invalid selection. Menu item 3. Menu item 4. Invalid selection. The example program output shows the output from the program when the characters 12e345 are sent to it. When this program is run, pressing keys 1 to 4 in the terminal window will select various menu items. If anything else is pressed, a default message is displayed to say that the selection was invalid. ● 191 C Programming with Arduino Figure 10-8 The switch Statement Four C keywords are associated with the switch construct, namely switch, case, default and break. The switch statement uses a set of parentheses that contain a variable or expression that is to be selected for in the body of the switch statement. Inside the body of the switch statement are a number of case statements. The switch variable or expression is checked to see if it matches one of the case statements. If it does, the statements under the case statement are run in the order that they appear. To stop running any further unwanted statements, the break keyword is used to break out of the body of the switch statement. If break was not used, all of the other statements below the case would run. The statements under the default keyword will be run if no match is found at any case statement. The switch statement does the same as the following code, but using switch is easier to read and more flexible: if (rx_data == '1') { printf("Menu item 1.\r\n"); } else if (rx_data == '2') { printf("Menu item 2.\r\n"); } else if (rx_data == '3') { printf("Menu item 3.\r\n"); } else if (rx_data == '4') { printf("Menu item 4.\r\n"); } else { printf("Invalid selection.\r\n"); } The default statement can be left out if desired and is the equivalent of leaving off the else statement in the code example above. Figure 10-8 shows the details of the switch statement. In the figure, only the case of rx_data containing a value of 1 is shown, but each of the other cases will behave the same way – e.g. if rx_data contains a value of 2, ● 192 Chapter 10 • Previous C Topics Revisited the code under case ‘2’ will run and then the break statement will exit the switch body. 10.8 • The Conditional Operator Another operator used for making decisions is known as the conditional operator. The sw_condition program demonstrates its use. For this project connect a switch to pin PC0 as shown in Figure 9-3 on page 166 and create the project using the template file. Output from the program is displayed in Tera Term and is continually sent out of the serial port. Project name: sw_condition Project location: CH10\sw_condition main.c #define #include #include #include #include F_CPU 16000000UL <avr/io.h> <util/delay.h> <stdio.h> "stdio_setup.h" int main(void) { unsigned char sw_state = 0; UartInit(); DDRC = 0x00; } // port C pins set as outputs while(1) { sw_state = (PINC & 0x01) ? 1 : 2; printf("Switch state: %d\r\n", sw_state); _delay_ms(200); } Example Program Output Switch Switch Switch Switch Switch Switch Switch Switch ... state: state: state: state: state: state: state: state: 2 2 2 1 1 1 2 2 The program monitors the push button switch and will display a message containing either 1 or 2, depending on if the switch is closed or not. The conditional operator is used to choose the number to send to the terminal program, depending on the state of the switch. Figure 10-9 on page 194 shows how the conditional operator works. The conditional operator consists of a condition that will evaluate to either true or false as we have previously seen in C. Should the result of the condition be true, the first expression will be the result of the operation. In the example program, the sw_state variable will be assigned a value of 1 when the condition is true, i.e. in this program when the switch is pressed or closed. ● 193 C Programming with Arduino Should the condition evaluate to false, the second expression will be the result and will therefore be substituted into the sw_state variable. The sw_state variable will then contain a value of 2 in this example. Figure 10-9 The Conditional Operator 10.9 • Functions and Pointers When you pass a variable to a function, the function is sent a copy of the variable and operates on this local copy of the variable. If the function modifies the variable, the original variable that was passed to it remains unmodified. In C the address of a variable can be passed to a function. The function will then be able to modify the value of the variable as it has the address of the variable and not a copy of the variable’s contents. 10.9.1 • Passing an Address to a Function The next project called point_func shows how to pass the address of a variable to a function. Create this project from the template file and view the output in the terminal window. Project name: point_func Project location: CH10\point_func main.c #include <stdio.h> #include "stdio_setup.h" void func1(int num); void func2(int *pnum); int main(void) { int number = 123; UartInit(); printf("Number is %d\r\n", number); func1(number); printf("Number is %d\r\n", number); ● 194 Chapter 10 • Previous C Topics Revisited func2(&number); printf("Number is %d\r\n", number); } while(1); void func1(int num) { num = 567; } printf("Func1 num %d\r\n", num); void func2(int *pnum) { *pnum = 987; } printf("Func2 pnum %d\r\n", *pnum); Program Output Number is 123 Func1 num 567 Number is 123 Func2 pnum 987 Number is 987 The variable number is first printed out and displays the value that it was initialised to (123). This variable is then passed to the function func1() which changes the value of the variable and prints the result out (567). When the number variable was passed to func1(), a copy of the variables contents was made to a "local" variable in func1() called num. This local variable was modified to contain the number 567 and left the variable that was passed to it unaffected. This is confirmed when, after returning from func1(), the value of number is printed out again and remains unchanged (123). Function func2() is passed the address of the variable number (&number). This address is stored in the pnum pointer variable which is local to func2(). Inside this function, the address variable is dereferenced (*pnum) in order to operate on the contents of the variable that it is pointing to. It is of course pointing to the variable number and not to a local copy of number. The following line of C code in func2() means "assign the value of 987 to the memory location that pnum is pointing to". *pnum = 987; pnum contains the address of number and is therefore pointing to it. The value of the variable that pnum is pointing to is then printed out inside func2() (987). After returning from func2(), the value of number is printed out again (987). It is now 987 and not 123 any more, because we modified the contents of this variable by passing its address to func2(). 10.9.2 • Returning More Than One Value from a Function Pointers enable us to return more than one value from a function. The return2 program uses two pointers to return two values from a function. ● 195 C Programming with Arduino Project name: return2 Project location: CH10\return2 main.c #include <stdio.h> #include "stdio_setup.h" void retfunc(int *val1, int *val2, int input); int main(void) { int num1, num2; UartInit(); retfunc (&num1, &num2, 34); printf("34 squared = %d\r\n", num1); printf("34 * 10 = %d\r\n", num2); } while (1); void retfunc(int *val1, int *val2, int input) { *val1 = input * input; *val2 = input * 10; } Program Output 34 squared = 1156 34 * 10 = 340 The retfunc() function in this program takes two pointer arguments and one integer argument. The pointer arguments are used to store the two output values from the function into the two variables that they point to. When retfunc() is called, it is passed the address of the variable num1 and the address of variable num2. The constant 34 is also passed to the function in the function variable input. The function then calculates the square of the variable input (input multiplied by itself) and saves it in the variable that pointer val1 is pointing to i.e. num1. Next, retfunc() calculates input multiplied by 10 and stores it in the variable that pointer val2 is pointing to, i.e. num2. The function has thus returned two values which it would not normally be able to do. After returning to main(), the calculated values are found in the variables num1 and num2 and are printed out. 10.10 • Variables and Scope 10.10.1 • Local Variables Variables defined in a function have what is known as "function scope". A variable defined in main() can only be used and "seen" inside main(). A function can only "see" the variables defined inside it and does not know about variables that are defined inside any other function. ● 196 Chapter 10 • Previous C Topics Revisited If you try to use a variable inside a function that was defined in another function, the compiler will display an error. A variable can be defined inside a function with the same name as a variable that has already been defined in another function because of the scope of the variables being limited to the functions in which they are defined. Variables that are defined inside a function are known as local variables as they are local to the function. 10.10.2 • Global Variables Global variables have "file scope", meaning that they can be "seen" and used by every function in a file – the C source file that they are defined in. A global variable is also known as an external variable. The glob_var program uses a global variable to pass information between functions. Project name: glob_var Project location: CH10\glob_var main.c #include <stdio.h> #include "stdio_setup.h" int glob_count = 0; void AddOne(void); void Delay(void); int main(void) { UartInit(); } while (1) { printf("Count is %d\r\n", glob_count); AddOne(); Delay(); } void AddOne(void) { glob_count++; } void Delay(void) { volatile long del = 1000000; while (del--); } Sample Program Output Count Count Count Count ... is is is is 0 1 2 3 In this program, the variable glob_count is defined as a global variable by defining it outside of main(). All functions inside the file in which it exists have access to it. In ● 197 C Programming with Arduino the program, glob_count is incremented in the AddOne() function and displayed in the main() function. Both these functions have therefore accessed this global variable. Global variables can be helpful, but it is not considered good programming practice to use too many global variables as one can loose track of which functions are changing these variables in a big program. Global variables must have unique names even if they occur in different files of a program. If the linker finds two global variables with the same name that were defined in two different files of the same project, it will not be able to link the project and will display an error message. 10.11 • Static Variables Variables that are declared within a function, i.e. local variables, are destroyed when the function returns. The memory that local variables use when the function is running is freed up for use by other parts of the program when the function returns. The static keyword can be used to prevent local variables from being destroyed as the static_var program demonstrates. Project name: static_var Project location: CH10\static_var main.c #define #include #include #include F_CPU 16000000UL <stdio.h> "stdio_setup.h" <util/delay.h> int Count(void); int StaticCount(void); int main(void) { UartInit(); } while (1) { printf("count %d, static count %d\r\n", Count(), StaticCount()); _delay_ms(1000); } int Count(void) { int count = 0; return count++; } int StaticCount(void) { static int count = 0; return count++; } ● 198 Chapter 10 • Previous C Topics Revisited Sample Program Output count count count count count ... 0, 0, 0, 0, 0, static static static static static count count count count count 0 1 2 3 4 This program calls two functions that perform identical actions of incrementing a local variable and returning the result. The only difference between the functions is that one uses a static variable and the other does not. Note that the post increment operator is used in these functions, so the count is first returned and then incremented. The non-static variable is always initialised to 0 when the function is called and it effectively "forgets" the value of this variable between calls. The result is that when the returned value is printed, a 0 is always displayed. The StaticCount() function uses a static local variable by preceding the variable definition with the static keyword. The variable is initialised to 0 the first time that the function is called and thereafter "remembers" the value that the variable contains between function calls. The result is that every time this function is called in the program, the next sequential count value is returned because this variable is never destroyed. 10.12 • Floating Point Data Types We covered the integer data types in chapter 7 and showed these data types in Table 7-3 on page 138. We will now take a look at the floating point data types. Floating point numbers are used to store numbers that have a decimal point, and approximate what is know as real numbers in mathematics. Table 10-3 shows the three floating point data types in C with their approximate ranges. Floating point numbers are signed numbers and always have one bit of the number dedicated to represent the sign (positive or negative) of the number. There is no such thing as an unsigned floating point number. Table 10-3 Floating Point Data Types Type Size Range Precision float 4 bytes 1.2E-38 to 3.4E+38 6 digits double 8 bytes 2.3E-308 to 1.7E+308 15 digits 8-bit AVR double Same as 8-bit AVR float long double 10 bytes 3.4E-4932 to 1.1E+4932 19 digits 8-bit AVR long double Same as 8-bit AVR float The float data type should be familiar to you by now as it was used in previous C example programs. In C there are two other floating point data types, namely, double and long double which allow even bigger floating point numbers to be stored. In the GCC compiler that is used by Atmel Studio, neither double nor long double have been implemented for 8-bit AVR microcontrollers. These two floating point data types can be used in a C program, but they are implemented in exactly the same way as the 4 byte float and don’t add any extra precision. ● 199 C Programming with Arduino The float_check project prints out the size of a float in bytes, the decimal digit precision, minimum positive value and maximum positive value of a float on an 8-bit AVR. Project name: float_check Project location: CH10\float_check main.c #include <stdio.h> #include "stdio_setup.h" #include <float.h> int main(void) { float num_float = 0.0; double num_double = 0.0; long double num_lng_double = 0.0; UartInit(); printf("float is %d bytes, precision %d, min %e, max %e\r\n", sizeof(num_float), FLT_DIG, FLT_MIN, FLT_MAX); printf("double is %d bytes\r\n", sizeof(num_double)); printf("long double is %d bytes\r\n", sizeof(num_lng_double)); } while(1); Program Output float is 4 bytes, precision 6, min 1.175494e-38, max 3.402823e+38 double is 4 bytes long double is 4 bytes Only the number of bytes that the double and long double data types consist of are printed out, and it can be seen that they are not implemented to their proper specification shown in Table 10-3 on page 199. Sizes for FLT_DIG, FLT_MIN and FLT_MAX are defined in the header file float.h which is included at the top of the C source file. These definitions contain the compiler specific sizes implemented for the floating point data type. C compilers implement the floating point data type in what is known as the IEEE 754 technical standard. Floating point numbers in C are usually best avoided on microcontrollers that have small amounts of memory, as the libraries that add support for floating point tend to use up a fair amount of memory. This is especially true for very small AVR microcontrollers that may have only 2k bytes of flash and a few hundred bytes of RAM. Higher precision floating point numbers were probably not implemented in the 8-bit AVR GCC compiler for this reason and will probably never be needed. 10.13 • Casts A cast forces an expression to become the type represented by type name of the cast and has the format: (type name) expression The following example casts an int type to a char type and a float type to an int type. ● 200 Chapter 10 • Previous C Topics Revisited int an_int = 4567; char ch; float flt = 1.234; ch = (char)an_int; an_int = (int)flt; // cast an int to a char // cast a float to an int The first cast tells the compiler that you want to convert an int to a char. Because a cast is used, the compiler will not complain by issuing a warning message. The compiler would normally complain because a char is only one byte long and an int is two bytes long. The resulting operation will put the least significant byte (lowest 8 bits) of the int into the char, the upper 8 bits will be discarded. The second cast forces a floating point number to become an integer. This will result in only the integer part of the floating point number being assigned to the integer variable and the fractional part after the decimal point being discarded. 10.14 • Exercises 10.14.1 • Count Down for Loop Use a for loop to count down from 10 to 0 and send each count value to be displayed in the Tera Term terminal window. When counting down, use the initialisation expression to initialise the loop variable to 10, the control expression to break out of the loop when the loop variable is less than 0 and the adjustment expression to decrement instead of increment the loop variable. 10.14.2 • LED Control using a switch Construct Use a switch statement that has two cases to switch a LED on or off by a user sending characters from a keyboard. When ‘a’ is sent, switch the LED on, when ‘b’ is sent, switch the LED off. 10.15 • Solutions 10.15.1 • Solution to 10.14.1 Count down project created using template file. Project name: CH10_1_count_down Project location: Exercises\CH10_1_count_down main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int count; UartInit(); for (count = 10; count >= 0; count--) { ● 201 C Programming with Arduino } printf("count = %d\r\n", count); } while (1); 10.15.2 • Solution to 10.14.2 LED select project created using template file. Project name: CH10_2_LED_select Project location: Exercises\CH10_2_LED_select main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { unsigned char rx_byte; DDRC = 0x01; // LED on PC0 UartInit(); while(1) { if (UCSR0A & 0x80) { // data received? rx_byte = UDR0; // get the byte switch (rx_byte) { case 'a': PORTC = 0x01; // switch LED on break; } } } } case 'b': PORTC = 0x00; // switch LED off break; 10.16 • Summary ● 202 • The break and continue statements are used to change the flow of a program and are commonly used in loops. • The do while loop will always run the body of the loop once and then check the loop expression. • The for loop is most often used to count or perform some action a set number of times. • Loops and decisions can be nested in C. Using proper indenting in source code makes nesting easier to read and understand so be sure to always indent a program clearly. Chapter 10 • Previous C Topics Revisited • The switch statement is another decision making construct in C which can be easier to read than using the if – else if – else construct especially when there are many comparisons being made. • The conditional operator is another decision making operator in C. • More than one value can be returned from a function by passing the addresses of variables to a function so that the function can store the values to be returned in these variables. • All variables in a C program have "scope". The scope of a variable determines which functions in the program can "see" and use the variable. Variables defined within a function have function scope as they can only be used by the function in which they are defined. Global variables have file scope and can be seen by all of the functions within the file that they are defined. • Static variables are not "destroyed" (freed up) when a function returns. A static variable and its contents are preserved between function calls to the function that contains the static variable. • Casts allow a programmer to force one type of variable’s value to be stored in another type of variable. ● 203 C Programming with Arduino ● 204 Chapter 11 • Arrays and Strings Chapter 11 • Arrays and Strings What you will do in this chapter: • See how arrays are used to access sequential memory locations • See how strings are used and stored in memory • Use C standard library string functions • Write functions that take strings and arrays as arguments You have already worked with strings in C when using the printf() function. C does not have a string data type but instead uses arrays of characters. An array of characters is simply a number of sequential characters in memory. In C a string is an array of characters, but it is also possible to have an array of a different data type. We will start by looking at arrays in general and see how to define and use arrays in C. We will then take a closer look at strings and some standard library functions used to manipulate them. 11.1 • Arrays An array is a set of consecutive memory locations that store data of a specified type. The key principal here is consecutive, as this is what makes arrays possible. As an example, in C an array of 5 integers is defined as follows: int arr_num[5]; This tells the compiler to make space for an array that consists of five integer numbers. Each element (integer) in the array can be accessed using array notation in C. If we wanted to assign a value to each element in the array, it can be done as follows: arr_num[0] arr_num[1] arr_num[2] arr_num[3] arr_num[4] = = = = = 20; 34; 9; 1023; 700; There are five elements in this array, but they are indexed starting at 0, so the five elements are accessed with indexes from 0 to 4 and not 1 to 5. The array definition: int arr_num[5]; is similar to defining 5 separate integer variables: int arr_num0, arr_num1, arr_num2, arr_num3, arr_num4; Defining separate variables, however does not guarantee that they are consecutively placed in memory. The separate variables can also not be referenced with array notation which, as we will see, is very useful. The index_array program initialises an array of integers and then use a for loop to move through the list of integers and send them out of the serial port. ● 205 C Programming with Arduino Project name: index_array Project location: CH11\index_array main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int num_list[5] = {34, 789, 327, 1000, 45}; int index; UartInit(); } for (index = 0; index < 5; index++) { printf("Element %d = %d\r\n", index, num_list[index]); } while (1); Program Output Element Element Element Element Element 0 1 2 3 4 = = = = = 34 789 327 1000 45 The first line inside main() defines an array of 5 integers and initialises each of these integers to a value that appears between braces after the = sign. The initialisation has the same effect as assigning a value to each element of the array as follows: int num_list[5]; num_list[0] = 34; num_list[1] = 789; num_list[2] = 327; num_list[3] = 1000; num_list[4] = 45; A for loop is used to count from 0 to 4. Inside this loop, the current count value (the variable index) and each element of the array is printed out. By referencing each element in the array with the current count that is counting from 0 through 4, we access each element in the array from 0 to 4. When index contains the value 0, then num_list[index] takes on the value of the first element in the array i.e. 34 in this example. After index has been incremented to 1, num_list[index], which is equivalent to num_list[1], now has the value of the second element in the array i.e. 789. Remember that we are counting from 0, so the first element in the array is indexed at position 0, the second at position 1 the third at position 2, etc. Figure 11-1 on page 207 shows the array used in this example in diagrammatic format as it would appear in memory. ● 206 Chapter 11 • Arrays and Strings Figure 11-1 An Array of Integers in Memory The next program, called LED_seq (LED sequence), initialises an array with a series of numbers that are written to LEDs to make them move through a sequence of different combinations of off and on LEDs. An array used in this way is known as a lookup table. Connect four LEDs on port pins PC0 to PC3 in the same way shown in Chapter 8, Figure 8-10 on page 154 for the Uno and figure 8.11 for the MEGA – just connect the bottom 4 LEDs. The values in the array are the values that are written to the microcontroller port to switch the LEDs on and off. Create this project from scratch without using the template file in Atmel Studio. Project name: LED_seq Project location: CH11\LED_seq main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { unsigned char seq[] = {0x01, 0x02, 0x04, 0x08, 0x06, 0x09, 0x0F, 0x05, 0x0A, 0x05, 0x0A}; int ind; DDRC = 0x0F; } // lower 4 bits of port C are outputs while(1) { for (ind = 0; ind < sizeof(seq); ind++) { PORTC = seq[ind]; _delay_ms(1000); } } Program Output See Figure 11-2 An array of type unsigned char is defined at the beginning of main() that contains the on and off number sequences to display on the LEDs. Figure 11-2 on page 208 shows what the numbers in the array produce when written to the LEDs. Each element of the array of unsigned characters is initialised to a value that will be written to port C of the AVR. The notation for defining the array is as follows, with the initialisation of each array element after the array definition: unsigned char seq[] ● 207 C Programming with Arduino The number of elements in this array is missing, and would normally be placed between the square brackets as the previous example showed. Because we have initialised the array with 11 values, the compiler will count the values and make the array the correct size of 11. If we did not initialise the array, we would have to specify the size of the array, as the compiler would then not know how big we intended to make the array. After defining and initialising the array and defining the ind variable to be used as an index into the array, Port C is set up to drive four LEDs on pins PC0 to PC3 before entering the while(1) loop. Figure 11-2 The Sequence of LED Patterns in the LED_seq Program Inside the while(1) loop is a for loop that is used to access each element of the array in sequence. When the for loop has counted to the last element of the array, it will start at the beginning of the array again because the for loop is inside the continuous while loop. When the top of the while(1) loop is reached, the for loop is initialised again and counts through the 11 elements of the array again. In this way it counts from 0 to 10 continually. The number to count up to in the for loop is obtained by using the C sizeof keyword. This keyword returns the size of the array in bytes and will return the number of elements in the array because each element is one byte wide. The reason for calculating the maximum value to count to in the for loop this way is to make it easier to change the patterns that are written out to the LEDs. To change the number of patterns, simply add or remove values where the array is initialised. The size of the array is automatically calculated by the compiler and the maximum number that the for loop must count to is also adjusted automatically. Inside the for loop, each number in the array (LED pattern) is written to port C. A delay function is then called to leave the pattern displayed on the LEDs long enough to see. After the delay, the next pattern is written to the LEDs to display. The previous two C code listings showed how to create an array of integers and an array of unsigned characters. Arrays of other data types can be created just as easily. An array of floating point numbers: float sample[30]; An array of long integers: long lng_array[10]; We will now look at strings which are just arrays of characters. ● 208 Chapter 11 • Arrays and Strings 11.2 • Strings Strings are useful in embedded systems wherever status information is to be displayed to a user or received from a keypad or keyboard input. Strings can be displayed on an LCD display or on a terminal emulator as we have been doing or even saved to SD card or disk if the embedded system has one. Strings are sequences or arrays of characters that are NULL terminated. This means that the last character in a string must have a value of 0. NULL is the first number in the ASCII table and has the escape sequence \0 and numerical value of zero. The strings program demonstrates the use of string constants and string variables. Create the project using the template file and connect to the terminal program to see the output. Project name: strings Project location: CH11\strings main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char msg[4]; UartInit(); printf("%s\r\n", "Hello"); msg[0] = 'B'; msg[1] = 'y'; msg[2] = 'e'; msg[3] = 0; printf("%s\r\n", msg); } while(1); Program Output Hello Bye At the beginning of this program a character array, msg, is defined and allocated 4 bytes of memory. The string "Hello" used in the printf() statement is an example of a string constant. This string cannot be changed after compiling when the program is running and is therefore constant. The msg array is used to demonstrate how a string is built up of characters and terminated with 0. It is known as a string variable as its contents can be modified when the program is running. As can be seen, the word "Bye" is written to the msg array by writing one character at a time to each element of the array. A zero is written to the last element in the array in order to correctly terminate the string which is now stored in this array. The escape sequence \0 could also be used to terminate the sting and would also result in a zero being written to the last element in the array, it would need to be written between single quotes i.e. '\0'. The size of an array used to store a string must always be one element longer than the string to leave space for the NULL terminating character ● 209 C Programming with Arduino of the string. We could have made the msg array in this program longer than 4 elements; the other elements of the array would then not be used. If the array was made too short and we tried to write a character to the array that was beyond the end of the array, memory that may have been allocated to other variables could then be overwritten causing unexpected program behaviour. Figure 11-3 shows what the string constant and string variable from the strings program look like in memory. The addresses shown would be whatever addresses were assigned to the strings during the build process. It is not important what these addresses are; only that they fall in the correct range of the microcontroller’s memory map and that each element in the string is consecutively placed in memory (consecutive addresses). Figure 11-3 The String Constant (left) and String Variable (right) used in the strings Program 11.2.1 • Writing to a String The write_string program allows a user to send a string to the embedded system from the terminal emulator program over the serial / USB port. Create this project using the template file in Atmel Studio. Project name: write_string Project location: CH11\write_string main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" #define MAX_STR_LEN 35 int main(void) { char in_str[MAX_STR_LEN]; int index = 0; UartInit(); printf("\r\nEnter a string, maximum length %d\r\n", MAX_STR_LEN - 1); while (index < (MAX_STR_LEN - 1)) { if (UCSR0A & 0x80) { /* character received? */ in_str[index] = UDR0; /* put char into array */ if (in_str[index] == '\r') { /* finished when Enter pressed */ ● 210 Chapter 11 • Arrays and Strings break; } printf("%c", in_str[index]); /* echo character on serial port */ index++; } } } in_str[index] = 0; /* NULL terminate the string */ printf("\r\nYou entered the string: %s\r\n", in_str); while (1); Example Program Output 1, Enter Key Pressed Enter a string, maximum length 34 Hello, Arduino AVR!!! You entered the string: Hello, Arduino AVR!!! Example Program Output 2, Maximum String Length Reached Enter a string, maximum length 34 QWERTYUIOPASDFGHJKLZXCVBNM12345678 You entered the string: QWERTYUIOPASDFGHJKLZXCVBNM12345678 This program prints a message in Tera Term asking the user to enter a string with a maximum length of 34. As the user enters each character of the string in the terminal window, each character is sent back to the terminal (echoed) so that the user can see what he is typing. When the user presses the Enter key, the program null terminates the string and then displays the string in the terminal window. If the user types more characters than can fit in the in_str string variable array, the string is also null terminated and displayed before the user can press the Enter key. This prevents more characters being written to the string array than can fit into the array, which would result in memory below the array being written to, possibly corrupting other program data. Let’s have a look at how the write_string program works. Firstly a maximum size for the string that the user will enter is defined using the #define directive. If you would like to change the maximum size of string that the user can enter in this program, you just need to change it in one place, where it is defined, and the array size and bounds checking throughout the program will automatically be adjusted. A message is sent to the terminal from the Arduino, prompting the user to enter a string. The maximum length of the string that can be entered by the user is also sent as part of the text message. This value is calculated by subtracting 1 from the length of the array that is used to store the string that will be input by the user. The reason for subtracting 1 from this length is because the null terminating character needs to be stored in the last element of the array if the maximum number of characters allowed in the string is used. A while loop is used to get the characters from the user via the serial port. The while loop is set to terminate if the user types more characters than can be stored in the array set aside for these characters. This is done by checking the index variable that is used to access each element of the array to see if it has gone out of bounds. We actually have to check this value against the length of the input array minus 2. This is firstly to accommodate the null termination character and secondly because the last element in the array is one less than the length of the array (because the first element in the array is counted as 0 and not as 1). The loop expression is as follows: ● 211 C Programming with Arduino index < (MAX_STR_LEN - 1) This checks that the index is less than the maximum string length minus one, which is equivalent to checking that the index is less than or equal to the maximum string length minus two. In other words we could have written: index <= (MAX_STR_LEN - 2) Inside the while loop, there is an if statement that checks to see if a character was received on the serial port. The body of the if statement is only run if a character is received. When a character is received, it is saved to the character array in_str. The variable index is used to determine which element of the array the character is saved to. This variable is initialised to 0, so stores the first character to the first element (element 0) of the array. The index variable is incremented after each character is saved so that the next character will be saved to the next element in the array. After a character has been saved to the array, the character is checked to see if it is the carriage return character (\r) i.e. we are checking to see if the user pressed the Enter key. If the Enter key was pressed, the break statement is used to exit out of the while loop without executing a further statement in the while loop. If Enter was not pressed, the character that the user typed is sent back to the terminal via the serial port by calling the printf() function which transmits a single character. After echoing the typed character back to the user, the index variable is incremented so that it will access the next element in the in_str array. The while loop’s loop expression is checked to see if index is out of bounds or not and then the if statement in the loop is run again to check for the next character from the serial port. The while loop is exited if the user enters too many characters or if the Enter key is pressed. If either of these two conditions occur, the null terminator is added to the string in the element that index is accessing. This would be the last element in the array if the loop was exited because the array was full. In the case of Enter being pressed, the \r character is saved to the array and then overwritten by the null terminating character as index is not incremented after breaking out of the loop. A text message is then displayed in the terminal with the string that the user entered, which is now saved in the in_str array. Finally program execution drops into the infinite while(1) loop. Press the reset button on the Arduino to run the program again. 11.2.2 • Initialising a String The init_string project, created from the template file, shows two ways of initialising a string variable with data. Project name: init_string Project location: CH11\init_string main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char str1[] = { 'F', 'i', 'r', 's', 't', '\r', '\n', '\0' }; char str2[] = "Second\r\n"; UartInit(); ● 212 Chapter 11 • Arrays and Strings printf("%s", str1); printf("%s", str2); } while(1); Program Output First Second Because a string is an array, we can initialise a string in the same way that we initialise an array of integers. The string str1 in this example is initialised with individual characters. Note that the null terminating character must also be put into the string. The second string, str2, in this program shows an easier way of initialising a string by putting it between double quotes. When this method is used, the compiler automatically inserts the null terminating character at the end of the string. 11.2.3 • C Library String Functions The C standard library provides a number of functions that are used for manipulating strings. Prototypes for these functions are found in the header file string.h. Table 11-1 shows some of these string functions and the str_func program demonstrates their use. Table 11-1 Some of the String Functions Available in the C Standard Library Function Description strcat(str1, str2) Concatenates (appends) str2 to str1 strcmp(str1, str2) Compares two strings. Returns 0 if the strings are equal, < 0 if str1 is less than str2, > 0 if str1 is greater than str2 strcpy(str1, str2) Copies str2 into str1 strlen(str) Returns the length of the string str, excluding the null- terminating character Project name: str_func Project location: CH11\str_func main.c #include <stdio.h> #include <string.h> #include "stdio_setup.h" int main(void) { char str1[10] = "abc"; char str2[] = "def"; UartInit(); printf("The length of str1 is %d\r\n", strlen(str1)); printf("Comparing str1 & str2: %d\r\n", strcmp(str1, str2)); ● 213 C Programming with Arduino printf("Comparing str2 & str1: %d\r\n", strcmp(str2, str1)); strcat(str1, str2); printf("str1 is now: %s\r\n", str1); strcpy(str1, str2); printf("str1 is now: %s\r\n", str1); printf("Comparing str1 & str2: %d\r\n", strcmp(str1, str2)); } while(1); Program Output The length of str1 is 3 Comparing str1 & str2: -3 Comparing str2 & str1: 3 str1 is now: abcdef str1 is now: def Comparing str1 & str2: 0 String str1 is forced to make space for a string that is 9 characters long plus the nullterminating character by specifying the length of the string as 10 between the square brackets when the string is defined. str1 is, however, initialised with a string that is only 3 characters long. The reason for allocating more space than necessary in this program is so that there will be enough space to concatenate a string to this one without going out of the array bounds. The strlen() function is called first and returns the length of string str1. The length returned is 3 which is the length of the string excluding the null-terminating character. strcmp() is called twice to compare the two initialised strings. The comparison is done by this function a character at a time. The difference between the first set of characters that do not match is returned. In the example, "abc" (str1) is compared with "def" (str2). The first characters that do not match are ‘a’ and ‘d’. Looking at the ASCII table, one can see that these characters have values of 97 and 100. The difference between the two values is 100 – 97 = 3. When these strings are compared again, they are compared in reverse order, so the difference between the two is now 97 – 100 = -3. The strcat() function is called to concatenate str2 onto str1. Some of the extra space made available when defining str1 is now used to store the extra string added to the end of the string contained in str1. str1 changes from "abc" to "abcdef" after concatenation. strcpy() is called to copy str2 into str1. This action overwrites the contents of str1 and replaces it with the contents of str2. strcmp() is called again to compare the two strings. Because str2 was copied to str1, they are now equal and strcmp() returns a value of 0. 11.3 • Arrays and Addresses 11.3.1 • Arrays and Array Element Addresses Unlike the name of a single variable that takes on the value of the variable, the name of an array is the address of the array. For example if we have an array of characters as follows: char processor[] = "AVR"; ● 214 Chapter 11 • Arrays and Strings The address of the array and therefore the address of the first element in the array is the name of the array, and in this example is processor. We do not get the address by writing &processor as with single variables. The value that the first element of the array is holding is obtained by using array notation: processor[0]. The contents of each of the other elements in the array can be accessed in a similar fashion e.g. processor[1], etc. The address of any element can be obtained by using the address operator with array notation e.g. &processor[1] is the address of the second element in this array. The array_addr project clarifies how array addresses work. Project name: array_addr Project location: CH11\array_addr main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char processor[] = "AVR"; int i; UartInit(); printf("Address of array is %u\r\n", (unsigned int)processor); for (i = 0; i < (sizeof(processor) - 1); i++) { printf("addr = %u, data = %d (%c)\r\n", (unsigned int)&processor[i], processor[i], processor[i]); } } while(1); Program Output on Arduino Uno Address of array is 2296 addr = 2296, data = 65 (A) addr = 2297, data = 86 (V) addr = 2298, data = 82 (R) This program takes the string "AVR" and prints out the address of the array (i.e. the address of the first element of the array) by using the name of the array. It then prints out the address of each element in the array as well as the contents of each element in decimal and character format. When the address of the array is printed using the name of the array, it is cast to an unsigned integer ((unsigned int)processor) to tell the compiler that we want to display the address of the array as an unsigned integer. The address of the array is a pointer to a character (char *), so the cast stops the compiler issuing a warning as we specify that we want to print this address as an unsigned integer. An unsigned integer was chosen because an address will never be negative. The for loop in this program prints out the address of each element of the array as well as the contents of each element of the array as previously explained. The terminating condition of the for loop (sizeof(processor) - 1) automatically calculates the element number of the last element of the string array. We could have just used the value 3 here. ● 215 C Programming with Arduino Note that the sizeof keyword returns the size of the string including the null-terminating character which is 4 in this program and is why we need to subtract 1. The null terminator in the processor[] array also has an address which will be the address of the last character in the array plus 1 for an array of 8-bit bytes, but was not printed in this program because the value of the null terminator, which is zero, is not a printable character. In other words, the address of the last element in the array which holds the null terminator could be printed out, but the value that the null terminator holds can not be printed out as a character. It would print out as a number which would be 0, but there is no corresponding character to print from the ASCII table when printed as a character. 11.3.2 • Passing an Array to a Function Because the name of an array is the address of the array, we can only pass the address of an array to a function. It is actually more desirable to pass the address of an array to a function than to copy the entire array to a local array of the function. When a function is passed the address of an array, we are actually passing a pointer to the array to the function. The function can be written to accept the pointer to the array using either array notation or pointer notation. These two functions can both take an array of type char as arguments: void Func1(char str[]); void Func2(char *str); The pass_array program uses two functions to demonstrate how to pass an array to a function as shown above. This program defines and initialises two arrays – a character array that is initialised with a string and an array of integers. PrintCharArray() is called and passed the address of the character array called text[]. PrintIntArray() is called and is passed the address of the integer array samples[] along with the length of the integer array. Each of the functions called demonstrate a different way to access an array, but they are essentially doing the same thing. PrintCharArray() accesses the address of the array with a local character pointer that is defined with array notation – char str[]. The first while loop in this function uses array notation to print the string out a character at a time. The second while loop does exactly the same thing, but this time using pointer notation. Both loops will only exit if the character that the pointer is pointing to is equal to 0 which is the null terminator. Project name: pass_array Project location: CH11\pass_array main.c #include <stdio.h> #include "stdio_setup.h" void PrintCharArray(char str[]); void PrintIntArray(int *ptr, int length); int main(void) { char text[] = "Start pump\r\n"; int samples[] = {115, 2000, 95, 9 }; UartInit(); ● 216 Chapter 11 • Arrays and Strings } PrintCharArray(text); PrintIntArray(samples, sizeof(samples) / 2); while (1); void PrintCharArray(char str[]) { int i = 0; } while (str[i] != 0) { printf("%c", str[i]); i++; } while (*str != 0) { printf("%c", *str); str++; } void PrintIntArray(int *ptr, int length) { int i; } for (i = 0; i < printf("%d: } for (i = 0; i < printf("%d: ptr++; } length; i++) { %4d\r\n", i, ptr[i]); length; i++) { %4d\r\n", i, *ptr); Program Output Start pump Start pump 0: 115 1: 2000 2: 95 3: 9 0: 115 1: 2000 2: 95 3: 9 To move from character to character through the array using pointer notation, the pointer itself is incremented using the increment operator (str++) which increments the address that the pointer is holding. If the array was at address 0x08A2, the pointer str would contain this address after the PrintCharArray() function was called. After each increment of this address in str, this pointer would be pointing to the next element in the array at the next address. This function demonstrates that pointer notation and array notation can be used interchangeably. You can use either one or mix them to achieve the same results. PrintIntArray() is passed the address of the array which it stores in the local integer pointer ptr. In the first for loop in this function, array notation is used with this pointer to access each element in the array that the pointer is pointing to. The second for loop uses pointer notation to do exactly the same thing as with the PrintCharArray() function. Again the pointer variable itself is incremented, which results in it pointing to ● 217 C Programming with Arduino the next address and therefore the next element in the array. It was necessary to pass the length of this array to this function as there is no other way to determine the number of integers in the array, unlike strings in which the null-terminator can be checked for. When PrintIntArray() is called, it is passed the size of the samples[] array in bytes divided by two as the second argument, shown in the following line of code from the above program. PrintIntArray(samples, sizeof(samples) / 2); The reason for dividing the number of bytes in the array by two is because each integer in the array is two bytes wide on an 8-bit AVR using the GCC compiler. The four integers in the samples[] array take up 4 × 2 bytes = 8 bytes, but we want to pass the number of integers to the function which is 8 bytes ÷ 2 = 4 integers. When the pointer variable ptr is incremented in the PrintIntArray() function, it takes into account that the array consists of two byte wide integers and increments the address in the pointer by two address locations, or two byte addresses at a time. If this were not so, the pointer would first point to the first byte of the first integer, then the second byte of the first integer, followed by the first byte of the second integer and the second byte of the second integer, etc. In other words the pointer must move up an integer, or two bytes, at a time to make sure that it points to the next integer and not the next byte. If the data type of the array was long, the pointer would move up four bytes at a time to point to the next four byte wide long integer. 11.4 • Strings as Pointers The string_ptr project shows that a string can be initialised as a pointer, again showing the similarities between pointer and array notation. Project name: string_ptr Project location: CH11\string_ptr main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { char *status1 = "Pump started.\r\n"; char status2[] = "Mixer started.\r\n"; UartInit(); printf("%s", status1); printf("%s", status2); } while(1) { } Program Output Pump started. Mixer started. ● 218 Chapter 11 • Arrays and Strings The first string in this program is initialised using pointer notation. In the program it can now be accessed using pointer or array notation. The second string is initialised as we have done before with array notation. It can also be accessed in the program with array or pointer notation. Note that initialising a string as a pointer can only be used with strings. If you wanted to have an array of integers or floating point numbers, you would have to initialise the array with array notation, although you are free in the program to access the array with either notation. 11.5 • Writing to a String using sprintf() The sprintf() function (string print formatted) can be used in the same way as the printf() function, except that instead of printing to the standard output of a system, it prints to a string. This allows all of the printf() formatting functionality to be used on a string. The string_print project demonstrates the use of the sprintf() function. Project name: string_print Project location: CH11\string_print main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" void UartTxMsg(char *msg); int main(void) { char the_string[30]; unsigned int data = 64809; UartInit(); sprintf(the_string, "Dec: %u, Hex: %X\r\n", data, data); UartTxMsg(the_string); } while(1) { } void UartTxMsg(char *msg) { int ch_num = 0; } while (msg[ch_num] != 0) { while(!(UCSR0A & 0x20)); UDR0 = msg[ch_num]; ch_num++; } // wait until ready to tx // transmit byte Program Output Dec: 64809, Hex: FD29 In the program, sprintf() is used to print the value of the variable data to the the_string[] array in both decimal and hexadecimal formats. The array that will contain the formatted output string is passed to sprintf() as the first argument. The other ● 219 C Programming with Arduino arguments to the sprintf() function are the same as for printf(). The string variable the_string[] was made extra long, i.e. 30 bytes, in order to prevent sprintf() from writing to memory beyond this array. In C there is no bounds checking of arrays which makes it possible for a program to write data beyond an array and modify data that it should not. It is up to the programmer to do bounds checking or to make an array sufficiently large enough so that a program will not be able to write to memory that is beyond the end of the array. In the string_print program, a function called UartTxMsg() is used to send the string that was written to by sprintf() out of the serial port for display in the terminal program and is shown below. void UartTxMsg(char *msg) { int ch_num = 0; } while (msg[ch_num] != 0) { while(!(UCSR0A & 0x20)); UDR0 = msg[ch_num]; ch_num++; } // wait until ready to tx // transmit byte The address of the string to transmit on the serial port is passed to UartTxMsg() and is available in this function as the pointer called msg. Code in the first while loop sends one character from the array or string that msg is pointing to out of the serial port at a time. Array notation is used in the first while loop expression to check for the null-terminating character which signals the end of the string that is being transmitted. Inside the first while loop, a second while loop with no body is used to wait until the transmitter is ready before sending a character from the array to UDR0 of the microcontroller’s USART which will transmit the byte. After a character from the array is transmitted, the index into the array stored in the variable ch_num is incremented to access the next element in the array. In this way, each character in the array that msg is pointing to will be transmitted by the serial port until the null-terminating character is found which will cause program execution to exit the first while loop and the function to return. The function could be written in a more compact way by inserting the post-increment operation on the ch_num variable into the array brackets of the preceding statement. In this case, the function would look as follows. void UartTxMsg(char *msg) { int ch_num = 0; } while (msg[ch_num] != 0) { while(!(UCSR0A & 0x20)); UDR0 = msg[ch_num++]; } // wait until ready to tx // transmit byte Because a post-increment operation is applied to ch_num, this variable will not be incremented until after the statement that it is used in has been executed. ● 220 Chapter 11 • Arrays and Strings 11.6 • Multidimensional Arrays Multidimensional arrays are arrays that contain other arrays and can be two or more levels deep. A two dimensional array can be defined in C as follows: int array[3][4]; This line tells the compiler to make space in memory for three arrays that are each four integers long. Both the array and each element in the array can be accessed using array notation. To assign a value to the second element of the first array, we would write: array[0][1] = 10; A two dimensional array can be initialised when it is defined as follows: int array[3][4] = {{1, 2, 3, 4}, {11, 12, 13, 14}, {21, 22, 23, 24}}; Figure 11-4 shows what this array would look like in memory. Figure 11-4 A Two Dimensional Array In Memory The multi_seq program uses a two dimensional array to store three sets of patterns that are written to four LEDs. A pattern is displayed on the LEDs continually and can be changed to one of the other patterns by pressing a push-button switch connected to one of the port pins. A switch is connected to one of the Arduino pins as previously done in Figure 9-1 on page 163, except that the switch is connected to port pin PC5 which is Arduino pin A5 on the Uno and Arduino digital pin 32 on the MEGA. Four LEDs are connected to port pins PC0 to PC3 using series resistors as before. The circuit shown in Appendix C on page 335 can be built and will be used with many of the programs that follow. Create the project as a new GCC C project and not from the template file. Project name: multi_seq Project location: CH11\multi_seq main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> void GetSwitch(void); int prog_num = 0; int main(void) { unsigned char seq[3][4] = {{0x09, 0x06, 0x0F, 0x06}, {0x08, 0x04, 0x02, 0x01}, {0x08, 0x09, 0x0D, 0x0F}}; int ptrn; DDRC = 0x0F; // PC0 to PC3 are outputs for LEDs while (1) { ● 221 C Programming with Arduino } } for (ptrn = 0; ptrn < 4; ptrn++) { // move through each set of arrays PORTC = seq[prog_num][ptrn]; // write the pattern from the chosen array GetSwitch(); // read switch and delay } void GetSwitch(void) { int del_cnt = 100; } while (del_cnt--) { if (PINC & 0x20) { _delay_ms(40); while (PINC & 0x20); prog_num ++; if (prog_num >= 3) { prog_num = 0; } } _delay_ms(10); } // // // // // read switch value on PC5 debounce switch wait for switch release move to next LED sequence stay within array bounds // delay * del_cnt = LED on time Program Output LEDs flash the pattern from each sub-array. Pressing the switch changes between sub-arrays. Inside main() the program defines and initialises a two-dimensional array called seq[][] that contains three sets of patterns that are to be written to the LEDs. DDRC is used to set up port C pins PC0 to PC3 as outputs that connect to the LEDs and the upper four bits as inputs including PC5 which is connected to a switch. The while(1) loop contains a for loop that continually writes a pattern to the LEDs from the two dimensional array. You have seen this before with a one dimensional array, except that this time the set of patterns that is output from the two dimensional array is selected by pressing a push-button. Global variable prog_num is incremented by GetSwitch() each time that the push button switch is pressed. When prog_num is incremented to 3, it is reset back to a value of 0 which selects the first sub-array in the two-dimensional array ensuring that this variable selects one of the three sub-arrays which will be either 0, 1 or 2. Inside the for loop, the following line of code writes the bit pattern to the LEDs. PORTC = seq[prog_num][ptrn]; In this line of code, prog_num stays the same until the push-button switch is pressed and has an initial value of 0 which selects the first sub-array. As the for loop increments the value of ptrn each time through the loop, each pattern from the first sub-array is written to the port C LEDs in turn. When the push-button is pressed for the first time, prog_num is incremented and the for loop now sends each bit pattern from the second sub-array to port C. The second time that the push-button switch is pressed, bit patterns from the third sub-array are written to the port C LEDs. The next time that the push-button is pressed, prog_num is reset to 0 and the first sub-array bit patterns are sent to port C again. ● 222 Chapter 11 • Arrays and Strings The GetSwitch() function serves three purposes – it provides a delay between each bit pattern written to the LEDs, it checks whether the push-button switch has been pressed and it provides switch debouncing. The while loop in GetSwitch() together with _delay_ms() create the delay time for the LED pattern. The delay function is set to delay for 10ms times 100 passes through the loop which creates a 1000ms or 1s delay. The delay was created this way so that the state of the push-button switch can be checked every 10ms, otherwise the program may not see that the switch was pressed if the delay between checking the switch state is too long. The following line of code is used to check if the push button switch is closed. if (PINC & 0x20) { This code uses a mask number to check if bit 5 of the PINC register is set which corresponds to port pin PC5. If the switch is closed, bit 5 will be set and the code in the body of the if statement is run. The next two lines of code, shown below, are used to debounce the switch and cause the program to wait until the switch is released or opened before the rest of the program is run. _delay_ms(40); while (PINC & 0x20); Switch bounce is caused when a switch is closed and the switch contacts do not close and stay closed, but actually "bounce" for a very short period of time. By bounce, we mean that the switch contacts close and open a few times until they finally settle in the closed position. Switch contact bouncing is due to the mechanical nature of the switch and occurs with all mechanical contact switches. It is necessary to debounce the switch in software by providing a small delay after the switch is closed that allows the switch contacts to settle – this is the 40ms delay in the above code. If the switch is not debounced, it appears to the microcontroller that the user has closed and opened the switch a few times, which will cause the LED pattern displayed to skip over one or two of the patterns in this program. The while loop shown in the above code waits until the user releases the switch, which will cause the loop expression to evaluate to false, before the next line of code is run. Holding the switch closed will cause the currently displayed LED pattern to freeze on the LEDs until the switch is released. After the switch is released, the global variable called prog_num that is used to access sub-arrays is incremented and kept in range so that it is not able to try and access a subarray that does not exist. The pattern selected by each switch press is done in the array as shown in Figure 11-5 on page 224. The global variable prog_num can be seen by every function in the source file so does not have to be passed to the function as an argument or returned by the function. ● 223 C Programming with Arduino Figure 11-5 Accessing Elements in a Two-Dimensional Array The next example program, status_array, shows how an array of strings is initialised and used for displaying status messages of an imaginary embedded controller system. Create this project using the template file. Project name: status_array Project location: CH11\status_array main.c #include <stdio.h> #include "stdio_setup.h" #define NUM_MSGS 5 int main(void) { int msg_num = 0; char status[NUM_MSGS][25] = {"Belt Jam Alarm.", "Conveyor Running.", "Conveyor Stopped.", "Out of Paper.", "System Ready."}; } UartInit(); for (msg_num = 0; msg_num < NUM_MSGS; msg_num++) { printf("%s\r\n", status[msg_num]); } while (1); Program Output Belt Jam Alarm. Conveyor Running. Conveyor Stopped. Out of Paper. System Ready. The two dimensional array status[][] is initialised with strings which are of course themselves arrays. The size of each string can be 25 characters long including the nullterminating character. For strings that are smaller than this, the extra bytes are wasted. The program uses a for loop to send each string out of the serial port to be displayed in the terminal window. The number that the for loop is counting through is used as an ● 224 Chapter 11 • Arrays and Strings index into the two dimensional array to print each string using printf(). A more efficient method can be used to initialise an array of strings and prevent memory being wasted by having to specify a fixed length for each string. Initialising an array of pointers to strings will not waste any bytes. A modified version of the previous program, called status_point uses an array of pointers to strings. Project name: status_point Project location: CH11\status_point main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int msg_num = 0; char *status[] = {"Belt Jam Alarm.", "Conveyor Running.", "Conveyor Stopped.", "Out of Paper.", "System Ready."}; } UartInit(); printf("Number of strings: %u\r\n\r\n", sizeof(status) / 2); for (msg_num = 0; msg_num < sizeof(status) / 2; msg_num++) { printf("%s\r\n", status[msg_num]); } while (1); Program Output Number of strings: 5 Belt Jam Alarm. Conveyor Running. Conveyor Stopped. Out of Paper. System Ready. It is not necessary to define the size of the top-level array, or number of string in the array, as this can be done by the compiler which allows us to add or remove strings from the array without having to change the array size, as this is done automatically. The array definition from the status_array project is changed from the following: char status[NUM_MSGS][25] To the following in the status_point project: char *status[] The changed line simply states that we want an array called status[] and that data stored in the array is going to be of the type pointers to characters (char *). The pointers in the array are 16-bit pointers and they point to character arrays or strings. ● 225 C Programming with Arduino The number of strings that the array is pointing to can be calculated by finding the number of bytes in the status[] array which is the number of bytes that all pointers in the array consist of, and diving this number by two to get the number of two byte or 16‑bit pointers in the array. The number of pointers in the array is the same as the number of strings that they are pointing to, because the array holds one pointer for each string. In the code, the sizeof operator is used to calculate the number of strings as follows: sizeof(status) / 2 Both the first printf() statement and the for loop use the above code to calculate the number of strings that the array points to. Figure 11-6 shows a screen capture from the debugger in Atmel Studio of the status[] array in memory on the Arduino Uno that better illustrates how the array of pointers to strings works. Figure 11-6 The status[] Array in Memory In the figure, it can be seen that the status[] array contains five pointers, or five addresses 0x012D, 0x013D, 0x014F, 0x0161 and 0x016F, but the actual array that holds these five pointers is at address 0x08F2. At the right of the figure is the memory that holds the actual strings that the array is pointing to. The memory box at the right displays the address of the memory on the left, the hexadecimal byte of data at the address and the ASCII character for the byte on the right. Where the byte is a non-printable ASCII character, a dot is inserted. The address of the first string in the array is found at the beginning of the array at address 0x08F2 (or status[0]) and is 0x012D. If we look at the memory at address 0x012D we see the first string "Belt Jam Alarm". The next string address is stored in the second array element, status[1], and is 0x013D. Looking in memory at 0x013D the second string "Conveyor Running." can be seen. And so on for each of the strings that the array elements point to. 11.7 • Exercises 11.7.1 • Button Message Write a program that sends a message to the terminal program that says which button on port pins PC4 and PC5 of the Arduino was pressed. The messages must be stored as an array of pointers to strings. Wire two switches to the Arduino as shown in the circuit in Appendix C on page 335. ● 226 Chapter 11 • Arrays and Strings 11.7.2 • Button Wait Modify the program from the previous exercise to print the message to the terminal window when the button is initially pressed so that the message is only displayed once. Wait until the button is released before checking the button status again. Include switch debouncing in the program. 11.8 • Solutions 11.8.1 • Solution to 11.7.1 Project created using the template file. Project name: CH11_1_btn_message Project location: Exercises\CH11_1_btn_message main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { char *sw_msgs[] = {"BUTTON 1", "BUTTON 2"}; UartInit(); } while(1) { if (PINC & 0x10) { // switch on PC4 printf("%s\r\n", sw_msgs[0]); } else if (PINC & 0x20) { // switch on PC5 printf("%s\r\n", sw_msgs[1]); } } 11.8.2 • Solution to 11.7.2 Project created using the template file. Project name: CH11_2_btn_wait Project location: Exercises\CH11_2_btn_wait main.c #define #include #include #include #include F_CPU 16000000UL <avr/io.h> <util/delay.h> <stdio.h> "stdio_setup.h" void ButtonWait(unsigned char mask); int main(void) { ● 227 C Programming with Arduino char *sw_msgs[] = {"BUTTON 1", "BUTTON 2"}; UartInit(); } while(1) { if (PINC & 0x10) { // switch on PC4 printf("%s\r\n", sw_msgs[0]); ButtonWait(0x10); } else if (PINC & 0x20) { // switch on PC5 printf("%s\r\n", sw_msgs[1]); ButtonWait(0x20); } } void ButtonWait(unsigned { _delay_ms(30); while(PINC & mask); _delay_ms(30); } char mask) // debounce for closing switch // wait for switch release // debounce for opening switch As with most C programs there are many ways to write a program and your solutions to these exercises may differ from the solutions in this book. The important things are that the program must correctly solve the problem and must be easy to read and understand. Always aim to write efficient code that uses as little memory and processing time as possible. 11.9 • Summary ● 228 • Arrays are sets of sequential data types. • Each element in an array can be accessed using array notation. • Strings are arrays of characters that are null terminated. • The address of an array in C is the name of the array. • Arrays can be accessed using array or pointer notation. Chapter 12 • Bit Manipulation and Logical Operators Chapter 12 • Bit Manipulation and Logical Operators What you will do in this chapter: • See how to change individual bits inside a variable by using bitwise operators • Move the contents of a variable left or right using the shift operators • Use logical operators to combine expressions 12.1 • Bit Manipulation with Bitwise Operators Bitwise operators enable a programmer to modify individual bits in a memory location and can be used to operate on bytes or larger groups of bits. The C programming language has four Boolean bitwise operators and two shift operators as shown in Table 12-1. The bitwise AND operator (&) should be familiar to you. Table 12-1 Boolean Bitwise and Shift Operators Boolean Bitwise Operators Operator Operation Example Result & AND 1101 & 0111 0101 | OR 1101 | 1001 1101 ^ XOR (Exclusive OR) 1101 ^ 1001 0100 ~ NOT (Compliment) ~1101 0010 Shift Operators Operator Operation Example Result << Left Shift 1010 << 1 10100 >> Right Shift 1010 >> 1 0101 Windows calculator will again come in handy when learning about bitwise operators as it has all of the bitwise operators available. Figure 12-1 on page 230 shows the Windows calculator and the equivalent C bitwise operators below it. The bitwise operators can be tested with the calculator in binary or hexadecimal mode and it is possible to change between these modes at any time while performing an operation. The keystrokes used to calculate the examples in Table 12-1 on the calculator are shown below from top to bottom – first put the calculator into Programmer mode and then click the Bin radio button. An explanation of each bitwise operator follows. 1101 And 111 = Note that the 0 place holder to the left of the second number is left out when entering the second number and is not displayed on the calculator in the result i.e. the result displayed is 101 and not 0101. 1101 Or 1001 = 1101 Xor 1001 = 1101 Not The result of this operation on the calculator turns all of the invisible 0 place holders to ● 229 C Programming with Arduino the left of the number into 1s and that is why the number is filled with 1s to its left. Click the Byte radio button to shorten the number to 8 bits which is easier to read. 1010 Lsh 1 = (Left shift by 1 bit) 1010 Rsh 1 = (Right shift by 1 bit) Figure 12-1 Bitwise Operations on the Windows Calculator In Windows Calculator, the buttons have the following meaning with the equivalent operators used in C shown in the middle: Or | bitwise OR – the symbol used in C is the vertical pipe found on the \ key Xor ^ bitwise XOR (exclusive OR) Lsh << left shift Rsh >> right shift Not ~ compliment And & bitwise AND Although RoL (rotate left) and RoR (rotate right) operate on bits in Windows Calculator, they do not have an equivalent in the C language. In order to understand what the Boolean bitwise operators do, we need to look at the truth tables of these operators. The truth tables are shown in Figure 12-2. The truth tables show the output of each bitwise operator when it is performed on the input bit(s). Figure 12-2 Truth Tables for the Boolean Bitwise Operators ● 230 Chapter 12 • Bit Manipulation and Logical Operators The NOT operator simply inverts each bit in a byte i.e. it changes all of the 0s to 1s and 1s to 0s. In C, the other three operators are performed on two or more values of length 8 bits (char), 16 bits (short or int) or 32 bits (long). Let’s take the OR operator as an example and use it to operate on two bytes. In C the OR operator can be used as follows: unsigned char byte1 = 0xC1; unsigned char byte2 = 0x13; unsigned char result; result = byte1 | byte2; The above example ORs two variables together and saves the result in a variable called result. The example could also use the two numbers directly i.e. it could be used to OR two constants together and store the result in a variable. Figure 12-3 shows how the OR operation is done. Figure 12-3 The Bitwise OR Operation on Two Bytes Bit 0: 1 and 1 are inputs in the truth table, 1 is the output in the truth table Bit 1: 0 and 1 are inputs in the truth table, 1 is the output in the truth table Bit 2: 0 and 0 are inputs in the truth table, 0 is the output in the truth table etc…. for each bit Each bit of byte1 is ORed with the corresponding bit in byte2 from top to bottom as shown in Figure 12-3. Using the OR truth table from Figure 12-2 on page 230, bit 0 from each byte are ORed together i.e. 1 OR 1 = 1. The result is stored in the corresponding bit of result i.e. bit 0 of result. Bit 1 is done in the same way: 0 OR 1 = 1. Bit 2: 0 OR 0 = 0. And so on for each bit from 0 to 7. The same operation would be done for the AND and XOR if they were used instead of the OR operator, the only difference would be the truth table used and the result produced. 12.1.1 • Why Do We Need Bitwise Operators? Bitwise operators make hardware programming easy to do, as there are many cases where a single bit needs to be changed in order to use a hardware device attached to a microcontroller. Bitwise operators also allow a single bit to be tested as seen in some of the previous code examples that use the AND operator to test the state of a single bit in a byte that was read back from a hardware register. Before looking at the left and right shift operators, we will look at some example programs that use the Boolean bitwise operators. When switching LEDs on and off using the I/O control registers of the AVR microcontroller in previous example programs, we wrote data to all of the bits in each of the registers. Writing to all of the bits in the registers is fine if we want to use all of the bits, but this is not always the case. In cases where only some of the bits in a register need to be ● 231 C Programming with Arduino changed, and the others left in their default state, we can use bitwise operators. 12.1.2 • Using the Bitwise AND and OR Operators The bitwise_port project shows how to set up four pins on port C as outputs and write to LEDs attached to them without changing the values in the other bits of the registers. For this project, attach four LEDs with series resistors to port C pins PC0 to PC3 as previously done using the circuit from appendix C. The upper four pins of this port will not be used and will be left in their default states by using bitwise operators to leave the upper bits in their corresponding registers in their default states. Create this project from scratch (GCC C executable project) and not from the template file. Project name: bitwise_port Project location: CH12\bitwise_port main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC = DDRC | 0x0F; PORTC = PORTC | 0x04; } while(1) { PORTC = PORTC & 0xFC; _delay_ms(1000); PORTC = PORTC | 0x02; _delay_ms(1000); PORTC = PORTC | 0x01; _delay_ms(1000); } Program Output LED on PC3 stays off, LED on PC2 stays on throughout the program. LEDs on PC1 and PC0 flash continually in the following pattern with a delay in between: Both LEDs off PC1 on, PC0 off PC1 and PC0 on At the beginning of main() the bitwise OR operator is used to set the lower bits of DDRC in order to set the corresponding pins PC0 to PC3 as output pins in the following line of code. DDRC = DDRC | 0x0F; Using the bitwise OR operator to OR the contents of DDRC with 0x0F (00001111B) and then saving the result back in DDRC leaves the upper bits of the register, bits 4 to 7, unchanged, but sets the lower bits, bits 0 to 3. From the OR truth table we can see that anything ORed with 1 is 1. All of the lower bits of the number 0x0F are 1, so the result of the OR operation will be all 1s that will be placed in the lower nibble of DDRC. In the ● 232 Chapter 12 • Bit Manipulation and Logical Operators OR truth table we can see that anything ORed with 0 will be itself (0 ORed with 0 is 0; 0 ORed with 1 is 1) which leaves the upper four bits of DDRC unchanged because they are all ORed with the 0 (0000B) in 0x0F. The second line of code in main() is used to switch the LED attached to PC2 on, while leaving all other pins in their default state. In the rest of the program, the LED attached to PC3 is left off, while the LED attached to PC2 is left on in order to demonstrate that bitwise operators used to only switch PC0 and PC1 leave the state of the other bits unchanged. PORTC = PORTC | 0x04; This line of code ORs the contents of PORTC with 0x04 (00000100B) which switches only PC2 on and leaves all other bits in their default state. In the while(1) loop, the bitwise AND operator is used to clear the bottom two bits of PORTC, in other words it is used to switch LEDs attached to PB0 and PB1 off, while leaving all other bits in PORTC in their current state. PORTC = PORTC & 0xFC; ANDing the contents of PORTC with 0xFC (11111100B) clears the bottom two bits of this register and leaves the other bits unchanged. Any number ANDed with 1 will be itself and any number ANDed with 0 is 0, as can be seen in the AND truth table. If any of the upper bits contains a logic 1, it will be ANDed with 1 and the result will be 1. If any of the upper bits contains a logic 0 and it is ANDed with 1, the result is 0. The lower two bits will always be zero because they are ANDed with 0. In the following OR operation, bit 1 is set, switching the LED attached to PC1 on. All other LEDs and bits in the PORTC register remain unaffected. PORTC = PORTC | 0x02; Bit 0 in PORTC is set using the bitwise OR operator, which switches the LED attached to pin PC0 on in the line of code below. All other bits in PORTC are unaffected, which leaves the PC1 LED on. PORTC = PORTC | 0x01; In the bitwise operations in the above program, the combination of bitwise operator and number used, changed the value of specific bits, while leaving all other bits unchanged in the register being operated on. AND operators are used to clear bits while OR operators are used to set bits. In this program a read-modify-write is performed on the registers when the bitwise operators are used. For example in the line: PORTC = PORTC | 0x04; The contents of the PORTC register are first read by the microcontroller – this moves the value in the PORTC register into an internal register of the microcontroller (read). The OR operation is then performed (modify) and the result is written back to the PORTC register (write). There is a shorthand way of writing the above line: ● 233 C Programming with Arduino PORTC |= 0x04; The |= is known as a bitwise assignment operator. This operator ORs the two values at the left and right of it together and then stores the result in the variable or register on the left of it. Table 12-2 on page 238 shows the C assignment operators. 12.1.3 • Using the Bitwise NOT Operator The next program shows the use of the bitwise NOT operator, also known as the compliment operator. The not_port project uses the bitwise NOT operator to invert (change 0s to 1s and 1s to 0s) the value written to the port and displayed on the LEDs. The same four LEDs on pins PC0 to PC3 are used as the previous project. Create the project without the template file. Project name: not_port Project location: CH12\not_port main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { unsigned char LED_pattern = 0x0A; DDRC = DDRC | 0x0F; } while(1) { PORTC |= LED_pattern; _delay_ms(1000); LED_pattern = ~LED_pattern; LED_pattern &= 0x0F; PORTC &= 0xF0; } // display the pattern // invert the LED pattern // clear the top bits // clear the LEDs Program Output The following two binary patterns are displayed on the LEDs continually with a delay in between. 1010 0101 This program writes a bit pattern to the LEDs, then uses the bitwise NOT operator to invert the bit pattern and write the new inverted bit pattern to the LEDs. In the while(1) loop of this program, the number 0x0A (1010B) stored in the LED_pattern variable is written to the LEDs followed by a delay. Using the OR bitwise assignment operator writes the number to the lower nibble of PORTC leaving the upper nibble unchanged. After the delay function is called, the value in LED_pattern is inverted as follows. LED_pattern = ~LED_pattern; ● 234 Chapter 12 • Bit Manipulation and Logical Operators This line of code inverts all of the bits in LED_pattern and stores the result back in LED_pattern. LED_pattern is 8 bits wide and holds the number 0x0A (00001010B), which, when inverted becomes 0xF5 (11110101B). If this number is written to PORTC using a bitwise OR operation, the upper four bits in PORTC will be set which we do not want. The next line of code uses the bitwise AND assignment operator to clear the upper four bits in LED_pattern. LED_pattern &= 0x0F; LED_pattern now holds the number 0x05 (00000101B) which can be used to change the lower four bits of PORTC, leaving the upper four bits unchanged. If we now use the OR operator to OR 0x05 with PORTC, the OR operation will set bits 0 and 2, but leave bits 1 and 3 set from the first bit pattern written to the LEDs. We therefore use the bitwise AND assignment operator to first clear the lower nibble of PORTC while leaving the upper nibble unchanged as shown in the code below. PORTC &= 0xF0; After this line of code is run, program execution continues at the top of the loop where the new inverted bit pattern is written to the LEDs. Although the above line of code switches the LEDs off for a moment, it is too quick to see and it appears that the bit pattern on the LEDs is inverted immediately. 12.1.4 • Using the Bitwise XOR Operator The bitwise XOR operator can be used to toggle a bit or LED while leaving all other bits in a register unchanged. By toggle we mean that the bit changes state. If the bit is on or 1 and it is toggled, it changes state to off or 0. If the bit is off or 0 and it is toggled, it changes state to on or 1. In the xor_toggle project, a single LED is toggled using XOR to toggle the state of one bit in a register while leaving all other bits unchanged. The LED is toggled in a loop with a delay between toggling which results in the LED being switched on and off continuously. Create the xor_toggle project without the use of the template file. Connect four LEDs to pins PC0 to PC3 as done in previous projects. Project name: xor_toggle Project location: CH12\xor_toggle main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC = DDRC | 0x0F; PORTC |= 0x0C; } while (1) { PORTC ^= 0x01; _delay_ms(400); } ● 235 C Programming with Arduino Program Output The following bit patterns are shown on the LEDs continuously with a delay in between: 1100 1101 In this program, pins PC0 to PC3 are set as output pins using DDRC. The top two of the four LEDs are switched on and the bottom two LEDs are switched off by writing 0x0C to PORTC. This number is written to the LEDs to show that the XOR bitwise operator can be used to toggle a single bit in a register while leaving other bits in the register unchanged. Inside the while(1) loop, the state of bit 0 in PORTC is toggled each time through the loop followed by a delay. The XOR assignment operator toggles bit 0 of PORTC in the following line of code, which toggles the LED attached to PC0. Other LEDs on port C are left unchanged by the XOR operator. PORTC ^= 0x01; The above line of code can also be written as follows. PORTC = PORTC ^ 0x01; The number being XORed with PORTC is 0x01 or 00000001B. In the XOR truth table, we can see that any bit XORed with 0 is itself. All 7 upper bits of the number being XORed with the PORTC register are 0, so all the corresponding bits in the register remain unchanged. Only bit 0 in the number 0x01 has the value 1, so when 1 from the number 0x01 is XORed with the default value of 0 in bit 0 of PORTC, the result is 1 as can be seen in the truth table (0 XOR 1 = 1). After the XOR operation, bit 0 of the number is still 1 and bit 0 of PORTC is now 1, so the next time through the loop 1 is XORed with 1 which results in 0 from the XOR truth table (1 XOR 1 = 0). We are now back to where we started with bit 0 of PORTC equal to 0 and bit 0 of the number equal to 1. The next time through the loop bit 0 in PORTC is changed to 1 again (0 XOR 1 = 1). This continuous XOR operation that occurs each time through the loop results in bit 0 of PORTC being continually toggled and the corresponding LED on pin PC0 being toggled, or switch on and off continually. 12.1.5 • The Left and Right Shift Operators The shift operators will shift the entire contents of a variable by the specified number of bits. The two shift operators can move the bits either to the left (<<) or to the right (>>). If an 8-bit number is shifted by one bit to the left, every bit in the number will move to the cell to the left of it and the LSB will be filled with 0. The MSB will be shifted out of the number and no longer exist as it is replaced by the bit that was at the right of it. The same number shifted to the right by one bit will result in every bit moving to the cell to the right of it and 0 shifted into the MSB. In this case the LSB is shifted out of the number and no longer exists. If an 8-bit number is shifted left or right by 8 bits, all the bits will be shifted out of the register or variable and every bit in the number will be 0. Shifting a number by two bits will shift each bit value by two cells in the register or variable shifting two zeros into the bottom two or top two bits, etc. The next program uses the shift operators to create a Knight Rider display on four LEDs connected to pins PC0 to PC3. Create the project without the template file. ● 236 Chapter 12 • Bit Manipulation and Logical Operators Project name: knight_disp Project location: CH12\knight_disp main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { unsigned char move_left = 1; unsigned char shift_val = 1; DDRC = DDRC | 0x0F; } while (1) { if (move_left) { shift_val = shift_val << 1; if (shift_val & 0x08) move_left = 0; } else { shift_val = shift_val >> 1; if (shift_val & 0x01) move_left = 1; } PORTC &= 0xF0; PORTC |= shift_val; _delay_ms(100); } Program Output Four LEDs display the "Knight Rider" or running LED display. This program uses the move_left variable as a "flag" that is used to flag when the value displayed on the LEDs is to be left shifted and when it is to be right shifted. By shifting a byte that has a single bit that is set, and then writing out this value to the LEDs, we achieve the effect of an LED appearing to run to the left and then to the right – the "Knight Rider" display. The value to write to the LEDs is stored in the variable shift_val and is initialised to a value of 1 i.e. this is a single byte and is initialised to the binary value of 00000001B. The flag variable, move_left is also initialised to a value of 1 (true). When a variable is used as a flag, it is checked in a program for being either true (contains a non-zero value) or false (contains 0). Upon entry into the while(1) loop, the move_left flag causes the code in the body of the if statement to be executed as this flag evaluates to true. The value in shift_val is shifted to the left by 1 bit and the result is stored back in shift_val. The value will therefore have changed from 00000001B to 00000010B. A check is done after this to see if the value 0x08 has been reached i.e. 00001000B. This check is done to see if the fourth bit from the right of this variable is set because we are writing to only four LEDs and if it was not done, the bit would be shifted too far to the left and would not display on the LEDs. The shift_val variable is written to PORTC using the bitwise OR assignment operator so that the shifted value will be displayed on the LEDs. Just before the bitwise OR is ● 237 C Programming with Arduino performed, a bitwise AND is used to clear the lower nibble of PORTC which turns all the LEDs off. After the delay function is called, the if statement is evaluated again. The shift_val variable will be shifted left again in the body of the if statement as before until the value becomes 0x08 (00001000B). The move_left flag will then be cleared and the body of the else statement will be run on the next pass through the loop. The code in the body of the else statement does exactly the same as in the if statement, except that the value in shift_val is now shifted right by one bit and a check is done for the value 0x01 (00000001B) before changing the move_left flag to true (1). 12.1.6 • The C Assignment Operators There is a shorthand method for performing arithmetic and bitwise logical operations on data in C and storing the result in one of the variables that is being operated on. We saw this with the bitwise AND, OR and XOR operators in some of the previous projects. In the previous example program, the following line: shift_val = shift_val << 1; could be changed to: shift_val <<= 1; Both of these lines do exactly the same thing, the second one is just a shorter way of writing it. The value inside shift_val is moved by 1 bit to the left and the result is stored back in shift_val. Table 12-2 shows all of the assignment operators in C with the equivalent operations shown in the Alternate Notation column. Table 12-2 The Assignment Operators in C Operator = += Usage Alternate Notation A=B A += B Meaning The value in B is assigned to variable A A=A+B A is assigned the sum of A and B -= A -= B A=A–B A is assigned the difference between A and B *= A *= B A=A*B A is assigned the value of A multiplied by B /= A /= B A=A/B A is assigned the value of A divided by B %= A %= B A=A%B A is assigned the value of the modulus of A and B &= A &= B A=A&B A is assigned the value of A ANDed with B ^= A ^= B A=A^B A is assigned the value of A XORed with B A |= B A=A|B A is assigned the value of A ORed with B <<= |= A <<= x A = A << x A is assigned the result of left shifting A by x bits >>= A >>= x A = A >> x A is assigned the result of right shifting A by x bits 12.2 • Logical Operators The logical operators in C are represented by symbols similar to the bitwise operators, but instead of operating on single bits are used to operate on the results of expressions used ● 238 Chapter 12 • Bit Manipulation and Logical Operators in decisions and loop statements. We will see what this means when we look at some examples. Table 12-3 shows the logical operators in C. From the table you should recognise the logical NOT operator (!) as we have used it before in example programs. Table 12-3 C Language Logical Operators Operator && Description Example Logical AND || Logical OR ! Logical NOT Result A && B True if A AND B are both true, else false A || B False if both A and B are false, else true !A True if A is false, False if a is true The logical operators use the same truth tables as the bitwise operators in Figure 12-2 on page 230. The next example program, or_keys, shows how the logical operators are used. Create this project using the template file. This program sends a message out of the serial port to be displayed in the terminal window if the ‘a’ or ‘A’ character is sent to it, and will send a different message if the ‘d’ or ‘D’ character is sent. It does not matter whether the Caps Lock key on the keyboard is active or not as the program will perform the same action if the correct upper or lower case character is pressed. This is achieved by using the logical OR operator (||). Project name: or_keys Project location: CH12\or_keys main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { char rx_data; UartInit(); } while(1) { if (UCSR0A & 0x80) { rx_data = UDR0; if (rx_data == 'a' || rx_data == 'A') { printf("\r\nSystem Activated...\r\n"); } else if (rx_data == 'd' || rx_data == 'D') { printf("\r\nSystem Deactivated...\r\n"); } } } Example Program Output System Activated... System Deactivated... Let’s examine the if statement to see how the logical OR operator works: ● 239 C Programming with Arduino if (rx_data == 'a' || rx_data == 'A') { If a character other than ‘a’ or ‘A’ has been received and stored in the rx_data variable, the if statement will evaluate each of the comparative statements which will both result in a value of 0 or false: if (0 || 0) From the truth table 0 OR 0 = 0, so the if statement will evaluate to false and the code in the body of this statement will not be run. Should the value received and stored in rx_data be the character ‘a’, the if statement will evaluate as follows: if (1 || 0) This is because the first comparative statement evaluates to true (1) as rx_data is equal to ‘a’, and the second one evaluates to false. From the OR truth table, 1 OR 0 = 1. The if statement will therefore evaluate to true and the code in the body of this statement will be run. The same happens if rx_data contains the character ‘A’, from the truth table: 0 OR 1 = 1, so the if statement will also evaluate to true in this case and the code in the body of this statement will be run. In this program it is not possible for the combination 1 OR 1 = 1 from the truth table to occur as the variable rx_data cannot contain both the characters ‘a’ and ‘A’ at the same time. The and_button program uses a mix of bitwise operators and logical operators to test if more than one switch is pressed on the embedded system. Create this project without the template file. LEDs are connected to port C pins PC0 to PC3 and switches are wired to pins PC4 and PC5 as done in previous projects and shown in the circuit diagram in appendix C. Project name: and_button Project location: CH12\and_button main.c #include <avr/io.h> int main(void) { unsigned char sw_val; DDRC |= 0x0F; } while(1) { sw_val = PINC; if ((sw_val & 0x10) && (sw_val & 0x20)) { PORTC |= 0x0F; // switch all 4 LEDs on } else { PORTC &= 0xF0; // switch all 4 LEDs off } } ● 240 Chapter 12 • Bit Manipulation and Logical Operators Program Output All four LEDs switch on only if both switches are closed. This program enables the microcontroller to drive four LEDs on port pins PC0 to PC3 and enables the buttons on PC4 and PC5 to be read. In the while(1) loop, the states of the buttons are read and only if both buttons are pressed, will all four LEDs be switched on. If either of the buttons or both are released, the four LEDs will be switched off. In the while(1) loop, the microcontroller PINC register is read which contains the state of the two switches or buttons that are being monitored. The value in this register is stored in the local variable sw_val. The if statement evaluates the state of the switches that are now captured in the sw_val variable. The bitwise AND (&) operations are first evaluated and then these results are evaluated together using the logical AND operator (&&). In the if statement: if ((sw_val & 0x10) && (sw_val & 0x20)) { sw_val & 0x10 is evaluated first and will evaluate to either true or false depending if the switch on PC4 is closed or not. Next, sw_val & 0x20 is evaluated to see if it is true or false depending on if the switch on PC5 is pressed or not. These results are then evaluated using the AND truth table with the result from this truth table allowing the code in the body of the if statement to be run if it evaluates to true (1): 0 AND 0 = 0, no switches are closed, so the if statement evaluates to false. 0 AND 1 = 0, the switch on PC5 is closed, the if statement evaluates to false. 1 AND 0 = 0, the switch on PC4 is closed, the if statement evaluates to false. 1 AND 1 = 1, both switches are closed, the if statement evaluates to true. In the code, the if statement was written with the bitwise operations in their own parentheses to make the code easier to read: if ((sw_val & 0x10) && (sw_val & 0x20)) { The same line of code could have been written as follows, without the extra parentheses: if (sw_val & 0x10 && sw_val & 0x20) { This line of code works exactly the same as the line of code with the extra parentheses but the code with the extra parentheses tends to look less confusing, and clearly shows that the results of two bitwise AND operations are to be evaluated using the logical AND operator. 12.3 • Operator Precedence Operator precedence refers to the order in which expressions with multiple operators are evaluated. For example in the following C statement: x = 5 + 3 * 2; Is 3 added to 5 first and then the result multiplied by 2 (x = 16)? Or is 3 multiplied by 2 and the result added to 5 (x = 11)? C follows the same rules as mathematics in this case where the multiplication sign takes precedence over the plus sign so that 3 is first ● 241 C Programming with Arduino multiplied by 2 and the result is added to 5 so that x is assigned the result of 11. Table 12-4 Operator Precedence Precedence Operator 1 () 2 ++ 3 cast operator 4 * / 5 + - 6 << 7 < 8 == != 9 & 10 ^ 11 | 12 && 13 || 14 ? : (conditional operator) 15 = 16 , (comma operator) [] . -- -> ! ~ + - * & sizeof % >> <= += > -= >= *= /= %= &= ^= |= <<= >>= Table 12-4 contains the operator precedence of the C operators. The only things that you have not seen so far that are in this table are the dot operator (.) and the structure pointer (->). When more that one operator is used in a C statement, they will be evaluated in the order that they appear in this table from top to bottom, for example: if (sw_val & 0x10 && sw_val & 0x20) The bitwise AND operator (&) appears before the logical AND operator (&&) in the table, so the bitwise ANDs in the above line will be evaluated before the logical AND. Parentheses can be used to change the order of precedence as well as make it clearer what you as the programmer are intending. As previously shown, the following line of code does the same as the line of code above. if ((sw_val & 0x10) && (sw_val & 0x20)) This shows that it is intended to first evaluate the bitwise AND operators as everything inside parentheses is evaluated first. In the same way, with our mathematical example: x = 5 + 3 * 2; We can write this instead: x = 5 + (3 * 2); This shows that we intend to multiply 3 by 2 first and then add 5 to the result before assigning the final result to x. We can also use parentheses to change the order of evaluation as follows: ● 242 Chapter 12 • Bit Manipulation and Logical Operators x = (5 + 3) * 2; This forces 5 to be added to 3 first, as the parentheses have higher precedence than the multiplication sign. The result of 5 + 3 (8) is then multiplied by 2. 12.4 • Using Pre-defined Bit Names with Shift Operators Header files that are included with the AVR GCC compiler have pre-defined names for bits found in AVR registers. A program becomes easer to write and read when using bit names instead of writing hexadecimal numbers to registers. Bit names used in conjunction with the left shift operator and the OR and AND bitwise operators can be used to set and clear individual bits in a register. 12.4.1 • Pre-defined Bit Names in C Code The line of code below sets bit 2 of DDRC which configures pin PC2 as an output pin while leaving the other bits in DDRC unchanged. DDRC |= (1 << DDC2); DDC2 is the name of bit number two in DDRC as found in the AVR datasheet and shown in Figure 12-4 which shows both the DDRC and PORT register with the names of each bit. The figure was taken from the ATmega328 datasheet which does not show the name of bit 7 because the corresponding pin, PC7, is not present on the ATmega328. The same registers in the ATmega2560 datasheet show bit 7 in both registers. Figure 12-4 PORTC and DDRC from the AVR Datasheet Showing Bit Names In the above line of code we can see that the result of a left-shift operation is assigned to DDRC using the bitwise OR assignment operator (|=) as we have done previously. To understand what the shift operation is doing, we need to look at what DDC2 is defined as. TIP Clicking on a bit name or register name in a C source code file in Atmel Studio will shown what the bit or register name is defined as in the second box directly above the source code. Right-clicking a bit name or register name in the C source code file will bring up a menu. In the pop-up menu, click Goto Implementation to open up the header file where the bit name is defined and highlight the bit name. ● 243 C Programming with Arduino In the header file iom328p.h, the bit definitions for register bits in the ATmega328P can be seen as shown below. #define #define #define #define #define #define #define DDC0 DDC1 DDC2 DDC3 DDC4 DDC5 DDC6 0 1 2 3 4 5 6 Substituting the defined value for DDC2 into the original line of code, which is what the C preprocessor does, we have the following. DDRC |= (1 << 2); From the above it can be seen that 1 << DDC2 shifts 1 by DDC2 places to the left, which shifts 1 by 2 places to the left. The OR assignment operator then puts the result in DDRC. In this way bit 2 of DDRC is set, which is the same as ORing DDRC with 0x04 (00000100B). The shift operation shifts 0x01 (00000001B) by two places to the left. After one shift to the left, the number becomes 0x02 (00000010B) and after the second shift, the number becomes 0x04 (00000100B). It can be seen that the program is a lot easier to understand by using the name of the bit instead of just a hexadecimal number to set the bit. The bit_names project uses pre-defined bit names to flash a bit pattern on two LEDs connected to port pins PC0 and PC1. Create the project without the template file. Project name: bit_names Project location: CH12\bit_names main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC |= (1 << DDC0) | (1 << DDC1); } while(1) { PORTC |= (1 << PORTC0); _delay_ms(1000); PORTC |= (1 << PORTC1); _delay_ms(1000); PORTC &= ~(1 << PORTC0 | 1 << PORTC1); _delay_ms(1000); } // PC0 on // PC1 on // PC1 and PC0 off Program Output The LEDs flash or blink continually in the following pattern. 01 11 00 ● 244 Chapter 12 • Bit Manipulation and Logical Operators The first line of code in main() sets bits 0 and 1 in DDRC to make pins PC0 and PC1 outputs as shown below. DDRC |= (1 << DDC0) | (1 << DDC1); The code does the bit shift operation as explained above, but does two bits in the same line of code. The result of (1 << DDC0) is bitwise ORed with the result of (1 << DDC1) the result of the OR is then ORed with DDRC. (1 << DDC0) is the same as (1 << 0) because DDC0 is defined as 0, which means that 1 is left-shifted 0 positions resulting in 1 or 0x01 (00000001B) which is what we want for bit 0 of the register. The same line of code could also be written as two separate statements as follows. DDRC |= (1 << DDC0); DDRC |= (1 << DDC1); Using just a hexadecimal number, the equivalent code is: DDRC |= 0x03; Inside the while(1) loop, the two bitwise OR assignment operators in conjunction with shift operators switch the two LEDs on. PORTC |= (1 << PORTC0); _delay_ms(1000); PORTC |= (1 << PORTC1); _delay_ms(1000); The code switches the LED connected to PC0 on first and then the LED on PC1, with a delay in between. In this code the left-shift and OR assignment work the same as before, only the names of the bits change as shown in the PORTC register in Figure 12-4 on page 243. Finally both LEDs are switched off as shown below. PORTC &= ~(1 << PORTC0 | 1 << PORTC1); To switch the LEDs off and leave the other bits in PORTC unchanged, we would normally write 0xFC (11111100B) to PORTC using the AND assignment operator. The result of the shift operations ORed together, (1 << PORTC0 | 1 << PORTC1) gives us 0x03 (00000011B) which is the inverse of what we want. The NOT operator is used to invert 0x03 to 0xFC, thus 00000011B becomes 11111100B in the above code. 12.4.2 • The AVR _BV() Macro A macro called _BV() is defined in the header file sfr_defs.h which is included in a project when the io.h file is included. The _BV() macro is defined as follows. #define _BV(bit) (1 << (bit)) Including Header Files in Header Files A header file can use #include to include other header files. The file io.h can be included ● 245 C Programming with Arduino in an AVR C file for any AVR part number and includes the header file sfr_defs.h at the top of io.h which contains the _BV() macro. After including sfr_defs.h, selective preprocessor directives are used in io.h to select the correct header file that contains register and bit definitions for the part number that was selected when creating the project in Atmel Studio. In this way iom328p.h is included in ATmega328P projects and iom2560.h is included in ATmega2560 projects, each with register and bit definitions for the specific microcontroller. The _BV() macro does a left shift of 1, just as we have done in the previous project, except that the shift is done by the compiler at compile-time and not at run-time on the AVR. The following line of code uses the _BV() macro. PORTC |= _BV(PORTC0); In the _BV() macro, bit in _BV(bit) is substituted into bit in (1 << (bit)) at compile time. In the above line of code, _BV(PORTC0) becomes (1 << (PORTC0)) and the result of this left shift operation is substituted into the code. The AVR Libc reference manual has the following to say about the _BV() macro and the advantage of using it instead of a direct left shift: "The bit shift is performed by the compiler which then inserts the result into the code. Thus, there is no run-time overhead when using _BV()." C Macros In C #define can actually take arguments, as demonstrated by the _BV() macro, allowing a value to be passed to #define. The ability to take an argument allows a powerful macro to be created using ". #define can also be used to define a C statement, for example: #define WARN_1 printf("Warning 1\r\n") Now, whenever WARN_1 is inserted into a C program, the C preprocessor will insert the above printf statement. The same example can be passed a value to print out a warning level as shown below. #define WARN(num) printf("Warning %d\r\n", num) In a C program, the above macro can be called as follows to print out different warning levels. The code can be found with the accompanying files in CH12\macro_demo WARN(1); WARN(2); Will print out: Warning 1 Warning 2 ● 246 Chapter 12 • Bit Manipulation and Logical Operators Compile-time and Run-time Compile-time refers to events or processes that the compiler takes care of when compiling code, rather than events or processes that occur when the code is running on the AVR. Run-time refers to events or processes that occur when code is running on the AVR, rather than events or processes that are handled by the compiler. The bit_names_bv project does exactly the same as the bit_names project, but uses the _BV() macro in place of the left-shift operations. Project name: bit_names_bv Project location: CH12\bit_names_bv main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC |= _BV(DDC0) | _BV(DDC1); } while(1) { PORTC |= _BV(PORTC0); // PC0 on _delay_ms(1000); PORTC |= _BV(PORTC1); // PC1 on _delay_ms(1000); PORTC &= ~(_BV(PORTC0) | _BV(PORTC1)); // PC1 and PC0 off _delay_ms(1000); } Program Output The same as the bit_names project. 12.5 • Exercises 12.5.1 • Print Binary Number Use the bitwise AND operator and the shift operator to check if each bit in a byte is a 0 or a 1. Use the results of the check to create the binary equivalent of the number and send it to be displayed in the terminal window. ● 247 C Programming with Arduino 12.6 • Solution 12.6.1 • Solution to 12.5.1 Project created using the template file. Project name: CH12_1_print_num Project location: Exercises\CH12_1_print_num main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { unsigned char num = 0xA3; unsigned char mask = 0x80; char str[20]; unsigned char str_index; UartInit(); /* count through each bit in the byte and build * * a string that represents the binary number in * * the byte */ for (str_index = 0; str_index < 8; str_index++) { if (mask & num) { /* check for 1 */ str[str_index] = '1'; /* bit is a 1 */ } else { str[str_index] = '0'; /* bit is a 0 */ } mask >>= 1; /* for checking next bit in byte */ } str[str_index] = 0; /* null terminate the string */ printf("0x%02hX = %sB\r\n", num, str); } while(1); This program uses a for loop to count through each of the bits in the byte that is to be displayed in binary format. A variable is set up as a mask with its most significant bit set. The program uses the mask to check whether each bit in num is a 1 or a 0 by shifting the mask by 1 bit to the right after each check. In other words it is used to check the state of bit 7, bit 6, bit 5, etc. and finally bit 0. A string is built up of ASCII characters ‘1’ and ‘0’ after each bit state in the number is checked. The string is null terminated when the for loop exits and then sent out of the serial port for display in the terminal window. 12.7 • Summary ● 248 • Bitwise operators can be used to change the state of individual bits in variables and registers, and to check the state of bits. • The shift operators in C can be used to shift the contents of a variable or register left or right by a specified number of bits. Chapter 12 • Bit Manipulation and Logical Operators • Logical operators are used to combine more than one expression. • Operator precedence in C determines what is evaluated first. Operator precedence can be changed by using parentheses. • Header files included with the AVR GCC compiler include pre-defined bits, registers and macros that can be very useful to programmers and make programs easier to read. ● 249 C Programming with Arduino ● 250 Chapter 13 • Programming Hardware Chapter 13 • Programming Hardware What you will do in this chapter: • Use the hardware / timer counter in the AVR to make accurate time delays • Read external analogue values using the analogue to digital converter • See how the watchdog timer works • Look at I/O ports in more detail We have covered most of the C language up to now. This chapter deals with using this knowledge to program more hardware devices on the microcontroller, namely, timers, the watchdog timer and the analogue to digital converter (ADC). We also take a look at I/O ports in more detail. After learning the C language, moving from one embedded system to another or one microcontroller architecture to another is all about learning how the new hardware works and then programming it. Because this is a book on learning the C language, we will not cover every detail of the microcontroller hardware, but rather demonstrate how various hardware devices are set up and used. 13.1 • Timer Counter Hardware The timer counters (TC) in AVR microcontrollers can be used for multiple purposes such as event counting, pulse generation, delay timing and more. More information about the timer/counters can be found in the AVR datasheet for the specific AVR being used. Microcontrollers on the Arduino Uno and Arduino MEGA 2560 have the following on-chip timer/counters. Arduino Uno (ATmega328P) • Timer/Counter0, 8-bit • Timer/Counter1, 16-bit • Timer/Counter2, 8-bit Arduino MEGA 2560 (ATmega2560) • Timer/Counter0, 8-bit • Timer/Counter1, 16-bit • Timer/Counter2, 8-bit • Timer/Counter3, 16-bit • Timer/Counter4, 16-bit • Timer/Counter5, 16-bit A hardware timer works by feeding a clock pulse into it which causes it to count up every clock cycle. The count value is stored in one of the timer's registers and updated by the hardware every clock cycle. A C program can read the value in the count register which is a 16-bit value in a 16-bit timer, or an 8-bit value if the timer is an 8-bit timer. If we had a 1Hz clock fed into a timer, then a count would occur every 1s because 1Hz is one clock cycle per second. A 5s time period could be generated by starting the timer and checking when the timer reaches a count of 5. Microcontrollers usually have clock sources that ● 251 C Programming with Arduino are a lot faster than 1Hz and are most often derived from the system clock which would normally be in the MHz range. If a 16MHz system clock is fed into a 16-bit timer, the timer would overflow after approximately 4ms. An overflow occurs when the timer counts from zero to its maximum value of 65535 and then wraps around to zero again. To get a longer time period from a 16-bit timer, a prescaler is used to divide the system clock down into a smaller value before being fed into the timer. The prescaler can be enabled from one of the timer / counter registers and has a number of different values that it can divide the system clock by. The duration or time period of a clock cycle can be calculated using the following formula: t= 1 f Where : t =time period ƒ =clock frequency For a 1Hz clock, the calculation is very simple: 1 = 1s 1 Hz = t A 16MHz clock has a period of: t= = 1 f 1 16 MHz = 1 16, 000, 000 = 0.0000000625 = 62.5 ns A 16MHz clock has a period of 62.5 nanoseconds. In a timer, we can easily generate a specific time period once we know how long one clock cycle takes. The timer will increment every clock cycle or clock period, therefore: ttimer = tperiod x count Where : ttimer = period generated by timer tperiod = clock period of clock fed into timer count = timer count The timer can therefore time in multiples of the clock period from the clock source that it is using. 13.1.1 • Polled Timer Time Delay The example program called timer that follows uses timer/counter1, which is a 16-bit timer/counter found on both the Uno and MEGA AVRs, for generating a delay period much like the Delay() and _delay_ms() functions that we have used in previous programs. Using the hardware timer to generate a delay is a much more accurate method than ● 252 Chapter 13 • Programming Hardware using a software delay function. Create the project using the template file. Project name: timer Project location: CH13\timer main.c #include <avr/io.h> #include <stdio.h> #include "stdio_setup.h" int main(void) { unsigned int seconds = 0; OCR1A = 15624; TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); UartInit(); } while(1) { if (TIFR1 & (1 << OCF1A)) { printf("Elapsed seconds = %u\r\n", ++seconds); TIFR1 = (1 << OCF1A); } } Sample Program Output Elapsed Elapsed Elapsed Elapsed Elapsed ... seconds seconds seconds seconds seconds = = = = = 1 2 3 4 5 Although timer/counter 1 has nine registers, we only need to use three of these registers to set up the timer to generate a timing delay. Figure 13-1 on page 254 shows the three registers from the AVR datasheet used in the timer project. In this project, the prescaler is set up to divide the system clock by 1024. The Arduino boards with their 16MHz system clock will therefore be clocking the timer at a frequency of 16MHz divided by 1024 which results in a 15.625kHz clock frequency. The period of this divided clock frequency, or the amount of time for each count of the timer is calculated as follows: t= 1 f 1 15625 = 64 µ S = Each count of the timer takes 64 microseconds. ● 253 C Programming with Arduino Figure 13-1 Timer / Counter 1 Registers Used for Polled Timing The timer can be set up in many different modes, depending on what it is required for. In this project the timer is set up in CTC mode (Clear Timer on Compare match) which means that we write a count value to a register and when the timer reaches the count value it sets a bit in a register to flag that the count has been reached and then clears its count and starts counting from 0 again up to the specified count. The specified count that the timer must count to before clearing itself and flagging the event is set in the OCR1A register in the above code as follows. OCR1A = 15624; OCR1A is a 16-bit register which is shown as two 8-bit registers in Figure 13-1 because the AVR is an 8-bit microcontroller and can only access 8 bits at a time. The above C code takes care of writing to the two 8-bit registers in one line of code. In this line of code, the timer is set up as a one second timer for a system with a 16MHz clock using a prescaler that divides by 1024. From the above calculation, we know that each count of the timer will take 64µS. To calculate the number that the timer must count up to for a 1 second time period, we divide 1 second by 64µS to get 15625 and then subtract 1 to get 15624. The equation is shown below. ttimer count = −1 tperiod where : ttimer = period generated by timer tperiod = clock period of clock fed into timer count = timer count One is subtracted from the result because the timer takes one clock period to transition from the value that it counts up to back to zero, which adds one extra clock period to the timer time. We must therefore subtract one clock period or one count from the calculated count. As an example, if the clock period was 1ms and we wanted to create a time period of 3ms and set the count to 3, the following would happen: a count from 0 to 1 = 1ms elapsed, count 1 to 2 = 2ms elapsed, count 2 to 3 = 3ms elapsed, count 3 back to 0 = 4ms elapsed instead of 3ms. We therefore need to subtract 1 and use a count of 2. After setting up the count value in OCR1A, the next line of code puts the timer into CTC mode and sets the prescaler to divide the clock by 1024 which also starts the timer. TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); ● 254 Chapter 13 • Programming Hardware Setting bit WGM12 in TCCR1B puts the timer into CTC mode. Setting bits CS12 and CS10 in TCCR1B sets the prescaler to divide the system clock by 1024 and feeds the resulting clock to the timer. The above settings and bit meanings are all obtained from the AVR datasheet. Once the timer has been set up as required and started, the code in the while(1) loop polls the OCF1A bit in the timer's TIFR1 register to see whether the calculated time period has elapsed. while(1) { if (TIFR1 & (1 << OCF1A)) { printf("Elapsed seconds = %u\r\n", ++seconds); TIFR1 = (1 << OCF1A); } } Poll means that the software continually checks the bit to see whether it has been set – this is done in the above if statement. The OCF1A bit will be set when the timer has counted up to the value set in OCR1A which signals that the timer has reached the set timer period of 1 second in this example. When the OCF1A bit is set, the current number of elapsed seconds in the seconds variable is incremented and printed to the terminal window. The OCF1A bit must be cleared by writing 1 to it in the TIFR1 register so that the if statement does not immediately evaluate to true the next time through the while(1) loop. The timer will start counting from 0 again and set the OCF1A bit after another second has elapsed. Polled vs. Interrupt Driven Hardware Polled hardware or polling hardware means that software is used to continually check a hardware device's register for a change of state that signals that the hardware needs to be serviced by the software. Servicing the hardware could mean reading a received byte from a UART, or performing some action after a timer has timed out. Interrupt driven hardware is an alternative to polling hardware and uses a microcontroller's interrupt system to automatically run a function, called an interrupt service routine or ISR, when a particular hardware event occurs. The advantage of interrupt driven hardware is that the microcontroller does not spend time polling a register which frees up the microcontroller's processing time. Interrupts are explained in Chapter 15. 13.1.2 • Hardware Timer Function In the next project called hw_timer, timer / counter 1 is used in the same mode as the previous project to create a hardware timer function. A function called TC1Delay() uses timer/counter 1 to generate a delay of 1ms which it uses to create time delays that are multiples of 1ms. Create this project without using the template file. Connect an LED to port C pin PC0 as done before. The LED is flashed on and off to demonstrate the time delay. ● 255 C Programming with Arduino Project name: hw_timer Project location: CH13\hw_timer main.c #include <avr/io.h> void TC1Delay(unsigned long ms_delay); int main(void) { DDRC |= (1 << DDC0); } while(1) { PORTC ^= (1 << PORTC0); TC1Delay(500); } // LED on PC0 // toggle LED // 500ms delay void TC1Delay(unsigned long ms_delay) { OCR1A = 15999; TCCR1B = (1 << WGM12) | (1 << CS10); } while(ms_delay) { if (TIFR1 & (1 << OCF1A)) { TIFR1 = (1 << OCF1A); ms_delay--; } } // stop timer TCCR1B = 0x00; Program Output LED on PC0 flashes on and off every second – 500ms on, 500ms off. TC1Delay() sets up timer / counter 1 with no prescaler value, which supplies the timer with the system clock of 16MHz. The value written to OCR1A sets the timer time period to 1ms. The value passed to variable ms_delay in the timer function is used to create a timer period that is a multiple of the timer / counter 1 period of 1ms. 13.2 • The Analogue to Digital Converter (ADC) The ADC converts an external analogue signal or voltage level placed on one of the AVR's ADC pins to a digital number that the microcontroller can read and store in a variable. A reference voltage used by the ADC allows us to calculate how many volts each count in the number read from the ADC represents. The voltage of the external analogue signal can be calculated by multiplying the number read from the ADC by the voltage that each count in the number represents. The external voltage applied to an ADC pin must be less than or equal to the reference voltage. On the Arduino Uno and MEGA, the default ADC reference voltage used is the 5V power supply voltage which comes from either the USB 5V or 5V from the regulator circuit when using an external power supply. A pin called AREF on the AVR microcontroller is connected to one of the pin headers of the Arduino where it is also called AREF. The AREF pin allows an external reference voltage to be used with the ADC. ● 256 Chapter 13 • Programming Hardware ADC resolution is the number of bits that make up the digital number that represents the analogue value or signal being measured. Both the ATmega328P and the ATmega2560 have 10-bit ADCs which means that the biggest number that can be read from the ADC is 1023 (210 – 1). This number represents the maximum voltage that can be applied to an ADC input pin and is the same voltage as the ADC reference voltage AREF. In other words if a value of 1023 is read from the ADC, we know that the voltage being measured is the same as the reference voltage. To calculate the voltage that each count of the ADC represents, we divide the reference voltage by 210 as shown in the following calculation. The voltage of the analogue voltage on an analogue pin can be calculated as follows. In the above equation, count is the number read from the ADC in software. The above two steps can be simplified into the single equation: Example with ADC count value read from ADC equal to 300 and reference voltage of 5V: The Arduino boards / AVR microcontrollers have the following ADC features. Arduino Uno (ATmega328P) • 10-bit resolution ADC • Multiplexed ADC channels • 6 ADC channels ADC0 to ADC5 on the AVR mapped to Arduino pins A0 to A5 • All ADC pins are found on port C pins PC0 to PC5 • On-chip temperature sensor on ADC8 • ADC reference voltage selectable between VCC, VREF or internal 1.1V reference Arduino MEGA 2560 (ATmega2560) • 10-bit resolution ADC • Multiplexed ADC channels ● 257 C Programming with Arduino • 16 ADC channels ADC0 to ADC15 on the AVR mapped to Arduino pins A0 to A15 • ADC pins are found on port F pins PF0 to PF7, and port K pins PK0 to PK7 • ADC reference voltage selectable between VCC, VREF or internal 1.1V or 2.56V references The analogue project reads the voltage on ADC channel 0, which is connected to Arduino pin A0, and displays the ADC count value as well as the calculated voltage for the voltage on A0. Create this project using the template file and use the terminal program to see the output from the ADC. To change the voltage on A0, a potentiometer can be used with one outer pin connected to GND, the other outer pin connected to 5V and the centre wiper connected to A0. Alternatively, a jumper wire can be used to connect A0 to GND to see the ADC display 0, to the Arduino 3.3V pin to see a value of about 676 and to 5V to see the maximum ADC value of 1023. Project name: analogue Project location: CH13\analogue main.c #define F_CPU 16000000UL #include #include #include #include <avr/io.h> <util/delay.h> <stdio.h> "stdio_setup.h" int main(void) { unsigned int ADC_data; float ADC_voltage; ADMUX = (1 << REFS0); // 5V supply used for ADC reference, select ADC channel 0 DIDR0 = (1 << ADC0D); // disable digital input on ADC0 pin // enable ADC, start ADC, ADC clock = 16MHz / 128 = 125kHz ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); UartInit(); } while(1) { if (!(ADCSRA & (1 << ADSC))) { // check if ADC conversion is complete ADC_data = ADC; // get the ADC value ADC_voltage = 5.0 * ADC_data / 1024.0; // calculate the ADC voltage printf("ADC = %4d 0x%03x %0.3f\r\n", ADC_data, ADC_data, ADC_voltage); ADCSRA |= (1 << ADSC); // start the next ADC conversion _delay_ms(1000); } } Example Program Output ADC = 482 0x1e2 2.354 ADC = 483 0x1e3 2.358 ... ● 258 Chapter 13 • Programming Hardware Again, it is essential to look at the datasheet to see which registers need to be used to set up the ADC and what the meaning of each bit in the registers is. The ADMUX register is used to select the voltage reference used with the ADC, and which ADC channel to read a value from. Selectable voltage references are AREF from the AREF pin, AVCC which is the 5V supply on the Arduino, or the 1.1V internal reference. The Arduino MEGA can also select a 2.56V internal reference. In the above code, AVCC is selected which uses the 5V supply as the ADC reference voltage. ADMUX is also used to select which analogue channel to read an analogue value from and convert to a digital value. In the code, ADC0 is selected by setting the lower four bits of ADMUX to 0. The ADC has multiplexed analogue inputs which means that only one of the analogue input pins can be read by the ADC at a time because all of the ADC channel pins share the same ADC. To read analogue values from two different channels, one channel must be selected and the analogue to digital conversion done. The second channel must then be selected and the analogue to digital conversion done on it. In this way the analogue input channels are time-shared by the ADC. Pins on AVR microcontrollers can be set up as inputs, outputs or used for an alternative function such as an ADC input. When a pin is used as an ADC channel input, the digital input circuitry is no longer needed on the pin. This digital input circuitry can optionally be disabled which will save some power on the AVR. Digital Input Disable Register 0 (DIDR0) is used to disable the input buffer circuitry of the ADC0 pin in the following line of code. DIDR0 = (1 << ADC0D); Before entering the while(1) loop, the ADC clock source is selected, the ADC is enabled and started using the code below. ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); Bits ADPS0 to ADPS2 select the ADC prescaler division factor to divide the system clock by 128. This divides the 16MHz Arduino system clock by 128 to get 125kHz which is used as the ADC clock. The ADEN bit is used to enable the ADC and the ADSC bit is used to start the ADC conversion. The if statement in the while(1) loop checks to see when the ADC has finished converting the analogue value to a digital value. When the conversion is complete, the ADSC bit in ADCSRA changes state from logic 1 to 0. The if statement uses the NOT operator so that the expression evaluates to true when the ADSC bit is 0 or false as shown in the code below. if (!(ADCSRA & (1 << ADSC))) { When the ADC conversion is complete, the ADC digital value found in the ADC register is saved to the ADC_data variable. ADC_data = ADC; The voltage on ADC0 or Arduino pin A0 is calculated using the equation shown earlier in this section which looks as follows in C. ADC_voltage = 5.0 * ADC_data / 1024.0; ● 259 C Programming with Arduino printf() is used to print out the raw ADC value in decimal and hexadecimal, and also to print out the calculated voltage from the above equation. After printing out the ADC value, the ADC conversion is started again to convert the voltage on ADC0 again. A delay after starting the next ADC conversion prevents too many consecutive messages being printed to the terminal program. Polling of the hardware was used again in this ADC code by continually checking the state of a bit in the if statement of the program. 13.3 • The Watchdog Timer A watchdog timer is a timer that is started at the beginning of a program and reset periodically in the program to prevent it from timing out. If the software program should hang up then it will not be able to reset the watchdog timer which will then time out. When the watchdog times out it will reset the microcontroller, breaking the program out of the locked up state. Create the watchdog project using the template file to see how the AVR watchdog timer works. Project name: watchdog Project location: CH13\watchdog main.c #define #include #include #include #include F_CPU 16000000UL <avr/wdt.h> <util/delay.h> <stdio.h> "stdio_setup.h" int main(void) { unsigned int count = 0; wdt_enable(WDTO_8S); UartInit(); printf("\r\n\r\n** Beginning of program **\r\n"); printf("Entering loop that resets watchdog...\r\n\r\n"); /* watchdog timer is reset in this loop */ while(1) { _delay_ms(1000); printf("\rcount = %4d", count++); wdt_reset(); if (count > 20) { count = 0; break; } } } printf("\r\n\r\nNow not resetting watchdog...\r\n"); printf("Expect the microcontroller to be reset...\r\n\r\n"); /* watchdog timer is not reset in this loop */ while (1) { _delay_ms(1000); printf("\rcount = %4d", count++); } ● 260 Chapter 13 • Programming Hardware Sample Program Output ** Beginning of program ** Entering loop that resets watchdog... count = 20 Now not resetting watchdog... Expect the microcontroller to be reset... count = 7 ** Beginning of program ** Entering loop that resets watchdog... count = 1 At the beginning of the watchdog program, the watchdog timer is enabled with an 8 second time-out value as shown in the following line of code. wdt_enable(WDTO_8S); In order to use the watchdog timer in a C program, the header file wdt.h must be included in the C source code file. When the wdt.h file is included, then the watchdog functions and definitions are available in the program. The watchdog timer has its own internal oscillator running at 128kHz and uses a prescaler to set its time-out period. Below are the various time-out periods in milliseconds and seconds that the watchdog can be set to using the wdt_enable() function. WDTO_15MS WDTO_30MS WDTO_60MS WDTO_120MS WDTO_250MS WDTO_500MS WDTO_1S WDTO_2S WDTO_4S WDTO_8S After enabling the watchdog timer, the program prints some messages to the terminal window and then enters the first while(1) loop. Inside the first while(1) loop a count value is printed every second and then incremented. The watchdog timer is reset by calling the wdt_reset() function so that the watchdog will not time out and reset the microcontroller. When the value of the count becomes greater than 20, the first while(1) loop is exited using the break statement. The second while(1) loop simulates what will happen if the software hangs up or gets stuck in an endless loop that does not reset the watchdog timer. Code in the second while(1) loop prints the count value and increments it exactly the same way as the code in the first loop did, but it never resets the watchdog timer. After the 8 second watchdog time period has elapsed, the watchdog timer resets the microcontroller which can be seen when the initial messages at the beginning of the program are printed to the terminal window again. The program then starts running from the beginning again. ● 261 C Programming with Arduino 13.4 • I/O Ports Each I/O port of an 8-bit AVR microcontroller has its own set of three registers called the data direction register (DDR), PORT register and PIN register. In this section we look at the I/O ports in more detail which shows additional functionality available when using the three port registers. Additional functionality includes enabling internal pull-up resistors on the port pins and the ability to toggle the state of a port pin using a register. We have been using port C of the AVR microcontroller on the Arduino Uno and Arduino MEGA boards because both these boards have pins from port C that are accessible on headers. When the I/O port registers are used in C programs, their names are followed by the letter of the port being used. When using port C, the register names become DDRC, PORTC and PINC. 13.4.1 • I/O Port Register Functionality Setting up Pins as Inputs and Outputs The DDR register sets a port pin up as an output when a logic 1 is written to the corresponding bit in this register. At power up, all bits in DDR are set to logic 0 and therefore all pins are set up as inputs. Changing the State of a Pin When port pins are set up as outputs and the PORT register is written to, the logic level written to this register appears on the corresponding pin, i.e. writing logic level 1 to a bit in the PORT register drives the corresponding pin high if the pin is set up as an output pin. Writing a logic level of 0 to a bit in the PORT register drives the corresponding pin low if the pin is set up as an output. Enabling the Pull-up Resistor on a Pin Each pin on an AVR port has a pull-up resistor that can be enabled to pull the pin high when it is set up as an input. When port pins are set up as inputs and the corresponding bits in the PORT register are set to logic level 1, pull-up resistors on these pins are enabled. Conversely when port pins are set up as inputs and the corresponding bits in the PORT register are set to logic level 0, pull-up resistors are disabled on these pins. Reading the State of an Input Pin The state of an input pin on a port is found by reading the PIN register and testing the state of the corresponding bit from the PIN register. Using a Register to Toggle a Pin Writing a logic 1 to a bit in the PIN register toggles the corresponding bit in the PORT register and the state of the corresponding pin if it is set up as an output pin. 13.4.2 • Register Pin Toggle The reg_toggle program shows how to toggle the state of a pin using the PIN register as described above. Create the project without the template file. An LED connected to port C pin 0 is used in this program. ● 262 Chapter 13 • Programming Hardware Project name: reg_toggle Project location: CH13\reg_toggle main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC |= (1 << DDC0); while (1) { PINC = (1 << PINC0); _delay_ms(500); } } // LED on pin PC0 // toggle the LED Program Output LED on pin PC0 toggles every 500ms. In this program it can be seen that writing a 1 to PINC toggles the LED on PC0. A 0 is never written to the register and no bitwise operator is used. 13.4.3 • Enabling Pull-up Resistors In the pull_up project below the internal pull-up resistor is enabled on port C pin PC4 which allows a switch to be connected directly between the port pin and GND without any external resistor needed. Figure 13-2 shows the circuit diagram used for the pull_up project. Create the project without using the template file. Figure 13-2 Circuit Diagrams for the pull_up Project for the Uno and MEGA ● 263 C Programming with Arduino Project name: pull_up Project location: CH13\pull_up main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { DDRC = (1 << DDC0); PORTC = (1 << PORTC4); } // LED on PC0, all other pins = inputs // enable pull-up on PC0 while (1) { if (PINC & (1 << PINC4)) { // switch is open PORTC &= ~(1 << PORTC0); } else { // switch is closed PORTC ^= (1 << PORTC0); _delay_ms(100); } } // switch LED off // flash LED Program Output The LED on PC0 remains off when the switch is open. The LED flashes on and off when the switch is closed. At the beginning of this program bit DDC0 in DDRC is set to logic 1 to set pin PC0 as an output. At the same time logic 0 is written to all of the other bits in DDRC making them inputs. Logic 1 is written to bit PORTC4 in the PORTC register to enable the pull-up resistor on pin PC4. In the while(1) loop, bit PINC4 in the PINC register is checked to see whether it is high or logic 1 which indicates that the switch is open. If the switch is open, the LED on PC0 is switched off. If the switch is closed then the if statement will evaluate to false and the code in the else block that flashes the LED on and off will run. Note that the switch logic has been reversed from when an external pull-down resistor was used in all of the previous programs that used a switch. Because the internal pull-up resistor is used, when nothing, or an open switch is connected to the pin, a logic 1 will be read on the pin because the resistor pulls the pin high. When the switch is closed, the pin is connected to GND causing the pin state to change to logic 0. If a program needed to perform a task only when the switch is closed then the logical NOT operator could be used in the if statement to invert the logic as the following line of code shows. if (!(PINC & (1 << PINC4))) { In the above line, code in the if block will run when the switch is closed if the switch is configured with a pull-up resistor. ● 264 Chapter 13 • Programming Hardware 13.5 • Summary • In order to program an embedded system in any language, knowledge of the hardware being programmed is required. • Information on a microcontroller's internal hardware is found in the datasheet for the microcontroller. • Information on an embedded system's hardware is found in the circuit diagram for the hardware and the embedded system manufacturer's documentation. • The timer/counter in AVR microcontrollers can be used to generate accurate timing intervals. • An analogue to digital converter (ADC) is used to read external analogue values and convert them into digital numbers. • Polling hardware refers to continually checking the hardware in a program to see if an event has occurred. • A watchdog timer is used to recover from software that hangs up a microcontroller by resetting the microcontroller. • AVR I/O port registers can be used to toggle the state of a pin and enable internal pull-up resistors on input pins in addition to normal I/O pin configuration and operation. ● 265 C Programming with Arduino ● 266 Chapter 14 • Debugging Chapter 14 • Debugging What you will do in this chapter: • Use the AVR Dragon or simulator in conjunction with Atmel Studio to debug a program • Single-step through source code in Atmel Studio • Look inside variables, registers and memory using the debugging tools An AVR Dragon used with Atmel Studio can single-step through C code in a project in order to find program errors as part of the debugging process. Contents of variables, memory and registers can be examined using the AVR Dragon. If you do not have an AVR Dragon, the AVR simulator can be used instead, but changes to externally interfaced hardware like LEDs switching on can not be seen with the simulator. 14.1 • Configuring Arduino Boards for Debugging AVR microcontrollers on the Arduino Uno and MEGA have an on-chip debug system called debugWIRE that interfaces debugging software to the AVR through a device such as the AVR Dragon connected to the ICSP header. In order for debugWIRE to work on the AVR on the Arduino boards, reset circuitry must be disconnected from the reset pin. Modern Arduino boards have a solder link labelled RESET-EN that consists of two solder pads connected by a circuit board track. The track must be cut through in order to disconnect part of the reset circuitry from the reset pin when using debugWIRE. The connection can be remade by soldering the two solder pads together. Figure 14-1 Location of the RESET-EN Solder Pads on the Arduino MEGA 2560 (left) and Arduino Uno (right) Figure 14-1 shows where the RESET-EN solder pads are found. Use a sharp blade from a craft knife or scalpel to cut through the track that joins the two solder pads. Be very careful not to slip and cut other tracks on the board. It is usual to make several cuts on the track to try to remove the track so that the pads are properly disconnected. 14.2 • Debugging Example Program Create the debug program below by starting a new Atmel Studio GCC C executable ● 267 C Programming with Arduino project from scratch without the template file. This project is used to demonstrate how to use the debugger. Use the circuit from Appendix C page 335 for this project. Project name: debug Project location: CH14\debug main.c #include <avr/io.h> void Delay(void); int main(void) { int var = 1; int result, val1, val2; float flt_res; unsigned char LED_val = 0; DDRC |= 0x0F; PORTC |= 0x0A; // four LEDs // switch two LEDs on val1 = 25; val2 = 39; result = val1 * val2; result /= 100; flt_res = result / 100.0; while (!(PINC & (1 << PINC4))); } while (1) { if (PINC & 1 << PINC5) { LED_val = 0; } PORTC &= 0xF0; PORTC |= LED_val; LED_val++; if (LED_val > 0x0F) { LED_val = 0; } Delay(); var++; } // wait for switch press // reset count // display count on LEDs // keep count within range void Delay(void) { volatile unsigned long del = 100000; } while (del) { del = del - 1; } Program Output 1010 is displayed on the LEDs followed by some calculations that can only be seen by using the debugger. When the button on pin PC4 is pressed a count is displayed on the LEDs. Pushing the button on pin PC5 resets the count switching all the LEDs off. ● 268 Chapter 14 • Debugging 14.3 • Switching Compiler Optimisation Off Before compiling the above program, we will switch compiler optimisation off because optimisation tends to optimise certain lines of code away making it impossible to step to certain lines of code or view some of the variables when debugging. To switch compiler optimisation off, click Project → debug Properties on the top menu, where debug is the name of the currently open project. In the page that opens, click Toolchain on the left menu and then Optimization under AVR/GNU C Compiler as shown in Figure 14-2. In the Optimization Level drop-down box, select None (-O0). Click the save button on the toolbar or Ctrl + S on the keyboard to save the changes. The project can now be built. Figure 14-2 Switching Compiler Optimisation Off 14.4 • Enabling debugWIRE on the AVR The default programming mode of the AVR is ISP mode which is used for programming the device only. In order to use the debugging capabilities of the AVR, it must be put into debugWIRE mode which uses the same ICSP header on the Arduino board as ISP mode. To enable debugWIRE mode, a single AVR fuse bit called DWEN must be set. No other changes need to be made to the hardware – the AVR Dragon remains plugged into the Arduino ICSP header as before. To set the DWEN fuse bit and enable debugWIRE mode, click the Device Programming icon on the top toolbar or click Tools → Device Programming on the top menu bar. In the Device Programming dialog box that pops up, make sure that the tool selected is the AVR Dragon and that the interface is set to ISP, then click the Apply button. The Read button under Device signature can be clicked to verify that the AVR Dragon and AVR on the Arduino board can be accessed by reading back the device signature and displaying it in the dialog box. In the left panel of the Device Programming dialog box, click Fuses and then click the DWEN checkbox as shown in Figure 14-3 on page 270. Finally click the Program button so that the fuse is set in the Arduino, and then close the dialog box. Now that the AVR is in debugWIRE mode, ISP mode can no longer be used until the device is switched back to ISP mode. For this reason we need to select debugWIRE as the interface for the AVR Dragon. Click the No Tool button on the toolbar; or on the top menu bar, click Project → <project name> Properties and then click Tool in the left menu of the page that opens. Select AVR Dragon and debugWIRE on the page as shown in Figure 14-4 on page 270 and then save the settings. ● 269 C Programming with Arduino Figure 14-3 Programming the DWEN Fuse to Put the AVR into debugWIRE Mode Once the AVR is using the debugWIRE interface, it can be programmed and debugged as much as needed when doing software development work with the AVR. Only when the development and debugging is finished does the AVR need to be put back into its default ISP mode which is explained in section 14.6 on page 277. Figure 14-4 Selecting the AVR Dragon and the debugWIRE Interface ● 270 Chapter 14 • Debugging 14.5 • Using the Debugger This section contains a series of exercises that go through various capabilities of the debugger in Atmel Studio using the above debug project. 14.5.1 • Starting the Debugger Click the Start Debugging and Break icon on the toolbar, or click Debug → Start Debugging and Break on the top menu bar. This will start the debugger which will then stop the C program at the beginning of main(), allowing us to single step through each line of code from the beginning of the program. A yellow arrow appears at the left of the current point of execution in the code and the line of code that will be executed next is highlighted. 14.5.2 • Single Stepping and Adding Watches Click the Step Over icon on the toolbar (Debug → Step Over on the top menu) or press F10 on the keyboard to step to the first line of code in the project. Click the Step Over icon or press F10 again to execute the following line of code which initialises the variable var with a value of 1. int var = 1; Move the mouse cursor over the variable name in the code and a box will pop up showing the value that the variable has been initialised with. The debugger steps over the next two lines of code because these lines of code only define variables without initialising them. Variables can be "watched" by adding them to the watch window that is found at the bottom left of Atmel Studio when debugging. Right click var in the code and then click Add Watch on the pop-up menu. This adds the variable to the bottom watch window and shows the value that it contains. The value that the variable contains will be updated in the watch window as it is changed in the program when we step over a line of code that modifies the variable. Use the same method to add the variables result, flt_res and LED_val to the watch window as shown in Figure 14-5 on page 272. 14.5.3 • Viewing Hardware Registers To view the registers of internal microcontroller hardware peripherals, click Debug → Windows → I/O on the top menu which will open a floating window displaying microcontroller peripherals. There is an I/O icon on the toolbar, but it is easier to find using the top menu bar at first. Click PORTC in the I/O window to display the port C registers in the bottom section of the window as shown in Figure 14-6 on page 272. Step over the line of code that sets the lower four bits of DDRC and notice that DDRC in the I/O window changes value from 0x00 to 0x0F. Individual bits displayed as squares next to DDRC in the I/O window change to solid red squares indicating that they have changed to logic 1 values. The lower four of these bits change to solid red squares while the upper four bits remain unchanged as we would expect. ● 271 C Programming with Arduino Figure 14-5 Variables Added to the Watch Window Stepping over the line of code that writes to the PORTC register switches two of the four LEDs attached to PORTC on and the same 1010B bit pattern is displayed next to PORTC in the I/O window. The previously changed bits in DDRC now change to solid blue squares and two bits in PORTC change to solid red squares in the I/O window. Solid red squares show the most recent changes to the register and that the values are logic level 1. Figure 14-6 Displaying the AVR Hardware Registers ● 272 Chapter 14 • Debugging 14.5.4 • Viewing and Modifying Variable and Register Values Step over three more lines of code so that the line of code containing the multiplication of two variables is executed. Notice that the value of variable result changes and is highlighted in red in the watch window. Again, red indicates that the value has changed after the most recent step through the code. Stepping over the next line of code that divides result by 100 changes the value in result again so that it is highlighted in red in the watch window. Step over the next line of code that divides result by 100.0 and stores the value of the calculation in flt_res and the value displayed next to result in the watch window changes from red back to black. The value of any variable can also be viewed by hovering the mouse cursor over the variable in the code while debugging. Figure 14-7 Manually Changed Variable Value using the Debugger Variables can be modified in the watch window by double-clicking the value of the variable in the watch window, typing a new value and pressing the Enter key as shown in Figure 14-7 where the value of result is manually changed from 9 to 27. Manually changing the value of a variable can be useful during debugging when a variable does not contain the value expected. The variable can then be manually changed in order to further test the code without having to make changes to the code, recompile and reload the program. Register values can also be changed in the debugger. In the I/O window, click the LSB block in the PORTC register and it will change from logic 0 to logic 1 switching on the LED connected to pin PC0. Clicking the block again will change it from solid blue to a block with a blue outline representing logic 0, at the same time the LED will switch off. The entire value in a register can be changed in the same way as a variable value by doubleclicking the value in the I/O window, typing in a new value and pressing Enter. 14.5.5 • Viewing Variables in Memory Memory can be viewed in the memory window of the debugger, which shows the addresses of bytes in memory, the hexadecimal value of the contents of the memory bytes and the ASCII value of the contents of the memory bytes. Addresses of variables can be seen in the watch window under the Type heading – they appear as a hexadecimal number next to the @ symbol as can be seen in Figure 14-7. In the figure, the variable var can be seen to have the address 0x08ed. To find a variable in memory, use the memory window at the bottom right of Atmel Studio. Select data IRAM in the Memory selection box and then type the address of the variable in the Address box as shown in Figure 14-8 on page 274. Figure 14-8 on page 274 shows two views of the watch window and memory window. The top image in the figure shows the variables in decimal format and the corresponding SRAM memory contents set to display the memory bytes in four columns. Click the small ● 273 C Programming with Arduino button to the very right of the address box to pop out a box that allows the memory to be displayed in different columns. The bottom image in Figure 14-8 shows the variable values displayed in the watch window in hexadecimal format and the value of variable result changed to 0xaabb to help show what it looks like in memory. Right click in the watch window and click Hexadecimal Display in the pop-up dialog box to display variable values in hexadecimal. In the memory window, the address at the top has been changed to 0x08F4, which is the address of variable result. The columns have been set to two bytes wide. It can now be seen how the contents of variable result have been stored in memory. The low byte of the two-byte wide integer result is stored at the address 0x08F4 and contains 0xbb. The high byte of result is stored at the next highest memory location, 0x08F5 and contains 0xaa. This order of byte arrangement is called little-endian where the least significant byte (LSB) is stored at the smallest address and the most significant byte (MSB) is stored at the biggest byte address. Big-endian storage stores the MSB at the smallest address and the LSB at the biggest address. Although an 8-bit microcontroller does not have endianness because it accesses a byte at a time, the compiler must use either little-endian or big-endian storage for variables that are larger than one byte. The choice of little or big-endian for the compiler will probably be influenced by how the internal registers are ordered when a 16-bit value is stored as two bytes – a high byte and a low-byte pair. Internal 8-bit AVR processor registers and hardware registers are ordered in little-endian format when they are grouped as two 8-bit registers to make up a 16-bit register. This exercise of looking at a variable in memory should further help to visualise how variables are stored in memory. In C, the value of a variable is accessed using the variable name, which is the equivalent of the contents of a memory location. Each variable has an address which is the address of the memory that holds the variable’s contents. Figure 14-8 Viewing Variables in Memory 14.5.6 • Stopping Debugging and Setting Breakpoints Stop the debugging session by clicking the Stop Debugging icon on the toolbar or click Debug → Stop Debugging on the top menu bar. We will now set a breakpoint in the code and start debugging again. A breakpoint allows a program to be run without single-stepping until the line of code that is marked with the breakpoint is reached. When the breakpoint is reached, program execution is stopped at the breakpoint allowing the debugger to be used to single-step through code again or to view variables and memory. ● 274 Chapter 14 • Debugging Set a breakpoint by clicking in the grey bar to the very left of a line of code, or by rightclicking a line of code and then selecting Breakpoint → Insert Breakpoint from the popup menu. Set a breakpoint at the line of code that does the floating point division as shown in Figure 14-9. The debugger can now be started by clicking the Start Debugging icon on the toolbar or Debug → Continue from the top menu bar. The program will now run until the breakpoint is reached, after which control is handed over to the Atmel Studio debugger. Breakpoints are often used to check if a certain line of code is reached which may not happen if there is a bug in the program. The breakpoint can then be moved to an earlier line of code to see if it is executed. To remove a breakpoint, simple click the red circle to the left of the line of code that contains the breakpoint or right-click the line of code and choose Breakpoint → Delete Breakpoint from the pop-up menu. To start the debugger and run to a certain line of code, an alternative to using a breakpoint is to use Run To Cursor. Click on a line of code to move the cursor to it and then click the Run To Cursor icon on the toolbar or Debug → Run To Cursor from the top menu. After running to the breakpoint, step over the line of code that does the floating point division. The breakpoint can now be removed. Figure 14-9 Setting a Breakpoint in Atmel Studio 14.5.7 • Viewing Pin State Changes The next line of code to step over is shown below and waits for the push-button switch connected to pin PC4 to be closed. while (!(PINC & (1 << PINC4))); // wait for switch press Open the I/O window again and click PORTC to view the contents of the PINC register. Step over the above line of code to run it. The code runs and waits for the push-button switch to be pressed – press the button to move to the next line of code. After pressing ● 275 C Programming with Arduino the button, the debugger can step to the next line of code and control is handed back to the debugger software. The state of bit 4 in the PINC register can be seen in the I/O window as a solid red block because the value of the bit changed from 0 to 1 when the switch was closed. Step over the next line of code and bit 4 in PINC is shown as a block with a red outline, indicating that its value changed from 1 to 0. Holding the push-button switch closed during any step will change the value of the corresponding bit in PINC in the I/O window. Releasing the switch and then stepping through the code again will update the state of the corresponding bit with logic 0. 14.5.8 • Viewing Variable Value Changes & Setting Conditional Breakpoints Step through the code several times to see the values of variables LED_val and var change in the watch window as they are incremented by the code. When the LEDs are written to using bitwise AND and OR operators, it can be seen that the AND operation clears the LEDs and the OR operation switches LEDs on. The fact that the LEDs switch off for a very short period of time would not normally be visible when the program is running at full speed, but can be seen when using the debugger. A conditional breakpoint can be set that will cause program execution to stop at the breakpoint only when a certain condition is met. We will set a breakpoint on variable var so that when it reaches a value of 10, program execution will stop. Figure 14-10 Setting a Conditional Breakpoint Set a breakpoint next to the line of code that increments var as shown in Figure 14-10. In the lower right window where we viewed memory, click the Breakpoints tab at the bottom of the window. Right-click the breakpoint in the bottom right window and select Settings... from the pop-up menu. In the Breakpoint Settings dialog box, check the Conditions box as shown at the top of Figure 14-10. Next to Conditional Expression, select Is true so that when the conditional expression that we will create next is true, program execution will stop at the breakpoint. A condition to break on must now be set – click the box to the right of Is true and enter var == 10 which will cause ● 276 Chapter 14 • Debugging the program to halt when the variable var contains a value of 10. Click the OK button in the dialog box when done. Run the program by first stopping the debugger using the Stop Debugging button on the toolbar, and then starting it again by clicking the Continue icon on the toolbar or Debug → Continue on the top menu bar. The program will now run until variable var has been incremented to 10, after which the breakpoint will stop the program and hand control over to the debugging software. 14.5.9 • Stepping into a Function We have been stepping over lines of code which will step over any functions and not step into them and show execution of lines of code in the function. Single-step until the cursor is over the Delay() function and then click the Step Into icon on the toolbar or click Debug → Step Into to step into the function. Alternatively press the F11 key on the keyboard. The cursor now moves into the function and we can single-step through lines of code in the function. Add the del variable to the watch window by right-clicking it and selecting Add Watch from the pop-up menu. Step through the code in the while loop in the function to see the del variable decrementing in the watch window. The del variable is initialised to a very large value and it would take a long time to decrement it to zero in order to exit the function. Click the Step Out button on the toolbar, or click Debug → Step Out or press Shift + F11 on the keyboard to step out of the function and back into main(). When code execution continues in main() after returning from the delay function, the local variable del is destroyed and no longer exists. The watch window now shows "Unknown identifier" next to del because it no longer exists. If we were to step back into the delay function, the variable would be created again and initialised and we would be able to view it in the watch window again. 14.6 • Enabling ISP on the AVR / Disabling debugWIRE To enable the default ISP programming on the AVR, debugWIRE must be disabled. While the debugger is still running, click Debug → Disable debugWire and Close from the top menu. If the debugging session is closed, this option will not be enabled on the menu, so the debugger must be started again before disabling debugWIRE. This can be done by clicking the Start Debugging and Break button on the toolbar or on the top Debug menu bar. After changing back into ISP mode, ISP must be selected as the connection method to the AVR in the tool settings. Click the button on the toolbar that displays debugWIRE on AVR Dragon to open the tool settings page, select ISP as the interface and save the changes. 14.7 • Exercises These exercises are just things to try with the debugging tools and do not have any answers. • Put a breakpoint in the body of the if that checks if the switch is closed in the while(1) loop of the program from this chapter. When the switch is pressed program execution will be handed over to the debugger. • Enable compiler optimisation and try using the debugger. ● 277 C Programming with Arduino 14.8 • Summary ● 278 • Debugging tools can be used to help with debugging a program and to examine the contents of variables and memory. • Watches can be set in the debugger to display the contents of selected variables. • Breakpoints set in a program will stop the program and hand control over to the debugger software when program execution hits a breakpoint. • Lines of code can be single-stepped through using the debugger and functions can be stepped into and out of. • AVR microcontrollers on the Arduino Uno and MEGA boards can be debugged by using the on-chip debugWIRE system. • In order to use debugWIRE the DWEN (debugWIRE enable) fuse must be set. Chapter 15 • Interrupts Chapter 15 • Interrupts What you will do in this chapter: • Find out how interrupts work • Use interrupts for servicing hardware instead of using polling • Use timer interrupts Up until now we have been "polling" hardware – continually checking it in our program to see if some condition has occurred. If you look at the timer program in section 13.1.2 on page 255, you will see one of the disadvantages of polling hardware. When the TC1Delay() function is called, it uses up all of the microcontroller’s processing time to check or poll the timer. If this function was to be used in a more useful program, the rest of the embedded system would not be able to respond to user button presses or other hardware that may need to be serviced while this function was running. Interrupts on a microcontroller are available to solve this problem. 15.1 • Handling Interrupts in C There is no standard way of handling interrupts in C. Interrupt configuration depends on on how interrupts have been implemented in the architecture of a specific microcontroller and how interrupts have been implemented in the compiler. Different C compilers for the same microcontroller may use different code to configure interrupts, so always check the compiler documentation to see how to use interrupts. Interrupts can be configured on the microcontroller so that, for example, when using the timer, it can be started and left to run on its own while the main program continues to run. When the timer reaches the time-out value e.g. after 1ms, an interrupt will be triggered. This interrupts the current flow of the main program and a function called an Interrupt Service Routine (ISR) is run in order to service the timer. When the interrupt occurs, the current state of the microcontroller is saved, a jump occurs to the ISR function where code is run that will respond to the timer. When the ISR function has finished running, a jump occurs back to where the main program left off and the saved state of the microcontroller is restored. The program carries on running normally after this until another interrupt occurs. 15.2 • Using the Timer Interrupt The timer_interrupt program uses 16-bit timer/counter 1 to generate an interrupt every second. When the timer time-out is reached after 1s, the interrupt service routine is run which increments a count value representing seconds elapsed. In the while(1) loop of the program an if statement checks if the count value has changed. Only if the count value has changed will the new count value be sent to the terminal window so as not to flood the terminal with count values which would happen if the count value was continually sent in the main loop. Create the timer_interrupt program using the template file. ● 279 C Programming with Arduino Project name: timer_interrupt Project location: CH15\timer_interrupt main.c #include #include #include #include <stdio.h> <avr/io.h> <avr/interrupt.h> "stdio_setup.h" volatile unsigned long g_count_s = 0; int main(void) { unsigned long saved_count = 0; OCR1A = 15624; TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); TIMSK1 = (1 << OCIE1A); sei(); UartInit(); } while(1) { if (saved_count != g_count_s) { printf("%lu\r\n", g_count_s); saved_count = g_count_s; } } ISR(TIMER1_COMPA_vect) { g_count_s++; } Sample Program Output 1 2 3 4 ... In the above code the timer is set up and started as before, but this time it is set to trigger an interrupt every second. When the timer times out and triggers the interrupt, the code that is running in the while(1) loop is "interrupted" and the ISR(TIMER1_COMPA_vect) interrupt service routine is run. This ISR function simply increments the global variable g_count_s by one. When the ISR function returns, the code in the while(1) loop continues running where it left off when the interrupt occurred. Let’s have a look at how the code works. In the following two lines of code timer/counter 1 is set up with a 1 second period, prescaler division of 1024 and CTC mode as done in the polled timer example. OCR1A = 15624; TCCR1B = (1 << WGM12) | (1 << CS12) | (1 << CS10); ● 280 Chapter 15 • Interrupts Bit OCIE1A (output compare A match interrupt enable) in TIMSK1 (interrupt mask register) is set in order to enable an interrupt when the count value of the timer reaches the value in OCR1A which occurs when one second has elapsed in this code. TIMSK1 = (1 << OCIE1A); The sei() macro sets the I flag in the AVR’s status register which globally enables interrupts. Setting the I flag is necessary to enable all interrupts for hardware devices such as the timer or other peripherals that have been enabled using their interrupt enable bits in their interrupt registers. The cli() macro can be used to clear the I bit in the status register which globally disables all enabled interrupts. sei(); In the following code that runs continually in the while(1) loop, a saved copy of the global count variable g_count_s is compared with the global count variable. g_count_s is independently incremented by the interrupt service routine every second. When g_count_s is incremented, it differs from the saved version of this count value in saved_count and the if statement evaluates to true. When the if statement evaluates to true then the new count value is printed to the terminal window and saved in the saved_count variable. This results in the count value only being printed to the terminal window when it changes, which occurs every second. if (saved_count != g_count_s) { printf("%lu\r\n", g_count_s); saved_count = g_count_s; } The AVR GCC library makes the ISR() macro available that marks the interrupt service routine function. The name of the interrupt service routine must be one of the pre-defined ISR names and is TIMER1_COMPA_vect for the timer 1 compare A interrupt in this example. In other words you may not give an ISR a name of your own choice. ISR(TIMER1_COMPA_vect) { g_count_s++; } In this program the ISR simply increments the global count variable g_count_s by one when the ISR is run every second, which occurs when the timer count value reaches the value set in OCR1A. Writing Interrupt Service Routines (ISRs) Always do as little as possible in an interrupt service routine so as not to hold up other code from running in the main loop and to prevent blocking the next interrupt or other interrupts, should they be enabled. As an example if you are using the USART interrupt to receive data, get the data byte in the ISR and save it to a buffer, then set a flag so that the main software knows that data is available. 15.3 • One Millisecond Tick Interrupt A one millisecond tick interrupt is very useful in an embedded system. It can be used to generate different time delays that are multiples of 1ms and be used to create the time ● 281 C Programming with Arduino period for debouncing a switch amongst other applications. Create the int_1_ms project using the template file. Project name: int_1_ms Project location: CH15\int_1_ms main.c #include #include #include #include #include <avr/io.h> <avr/interrupt.h> <util/atomic.h> <stdio.h> "stdio_setup.h" // interval in milliseconds to print time #define TIME_PERIOD_MS 1000 // milliseconds elapsed since timer started volatile unsigned long g_time_ms = 0; int main(void) { unsigned long prev_time_ms = 0; // saved elapsed time UartInit(); // set OCR0A TCCR0A TCCR0B TIMSK0 sei(); } up counter/timer 0 and enable interrupt // count value for 1ms period = 249; // CTC mode = (1 << WGM01); // divide by 64 prescaler = (1 << CS01) | (1 << CS00); // enable interrupt = (1 << OCIE0A); // global interrupt enable while(1) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { // check if desired time period has elapsed if ((g_time_ms - prev_time_ms) >= TIME_PERIOD_MS) { // save the current time prev_time_ms = g_time_ms; // print elapsed ms printf("%lu\r\n", g_time_ms); } } } // 1ms tick interrupt ISR(TIMER0_COMPA_vect) { g_time_ms++; } Sample Program Output 1000 2000 3000 4000 5000 … ● 282 Chapter 15 • Interrupts Sample Program Output without ATOMIC_BLOCK 1000 2000 3000 4000 4864 5864 6864 7864 8864 9728 10728 … The int_1_ms project uses timer/counter 0 in CTC mode to generate an interrupt every millisecond which is our 1ms tick interrupt. In the ISR of the timer a global variable called g_time_ms is incremented which stores the number of milliseconds elapsed since the timer was started. Code in the while(1) loop uses the elapsed millisecond value to create a one second time period in which the elapsed milliseconds are printed to the terminal window. Timer/counter 0 is set up to use a prescaler value of 64 which then feeds a clock pulse with a 4µs period to the timer when the main 16MHz clock is divided by 64. To find the count value that the timer must count to for a 1ms period, we divide 1ms by 4µs which gives 250. One must be subtracted from the calculated value of 250 as previously explained, which gives a value of 249 that the compare register A (OCR0A) must be initialised to. After the timer has been set up and interrupts enabled, the code in the while(1) loop runs with the 1ms timer interrupt interrupting it every millisecond to increment g_time_ms. The if statement is put into a special block that will be explained shortly. The expression in the if statement will evaluate to true when the time period defined with TIME_PERIOD_MS has elapsed. Subtracting a saved copy of the elapsed millisecond time from the current time calculates the time that has elapsed since the last time that the if expression evaluated to true. When the if expression does evaluate to true, a copy of the current elapsed time from g_time_ms is saved to prev_time_ms and will be used in the following if comparisons. A printf statement then prints out the current millisecond time. It is necessary to use the ATOMIC_BLOCK() macro that is defined in header file atomic.h, and to insert accesses to g_time_ms into the body of this block in order to prevent this variable from being corrupted. If an interrupt occurs when the global variable g_time_ms is being read, then this variable could be incremented by the code in the ISR during the four byte read of this variable that occurs in the main code. Because the AVR is an 8-bit microcontroller it can only access a byte at a time, so C code that reads a four byte wide variable results in four byte wide reads in the machine code generated by the compiler. Atomic, in this context, means that the code in the atomic block can not be interrupted. Code placed in the body of ATOMIC_BLOCK is guaranteed to never be interrupted. Passing a type of ATOMIC_RESTORESTATE to the ATOMIC_BLOCK macro restores the SREG status register after execution of the atomic block completes. Sample program output for the above code without the atomic block is shown above demonstrating that the value of the global variable gets corrupted. ● 283 C Programming with Arduino 15.4 • Summary ● 284 • The C language does not have a standard way of handling interrupts. • Interrupts are implemented by the writers of a C compiler, so it is necessary to read the compiler documentation for each different compiler to find out how to use interrupts in a C program. • Interrupts for AVR peripherals are enabled by setting an interrupt bit in one of the registers of each peripheral device. • In order for all enabled interrupts to work, global interrupts must be enabled by using the sei() macro. • Code that must run without being interrupted can be put into an atomic block. Chapter 16 • Wrapping Up the C Language Chapter 16 • Wrapping Up the C Language What you will do in this chapter: • Use structures to group different data types together • Use pointers to access structures • Learn about unions • See how the enumerated data type works • Look at miscellaneous topics in C to complete coverage of the C language 16.1 • Structures Arrays allow us to store and access more than one variable of the same type. Structures can be used to group data of different types, for example a structure can contain a float, int and a char, but an array can only consist of one of these types. 16.1.1 • Using Structures in C Programs The sample_struct project uses a structure to group a time-stamp value and sample data value together in order to demonstrate the use of structures in C. Create this project using the template file. Project name: sample_struct Project location: CH16\sample_struct main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { struct sample { int time; float data; }; struct sample samp1; /* declare a data type structure */ /* integer member of structure */ /* float member of structure */ /* define a structure of type sample */ UartInit(); } /* assign values to struct members samp1.time = 20; samp1.data = 56.23; printf("\r\nTime in seconds is: %d\r\n", samp1.time); printf("Sampled data is: %.2f", samp1.data); while (1); */ Program Output Time in seconds is: 20 Sampled data is: 56.23 ● 285 C Programming with Arduino A structure can contain any number of different data types and the compiler therefore does not know what you are going to put in a structure, so the structure must first be declared. In the sample_struct program, this is done with the following statement: struct sample { int time; float data; }; This declares a new data type called struct sample which has two member variables. The name sample is known as a tag and is a type name – i.e. we have declared a new data type that can now be used in our program. Figure 16-1 shows the details of a C structure declaration. Figure 16-1 A C Structure Declaration In the same way that we can use the keyword int to define an integer variable in a program, we can use the new data type struct sample to define a new structure of this type. This is what the next statement in the program does: struct sample samp1; The declaration of the new structure type did not allocate any memory for the structure, it just tells the compiler of the new data type that we are going to use in our program. The above statement now creates a new variable called samp1 of type struct sample. This allocates memory for the structure and the variable name for this structure is now samp1. We could declare another variable in the program of type struct sample as follows: struct sample samp2; This will define another structure in the program of type struct sample with its own name. In other words a second structure of the same type would exist in memory that can be accessed by using its name – samp2, just like we could define two integers or other variable types in a program: int val1; int val2; struct sample samp1; struct sample samp2; Each member variable of the structure can be accessed with the dot (.) operator as we can see in the following lines from the program: samp1.time = 20; samp1.data = 56.23; ● 286 Chapter 16 • Wrapping Up the C Language In each case, the structure name followed by the dot operator and then the name of the member variable is used to access the member variable of the structure. 16.1.2 • Initialising Structures Structures, like arrays can be initialised with data values. The init_struct program initialises three structures that each contain a pattern to display on the LEDs and the duration to display it for. Create this project without the use of the template file. Connect LEDs to port C pins PC0 to PC3 as done in previous projects and shown in appendix C. Project name: init_struct Project location: CH16\init_struct main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { struct pattern_time { unsigned char pattern; unsigned int time_ms; }; struct pattern_time pt1 = {0x05, 2000}; struct pattern_time pt2 = {0x0A, 5000}; struct pattern_time pt3 = {0x03, 3000}; DDRC |= 0x0F; } while(1) { PORTC = pt1.pattern; _delay_ms(pt1.time_ms); PORTC = pt2.pattern; _delay_ms(pt2.time_ms); PORTC = pt3.pattern; _delay_ms(pt3.time_ms); } Program Output The following bit patters are displayed on the four LEDs in a continuous loop: 0101 for 2 seconds 1010 for 5 seconds 0011 for 3 seconds A structure is declared that contains two member variables that are used to store a bit pattern to be displayed on the LEDs, as well as the duration to display the pattern for in milliseconds. Three structures are then defined and initialised using the structure that was declared. This makes space in memory for three structures named pt1 to pt3 and assigns values to their member variables. In the structure, the pattern member variable occurs first and the time_ms member variable occurs second. When initialisation takes place, the first value in the braces is assigned to the first member variable – pattern and the second value in the braces is assigned to the second member variable – time_ms. ● 287 C Programming with Arduino 16.1.3 • Pointers to Structures The next example, point_struct, uses a pointer to a structure in order to show the syntax used to access structure members with a pointer. Create this project from the template file. Project name: point_struct Project location: CH16\point_struct main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { struct time { unsigned char hour; unsigned char minute; unsigned char second; }; struct time cur_time = {12, 34, 27}; struct time *t_ptr; t_ptr = &cur_time; /* structure variable */ /* pointer to struct */ UartInit(); printf("Time is %02d:%02d:%02d\r\n", t_ptr->hour, t_ptr->minute, t_ptr->second); } while(1); Program Output Time is 12:34:27 This program first declares a structure of type struct time. A structure of type struct time called cur_time is then defined and initialised. A pointer to a structure of type struct time is then defined called t_ptr. This makes space in memory to store the address of a structure of this particular type. The line t_ptr = &cur_time; assigns the address of the cur_time structure to the structure pointer t_ptr. When the contents of the structure is accessed by the pointer in the printf() function, the arrow operator (->) is used to access each member variable. The arrow operator is always used to access member variables of a structure when we are using a pointer to a structure. The dot operator is always used to access the member variables of a structure when we are not using a pointer to a structure, but are accessing the structure directly. 16.2 • Unions A union in C uses the same syntax as a structure, except that the keyword union is used instead of struct when declaring it. The members of a union share the same space in memory which allows us to access the single space in memory as one type of variable or another. ● 288 Chapter 16 • Wrapping Up the C Language The union program demonstrates how a C union works. Create this project using the template file. Project name: union Project location: CH16\union main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { union unnum { float flt; long dec; }; union unnum my_union; UartInit(); my_union.flt = 23.7; printf("\r\nnum as float is %.2f\r\n", my_union.flt); printf("num as long int is %ld\r\n", my_union.dec); my_union.dec = 100; printf("\r\nnum as float is %.2f\r\n", my_union.flt); printf("num as long int is %ld\r\n", my_union.dec); } while (1); Program Output num as float is 23.70 num as long int is 1102944666 num as float is 0.00 num as long it is 100 A union called unnum is declared in this program. A union called my_union is then defined. The union contains a floating point member variable as well as an integer member variable. The integer is of type long int to make it the same number of bytes wide as a float on an AVR – both of these variables occupy four bytes of memory. When they occur in a union, they both occupy the same four bytes of memory, so the same four bytes of memory can be viewed as an integer or a floating point number. In the program the floating point member variable is first initialised with a value of 23.7. This value is then displayed in the terminal window as a floating point number and then as an integer. To display the number as a floating point number, we access the floating point member variable flt. To display this number (4 byte memory location) as an integer, we access the integer member variable dec. This results in a value of 1102944666 being displayed because the two numbers are in different formats i.e. the floating point number contains an integer part and a fractional part in the four byte memory location and this gives us a meaningless number when we view these four bytes as a decimal number. Take note that viewing the same memory location as a float and int is completely different to viewing a variable as a float and then casting it to an int or copying a float ● 289 C Programming with Arduino to an int and truncating the fractional part of the float. Looking at the same memory locations as a floating point number and then integer reveals the underlying encoding of the floating point number. To prove that we are looking at the same four bytes in memory, the integer member variable is initialised to a value of 100. This causes the floating point member variable to change to 0.00 when printed in the terminal window. A more practical use of a union is demonstrated in the next program called multibyte where a union is used to access an integer as a 32-bit integer or as individual bytes. This could be used in a program to access a 32-bit number as four single bytes to send over a serial port that sends a byte at a time. Create this project using the template file. Project name: multibyte Project location: CH16\multibyte main.c #include <stdio.h> #include "stdio_setup.h" int main(void) { int count; union wrd_byte{ unsigned long word; unsigned char byte[4]; } port; UartInit(); } port.word = 0xFF5501AA; printf("\r\nWord is: 0x%08lX\r\n", port.word); for (count = 0; count < 4; count++) { printf("Byte%d: 0x%02X\r\n", count, port.byte[count]); } while (1); Program Output Word is: 0xFF5501AA Byte0: 0xAA Byte1: 0x01 Byte2: 0x55 Byte3: 0xFF In this program a union is declared and a union with the name port is defined in one step by putting the name of the union to be defined between the closing brace and the terminating semicolon of the union declaration. The union consists of two member variables: a four byte integer called word and a four byte array called byte. In the program the word member variable is initialised to a value of 0xFF5501AA and then printed in the terminal window. A for loop is used to print the same number as four individual bytes by accessing each byte of the byte array. Of course because the byte and word member variables share the same four bytes in memory, the value that word was initialised to is displayed in the terminal in four parts starting with the least significant byte in word. Figure 16-2 on page 291 shows what the union looks like in memory. The figure shows that the word variable and byte array take up the same four memory locations. ● 290 Chapter 16 • Wrapping Up the C Language Figure 16-2 A union in Memory 16.3 • Enumerated Type The enumerated data type in C is an integer data type that can be used to make a program easier to read and understand. The enumerated data type allows you to create your own data type and specify the values that can be assigned to it. If an attempt is made to assign a value other than you have specified, the compiler will generate an error. The next program uses the Arduino as a controller for an imaginary dough mixing machine. Three LEDs on port pins PC0 to PC2 represent three solenoids that are used to control the machine. The switch on pin PC5 is used by the machine operator to start the machine. The hardware is configured as follows: Outputs: PC0 = water pump solenoid – adds water to the mixing bowl when activated PC1 = flour hopper solenoid – adds flour to the mixing bowl when activated PC2 = mixer – starts and runs the mixer when activated Input: PC5 = start button – pressed by the machine operator to make a batch of dough Run the program and press the button on PC5 to start the dough mixing procedure. Each of the LEDs (solenoids) will switch on in sequence so that water is added to the bowl for 4 seconds, flour is added for 2 seconds and the dough is mixed for 5 seconds. After this the machine goes back into standby mode and waits for the operator to press the start button again to make another batch of dough. The enum_seq program is used to demonstrate enumerated data types in C. Create this project without the template file. Project name: enum_seq Project location: CH16\enum_seq main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> int main(void) { enum machine_state { standby, pump_water, add_flour, mixing }; enum machine_state state = standby; ● 291 C Programming with Arduino DDRC = 0x07; /* LEDs on PC0, PC1 and PC2 */ while (1) { switch (state) { case standby: /* switch all solenoids off */ PORTC &= 0xF8; /* check for start button pressed */ if (PINC & (1 << PINC5)) { state = pump_water; } break; case pump_water: /* switch water pump solenoid on for 4s (PC0) */ PORTC = (1 << PORTC0); _delay_ms(4000); state = add_flour; break; case add_flour: /* switch flour hopper solenoid on for 2s (PC1) */ PORTC = (1 << PORTC1); _delay_ms(2000); state = mixing; break; case mixing: /* mix dough for 5s (PC2) */ PORTC = (1 << PORTC2); _delay_ms(5000); state = standby; break; } } } default: break; Program Output When the push-button switch on PC5 is closed, the following happens: - The LED on PC0 switches on for four seconds - The LED on PC0 switches off and the LED on PC1 switches on for two seconds - The LED on PC1 switches off and the LED on PC2 switches on for five seconds - The LED on PC2 switches off and the push-button switch is monitored again Inside main() an enumerated data type is declared as shown below. enum machine_state { standby, pump_water, add_flour, mixing }; The enum keyword tells the compiler that this is to be an enumerated data type. The tag machine_state gives it a name so that we can use it in our program. The four names between braces are the four possible values that an enumerated data type of this kind can take on. These names are actually integer values that start from 0 and count up, so standby has the value 0, pump_water has the value 1, add_flour has the value 2, etc. These are not variable names, but are actual integers that we refer to by these meaningful names. ● 292 Chapter 16 • Wrapping Up the C Language The next line in the program creates a variable of type enum machine_state called state and assigns it the value of standby. enum machine_state state = standby; We now have a variable called state that can take on only one of the values that are part of the enumerated type i.e. standby, pump_water etc. DDRC is used to set up PC0, PC1 and PC2 as outputs for our three imaginary solenoids that are simulated on LEDs attached to these pins by setting the lower three bits of this register. A switch statement is used to sequence the machine. The switch statement used in this way is known as a "state machine" as it controls the state of the program. When the switch statement is run, it checks the value of the enumerated variable state. Because we initialised this variable with the value of standby, the first case statement will be continually run as it is inside the while(1) loop. This case statement is used to check if the start button is pressed by the machine operator. It also switches all of the solenoid valves (LEDs) off. Should the operator press the start button that is wired to pin PC5, the value of the enumerated data type state is changed to the value of pump_water inside the if statement. This results in the next case statement (pump_water) being run the next time that the switch statement is executed. The pump_water case switches on the water pump solenoid for four seconds and then changes the state of the state machine to add_flour. The add_flour and mixing states operate in a similar manner, each switching off the previous solenoid valve and switching on a new one, then waiting for a specified time period before changing the state of the state machine. When the mixing state has been completed, the state is changed back to standby and the push button switch is again monitored for the operator to start a new batch of dough. Note how the enumerated data type makes the program easier to understand as it uses easy to understand names instead of numbers to represent the four states of the machine. Also notice that bitwise assignment operators were not used to switch the LEDs on, instead a bit-shifted value is written directly to the PORTC register which then switches all other LEDs off and only the selected LED on. This method can be used when it makes sense to do so and has the advantage that the previously switched on LED does not need to be switched off first in a separate line of code. Switch debouncing is not needed and is automatically built into the program because of the way the program is written. When the switch is closed, it is not monitored again until the entire LED switching sequence is complete, so the microcontroller will never read more than one switch press before moving to the next step in the sequence. Calls to the delay function in the program effectively give the switch many seconds of time for the switch contacts to stop bouncing when the push-button switch is closed and again when it is opened. 16.4 • The typedef Declarator The typedef keyword enables us to give another name to a C data type. This can be used to make a program easier to read and write. The following lines of code show how to use typedef to give a different name to a C data type: typedef unsigned char BYTE; typedef unsigned char UCHAR; ● 293 C Programming with Arduino Either BYTE or UCHAR can now be used in a program wherever we would have used unsigned char – this has effectively made a new C data type. Using the name BYTE is more descriptive especially when programming hardware. UCHAR is a shorter way of writing unsigned char and can save some typing and space on a line especially when a function takes a number of arguments that are of this type. In the program we can now define variables of this type: BYTE port1_val; BYTE port2_val; And is that same as: unsigned char port1_val; unsigned char port2_val; typedef can also be very useful when used to create a new structure. The typedef declaration can be made in a header file and every source file that the header file is included in can make use of the new data type. As an example: typedef struct { unsigned char hours; unsigned char minutes; unsigned char seconds; } time_struct; Notice that we do not even need a tag name in this typedef declaration. This structure can now easily be used in any program as follows: time_struct curr_time = { 2, 32, 56 }; time_struct alarm_time = { 3, 30, 00 }; We have effectively created a new data type called time_struct and can now create variables of this type by using its type name. 16.5 • Storage Class Specifiers There are four storage class specifiers in C of which you have already seen the static specifier. The other three are auto, register and extern. Only one storage class specifier may appear in a declaration. The meaning of each of these specifiers is described below: auto – This keyword is actually outdated as any variable that is declared in a function is an automatic variable. When a variable is declared with the auto specifier it will have automatic storage duration. This means that when a function is called, the automatic variable will be created (it will be given space in memory) and when the function exits, the variable will be destroyed (the memory will be freed for use by other parts of the program). This happens by default anyway. Example of usage: auto int val; extern – Functions and variables declared with the extern keyword can be used anywhere in the program. They will have "global" visibility. e.g: extern int val; register – This keyword is a suggestion to the compiler that you want to store a variable in an internal CPU register to make access to it faster. This is not however guaranteed to happen. You can not use the address operator with variables that are declared with this ● 294 Chapter 16 • Wrapping Up the C Language specifier. e.g: register float fast_flt; static – Variables that are declared static will retain their value even when they go out of scope e.g. when a function in which they exist returns, they will not be destroyed. e.g: static unsigned int count; An example of the use of the extern keyword is if our C program consists of more than one file and we want to be able to use the global variable that was created in one file in the second file, we would use the extern keyword in the second file. For example: In main.c we create a global variable (a variable defined outside of a function): int globvar = 2; We now want to use this global variable in another file in this program – comms.c. In comms.c we add the following at the top of the file or in a header file that is included in comms.c: extern int globvar; We are now free to use globvar in the comms.c file. 16.6 • Type Qualifiers C has two type qualifiers namely const and volatile. const – a variable that is qualified with the const keyword is constant and it can not be changed by the program. A variable qualified with const is effectively "read only" and code that tries to write to this variable will be flagged as an error by the compiler. Example of using const: const int ref = 3; volatile – we have already used the volatile keyword in several example programs. This keyword forces the program to reread the value that is qualified as volatile whenever it is used as it may be modified by an external process or event. An example of using volatile is on a variable that is modified by an interrupt service routine, but never modified by the main program. If compiler optimisation is enabled, which is the default in an Atmel Studio project, the compiler may optimise the variable out of existence if it is not qualified by the volatile keyword. 16.7 • The goto Statement Use of the goto statement is considered bad programming practice by most C programmers. The goto statement can make programs hard to understand and debug because it can cause program flow to jump anywhere in a program. Using the goto statement is unnecessary in a program and there will always be a better way to write a program without this statement by using a loop or if statement. How to use the goto statement is included here as you may come across it in someone else’s code and will need to understand how it works. ● 295 C Programming with Arduino The goto statement is always used in conjunction with a label. An example will illustrate: if (time == alarm_time) goto ring_alarm; else CountSeconds(); ring_alarm: AlarmOn(); TimeDelay(5000); AlarmOff(); The goto statement will cause a jump to the ring_alarm label and the code under this label will be run. 16.8 • A List of All C Keywords Table 16-1 contains a list of all 32 C keywords as a summary of what we have covered. Table 16-1 All C Keywords auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while 16.9 • More Preprocessor Directives We have used the #include and #define preprocessor directives in various example programs. There are other preprocessor directives that can be used for conditional compilation. The #ifdef directive can be used to compile certain parts of a program and not others when used in combination with the #define and #endif directives. An example of the use of conditional compilation is if we want to write a C program that can run on two different microcontrollers or on two different embedded systems that may have slightly different hardware. We would want to compile the same program to use one microcontroller or embedded system’s hardware, but would also want to be able to compile the same program to use on the other microcontroller or embedded system hardware. Preprocessor directives can be used to add in and remove code depending on which microcontroller or embedded system is selected. The following program called conditional_compile demonstrates how to compile conditionally for the Arduino Uno and MEGA that have the on-board LED attached to different port pins. ● 296 Chapter 16 • Wrapping Up the C Language Project name: conditional_compile Project location: CH16\conditional_compile main.c #define F_CPU 16000000UL #include <avr/io.h> #include <util/delay.h> #define ARDUINO_UNO int main(void) { #ifdef ARDUINO_UNO DDRB = 0x20; #else DDRB = 0x80; #endif } while(1) { #ifdef ARDUINO_UNO PORTB ^= 0x20; #else PORTB ^= 0x80; #endif _delay_ms(1000); } Program Output The on-board LED flashes on and off. This program can be compiled for the Arduino Uno or the Arduino MEGA and will flash the on-board LED on and off despite the on-board LED being attached to different port pins on each board. When compiling for the Uno, ARDUINO_UNO must be defined as it is shown in the code listing above. When compiling for the MEGA, the line of code that defines ARDUINO_UNO must either be commented out or could be changed to ARDUINO_ MEGA. When the program is compiled with ARDUINO_UNO defined as above, the code under the #ifdef ARDUINO_UNO directives will be included in the program by the pre-processor and the code between the #else and #endif directives will be excluded. When #define ARDUINO_UNO is removed from the file, commented out or the name changed, only the code between the #else and #endif directives will be compiled. The #endif directive is used to show that the #ifdef and #else directives end here – like the closing brace of a construct in C. These preprocessor directives conditionally compile the lines of code that need to be different for handling the hardware differences between the two boards. At the beginning of the program the port pin that the LED is attached to is set as an output pin by conditionally selecting the correct line of code to set the correct bit in DDRB. The same happens when the LED is toggled – the correct bit in PORTB is toggled for the selected hardware. ● 297 C Programming with Arduino 16.10 • C99 Variable Types Most technical standards are updated every few years to add improvements to the standards and to include changes that need to be made due to improving or changing technology or usage of the standards. The C language standard is no exception and the C99 revision of the language added new data types, some of which are shown below. Fixed width integers: int8_t – equivalent to signed char in the AVR GCC compiler int16_t – equivalent to short and int in the AVR GCC compiler int32_t – equivalent to long in the AVR GCC compiler Fixed width unsigned integers: uint8_t – equivalent to char and unsigned char in the AVR GCC compiler uint16_t – equivalent to unsigned short and unsigned int in the AVR GCC compiler uint32_t – equivalent to unsigned long in the AVR GCC compiler These fixed width integer types are found in the stdint.h header file that must be included in the C source files that use these data types. The number in each of these data types is the bit width of the integer – uint8_t is an 8-bit unsigned integer, int32_t is a signed 32-bit integer, etc. Fixed width data types solve the problem with varying integer widths that occurs when porting C code between different microcontrollers and/or compilers. When an integer is defined with the int keyword, it could be 16-bits wide, 32-bits wide, or more, depending on the microcontroller and compiler being used. Fixed width integer data types solve this problem by specifying the width of the integer and will be the same width no matter which microcontroller or compiler are used. An unsigned 16-bit wide integer defined with uint16_t will be 16-bits wide whether it is defined in code that is compiled for an 8-bit AVR or a 32-bit ARM microcontroller. 16.11 • Alternative Continuous Loop There is an alternative way of writing a continuous loop like we have been doing with while(1), but using a for loop instead. You may come across this way of writing a continuous loop when looking at C code from other authors. The following two code listings do exactly the same thing, creating a continuous loop that runs the code placed in the body of the loop forever. while(1) { } for(;;) { } ● 298 Chapter 16 • Wrapping Up the C Language 16.12 • Summary • Structures can be used to group data of different types together. • The dot operator is used to access members of a structure. • The arrow operator is used to access structure members when using a pointer to the structure. • A union is similar to a structure but its member variables share the same memory space. • The enumerated data type can be used to make a program easier to read and understand. • The typedef declarator can be used to rename a data type or give a name to a new data type such as a user defined structure. • Storage class specifiers (auto, extern, static, register) can be used to modify the way a program treats a variable. • The const keyword specifies that a variable is constant and cannot be changed in the program. • The volatile keyword is used wherever a register or variable can be changed by hardware or a process that operates externally to the C program and can change the value of the register or variable without the program knowing about the change. • The goto statement can make a program difficult to understand as it can cause a jump to anywhere in the program. It is considered good programming practice not to use the goto statement as there is always a better way of writing a program without this statement. • C code can be conditionally compiled by using preprocessor directives. • Fixed width integer data types from the C99 revision of the C language allow the width of integers to be specified. ● 299 C Programming with Arduino ● 300 Chapter 17 • C Projects Chapter 17 • C Projects What you will do in this chapter: • Write serial port driver functions to replace printf() • Write functions to convert numbers to decimal and hexadecimal strings • Make a voltmeter project • Create a stopwatch This chapter consists of a series of C code projects that demonstrate the use of the C language already learned in the previous chapters. Several C topics are added to and expanded in the explanations that follow each project. 17.1 • Serial Port Driver Functions As already discussed, the printf() functions tends to use up a fair amount of memory which is a scarce resource on 8-bit AVR microcontrollers, especially the smaller parts that have less memory. The solutions suggested in Chapter 10 was to write serial port functions that can send and receive text and characters over the UART serial port. The project in this section modifies the code from the serial_port project found in section 10.5.2 of Chapter 10 on page 184. The following modifications are made to the serial_ port project. • The serial port / UART driver functions are put into their own C file • UART baud rate settings and microcontroller clock frequency are set in global.h • An extra function is added that transmits a null-terminated string • The use of commenting is demonstrated in the program The serial port driver functions allow strings to be sent over the Arduino serial / USB port in the same way as the printf() function does when a project is created using the template file, but the serial port driver functions do not have the formatting capabilities of printf(). Using the serial port function for transmitting a string instead of printf() reduces program memory usage. 17.1.1 • Serial Port Driver Design Notes Although the hardware device in the AVR microcontroller that handles serial port communications is called a USART, it is used as a UART in the serial port driver code so file names and function names to do with using the USART are named UART and not USART. The uart_driver project that contains the serial port driver functions is split into four separate source code files, namely main.c, uart.c, uart.h and global.h. main.c contains the code that demonstrates the use of the serial port functions from uart.c. uart.h is the header file for uart.c and must be included in any C source file that uses the serial port driver functions. global.h contains the clock frequency of the embedded system that the code will be compiled for, defined as F_CPU, as well as the baud rate setting for the UART. It was decided to put the baud rate setting into the global header file so that the baud rate can be changed without making any changes to the UART driver ● 301 C Programming with Arduino files. In this way the driver files can be copied from project to project without any need to modify them. Any changes to the baud rate or microcontroller frequency can be made in the global.h header file. 17.1.2 • Serial Port Driver Code Code listings for all four source code files of the uart_driver project are shown below. Project name: uart_driver Project location: CH17\uart_driver main.c /*-----------------------------------------------------------------------------FILE NAME: main.c from uart_driver project DESCRIPTION: Tests the UART driver functions from uart.c AUTHOR: W.A. Smith DATE: 30 October 2015 ------------------------------------------------------------------------------*/ #include <avr/io.h> #include "uart.h" int main(void) { // stores data received from the UART int rx_data; // initialize the UART to the baud rate set in global.h UartInit(); // test the UartTxString() and UartTxByte() functions UartTxString("Start of UART driver test program.\r\n"); UartTxString("Transmitting a character:"); UartTxByte('C'); UartTxString("\r\nEchoing typed characters:\r\n"); } // echo back any character typed in the terminal window and terminate with // carriage return and newline characters while(1) { // get a byte from the UART if available rx_data = UartRxByte(); // was a byte received if (rx_data > -1) { // echo the received byte back over the UART UartTxByte(rx_data); // terminal window cursor to new line UartTxString("\r\n"); } } global.h #ifndef GLOBAL_H_ #define GLOBAL_H_ // microcontroller clock frequency #define F_CPU 16000000UL // USART baud rate settings #define BAUD 38400 #define BAUD_TOL 1 #endif /* GLOBAL_H_ */ ● 302 Chapter 17 • C Projects uart.c /*-----------------------------------------------------------------------------FILE NAME: uart.c DESCRIPTION: UART driver functions for UART0 on Arduino Uno and MEGA NOTES: Set the baud rate and clock frequency in global.h AUTHOR: W.A. Smith DATE: 30 October 2015 ------------------------------------------------------------------------------*/ #include "global.h" #include <avr/io.h> #include <util/setbaud.h> #include "uart.h" /*-----------------------------------------------------------------------------FUNCITON NAME: UartInit() PURPOSE: Initializes UART0 to the baud rate set in global.h USAGE: Call once at the start of a program before using other UART functions ------------------------------------------------------------------------------*/ void UartInit(void) { // set UART0 baud rate UBRR0H = UBRRH_VALUE; UBRR0L = UBRRL_VALUE; // dedicate pins to USART0 for transmit and receive UCSR0B = (1 << RXEN0) | (1 << TXEN0); // 8-bit, 1 stop bit, no parity, asynchronous UART UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); } /*-----------------------------------------------------------------------------FUNCITON NAME: UartTxByte() PURPOSE: Transmits a single byte from the UART INPUTS: data - data byte or character to transmit ------------------------------------------------------------------------------*/ void UartTxByte(char data) { while(!(UCSR0A & 0x20)); // wait until ready to tx UDR0 = data; // transmit byte } /*-----------------------------------------------------------------------------FUNCITON NAME: UartTxString() PURPOSE: Transmits a null terminated string from the UART INPUTS: str - a pointer to the string to transmit ------------------------------------------------------------------------------*/ void UartTxString(char *str) { uint16_t str_index = 0; while (str[str_index] != 0) { // exit on null terminator while(!(UCSR0A & 0x20)); // wait until read to tx ● 303 C Programming with Arduino } } UDR0 = str[str_index++]; // transmit the byte /*-----------------------------------------------------------------------------FUNCITON NAME: UartRxByte() PURPOSE: Receives a single byte from the UART if available RETURNS: The data byte or character received if available Returns -1 if no data byte or character was received ------------------------------------------------------------------------------*/ int UartRxByte(void) { int data; if (UCSR0A & 0x80) { data = UDR0; } else { data = -1; } } // check if byte received // get received byte // byte not received return data; uart.h /*-----------------------------------------------------------------------------FILE NAME: uart.h DESCRIPTION: Header file for uart.c AUTHOR: W.A. Smith DATE: 30 October 2015 ------------------------------------------------------------------------------*/ #ifndef UART_H_ #define UART_H_ void UartInit(void); void UartTxByte(char data); void UartTxString(char *str); int UartRxByte(void); #endif /* UART_H_ */ ● 304 Chapter 17 • C Projects Example Program Output Start of UART driver test program. Transmitting a character:C Echoing typed characters: a b c d e f g 1 2 3 4 Creating and using the project The uart_driver project is created without the use of the template file. When connecting Tera Term to the port of the Arduino board that runs this code, change the baud rate in Tera Term to 38400. Previous projects all used a baud rate of 9600, but by speeding up the baud rate the microcontroller will take less time to send and receive data. Baud rate settings can be found by using the top Tera Term menu and selecting Setup → Serial port... to open up the serial port setting dialog box. main.c code Code in main.c should be easy to understand as it works much the same as the code in the serial_port project from section 10.5.2 on page 184, with the main differences being that a new function named UartTxString() is called that transmits an entire string, and when a character is typed in the terminal window, the cursor of the terminal window is moved to a new line after echoing the character. The idea of this project is to create a set of serial port driver functions that can be used in other projects by copying the driver function files to the other projects. The code in main.c simply tests the driver functions and demonstrates their usage. Definitions in global.h and including global.h The file global.h contains the F_CPU microcontroller clock frequency setting that we have used before and also contains the baud rate settings for the UART. When global.h is included in a C source code file it is important to include it before any other included files because other included files may need the settings that are defined in global.h. The compiler works through a file from top to bottom, so if it finds the F_CPU setting being used in an included header file before it knows what value of F_CPU was defined as, it will generate a warning. This will occur if global.h is included below any file that uses it. Header file preprocessor directives Atmel Studio automatically adds preprocessor directives to header files that are created in Atmel Studio and added to a project. The preprocessor directives prevent definitions or function prototypes in a header file from being declared more that once which can happen if the header file is included in a C source file more than once. Code added to the header file should be placed between the preprocessor directives as shown below. ● 305 C Programming with Arduino #ifndef GLOBAL_H_ #define GLOBAL_H_ // add header file code here #endif /* GLOBAL_H_ */ Preprocessor directive #ifndef means "if not defined". In the above code, GLOBAL_H_ will not be defined when the header file is included in a C source code file for the first time. #ifndef GLOBAL_H_ will therefore be true and the next preprocessor directive will be evaluated which defines GLOBAL_H_. All code between the opening #ifndef and closing #endif will be included in the C source file. If the header file is included in the same C source file a second time, #ifndef GLOBAL_H_ will then evaluate to false and nothing between the opening #ifndef and closing #endif will be evaluated or included in the C source file. uart.c functions All of the serial port driver functions are found in the uart.c file and are the same functions from the serial_port project in section 10.5.2 on page 184 except for a new function called UartTxString() that transmits a null-terminated string passed to it over the UART serial port. UartTxString() checks for the null-terminator in the expression of the while loop. If the null-terminator character is found, the function returns. If the nullterminator character is not found, the character from the string that the variable str_index is indexing in the string is sent over the serial port. uart.h The uart.h file contains the function prototypes for the functions in the uart.c file. Any file that uses a function from uart.c must include uart.h. 17.2 • Converting Numbers to Strings When using the serial port drivers instead of printf() to send a string over the serial port, there are no formatting options available for printing numbers. The project in this section shows how to write functions to convert numbers stored as variables or constants to strings that will print the numbers in decimal or hexadecimal format. Before looking at the actual project code, we explore some functions from the AVR GCC C library that can be used for the same purpose. 17.2.1 • Using AVR C Library Functions C library functions are available to convert numbers into ASCII text strings. We have already looked at the sprintf() function that has all of the formatting options of printf(), but if used will probably make code size as big as if printf() was used. Another function, called itoa(), can be used to convert integer numbers to ASCII strings, but it is not a standard C function and is not defined in the C standard specification. Some C compilers do include itoa(), as does the AVR GCC compiler used by Atmel Studio. The itoa project demonstrates the use of the itoa() function. Create this project without the template file, but add the serial port driver files to the project from the previous project. ● 306 Chapter 17 • C Projects Project name: itoa Project location: CH17\itoa main.c #include <avr/io.h> #include <stdlib.h> #include "uart.h" int main(void) { int val = 23; char str_val[30] = {0}; UartInit(); UartTxString("Number in decimal: "); itoa(val, str_val, 10); UartTxString(str_val); UartTxString("\r\n"); UartTxString("Number in hexadecimal: 0x"); itoa(val, str_val, 16); UartTxString(str_val); UartTxString("\r\n"); } while(1); Program Output Number in decimal: 23 Number in hexadecimal: 0x17 The three serial port driver files can be added to the project by creating them as new files in the new project and then copying and pasting the code from the same files in the previous project, but there is an easier way of adding the files to a new project. Copy the three files, namely global.h, uart.h and uart.c from the previous project to the new project. This means copying them from the uart_driver\uart_driver\ folder to the itoa\ itoa folder using the Windows file manager as shown in Figure 17-1. Figure 17-1 Copying Files Between Projects ● 307 C Programming with Arduino After the files have been copied to the new project, they must be added to the project in Atmel Studio. Right-click the project name in Solution Explorer and select Add → Existing Item... from the pop-up menu as shown in Figure 17-2. In the Add Existing Item dialog box that opens, all three files can be added to the project at the same time by holding down the Ctrl key on the keyboard while selecting each file with the mouse as shown in Figure 17-3 on page 309. After the serial port driver files have been added to the project, the code from the above listing can be added to main.c and the project built. The project uses the integer to ASCII conversion function itoa() to convert the value of a variable to a string in decimal and hexadecimal format. The output from the itoa() function is a string that is stored in the str_val string array. This string array is passed to UartTxString() so that it can be sent to the terminal window. Passing 10 to the itoa() function as the third parameter lets the function know that we want to convert the number passed as the first parameter to a string using the base 10 decimal system. Passing 16 as the third parameter converts the number to a base 16 hexadecimal string. Figure 17-2 Adding Existing Files to a Project There are a number of other functions related to itoa() that convert different sized integers to ASCII strings as listed below. All of these functions have their prototypes declared in the stdlib.h header file. ● 308 Chapter 17 • C Projects • itoa() – converts an integer to a string • ltoa() – converts a long integer to a string • utoa() – converts an unsigned integer to a string • ultoa() – converts an unsigned long integer to a string Each of these functions are used in the same manner, but act on different data types. They are passed the following arguments when called as the example for itoa() below shows. itoa(int val, char *s, int radix); Where val is the number to convert to a string, s is a pointer to a string that will store the output string from the function and radix is the base of the number system to convert to. A binary number can also be printed by these functions by setting the radix to a value of 2. Figure 17-3 Selecting Multiple Existing Files to Add to a Project 17.2.2 • Writing Number to String Conversion Functions Two functions in the num2str project listed below demonstrate one solution to converting numbers to strings for printing if the compiler being used does not include these functions. The first function converts a number to a string in decimal format and the second function converts a number to a string in hexadecimal format. Create the project without the template file and add the serial port driver files to the project as done in the previous project. Code in the main.c file demonstrates the use of the number to string conversion functions found in the file num2str.c that has the same name as the project. Altogether the project consists of six source code files, namely main.c, num2str.c, num2str.h, uart.c, uart.h and global.h. The number to string conversion functions in this project are not the most efficient ● 309 C Programming with Arduino functions in terms memory usage and number of lines of C code, but are easier for beginner C programmers to understand. That being said, the code will still be smaller than using functions such as printf(). The functions have also been written to store the intermediate values from calculations in variables so that they are easier to view in a debugger. If you are having trouble understanding the code, it is a worthwhile exercise to step through the conversion functions using the debugger in Atmel Studio using a debugging device such as the AVR Dragon. Put watches on the variables that you would like to see and single-step through the code to see what each line of code does. Another worthwhile exercise is to search the Internet for C code that does the same or similar conversion and to see how it works. Code listings below are printed without the file header comments and top function comments to save space in the book. The same code with the accompanying files has the top comments included. Project name: num2str Project location: CH17\num2str main.c #include #include #include #include <avr/io.h> <stdlib.h> "uart.h" "num2str.h" int main(void) { char str_val[30] = {0}; UartInit(); UartTxString("Decimal Numbers:\r\n"); Int2DecStr(-2923, str_val); UartTxString(str_val); UartTxString("\r\n"); Int2DecStr(0, str_val); UartTxString(str_val); UartTxString("\r\n"); Int2DecStr(32767, str_val); UartTxString(str_val); UartTxString("\r\n"); Int2DecStr(9002, str_val); UartTxString(str_val); UartTxString("\r\n\r\n"); UartTxString("Hexadecimal Numbers:\r\n"); UInt2HexStr(0x1A2B, str_val); UartTxString(str_val); UartTxString("\r\n"); UInt2HexStr(0, str_val); UartTxString(str_val); UartTxString("\r\n"); UInt2HexStr(0xFFFF, str_val); UartTxString(str_val); UartTxString("\r\n"); UInt2HexStr(30123, str_val); UartTxString(str_val); UartTxString("\r\n\r\n"); } while(1); ● 310 Chapter 17 • C Projects num2str.h #ifndef NUM2STR_H_ #define NUM2STR_H_ void Int2DecStr(int num, char *str); void UInt2HexStr(int num, char *str); #endif /* NUM2STR_H_ */ num2str.c void Int2DecStr(int num, char *str) { // stores individual digit from number char digit; // stores number as reversed string char temp_str[10]; // index into temp_str int temp_index = 0; // index int str int str_index = 0; } // check if number is negative if (num < 0) { // add sign to string if number is negative str[str_index++] = '-'; // change number to positive number num = -num; } do { // get smallest digit digit = num % 10; temp_str[temp_index++] = digit + 0x30; // convert digit to ASCII num /= 10; // remove smallest digit } while (num != 0); temp_index--; // point index to last digit in temp string // reverse the reversed temporary string and save in str while (temp_index >= 0) { str[str_index++] = temp_str[temp_index--]; } // terminate the string str[str_index] = 0; void UInt2HexStr(unsigned int num, char *str) { // stores individual digit from number char digit; // stores number as reversed string char temp_str[10]; // index into temp_str int temp_index = 0; // index int str int str_index = 0; } do { digit = num & 0x0F; if (digit > 9) { // convert digit to letter from A to F temp_str[temp_index++] = digit + 55; } else { // convert digit to ASCII from 0 to 9 temp_str[temp_index++] = digit + 48; } num >>= 4; } while (num > 0); temp_index--; // point index to last digit in temp string // reverse the reversed temporary string and save in str while (temp_index >= 0) { str[str_index++] = temp_str[temp_index--]; } str[str_index] = 0; // terminate the string ● 311 C Programming with Arduino uart.c, uart.h and global.h Copy these serial port driver files and add them to the project. Program Output Decimal Numbers: -2923 0 32767 9002 Hexadecimal Numbers: 1A2B 0 FFFF 75AB The main program calls the integer to decimal string conversion function Int2DecStr() and the unsigned integer to hexadecimal conversion function UInt2HexStr() several times to demonstrate their usage. Int2DecStr() To convert an integer number to a string that is an ASCII string representation of the number in decimal format, each digit of the number in decimal format must be converted to its ASCII code equivalent. The technique used by the Int2DecStr() function to get each individual decimal digit of the number is to use the modulus operator and the division operator. When the modulus operator is used as shown in the line of code below, the remainder of dividing the number by ten is saved in the digit variable. digit = num % 10; A decimal number consists of units, tens, hundreds, thousands, etc. Every part of the decimal number is divisible by ten, except the units which will be the remainder when the number is divided by 10. If num in the above line of code has a value of 2923, then 3 is the result of the above modulus operation. This has the effect of isolating the units part of the number. In the ASCII table, numbers start at 0x30, so to adjust the number so that it represents the equivalent ASCII character, we add 0x30 to it as shown in the line of code below. In this example, we add 0x30 to 3 to get 0x33 which represents the ASCII character ‘3’ as can be seen in the ASCII table in Appendix A on page 329. temp_str[temp_index++] = digit + 0x30; // convert digit to ASCII In the above line of code, the ASCII character is saved to the beginning of the temp_ str array. The only problem with this is that the last digit of the number is stored at the beginning of the array. As ASCII digits of the number are added to the array, it has the effect of reversing the number. The number 2923 will be saved as the string 3292 in the array. After separating the units from the number 2923 in this example, this number is divided by ten so that the tens part of the number now becomes the new units part of the number. Because we are using integers, the number becomes 292 instead of 292.3 as the fractional part of the number is discarded. After dividing the number by 10, the do-while loop repeats if the result of the division is not equal to zero. The modulus ● 312 Chapter 17 • C Projects operator now gets the next part of the number which is 2 and this is then converted to the ASCII character ‘2’ and added to the next element in the temporary string. The loop will only terminate when all digits of the number have been converted. When the loop does terminate, the temporary string temp_str contains a reversed ASCII representation of the number in decimal format. Code that follows the loop reverses the reversed string so that it is now the right way around and stores it in the string array str. If the number to be converted was negative, a negative sign would have been stored at the beginning of the str array by the first few lines of code in the function, after which the number is changed to a positive number to make the conversion easier. UInt2HexStr() The flow of the UInt2HexStr() function is the same as the Int2DecStr() function in that a reversed copy of the input number is saved in ASCII format in a temporary string which is then reversed to get the number in the correct order. The number input to this function is converted to a hexadecimal representation in an ASCII string. The number is converted to hexadecimal ASCII digits by firstly isolating the least significant hexadecimal digit of the number using the bitwise AND operator as shown in the line of code below. digit = num & 0x0F; The above line of code gets the least significant hexadecimal digit by clearing all of the upper hexadecimal digits from the number. After the single digit has been isolated, it is converted to its ASCII equivalent by the following code. if (digit > 9) { // convert digit to letter from A to F temp_str[temp_index++] = digit + 55; } else { // convert digit to ASCII from 0 to 9 temp_str[temp_index++] = digit + 48; } If the digit has a value greater than nine, 55 decimal is added to it so that it is changed to the value that represents the letters A to F in the ASCII table. Possible values greater than nine are 10, 11, 12, 13, 14 and 15. If the value of the digit was 10 and 55 is added to it, it becomes 65 decimal which has a value of ‘A’ in the ASCII table. If the digit has a value less than or equal to nine, 48 decimal is added to it which is the same as adding 0x30. This converts the number to the ASCII values from ‘0’ to ‘9’ as the Int2DecStr() function did. After a single digit from the number is converted, which is the least significant hexadecimal digit, the number is right-shifted by four so that the hexadecimal number to its left becomes the new least significant hexadecimal digit as shown in the line of code below. num >>= 4; The conversion process repeats converting each digit of the number from LSD to MSD and saving the result in the temporary string which is then reversed to get the correct number in ASCII. ● 313 C Programming with Arduino 17.3 • Voltmeter The voltmeter program uses ADC channel 0 as a voltmeter that can measure voltage from 0V to 5V and display this measured voltage in the terminal window in the same way that the example program from Chapter 13, section 13.2 on page 256 did, but this time the program filters the ADC value by averaging it, and allows the sample rate to be changed. An LED is used to indicate when the voltage measured is near to the maximum value that can be measured. This project reuses code from previous code examples found in this book by breaking the code up into functions which are put into their own files. The project consists of eight source code files, namely main.c, adc.c, adc.h, global.h, timer_tick.c, timer_tick.h, uart.c and uart.h. Files global.h, uart.c and uart.h are reused again to add serial port UART functionality to the project for printing the calculated voltage to the terminal window. The timer_tick files add the 1ms timer with ISR to the project. ADC functions are found in the adc.c file with adc.h header file. 17.3.1 • Voltmeter Project Features The voltmeter project has the following features: • Filters the ADC value by averaging several ADC values • Number of ADC values to average can be changed • Rate at which ADC values are read can be changed • One millisecond timer interrupt is used to generate the ADC sampling rate • An LED is switched on when the measured voltage is 4.8V or higher 17.3.2 • Voltmeter Project Code Create the voltmeter project without the use of the template files. Add the three UART driver files to the project and create the new source files listed below. Voltage is measured on the Arduino A0 pin and an LED and series resistor are connected to port pin PC5 to indicate high voltage. Project name: voltmeter Project location: CH17\voltmeter main.c #include #include #include #include #include #include #include <avr/io.h> <avr/interrupt.h> <util/atomic.h> <stdio.h> "uart.h" "adc.h" "timer_tick.h" int main(void) { uint16_t adc_val; float adc_voltage; char out_str[30] = {0}; DDRC |= (1 << DDC5); UartInit(); ● 314 // average ADC value // calculated ADC voltage // string sent to terminal // LED on pin PC5 // initialize UART Chapter 17 • C Projects AdcInit(); AdcStartConverstion(); UartTxString("\x1B[2J"); UartTxString("\x1B[?25l"); TimerTickInit(); sei(); } // // // // // // enable ADC start first ADC conversion clear screen hide cursor enable 1ms tick interrupt global interrupt enable while(1) { ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { if (g_buf_full) { // check if ADC buffer is full g_buf_full = 0; // reset buffer flag // calculate average ADC value adc_val = AdcCalcAverageVal(); // calculate ADC voltage adc_voltage = AdcCalcVoltage(adc_val); // print voltage and average ADC value to terminal sprintf(out_str, "%4.2fV, %4u\r", adc_voltage, adc_val); UartTxString(out_str); // check voltage and switch warning LED on if high if (adc_voltage >= 4.8) { PORTC |= (1 << PORTC5); // warning LED on } else { PORTC &= ~(1 << PORTC5); // warning LED off } } } } adc.c #include <avr/io.h> #include "adc.h" volatile uint16_t g_adc_buffer[ADC_BUF_SZ] = {0}; volatile uint16_t g_adc_buf_idx = 0; volatile uint8_t g_buf_full = 0; void AdcInit(void) { ADMUX = (1 << REFS0); // 5V supply used for ADC reference, select ADC channel 0 DIDR0 = (1 << ADC0D); // disable digital input on ADC0 pin // enable ADC, start ADC, ADC clock = 16MHz / 128 = 125kHz ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); } void AdcStartConverstion(void) { ADCSRA |= (1 << ADSC); // start ADC conversion } uint16_t AdcCalcAverageVal(void) { uint32_t average = 0; uint16_t index = 0; for (index = 0; index < ADC_BUF_SZ; index++) { average += g_adc_buffer[index]; } average /= ADC_BUF_SZ; ● 315 C Programming with Arduino } return (uint16_t)average; float AdcCalcVoltage(uint16_t adc_val) { return (5.0 * adc_val / 1024.0); // calculate the ADC voltage } adc.h #ifndef ADC_H_ #define ADC_H_ // size of ADC buffer array #define ADC_BUF_SZ 20 extern volatile uint16_t g_adc_buffer[]; extern volatile uint16_t g_adc_buf_idx; extern volatile uint8_t g_buf_full; void AdcInit(void); void AdcStartConverstion(void); uint16_t AdcCalcAverageVal(void); float AdcCalcVoltage(uint16_t adc_val); #endif /* ADC_H_ */ timer_tick.c #include #include #include #include <avr/io.h> <avr/interrupt.h> "timer_tick.h" "adc.h" void TimerTickInit(void) { // set up counter/timer 0 and enable interrupt OCR0A = 249; // count value for 1ms period TCCR0A = (1 << WGM01); // CTC mode TCCR0B = (1 << CS01) | (1 << CS00); // divide by 64 prescaler TIMSK0 = (1 << OCIE0A); // enable interrupt } // 1ms tick interrupt ISR(TIMER0_COMPA_vect) { static uint8_t time_ms = 0; time_ms++; if (time_ms >= SAMPLE_RATE_MS) { time_ms = 0; // check if ADC conversion is complete if (ADCSRA & (1 << ADIF)) { g_adc_buffer[g_adc_buf_idx++] = ADC; // get the ADC value // keep buffer index within buffer if (g_adc_buf_idx >= ADC_BUF_SZ) { g_adc_buf_idx = 0; g_buf_full = 1; // flag full buffer } ● 316 Chapter 17 • C Projects } } } // clear the ADIF flag by writing 1 to it ADCSRA |= (1 << ADIF); // start the next ADC conversion AdcStartConverstion(); timer_tick.h #ifndef TIMER_TICK_H_ #define TIMER_TICK_H_ #define SAMPLE_RATE_MS 10 void TimerTickInit(void); #endif /* TIMER_TICK_H_ */ uart.c, uart.h and global.h Copy these serial port driver files and add them to the project. Example Program Output 3.33V, 682 Example Program Output when printf Linker Flags not Set ?V, 0 17.3.3 • Enabling the printf Family of Functions to Work Certain linker flags, shown below, must be passed to the linker in order for the printf functions to work such as the sprintf() function that was used in this project. -Wl,-u,vfprintf -lprintf_flt To add the above linker options to the project, open the settings page for the project by selecting Project → voltmeter Properties... from the top menu in Atmel Studio. In the left column of the page that opens, click Toolchain to display the settings for the AVR GCC toolchain. In the pane that appears on the settings page, click Miscellaneous under the AVR/GNU Linker heading as shown in Figure 17-4 on page 318. Add the above flags to the Other Linker Flags box as shown in the figure. Save the page and then compile the project. 17.3.4 • Principle of Operation of the Code Although the ADC has its own interrupt that will fire when an ADC conversion has taken place, it was decided to use a timer interrupt to service the ADC by reading the currently converted ADC value and starting a new ADC conversion in the ISR. This allows the sample rate of the ADC to be easily changed. In the 1ms timer ISR, a count is checked to see how many milliseconds have passed since the last ADC conversion. This allows the ● 317 C Programming with Arduino conversion rate to be changed by changing the value that is compared to the count value. Each sample or ADC value read in the ISR is placed into a buffer, and when the buffer is full a flag is set to notify the code running in the while(1) loop that the buffer is ready. Code in the while(1) loop adds up all of the ADC values in the buffer and then divides this sum by the size of the buffer so that the average ADC value is calculated. After the average ADC value is calculated, the voltage that is being measured is calculated. Both the calculated voltage and the ADC value are then printed to the terminal window. Figure 17-4 Linker Settings to Enable Printing of Floating Point Numbers 17.3.5 • Examining the Code main.c Code at the beginning of main.c calls functions that initialise the hardware used in the project, namely the output pin that the warning LED is attached to, the USART in UART mode, the ADC and the 1ms timer with interrupt. The first ADC conversion is also started and commands are sent to the terminal window to clear the screen and hide the cursor. All code in the while(1) loop is run inside an atomic block so that the code is not interrupted and so that the ADC buffer is not written to by the ISR while the values in it are being averaged. The global variable g_buf_full is used as a flag that is set by the ISR to indicate when the buffer is full of ADC samples. Only when the buffer is full is this flag reset and the ADC samples in the buffer averaged. The global variable g_buf_full is defined in the adc.c file and made available to main.c by declaring it extern in the adc.h file and including the adc.h file in main.c. When the buffer is full, two functions called AdcCalcAverageVal() and ● 318 Chapter 17 • C Projects AdcCalcVoltage() from adc.c are called to calculate the average ADC value and to calculate the voltage measurement from the average ADC value. After the values have been calculated they are printed to a string using sprintf() which is then sent to the terminal window by calling UartTxString(). Finally the calculated voltage value is checked to see if it is greater than or equal to 4.8V. If the voltage is greater than or equal to 4.8V, the warning LED is switched on to let the user know that the measured voltage is approaching the maximum voltage that can be measured which is 5V. adc.c Three global variables are defined in adc.c that are used to store ADC values and to indicate when the buffer used to store the values is full. The g_adc_buffer array is used as a buffer to store ADC values. The size of the buffer is set by changing the defined value of ADC_BUF_SZ found in adc.h. g_adc_buf_idx is used as an index into the buffer and is used by the timer ISR to index each element of the buffer when writing new ADC values to it. We have already seen g_buf_full used in main() as a flag that indicates when the buffer is full. All three of these variables are declared extern in adc.h to make them available to any C source file that includes adc.h. AdcInit() initialises the ADC, but does not start the ADC conversion. A separate function called AdcStartConversion() can be called to start each ADC conversion. Function AdcCalcAverageVal() calculates the average value of all the ADC samples stored in the buffer and so should only be called when the buffer is full. AdcCalcVoltage() calculates the voltage from the ADC value that is passed to it which should be the average ADC value in this program. It is important to select the correct variable size in an application. Because the buffer that stores the ADC values can be changed by changing ADC_BUF_SZ in adc.h, the sum of all of the voltages from the buffer could get large. In AdcCalcAverageVal() the variable average has been made a 32-bit unsigned integer so that it can hold a large sum. If the buffer size is made bigger than 64 elements, and all of the values in the buffer were the maximum ADC value of 1023, the sum of all of the buffer elements would be a number too big to fit in a 16-bit unsigned integer. In this case, if average was a 16-bit integer it would then overflow and the incorrect voltage would be calculated. Always check that a variable is the correct size for what it is used for. timer_tick.c The TimerTickInit() function found in timer_tick.c initialises timer/counter 0 to generate a 1ms timer interrupt as we have done before. When the timer interrupt fires, the timer interrupt service routine TIMER0_COMPA_vect is run. The timer ISR is run every millisecond, but the ADC is only read, and a new ADC conversion started, every 10ms as set by SAMPLE_RATE_MS defined in timer_tick.h. This is made possible by incrementing static variable time_ms every millisecond and comparing it with SAMPLE_RATE_MS. Every 10ms code in the ISR checks if an ADC conversion is complete, and if it is, copies the ADC value read from the ADC to the g_adc_buffer array. When the buffer array is full, the g_buf_full flag is set to flag the main program that the buffer is full and ready to be averaged. AdcStartConverstion() is then called by the ISR to start the next ADC conversion. 17.3.6 • Code Timing In embedded programming it is most often important to be aware of the timing of a program and the timing of hardware devices used in a program. The voltmeter program is ● 319 C Programming with Arduino a good example for explaining timing of hardware and code. The ADC is set up to use a prescaler value of 128 which divides the main 16MHz clock by 128 supplying the ADC with a 125kHz clock. This sets the ADC clock within the 50kHz to 200kHz range that is necessary to get maximum ADC resolution. According to the AVR datasheet, a single ADC conversion takes 13 ADC clock cycles. The period of the 125kHz clock is 8µs, so a single ADC conversion will take 104µs (8µs × 13). A single ADC conversion will therefore be finished a long time before our 10ms sample rate setting. Using the timer interrupt effectively allows us to reduce the sample rate of the ADC. The actual update of the display or terminal window will take place after the time it takes to fill the buffer plus the time it takes to calculate the average ADC value and voltage and then convert it to a string. With a buffer size of 20 and a sample rate of 10ms, the average ADC value is calculated every 200ms or five times a second. It takes approximately 3ms to run the code inside the body of the if statement in the while(1) loop of main(). Most of this time is used to send the voltage/ADC string to the terminal window via the UART. This time value was measured by setting an AVR port pin at the beginning of the code and clearing the pin at the end of the code and then using an oscilloscope to measure the width of the pulse on the pin. During this time period the timer interrupt is disabled, so no interrupts occur which is not a problem as we do not need to keep receiving interrupts in this application. When the interrupts are enabled again the next set of conversions will take place as usual. 17.4 • Stopwatch The stopwatch program uses timer/counter 1 on the microcontroller to implement a stopwatch that counts up in 10ths of a second. A single push-button switch is used to start and stop the timer. A second push-button switch is used to clear the stopwatch time which is displayed in the terminal window. Timer/counter 1 is set up to generate an interrupt every tenth of a second or 100ms which is the timebase that the stopwatch uses to count from. 17.4.1 • Stopwatch Project Hardware and Code Create the project without using the template file and add the UART driver files as done in the previous projects. Create two new files called timer1.c and timer1.h in the project and add the code listed below to these files. Push-button switches are connected to port C pins PC4 and PC5. An optional LED is connected to port C pin PC1 which is toggled by the 100ms timer interrupt, so the circuit from appendix C can be used, although the extra three LEDs on the port will not be needed. Project name: stopwatch Project location: CH17\stopwatch main.c #include #include #include #include #include #include #include #include ● 320 <avr/io.h> <util/atomic.h> <stdlib.h> <string.h> "uart.h" "timer1.h" "global.h" <util/delay.h> Chapter 17 • C Projects #define BTN_START_STOP (1 << 4) #define BTN_CLEAR (1 << 5) void BuildTimeString(char str[], uint32_t seconds, uint8_t tenths); int main(void) { uint8_t timer_stopped = 1; uint8_t prev_time_s_10ths = 0; char time_string[15] = {0}; UartInit(); UartTxString("\x1B[2J"); UartTxString("\x1B[?25l"); Timer1Init(); } // clear screen // hide cursor while (1) { if ((PINC & BTN_START_STOP) && timer_stopped) { timer_stopped = 0; Timer1Start(); _delay_ms(40); // debounce closing switch while (PINC & BTN_START_STOP); // start timer on open switch _delay_ms(40); // debounce opening switch } else if ((PINC & BTN_START_STOP) && !timer_stopped) { timer_stopped = 1; Timer1Stop(); _delay_ms(40); // debounce closing switch while (PINC & BTN_START_STOP); // wait for switch to open _delay_ms(40); // debounce opening switch } if (PINC & BTN_CLEAR) { if (timer_stopped) { prev_time_s_10ths = 0; g_time_s_10ths = 0; g_time_s = 0; UartTxString("\x1B[2J"); // clear screen Timer1Clear(); } } if (g_time_s_10ths != prev_time_s_10ths) { prev_time_s_10ths = g_time_s_10ths; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { BuildTimeString(time_string, g_time_s, prev_time_s_10ths); } UartTxString(time_string); } } void BuildTimeString(char str[], uint32_t seconds, uint8_t tenths) { int string_len; } ultoa(seconds, str, 10); string_len = strlen(str); str[string_len++] = '.'; str[string_len++] = '0' + tenths; str[string_len++] = '\r'; str[string_len] = 0; // put seconds into string // insert decimal point // insert tenths of seconds // terminate string; ● 321 C Programming with Arduino timer1.c #include <avr/io.h> #include <avr/interrupt.h> volatile uint8_t g_time_s_10ths = 0; volatile uint32_t g_time_s = 0; void Timer1Init(void) { OCR1A = 24999; TCCR1B = (1 << WGM12); TIMSK1 = (1 << OCIE1A); DDRC |= (1 << 1); } // // // // count for 100ms period CTC mode, enable compare A interrupt LED on PC1 void Timer1Start(void) { // selecting the clock source starts the timer TCCR1B |= (1 << CS11) | (1 << CS10); // divide by 64 prescaler sei(); // global interrupt enable } void Timer1Stop(void) { cli(); // global interrupt disable // select no clock source to stop timer TCCR1B &= ~((1 << CS12) | (1 << CS11) | (1 << CS10)); } void Timer1Clear(void) { TCNT1 = 0; // clear the count value PORTC &= ~(1 << 1); // switch LED off } ISR(TIMER1_COMPA_vect) { g_time_s_10ths++; if (g_time_s_10ths >= 10) { g_time_s_10ths = 0; g_time_s++; } PORTC ^= (1 << 1); } // count tenths of a second // count seconds // toggle LED timer1.h #ifndef TIMER1_H_ #define TIMER1_H_ extern volatile uint8_t g_time_s_10ths; extern volatile uint32_t g_time_s; void void void void Timer1Init(void); Timer1Start(void); Timer1Stop(void); Timer1Clear(void); #endif /* TIMER1_H_ */ ● 322 Chapter 17 • C Projects uart.c, uart.h and global.h Copy these serial port driver files and add them to the project. Example Program Output 24.6 17.4.2 • Stopwatch Project Design The push-button switch connected to port C pin PC4 is used to start and stop the timer. When the switch is closed, the timer will immediately start timing. When the switch is closed a second time the timer will stop, freezing the time period in the terminal window. After the stopwatch has been stopped, the user can either start it again, in which case the timer will carry on timing from where it was stopped, or the user can press the second push-button switch on PC5 to clear the display and reset the timer. An optional LED is attached to port C pin PC1 and is toggled in the 100ms interrupt service routine of the timer to give a visual indication that the stopwatch is busy timing. The time taken to convert the stopwatch time to a string and send it to the terminal window is much less than the 100ms timer tick, so the code will always update the display every 100ms without skipping any updates. A function in main.c is used to convert the timer seconds and 10ths of a second values to a string to send to the terminal window, so the project does not use printf() or sprintf() which keeps the memory usage to a minimum. 17.4.3 • Stopwatch Project Code Explanation Defining button names Button names for the two push-button switches, which are the bits in the PINC register for the pins that the switches are connected to, are defined in main.c, giving them meaningful names as shown below. #define BTN_START_STOP (1 << 4) #define BTN_CLEAR (1 << 5) In this code the left-shift operator is used in conjunction with the bit position of the port pin that the button is attached to, which is perhaps easier than trying to remember the name of the bit in the port register. The above code is equivalent to using the predefined bit names PINC4 instead of 4 and PINC5 instead of 5, as they are used to check the switch statuses in the PINC register. Stopwatch variables The following variables are defined in main.c. uint8_t timer_stopped = 1; uint8_t prev_time_s_10ths = 0; char time_string[15] = {0}; The timer_stopped variable is used as a flag to store the current state of the stopwatch. When this variable holds the value 1, or in other words evaluates to true, it indicates that the timer is stopped. When it holds a value of 0, or evaluates to false, it indicates that the ● 323 C Programming with Arduino timer has started. A copy of the 10ths of seconds, which is incremented by the timer ISR, is kept in prev_time_s_10ths and used to test when the ISR has incremented the global variable that holds the 10ths of a second. When the current stopwatch time is converted to a string for sending to the terminal window, it is stored in the time_string array. The following global variables are defined in timer1.c. volatile uint8_t g_time_s_10ths = 0; volatile uint32_t g_time_s = 0; Each 10th of a second is stored in g_time_s_10ths and incremented by the timer ISR. This variable will increment from 0 to 9, and when it reaches 10 is set back to 0 and the variable that holds the seconds is incremented. Seconds are stored in g_time_s and incremented every ten 10ths of a second. Program flow At the beginning of main(), the UART is initialised, the terminal window cleared and the cursor hidden. Timer 1 is initialised by calling the Timer1Init() function from timer1.c which sets up the timer to time 100ms intervals and enables the timer interrupt but does not start the timer. It also sets up port pin PC1 as an output for driving the LED. In the while(1) loop the code checks to see if either of the two push-button switches is closed and if the 10ths of a second value has changed. When the 10ths of a second value does change, the stopwatch value is sent to the terminal window. Timer 1 ISR The timer 1 ISR found in timer1.c increments the 10th of a second variable every time that the interrupt fires, which occurs every 100ms after the timer has been started and the global interrupt enabled. Every time that the 10th of a second variable reaches a value of 10, it is reset to 0 and the variable that holds the seconds is incremented by 1. Therefore once the timer is started, the current 10th of a second value can be found in g_time_s_10ths and the current seconds value can be found in g_time_s. The LED on port pin PC1 is also toggled by the timer 1 ISR. Starting the timer When code in the while(1) loop starts running, the timer has been initialised, but not started. If the user presses the push-button switch connected to pin PC4, the if statement shown below evaluates to true and the timer is started by calling Timer1Start(). if ((PINC & BTN_START_STOP) && timer_stopped) { timer_stopped = 0; Timer1Start(); _delay_ms(40); // debounce closing switch while (PINC & BTN_START_STOP); // start timer on open switch _delay_ms(40); // debounce opening switch } Timer1Start() starts the timer by selecting the clock source from the timer prescaler and also sets the global interrupt flag by calling sei() which enables the timer interrupt. The above code also updates the timer_stopped flag to indicate that the timer has started, and debounces the push-button switch. If the start/stop push-button switch on PC4 is held in by the user, the stopwatch will carry on timing because the main code will be interrupted and the timer ISR run, but the time will not be displayed in the terminal window until the user releases the switch. The reason that the time is not displayed in the terminal window when the switch is held closed is because the code that updates the time in the terminal window is in the main program loop which is held up by the closed switch. ● 324 Chapter 17 • C Projects This is not seen as a problem because normal operation of the stopwatch is to push and immediately release the start/stop button and a user who holds the button in will see that the display is not updating and should release the button. Stopping the timer The following code will only run if the timer is running and the start/stop push-button switch is closed. else if ((PINC & BTN_START_STOP) && timer_stopped = 1; Timer1Stop(); _delay_ms(40); while (PINC & BTN_START_STOP); _delay_ms(40); } !timer_stopped) { // debounce closing switch // wait for switch to open // debounce opening switch This code updates the timer_stopped flag to indicate that the timer is now stopped. The timer is then stopped by calling Timer1Stop() which disables the global interrupt and selects no clock source to stop the timer. The timer is not cleared so that if the timer is started again it will start timing from where it left off when it was stopped. Again the push-button switch is debounced so that the program does not receive multiple open and closed switch states and start and stop the stopwatch accordingly. Updating the timer display The following code sends the current stopwatch time to the terminal window only when the time has changed. if (g_time_s_10ths != prev_time_s_10ths) { prev_time_s_10ths = g_time_s_10ths; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { BuildTimeString(time_string, g_time_s, prev_time_s_10ths); } UartTxString(time_string); } By comparing a copy of the 10ths of a second value with the current 10ths of a second value, the program can tell when the ISR has updated the 10ths of a second variable. When the time has changed, it is converted into a string by calling the BuildTimeString() function, shown on the next page, and transmitted to the terminal window by calling UartTxString(). BuildTimeString() builds a string that consists of the seconds and 10ths of a second separated by a decimal point and followed by a carriage return character. The ultoa() function is used to convert the seconds part of the time into a string. Because we don’t know how many characters the string consists of, as the seconds are always increasing and the number of digits that the number consists of will increase, strlen() is called to get the string length. The length of the string is needed so that we can append the rest of the characters to the seconds part of the string. After getting the string length and storing it in the string_len variable, this variable is used as an index into the string. It automatically indexes the next character after the seconds in the string because the elements of the string array are numbered from zero and not from one. Array notation combined with string_len used as an index are used to insert the decimal point ‘.’, tenths of a second, the carriage return character ‘\r’ and the string null terminator into the string. Because the 10ths of a second value will always be a value ● 325 C Programming with Arduino from 0 to 9, we can convert it to an ASCII character by adding the character ‘0’ to it which is the same as adding 0x30 to it. void BuildTimeString(char str[], uint32_t seconds, uint8_t tenths) { int string_len; } ultoa(seconds, str, 10); string_len = strlen(str); str[string_len++] = '.'; str[string_len++] = '0' + tenths; str[string_len++] = '\r'; str[string_len] = 0; // put seconds into string // insert decimal point // insert tenths of seconds // terminate string; Clearing the timer When the stopwatch clear push-button switch is closed, the stopwatch time will only be cleared if the stopwatch is stopped as shown in the following code. if (PINC & BTN_CLEAR) { if (timer_stopped) { prev_time_s_10ths = 0; g_time_s_10ths = 0; g_time_s = 0; UartTxString("\x1B[2J"); Timer1Clear(); } } // clear screen When the timer is stopped and the clear button pushed, all of the variables that are used to store the time are cleared to zero. The command that clears the terminal window is sent and the Timer1Clear() function is called. Timer1Clear() clears the actual count value in the timer register so that the timer will start counting from zero when the stopwatch is started again. The function also switches the LED off. Final program notes A program can be written as simply as possible or made more complex by adding more features to it. The stopwatch program can have many more features added to it, for example it could be modified to display hours, minutes and seconds instead of just seconds and 10ths of a second. It could even be modified to display days as well, if needed. After writing a program, it may become obvious that the program can be better broken up into functions, or simplified, for example in the stopwatch program code used to debounce the switch is written twice – once for starting the stopwatch and once for stopping the stopwatch. This code could be put into a function and called twice. Always remember to comment your code. Many of the examples in this book minimised the use of comments to reduce the print size of the code and because the code is fully explained in the book, but in normal programming code should be fully commented. A mix of commenting styles were also used to demonstrate their use, but it is best to stick to one style to make all code consistent. ● 326 Chapter 17 • C Projects 17.5 • Where to from here? I hope that you have enjoyed using this book to learn the C programming language for embedded systems. Although it would have been nice to have programmed more hardware devices, limited space in this book as well as an attempt to make the book accessible to everyone by not requiring special hardware devices prevented this. I will leave you to explore other hardware devices on your own. The key to becoming a proficient C programmer is to write programs as well as to read programs from other authors. Use the Internet to obtain source code that you can read and learn from. Write lots of your own programs and never be afraid to experiment. Be sure to look at Appendix D on page 337 that contains useful references to do with C programming and AVR microcontrollers. Application notes from Atmel for AVR microcontrollers are often a good source for learning how various microcontroller peripheral devices work and often contains C code examples. Good luck! I hope that you spend many enjoyable hours programming embedded systems in C! ● 327 C Programming with Arduino ● 328 Appendix A • The ASCII Table Appendix A • The ASCII Table On Windows computers characters from the extended part of the ASCII table will often not print correctly because of the font selected by the program using the characters. Some terminal programs have a default font that does not print the graphical characters from the ASCII table. This can usually be remedied by selecting the Terminal font if the program being used allows the font to be changed. Table A-1 ASCII characters 0 - 127 DEC HEX CHAR DEC HEX 0 0 NULL 32 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 2 3 4 5 6 7 8 9 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 CHAR DEC space ! " # $ % & ' ( ) * + , . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ? HEX CHAR DEC HEX CHAR 64 40 @ 96 60 ` 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _ 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~ delete • 0 to 31 are unprintable control codes used for communications • 32 to 126 are printable ASCII characters and character 127 is unprintable ● 329 C Programming with Arduino Table A-2 Extended ASCII characters DEC HEX CHAR DEC HEX CHAR DEC HEX CHAR DEC HEX CHAR 128 80 Ç 160 A0 á 192 C0 └ 224 E0 α 129 81 ü 161 A1 í 193 C1 ┴ 225 E1 ß 130 82 é 162 A2 ó 194 C2 ┬ 226 E2 Γ 131 83 â 163 A3 ú 195 C3 ├ 227 E3 π 132 84 ä 164 A4 ñ 196 C4 ─ 228 E4 Σ 133 85 à 165 A5 Ñ 197 C5 ┼ 229 E5 σ 134 86 å 166 A6 ª 198 C6 ╞ 230 E6 µ 135 87 ç 167 A7 º 199 C7 ╟ 231 E7 τ 136 88 ê 168 A8 ¿ 200 C8 ╚ 232 E8 Φ 137 89 ë 169 A9 ⌐ 201 C9 ╔ 233 E9 Θ 138 8A è 170 AA ¬ 202 CA ╩ 234 EA Ω 139 8B ï 171 AB ½ 203 CB ╦ 235 EB δ 140 8C î 172 AC ¼ 204 CC ╠ 236 EC ∞ 141 8D ì 173 AD ¡ 205 CD ═ 237 ED φ 142 8E Ä 174 AE « 206 CE ╬ 238 EE ε 143 8F Å 175 AF » 207 CF ╧ 239 EF ∩ 144 90 É 176 B0 ░ 208 D0 ╨ 240 F0 ≡ 145 91 æ 177 B1 ▒ 209 D1 ╤ 241 F1 ± 146 92 Æ 178 B2 ▓ 210 D2 ╥ 242 F2 ≥ 147 93 ô 179 B3 │ 211 D3 ╙ 243 F3 ≤ 148 94 ö 180 B4 ┤ 212 D4 ╘ 244 F4 ⌠ 149 95 ò 181 B5 ╡ 213 D5 ╒ 245 F5 ⌡ 150 96 û 182 B6 ╢ 214 D6 ╓ 246 F6 ÷ 151 97 ù 183 B7 ╖ 215 D7 ╫ 247 F7 ≈ 152 98 ÿ 184 B8 ╕ 216 D8 ╪ 248 F8 ° 153 99 Ö 185 B9 ╣ 217 D9 ┘ 249 F9 ∙ 154 9A Ü 186 BA ║ 218 DA ┌ 250 FA · 155 9B ¢ 187 BB ╗ 219 DB █ 251 FB √ 156 9C £ 188 BC ╝ 220 DC ▄ 252 FC ⁿ 157 9D ¥ 189 BD ╜ 221 DD ▌ 253 FD ² 158 9E ₧ 190 BE ╛ 222 DE ▐ 254 FE ■ 159 9F ƒ 191 BF ┐ 223 DF ▀ 255 FF ● 330 Appendix B • Arduino – AVR Port Pin Mapping Appendix B • Arduino – AVR Port Pin Mapping Arduino pin numbers to AVR microcontroller port pin mapping can be found below for the Arduino Uno and Arduino MEGA 2560. B.1 • Arduino Uno Digital Pins Pin Alternate Function Arduino AVR Arduino AVR 0 PD0 RX PCINT16/RXD 1 PD1 TX PCINT17/TXD 2 PD2 3 PD3 4 PD4 5 PD5 PWM PCINT21/OC0B/T1 6 PD6 PWM PCINT22/OC0A/AIN0 7 PD7 8 PB0 9 PB1 PWM OC1A/PCINT1 10 PB2 PWM SS/OC1B/PCINT2 11 PB3 PWM MOSI/OC2A/PCINT3 12 PB4 13 PB5 PCINT18/INT0 PWM PCINT19/OC2B/INT1 PCINT20/XCK/T0 PCINT23/AIN1 PCINT0/CLKO/ICP1 MISO/PCINT4 L LED SCK/PCINT5 Analogue Pins Pin Alternate Function Arduino AVR Arduino AVR A0 PC0 ADC0/PCINT8 A1 PC1 ADC1/PCINT9 A2 PC2 ADC2/PCINT10 A3 PC3 ADC3/PCINT11 A4 PC4 ADC4/SDA/PCINT12 A5 PC5 ADC5/SCL/PCINT13 ● 331 C Programming with Arduino B.2 • Arduino MEGA Digital Pins Pin Alternate Function Arduino AVR Arduino AVR 0 PE0 RX0 RXD0/PCINT8 1 PE1 TX0 TXD0 2 PE4 PWM OC3B/INT4 3 PE5 PWM OC3C/INT5 4 PG5 PWM OC0B 5 PE3 PWM OC3A/AIN1 6 PH3 PWM OC4A 7 PH4 PWM OC4B 8 PH5 PWM OC4C 9 PH6 PWM OC2B 10 PB4 PWM OC2A/PCINT4 11 PB5 PWM OC1A/PCINT5 12 PB6 PWM OC1B/PCINT6 13 PB7 L LED/PWM OC0A/OC1C/PCINT7 14 PJ1 TX3 TXD3/PCINT10 15 PJ0 RX3 RXD3/PCINT9 16 PH1 TX2 TXD2 17 PH0 RX2 RXD2 18 PD3 TX1 TXD1/INT3 19 PD2 RX1 RXD1/INT2 20 PD1 SDA SDA/INT1 21 PD0 SCL SCL/INT0 22 PA0 AD0 23 PA1 AD1 24 PA2 AD2 25 PA3 AD3 26 PA4 AD4 27 PA5 AD5 28 PA6 AD6 29 PA7 AD7 30 PC7 A15 31 PC6 A14 32 PC5 A13 33 PC4 A12 ● 332 Appendix B • Arduino – AVR Port Pin Mapping Digital Pins Pin Alternate Function Arduino AVR 34 PC3 Arduino AVR A11 35 PC2 A10 36 PC1 A9 37 PC0 A8 38 PD7 T0 39 PG2 ALE 40 PG1 RD 41 PG0 WR 42 PL7 43 PL6 44 PL5 OC5C 45 PL4 OC5B 46 PL3 OC5A 47 PL2 T5 48 PL1 ICP5 49 PL0 ICP4 50 PB3 MISO/PCINT3 51 PB2 MOSI/PCINT2 52 PB1 SCK/PCINT1 53 PB0 SS/PCINT0 Analogue Pins Pin Alternate Function Arduino AVR Arduino AVR A0 PF0 ADC0 A1 PF1 ADC1 A2 PF2 ADC2 A3 PF3 ADC3 A4 PF4 ADC4/TCK A5 PF5 ADC5/TMS A6 PF6 ADC6/TDO A7 PF7 ADC7/TDI A8 PK0 ADC8/PCINT16 A9 PK1 ADC9/PCINT17 A10 PK2 ADC10/PCINT18 ● 333 C Programming with Arduino Analogue Pins Pin Alternate Function Arduino AVR A11 PK3 ADC11/PCINT19 A12 PK4 ADC12/PCINT20 A13 PK5 ADC13/PCINT21 A14 PK6 ADC14/PCINT22 A15 PK7 ADC15/PCINT23 ● 334 Arduino AVR Appendix C • Standard Arduino I/O Circuit Appendix C • Standard Arduino I/O Circuit The following circuit for the Arduino Uno and MEGA is used by many of the code examples in this book – LEDs on pins PC0 to PC3, switches on PC4 and PC5. Don’t forget to connect the common GND to one of the Arduino GND pins. C.1 • Arduino Uno Figure C-1 Circuit for the Arduino Uno ● 335 C Programming with Arduino C.2 • Arduino MEGA Figure C-2 Circuit for the Arduino MEGA ● 336 Appendix D • References and Resources Appendix D • References and Resources The following references and resources are useful when programming AVR microcontrollers, especially the AVR datasheets and AVR GCC C library reference. D.1 • Book Website and Files The accompanying website for this book can be found at wspublishing.net/avr-c All the code examples found in this book can be downloaded from www.elektor.com D.2 • Datasheets and Application Notes Datasheets for AVR microcontrollers can be found on the Atmel website at: www.atmel.com ATmega328P datasheet for Arduino Uno: www.atmel.com/devices/ATMEGA328P.aspx ATmega2560 datasheet for Arduino MEGA 2560: www.atmel.com/devices/ATMEGA2560.aspx Click the Documents tab on either of the above two pages to see additional documentation and application notes for these microcontrollers. D.3 • AVR Libc Reference The AVR Libc C library documentation can be found at www.nongnu.org/avr-libc/ where the Libc manual can be downloaded in HTML or PDF format. The manual can also be viewed online. User manual online: www.nongnu.org/avr-libc/user-manual/pages.html Library reference online: www.nongnu.org/avr-libc/user-manual/modules.html D.4 • AVR Forum The AVR Freaks forum contains many AVR related topics and answers to AVR and AVR C programming questions. The forum can be found at www.avrfreaks.net D.5 • Additional Resources AVR tutorials can be found on the AVR Freaks website at: www.avrfreaks.net/forums/tutorials Several useful AVR related articles can be found on Dean Camera’s website at: www.fourwalledcubicle.com/AVRArticles.php ● 337 C Programming with Arduino ● 338 Index Index Symbols |= 238, 242 || 239, 242 ~ 229, 242 ^ 229, 242 8-bit AVR double 199 ^= 238, 242 8-bit AVR long double 199 - 51, 242 %c 141 -- 242 #define 99, 110 -= 238, 242 %d or %i 141 -> 242 #include 102, 110 : 242 %u 141 ! 239, 242 %x 141 != 62, 242 %X 141 ? 242 . 242 () 242 [] 242 * 51, 242 *= 238, 242 / 51, 242 /= 238, 242 & 229, 242 && 239, 242 &= 238, 242 A ADC 145, 256 Adding Watches 271 Additional Resources 337 Addresses 133 Algorithm 89 AND Bitwise Operator 167 Application Notes 337 Arduino Board 21 Arduino I/O Circuit 335 % 51, 242 Arduino MEGA 2560 20, 21, 145, 147, 152, 332 %= 238, 242 Arduino MEGA 2560 Circuit 154 + 51, 242 Arduino MEGA AVR 153 ++ 242 Arduino Uno 146, 152, 331 += 238, 242 Arduino Uno AVR 153 < 62, 242 Arduino Uno Circuit 154 << 229, 242 Arithmetic Operators 49 <<= 238, 242 array definition 205 <= 62, 242 Array Element Addresses 214 = 238, 242 Array of Integers 207 == 62, 242 Arrays 205 > 62, 242 ASCII Table 124, 329 >= 62, 242 Assignment Operators 238 >> 229, 242 ATmega328P 21, 34, 142, 145, 149 >>= 238, 242 ATmega2560 34, 142, 147, 150 | 229, 242 ● 339 C Programming with Arduino ATmega2560 Ports 146 continue 178, 179, 296 Atmel Studio 17, 25 default 192, 296 AVR Architecture 17 do 178, 180, 181, 202, 296 AVR C Library 306 double 199, 200, 296 AVR Dragon 32, 267 else 63, 64, 67, 68, 296 AVR Forum 337 enum 292, 296 AVRISP 32 extern 294, 296, 299 AVR Libc Reference 337 float 48, 285, 289, 295, 296 AVR Memory Map 169 for 296 AVR microcontroller 17, 41, 163, 301 goto 295, 296, 299 AVR Pins and Ports 145 if 63, 293, 295, 296, 320 AVR Port Pin Mapping 331 int 92, 98, 124, 138, 296 B long 138, 141, 142, 296 Baud Rate 186 Binary Basics 111 binary digit 111 binary numbers 111, 113 Bitwise NOT Operator 234 Bitwise XOR Operator 235 Boolean Bitwise Operators 229 bounce 223 braces {} 40 break Statement 179 Button Message 226 Button Wait 227 bytes 111, 130 register 133, 137, 231, 243, 296 return 95, 96, 296 short 139, 141, 296 signed 139, 141, 296 sizeof 139, 140, 208, 226, 296 static 294, 296, 319 struct 286, 288, 296 switch 293, 296 typedef 293, 294, 296, 299 union 288, 296, 299 unsigned 293, 294, 296, 298 void 36, 91, 296 volatile 98, 296 while 178, 180, 202, 296 C Class Specifiers 294 C99 Variable Types 298 clock cycle 251 Casts 200 C Macros 246 char 58 Code Listings 30 Characters 35 Code Timing 319 Character Variables 48 Commenting Programs 79 C Keywords 296 Communication Parameters 186 auto 294, 296 Comparative Operators 61, 72 break 178, 179, 192, 202, 296 Compiler Optimisation 269 case 192, 296 Compile-time 247 char 58, 231, 285, 296, 298 Compiling 52 const 295, 296, 299 Conditional Operator 193 ● 340 Index continue Statement 179 Flash memory 133, 170 Continuous Loop 298 float 58, 199 control bus 130 floating point 103 convert from binary to hexadecimal 119 Floating Point Data Types 199 C Projects 301 Floating Point Variables 48 D for Loop 181 data bus 130 Data Direction Register 148 Data memory 170 Format Specifiers 173 Functions 36, 194 Functions Calling 103 Datasheets 337 G Debugging 22, 267 GCC compiler 218 debugWIRE 267, 269 GCC toolchain 89, 107 Decimal Numbers 112 Global Variables 197 Decisions 62 GNU C compiler 138 Decrement Operators 159 goto Statement 295 Design Notes 301 H double 199 double forward slash 81 do – while Loop 180 Driver Code 302 DWEN 269 E ELF format 53 else 63 else - if 67 Embedded Systems 18 Enabling ISP 277 Enumerated Type 291 Errors 53 Escape Sequences 176 Extended ASCII characters 330 Handling Interrupts 279 Hardware Registers 271 Header Files 106, 245, 305 Hexadecimal Numbers 116, 121 High 70 I ICSP header 147, 267 IDE 17 if 63, 185 Increment Operators 159 Input 41, 46 Input Pins 163, 166 int 58, 138 Integer Data Types 138 Integer Format Specifiers 141 F Interrupt Driven Hardware 255 False 61, 70 Interrupts 279 Field Width 174 Invalid variable names 58 Field Width Specifiers 51 I/O 163 Files 337 I/O Ports 262 Flashing LED 97, 151 ISP 269 ● 341 C Programming with Arduino ISRs 281 non-zero value 72 K O keywords 36, 72 Operator Precedence 241 L Output 41 least significant bit (LSB) 111 Output pins 166 least significant digit (LSD) 111 P Left Shift Operators 236 parentheses () 40 Library Files 107 peripheral devices 130 Library String Functions 213 Peripherals 133 Link Errors 56 pointer notation 217 Linking 52 Pointers 134, 194 Local Variables 196 Polled Timer 252 Logical Operators 239 Port Pin 163 logic state 165 Post-decrement 159 long 138 Post-increment 159 long Data Type 98 Pre-decrement 159 long double 199 Pre-defined Bit Names 243 Loops 178 Pre-increment 159 Low 70 Preprocessor Directives 37, 296 M printf() 36 main() 36 mathematical constant 101 Program Statements 37 Pull-up Resistors 263 Memory 129, 133 R Memory Chip 130, 132 RAM 129 Memory Maps 163 Receiving a Byte 187 Memory Sizes 142 References 337 Microcontroller 18, 130 Register Functionality 262 Microprocessors 18, 133 Registers 186 most significant bit (MSB) 111 Resources 337 most significant digit (MSD) 111 Right Shift Operators 236 Multidimensional Arrays 221 RISC 17 Multiple Source Files 103 ROM 129 N Run-time 247 Nested Loops 188 S Nesting Decisions 189 scanf() 44 New Project 27 Serial Port 182, 184 nibble 111 Serial Port Driver 301 ● 342 Index Setting Breakpoints 274 UART 145, 147, 301 Shift Operators 229 UartInit() 36 short 138 Unions 288 signed char 138 unsigned char or char 138 significant bit 111 unsigned int 138 Single Stepping 271 unsigned long 138 Solution Explorer 28, 105 unsigned short 138 specific address 133 USART 183, 301 SRAM 130, 133, 170 USB 147, 183, 301 Static Variables 198 V Stopping Debugging 274 Stopwatch 301, 320 String Conversion Functions 309 Strings 35, 209 Structures 285, 287 Style 81 subroutines 36 switch Statement 190 T Tab Settings 81 Tab Width 81 Template Files 26 Tera Term 211 Terminal Control Codes 101 Terminal Emulator 26 Valid variable names 58 Variable Compare 70 Variables 41, 43 Viewing Pin State 275 Viewing Variables 273 Voltmeter 314 W Warnings 53 Watchdog Timer 260 Websites 337 while(1) 76, 98, 151 while Loop 73 Whitespace Characters 37 word 111 Tick Interrupt 281 Time Delay 252 Timer Counter 251 Timer Function 255 Timer Interrupt 279 Toolchain 17, 52 Transmitting a Byte 187 True 61, 70 Truth Tables 230 two-dimensional array 222 typedef Declarator 293 Type Qualifiers 295 U ● 343 Warwick A. Smith Warwick A. Smith lives in South Africa and works as an Electronics Engineer and Embedded System Programmer. He is a bestselling author of the books C Programming for Embedded Microcontrollers, ARM Microcontroller Interfacing and Open Source Electronics on Linux. ISBN 978-1-907920-46-2 Arduino is the hardware platform used to teach the C programming language as Arduino boards are available worldwide and contain the popular AVR microcontrollers from Atmel. Atmel Studio is used as the development environment for writing C programs for AVR microcontrollers. It is a full-featured integrated development environment (IDE) that uses the GCC C software tools for AVR microcontrollers and is free to download. • Start learning to program from the very first chapter • No programming experience is necessary • Learn by doing - type and run the example programs • A fun way to learn the C programming language • Ideal for electronic hobbyists, students and engineers wanting to learn the C programming language in an embedded environment on AVR microcontrollers • Use the free full-featured Atmel Studio IDE software for Windows • Write C programs for 8-bit AVR microcontrollers as found on the Arduino Uno and MEGA boards • Example code runs on Arduino Uno and Arduino MEGA 2560 boards and can be adapted to run on other AVR microcontrollers or boards • Use the AVR Dragon programmer / debugger in conjunction with Atmel Studio to debug C programs DESIGN www.elektor.com Technology is constantly changing. New microcontrollers become available every year. The one thing that has stayed the same is the C programming language used to program these microcontrollers. If you would like to learn this standard language to program microcontrollers, then this book is for you! AVR MICROCONTROLLERS AND ATMEL STUDIO FOR C PROGRAMMING WITH ARDUINO LEARN Elektor International Media BV C PROGRAMMING WITH ARDUINO C PROGRAMMING WITH ARDUINO ● WARWICK A. SMITH AVR MICROCONTROLLERS AND ATMEL STUDIO FOR Warwick A. Smith LEARN DESIGN SHARE SHARE LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● S ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE GN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE ● LEARN ● DESIGN ● SHARE
Puede agregar este documento a su colección de estudio (s)
Iniciar sesión Disponible sólo para usuarios autorizadosPuede agregar este documento a su lista guardada
Iniciar sesión Disponible sólo para usuarios autorizados(Para quejas, use otra forma )